Skip to content

feat(plugin): add OpenCode compatibility host - #25

Open
yan-ad wants to merge 12 commits into
Blankeos:mainfrom
yan-ad:feat/plugin-compatibility
Open

yan-ad wants to merge 12 commits into
Blankeos:mainfrom
yan-ad:feat/plugin-compatibility

Conversation

@yan-ad

@yan-ad yan-ad commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • add OpenCode-compatible plugin configuration and local discovery from .opencode/plugin and .opencode/plugins
  • add a persistent Bun sidecar with a versioned JSON-lines RPC boundary for plugin loading and hook invocation
  • isolate plugin failures, timeouts, stdout, and process lifecycle from the Rust application
  • add regression coverage for hook ordering, options, no-op hooks, errors, timeouts, process exits, stdout isolation, and cleanup

Closes #24 once the remaining trust and production hook integration work is completed.

Current scope

This is intentionally opened as a draft. The host lifecycle is opt-in through:

CRABCODE_ENABLE_OPENCODE_PLUGINS=1 crabcode

The draft establishes the compatibility/runtime foundation and an end-to-end generic hook path. Before marking it ready, I plan to align with maintainer feedback on:

  • workspace plugin trust/confirmation UX
  • which OpenCode hooks should be wired first (Ponytail is the motivating target)
  • package plugin installation/runtime distribution policy
  • documentation and configuration contract

Architecture

Crabcode remains responsible for orchestration, permissions, UI, and process supervision. OpenCode-style Node plugins run in one persistent Bun sidecar per application lifecycle. The sidecar is loaded once and communicates with Rust over versioned JSON-lines RPC on stdio.

This avoids spawning a runtime per hook while keeping JavaScript crashes and hangs outside the Rust process.

Validation

  • cargo fmt --all -- --check
  • cargo check
  • cargo test — 1,155 passed, 0 failed
  • complete parallel suite repeated 3 consecutive times to verify race-free behavior
  • cargo test config::configuration::tests:: -- --test-threads=1 — 29 passed
  • cargo test --test plugin_host_regression -- --test-threads=1 — 6 passed
  • cargo clippy --tests — succeeds (existing repository warnings remain)
  • cargo clippy --test plugin_host_regression -- -D warnings

Full-suite stability

The draft also removes pre-existing test nondeterminism encountered while validating the adapter:

  • serializes tests sharing the Ollama singleton cache
  • avoids process-wide current-directory mutation
  • uses built-in theme fixtures instead of missing relative files
  • avoids provider IDs that are overridden by runtime catalog extensions
  • aligns chat rendering assertions with the current renderer

The complete local suite is now fully green and remained green across three consecutive parallel runs.

Refs #24

yan-ad added 5 commits August 13, 2026 15:59
Introduce an opt-in Bun sidecar for discovering, loading, and invoking OpenCode-style plugins while keeping failures isolated from the Rust application. Add regression coverage for ordering, errors, timeouts, stdout isolation, and process cleanup.\n\nRefs Blankeos#24
…bility

# Conflicts:
#	src/ui/components/chat.rs
@yan-ad
yan-ad marked this pull request as ready for review August 18, 2026 06:19
@yan-ad

yan-ad commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author
image image

Hi @Blankeos i need your help to try out this PR, since it's successfully load opencode plugin like ponytail in my local build

@Blankeos

Blankeos commented Aug 27, 2026 •

Copy link
Copy Markdown
Owner

Thanks @yan-ad will definitely review it this week, thank you!

Update (Sept4, 2026): Sorry it's taking some time, scope is kind of big and decisive, so I'm still planning some more.

@yan-ad

yan-ad commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

Resolved conflicts with upstream main (bf5c111) in merge commit 312caff.

  • Preserved opt-in Bun plugin startup/shutdown and plugin configuration hydration.
  • Retained upstream TUI runtime options, including --yolo / --dangerously-skip-permissions.
  • Removed the obsolete duplicate discovery assignment after upstream centralized runtime application.

