Repository navigation
fix: OpenCode 2.0.29 GA RPC compatibility and Windows support - #40
ahmedtohamy1 wants to merge 5 commits into
Conversation
…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.
There was a problem hiding this comment.
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}"`], { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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}`)) |
There was a problem hiding this comment.
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]) |
There was a problem hiding this comment.
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>
| ``` | ||
|
|
||
| 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: |
There was a problem hiding this comment.
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:
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
output(and thechangedevent payload), which the newer host cannot encode, so everystate/applycall failed withInvalidRequestError: Expected JSON value at ["output"](and laterrpc.invalid_output: Expected JSON value at ["accounts"]), leaving the/antigravitymenu and sidebar without a snapshot.verifyPrerequisitescompared realpaths against a hardcoded/node_modules/separator, and the library build calledchmod.cmd /c start, andcmd.exesplits it at the first&, droppingresponse_type; the CLI also wrote the retired pool file instead of the account store, and never printed the URL under--no-browser.writeTransferfsynced a directory handle, which Windows rejects withEPERM: operation not permitted, fsync.Changes
ga/rpc/protocol.ts,ga/server/index.ts: register a JSON Schema for each method's wireoutputand thechangedevent payload (inputs keep the strict Standard Schema), and stripundefined-valued keys from every answer. The strict contract still runs inside the handler and in the client's own parse.packages/opencode/scripts/build-tui.ts,tools/common-auth-build/build.mjs: use the platform separator for containment checks; skipchmodon Windows.packages/opencode/src/cli.ts,plugin/oauth-login.ts: quote the consent URL and passwindowsVerbatimArguments; print the authorization URL when the browser is not opened; persist through the account store withcommitLogins.packages/core/src/account-repository.ts: skip directory fsync on Windows (the file is still flushed).README.md(requiredproviders.googleentry) andCHANGELOG.md.Testing
bun test --isolate src/ga→ 156 pass, 0 fail, including the newrpc-json.test.ts.POST /api/rpc/antigravity-auth/state→200 { output: … }for both session and sessionless scopes./antigravitycommand registered.opencode run -m google/antigravity-gemini-3.8-flash "Reply with exactly: OK"→OK.Notes
%TEMP%, which the private-files policy rejects as writable-by-others, plus POSIX-mode and native-prebuilt checks.googleprovider, so a host that does not expose one now needs the documentedproviders.googleentry.core.autocrlfmust befalseso 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
outputandchangedevent 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 ofundefined-valued keys.state/applycalls previously failed withInvalidRequestError/rpc.invalid_output, leaving the/antigravitymenu and sidebar without a snapshot.chmod; both now respect the platform.cmd /c start, losingresponse_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.EPERM.Migration
googleprovider, so hosts without one must configure it; the README documents the requiredproviders.googleentry.Written for commit 970bea9. Summary will update on new commits.