Skip to content

Latest commit

 

History

History
381 lines (279 loc) · 21 KB

File metadata and controls

381 lines (279 loc) · 21 KB

OCP 0.1 — OpenCode Compatibility Protocol

Date: 2026-07-19 (revised: final-product framing + one-package UX + Option B) Status: Product specification — build this; not a phased draft roadmap Semver: 0.1.x train (unstable until 1.0.0; hosts pin exact published versions) Repo: opencode-plugin-compat — this file is the canonical OCP 0.1 contract User install: ../../INSTALL.md Parent plan: ../plans/universal-opencode-plugin-compat-plan.md Evidence: ../plans/phase0-hooks-parity.md · ../plans/phase0-adr-universal-compat.md


1. Purpose

OCP defines a portable contract so plugins written against @opencode-ai/plugin (and selected SDK imports) can run on cooperating hosts:

Host ID Native plugin package OCP 0.1 target tier
opencode @opencode-ai/plugin T1–T5 (reference)
mimo @mimo-ai/plugin T1 (+ T3 if host kit embedded)
kilo @kilocode/plugin T1 (+ T3 if host kit embedded)
zcode (none — marketplace ABI) T0 only (stub / doctor)

OCP is not ownership of the @opencode-ai npm scope. Facades remapped in plugin install trees / operator overrides satisfy plugin imports without spoofing the public registry org. OCP is an external compatibility layer — hosts are read-only references.

User delivery UX (locked): one umbrella package @opencode-compat/ocp + ocp setup writes Layer A overrides and (default) Option B provider entry shims. Users then add consumer plugins via host config unchanged. Listing OCP itself in plugin / equivalent is optional bootstrap only — it does not intercept other plugins’ @opencode-ai/plugin imports by itself. Step-by-step: INSTALL.md.


2. Design goals

  1. Unchanged plugins first — minimize republish for classic npm plugins.
  2. Capability flags over silent no-ops — missing domains fail loud (except documented polyfills).
  3. HostProfile is data — adapters are thin; behavior driven by profiles.
  4. v2 is a host problem — exporting types without invoking aisdk/catalog/… is non-compliant.
  5. Honest ZCode boundary — marketplace plugins are a different protocol; do not claim OCP compliance.

Non-goals for 0.1 product: Effect v2 completeness, perfect SDK event-union identity, IDE extension bridges, ZCode marketplace↔OCP translation. Incomplete Promise domains ship as loud stubs in the same release (not deferred phases).


3. Terminology

Term Meaning
Portable plugin Imports @opencode-ai/plugin (classic and/or v2/promise) and does not hardcode fork XDG/env
Host-aware plugin (TX) Hardcodes OPENCODE_*, .opencode, or host agents — bridge should absorb where possible; residual gaps are documented, not solved by per-host plugin forks
Facade Package that satisfies @opencode-ai/plugin / sdk import paths via install override
Adapter One universal runtime (@opencode-compat/adapter) that autodetects the host (HostProfile) and maps facade calls onto that host’s native SDK. Host differences are profile data + internal dispatch — not separate packages per host
Host kit Runtime embedded by the host that implements Promise v2 domains (aisdk MVP)
Tier Declared compatibility level (T0–T5, TX)

4. Compatibility tiers

Tier Requirement
T0 Unsupported. Doctor explains why (ZCode today).
T1 Classic Hooks plugins resolve @opencode-ai/plugin (+ /tool, /tui) via facade→universal adapter (autodetect)→native. Core hooks from §6 work.
T2 Local project plugins under .opencode/plugins or host-native project dir plus optional .opencode compat expectation (docs/doctor/operator copy/symlink).
T3 Promise v2: import { define } from "@opencode-ai/plugin/v2/promise" + ctx.aisdk language/model injection works end-to-end.
T4 Additional Promise v2 domains: catalog, agent, command, skill, reference, integration (progressive).
T5 Effect v2 host alignment.
TX Host-aware risk; OCP aims to cover via path/env bridge. Do not recommend host-specific republishes of the plugin.