Validation:

  • cargo fmt --all -- --check, git diff --check, and cargo check: passed.
  • Plugin host regression target: 8 passed (Bun available).
  • Configuration tests: 56 passed; runtime tests: 6 passed.
  • Serial unit suite: 1,848 passed, 3 ignored, 0 failed.
  • ACP integration target: 6 passed, 1 failed (advertises_and_dispatches_commands_over_stdio). The same failure reproduces on unmodified upstream bf5c111 in a separate worktree.
  • Default parallel suite: 1,843 passed, 3 ignored, 5 failed; those five unit failures disappear with --test-threads=1.

GitHub now reports this PR as MERGEABLE / CLEAN. This update resolves merge conflicts; it does not expand the existing plugin trust or production-hook scope.

@Blankeos

Blankeos commented Nov 7, 2026

Copy link
Copy Markdown
Owner

feat/plugin-compatibility → main
Model: claude-sonnet-5-5, effort unknown

Confidence: 2/5

It builds, the new tests pass, and it merges cleanly. But I ran it end to end and found that the installer and the loader don't connect, one bad plugin disables all plugins, and the new hook API is never called by the app. It also rewrites the user's shared OpenCode config and has no trust model yet.

Checks run (worktree of PR head 312caff, separate target dir):

  • cargo fmt --check: clean.
  • cargo build: OK.
  • cargo test --no-fail-fast (whole suite, run in parallel with 7 other review builds on the same machine):
    • lib/bin: 1846 passed, 2 failed (model::discovery::tests::custom_endpoint_failure_preserves_manual_models_and_is_cached and acp::service::tests::close_reopen_waits_for_old_prompt_finalization).
    • acp_stdio: 7 passed. plugin_host_regression (needs Bun, installed here): 8 passed.
    • The first discovery failure was left: 2, right: 1. That test, and the other Discovery tests, share /tmp/crabcode_test_cache, which the other concurrent reviews on this machine can also touch. I re-ran the discovery test alone twice and it passed.
    • So I can't call the "1,155 green, 3x stable" claim confirmed. The "suite stabilised" part of the PR may still be racy, and I couldn't tell apart a real race and machine contention.
  • cargo clippy --all-targets: no warnings on lines this PR adds. The warnings in configuration.rs and main.rs are in old lines.
  • Mergeable: yes. GitHub says CLEAN, and git merge-tree against current origin/main (4c08ae6, 14 commits ahead of the PR base) reports no conflicts. I did not resolve or rebase anything.
  • Overlap with other open PRs:
  • Exercised for real, with XDG_* pointed at /tmp/pr25-demo, CRABCODE_TEST_MODE=1 and CRABCODE_ENABLE_OPENCODE_PLUGINS=1. I used a fake bun add that does nothing, so there were no network calls.
    1. crabcode plugin @scope/demo@latest against an opencode.jsonc containing // and /* */ comments, a trailing comma, and a provider block. It added the plugin, but the file came back as plain pretty-printed JSON and all comments were lost. That is the user's shared global OpenCode config, so OpenCode users would lose their annotations.
    2. Path traversal: crabcode plugin @a/../../../x passes the validator (only a leading - and non-@ slashes are rejected). It created $XDG_CACHE_HOME/x, outside the opencode/packages cache, and then edited the config.
    3. Workspace plugin: I put .opencode/plugins/hello.mjs in a scratch project and started the TUI with the env flag. The Bun sidecar started, the plugin ran immediately (it wrote a marker file) and the process was cleaned up on exit. There is no trust or confirmation step for project plugins (the PR body lists this as TODO).
    4. A package installed the way the installer lays it out (~/.cache/opencode/packages/@scope/demo@latest/node_modules/@scope/demo) can't be loaded. Bun.resolveSync(source, workspace) only resolves from the project dir, and the spec still contains @latest. The sidecar answered Cannot find package '@scope/demo@latest'. So crabcode plugin X followed by starting the app doesn't load X.
    5. One unresolvable entry in plugin aborts the whole load_plugins call. main.rs then shuts the host down and loads nothing, so a config with a missing package plus a good local plugin loaded neither. The only trace is a startup diagnostic.
    6. In a plain grep, invoke_hook is only called from tests. In the app, nothing ever calls a hook, so a loaded plugin can't influence behaviour yet. Only the .opencode/command discovery part is live.
  • Screenshots: not applicable. I did run the TUI, and it starts normally with the flag, but nothing in the TUI surfaces plugin state, so a capture would only show the normal home screen.

