feat: add OpenCode v2 (opencode2) as a first-class client - #4
feat: add OpenCode v2 (opencode2) as a first-class client#4DatScreamer wants to merge 8 commits into
Conversation
Install a distinct plugin (interlinked-opencode2) so v1 and v2 can both be enabled without clobbering .opencode/plugins/interlinked.ts or sharing a plugin id. v2 talks to the local harness socket; v1 keeps the managed hook-entry bridge.
The user plugin path is ~/.config/opencode/plugins/interlinked-opencode2.ts. The previous .js rename was a stale-service artifact; plugin list after restarting opencode2 loads the TypeScript plugin.
There was a problem hiding this comment.
🟡 Changes recommended
The OpenCode v2 cold-fallback deletion guard is bypassable (rm -fr /), and the generated plugin does not emit the documented session.deleted/SessionEnd event.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR adds OpenCode v2 support (opencode2) as a first-class client/runner across the Interlinked CLI and harness, including a dedicated plugin source, adapter integration, install/uninstall plumbing, and documentation/help-contract updates.
Changes:
- Introduces an OpenCode v2 tool-name mapping + cold-fallback blocking helpers, and a self-contained plugin generator written to
.opencode/plugins/interlinked-opencode2.ts. - Adds a new harness runner adapter (
opencode2) and wires it through runner detection, unified event typing, installer manifests, and CLI commands. - Updates CLI help text, environment docs, skills installer templates, and existing tests/contracts to include
opencode2.
File summaries
| File | Description |
|---|---|
| src/registrars/setup.ts | Updates --clients/--runner help text to include opencode2. |
| src/registrars/setup.mutation-kill.test.ts | Updates help-contract expectations for opencode2. |
| src/lib/viz/agent-roster.ts | Adds opencode2 to recognized runner families for display/roster logic. |
| src/lib/skill-installers.test.ts | Ensures opencode2 skill installs stay under strict description limits. |
| src/lib/skill-install-templates.ts | Adds opencode2 to short-description runner set + targets .opencode skills path. |
| src/lib/skill-install-templates.test.ts | Tests runnerTargets() for opencode2. |
| src/lib/skill-install-ownership.ts | Maps opencode2 skill root to .opencode/skills. |
| src/lib/settings.ts | Adds opencode2 client config + runner mapping + env detection split from v1. |
| src/lib/opencode-tool-map.ts | New: OpenCode v2 tool-name → harness tool mapping + cold-fallback block reasons. |
| src/lib/opencode-tool-map.test.ts | New: unit tests for tool mapping + cold-fallback blocking. |
| src/lib/opencode-runtime.ts | New: shared v1/v2 environment detection helper. |
| src/lib/opencode-plugin-source.ts | New: generator for self-contained OpenCode v2 plugin source. |
| src/lib/hooks.ts | Registers opencode2 install entry and uninstall hook wiring. |
| src/lib/hooks-template.ts | Adds opencode2 detection/normalization path in hook template. |
| src/lib/hook-types.ts | Adds CLIENT_OPENCODE2 constant. |
| src/lib/hook-template-chunks/collection-writer.ts | Treats opencode2 as a direct provider runner in collection writing. |
| src/lib/hook-installers.ts | Re-exports opencode2 installer/uninstaller APIs and events list. |
| src/lib/hook-installers-opencode.ts | New: install/uninstall logic for the OpenCode v2 plugin file(s). |
| src/lib/hook-installers-opencode.test.ts | New: tests plugin install/uninstall behavior and ownership detection. |
| src/lib/collection/builder.ts | Includes opencode2 in direct-provider runner set for collection building. |
| src/harness/unified-event.ts | Extends RunnerId union with opencode2. |
| src/harness/types/events.ts | Extends AgentSource union with opencode2. |
| src/harness/legacy-client.ts | Maps runner opencode2 to legacy agent_source. |
| src/harness/legacy-client.test.ts | Tests legacy mapping for opencode2. |
| src/harness/interlinked-config.ts | Adds opencode2 to valid runner list for config validation. |
| src/harness/installer-manifest.ts | Adds opencode2 to valid installer-manifest runner set. |
| src/harness/adapters/types.ts | Adds postUninstall hook for adapters needing non-JSON cleanup (used by opencode2). |
| src/harness/adapters/README.md | Documents opencode2 adapter surface/capabilities in the adapter matrix. |
| src/harness/adapters/opencode2.ts | New: opencode2 runner adapter implementation. |
| src/harness/adapters/opencode2.test.ts | New: tests opencode2 adapter identity, env detection, parsing, decisions. |
| src/harness/adapters/opencode.ts | Ensures v1 OpenCode adapter does not claim v2 environments; adds v2 no-op guard in generated v1 plugin. |
| src/harness/adapters/opencode.test.ts | Tests the v1 adapter/plugin no-ops when running under OpenCode v2. |
| src/harness/adapters/index.ts | Wires opencode2 adapter into registry + detection ordering. |
| src/harness/adapters/index.test.ts | Updates adapter-count and detection tests to include opencode2. |
| src/commands/uninstall-hooks.ts | Adds opencode2 to uninstall-hooks valid runner list. |
| src/commands/install-hooks.ts | Adds opencode2 to install-hooks valid runner list. |
| src/commands/install-hooks.test.ts | Updates installer-manifest entry count for new runner. |
| src/commands/install-hooks.mutation-kill-w44.test.ts | Updates mutation-kill runner parsing fixtures to include opencode2. |
| src/commands/env.ts | Updates env-doc example client list to include opencode2. |
| src/commands/env.mutation-kill-w56.test.ts | Updates env-doc example expectation for opencode2. |
| src/commands/enable.ts | Adds opencode2 client support + destination info + restart notes. |
| src/commands/enable.test.ts | Updates enable command tests to include opencode2 in supported clients and output. |
| src/commands/enable.mutation-kill-w44.test.ts | Updates mutation-kill output expectation to include opencode2. |
| src/commands/disable.ts | Adds opencode2 to disable command client list. |
| src/commands/disable.test.ts | Updates disable tests to include opencode2. |
| skills/interlinked-setup/SKILL.md | Updates setup skill docs to mention opencode2 flags and install paths. |
| AGENTS.md | Updates supported clients list to include opencode2 and documents v1/v2 plugin paths. |
Review details
- Files reviewed: 47/47 changed files
- Comments generated: 5
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Cold fallback now matches rm -rf and rm -fr of / or ~. The generated plugin hooks session.deleted as SessionEnd. User plugin path in the setup skill matches ~/.config/opencode/plugins/interlinked-opencode2.ts.
…k paths Cross-runner contract: every adapter's installed events must appear in the settings fragment or plugin source; project_hook_path must match the fragment; skill hook-file paths must resolve to an adapter; every cold rm detector must block both -rf and -fr. Wire OpenCode v2 into the capability catalog so that list cannot drift again.
QuentinCody
left a comment
There was a problem hiding this comment.
Request changes
I reviewed the complete PR again at current head aad46e3, including an uncommitted test merge into current main. The latest commit usefully fixes the previously missing RunnerAdapter.capabilities catalog and reduces the installed-event list to the five provider-native callbacks. The following issues remain.
Release blockers
-
The PR still does not compile against current
main.src/harness/types/events.tsadds"opencode2"toAgentSource, butPROVIDER_BY_SOURCE: Record<AgentSource, string>insrc/harness/server/agent-event-capture.ts:69has noopencode2key. Both TypeScript compilers reject the effective merge with TS2741. Please add the attribution and an exhaustive parity pin. -
File writes lose their write-gate classification on the hook-entry path.
src/harness/adapters/opencode2.tsmaps provider tools to Claude names and then storestool_name: toolName.toLowerCase()(Editbecomesedit,Writebecomeswrite). The hook-entry normalization only restores Claude casing for Claude/Codex, and the daemon's file-write list does not contain these lowercase names. An OpenCode v2 edit can consequently bypass file-write gates. The existing test pins the wrong lowercase value. Preserve the canonical mapped tool name and add a composed Edit/Write gate test. -
Malformed or incomplete daemon responses fail open.
In
src/lib/opencode-plugin-source.ts, JSON parse failure and socket close before a complete response resolve{ decision: "allow" }, while no-socket, timeout, and connect-error paths invokecoldBlock. A destructive command during daemon restart/churn can therefore bypass the cold guard merely through malformed/empty response bytes. Route every invalid/incomplete response through the same unavailable-runtime fallback and pin malformed JSON plus EOF-before-newline. -
The v1 bridge can incorrectly disable itself through a loose substring heuristic.
src/harness/adapters/opencode.tsnow returns{}wheneverinterlinkedIsOpenCodeV2()sees"opencode2"in any argv element or"opencode-v2"anywhere inXDG_CONFIG_HOME. A normal v1 invocation whose argument/path happens to contain that text silently loses all guarding. Runtime selection needs a provider-owned, exact identity signal; arbitrary argv/path substrings cannot be a security boundary. -
The OpenCode v2 installer bypasses scope, ownership, rollback, and manifest guarantees.
renderSettingsFragment()already provides one scope-resolved managedfileContent, butpostInstalladditionally callsinstallOpencode2Hooks(). That helper ignoresoptions.scope, writes both project and user plugin files with barewriteFileSync, and removes marker-matching files withrmSync. Consequences include:- a project-scoped install writes into
$HOME; - a user-scoped install creates an untracked project artifact;
- foreign files can be overwritten or deleted based only on marker text;
- refresh snapshots and semantic verification do not cover the extra artifact;
- tests can write into the developer's real OpenCode configuration;
- disabling one repository can remove a global plugin used by others.
postUninstallis also dead: the manifest uninstaller never invokes it, so uninstall can remove the recorded artifact while leaving the second plugin active. Make the existing managedfileContentthe sole install artifact. Handle genuine legacy cleanup explicitly through hash/manifest-aware, transactional migration. - a project-scoped install writes into
-
Advertised lifecycle callbacks are not registered through the documented V2 lifecycle API.
src/lib/opencode-plugin-source.tscallsctx.session.hook("created"|"deleted"|"idle", ...), but the documented V2 session-hook surface does not expose those lifecycle names; lifecycle events use the event subscription domain. SessionStart, SessionEnd, and Stop are therefore not proven to reach the daemon, despite being advertised. Register through the actual V2 event API or remove those claims until implemented, then drive all three through a contract-faithful host test. -
PostToolUse checks run for every tool and their results are discarded.
Every
tool.execute.after—including read, grep, glob, and web fetch—performs a full daemon round-trip of up to five seconds. That violates this repository's mutating-only PostToolUse contract and recreates the hook noise/load problem.gateAfter()then ignores the response entirely, so warnings andadditional_contextnever reach the agent even thoughadapters/README.mdclaims OpenCode v2 can append findings to tool output. Filter to the canonical mutating tool set and surface non-blocking findings through a supported V2 model/user-visible channel. -
Normal discovery selects both OpenCode clients.
opencodeandopencode2share.opencodeasconfigDir, whiledetectClients()treats directory existence as sufficient before environment discrimination. Any repository with.opencode/therefore detects both and bareinterlinked enableinstalls both plugins. Require an unambiguous v2 signal or explicit--clients opencode2, and pin mutual exclusion.
Additional correctness and contract gaps
-
User scope ignores
XDG_CONFIG_HOME.getOpencodePluginPath("user")hardcodes$HOME/.config/opencode/plugins, while the v2 detection/tests recognize an alternate XDG config home. Centralize the provider path derivation, honorXDG_CONFIG_HOME, and bind that exact path in the manifest. -
Roster classification makes
opencode2unreachable.src/lib/viz/agent-roster.tsusesRUNNERS.find(r => lower.includes(r))withopencodebeforeopencode2; every v2 name matches v1 first. Use exact/token-aware matching or longest-first ordering, with a v2 regression pin. -
The generated plugin is not executed by the tests. Current tests largely check source substrings/file existence. Even the new install-contract test searches a haystack for event-name text; it does not prove the provider registered a callable hook or that an event reached the socket. Load the generated source against a contract-faithful fake V2 host and drive before, after, lifecycle, block, warning, malformed-response, and unavailable-daemon cases. Add
opencode2to the cross-runner semantic-equivalence suite using a real provider payload. -
Canonical documentation remains incomplete.
docs/architecture.mdstill omits the new client/support status. The setup skill and adapter matrix should consistently identify this as experimental until the provider contract and real-host acceptance tests pass. -
Payload-consumption bookkeeping was not updated. The new payload readers should be reflected in
CONSUMED_PAYLOAD_KEYSin the same change, per the repository's payload-contract rule. -
The self-contained plugin duplicates mapping/runtime functions by hand. This has already allowed source and installed-runtime behavior to diverge. Prefer the existing
.toString()/shared-chunk generation pattern or add byte/behavior parity tests that make drift impossible. -
Formatting:
git diff --checkstill reports an extra blank line at EOF insrc/lib/opencode-tool-map.ts.
Verification
At current PR head aad46e3, merged without committing into current main:
npm run typecheck— fails, TS2741 atsrc/harness/server/agent-event-capture.ts:69.- The earlier missing-capabilities compiler/test failure is resolved by the latest commit.
- The earlier 10-vs-5 installed-event count is also resolved by deriving native events from
OPENCODE2_CAPABILITIES. - A full suite is not meaningful until the remaining compile failure is fixed.
The overall wiring direction is reasonable and the latest author follow-up improved capability parity, but the guard, installer, and provider-runtime contracts above need to be closed before this can safely merge.
Keep Edit/Write casing so write gates fire. Fail closed on malformed or truncated daemon replies. Identify v2 only via OPENCODE2 / INTERLINKED_CLIENT. Install a single managed plugin file (no dual home write). Subscribe to session lifecycle through event.subscribe. PostToolUse only on mutating tools and append warnings. Detect v2 only with an explicit signal. Honor XDG_CONFIG_HOME. Match opencode2 in the roster by longest token. Execute the generated plugin against a fake host. Document experimental status, CONSUMED_PAYLOAD_KEYS, and PROVIDER_BY_SOURCE opencode2.
A new adapter must ship a native Write fixture that isFileWrite accepts, an exhaustive AgentSource provider map, blank-env non-detection, and a project-relative managed plugin path. Shared .opencode/ must not detect both OpenCode clients. Register lowercase write/edit spellings in the write-tool table so OpenCode/Cursor casing cannot skip the file-write list.
Every adapter must: not treat ask as allow when it has no prompt primitive; keep project-scope managed plugins out of \$HOME; appear in architecture.md by hook path; map AgentSource tokens in the roster; and list Write-fixture keys in CONSUMED_PAYLOAD_KEYS. OpenCode v2 additionally cold-blocks when the daemon closes before a complete response line.
Claude and Cursor still omit client_runner (Cursor uses cursor_version). detectClients / enable / disable tests include the opencode2 client id.
Closes #3
Adds OpenCode v2 (
opencode2) as a first-class client, distinct from OpenCode v1 (opencode).opencode2.opencode/plugins/interlinked-opencode2.ts(TypeScript; also~/.config/opencode/plugins/interlinked-opencode2.ts)interlinked enable --clients opencode2Verified live with Muse Spark 1.2 Free: Write of a 40-branch
probeBadwas blocked by[interlinked:cyclomatic](41 vs cap 22) after restarting the opencode2 service so the.tsplugin loaded.