Skip to content

fix: OpenCode 2.0.29 GA RPC compatibility and Windows support - #40

Open
ahmedtohamy1 wants to merge 5 commits into
cortexkit:mainfrom
ahmedtohamy1:fix/opencode-2.0.29-compat
Open

ahmedtohamy1 wants to merge 5 commits into
cortexkit:mainfrom
ahmedtohamy1:fix/opencode-2.0.29-compat

Conversation

@ahmedtohamy1

@ahmedtohamy1 ahmedtohamy1 commented Oct 15, 2026 •

Copy link
Copy Markdown

Summary

Makes the GA (OpenCode 2.x) adapter work on the current 2.0.29 host and on Windows, and fixes the standalone CLI login. Verified end-to-end against OpenCode 2.0.29 on Windows.

Root causes fixed

  1. GA RPC never answered on 2.0.29. OpenCode 2 serializes an RPC method's answer through the registered schema. The adapter registered its strict Standard Schemas as each method's wire output (and the changed event payload), which the newer host cannot encode, so every state/apply call failed with InvalidRequestError: Expected JSON value at ["output"] (and later rpc.invalid_output: Expected JSON value at ["accounts"]), leaving the /antigravity menu and sidebar without a snapshot.
  2. Windows build. verifyPrerequisites compared realpaths against a hardcoded /node_modules/ separator, and the library build called chmod.
  3. Windows CLI login. The browser opener passed the consent URL unquoted to cmd /c start, and cmd.exe splits it at the first &, dropping response_type; the CLI also wrote the retired pool file instead of the account store, and never printed the URL under --no-browser.
  4. Windows store write. writeTransfer fsynced a directory handle, which Windows rejects with EPERM: operation not permitted, fsync.

Changes

  • RPC wire contract — ga/rpc/protocol.ts, ga/server/index.ts: register a JSON Schema for each method's wire output and the changed event payload (inputs keep the strict Standard Schema), and strip undefined-valued keys from every answer. The strict contract still runs inside the handler and in the client's own parse.
  • Windows build — packages/opencode/scripts/build-tui.ts, tools/common-auth-build/build.mjs: use the platform separator for containment checks; skip chmod on Windows.
  • CLI login — packages/opencode/src/cli.ts, plugin/oauth-login.ts: quote the consent URL and pass windowsVerbatimArguments; print the authorization URL when the browser is not opened; persist through the account store with commitLogins.
  • Core — packages/core/src/account-repository.ts: skip directory fsync on Windows (the file is still flushed).
  • Docs — README.md (required providers.google entry) and CHANGELOG.md.

Testing

  • bun test --isolate src/ga → 156 pass, 0 fail, including the new rpc-json.test.ts.
  • Biome clean on all changed files.
  • End-to-end on Windows + OpenCode 2.0.29:
    • POST /api/rpc/antigravity-auth/state → 200 { output: … } for both session and sessionless scopes.
    • /antigravity command registered.
    • opencode run -m google/antigravity-gemini-3.8-flash "Reply with exactly: OK" → OK.

Notes

  • The full package suite has pre-existing Windows environment failures unrelated to this change: tests create directories under %TEMP%, which the private-files policy rejects as writable-by-others, plus POSIX-mode and native-prebuilt checks.
  • The GA adapter attaches its catalog to the host's google provider, so a host that does not expose one now needs the documented providers.google entry.
  • core.autocrlf must be false so the embedded-manifest hash check passes; that is a checkout setting, not part of this change.

Summary by cubic

Makes the OpenCode 2.x GA adapter work with OpenCode 2.0.29 and on Windows, and fixes the standalone CLI login.

Bug Fixes

  • The adapter's strict Standard Schema wire output and changed event payload could not be serialized by OpenCode 2; they now use a permissive JSON Schema while inputs keep the strict validator, and every answer is stripped of undefined-valued keys. state/apply calls previously failed with InvalidRequestError / rpc.invalid_output, leaving the /antigravity menu and sidebar without a snapshot.
  • The Windows build used POSIX path separators and chmod; both now respect the platform.
  • The Windows CLI login passed the consent URL unquoted to cmd /c start, losing response_type; the URL is now quoted. Logins also persist through the account store instead of the retired pool file, and the URL is printed when the browser isn't opened.
  • The account store no longer fsyncs directory handles, which Windows rejects with EPERM.

Migration

  • The adapter attaches its catalog to the host's google provider, so hosts without one must configure it; the README documents the required providers.google entry.

Written for commit 970bea9. Summary will update on new commits.

View guided diff

…file

`StoreAccountRepository.writeTransfer` fsynced the transfers and store
directories after publishing the transfer entry. Windows rejects fsync on
a directory handle with EPERM, so a pool replacement failed with
`EPERM: operation not permitted, fsync`.

Guard the directory sync with the same Windows check the migration
durability path already uses; the file itself is still flushed.
The build scripts assumed POSIX. `verifyPrerequisites` compared resolved
realpaths against a hardcoded `/node_modules/` separator, and the library
build called `chmod`, so `bun run build` failed on Windows before emitting
any artifact.

