OpenCode SDK v2 Boundary¶
Open Cowork is a product layer on top of OpenCode. OpenCode owns execution: sessions, tools, permissions, questions, MCP behavior, native skills, compaction, and event stream semantics. Open Cowork owns composition: desktop and web UI, cloud control plane, projection, policy, sync, workflows, gateway adapters, deployment, and product ergonomics.
The repository enforces that boundary with package-boundary tests. New OpenCode SDK imports must be intentional, documented here, and limited to runtime composition or SDK-event normalization code.
OpenCode compatibility assumptions that intentionally ship with Open Cowork are listed in apps/desktop/src/main/opencode-compatibility.ts. Each entry names its category, owner, source version, product modes, tests, and removal condition when it is a shim or blocked policy. Runtime diagnostics export this registry beside provider, model, MCP, and skill provenance so SDK drift, capability state, and plugin policy are visible before release.
Run pnpm proof:opencode:compatibility before release and after every OpenCode SDK/runtime bump. The command validates the same registry exported by diagnostics and fails closed on missing bundled OpenCode version metadata, unknown compatibility states, private assumptions, source-version drift, missing proving tests, and shim/blocked entries that do not name removal conditions. The fixture in tests/fixtures/opencode-compatibility-registry.json records the intentional registry shape; changing it is a compatibility review, not a snapshot refresh.
Import Rules¶
- Cloud Channel Gateway packages must not import
@opencode-ai/sdk,opencode-ai, direct Postgres clients, or cloud control-plane store implementations. - Standalone Gateway is a separate execution authority. Its SDK usage is limited to the documented private OpenCode adapter in
apps/standalone-gateway/src/opencode.ts. - Durable Gateway (
products/gateway, packagecowork-gateway) is a product partition that coordinates OpenCode via its own daemon/MCP and declares@opencode-ai/sdkat the same pin as Desktop/Cloud (1.18.1) — enforced byscripts/check-opencode-pin-lockstep.mjs(JOE-945). Post JOE-941: construction uses the V2 client entry (@opencode-ai/sdk/v2); session I/O goes through a façade that prefersclient.v2.session.*(classicsession.*only as a partial-mock fallback). Inventory + gate:docs/opencode-durable-gateway-classic-burndown.md,scripts/check-durable-opencode-classic-gate.mjs. It is not part of the Desktop Electron runtime-host path and must not be imported byapps/desktoporpackages/app. - Web, website, renderer, preload, and
@open-cowork/cloud-clientcode must not import@opencode-ai/sdk. - Desktop and cloud runtime code may import the SDK only in the files listed below.
- After SDK events are normalized, desktop, cloud, and gateway code should use Open Cowork's shared cloud projection event contract from
packages/shared/src/cloud-session-projection.ts. - BYOK credentials must enter OpenCode through runtime config provider options, never through process environment variables.
Allowed SDK Import Paths¶
These files are the current SDK boundary. If a path no longer needs the SDK, remove the import and remove it from this list. If a new file needs the SDK, first decide whether the concept belongs in OpenCode runtime composition or in Open Cowork product state.
The OpenCode runtime substrate (config composition, the managed/runtime server, session loading, agent/permission/skill composition) now lives in @open-cowork/runtime-host so the desktop main process and the cloud server can share it. The remaining apps/desktop/src/main entries are product seams that still touch SDK event/types at the desktop edge.
packages/cloud-server/src/byok-runtime-config.tspackages/cloud-server/src/opencode-runtime-adapter.tspackages/cloud-server/src/runtime-adapter.tsapps/desktop/src/main/durable-session-events.tsapps/desktop/src/main/event-subscriptions.tsapps/desktop/src/main/events.tsapps/desktop/src/main/ipc/context.tsapps/desktop/src/main/ipc/provider-handlers.tsapps/desktop/src/main/runtime-mcp-status-polling.ts
Desktop residual seams and removal plan: desktop-composition-shell.md (JOE-842). question-normalization moved to @open-cowork/runtime-host (no SDK import). - apps/standalone-gateway/src/opencode.ts - packages/runtime-host/src/agent-config.ts - packages/runtime-host/src/agent-prompts.ts - packages/runtime-host/src/opencode-adapter.ts - packages/runtime-host/src/opencode-client-kernel.ts - packages/runtime-host/src/opencode-v2.ts - packages/runtime-host/src/permission-config.ts - packages/runtime-host/src/provider-utils.ts - packages/runtime-host/src/runtime-client-access.ts - packages/runtime-host/src/runtime-config-builder.ts - packages/runtime-host/src/runtime-managed-server-core.ts - packages/runtime-host/src/runtime-managed-server.ts - packages/runtime-host/src/runtime-node-managed-server.ts - packages/runtime-host/src/runtime-skill-verifier.ts - packages/runtime-host/src/runtime-state.ts - packages/runtime-host/src/runtime.ts - packages/runtime-host/src/session-history-loader.ts - products/gateway/src/opencode-client.ts (V2 entry at pin 1.18.1; peer allowlist + Basic auth; JOE-941 + scripts/check-durable-opencode-classic-gate.mjs) - products/gateway/src/opencode-session-runtime.ts (session I/O façade prefers client.v2.session.* with classic mock fallback; other Durable modules take DurableOpencodeClient from here and must not import @opencode-ai/sdk directly)
Native V2 Capability Gaps¶
The production boundary uses client.v2.* wherever OpenCode 1.18.1 exposes a working native route. A small classic-client allowlist remains for capabilities that the generated V2 client does not yet provide. The boundary test pins every remaining call by file, method, and count so this list cannot expand silently:
- Session actions without working native V2 routes:
session.command,session.delete,session.diff,session.fork,session.share,session.summarize,session.todo,session.unshare, andsession.update. OpenCode 1.18.1 generatesv2.session.compact, but its server implementation returnsOperationUnavailable; the classic summarizer remains the qualified route until the pinned runtime implements V2 compaction. - MCP lifecycle/authentication, which has no native V2 group:
mcp.auth.authenticate,mcp.auth.remove,mcp.connect,mcp.disconnect, andmcp.status. - Explorer operations without working V2 equivalents:
file.status,find.symbols, andfind.text. The generatedfile.readV2 method does not expose the wildcard path required by/api/fs/read/*, sofile.readremains classic until the SDK can address a file. Directory listing and file finding already usev2.fs.listandv2.fs.find. - Runtime tool discovery:
tool.list; V2 exposes agents, commands, skills, providers, and models, but not the effective tool catalog.
Remove an allowlist entry as soon as the pinned SDK exposes a working native V2 equivalent. Do not emulate these OpenCode-owned behaviors in Open Cowork.
Status on OpenCode 1.18.1 (verified): no allowlist row is burnable yet. JOE-845 decision: full classic-allowlist burn-down is Won't Do on this pin — do not invent V2 APIs. Track residuals and reopen on every OpenCode bump via opencode-classic-sdk-burndown.md.
| Gap | Why it stays classic |
|---|---|
session.summarize | Generated v2.session.compact exists but server returns OperationUnavailable. |
session.command / delete / diff / fork / share / todo / unshare / update | No working native V2 routes on the pin. |
MCP auth.* / connect / disconnect / status | No V2 MCP group. |
file.read | Generated v2.fs.read does not expose the wildcard path for /api/fs/read/*. |
file.status, find.symbols, find.text | No working V2 equivalents (v2.fs.list / v2.fs.find already cover list/find-files). |
tool.list | V2 catalogs agents/commands/skills/providers/models, not the effective tool set. |
Burn-down is gated on an OpenCode SDK/runtime bump that proves each method, then removes the exact allowlist entry in tests/opencode-sdk-boundary.test.ts and switches the call site to client.v2.*. See the residual registry and bump checklist in opencode-classic-sdk-burndown.md.
Shared Event Contract¶
The OpenCode SDK event stream is normalized once by the canonical translator in @open-cowork/shared (packages/shared/src/opencode-event-translator.ts). Desktop, Cloud, and Standalone Gateway fan out after translation only:
OpenCode SDK event
-> normalizeOpencodeEventEnvelope + classifyOpencodeSdkEvent (shared)
-> surface fan-out:
Desktop live -> SessionEngine handlers (IPC / view model)
Cloud -> opencode-runtime-adapter payload shapes
-> CloudRuntimeEvent / CloudSessionEventType
-> shared cloud projection reducer
Standalone -> translateOpencodeEventForStandalone
-> channel-safe StandaloneRuntimeEvent
-> desktop/web/gateway rendering
Rules (JOE-838):
- Do not re-parse raw SDK envelopes in surface code. Use
normalizeOpencodeEventEnvelope/normalizeRuntimeEventEnvelope. - Product kind decisions (permission.requested, tool.call, …) live in
classifyOpencodeSdkEvent. Surfaces must not invent parallel type maps. - Shared fixtures:
tests/fixtures/opencode-sdk-v2-events.jsonplustests/opencode-event-translator.test.tsandtests/opencode-sdk-event-projection.test.ts.
Cloud Channel Gateway rendering consumes @open-cowork/cloud-client session events. It should never receive SDK client objects, SDK event envelopes, or OpenCode subprocess handles. If a Cloud Channel Gateway feature needs more detail, add that detail to the shared cloud event contract and projection tests rather than importing the SDK.
Projection causality is modeled in the same shared contract, not in SDK event objects. Use createCloudProjectionFenceToken, createCloudProjectionCheckpoint, cloudProjectionFenceObserved, and createCloudAutomationEventEnvelope from packages/shared/src/cloud-session-projection.ts when a Cloud, Gateway, workflow, paired Desktop, or automation path needs to prove that a mutation has been observed by product projection. Fence identities must use exact tenant/workspace/session/workflow-run/client ids; fuzzy or suffix matching is not part of the contract.
Standalone Gateway has its own private OpenCode runtime boundary. SDK client objects and raw SDK events must stay inside apps/standalone-gateway/src/opencode.ts; the rest of the standalone app should consume normalized standalone events and durable Gateway repository records. Each channel delivery or queued job supplies a stable admission key, which the adapter maps to the native V2 prompt id. After admission, the adapter consumes v2.session.events from the returned admittedSeq rather than relying on the lossy global event tail. This preserves fast completions and crash retries without re-executing tool side effects. A bounded execution deadline interrupts the native session if no terminal event arrives.
Desktop local runtime uses the same durable admission contract:
v2.event.subscriberemains the process-level control plane (permissions, questions, untracked sessions, heartbeats).- After each local
v2.session.promptadmission, Desktop tracks that session onv2.session.eventsfromadmittedSeqand advances anaftercursor from observeddurable.seqvalues. - While a session is durable-tracked, global-stream transcript (
session.next.*, classicmessage.*) and idle terminals for that session are suppressed so two SSE tails cannot double-project. - Shared helpers live in
packages/runtime-host/src/opencode-durable-session-events.ts; the Desktop hub isapps/desktop/src/main/durable-session-events.ts.
Classic vs session.next family ownership¶
OpenCode may emit both classic message.part.* and native session.next.* for the same turn. Open Cowork receive-side ownership is:
- Prefer
session.next.*for live transcript once a message (or toolcallID) has been observed on the native family. - Classic
message.part.delta/message.part.updatedfor that message or call id is suppressed thereafter. - Classic handlers remain for history-shaped events and turns that never emit native events.
Idle multi-signal dedupe (session.status idle, session.idle, non-tool session.next.step.ended) is unchanged and lives in event-runtime-handlers.ts.
SDK v2 Upgrade Checklist¶
Use this checklist for every OpenCode SDK or opencode-ai runtime bump:
- Confirm
apps/desktop/package.jsonpins both@opencode-ai/sdkandopencode-ai, then updatepnpm-lock.yaml.packages/runtime-host/package.jsonpins both@opencode-ai/sdkandopencode-ai(it is the node + SDK runtime substrate shared by the desktop main process and the cloud server, and it resolves the bundled OpenCode CLI); keep its pins in lockstep. - Run
pnpm proof:opencode:compatibilityand resolve any registry drift before accepting the bump. - Run
pnpm typecheckfirst. SDK type drift in runtime config, client calls, or server options is treated as real drift. - Verify
packages/runtime-host/src/runtime-config-builder.tsstill emits SDK-native config for providers, agents, skills, MCPs, permissions, and model defaults. - Verify
packages/cloud-server/src/byok-runtime-config.tsstill injects BYOK provider credentials throughprovider.<id>.optionsonly. - Update SDK event fixtures for assistant message parts, tool calls, permission requests/resolutions, question requests/resolutions, todos, status, idle, and errors.
- Verify cloud event normalization maps every projection-critical SDK event into
CloudSessionEventType. - Verify permission and question round-trips still work through desktop, web, cloud worker, and gateway approval flows.
- Verify tool-call projection preserves tool id, name, status, input, output, attachments, and task-run association where the SDK provides them.
- Verify cloud worker startup still launches OpenCode through the runtime adapter and does not require Electron-only process APIs.
- Run the portability proof for runtime home/session resume assumptions:
pnpm proof:cloud:opencode-portability. - Run boundary and projection tests:
node --no-warnings --experimental-sqlite --experimental-strip-types --test tests/opencode-sdk-boundary.test.ts tests/opencode-sdk-event-projection.test.ts tests/cloud-opencode-runtime-adapter.test.ts tests/cloud-session-projection-contract.test.ts tests/gateway-package-boundary.test.ts. - Run the BYOK runtime injection tests:
node --no-warnings --experimental-sqlite --experimental-strip-types --test tests/byok-runtime-injection.test.ts tests/byok-boundary-regression.test.ts. - Run gateway renderer tests after
pnpm --filter @open-cowork/channel-gateway build:node --no-warnings --experimental-strip-types --test apps/channel-gateway/src/event-renderer.test.ts.