feat(server): support OpenCode 2 alongside OpenCode 1 - #8207
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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 |
|
need to test locally |
There was a problem hiding this comment.
Effect Service Conventions review: 2 findings on OpenCode v2 support (hidden HTTP dependency in the runtime service, and an unmodeled defect for malformed OpenCode config in the adapter). Details inline.
Posted via Macroscope — Effect Service Conventions
| import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess"; | ||
| import { resolveSpawnCommand, SpawnExecutableResolution } from "@t3tools/shared/shell"; | ||
| const encodeUnknownJsonStringExit = Schema.encodeUnknownExit(Schema.fromJsonString(Schema.Unknown)); | ||
| const nodeFetch = globalThis.fetch; |
There was a problem hiding this comment.
nodeFetch captures globalThis.fetch in a module global, so the new /api/health probe in connectToOpenCodeServer (line 736) performs HTTP through a dependency that is invisible in OpenCodeRuntimeLive's requirements and only reachable by patching a global — which is why the globalFetch:off suppression is needed.
Other services in apps/server acquire the client from the environment (const httpClient = yield* HttpClient.HttpClient in preview/PortScanner.ts, provider/providerMaintenance.ts, the provider drivers), and FetchHttpClient.layer is already provided in server.ts. Consider acquiring HttpClient.HttpClient in makeOpenCodeRuntime and issuing the health request through it so the dependency stays typed and layer-testable.
Posted via Macroscope — Effect Service Conventions
| const parsedConfig: unknown = JSON.parse(resolveOpenCodeConfigContent(environment)); | ||
| if (!parsedConfig || typeof parsedConfig !== "object" || Array.isArray(parsedConfig)) { | ||
| throw new Error("OpenCode configuration content must be a JSON object."); |
There was a problem hiding this comment.
A malformed OPENCODE_CONFIG_CONTENT becomes an unmodeled defect: withOpenCodeRuntimePermissions is invoked synchronously inside startSession's Effect.gen (line 1303), so this throw (and a JSON.parse syntax error) dies instead of failing in the typed error channel.
OpenCodeTextGeneration.ts handles the same input with a schema decode plus a typed TextGenerationError. Consider having this helper return an Effect (or a decode Exit/Result) and failing with an existing adapter error such as ProviderAdapterProcessError, so session start reports a structured, catchable failure.
Posted via Macroscope — Effect Service Conventions
There was a problem hiding this comment.
Note
🤖 GPT-5.6 Sol responding on behalf of Theo
The JSON parsing defect does not escape as an unmodeled adapter failure. The surrounding Effect.exit captures it and converts it to the existing process error path.
There is a separate compatibility failure: OpenCode accepts JSONC comments and trailing commas, while this helper uses strict JSON.parse. The later JSONC thread describes that real failure.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 1a873b5. Configure here.
| ), | ||
| ], | ||
| } | ||
| : {}), |
There was a problem hiding this comment.
V2 permissions omitted for empty config
Medium Severity
withOpenCodeRuntimePermissions builds OpenCode 2 permissions rules (including the bash → shell mapping) but only writes that array when the config already has a permissions key. The common empty config ({}) therefore only gets the deprecated singular permission object with v1 bash keys, so supervised modes may never reach OpenCode 2’s real rule engine on locally spawned servers.
Reviewed by Cursor Bugbot for commit 1a873b5. Configure here.
There was a problem hiding this comment.
Note
🤖 GPT-5.6 Sol responding on behalf of Theo
This specific claim is incorrect for both tested v2 betas. OpenCode's config normalization migrates the singular permission map into native rules and maps bash to shell. The empty-config supervised control returned ask.
The real bypass is an agent-level allow rule applied after the global rules. I left a separate blocker on that path.
|
holybotslop |
|
|
||
| return runOpenCodeSdk("health.get", async () => { | ||
| // @effect-diagnostics-next-line globalFetch:off | ||
| const response = await nodeFetch( |
There was a problem hiding this comment.
🟠 High provider/opencodeRuntime.ts:737
A stalled external /api/health request causes connectToOpenCodeServer to ignore input.timeoutMs and remain blocked until the fetch transport eventually times out. Pass an abort signal derived from input.timeoutMs to enforce the requested startup timeout.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/opencodeRuntime.ts around line 737:
A stalled external `/api/health` request causes `connectToOpenCodeServer` to ignore `input.timeoutMs` and remain blocked until the fetch transport eventually times out. Pass an abort signal derived from `input.timeoutMs` to enforce the requested startup timeout.
Evidence trail:
apps/server/src/provider/opencodeRuntime.ts:157-179, 550, 686-711, 726-746 at commit 1a873b5; Node.js fetch/AbortSignal documentation: https://nodejs.org/api/globals.html
| environment: NodeJS.ProcessEnv | undefined, | ||
| runtimeMode: RuntimeMode, | ||
| ): NodeJS.ProcessEnv { | ||
| const parsedConfig: unknown = JSON.parse(resolveOpenCodeConfigContent(environment)); |
There was a problem hiding this comment.
🟠 High Layers/OpenCodeAdapter.ts:379
withOpenCodeRuntimePermissions throws on valid JSONC in OPENCODE_CONFIG_CONTENT, so every locally spawned restricted-mode session with comments or trailing commas fails before it can connect. JSON.parse only accepts strict JSON, while OpenCode parses this environment value as JSONC; use the same JSONC parser for this content before merging the permission rules.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Layers/OpenCodeAdapter.ts around line 379:
`withOpenCodeRuntimePermissions` throws on valid JSONC in `OPENCODE_CONFIG_CONTENT`, so every locally spawned restricted-mode session with comments or trailing commas fails before it can connect. `JSON.parse` only accepts strict JSON, while OpenCode parses this environment value as JSONC; use the same JSONC parser for this content before merging the permission rules.
Evidence trail:
Commit 1a873b5c748: apps/server/src/provider/Layers/OpenCodeAdapter.ts:375-382 (`JSON.parse` in `withOpenCodeRuntimePermissions`); apps/server/src/provider/Layers/OpenCodeAdapter.ts:1301-1305 (called before local server connection for non-full-access modes); apps/server/src/provider/opencodeRuntime.ts:45-53 and 574-597 (config content resolution and child-process propagation). OpenCode documentation: https://opencode.ai/docs/config/ (Format section states JSON and JSONC are supported; Locations section documents `OPENCODE_CONFIG_CONTENT`). OpenCode source: https://github.com/sst/opencode/blob/dev/packages/opencode/src/config/config.ts (`ConfigParse.jsonc` loads `OPENCODE_CONFIG_CONTENT`).
| let latestUser: string | undefined; | ||
|
|
||
| do { | ||
| const page = await client.message.list({ |
There was a problem hiding this comment.
🟡 Medium provider/openCodeV2Client.ts:116
Paginated message.list requests fail with InvalidCursorError once a second page is needed, so listMessages and synchronous session.prompt cannot return complete history or the assistant response. Both requests send order together with cursor; omit order when following a cursor.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/openCodeV2Client.ts around line 116:
Paginated `message.list` requests fail with `InvalidCursorError` once a second page is needed, so `listMessages` and synchronous `session.prompt` cannot return complete history or the assistant response. Both requests send `order` together with `cursor`; omit `order` when following a cursor.
Evidence trail:
t3code @ 1a873b5: apps/server/src/provider/openCodeV2Client.ts:106-150, 1011-1047; apps/server/src/provider/openCodeV2Client.test.ts:335-410; apps/server/package.json:31-32. Official API reference: https://opencode.ai/v2/docs/api/session/v2-message-list. Git commands: `git show 1a873b5 -- apps/server/src/provider/openCodeV2Client.ts apps/server/src/provider/openCodeV2Client.test.ts apps/server/package.json`
There was a problem hiding this comment.
Note
🤖 GPT-5.6 Sol responding on behalf of Theo
The impact is larger than this comment states. The real endpoint returns cursor.next for every nonempty page, including the last page. This loop therefore sends order with cursor after any nonempty first page and fails with InvalidCursorError.
I reproduced this on both tested betas with one exchange. Unlimited readThread and rollbackThread calls fail for every nonempty session, not only after 200 messages.
There was a problem hiding this comment.
Thanks for reproducing and clarifying the broader impact. Would you like me to prepare the pagination fix?
| const serverPassword = | ||
| input.serverPassword ?? | ||
| environment.OPENCODE_PASSWORD ?? | ||
| environment.OPENCODE_SERVER_PASSWORD; | ||
|
|
||
| return runOpenCodeSdk("health.get", async () => { | ||
| // @effect-diagnostics-next-line globalFetch:off | ||
| const response = await nodeFetch( | ||
| new URL("/api/health", serverUrl), | ||
| serverPassword | ||
| ? { | ||
| headers: { | ||
| Authorization: `Basic ${Buffer.from(`opencode:${serverPassword}`, "utf8").toString("base64")}`, | ||
| }, |
There was a problem hiding this comment.
🟠 High provider/opencodeRuntime.ts:730
External v2 connections using a non-default OPENCODE_SERVER_USERNAME send opencode:<password> to /api/health, so valid credentials receive HTTP 401 and the connection is incorrectly reported as an authentication failure. Read the configured username from OPENCODE_SERVER_USERNAME and use it when constructing the Basic-auth header, falling back to opencode when unset.
const serverPassword =
input.serverPassword ??
environment.OPENCODE_PASSWORD ??
environment.OPENCODE_SERVER_PASSWORD;
+ const serverUsername = environment.OPENCODE_SERVER_USERNAME ?? "opencode";
return runOpenCodeSdk("health.get", async () => {
@@
- Authorization: `Basic ${Buffer.from(`opencode:${serverPassword}`, "utf8").toString("base64")}`,
+ Authorization: `Basic ${Buffer.from(`${serverUsername}:${serverPassword}`, "utf8").toString("base64")}`,🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/opencodeRuntime.ts around lines 730-743:
External v2 connections using a non-default `OPENCODE_SERVER_USERNAME` send `opencode:<password>` to `/api/health`, so valid credentials receive HTTP 401 and the connection is incorrectly reported as an authentication failure. Read the configured username from `OPENCODE_SERVER_USERNAME` and use it when constructing the Basic-auth header, falling back to `opencode` when unset.
Evidence trail:
Reviewed commit 1a873b5. Inspect with `git show 1a873b5 -- apps/server/src/provider/opencodeRuntime.ts`. Relevant code: `apps/server/src/provider/opencodeRuntime.ts:726-753`, callers pass the environment at `apps/server/src/provider/Layers/OpenCodeAdapter.ts:1301-1316` and `apps/server/src/provider/Layers/OpenCodeProvider.ts:429-447`. OpenCode documentation: https://opencode.ai/docs/server/. OpenCode auth implementation: https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/server/auth.ts
There was a problem hiding this comment.
Note
🤖 GPT-5.6 Sol responding on behalf of Theo
This fix would break authentication for the tested v2 releases. Their server auth fixes the Basic-auth username to opencode; OPENCODE_SERVER_USERNAME is not accepted on this path. I verified the live health response with generated authentication on both betas.
| } | ||
|
|
||
| function toolOutput(content: ReadonlyArray<ToolContent | ToolContent1>): string { | ||
| return content.map((entry) => (entry.type === "text" ? entry.text : entry.uri)).join("\n"); |
There was a problem hiding this comment.
🟡 Medium provider/openCodeV2Client.ts:1487
toolOutput exposes file and image results as raw uri strings, including data: payloads, so detailFromToolPart shows the URI/base64 content as user-visible activity detail even though the entries are also represented as attachments. Filter out non-text entries here so file results are represented only as attachments.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/openCodeV2Client.ts around line 1487:
`toolOutput` exposes file and image results as raw `uri` strings, including `data:` payloads, so `detailFromToolPart` shows the URI/base64 content as user-visible activity detail even though the entries are also represented as attachments. Filter out non-text entries here so file results are represented only as attachments.
Evidence trail:
Reviewed commit 1a873b5c74813d97e07322bb5ab4dbd6bf8b4871: apps/server/src/provider/openCodeV2Client.ts:1400-1415, 1486-1511; apps/server/src/provider/Layers/OpenCodeAdapter.ts:559-569, 981-1017; apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts:2030-2041
| }; | ||
| } | ||
| const part: ToolPart = { | ||
| id: event.data.id, |
There was a problem hiding this comment.
🟡 Medium provider/openCodeV2Client.ts:616
Reused provider tool-call IDs overwrite earlier entries in state.parts and the adapter's partById, so later updates can attach to the wrong assistant turn and history/UI consumers lose the earlier tool item. Because event.data.id is only session/turn-scoped, generate a distinct ToolPart.id while preserving the provider ID in callID.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/openCodeV2Client.ts around line 616:
Reused provider tool-call IDs overwrite earlier entries in `state.parts` and the adapter's `partById`, so later updates can attach to the wrong assistant turn and history/UI consumers lose the earlier tool item. Because `event.data.id` is only session/turn-scoped, generate a distinct `ToolPart.id` while preserving the provider ID in `callID`.
Evidence trail:
Commit 1a873b5: apps/server/src/provider/openCodeV2Client.ts:615-653, 674-748; apps/server/src/provider/Layers/OpenCodeAdapter.ts:928-1017; apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts:792-880; apps/web/src/components/chat/MessagesTimeline.logic.ts:422-430; apps/web/src/session-logic.test.ts:1293-1322. Git commands: git show 1a873b5 -- apps/server/src/provider/openCodeV2Client.ts; git grep -n "partById\|toolCallId" 1a873b5. Related reference: https://github.com/cloudflare/agents/issues/1992
| if (field.type === "number" || field.type === "integer") { | ||
| const number = Number(first); | ||
| return Number.isFinite(number) ? number : undefined; |
There was a problem hiding this comment.
🟡 Medium provider/openCodeV2Client.ts:1598
toFormValue submits fractional values such as 1.5 for integer fields, so OpenCode can reject the form reply or receive an invalid integer. The shared Number.isFinite check does not enforce integrality; validate Number.isInteger(number) when field.type === "integer".
- if (field.type === "number" || field.type === "integer") {
- const number = Number(first);
- return Number.isFinite(number) ? number : undefined;
+ if (field.type === "number" || field.type === "integer") {
+ const number = Number(first);
+ return Number.isFinite(number) && (field.type !== "integer" || Number.isInteger(number)) ? number : undefined;🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/openCodeV2Client.ts around lines 1598-1600:
`toFormValue` submits fractional values such as `1.5` for `integer` fields, so OpenCode can reject the form reply or receive an invalid integer. The shared `Number.isFinite` check does not enforce integrality; validate `Number.isInteger(number)` when `field.type === "integer"`.
Evidence trail:
Commit 1a873b5: apps/server/src/provider/openCodeV2Client.ts:1095-1113, 1584-1605; apps/server/src/provider/openCodeV2Client.test.ts:723-831. OpenCode form API documentation: https://opencode.ai/v2/docs/api/form/v2-session-form-create. Verification command: git show 1a873b5 -- apps/server/src/provider/openCodeV2Client.ts
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR adds a broad OpenCode 2 runtime and compatibility layer, changing server discovery, authentication, session/event translation, permissions, and background generation across multiple production paths. The scope and unresolved risks around permission enforcement, credentials, reconnects, pagination, and request handling require human review. Not approved because:
Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more. |
|
Sure, there's a bit of bot slop, but I think there are good comments in here too. Let me know when you've tested thoroughly, and if you have a video that you can use to demo it, let me know. Definitely want to get OpenCode v2 support in ASAP. Really appreciate the effort here. |
|
yeah once v2 publicly launches i'll have it ready |
t3dotgg
left a comment
There was a problem hiding this comment.
Note
🤖 GPT-5.6 Sol responding on behalf of Theo
Do not merge this revision yet. I tested commit 1a873b5 against OpenCode 0.0.0-beta-18155 and 0.0.0-beta-18387. I reproduced 12 failures. Three are blockers. Seven findings had no matching thread, so I added them inline. Existing threads already cover pending-request recovery, JSONC parsing, tool-call ID reuse, and the external health timeout. I also corrected the pagination impact in its existing thread.
|
|
||
| return { | ||
| ...(environment ?? process.env), | ||
| OPENCODE_CONFIG_CONTENT: JSON.stringify({ |
There was a problem hiding this comment.
Note
🤖 GPT-5.6 Sol responding on behalf of Theo
Blocker: a configured agent can bypass T3's supervised mode. I passed an agent-level shell: allow rule through this helper. Both tested OpenCode betas returned allow in approval-required mode. The control without an agent override returned ask.
OpenCode applies global rules first, then agent rules. The last matching rule wins. The bridge's session permission argument only fills a local cache, so it does not enforce a server-side limit. Enforce T3's selected mode after the effective agent policy, or reject restricted startup when OpenCode cannot enforce it.
| const config = decodedConfig.value; | ||
| const serverEnvironment = { | ||
| ...resolvedEnvironment, | ||
| OPENCODE_CONFIG_CONTENT: encodeOpenCodeConfigContent({ |
There was a problem hiding this comment.
Note
🤖 GPT-5.6 Sol responding on behalf of Theo
Blocker: background text generation can execute tools. I ran this exact local generation configuration through the bridge. A configured agent allowed shell, the fake model requested printf audit, and OpenCode executed it on both tested betas despite the global denial and deny-all session permission.
The global denial can be overridden by agent rules. The session permission is only a bridge cache. The external generation path does not add this global denial at all. Use a generation path that cannot execute tools, or enforce denial on the effective agent.
| } | ||
| } | ||
|
|
||
| if (parameters.agent && session?.agent !== parameters.agent) { |
There was a problem hiding this comment.
Note
🤖 GPT-5.6 Sol responding on behalf of Theo
Leaving plan mode does not restore the normal agent. T3 sends agent: "plan" in plan mode, then omits agent for a normal turn when no custom agent is selected. OpenCode stores the selected agent on the session, so omission keeps plan.
I ran the real adapter through one plan turn and one default turn. Both tested betas still reported agent: "plan" after the default turn. Resolve and send the intended normal agent when leaving plan mode.
| event: { | ||
| subscribe: async (_parameters?: unknown, options?: LegacySubscribeOptions) => ({ | ||
| stream: translateEvents( | ||
| client.event.subscribe(options?.signal ? { signal: options.signal } : undefined), |
There was a problem hiding this comment.
Note
🤖 GPT-5.6 Sol responding on behalf of Theo
One transient SSE transport error stops the T3 session. The old SDK retries this stream. The v2 promise client returns the first error, and the adapter treats it as an unexpected exit and interrupts upstream.
With one injected failure followed by a healthy stream, v1 made two fetches and received server.connected. This path made one fetch and returned only the transport error. Add reconnect plus state and pending-request reconciliation. Also handle an unexpected clean EOF.
| ), | ||
| ).pipe(Effect.orElseSucceed(() => undefined)); | ||
|
|
||
| if (service) { |
There was a problem hiding this comment.
Note
🤖 GPT-5.6 Sol responding on behalf of Theo
Shared-service inventory ignores this provider instance's environment and config. I registered a real isolated service, then loaded inventory for an instance that defined provider second. Discovery returned the service's opencode and audit providers instead.
Chat starts its own process with the instance settings, so inventory can hide usable models or advertise models chat cannot use. Load inventory from the same effective environment as chat unless the user explicitly selected an external server.
| description: option.description ?? "", | ||
| })) | ||
| : []; | ||
| return { |
There was a problem hiding this comment.
Note
🤖 GPT-5.6 Sol responding on behalf of Theo
Conditional forms lose their conditions and can no longer be submitted. This conversion drops when, required, numeric bounds, and string constraints, then the reply path sends answers for every field.
I tested a boolean plus a required string that was active only when the boolean was true. The bridge displayed both fields. Answering No plus the string failed on both betas with FormInvalidAnswerError: Form field is not active: name. Preserve the form rules through client rendering and answer conversion, or reject unsupported forms explicitly.
| openCodeSettings.serverUrl.length > 0 | ||
| ? yield* runAgainstServer({ url: openCodeSettings.serverUrl }) | ||
| ? yield* openCodeRuntime | ||
| .connectToOpenCodeServer({ |
There was a problem hiding this comment.
Note
🤖 GPT-5.6 Sol responding on behalf of Theo
External text generation drops the provider instance environment. Chat passes the resolved environment here, but this call does not. I supplied OPENCODE_PASSWORD only through the instance environment and ran title generation against a real authenticated server. Both tested betas returned HTTP 401.
Pass resolvedEnvironment into connectToOpenCodeServer, as the adapter does, and test environment-based credentials as well as the settings password.


Problem
OpenCode 2 installs as
opencode2and uses a different authenticated HTTP API, so the existing OpenCode 1 integration cannot discover preview builds, load model inventories, start sessions, or stream events reliably.Changes
@opencode-ai/clientbridge for OpenCode 2 model/provider inventories, agents, skills, sessions, prompts, attachments, approvals, forms, streaming, rollback, and MCP registration.opencode2, accept preview versions, authenticate local/shared/external servers, preserve supervised permission modes, and update background text generation plus installation documentation.Verification
opencode2, and external-server configurations: 417 models and 32 skills.Harness: OpenCode
Note
Medium Risk
Large changes to provider session auth, permission enforcement, and approval mapping; mistakes could mis-handle credentials or supervised tool approvals, though behavior is heavily covered by new tests.
Overview
Adds OpenCode 2 (
opencode2) as a first-class provider path while keeping OpenCode 1 behavior unchanged. The server picks v1 vs v2 from CLI version output, binary name, and external/api/healthprobes, then routes inventory and sessions through either the existing SDK or a new@opencode-ai/clientbridge (openCodeV2Client) that speaks the v2 HTTP API but exposes the legacy client shape.Discovery and connectivity: missing-binary messaging mentions both executables; v2 preview/semver parsing skips the v1 minimum; local v2 servers get generated Basic auth (with configured passwords still used for external URLs); inventory can load from a registered shared service or a scoped v2 server.
Sessions and streaming: the adapter passes API generation and resolved passwords into the client, injects runtime permission config for non–full-access locally spawned v2 servers, treats
SessionNotFoundErroras a safe resume miss, aborts the event stream before scope teardown, and maps shell tools/permissions to command execution with v2-specific approval labels (project-wide acceptAlways vs session once).Maintenance:
opencode2/ unresolved defaultopencoderesolves to manual-only updates instead of the legacy package updater.Reviewed by Cursor Bugbot for commit 1a873b5. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add OpenCode 2 support alongside OpenCode 1 in server provider
OpenCodeApiVersion(v1|v2) threading through runtime, adapter, provider, and text-generation layers; auto-detects version from CLI output or binary name (opencodevsopencode2) with fallback resolution.createOpenCodeV2Clientin openCodeV2Client.ts backed by the new@opencode-ai/clientdependency, with Basic auth header support and legacy-shape session/message conversion.opencodeRuntimegenerates a random 32-byte server password for locally spawned v2 servers and injectsOPENCODE_PASSWORD/OPENCODE_SERVER_PASSWORD; external server connections probe/api/healthto infer apiVersion and forward credentials.OpenCodeAdaptermaps v2shelltools tocommand_execution, surfaces v2 permission approvals with explicit decision options, translatesacceptForSession/acceptAlwaysto v2once/alwaysreplies, and aborts the event stream on session stop or unexpected exit.OpenCodeTextGenerationnow enforces deny-by-default inOPENCODE_CONFIG_CONTENTfor managed local servers, appending a terminal deny-all rule while preserving user-specified permissions.parseServerUrlFromOutputreturn type changed fromstring|nullto{url, apiVersion}|null; callers inopencodeRuntime.tsare updated. Minimum-version gating applies to v1 only; v2 prerelease/beta versions bypass the check.OpenCodeDriverreturns manual-only maintenance capabilities for v2 or unresolved-default binaries, removing auto-update paths for those cases.📊 Macroscope summarized 1a873b5. 11 files reviewed, 18 issues evaluated, 5 issues filtered, 13 comments posted
🗂️ Filtered Issues
apps/server/src/provider/Layers/OpenCodeAdapter.ts — 2 comments posted, 4 evaluated, 2 filtered
config.permissionsarray (the normal case), this conditional omits the generated v2 rules entirely and only writes the legacy singularpermissionfield. OpenCode v2 explicitly consumespermissionsand does not usepermission/bash; its default agent policy allows most tools, so locally spawned v2 sessions can execute edits/shell commands without the selected supervised or auto-accept-edits approvals. The generated rules must be emitted intopermissionseven when the incoming config has none. [ Cross-file consolidated ]withOpenCodeRuntimePermissions, but that helper only writes the generated V2permissionsarray when the user's config already containspermissions. With a normal config such as{}(no existing array), it emits only the legacypermissionobject, which OpenCode V2 ignores, soapproval-requiredandauto-accept-editssessions run with OpenCode's permissive defaults instead of prompting/limiting tools. This bypasses the runtime safety mode for users without custom permission rules. [ Cross-file consolidated ]apps/server/src/provider/opencodeRuntime.ts — 2 comments posted, 3 evaluated, 1 filtered
versionand numericpid. The v2 health handler's contract returns{ healthy: true }(the v2 endpoint is/api/health), so a valid v2 external server is labeledv1; callers then construct the legacy client and request legacy routes, making external OpenCode 2 inventory and sessions fail. Check the v2 discriminator using the actual health contract rather than requiring these unrelated fields. [ Failed validation ]apps/server/src/textGeneration/OpenCodeTextGeneration.ts — 1 comment posted, 2 evaluated, 1 filtered
permissionsarray, but OpenCode evaluates agent-specific rules after global rules and the last match wins; a configuredagents.build.permissions(or another selected agent) can therefore override this deny and allowshell,edit, or other tools. The v2 compatibility client'ssession.createalso does not send thepermissionargument to OpenCode, so the deny passed by the caller cannot repair this. A model using such a configured agent during background generation can execute commands or mutate the project despite this restriction. [ Already posted ]docs/user/permission-modes.md — 0 comments posted, 1 evaluated, 1 filtered
OpenCodeAdapter.startSessionstill sendsbuildOpenCodePermissionRules(input.runtimeMode)in the v2session.create/session.updaterequest even whenserver.externalis true, and the v2 API accepts session-levelpermissionrules. Users following this guidance may configure the remote server unnecessarily and misunderstand the per-thread permission mode they selected. [ Out of scope (triage) ]