Skip to content

feat(server): support OpenCode 2 alongside OpenCode 1 - #8207

Open
R44VC0RP wants to merge 1 commit into
pingdotgg:mainfrom
R44VC0RP:opencode-v2-support
Open

feat(server): support OpenCode 2 alongside OpenCode 1#8207
R44VC0RP wants to merge 1 commit into
pingdotgg:mainfrom
R44VC0RP:opencode-v2-support

Conversation

@R44VC0RP

@R44VC0RP R44VC0RP commented Aug 25, 2026

Copy link
Copy Markdown

Problem

OpenCode 2 installs as opencode2 and 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

  • Preserve OpenCode 1 support and add an official @opencode-ai/client bridge for OpenCode 2 model/provider inventories, agents, skills, sessions, prompts, attachments, approvals, forms, streaming, rollback, and MCP registration.
  • Discover opencode2, accept preview versions, authenticate local/shared/external servers, preserve supervised permission modes, and update background text generation plus installation documentation.
  • Prevent incompatible OpenCode 1 updater actions, close v2 event streams before session teardown, and keep filesystem restoration owned by T3 checkpoints.

Verification

  • 234 focused tests across OpenCode runtime, client, provider, adapter, text generation, contracts, orchestration, web approvals, and desktop packaging.
  • Server and contracts typechecks, targeted lint/format checks, and the production server bundle.
  • Live OpenCode 2 validation for default, explicit opencode2, and external-server configurations: 417 models and 32 skills.
  • Isolated fake-model end-to-end chat covering supervised mode, streamed output, turn completion, and clean shutdown.

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/health probes, then routes inventory and sessions through either the existing SDK or a new @opencode-ai/client bridge (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 SessionNotFoundError as 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 default opencode resolves 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

  • Introduces OpenCodeApiVersion (v1|v2) threading through runtime, adapter, provider, and text-generation layers; auto-detects version from CLI output or binary name (opencode vs opencode2) with fallback resolution.
  • Adds createOpenCodeV2Client in openCodeV2Client.ts backed by the new @opencode-ai/client dependency, with Basic auth header support and legacy-shape session/message conversion.
  • opencodeRuntime generates a random 32-byte server password for locally spawned v2 servers and injects OPENCODE_PASSWORD/OPENCODE_SERVER_PASSWORD; external server connections probe /api/health to infer apiVersion and forward credentials.
  • OpenCodeAdapter maps v2 shell tools to command_execution, surfaces v2 permission approvals with explicit decision options, translates acceptForSession/acceptAlways to v2 once/always replies, and aborts the event stream on session stop or unexpected exit.
  • OpenCodeTextGeneration now enforces deny-by-default in OPENCODE_CONFIG_CONTENT for managed local servers, appending a terminal deny-all rule while preserving user-specified permissions.
  • Risk: parseServerUrlFromOutput return type changed from string|null to {url, apiVersion}|null; callers in opencodeRuntime.ts are updated. Minimum-version gating applies to v1 only; v2 prerelease/beta versions bypass the check. OpenCodeDriver returns 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
  • line 407: When the caller has no pre-existing config.permissions array (the normal case), this conditional omits the generated v2 rules entirely and only writes the legacy singular permission field. OpenCode v2 explicitly consumes permissions and does not use permission/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 into permissions even when the incoming config has none. [ Cross-file consolidated ]
  • line 1303: For a locally spawned server in any restricted runtime mode, this call routes through withOpenCodeRuntimePermissions, but that helper only writes the generated V2 permissions array when the user's config already contains permissions. With a normal config such as {} (no existing array), it emits only the legacy permission object, which OpenCode V2 ignores, so approval-required and auto-accept-edits sessions 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
  • line 758: The external-server probe only classifies a response as v2 when the health JSON also contains string version and numeric pid. The v2 health handler's contract returns { healthy: true } (the v2 endpoint is /api/health), so a valid v2 external server is labeled v1; 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
  • line 320: The local v2 text-generation server is not actually guaranteed tool-free. This appends the wildcard deny only to the top-level permissions array, but OpenCode evaluates agent-specific rules after global rules and the last match wins; a configured agents.build.permissions (or another selected agent) can therefore override this deny and allow shell, edit, or other tools. The v2 compatibility client's session.create also does not send the permission argument 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
  • line 50: The new claim that T3 Code cannot make an externally managed OpenCode 2 server stricter per thread is false. OpenCodeAdapter.startSession still sends buildOpenCodePermissionRules(input.runtimeMode) in the v2 session.create/session.update request even when server.external is true, and the v2 API accepts session-level permission rules. Users following this guidance may configure the remote server unnecessarily and misunderstand the per-thread permission mode they selected. [ Out of scope (triage) ]

@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 11f013e8-004a-4dc8-95fe-db219a183d9c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Aug 25, 2026
@R44VC0RP

Copy link
Copy Markdown
Author

need to test locally

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Comment on lines +379 to +381
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.");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

),
],
}
: {}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

V2 permissions omitted for empty config

Medium Severity

withOpenCodeRuntimePermissions builds OpenCode 2 permissions rules (including the bashshell 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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1a873b5. Configure here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

@R44VC0RP

Copy link
Copy Markdown
Author

holybotslop


return runOpenCodeSdk("health.get", async () => {
// @effect-diagnostics-next-line globalFetch:off
const response = await nodeFetch(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 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));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 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({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for reproducing and clarifying the broader impact. Would you like me to prepare the pagination fix?

Comment on lines +730 to +743
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")}`,
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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

Comment on lines +1598 to +1600
if (field.type === "number" || field.type === "integer") {
const number = Number(first);
return Number.isFinite(number) ? number : undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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

Comment thread apps/server/src/provider/openCodeV2Client.ts
Comment thread apps/server/src/provider/openCodeV2Client.ts
Comment thread apps/server/src/provider/openCodeV2Client.ts
@macroscopeapp

macroscopeapp Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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:

  • 13 blocking correctness issues found at or above your repo's Minimum Blocking Severity

Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more.

@t3dotgg

t3dotgg commented Aug 26, 2026

Copy link
Copy Markdown
Member

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.

@R44VC0RP

Copy link
Copy Markdown
Author

yeah once v2 publicly launches i'll have it ready

@t3dotgg t3dotgg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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({

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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({

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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({

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

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

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants