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
2 changes: 1 addition & 1 deletion packages/junior/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -693,7 +693,7 @@ export async function createApp(options?: JuniorAppOptions): Promise<Hono> {
validateBuildIncludesPluginPackages(pluginConfig, virtualConfig);
}
validateBuildIncludesPluginRuntimeRegistrations(plugins, virtualConfig);
validatePlugins(plugins);
validatePlugins(configuredPlugins?.registrations ?? []);
getDb();
const shouldValidatePluginCatalog =
hasConfiguredPluginCatalog(pluginConfig) ||
Expand Down
8 changes: 0 additions & 8 deletions packages/junior/src/chat/briefs/registration.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import type { PluginRegistration } from "@sentry/junior-plugin-api";
import { briefsTaskRegistration } from "./task";

type BriefsConfig = Readonly<{ enabled?: boolean }>;

let configuredBriefs: BriefsConfig = {};
Expand All @@ -16,8 +13,3 @@ export function setBriefsConfig(config?: { enabled?: boolean }): BriefsConfig {
export function isBriefsEnabled(): boolean {
return configuredBriefs.enabled === true;
}

/** Return enabled core task registrations. */
export function coreTaskRegistrations(): PluginRegistration[] {
return isBriefsEnabled() ? [briefsTaskRegistration] : [];
}
4 changes: 2 additions & 2 deletions packages/junior/src/chat/briefs/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,8 @@ export async function updateConversationBrief(
);
}