OCP 0.1 compliance claim = at least T1 + capability flags accurate. Advertising T3 requires aisdk conformance fixtures green.


5. HostProfile (normative sketch)

type HostId = "opencode" | "mimo" | "kilo" | "zcode" | "unknown"

type HostProfile = {
  id: HostId
  ocpVersion: string              // e.g. "0.1.0"
  nativePlugin: string
  nativeSdk: string
  upstreamPin?: string             // e.g. kilo "v1.17.4"
  paths: {
    configDir: string
    dataDir: string
    cacheDir: string
    projectDirs: string[]          // actually scanned today
    compatProjectDirs?: string[]   // matrix/doctor expectation, e.g. [".opencode"]
    pluginInstallDir?: string      // e.g. "~/.cache/kilo/packages"
  }
  configFiles: string[]
  envPrefix: string                // OPENCODE | MIMOCODE | KILO | ZCODE
  capabilities: {
    classicHooks: boolean
    promiseV2: boolean             // exports AND host wiring
    effectV2: boolean
    aisdkProviderHooks: boolean
    localPluginScan: boolean
    scansDotOpencode: boolean
    /** Host SessionProcessor creates tool parts on bare tool-call (OpenCode/Kilo). false → MiMo; OCP shim emits tool-input-start */
    streamToolCallEnsure: boolean
    /** Host bash schema requires description (MiMo). true → OCP may fill missing bash.description only */
    bashDescriptionRequired: boolean
    marketplacePlugins?: boolean   // zcode
  }
  hooks: {
    core: string[]                 // portable classic hooks implemented
    missing: string[]              // core hooks absent (compat gaps)
    extensions: string[]           // host-only hooks
  }
  /** Sparse override; omit entirely when the host matches upstream names. */
  tools?: Partial<{
    subagent: string               // default "task";      MiMo "actor"
    todoWrite: string              // default "todowrite";  MiMo "task"
    todoRead: string               // default "todoread";   MiMo "task"
  }>
  agents?: { builtins: string[]; aliases?: Record<string, string> }
}

5.1 Detection order

  1. OPENCODE_COMPAT_HOST=opencode|mimo|kilo|zcode
  2. Native binary / package identity (mimo, kilo, opencode, zcode)
  3. Config-dir heuristics
  4. zcode → refuse OCP load; print T0 doctor
  5. unknown → fail with doctor

6. Classic Hooks — OCP 0.1 core set

Portable core = intersection shaped from OpenCode tip, with gaps called out.

6.1 Required core (facade MUST accept)

From hooks parity research (OpenCode 1.18.3 / Kilo 7.4.11; see phase0-hooks-parity.md):

dispose, event, config, tool, auth, provider, chat.message, chat.params, chat.headers, permission.ask, command.execute.before, tool.execute.before, tool.execute.after, shell.env, tool.definition, experimental.chat.messages.transform, experimental.chat.system.transform, experimental.provider.small_model, experimental.session.compacting, experimental.compaction.autocontinue, experimental.text.complete

6.2 Known host gaps (must declare in hooks.missing)

Host Missing vs core Adapter policy (0.1)
MiMo 0.1.6 dispose, experimental.provider.small_model Accept on facade; no-op + compat doctor warning unless/until MiMo adds them
Kilo 7.4.11 (none at type level) Pass-through
OpenCode Reference

6.3 Host extensions (MUST NOT be required for portable plugins)

Host Extensions
MiMo actor.preStop, actor.postStop, session.pre, session.post, session.userQuery.pre, session.userQuery.post

Facades may re-export extension types under a non-portable path later (@opencode-compat/extensions/mimo); they are out of OCP 0.1 portable surface.

6.4 Module identity (Layer A)

Install override (inside host plugin cache / project plugin tree), written by ocp setup (or equivalent compat setup / printed by compat overrides):

