Skip to content

feat(opencode-v2): OpenCode 2.x host adapter (reuses the existing core) - #13

Open
Vladimir-Human wants to merge 5 commits into
cortexkit:mainfrom
Vladimir-Human:feat/opencode-v2-host-adapter
Open

feat(opencode-v2): OpenCode 2.x host adapter (reuses the existing core)#13
Vladimir-Human wants to merge 5 commits into
cortexkit:mainfrom
Vladimir-Human:feat/opencode-v2-host-adapter

Conversation

@Vladimir-Human

@Vladimir-Human Vladimir-Human commented Aug 18, 2026

Copy link
Copy Markdown

Title

Add an OpenCode 2.x host adapter (reuses the existing core, no changes to the 1.x package)

Background

OpenCode 2.x (@opencode-ai/cli@0.0.0-beta-*, binary opencode2) replaced the plugin surface:

  • no fetch() patching; instead session.hook("http.request" | "http.response");
  • providers are declared with native packages in config, e.g.
    "package": "@opencode-ai/ai/providers/google";
  • OAuth methods are registered through integration.transform, tools through tool.transform;
  • the existing @cortexkit/opencode-antigravity-auth declares
    engines.opencode: ">=1.17.13 <2" and cannot load on 2.x (neither the TUI sidebar nor the
    fetch patch exist there).

So 2.x users currently have no Antigravity access. I wrote the host adapter; all logic still
comes from @cortexkit/antigravity-auth-core@2.1.0 — OAuth, transport, account pool, quota and
the model registry are untouched.

Implementation

  1. session.hook("http.request") intercepts Antigravity model calls and rewrites the URL to a
    loopback server owned by the plugin.
    Why a loopback server: 2.x sends whatever event.request the hook leaves behind through its
    own HTTP client, and http.response only runs after that request succeeded, so a custom
    Response cannot be returned from the hook. The loopback keeps the core's
    fetchWithAgyCliTransport (agy header order, proxy support) while the host still sees a
    plain, streamable, cancellable SSE.
  2. Inside the loopback: getCurrentOrNextForFamily (hybrid) → refreshAntigravityToken
    ensureProjectContextbuildAgyAgentRequestMetadata +
    orderAgyRequestPayloadInPlaceANTIGRAVITY_ENDPOINT_FALLBACKS.
    429/403 cools the account down via markRateLimitedWithReason and rotates, 401 forces a
    refresh, and an empty-candidate STOP is retried up to three times.
  3. The native @opencode-ai/ai/providers/google package stays the codec, so image, PDF and
    tool-call handling comes from the host rather than a hand-written adapter.

Four issues found and worked around (may matter for 1.x / pi too)

  1. GPT-OSS tool schemas — the AGY GPT bridge re-encodes protobuf numeric constraints as
    strings, so minLength: 1 returns 400 INVALID_ARGUMENT. Fixed with
    normalizeGeminiTools(request, { moveNumericConstraintsToDescription: true }) for gpt-*
    wire models (the flag already exists in core; it just needs to be enabled for that family).
  2. Strict native event schema — GPT-OSS opens a turn with content and no parts, and
    Claude sometimes uses role assistant; 2.x rejects both with
    Invalid google/gemini stream event. Frames must be normalised before forwarding.
  3. gzip header — the core transport already inflates the body, so copying upstream
    content-encoding: gzip to the host makes it inflate twice and fail.
  4. PDF attachments — the 2.x CLI drops PDF attachments before the provider sees them (no
    inlineData in the request). I therefore register an antigravity_read_document tool that
    loads the local PDF/image itself and asks a multimodal model. Images pasted into chat work
    natively. Generated images are written to disk and returned as a text path, because the 2.x
    native parser only renders text and tool calls.

Verified (Windows 11 / Node 24 / OpenCode 0.0.0-beta-17595 / two-account pool)

  • all eight selectors answered live requests, every reasoning tier included:
    Gemini 3.7 / 3.6 / 3.5 Flash, Gemini 3.1 Pro, Gemini 3.1 Flash Image,
    Claude Sonnet 4.6 Thinking, Claude Opus 4.6 Thinking, GPT-OSS 120B;
  • tool calling works end to end; PNG attachment recognised; PDF text extracted through the
    tool; image generation written to disk; disabling account #0 failed over to #1.

Privacy and credentials

  • No accounts, e-mail addresses, tokens, project ids or local paths appear in the code or docs.
  • Logs contain only #<account index>, upstream status codes and routing — no prompts, tokens
    or e-mail addresses.
  • Credentials stay in the core-owned antigravity-accounts.json (v4 schema + fenced lock),
    shared with the 1.x plugin and the standalone CLI.
  • Testing was done locally with my own accounts; no test data is included.