Use the platform separator for the containment checks and skip `chmod` on
Windows.
The Windows browser opener passed the consent URL unquoted to
`cmd /c start`, and cmd.exe splits it at the first `&`, dropping
`response_type` and the rest of the query, so Google rejected the request.
Quote the URL and pass it with `windowsVerbatimArguments`.

Also print the authorization URL when the browser is not opened
(`--no-browser` or headless), and persist a CLI login through the account
store with `commitLogins` instead of the retired pool file: a migrated
store refuses the pre-store writer, and a pool replacement leaves a
resumable journal that does not complete on Windows.
OpenCode 2 serializes an RPC method answer through the registered schema.
The adapter registered its strict Standard Schemas as each method's wire
`output` (and the `changed` event payload), which the newer host cannot
encode, so every `state`/`apply` call failed with
`InvalidRequestError: Expected JSON value at ["output"]` and the
`/antigravity` menu and sidebar never received a snapshot.

Register a JSON Schema for the wire output and event payload, keep the
strict Standard Schema for inputs, and strip `undefined`-valued keys from
every answer, which a strict JSON-value check rejected with
`rpc.invalid_output: Expected JSON value at ["accounts"]`. The strict
contract still runs inside the handler and in the client's own parse.
The GA adapter attaches its catalog to the host's `google` provider, so a
host that does not expose one registers nothing. Add the required
`providers.google` entry, with placeholder credentials, to the OpenCode GA
install example.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

5 issues found across 11 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="tools/common-auth-build/build.mjs">

<violation number="1" location="tools/common-auth-build/build.mjs:41">
P2: On Windows, `realpath()` can return canonical casing that differs from `toolRoot`, but `startsWith()` compares strings case-sensitively. A checkout opened through a differently cased path is therefore rejected as escaped; compare against a canonicalized `node_modules` root or use Windows-aware path containment.</violation>
</file>

<file name="packages/opencode/src/cli.ts">

<violation number="1" location="packages/opencode/src/cli.ts:406">
P1: Quoting does not disable `cmd.exe`’s `%...%` expansion, so the percent-encoded OAuth `redirect_uri` can be consumed and corrupted. Launch the URL without `cmd.exe`.</violation>

<violation number="2" location="packages/opencode/src/cli.ts:595">
P2: A repeat standalone `login` cannot update an existing account in a migrated store when OAuth returns a new refresh token. `commitLogins` rejects that same-email/different-credential case as `duplicate-identity`, so route this flow through an explicit reauthentication/replacement path or preserve the CLI’s previous fresh-login behavior.</violation>
</file>

<file name="packages/opencode/src/plugin/oauth-login.ts">

<violation number="1" location="packages/opencode/src/plugin/oauth-login.ts:62">
P1: This can leave OpenCode `auth login` waiting for a callback without showing the authorization URL: its `performOAuthLogin` caller omits `announce`, and `OPENCODE_HEADLESS=1` can reach this branch when `shouldSkipLocalServer()` is false. Wire an announcement callback at that call site or route headless logins through the manual URL prompt.</violation>
</file>

<file name="README.md">

<violation number="1" location="README.md:83">
P3: This says the plugin registers nothing without `providers.google`, but setup still registers the RPC, bridge, hooks, and `/antigravity` command; only the catalog update is skipped. Say it registers no Antigravity models.</violation>
</file>

Reply to a comment to ask cubic a question or push back. It learns from your replies.

View guided diff | Re-trigger cubic