{
  "@opencode-ai/plugin": "npm:@opencode-compat/facade-plugin@0.1.x",
  "@opencode-ai/sdk": "npm:@opencode-compat/facade-sdk@0.1.x"
}

Do not treat a host plugin list entry for OCP as a substitute for these overrides.

6.5 Provider LanguageModel adoption (Option B)

After Layer A reify, ocp setup (default --provider-shim) writes in-place entry shims in the plugin install tree so custom LanguageModel / create* providers adopt host profile policy:

Capability When false / true OCP adoption
streamToolCallEnsure false (MiMo) Emit tool-input-start before bare AI SDK tool-call
bashDescriptionRequired true (MiMo) Fill missing bash.description only
tools (role rotation) any role differs from upstream Translate the tool catalog, calls and prior turns between upstream and host vocabulary (§6.6)

The same shim also aligns provider-emitted argument keys to each call's advertised tool schema. Matching is host-independent: exact keys win, then a unique comparison after removing separators and folding case (for example, filePathfile_path). It applies recursively to objects and arrays, including local $ref and composed schemas. Unknown or ambiguous keys remain unchanged, so OCP does not guess semantic mappings such as pathfile_path. This applies equally to built-in and MCP tools because the target schema, not a host/tool name table, owns the spelling.

On hosts where both profile flags already match OpenCode (Kilo today), the wrapper still performs this schema-driven key adoption but otherwise leaves stream parts unchanged.

Shims are install-tree only (stock tarball / plugin repo untouched). The large ocp-lm-runtime.js copied beside each shim is a build artifact bundled from adapter TypeScript (src/runtime-entry.ts), not a hand-edited template string; ocp setup only copies that already-built zero-dependency artifact and fails closed if it is missing. Re-run ocp setup after plugin install/upgrade/reify. Skip with --no-provider-shim. Operator notes: docs/hosts/mimo.md §1.1, docs/hosts/kilo.md §1.1.

6.6 Tool vocabulary translation

Forks rotate builtin tool names while keeping the vocabulary. MiMo moved the subagent spawner from task to actor, then reused the freed task name for its work-item tracker — the role upstream calls todowrite / todoread, which MiMo does not have at all.

An unmodified plugin that says task therefore means spawn a subagent on OpenCode and record a todo on MiMo. Nothing type-checks differently and nothing errors until the fork's strict schema rejects the payload, so the failure surfaces as the model losing track of its own catalog rather than as a tool error.

OCP resolves builtin tools by role, never by literal name, and translates in three places:

Surface Direction MiMo
Catalog (options.tools) host → plugin actortask (flat schema); tasktodowrite + todoread
Emitted tool-call parts plugin → host taskactor; todoreadtask{list}; todowriteN × task
Prior turns (options.prompt) host → plugin fold id#0…id#N back into the one call the plugin made

Normative rules:

  1. Roles are resolved against the tools advertised with the call, not the profile's static expectation. A user may disable a builtin; translating onto a tool that is not there is worse than not translating.
  2. An explicit override never falls back to the upstream default. MiMo with actor disabled has no spawner; falling through to task would post a subagent spawn into the work-item tracker.
  3. Only declared roles are touched. Every other tool, and every subagent_type — including user-defined agents, whose enum is lifted from the host's own schema — passes through unchanged.
  4. Hosts matching upstream take an identical code path. When no role is rotated the vocabulary is undefined and no translation code runs.
  5. Fan-out state is derived from the conversation, not cached. Upstream todowrite is a positional snapshot with no ids, while the host assigns its own; OCP replays the operations it previously emitted, and the ids their results returned, out of the prompt. This is restart-safe and correct under concurrent sessions. A transition whose host id is not yet known is skipped, never guessed, and re-derived from the next snapshot.

This is the one place OCP deliberately does swap a host tool catalog. An earlier revision refused to, reasoning that the catalog is the host's to define; that was wrong, because a rotated name is precisely the incompatibility OCP exists to absorb.