How you may want to take it

  1. I can shape it into packages/opencode-v2 (or any name you prefer) and open a PR, including
    the bilingual README, an example config and the MIT licence;
  2. or you take only the fixes (GPT-OSS schema flag, frame normalisation, gzip header) and I
    split them into small PRs;
  3. or I publish it as a separate repository crediting and linking your core.

Whatever suits you. Thanks for the core — its transport and account-pool design made this
adapter straightforward.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Adds @cortexkit/opencode-v2-antigravity-auth, a native OpenCode 2.x adapter that restores Antigravity access by rerouting Google model calls to a loopback that reuses the shared core. 2.x previously had no Antigravity support; 1.x behavior is unchanged. Non‑stream generateContent now returns a merged JSON response, and generated images are saved to disk.

  • Intercepts /models/*:(stream)generateContent via session.hook("http.request"), forwards to 127.0.0.1, unwraps SSE frames, normalizes events for the strict 2.x schema, retries bare STOP up to 3 times, and merges non‑stream calls into one JSON response.
  • Keeps @opencode-ai/ai/providers/google as the codec; enables normalizeGeminiTools(..., { moveNumericConstraintsToDescription: true }) for GPT‑OSS tool schemas.
  • Transport and pool: hybrid rotation; 401 forces one refresh then excludes the account; 429/403 cools and rotates (honors retry-after); endpoint fallbacks via ANTIGRAVITY_ENDPOINT_FALLBACKS.
  • Adds antigravity_read_document for PDFs/images; by default only files under the user’s home directory are readable, sensitive paths are always denied, the boundary is enforced on the real path (prevents symlink/junction escape), and readDocumentRoots are canonicalized and can narrow access.
  • OAuth “Google Antigravity (add account)” appends to the shared pool (antigravity-accounts.json, v4). Callback listens on 127.0.0.1:51121/oauth-callback; the callback page only acknowledges receipt.
  • Logs to <state dir>/antigravity-v2.log without prompts/tokens. Adds example config and tests; includes the package in the publish matrix and scripts/version-sync.mjs; typechecks both plugin and callback with node --check; tests use bun test --isolate.
  • Fix: import resolve explicitly in the path allowlist to avoid a runtime ReferenceError.

Rollout

  • Requires OpenCode 2.x and @cortexkit/antigravity-auth-core@2.1.0.
  • Add the plugin to opencode.json pointing to src/plugin.mjs (absolute path or node_modules/@cortexkit/opencode-v2-antigravity-auth/src/plugin.mjs), and declare models under the google provider using @opencode-ai/ai/providers/google.
  • To add accounts: connect the google integration and select “Google Antigravity (add account)”; allow localhost access and port 51121.
  • Optional: override the pool path with ANTIGRAVITY_ACCOUNTS_FILE; set readDocumentRoots to restrict document access.

Written for commit ccd60d9. Summary will update on new commits.

Review in cubic

OpenCode 2.x replaced the plugin surface the 1.x package relies on: there is no
fetch() patch and no TUI sidebar, and providers are declared with native packages
in config. packages/opencode declares engines.opencode '>=1.17.13 <2', so 2.x
users currently have no Antigravity access.

This package is the missing host adapter. All logic still comes from
@cortexkit/antigravity-auth-core: OAuth, transport, account pool, quota and the
model registry are untouched.

- session.hook('http.request') reroutes Antigravity model calls to a loopback
  server owned by the plugin, which performs the real request through
  fetchWithAgyCliTransport so the agy wire format and proxy support survive
- account selection via getCurrentOrNextForFamily (hybrid), refreshAntigravityToken,
  ensureProjectContext, agent envelope from buildAgyAgentRequestMetadata plus
  orderAgyRequestPayloadInPlace, endpoint fallback list
- 429/403 cools the account down and rotates, 401 forces a refresh, an
  empty-candidate STOP is retried three times
- the native @opencode-ai/ai/providers/google package stays the codec, so image,
  PDF and tool-call handling comes from the host
- normalizeGeminiTools with moveNumericConstraintsToDescription for gpt-* models,
  which fixes 400 INVALID_ARGUMENT on GPT-OSS tool schemas
- stream frames are normalised for the strict native Gemini event schema
  (GPT-OSS opens a turn without parts, Claude may use role assistant)
- antigravity_read_document tool, because the 2.x CLI drops PDF attachments
- generated images are written to the data dir and announced as text
- logs carry account indexes only: no prompts, tokens or e-mail addresses

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

1 issue found across 8 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/opencode-v2/package.json">

<violation number="1" location="packages/opencode-v2/package.json:30">
P3: This new package ships no `scripts` and is not wired into the root workspace scripts: root `build`/`typecheck`/`test` cover only `packages/core`, `packages/opencode`, and `packages/pi`. As a result nothing builds, lints, or tests the opencode-v2 source in CI, even though the `packages/*` workspace glob pulls it into `bun install`. If this stays in the monorepo, add at least a `lint`/`build`/`test` script and reference it from the root; if it is meant to be a standalone package, that is fine, but the decision should be explicit rather than leaving a workspace member with zero CI coverage.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/opencode-v2/src/plugin.mjs Outdated
Comment thread packages/opencode-v2/src/oauth-callback.mjs Outdated
Comment thread packages/opencode-v2/src/plugin.mjs
Comment thread packages/opencode-v2/README.md Outdated
Comment thread packages/opencode-v2/src/plugin.mjs
Comment thread packages/opencode-v2/src/plugin.mjs Outdated
Comment thread README.md
@@ -0,0 +1,45 @@
{

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 new package ships no scripts and is not wired into the root workspace scripts: root build/typecheck/test cover only packages/core, packages/opencode, and packages/pi. As a result nothing builds, lints, or tests the opencode-v2 source in CI, even though the packages/* workspace glob pulls it into bun install. If this stays in the monorepo, add at least a lint/build/test script and reference it from the root; if it is meant to be a standalone package, that is fine, but the decision should be explicit rather than leaving a workspace member with zero CI coverage.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode-v2/package.json, line 30:

<comment>This new package ships no `scripts` and is not wired into the root workspace scripts: root `build`/`typecheck`/`test` cover only `packages/core`, `packages/opencode`, and `packages/pi`. As a result nothing builds, lints, or tests the opencode-v2 source in CI, even though the `packages/*` workspace glob pulls it into `bun install`. If this stays in the monorepo, add at least a `lint`/`build`/`test` script and reference it from the root; if it is meant to be a standalone package, that is fine, but the decision should be explicit rather than leaving a workspace member with zero CI coverage.</comment>

<file context>
@@ -0,0 +1,45 @@
+    "node": ">=20.0.0",
+    "opencode": ">=2 <3"
+  },
+  "main": "./src/plugin.mjs",
+  "exports": {
+    ".": "./src/plugin.mjs",
</file context>

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 0660034: the package ships lint/format:check/typecheck/test scripts plus a smoke test (packages/opencode-v2/test/plugin.test.mjs), and is wired into the root typecheck (node --check) and test chains. bun.lock is regenerated. Note: this package is plain ESM by design — the typecheck step is node --check rather than tsc.

- P1: allow only one forced OAuth refresh per account/request; a repeated 401
  now excludes the account and continues pool selection instead of looping
- P2: mark request execution complete after a terminal stream, a collected
  tool response or a non-stream answer, so follow-up tool-result requests
  keep the core's last_execution_id trajectory metadata
- P2: answer non-streaming generateContent calls with a single JSON response
  (frames are merged) instead of an SSE body the host cannot parse
- P2: restrict antigravity_read_document to the user's home directory by
  default, block well-known credential/secret paths always, and allow
  narrowing via the readDocumentRoots plugin option; README documents the
  prompt-injection/exfiltration risk
- P2: OAuth callback page only reports that the authorization was received;
  the final result is reported by OpenCode after exchange + persistence
- P2: fix the Install instructions (the adapter package, not just the core)
  and explain how the plugin path should be set
- P3: add the package to the release publish matrix and fix the package-count
  references in the root README
- P3: add lint/format/typecheck/test scripts, a smoke test and wire the
  package into the root typecheck/test chains; update bun.lock

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

1 issue found across 12 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name=".github/workflows/release.yml">

<violation number="1" location=".github/workflows/release.yml:82">
P2: Adding `@cortexkit/opencode-v2-antigravity-auth` to the publish matrix breaks the release because its version is never synchronized. The workflow's `Sync version` step runs `scripts/version-sync.mjs <tag-version>`, whose `packageJsonPaths` array only covers `packages/{core,opencode,pi}/package.json` and omits `packages/opencode-v2/package.json`. So opencode-v2 always publishes its hardcoded `2.1.0`, which diverges from the release tag and, on the second release, collides with the already-published version and fails the `npm publish` job (which blocks the downstream `github-release`). Add the opencode-v2 path to `version-sync.mjs` alongside this matrix entry.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/opencode-v2/src/plugin.mjs Outdated
Comment thread packages/opencode-v2/src/plugin.mjs Outdated
workspace: "@cortexkit/opencode-antigravity-auth"
- package: "@cortexkit/pi-antigravity-auth"
workspace: "@cortexkit/pi-antigravity-auth"
- package: "@cortexkit/opencode-v2-antigravity-auth"

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: Adding @cortexkit/opencode-v2-antigravity-auth to the publish matrix breaks the release because its version is never synchronized. The workflow's Sync version step runs scripts/version-sync.mjs <tag-version>, whose packageJsonPaths array only covers packages/{core,opencode,pi}/package.json and omits packages/opencode-v2/package.json. So opencode-v2 always publishes its hardcoded 2.1.0, which diverges from the release tag and, on the second release, collides with the already-published version and fails the npm publish job (which blocks the downstream github-release). Add the opencode-v2 path to version-sync.mjs alongside this matrix entry.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/release.yml, line 82:

<comment>Adding `@cortexkit/opencode-v2-antigravity-auth` to the publish matrix breaks the release because its version is never synchronized. The workflow's `Sync version` step runs `scripts/version-sync.mjs <tag-version>`, whose `packageJsonPaths` array only covers `packages/{core,opencode,pi}/package.json` and omits `packages/opencode-v2/package.json`. So opencode-v2 always publishes its hardcoded `2.1.0`, which diverges from the release tag and, on the second release, collides with the already-published version and fails the `npm publish` job (which blocks the downstream `github-release`). Add the opencode-v2 path to `version-sync.mjs` alongside this matrix entry.</comment>

<file context>
@@ -79,6 +79,8 @@ jobs:
             workspace: "@cortexkit/opencode-antigravity-auth"
           - package: "@cortexkit/pi-antigravity-auth"
             workspace: "@cortexkit/pi-antigravity-auth"
+          - package: "@cortexkit/opencode-v2-antigravity-auth"
+            workspace: "@cortexkit/opencode-v2-antigravity-auth"
     steps:
</file context>

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in b9136d9: scripts/version-sync.mjs now includes packages/opencode-v2/package.json in packageJsonPaths, so release tags bump the adapter version together with core/opencode/pi and the publish matrix cannot collide with a stale hardcoded version.

Comment thread package.json Outdated
Comment thread packages/opencode-v2/package.json Outdated
Comment thread packages/opencode-v2/package.json Outdated
@Vladimir-Human

Copy link
Copy Markdown
Author

All eight review comments are addressed in 0660034 (push e58698a..0660034), with replies on each thread:

  • P1 — at most one forced refresh per account/request; repeated 401 excludes the account and pool selection continues.
  • P2requestSessions.completeExecution() is called after terminal streams, collected tool responses, and non-stream answers.
  • P2 — non-streaming generateContent calls now get a single JSON GenerateContentResponse (frames merged) instead of an SSE body.
  • P2antigravity_read_document refuses well-known credential/secret paths (.ssh, .config, .local, AppData, .env, *.key/*.pem/*.p12/*.pfx, id_rsa, auth.json, antigravity-accounts.json, …), defaults to the user's home directory, and can be narrowed via the readDocumentRoots plugin option; the exfiltration risk is documented in both READMEs (EN + zh-CN).
  • P2 — the OAuth callback page reports only that the authorization was received; the result is reported by OpenCode after exchange + persistence.
  • P2 — Install instructions fixed (adapter package, not just core) incl. plugin-path resolution.
  • P3 — root README package-count references fixed; release publish matrix now includes @cortexkit/opencode-v2-antigravity-auth.
  • P3 — the package ships lint/format:check/typecheck/test scripts and a smoke test, wired into the root typecheck and test chains; bun.lock regenerated.

Verified locally before pushing: biome check clean (repo-wide), node --check clean, package smoke test passes, and a live request through the adapted plugin answered (gemini-3.6-flash, PDF through antigravity_read_document).

The path allowlist used path.resolve() but only imported { join } from
node:path, so the document tool would throw a ReferenceError on every call
in a runtime without an ambient path global. Import resolve explicitly.
- P1: enforce the document boundary on the real path too — symlinks and
  junctions inside an allowed root could bypass the check because readFile
  follows them; realpath() is resolved and re-checked before reading
- P2: add packages/opencode-v2/package.json to scripts/version-sync.mjs so
  release tags actually bump the adapter version (the publish matrix alone
  would publish a stale hardcoded 2.1.0 and collide on the second release)
- P3: node --check only validates its first argument, so oauth-callback.mjs
  was never checked — typecheck scripts now run the check per file
- P3: package test script uses bun test --isolate (repo convention) via the
  root composition that the bundled test runner accepts

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

All reported issues were addressed across 4 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/opencode-v2/src/plugin.mjs
… check

A configured root that is itself a symlink or Windows junction would never
match the resolved document path, rejecting every file under it. Roots are
now canonicalized (realpathSync, lexical fallback) so junction-configured
roots stay usable while the boundary is still enforced on the real path.
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