diff --git a/src/plugins/agent-plugins.test.ts b/src/plugins/agent-plugins.test.ts index 71272ce99..d9386c3fb 100644 --- a/src/plugins/agent-plugins.test.ts +++ b/src/plugins/agent-plugins.test.ts @@ -95,4 +95,34 @@ describe("resolveAgentPluginProfiles", () => { const profiles = await resolveAgentPluginProfiles([mod], config); expect(profiles[0]!.source).toBe("claude"); }); + + // Gating uses isPluginModuleEnabled (same as skills), not the bare + // isPluginEnabled (settings-only) that tool plugins use for consent-gating. + test("loads profiles from a repo plugin with defaultEnabled and no settings entry", async () => { + const mod: PluginModule = { + manifest: { id: "p1", name: "p1", kind: "agent", defaultEnabled: true }, + agentPlugin: { agents: [validProfile] }, + origin: "repo", + }; + const profiles = await resolveAgentPluginProfiles([mod], {}); + expect(profiles.map((p) => p.id)).toEqual(["explorer"]); + }); + + test("does not load profiles from a non-repo plugin with defaultEnabled and no settings entry", async () => { + const mod: PluginModule = { + manifest: { id: "p1", name: "p1", kind: "agent", defaultEnabled: true }, + agentPlugin: { agents: [validProfile] }, + origin: "user", + }; + expect(await resolveAgentPluginProfiles([mod], {})).toEqual([]); + }); + + test("explicit enabled: false overrides repo defaultEnabled", async () => { + const mod: PluginModule = { + manifest: { id: "p1", name: "p1", kind: "agent", defaultEnabled: true }, + agentPlugin: { agents: [validProfile] }, + origin: "repo", + }; + expect(await resolveAgentPluginProfiles([mod], { p1: { enabled: false } })).toEqual([]); + }); }); diff --git a/src/plugins/agent-plugins.ts b/src/plugins/agent-plugins.ts index cbdeb3293..e61d6a9c8 100644 --- a/src/plugins/agent-plugins.ts +++ b/src/plugins/agent-plugins.ts @@ -4,7 +4,7 @@ import type { AgentProfile } from "../agent/profiles.js"; import { AgentProfileSchema } from "../agent/profiles.js"; import type { PluginModule } from "./loader.js"; import type { PluginConfig } from "../config/settings.js"; -import { isPluginEnabled } from "./register.js"; +import { isPluginModuleEnabled } from "./register.js"; import { pluginWarningSink, type PluginLoadDiagnostics, @@ -34,7 +34,12 @@ function resolveAgentProfileWarningHandler( // validated against the AgentProfileSchema so a malformed entry is skipped // rather than crashing the sub-agent dispatcher. Enabled-only gating (no // consent) is sufficient: an agent profile is configuration data (tier, -// capabilities, role prompt), not in-process code execution. +// capabilities, role prompt), not in-process code execution. Gating uses +// isPluginModuleEnabled (same as skills), so a first-party repo plugin with +// manifest.defaultEnabled loads its profiles without an explicit settings +// entry. This differs from tool plugins (isToolPluginActive), which require +// explicit enabled+consented in settings even for repo plugins, because a +// tool plugin runs in-process code rather than declaring configuration data. // // Warnings fire whenever a profile is rejected so JS-plugin authors get the // same feedback loop data-only plugin authors already enjoy. Pass `diagnostics` @@ -49,7 +54,7 @@ export async function resolveAgentPluginProfiles( for (const mod of modules) { if (mod.manifest?.kind !== "agent") continue; if (mod.agentPlugin === undefined) continue; - if (!isPluginEnabled(config, mod.manifest.id)) continue; + if (!isPluginModuleEnabled(mod, config)) continue; const rawAgents = mod.agentPlugin.agents; if (!Array.isArray(rawAgents)) continue; diff --git a/src/plugins/tool-plugins.ts b/src/plugins/tool-plugins.ts index b00a069bb..4b9ad609a 100644 --- a/src/plugins/tool-plugins.ts +++ b/src/plugins/tool-plugins.ts @@ -36,7 +36,10 @@ export function collectToolPlugins(modules: PluginModule[]): ToolPluginCandidate } // A tool plugin adds in-process agent capabilities, so it is wired in only when -// the user has both enabled it AND given one-time consent. +// the user has both enabled it AND given one-time consent. Unlike skills +// (isPluginModuleEnabled) and agent profiles (resolveAgentPluginProfiles), +// repo manifest.defaultEnabled never activates a tool plugin on its own — this +// is intentional, not an oversight, until product intent changes. export function isToolPluginActive(config: Record, id: string): boolean { return config[id]?.enabled === true && config[id]?.consented === true; }