Generated shims detect the live host from explicit, binary, and host-owned environment identity. If a provider worker exposes none of those, the host-specific install tree's setup-time hint is the final fallback; unknown hosts remain pass-through.

Facade classic exports:

Import Behavior
@opencode-ai/plugin Types + plugin entry re-export via adapter
@opencode-ai/plugin/tool Delegate to native ./tool
@opencode-ai/plugin/tui Delegate to native ./tui
@opencode-ai/plugin/v2/promise If capabilities.promiseV2: host kit; else throw with upgrade path
@opencode-ai/plugin/v2/effect If capabilities.effectV2; else throw

7. Promise v2 — OCP 0.1 product (T3)

7.1 Minimum API

Plugins may:

import { define } from "@opencode-ai/plugin/v2/promise"

export default define({
  id: "example.provider",
  async setup(ctx) {
    await ctx.aisdk.sdk(async (event) => {
      // event.sdk = …
    })
    await ctx.aisdk.language(async (event) => {
      // event.language = LanguageModelV3
    })
  },
})

OCP-layer host (MiMo/Kilo/OpenCode via facades):

import { wirePromiseV2 } from "@opencode-compat/ocp"

const host = wirePromiseV2()
await host.register(plugin)
const { language, sdk } = await host.resolveProvider({
  providerID,
  modelID,
  package: pkg,
})

7.2 PluginContext domains (0.1)

Domain 0.1 requirement
aisdk Required for T3language (+ ideally sdk) hooks
options Required
plugin Required enough to register/load
agent, catalog, command, skill, reference, integration Loud stub in same ship — calling unsupported registration throws with domain name

7.3 Delivery

Shared package @opencode-compat/host-promise-v2 is wired from the OCP layer via createPromiseV2Host / wirePromiseV2 (adapter/umbrella) wherever provider-resolve can be reached without modifying host source. MiMo/Kilo HostProfile capabilities set promiseV2 + aisdkProviderHooks when the OCP kit is the provider (not native fork exports). Live host processes still need an operator/sidecar call into resolveProvider — incomplete live seams do not block in-process T3 conformance. Ship the host kit with the product — do not withhold it for a later “phase.”

Operator recipe (MiMo): concrete registerresolveProvider sidecar, success criteria, and “does not feed mimo models” boundary — docs/hosts/mimo.md §3.


8. Path & env federation (Layers B/C)

8.1 Project dir scan (T2)

Precedence (native wins on conflict):

  1. Host native project dirs (.mimocode / .kilo+.kilocode / .opencode)
  2. Compat: .opencode if not already native

Kilo and MiMo do not scan .opencode today. Closing that gap is the bridge’s job (doctor honesty, docs, optional operator copy/symlink into host-native dirs). Matrix --compat-scan exercises the documented expectation.

8.2 Env bridge (opt-in)

OPENCODE_COMPAT_PATH_BRIDGE=1:

  • Read fallback: if OPENCODE_* unset, consult host prefix equivalents where mapped.
  • Do not dual-write secrets by default.
  • Document mapped keys per adapter.

9. ZCode policy (normative)

  1. HostId = "zcode"ocpVersion = "none", tier T0.
  2. Doctor text MUST state: ZCode Agent Mode marketplace (.zcode-plugin / Claude-/Codex-style) ≠ @opencode-ai/plugin.
  3. External “OpenCode” agent tile (runs OpenCode CLI / reads ~/.config/opencode) is not OCP plugin compatibility.
  4. Future Z.AI loader cooperation would mint a real adapter; until then stub only.
  5. Evidence (2026-07-19): public plugins (tmdgusya/glm-hammer, marketplace jhlee0409/zcode-glm-fleet) use .zcode-plugin/plugin.json, Claude-compatible subprocess hooks (SessionStartStop), skills/commands/MCP — see oa-tools/zcode-review/ZCODE_RESEARCH.md §7. Doctor may cite these as examples of the other ABI.
  6. Companion (optional, non-OCP): @opencode-compat/migrate-zcode / compat migrate-zcode may pack plugin-packaged skills/commands/marketplace manifests into a .zcode-plugin tree. It does not change this host’s OCP tier, does not load @opencode-ai/plugin hooks, and does not migrate host MCP from opencode.json.