Confidence bumps:

  • A real Ponytail / npm plugin run needs network access to npm. Not done. If it loads and its commands show up in the TUI, that raises this to about 3/5.
  • Real OpenCode compatibility (that OpenCode accepts the rewritten opencode.jsonc and shares ~/.cache/opencode/packages) needs OpenCode installed. Not tested.
  • Windows and Linux were not tested (Bun path and cache dir behaviour).

Summary

This adds opt-in OpenCode plugin support. Behind CRABCODE_ENABLE_OPENCODE_PLUGINS=1:

  • plugin is read from config, and .opencode/plugin(s) files are discovered.
  • A persistent Bun sidecar loads the plugins and talks JSON-lines RPC over stdio.
  • Plugin packages can ship slash commands from .opencode/command.
  • crabcode plugin <module> runs bun add and appends to the global OpenCode config.
  • The PR also fixes several flaky tests (cwd mutation, Ollama cache lock, fixtures).

The sidecar protocol and its regression tests are solid. The Rust side handles timeouts, hung plugins, exit mid-call and stdout isolation, and the tests prove those.

The integration around it is not finished. Because it is a draft-style foundation, I'd treat it as an RFC rather than something to merge now.

Map

flowchart TD
  CLI["main.rs: 'plugin' subcommand + opt-in startup/shutdown"] --> INST["plugin/installer.rs: bun add + rewrite global opencode.jsonc"]
  CLI --> HOST["plugin/mod.rs: PluginHost (JSON-lines RPC, timeouts)"]
  HOST --> SIDE["plugin/sidecar.mjs: Bun sidecar loads plugins"]
  HOST --> PROTO["plugin/protocol.rs"]
  CFG["config/configuration.rs: plugin key, .opencode/plugin(s) discovery, plugin-shipped commands"] --> CLI
  CFG --> APP["app.rs: plugin_specs, project_root"]
  APP --> CLI
  TESTS["tests/plugin_host_regression.rs"] -.-> HOST
  FLAKY["test-only fixes: discovery, ollama, chat, question_dialog, mutation, remote"]
Loading

Start at src/plugin/mod.rs and sidecar.mjs, then the main.rs startup block.

Regressions

  • With the flag off (the default), the runtime behaviour is unchanged. The plugin key moved from "ignored" to "allowed", so OpenCode configs that contain plugin no longer produce the "ignored key" diagnostic, and plugin-shipped command directories are now scanned on every start. That last part runs even with the flag off, and it loads commands from node_modules/<plugin>/.opencode.
  • crabcode plugin rewrites a file shared with OpenCode, and rewriting it drops the user's comments (see check 1). That is a user-visible regression risk for OpenCode users.
  • If any open PR adds startup code in main.rs (feat(cli): add --auto permission mode #69, feat(auth): add Google Antigravity browser OAuth #70), expect a manual merge there.

Migrations

None needed. No DB, schema or config-format changes, but the PR adds a runtime dependency on Bun. The installer shells out to bun add, and without Bun it fails with an error.

Before merging

  • Make the installer and loader agree. Resolve packages from ~/.cache/opencode/packages/<name> (strip the version suffix), and add a test that installs, then loads, a package.
  • Load plugins one at a time, so one failing plugin only warns. Also surface that warning to the user.
  • Preserve comments when editing opencode.jsonc (or don't touch the shared OpenCode config; use a crabcode-owned file). Reject .. and anything else not matching npm package-name syntax in package_directory.
  • Add a trust prompt for project-local plugins before executing them, as the PR itself planned. Today the flag is the only gate.
  • Wire at least one real hook, or say clearly that this PR only makes plugin-shipped commands work.
  • Decide whether DEFAULT_TIMEOUT = 5s is also fine for the initial load_plugins (first import of a big package can be slower than that, and a timeout kills the host).
  • Split the unrelated test-stabilisation commits out, or confirm the full suite is green on a clean machine. I got two flaky failures in parallel (see Checks).
  • Rebase onto current main (14 commits behind), and coordinate with feat(auth): add Google Antigravity browser OAuth #70 and feat(cli): add --auto permission mode #69, which touch main.rs.

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.

[Feature] Add plugin system support

2 participants