anomalyco / anomalyco/opencode
v1 file-plugin loader never falls back to legacy named exports when a v2 default export is present
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 --hardin a scratch repo executed (data loss in scratch, block missed).~/.config/opencode/plugins/dcg-v1-shim.jscontaining onlyexport const server = DcgGuardre-exported from the same file (same function reference, no default export): identicalgit reset --hardaborted 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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