10. Conformance (Layer F) — required for any “supports OCP 0.1” claim

Fixture ID Tier Assert
classic.auth.oauth-shape T1 Auth hook types load; host invokes authorize path smoke
classic.config-mutate T1 config hook runs
classic.tool-before-after T1 tool execute hooks
classic.chat-params T1 chat.params mutates options
alias.resolve-plugin T1 @opencode-ai/plugin resolves via override
local.dot-opencode-scan T2 skipped if scansDotOpencode=false and no compat PR
v2.aisdk.language T3 LanguageModelV3 injection
v2.unsupported-domain T3 loud error
zcode.t0-doctor T0 doctor message only

Publish a public matrix: Plugin × Host × Tier × last pass. All fixtures above are part of the product ship, not a later test phase.


11. Package map (product packages)

Package Role
@opencode-compat/ocp Umbrella UX (user-facing): one install + ocp setup → Layer A overrides; depends on / re-exports bridge packages
@opencode-compat/profile HostProfile types + shipped drafts (opencode / mimo / kilo / zcode) + detect()
@opencode-compat/facade-plugin Install-override stand-in for @opencode-ai/plugin
@opencode-compat/facade-sdk Stand-in for @opencode-ai/sdk (minimal surface)
@opencode-compat/adapter Universal host adapter — one runtime; autodetect + dispatch (ZCode → T0 doctor)
@opencode-compat/host-promise-v2 Shared aisdk host kit
@opencode-compat/cli compat doctor + matrix runner + setup/overrides entry + migrate-zcode companion CLI
@opencode-compat/migrate-zcode Companion library: plugin-package → .zcode-plugin (not OCP; no host MCP)

User-facing default: @opencode-compat/ocp. Other @opencode-compat/* bridge packages are implementation detail / transitive deps unless an operator pins them deliberately.

Not shipped: @opencode-compat/adapter-{opencode,mimo,kilo,zcode} — host variance is data inside profile + adapter, not separate publishable packages.

Repo home: opencode-plugin-compat (see ADR).


12. Versioning policy

  • 0.x — product; hosts pin exact versions.
  • Bump minor when adding optional domains/flags without breaking T1.
  • Bump major (toward 1.0) when core hook set or T1 semantics break.
  • Capability flags are part of the profile, not silent behavior changes.

13. Open implementation details (defaults while building)

These are not phase gates. Use defaults below unless evidence forces a change:

  1. Minimal @opencode-ai/sdk facade — start from methods used by sample classic plugins + Cursor provider; expand via fixtures.
  2. MiMo gaps (dispose, small_model, .opencode paths) — ship no-op gaps + doctor; path gaps via docs/doctor/operator copy-symlink.
  3. Kilo .opencode scan — hosts do not scan it today; document copy/symlink into .kilo / .kilocode and keep matrix --compat-scan honest.
  4. facade-sdk depth — close with T3+path smoke on unchanged plugins (ADR-6); no dual-package escape hatch.
  5. Naming@opencode-compat/* (preferred over @ocp/*).

14. Change log

Date Change
2026-07-19 Initial 0.1 draft from discovery
2026-07-19 Reframed as final product spec (no phased delivery)
2026-07-19 Locked user UX: @opencode-compat/ocp + ocp setup (Layer A); plugin list entry alone is not enough
2026-07-20 Documented Option B provider LanguageModel adoption (streamToolCallEnsure / bashDescriptionRequired); linked INSTALL.md; framed train as 0.1.x