anomalyco / anomalyco/opencode

v1 file-plugin loader never falls back to legacy named exports when a v2 default export is present

Open
#50,140 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
209k
Forks
27.5k
Avg merge
7h 2m
Merged PRs (30d)
384

Description

v1 file-plugin loader never falls back to legacy named exports when a v2 default export is present

Version: 1.18.31 (desktop AppImage; same core as CLI)
Area: packages/opencode/src/plugin/{index,shared}.ts — external file-plugin loading

What happens

A file plugin that carries both contracts — a named hook-map export (v1) and a default { id, setup } export (v2), like the official dcg-guard.js generated by dcg install --opencode — is silently skipped on the v1 runtime. Its tool.execute.before hook never fires. For a security guard this means protection is down with zero visible signal in the app (the failure is only an Effect log line).

Repro (live A/B, same hook function)
  • ~/.config/opencode/plugins/dcg-guard.js (official, dual-shape): git reset --hard in a scratch repo executed (data loss in scratch, block missed).
  • ~/.config/opencode/plugins/dcg-v1-shim.js containing only export const server = DcgGuard re-exported from the same file (same function reference, no default export): identical git reset --hard aborted with dcg's block message, worktree intact.

The only difference between the two cases is the module shape, so the loader — not the hook — is at fault.

Root cause

applyPlugin (plugin/index.ts) calls readV1Plugin(mod, spec, "server", "detect") first. With a default export of { id, setup } (has id, no server), readV1Plugin (plugin/shared.ts) does not bail out in detect mode — it falls through to:

if (kind === "server" && server === undefined) {
  throw new TypeError(`Plugin ${spec} must default export an object with server()`)
}

The throw rejects applyPlugin, the caller logs failed to load plugin and continues, and the legacy path (getLegacyPlugins, which would have picked up the named DcgGuard export) is never reached — it sits behind the if (plugin) branch that threw.

Notably the docs say a plugin "exports one or more plugin functions" and named-only modules (no default export) do load fine through the legacy path — so dual-shape modules are strictly worse than v1-only ones, which contradicts the documented contract.

Proposed patch

In detect mode, a non-matching default export should mean "not a v1 plugin", not an error — let the legacy path try:

--- a/packages/opencode/src/plugin/shared.ts
+++ b/packages/opencode/src/plugin/shared.ts
@@ readV1Plugin
   if (kind === "server" && server === undefined) {
+    if (mode === "detect") return
     throw new TypeError(`Plugin ${spec} must default export an object with server()`)
   }
   if (kind === "tui" && tui === undefined) {
+    if (mode === "detect") return
     throw new TypeError(`Plugin ${spec} must default export an object with tui()`)
   }

Strict mode (explicit v1 plugins) keeps throwing; only auto-detection falls back. Happy to PR this + a loader test if the approach looks right.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in packages/opencode/src/plugin/shared.ts at readV1Plugin and trace its detect-mode callers in packages/opencode/src/plugin/index.ts, especially applyPlugin and getLegacyPlugins. Add coverage for a dual-shape module so detection reaches the named legacy export, while strict v1 loading still errors; verify the legacy hook runs and the module is not silently skipped.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
tooling
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.