// `cmd.exe` treats `&` in the URL as a command separator, so the URL must be
// passed as a single quoted token or everything after the first `&` (for
// example `response_type`) is lost.
await execFileAsync('cmd', ['/c', 'start', '""', `"${url}"`], {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Quoting does not disable cmd.exe’s %...% expansion, so the percent-encoded OAuth redirect_uri can be consumed and corrupted. Launch the URL without cmd.exe.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/opencode/src/cli.ts, line 406:

<comment>Quoting does not disable `cmd.exe`’s `%...%` expansion, so the percent-encoded OAuth `redirect_uri` can be consumed and corrupted. Launch the URL without `cmd.exe`.</comment>

<file context>
@@ -400,7 +400,12 @@ const execFileAsync = promisify(execFile)
+    // `cmd.exe` treats `&` in the URL as a command separator, so the URL must be
+    // passed as a single quoted token or everything after the first `&` (for
+    // example `response_type`) is lost.
+    await execFileAsync('cmd', ['/c', 'start', '""', `"${url}"`], {
+      windowsVerbatimArguments: true,
+    })
</file context>

if (!request.noBrowser && !request.isHeadless) {
await deps.openBrowser(authorization.url)
} else {
deps.announce?.(authorization.url)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: This can leave OpenCode auth login waiting for a callback without showing the authorization URL: its performOAuthLogin caller omits announce, and OPENCODE_HEADLESS=1 can reach this branch when shouldSkipLocalServer() is false. Wire an announcement callback at that call site or route headless logins through the manual URL prompt.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/opencode/src/plugin/oauth-login.ts, line 62:

<comment>This can leave OpenCode `auth login` waiting for a callback without showing the authorization URL: its `performOAuthLogin` caller omits `announce`, and `OPENCODE_HEADLESS=1` can reach this branch when `shouldSkipLocalServer()` is false. Wire an announcement callback at that call site or route headless logins through the manual URL prompt.</comment>

<file context>
@@ -56,6 +58,8 @@ export async function performOAuthLogin(
     if (!request.noBrowser && !request.isHeadless) {
       await deps.openBrowser(authorization.url)
+    } else {
+      deps.announce?.(authorization.url)
     }
 
</file context>

resolve(toolRoot, 'node_modules', name, 'package.json'),
)
if (!installed.startsWith(`${toolRoot}/node_modules/`))
if (!installed.startsWith(`${toolRoot}${sep}node_modules${sep}`))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: On Windows, realpath() can return canonical casing that differs from toolRoot, but startsWith() compares strings case-sensitively. A checkout opened through a differently cased path is therefore rejected as escaped; compare against a canonicalized node_modules root or use Windows-aware path containment.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tools/common-auth-build/build.mjs, line 41:

<comment>On Windows, `realpath()` can return canonical casing that differs from `toolRoot`, but `startsWith()` compares strings case-sensitively. A checkout opened through a differently cased path is therefore rejected as escaped; compare against a canonicalized `node_modules` root or use Windows-aware path containment.</comment>

<file context>
@@ -38,7 +38,7 @@ async function verifiedCompiler() {
       resolve(toolRoot, 'node_modules', name, 'package.json'),
     )
-    if (!installed.startsWith(`${toolRoot}/node_modules/`))
+    if (!installed.startsWith(`${toolRoot}${sep}node_modules${sep}`))
       throw new Error(`Private tool root escaped: ${name}`)
     if (JSON.parse(await readFile(installed, 'utf8')).version !== version)
</file context>

},
})
try {
const [committed] = await commitLogins(repository, [result])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: A repeat standalone login cannot update an existing account in a migrated store when OAuth returns a new refresh token. commitLogins rejects that same-email/different-credential case as duplicate-identity, so route this flow through an explicit reauthentication/replacement path or preserve the CLI’s previous fresh-login behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/opencode/src/cli.ts, line 595:

<comment>A repeat standalone `login` cannot update an existing account in a migrated store when OAuth returns a new refresh token. `commitLogins` rejects that same-email/different-credential case as `duplicate-identity`, so route this flow through an explicit reauthentication/replacement path or preserve the CLI’s previous fresh-login behavior.</comment>

<file context>
@@ -548,6 +557,48 @@ async function readAccountStore(legacyPath: string): Promise<CliAccountsRead> {
+    },
+  })
+  try {
+    const [committed] = await commitLogins(repository, [result])
+    if (committed?.status === 'refused') throw new Error(committed.message)
+  } finally {
</file context>

Comment thread README.md
```

The directory example follows the shipped forwarders, not a verified GA npm-installer command. The pinned harness uses an instrumented wrapper directory; direct npm-spec registration/installer syntax is not established here. Consult the matching host's install/config documentation rather than assuming V1's `opencode plugin` or `tui.json` behavior. Do not copy the harness's synthetic Google API key or mock `baseURL` into a real config. The harness's default fixture config has no user model map; no copied model-map requirement is established. Models are served under `google/…`, and the package supplies the Antigravity catalog.
The package attaches its Antigravity catalog to the host's `google` provider, so the host must expose one. OpenCode 2.0.22 shipped a `google` provider by default; hosts that do not (for example later 2.x builds whose catalog omits it until configured) register nothing until `providers.google` exists. Declare it alongside the plugin:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: This says the plugin registers nothing without providers.google, but setup still registers the RPC, bridge, hooks, and /antigravity command; only the catalog update is skipped. Say it registers no Antigravity models.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At README.md, line 83:

<comment>This says the plugin registers nothing without `providers.google`, but setup still registers the RPC, bridge, hooks, and `/antigravity` command; only the catalog update is skipped. Say it registers no Antigravity models.</comment>

<file context>
@@ -80,7 +80,23 @@ The shipped `server.js`/`tui.js` forwarders support registering the installed pa

-The directory example follows the shipped forwarders, not a verified GA npm-installer command. The pinned harness uses an instrumented wrapper directory; direct npm-spec registration/installer syntax is not established here. Consult the matching host's install/config documentation rather than assuming V1's opencode plugin or tui.json behavior. Do not copy the harness's synthetic Google API key or mock baseURL into a real config. The harness's default fixture config has no user model map; no copied model-map requirement is established. Models are served under google/…, and the package supplies the Antigravity catalog.
+The package attaches its Antigravity catalog to the host's google provider, so the host must expose one. OpenCode 2.0.22 shipped a google provider by default; hosts that do not (for example later 2.x builds whose catalog omits it until configured) register nothing until providers.google exists. Declare it alongside the plugin:
+
+```jsonc
</file context>


</details>

```suggestion
The package attaches its Antigravity catalog to the host's `google` provider, so the host must expose one. OpenCode 2.0.22 shipped a `google` provider by default; hosts that do not (for example later 2.x builds whose catalog omits it until configured) register no Antigravity models until `providers.google` exists. Declare it alongside the plugin:

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant