From d0a731f8cfe6180f7c82c0315061886da3f6de3b Mon Sep 17 00:00:00 2001 From: Tehan Date: Tue, 23 Jun 2026 22:06:58 +0200 Subject: [PATCH] =?UTF-8?q?fix(plugin):=20move=20buildHiddenAgentConfig=20?= =?UTF-8?q?out=20of=20the=20entry=20module=20=E2=80=94=20opencode=201.17?= =?UTF-8?q?=20invokes=20every=20entry=20export=20as=20a=20plugin=20factory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit opencode 1.17 calls every exported function in a plugin's entry module (index.ts) as its own plugin factory: fn(ctx). The helper buildHiddenAgentConfig was exported from index.ts (introduced upstream in 29a49cb6 'D19 resilience safe parts'), so opencode invoked it as buildHiddenAgentConfig(ctx) — passing the plugin context as `prompt` and undefined as `allowedTools` — which threw 'undefined is not an object (evaluating allowedTools)' during plugin load. Result: no hooks/tools registered, all ctx_* tools dead, empty MC runtime log, migration churn each boot. The entry module must export ONLY default (the plugin factory). Move the helper to a sibling module (hidden-agent-config.ts) where it can still be exported and unit-tested without being mis-invoked as a factory. index-refresh.test.ts now imports it from there. Verified: dist export keys = ['default']; full plugin suite green (the lone tui-config ordering flake is pre-existing on master); tsc + lint clean. --- packages/plugin/src/hidden-agent-config.ts | 51 ++++++++++++++++++++++ packages/plugin/src/index-refresh.test.ts | 2 +- packages/plugin/src/index.ts | 43 +----------------- 3 files changed, 53 insertions(+), 43 deletions(-) create mode 100644 packages/plugin/src/hidden-agent-config.ts diff --git a/packages/plugin/src/hidden-agent-config.ts b/packages/plugin/src/hidden-agent-config.ts new file mode 100644 index 000000000..c74a0916a --- /dev/null +++ b/packages/plugin/src/hidden-agent-config.ts @@ -0,0 +1,51 @@ +import { buildAllowOnlyPermission } from "./agents/permissions"; + +/** + * Build a hidden-agent config with a deny-everything-by-default permission + * baseline and a hard tool-iteration ceiling. User overrides may lower + * `steps`/`maxSteps`, but cannot raise either above the built-in cap. + * + * Lives in its own module — NOT in the plugin entry (`index.ts`) — because + * opencode 1.17 invokes EVERY exported function in a plugin's entry module as + * its own plugin factory. Exporting this helper from `index.ts` made opencode + * call it as `buildHiddenAgentConfig(ctx)`, passing the plugin context as + * `prompt` and `undefined` as `allowedTools`, which crashed plugin load + * ("undefined is not an object (evaluating 'allowedTools')"). The entry module + * must export only `default`; helpers that need to be exported (e.g. for tests) + * live in sibling modules like this one. + */ +export function buildHiddenAgentConfig( + prompt: string, + allowedTools: readonly string[], + maxSteps: number, + overrides?: Record, +) { + const { permission: overridePermission, ...restOverrides } = (overrides ?? {}) as { + permission?: Record; + [key: string]: unknown; + }; + const basePermission = buildAllowOnlyPermission(allowedTools); + return { + prompt, + // No builtin fallback chain: the user's `fallback_models` (if any) flow + // through `restOverrides`. A hardcoded chain names providers the user may + // not have, producing `Model not found` retry storms. + ...restOverrides, + steps: clampHiddenAgentStepLimit(restOverrides.steps, maxSteps), + maxSteps: clampHiddenAgentStepLimit(restOverrides.maxSteps, maxSteps), + // Permission baseline goes after `restOverrides` so that accidental + // `permission` keys in user overrides we DIDN'T explicitly destructure + // can't bypass the deny. The explicit override (destructured above) is + // then layered on top. + permission: { + ...basePermission, + ...(overridePermission ?? {}), + }, + mode: "subagent" as const, + hidden: true, + }; +} + +function clampHiddenAgentStepLimit(value: unknown, cap: number): number { + return typeof value === "number" && Number.isFinite(value) ? Math.min(value, cap) : cap; +} diff --git a/packages/plugin/src/index-refresh.test.ts b/packages/plugin/src/index-refresh.test.ts index f8a5d8f0a..3bf3fc671 100644 --- a/packages/plugin/src/index-refresh.test.ts +++ b/packages/plugin/src/index-refresh.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test"; import { readFileSync } from "node:fs"; import { join } from "node:path"; -import { buildHiddenAgentConfig } from "./index"; +import { buildHiddenAgentConfig } from "./hidden-agent-config"; describe("plugin model-limit cache warmup", () => { test("warms model limits once at startup and does not schedule periodic refresh", () => { diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index 2e41432cf..8f98255f8 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -3,7 +3,6 @@ import { DREAMER_AGENT } from "./agents/dreamer"; import { HISTORIAN_AGENT, HISTORIAN_EDITOR_AGENT } from "./agents/historian"; import { applyDisallowedTools, - buildAllowOnlyPermission, DREAMER_ALLOWED_TOOLS, HISTORIAN_ALLOWED_TOOLS, SIDEKICK_ALLOWED_TOOLS, @@ -23,6 +22,7 @@ import { } from "./features/magic-context/storage-db"; import { recordToolDefinition } from "./features/magic-context/tool-definition-tokens"; import { runDeferredV22Backfill } from "./features/magic-context/v22-deferred-backfill"; +import { buildHiddenAgentConfig } from "./hidden-agent-config"; import { createAutoUpdateCheckerHook } from "./hooks/auto-update-checker"; import { COMPARTMENT_AGENT_SYSTEM_PROMPT, @@ -52,47 +52,6 @@ const HISTORIAN_MAX_STEPS = 40; const SIDEKICK_MAX_STEPS = 40; const DREAMER_MAX_STEPS = 150; -function clampHiddenAgentStepLimit(value: unknown, cap: number): number { - return typeof value === "number" && Number.isFinite(value) ? Math.min(value, cap) : cap; -} - -/** - * Build a hidden-agent config with a deny-everything-by-default permission - * baseline and a hard tool-iteration ceiling. User overrides may lower - * `steps`/`maxSteps`, but cannot raise either above the built-in cap. - */ -export function buildHiddenAgentConfig( - prompt: string, - allowedTools: readonly string[], - maxSteps: number, - overrides?: Record, -) { - const { permission: overridePermission, ...restOverrides } = (overrides ?? {}) as { - permission?: Record; - [key: string]: unknown; - }; - const basePermission = buildAllowOnlyPermission(allowedTools); - return { - prompt, - // No builtin fallback chain: the user's `fallback_models` (if any) flow - // through `restOverrides`. A hardcoded chain names providers the user may - // not have, producing `Model not found` retry storms. - ...restOverrides, - steps: clampHiddenAgentStepLimit(restOverrides.steps, maxSteps), - maxSteps: clampHiddenAgentStepLimit(restOverrides.maxSteps, maxSteps), - // Permission baseline goes after `restOverrides` so that accidental - // `permission` keys in user overrides we DIDN'T explicitly destructure - // can't bypass the deny. The explicit override (destructured above) is - // then layered on top. - permission: { - ...basePermission, - ...(overridePermission ?? {}), - }, - mode: "subagent" as const, - hidden: true, - }; -} - const plugin: Plugin = async (ctx) => { const pluginConfig = loadPluginConfig(ctx.directory); // Apply SQLite connection tuning before the first openDatabase() below.