Skip to content
Merged
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
30 changes: 30 additions & 0 deletions src/plugins/agent-plugins.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([]);
});
});
11 changes: 8 additions & 3 deletions src/plugins/agent-plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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`
Expand All @@ -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;
Expand Down
5 changes: 4 additions & 1 deletion src/plugins/tool-plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, PluginConfig>, id: string): boolean {
return config[id]?.enabled === true && config[id]?.consented === true;
}
Expand Down
Loading