/** Core task registration kept outside the installed plugin catalog. */
export const briefsTaskRegistration: PluginRegistration = {
/** Register Briefs outside the installed plugin catalog. */
export const briefsFeatureRegistration: PluginRegistration = {
manifest: {
name: "briefs",
displayName: "Briefs",
Expand Down
5 changes: 5 additions & 0 deletions packages/junior/src/chat/core-features/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Core features

Core features use the plugin runtime contract. They do not use plugin discovery or appear in the installed plugin catalog.

`registration.ts` owns core feature registration and reserved names.
40 changes: 40 additions & 0 deletions packages/junior/src/chat/core-features/registration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { PluginRegistration } from "@sentry/junior-plugin-api";
import { isBriefsEnabled } from "@/chat/briefs/registration";
import { briefsFeatureRegistration } from "@/chat/briefs/task";

const coreFeatures = [
{ isEnabled: isBriefsEnabled, registration: briefsFeatureRegistration },
];
/** Return all core registrations, including disabled features. */
export function allCoreFeatureRegistrations(): PluginRegistration[] {
return coreFeatures.map((feature) => feature.registration);
}

/** Return enabled core feature registrations. */
export function coreFeatureRegistrations(): PluginRegistration[] {
return coreFeatures
.filter((feature) => feature.isEnabled())
.map((feature) => feature.registration);
}

/** Return enabled core features followed by installed plugins. */
export function runtimeFeatureRegistrations(
plugins: PluginRegistration[],
): PluginRegistration[] {
return [...coreFeatureRegistrations(), ...plugins];
}

/** Reject a plugin that claims a core feature name. */
export function assertNoCoreFeatureNameCollisions(
plugins: PluginRegistration[],
): void {
const coreNames = new Set(
allCoreFeatureRegistrations().map((feature) => feature.manifest.name),
);
for (const plugin of plugins) {
const name = plugin.manifest.name;
if (coreNames.has(name)) {
throw new Error(`Plugin registration name "${name}" is reserved by core`);
}
}
}
18 changes: 11 additions & 7 deletions packages/junior/src/chat/plugins/agent-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ import { z } from "zod";
import { workspaceRepoCheckoutPath } from "@/chat/workspaces/checkout-path";
import { listWorkspaceNamesByRepository } from "@/chat/workspaces/store";
import { createCodeChangePublisher } from "@/chat/code/publisher";
import { coreTaskRegistrations } from "@/chat/briefs/registration";
import {
assertNoCoreFeatureNameCollisions,
runtimeFeatureRegistrations,
} from "@/chat/core-features/registration";

/** Signal that a plugin intentionally denied a tool execution. */
export class PluginHookDeniedError extends Error {
Expand Down Expand Up @@ -382,6 +385,7 @@ function logInvalidPromptContributions(args: {

/** Validate plugin identity before it can affect process-wide hooks. */
export function validatePlugins(plugins: PluginRegistration[]): void {
assertNoCoreFeatureNameCollisions(plugins);
const seen = new Set<string>();
for (const plugin of plugins) {
const name = plugin.manifest.name;
Expand Down Expand Up @@ -462,7 +466,7 @@ export async function getPluginSystemPromptContributions(
): Promise<PluginPromptContributionContext[]> {
const contributions: PluginPromptContributionContext[] = [];
let totalChars = 0;
for (const plugin of getPlugins()) {
for (const plugin of runtimeFeatureRegistrations(getPlugins())) {
const pluginName = plugin.manifest.name;
const hook = plugin.hooks?.systemPrompt;
if (!hook) {
Expand Down Expand Up @@ -532,7 +536,7 @@ export async function getPluginUserPromptContributions(args: {
const contributions: PluginPromptContributionContext[] = [];
let totalChars = 0;
let totalContextBytes = 0;
for (const plugin of getPlugins()) {
for (const plugin of runtimeFeatureRegistrations(getPlugins())) {
const pluginName = plugin.manifest.name;
const hook = plugin.hooks?.userPrompt;
if (!hook) {
Expand Down Expand Up @@ -612,7 +616,7 @@ export function getPluginTools(
sandbox: PluginSandbox = createSandboxCapability(context.workspace),
): Record<string, AnyToolDefinition> {
const tools: Record<string, AnyToolDefinition> = {};
for (const plugin of getPlugins()) {
for (const plugin of runtimeFeatureRegistrations(getPlugins())) {
const pluginName = plugin.manifest.name;
const hook = plugin.hooks?.tools;
if (!hook) {
Expand Down Expand Up @@ -872,7 +876,7 @@ export function getPluginRoutes(options: {
const seen = new Set<string>();
const methodsByPath = new Map<string, Set<PluginRouteMethod>>();

for (const plugin of getPlugins()) {
for (const plugin of runtimeFeatureRegistrations(getPlugins())) {
const pluginName = plugin.manifest.name;
const hook = plugin.hooks?.routes;
if (!hook) {
Expand Down Expand Up @@ -991,7 +995,7 @@ export function getPluginRoutes(options: {
export function getPluginApiRoutes(): PluginApiRouteRegistration[] {
const routes: PluginApiRouteRegistration[] = [];

for (const plugin of getPlugins()) {
for (const plugin of runtimeFeatureRegistrations(getPlugins())) {
const pluginName = plugin.manifest.name;
const hook = plugin.hooks?.apiRoutes;
if (!hook) {
Expand Down Expand Up @@ -1356,7 +1360,7 @@ export async function getPluginOperationalReports(
nowMs: number,
): Promise<PluginOperationalReport[]> {
const reports: PluginOperationalReport[] = [];
for (const plugin of [...coreTaskRegistrations(), ...getPlugins()]) {
for (const plugin of runtimeFeatureRegistrations(getPlugins())) {
const pluginName = plugin.manifest.name;
const hook = plugin.hooks?.operationalReport;
if (!hook) {
Expand Down
4 changes: 2 additions & 2 deletions packages/junior/src/chat/plugins/conversation-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {
PluginConversationEventValue,
PluginRegistration,
} from "@sentry/junior-plugin-api";
import { briefsTaskRegistration } from "@/chat/briefs/task";
import { allCoreFeatureRegistrations } from "@/chat/core-features/registration";
import { getConversationEventStore } from "@/chat/db";
import { getPlugins } from "./agent-hooks";

Expand Down Expand Up @@ -80,7 +80,7 @@ export function renderPluginConversationEvent(args: {
namespace: string;
version: number;
}): ConversationEventPresentation | undefined {
const plugin = [briefsTaskRegistration, ...getPlugins()].find(
const plugin = [...allCoreFeatureRegistrations(), ...getPlugins()].find(
(candidate) => candidate.manifest.name === args.namespace,
);
const definition = plugin?.conversationEvents?.find(
Expand Down
12 changes: 5 additions & 7 deletions packages/junior/src/chat/plugins/task-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import {
getTurnRecord,
type TurnRecord,
} from "@/chat/task-execution/checkpoint";
import { coreTaskRegistrations } from "@/chat/briefs/registration";
import { runtimeFeatureRegistrations } from "@/chat/core-features/registration";
import { getPlugins } from "./agent-hooks";
import {
pluginTaskId,
Expand Down Expand Up @@ -442,7 +442,7 @@ function taskPluginContext(
}

function findPluginTask(message: PluginTaskQueueMessage) {
const plugin = [...coreTaskRegistrations(), ...getPlugins()].find(
const plugin = runtimeFeatureRegistrations(getPlugins()).find(
(candidate) => candidate.manifest.name === message.plugin,
);
if (!plugin?.tasks || !Object.hasOwn(plugin.tasks, message.name)) {
Expand All @@ -458,11 +458,9 @@ export async function scheduleSessionCompletedPluginTasks(
options: ScheduleSessionCompletedPluginTasksOptions = {},
): Promise<void> {
const coreParams = pluginTaskParamsSchema.parse(params);
const taskRegistrations = [
...getPlugins(),
...coreTaskRegistrations(),
].flatMap((plugin) =>
Object.keys(plugin.tasks ?? {}).map((name) => ({ name, plugin })),
const taskRegistrations = runtimeFeatureRegistrations(getPlugins()).flatMap(
(plugin) =>
Object.keys(plugin.tasks ?? {}).map((name) => ({ name, plugin })),
);
if (taskRegistrations.length === 0) {
return;
Expand Down
5 changes: 3 additions & 2 deletions packages/junior/src/chat/plugins/user-pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ import { getDb } from "@/chat/db";
import { createPluginLogger } from "@/chat/plugins/logging";
import { resolveViewerUser } from "@/chat/plugins/viewer";
import { getPlugins } from "@/chat/plugins/agent-hooks";
import { runtimeFeatureRegistrations } from "@/chat/core-features/registration";

/** List safe navigation metadata for registered plugin user pages. */
export function readPluginUserPageLinks(): PluginUserPageLink[] {
return pluginUserPageLinksSchema.parse(
getPlugins().flatMap((plugin) =>
runtimeFeatureRegistrations(getPlugins()).flatMap((plugin) =>
(plugin.userPages ?? []).map((page) => ({
description: page.description,
id: page.id,
Expand Down Expand Up @@ -50,7 +51,7 @@ export async function readPluginUserPage(input: {
pluginName: string;
query: PluginUserPageInput;
}): Promise<PluginUserPageContent | undefined> {
const plugin = getPlugins().find(
const plugin = runtimeFeatureRegistrations(getPlugins()).find(
(candidate) => candidate.manifest.name === input.pluginName,
);
const page = plugin?.userPages?.find(
Expand Down
10 changes: 7 additions & 3 deletions packages/junior/src/cli/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
validatePluginRegistrations,
} from "@/chat/plugins/validation";
import { loadAppPluginSet } from "@/plugin-module";
import { runtimeFeatureRegistrations } from "@/chat/core-features/registration";
import {
pluginCliRegistrationsFromPluginSet,
pluginCatalogConfigFromPluginSet,
Expand Down Expand Up @@ -226,14 +227,17 @@ async function loadPluginRegistrations(args: {
runtimePlugins: PluginRegistration[];
}> {
const pluginSet = args.pluginSet;
const cliPlugins = runtimeFeatureRegistrations(
pluginCliRegistrationsFromPluginSet(pluginSet),
).filter((registration) => registration.cli);
if (!pluginSet) {
return { cliPlugins: [], runtimePlugins: [] };
args.validateConfiguredCommands?.(cliPlugins);
return { cliPlugins, runtimePlugins: [] };
}

const cliPlugins = pluginCliRegistrationsFromPluginSet(pluginSet);
const runtimePlugins = pluginRuntimeRegistrationsFromPluginSet(pluginSet);
const pluginConfig = pluginCatalogConfigFromPluginSet(pluginSet);
validatePlugins(runtimePlugins);
validatePlugins(pluginSet.registrations);
const previousPluginCatalogConfig =
pluginCatalogRuntime.setConfig(pluginConfig);
try {
Expand Down
41 changes: 41 additions & 0 deletions packages/junior/tests/unit/core-feature-registration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { defineJuniorPlugin } from "@sentry/junior-plugin-api";
import { afterEach, describe, expect, it } from "vitest";
import { setBriefsConfig } from "@/chat/briefs/registration";
import {
allCoreFeatureRegistrations,
coreFeatureRegistrations,
} from "@/chat/core-features/registration";
import { validatePlugins } from "@/chat/plugins/agent-hooks";

const briefsPlugin = defineJuniorPlugin({
manifest: {
name: "briefs",
displayName: "Briefs",
description: "Conflicting Briefs plugin",
},
});

afterEach(() => {
setBriefsConfig(undefined);
});

describe("core feature registration", () => {
it("enables Briefs from app config but keeps its registration available", () => {
expect(coreFeatureRegistrations()).toEqual([]);
expect(
allCoreFeatureRegistrations().map((feature) => feature.manifest.name),
).toEqual(["briefs"]);

setBriefsConfig({ enabled: true });

expect(
coreFeatureRegistrations().map((feature) => feature.manifest.name),
).toEqual(["briefs"]);
});

it("reserves core feature names", () => {
expect(() => validatePlugins([briefsPlugin])).toThrow(
'Plugin registration name "briefs" is reserved by core',
);
});
});
Loading