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.