Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions packages/plugin/src/hidden-agent-config.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>,
) {
const { permission: overridePermission, ...restOverrides } = (overrides ?? {}) as {
permission?: Record<string, unknown>;
[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;
}
2 changes: 1 addition & 1 deletion packages/plugin/src/index-refresh.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down
43 changes: 1 addition & 42 deletions packages/plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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<string, unknown>,
) {
const { permission: overridePermission, ...restOverrides } = (overrides ?? {}) as {
permission?: Record<string, unknown>;
[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.
Expand Down
Loading