From c4f431c25f29313ad2651d75c0d5328072753c62 Mon Sep 17 00:00:00 2001 From: fullsend-code <278716306+fullsend-ai-coder[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 20:23:54 +0000 Subject: [PATCH 1/3] feat(#3297): add boost-common types/permissions and boost-node service ref Scaffold the two foundation packages for the boost workspace: boost-common (common-library): - AgenticProvider, ProviderDescriptor, ProviderCapabilities interfaces - NormalizedStreamEvent discriminated union (19 event types) - ConversationSummary, ConversationDetails, InputItem types - ChatRequest, ChatResponse, ResponseUsage types - 16 resource permissions (10 agent + 5 tool + 1 kagenti-infra) - 5 functional permissions (chat.read/create, documents/mcp/config) - Resource types: boost-agent, boost-tool - Conditional rule constants: IS_OWNER, IS_NOT_CREATOR, HAS_LIFECYCLE_STAGE - No dependency on @backstage/backend-plugin-api (browser-safe) boost-node (node-library): - boostAiProviderServiceRef via createServiceRef (id: boost.ai-provider) - Depends on @backstage/backend-plugin-api (backend-only) - Depends on boost-common for the AgenticProvider type parameter The serviceRef lives in boost-node (not boost-common) to avoid pulling @backstage/backend-plugin-api into browser bundles, per PR #3396 review feedback and Backstage convention (plugin-catalog-common + plugin-catalog-node). Closes #3297 --- .../boost/plugins/boost-common/package.json | 9 +- .../plugins/boost-common/src/index.test.ts | 365 ++++++++++- .../boost/plugins/boost-common/src/index.ts | 90 +++ .../plugins/boost-common/src/permissions.ts | 405 ++++++++++++ .../boost/plugins/boost-common/src/types.ts | 605 ++++++++++++++++++ .../boost/plugins/boost-node/.eslintrc.js | 1 + .../boost/plugins/boost-node/package.json | 55 ++ .../plugins/boost-node/src/index.test.ts | 29 + .../boost/plugins/boost-node/src/index.ts | 23 + .../boost/plugins/boost-node/src/services.ts | 37 ++ workspaces/boost/yarn.lock | 15 +- 11 files changed, 1631 insertions(+), 3 deletions(-) create mode 100644 workspaces/boost/plugins/boost-common/src/permissions.ts create mode 100644 workspaces/boost/plugins/boost-common/src/types.ts create mode 100644 workspaces/boost/plugins/boost-node/.eslintrc.js create mode 100644 workspaces/boost/plugins/boost-node/package.json create mode 100644 workspaces/boost/plugins/boost-node/src/index.test.ts create mode 100644 workspaces/boost/plugins/boost-node/src/index.ts create mode 100644 workspaces/boost/plugins/boost-node/src/services.ts diff --git a/workspaces/boost/plugins/boost-common/package.json b/workspaces/boost/plugins/boost-common/package.json index fa5402e64a..c3de9a7b28 100644 --- a/workspaces/boost/plugins/boost-common/package.json +++ b/workspaces/boost/plugins/boost-common/package.json @@ -16,7 +16,8 @@ "pluginId": "boost", "pluginPackage": "@red-hat-developer-hub/backstage-plugin-boost-common", "pluginPackages": [ - "@red-hat-developer-hub/backstage-plugin-boost-common" + "@red-hat-developer-hub/backstage-plugin-boost-common", + "@red-hat-developer-hub/backstage-plugin-boost-node" ] }, "sideEffects": false, @@ -28,6 +29,12 @@ "prepack": "backstage-cli package prepack", "postpack": "backstage-cli package postpack" }, + "dependencies": { + "@backstage/plugin-permission-common": "^0.9.9" + }, + "peerDependencies": { + "@backstage/plugin-permission-common": "^0.9.9" + }, "devDependencies": { "@backstage/cli": "^0.34.5" }, diff --git a/workspaces/boost/plugins/boost-common/src/index.test.ts b/workspaces/boost/plugins/boost-common/src/index.test.ts index 2753ecae64..daa5553821 100644 --- a/workspaces/boost/plugins/boost-common/src/index.test.ts +++ b/workspaces/boost/plugins/boost-common/src/index.test.ts @@ -14,10 +14,373 @@ * limitations under the License. */ -import { BOOST_PLUGIN_ID } from './index'; +import { + BOOST_PLUGIN_ID, + RESOURCE_TYPE_BOOST_AGENT, + RESOURCE_TYPE_BOOST_TOOL, + boostResourcePermissions, + boostFunctionalPermissions, + boostPermissions, + boostAgentListPermission, + boostAgentRegisterPermission, + boostAgentPromotePermission, + boostAgentApprovePermission, + boostAgentDemotePermission, + boostAgentPublishPermission, + boostAgentUnpublishPermission, + boostAgentWithdrawPermission, + boostAgentDeletePermission, + boostAgentConfigurePermission, + boostToolPromotePermission, + boostToolApprovePermission, + boostToolDemotePermission, + boostToolPublishPermission, + boostToolUnpublishPermission, + boostKagentiAdminPermission, + boostChatReadPermission, + boostChatCreatePermission, + boostDocumentsManagePermission, + boostMcpManagePermission, + boostConfigManagePermission, + BOOST_RULE_IS_OWNER, + BOOST_RULE_IS_NOT_CREATOR, + BOOST_RULE_HAS_LIFECYCLE_STAGE, +} from './index'; + +import type { + AgenticProvider, + ProviderDescriptor, + ProviderCapabilities, + NormalizedStreamEvent, + ConversationSummary, + ConversationDetails, + InputItem, +} from './index'; describe('boost-common', () => { it('exports the boost plugin ID', () => { expect(BOOST_PLUGIN_ID).toBe('boost'); }); + + describe('resource types', () => { + it('defines boost-agent resource type', () => { + expect(RESOURCE_TYPE_BOOST_AGENT).toBe('boost-agent'); + }); + + it('defines boost-tool resource type', () => { + expect(RESOURCE_TYPE_BOOST_TOOL).toBe('boost-tool'); + }); + }); + + describe('permissions', () => { + it('exports exactly 16 resource permissions', () => { + expect(boostResourcePermissions).toHaveLength(16); + }); + + it('exports exactly 5 functional permissions', () => { + expect(boostFunctionalPermissions).toHaveLength(5); + }); + + it('exports 21 total permissions', () => { + expect(boostPermissions).toHaveLength(21); + }); + + describe('agent permissions', () => { + it('defines 10 agent permissions with boost.agent.* names', () => { + const agentPermissions = [ + boostAgentListPermission, + boostAgentRegisterPermission, + boostAgentPromotePermission, + boostAgentApprovePermission, + boostAgentDemotePermission, + boostAgentPublishPermission, + boostAgentUnpublishPermission, + boostAgentWithdrawPermission, + boostAgentDeletePermission, + boostAgentConfigurePermission, + ]; + + for (const perm of agentPermissions) { + expect(perm.name).toMatch(/^boost\.agent\./); + } + expect(agentPermissions).toHaveLength(10); + }); + + it('scopes resource permissions to boost-agent', () => { + const resourceScoped = [ + boostAgentPromotePermission, + boostAgentApprovePermission, + boostAgentDemotePermission, + boostAgentPublishPermission, + boostAgentUnpublishPermission, + boostAgentWithdrawPermission, + boostAgentDeletePermission, + ]; + + for (const perm of resourceScoped) { + expect(perm.resourceType).toBe(RESOURCE_TYPE_BOOST_AGENT); + } + }); + + it('defines basic agent permissions without resource type', () => { + expect(boostAgentListPermission).not.toHaveProperty('resourceType'); + expect(boostAgentRegisterPermission).not.toHaveProperty('resourceType'); + expect(boostAgentConfigurePermission).not.toHaveProperty( + 'resourceType', + ); + }); + }); + + describe('tool permissions', () => { + it('defines 5 tool permissions with boost.tool.* names', () => { + const toolPermissions = [ + boostToolPromotePermission, + boostToolApprovePermission, + boostToolDemotePermission, + boostToolPublishPermission, + boostToolUnpublishPermission, + ]; + + for (const perm of toolPermissions) { + expect(perm.name).toMatch(/^boost\.tool\./); + } + expect(toolPermissions).toHaveLength(5); + }); + + it('scopes all tool permissions to boost-tool', () => { + const toolPermissions = [ + boostToolPromotePermission, + boostToolApprovePermission, + boostToolDemotePermission, + boostToolPublishPermission, + boostToolUnpublishPermission, + ]; + + for (const perm of toolPermissions) { + expect(perm.resourceType).toBe(RESOURCE_TYPE_BOOST_TOOL); + } + }); + }); + + describe('kagenti admin permission', () => { + it('defines kagenti admin permission', () => { + expect(boostKagentiAdminPermission.name).toBe('boost.kagenti.admin'); + }); + }); + + describe('functional permissions', () => { + it('defines chat.read permission', () => { + expect(boostChatReadPermission.name).toBe('boost.chat.read'); + expect(boostChatReadPermission.attributes.action).toBe('read'); + }); + + it('defines chat.create permission', () => { + expect(boostChatCreatePermission.name).toBe('boost.chat.create'); + expect(boostChatCreatePermission.attributes.action).toBe('create'); + }); + + it('defines documents.manage permission', () => { + expect(boostDocumentsManagePermission.name).toBe( + 'boost.documents.manage', + ); + expect(boostDocumentsManagePermission.attributes.action).toBe('update'); + }); + + it('defines mcp.manage permission', () => { + expect(boostMcpManagePermission.name).toBe('boost.mcp.manage'); + expect(boostMcpManagePermission.attributes.action).toBe('update'); + }); + + it('defines config.manage permission', () => { + expect(boostConfigManagePermission.name).toBe('boost.config.manage'); + expect(boostConfigManagePermission.attributes.action).toBe('update'); + }); + }); + + it('has no duplicate permission names', () => { + const names = boostPermissions.map(p => p.name); + expect(new Set(names).size).toBe(names.length); + }); + }); + + describe('conditional rules', () => { + it('defines IS_OWNER rule', () => { + expect(BOOST_RULE_IS_OWNER).toBe('IS_OWNER'); + }); + + it('defines IS_NOT_CREATOR rule', () => { + expect(BOOST_RULE_IS_NOT_CREATOR).toBe('IS_NOT_CREATOR'); + }); + + it('defines HAS_LIFECYCLE_STAGE rule', () => { + expect(BOOST_RULE_HAS_LIFECYCLE_STAGE).toBe('HAS_LIFECYCLE_STAGE'); + }); + }); + + describe('type exports', () => { + it('exports AgenticProvider interface', () => { + // Type-level test: verify the interface shape compiles correctly + const provider: AgenticProvider = { + id: 'test', + displayName: 'Test Provider', + initialize: async () => {}, + postInitialize: async () => {}, + getStatus: async () => ({ + provider: { + connected: true, + baseUrl: 'http://localhost', + model: 'test-model', + }, + timestamp: new Date().toISOString(), + ready: true, + configurationErrors: [], + }), + chat: async () => ({ message: 'hello' }), + chatStream: async () => {}, + }; + + expect(provider.id).toBe('test'); + expect(provider.displayName).toBe('Test Provider'); + }); + + it('exports ProviderDescriptor interface', () => { + const descriptor: ProviderDescriptor = { + id: 'test', + displayName: 'Test', + description: 'A test provider', + implemented: true, + capabilities: { + chat: true, + rag: false, + safety: false, + evaluation: false, + conversations: false, + mcpTools: false, + tools: false, + toolLifecycle: false, + agentLifecycle: false, + devSpaces: false, + contextHydration: false, + providerRoutes: false, + }, + configFields: [], + }; + + expect(descriptor.id).toBe('test'); + expect(descriptor.capabilities.chat).toBe(true); + }); + + it('exports ProviderCapabilities interface', () => { + const capabilities: ProviderCapabilities = { + chat: true, + rag: true, + safety: false, + evaluation: false, + conversations: true, + mcpTools: true, + tools: false, + toolLifecycle: false, + agentLifecycle: false, + devSpaces: false, + contextHydration: false, + providerRoutes: false, + }; + + expect(capabilities.chat).toBe(true); + expect(capabilities.rag).toBe(true); + expect(capabilities.safety).toBe(false); + }); + + it('exports NormalizedStreamEvent union type', () => { + const events: NormalizedStreamEvent[] = [ + { type: 'stream.started', responseId: 'r1' }, + { type: 'stream.text.delta', delta: 'hello' }, + { type: 'stream.text.done', text: 'hello world' }, + { type: 'stream.error', error: 'something failed' }, + { type: 'stream.completed' }, + ]; + + expect(events).toHaveLength(5); + expect(events[0].type).toBe('stream.started'); + }); + + it('exports ConversationSummary interface', () => { + const summary: ConversationSummary = { + responseId: 'r1', + preview: 'Hello', + createdAt: new Date(), + model: 'test-model', + status: 'completed', + }; + + expect(summary.responseId).toBe('r1'); + expect(summary.status).toBe('completed'); + }); + + it('exports ConversationDetails interface', () => { + const details: ConversationDetails = { + id: 'c1', + model: 'test-model', + status: 'completed', + createdAt: new Date(), + input: [], + output: [], + }; + + expect(details.id).toBe('c1'); + }); + + it('exports InputItem interface', () => { + const item: InputItem = { + type: 'message', + role: 'user', + content: 'Hello', + }; + + expect(item.type).toBe('message'); + expect(item.role).toBe('user'); + }); + }); + + describe('no provider-specific types', () => { + it('does not export provider-specific types', () => { + // This test verifies the design principle that no provider-specific + // types (e.g., LlamaStackConfig, KagentiConfig) exist in the + // common package. If any such type is added, it should be caught + // in code review. + const exports = require('./index'); + const exportNames = Object.keys(exports); + + // Should not contain provider-specific prefixes + const providerSpecific = exportNames.filter( + name => + name.startsWith('Kagenti') || + name.startsWith('kagenti') || + name.startsWith('LlamaStack') || + name.startsWith('llamastack') || + name.startsWith('Adk') || + name.startsWith('adk'), + ); + + // boostKagentiAdminPermission is expected — it's a permission name, + // not a provider-specific type + const filtered = providerSpecific.filter( + name => name !== 'boostKagentiAdminPermission', + ); + + expect(filtered).toEqual([]); + }); + }); + + describe('no @backstage/backend-plugin-api dependency', () => { + it('does not depend on backend-plugin-api', () => { + // eslint-disable-next-line @backstage/no-relative-monorepo-imports + const pkg = require('../package.json'); + const allDeps = { + ...pkg.dependencies, + ...pkg.peerDependencies, + }; + expect(allDeps).not.toHaveProperty('@backstage/backend-plugin-api'); + }); + }); }); diff --git a/workspaces/boost/plugins/boost-common/src/index.ts b/workspaces/boost/plugins/boost-common/src/index.ts index 04ae23bf05..740a9e7be0 100644 --- a/workspaces/boost/plugins/boost-common/src/index.ts +++ b/workspaces/boost/plugins/boost-common/src/index.ts @@ -14,9 +14,99 @@ * limitations under the License. */ +/** + * Common types, permissions, and constants for the boost plugin. + * + * @packageDocumentation + */ + /** * The plugin ID for the boost plugin. * * @public */ export const BOOST_PLUGIN_ID = 'boost'; + +// Shared types — provider abstraction, conversation, streaming +export type { + // Provider abstraction + AgenticProvider, + ProviderDescriptor, + ProviderCapabilities, + ProviderConfigField, + AgenticProviderStatus, + ProviderStatus, + // Chat + ChatRequest, + ChatResponse, + ResponseUsage, + // Conversation types + ConversationSummary, + ConversationDetails, + InputItem, + // Normalized streaming events (union + individual) + NormalizedStreamEvent, + StreamStartedEvent, + StreamTextDeltaEvent, + StreamTextDoneEvent, + StreamReasoningDeltaEvent, + StreamReasoningDoneEvent, + StreamToolDiscoveryEvent, + StreamToolStartedEvent, + StreamToolDeltaEvent, + StreamToolCompletedEvent, + StreamToolFailedEvent, + StreamToolApprovalEvent, + StreamRagResultsEvent, + StreamAgentHandoffEvent, + StreamFormRequestEvent, + StreamFormField, + StreamFormDescriptor, + StreamAuthRequiredEvent, + StreamSecretDemand, + StreamArtifactEvent, + StreamCitationReference, + StreamCitationEvent, + StreamCompletedEvent, + StreamErrorEvent, +} from './types'; + +// Permissions — resource types, permission constants, rules, aggregations +export { + // Resource types + RESOURCE_TYPE_BOOST_AGENT, + RESOURCE_TYPE_BOOST_TOOL, + // Agent permissions (10) + boostAgentListPermission, + boostAgentRegisterPermission, + boostAgentPromotePermission, + boostAgentApprovePermission, + boostAgentDemotePermission, + boostAgentPublishPermission, + boostAgentUnpublishPermission, + boostAgentWithdrawPermission, + boostAgentDeletePermission, + boostAgentConfigurePermission, + // Tool permissions (5) + boostToolPromotePermission, + boostToolApprovePermission, + boostToolDemotePermission, + boostToolPublishPermission, + boostToolUnpublishPermission, + // Kagenti infra (1) + boostKagentiAdminPermission, + // Functional permissions (5) + boostChatReadPermission, + boostChatCreatePermission, + boostDocumentsManagePermission, + boostMcpManagePermission, + boostConfigManagePermission, + // Conditional rule names + BOOST_RULE_IS_OWNER, + BOOST_RULE_IS_NOT_CREATOR, + BOOST_RULE_HAS_LIFECYCLE_STAGE, + // Aggregations + boostResourcePermissions, + boostFunctionalPermissions, + boostPermissions, +} from './permissions'; diff --git a/workspaces/boost/plugins/boost-common/src/permissions.ts b/workspaces/boost/plugins/boost-common/src/permissions.ts new file mode 100644 index 0000000000..eebda4c7a0 --- /dev/null +++ b/workspaces/boost/plugins/boost-common/src/permissions.ts @@ -0,0 +1,405 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + createPermission, + type ResourcePermission, +} from '@backstage/plugin-permission-common'; + +// ============================================================================= +// Resource Types +// ============================================================================= + +/** + * Resource type for boost agents. + * Used with resource-scoped permissions that support conditional rules. + * + * @public + */ +export const RESOURCE_TYPE_BOOST_AGENT = 'boost-agent'; + +/** + * Resource type for boost tools. + * Used with resource-scoped permissions that support conditional rules. + * + * @public + */ +export const RESOURCE_TYPE_BOOST_TOOL = 'boost-tool'; + +// ============================================================================= +// Agent Lifecycle Permissions (Resource-Based) +// ============================================================================= +// 10 agent permissions: 3 basic + 7 resource-scoped + +/** + * Permission to view the agent list. + * @public + */ +export const boostAgentListPermission = createPermission({ + name: 'boost.agent.list', + attributes: { + action: 'read', + }, +}); + +/** + * Permission to register an agent for governance. + * @public + */ +export const boostAgentRegisterPermission = createPermission({ + name: 'boost.agent.register', + attributes: { + action: 'create', + }, +}); + +/** + * Permission to submit a draft agent for review (draft -> pending). + * Conditional rules: IS_OWNER, HAS_LIFECYCLE_STAGE + * @public + */ +export const boostAgentPromotePermission: ResourcePermission<'boost-agent'> = + createPermission({ + name: 'boost.agent.promote', + attributes: { + action: 'update', + }, + resourceType: RESOURCE_TYPE_BOOST_AGENT, + }); + +/** + * Permission to approve a pending agent (pending -> published). + * Conditional rules: IS_NOT_CREATOR, HAS_LIFECYCLE_STAGE + * @public + */ +export const boostAgentApprovePermission: ResourcePermission<'boost-agent'> = + createPermission({ + name: 'boost.agent.approve', + attributes: { + action: 'update', + }, + resourceType: RESOURCE_TYPE_BOOST_AGENT, + }); + +/** + * Permission to demote/reject an agent. + * @public + */ +export const boostAgentDemotePermission: ResourcePermission<'boost-agent'> = + createPermission({ + name: 'boost.agent.demote', + attributes: { + action: 'update', + }, + resourceType: RESOURCE_TYPE_BOOST_AGENT, + }); + +/** + * Permission to publish an approved agent. + * @public + */ +export const boostAgentPublishPermission: ResourcePermission<'boost-agent'> = + createPermission({ + name: 'boost.agent.publish', + attributes: { + action: 'update', + }, + resourceType: RESOURCE_TYPE_BOOST_AGENT, + }); + +/** + * Permission to request unpublishing an agent. + * Conditional rules: IS_OWNER + * @public + */ +export const boostAgentUnpublishPermission: ResourcePermission<'boost-agent'> = + createPermission({ + name: 'boost.agent.unpublish', + attributes: { + action: 'update', + }, + resourceType: RESOURCE_TYPE_BOOST_AGENT, + }); + +/** + * Permission to withdraw a pending submission. + * Conditional rules: IS_OWNER + * @public + */ +export const boostAgentWithdrawPermission: ResourcePermission<'boost-agent'> = + createPermission({ + name: 'boost.agent.withdraw', + attributes: { + action: 'update', + }, + resourceType: RESOURCE_TYPE_BOOST_AGENT, + }); + +/** + * Permission to delete an agent. + * Conditional rules: IS_OWNER, HAS_LIFECYCLE_STAGE + * @public + */ +export const boostAgentDeletePermission: ResourcePermission<'boost-agent'> = + createPermission({ + name: 'boost.agent.delete', + attributes: { + action: 'delete', + }, + resourceType: RESOURCE_TYPE_BOOST_AGENT, + }); + +/** + * Permission to edit agent configuration. + * @public + */ +export const boostAgentConfigurePermission = createPermission({ + name: 'boost.agent.configure', + attributes: { + action: 'update', + }, +}); + +// ============================================================================= +// Tool Lifecycle Permissions (Resource-Based) +// ============================================================================= +// 5 tool permissions: all resource-scoped + +/** + * Permission to promote a tool's lifecycle stage. + * Conditional rules: IS_OWNER + * @public + */ +export const boostToolPromotePermission: ResourcePermission<'boost-tool'> = + createPermission({ + name: 'boost.tool.promote', + attributes: { + action: 'update', + }, + resourceType: RESOURCE_TYPE_BOOST_TOOL, + }); + +/** + * Permission to approve a tool promotion. + * Conditional rules: IS_NOT_CREATOR + * @public + */ +export const boostToolApprovePermission: ResourcePermission<'boost-tool'> = + createPermission({ + name: 'boost.tool.approve', + attributes: { + action: 'update', + }, + resourceType: RESOURCE_TYPE_BOOST_TOOL, + }); + +/** + * Permission to demote a tool's lifecycle stage. + * @public + */ +export const boostToolDemotePermission: ResourcePermission<'boost-tool'> = + createPermission({ + name: 'boost.tool.demote', + attributes: { + action: 'update', + }, + resourceType: RESOURCE_TYPE_BOOST_TOOL, + }); + +/** + * Permission to publish a tool. + * @public + */ +export const boostToolPublishPermission: ResourcePermission<'boost-tool'> = + createPermission({ + name: 'boost.tool.publish', + attributes: { + action: 'update', + }, + resourceType: RESOURCE_TYPE_BOOST_TOOL, + }); + +/** + * Permission to unpublish a tool. + * @public + */ +export const boostToolUnpublishPermission: ResourcePermission<'boost-tool'> = + createPermission({ + name: 'boost.tool.unpublish', + attributes: { + action: 'update', + }, + resourceType: RESOURCE_TYPE_BOOST_TOOL, + }); + +// ============================================================================= +// Infrastructure Permission +// ============================================================================= + +/** + * Permission for Kagenti infrastructure operations + * (namespace management, build pipelines, sandbox, platform links). + * @public + */ +export const boostKagentiAdminPermission = createPermission({ + name: 'boost.kagenti.admin', + attributes: { + action: 'update', + }, +}); + +// ============================================================================= +// Functional Permissions (non-lifecycle) +// ============================================================================= + +/** + * Permission to view the chat interface and read messages. + * @public + */ +export const boostChatReadPermission = createPermission({ + name: 'boost.chat.read', + attributes: { + action: 'read', + }, +}); + +/** + * Permission to send messages and start sessions. + * @public + */ +export const boostChatCreatePermission = createPermission({ + name: 'boost.chat.create', + attributes: { + action: 'create', + }, +}); + +/** + * Permission to upload documents and sync RAG sources. + * @public + */ +export const boostDocumentsManagePermission = createPermission({ + name: 'boost.documents.manage', + attributes: { + action: 'update', + }, +}); + +/** + * Permission to configure MCP servers. + * @public + */ +export const boostMcpManagePermission = createPermission({ + name: 'boost.mcp.manage', + attributes: { + action: 'update', + }, +}); + +/** + * Permission to modify admin configuration. + * @public + */ +export const boostConfigManagePermission = createPermission({ + name: 'boost.config.manage', + attributes: { + action: 'update', + }, +}); + +// ============================================================================= +// Conditional Rule Names +// ============================================================================= +// +// These are string constants for the conditional permission rules. +// The actual rule implementations live in boost-backend where +// the resource loader functions resolve agents/tools from the store. + +/** + * Conditional rule: checks resource.createdBy === currentUser. + * Used for ownership-scoped actions. + * @public + */ +export const BOOST_RULE_IS_OWNER = 'IS_OWNER'; + +/** + * Conditional rule: checks resource.createdBy !== currentUser. + * Used for separation-of-duties (e.g., no self-approval). + * @public + */ +export const BOOST_RULE_IS_NOT_CREATOR = 'IS_NOT_CREATOR'; + +/** + * Conditional rule: checks resource.lifecycleStage against allowed stages. + * Used to enforce valid lifecycle transitions at the permission layer. + * @public + */ +export const BOOST_RULE_HAS_LIFECYCLE_STAGE = 'HAS_LIFECYCLE_STAGE'; + +// ============================================================================= +// Permission Aggregation +// ============================================================================= + +/** + * All 16 resource permissions (10 agent + 5 tool + 1 kagenti-infra). + * + * @public + */ +export const boostResourcePermissions = [ + // Agent permissions (10) + boostAgentListPermission, + boostAgentRegisterPermission, + boostAgentPromotePermission, + boostAgentApprovePermission, + boostAgentDemotePermission, + boostAgentPublishPermission, + boostAgentUnpublishPermission, + boostAgentWithdrawPermission, + boostAgentDeletePermission, + boostAgentConfigurePermission, + // Tool permissions (5) + boostToolPromotePermission, + boostToolApprovePermission, + boostToolDemotePermission, + boostToolPublishPermission, + boostToolUnpublishPermission, + // Kagenti infra (1) + boostKagentiAdminPermission, +] as const; + +/** + * All 5 functional permissions (non-lifecycle). + * + * @public + */ +export const boostFunctionalPermissions = [ + boostChatReadPermission, + boostChatCreatePermission, + boostDocumentsManagePermission, + boostMcpManagePermission, + boostConfigManagePermission, +] as const; + +/** + * All boost permissions for registration via + * `permissionsRegistry.addPermissions()`. + * + * @public + */ +export const boostPermissions = [ + ...boostResourcePermissions, + ...boostFunctionalPermissions, +] as const; diff --git a/workspaces/boost/plugins/boost-common/src/types.ts b/workspaces/boost/plugins/boost-common/src/types.ts new file mode 100644 index 0000000000..1c702ab56c --- /dev/null +++ b/workspaces/boost/plugins/boost-common/src/types.ts @@ -0,0 +1,605 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// ============================================================================= +// Provider Abstraction Types +// ============================================================================= +// +// These are the shared interfaces for the boost provider abstraction. +// Provider-specific types (e.g., Kagenti config, Llama Stack config) +// live in their respective provider modules — NOT here. + +/** + * Declares the capability matrix for a provider. + * Frontend uses these flags for capability-based feature gating — + * never provider ID string checks. + * + * @public + */ +export interface ProviderCapabilities { + /** Provider supports chat (required) */ + readonly chat: boolean; + /** Provider supports RAG/document search */ + readonly rag: boolean; + /** Provider supports safety shields */ + readonly safety: boolean; + /** Provider supports evaluation/scoring */ + readonly evaluation: boolean; + /** Provider supports conversation history */ + readonly conversations: boolean; + /** Provider supports MCP tool servers */ + readonly mcpTools: boolean; + /** Provider supports tool registry */ + readonly tools: boolean; + /** Provider manages tool lifecycle (build, deploy, route) */ + readonly toolLifecycle: boolean; + /** Provider manages agent lifecycle (build, deploy, migrate) */ + readonly agentLifecycle: boolean; + /** Provider supports DevSpaces integration */ + readonly devSpaces: boolean; + /** Provider needs context hydrated from DB on non-streaming paths */ + readonly contextHydration: boolean; + /** Provider registers its own sub-routes */ + readonly providerRoutes: boolean; +} + +/** + * Configuration field definition for the admin panel. + * + * @public + */ +export interface ProviderConfigField { + /** Config key (matches admin config key) */ + readonly key: string; + /** Human-readable label for the form field */ + readonly label: string; + /** Field type determines the form control rendered */ + readonly type: 'string' | 'boolean' | 'number' | 'select'; + /** Whether the field is required for the provider to function */ + readonly required: boolean; + /** Help text shown below the field */ + readonly description?: string; + /** Options for 'select' type fields */ + readonly options?: readonly string[]; + /** Placeholder text for text inputs */ + readonly placeholder?: string; + /** Whether the field value is sensitive and should be masked in UI */ + readonly sensitive?: boolean; +} + +/** + * Describes a registered provider — its identity, capabilities, + * and admin panel config fields. + * + * @public + */ +export interface ProviderDescriptor { + /** Unique provider identifier (e.g., 'llamastack', 'kagenti') */ + readonly id: string; + /** Human-readable display name (e.g., "Llama Stack") */ + readonly displayName: string; + /** Short description of the provider */ + readonly description: string; + /** Whether this provider has a working implementation */ + readonly implemented: boolean; + /** Which capability categories this provider supports */ + readonly capabilities: ProviderCapabilities; + /** Provider-specific config field definitions for the admin panel */ + readonly configFields: readonly ProviderConfigField[]; +} + +/** + * The core provider interface. This is the abstraction boundary between + * the Backstage plugin and the underlying AI/agentic runtime. + * + * The router and plugin lifecycle interact ONLY through this interface. + * Provider-specific code lives entirely within the provider implementation. + * + * Required methods: `chat()` and `chatStream()`. + * Optional capabilities are exposed as optional properties. + * + * @public + */ +export interface AgenticProvider { + /** Unique identifier for this provider type (e.g., 'llamastack', 'kagenti') */ + readonly id: string; + + /** Human-readable display name */ + readonly displayName: string; + + /** Initialize the provider (connect, validate config, etc.) */ + initialize(): Promise; + + /** Post-initialization hook (after all providers are registered) */ + postInitialize(): Promise; + + /** Get current provider status including capabilities and health */ + getStatus(): Promise; + + /** Synchronous chat request */ + chat(request: ChatRequest): Promise; + + /** Streaming chat request */ + chatStream( + request: ChatRequest, + onEvent: (event: NormalizedStreamEvent) => void, + signal?: AbortSignal, + ): Promise; + + /** Graceful shutdown */ + shutdown?(): Promise; + + /** Invalidate cached runtime config */ + invalidateRuntimeConfig?(): void; + + /** Refresh dynamic configuration from the DB */ + refreshDynamicConfig?(): Promise; + + /** Get the effective (merged) configuration */ + getEffectiveConfig?(): Promise>; + + /** List available models */ + listModels?(): Promise< + Array<{ id: string; owned_by?: string; model_type?: string }> + >; + + /** Test connectivity to a model */ + testModel?( + model?: string, + baseUrl?: string, + ): Promise<{ + connected: boolean; + modelFound: boolean; + canGenerate: boolean; + error?: string; + }>; + + /** Set user context for per-user identity delegation */ + setUserContext?(userRef: string): void; +} + +// ============================================================================= +// Provider Status Types +// ============================================================================= + +/** + * Overall status of an agentic provider. + * + * @public + */ +export interface AgenticProviderStatus { + /** Provider connectivity and health */ + provider: ProviderStatus; + /** Timestamp of the status check */ + timestamp: string; + /** Whether the provider is ready to serve requests */ + ready: boolean; + /** Any configuration errors preventing operation */ + configurationErrors: string[]; + /** Summary of available capabilities */ + capabilities?: { + chat: boolean; + rag: { available: boolean; reason?: string }; + mcpTools: { available: boolean; reason?: string }; + agentCatalog?: boolean; + }; +} + +/** + * Provider connectivity status. + * + * @public + */ +export interface ProviderStatus { + /** Whether the provider is connected to its backend */ + connected: boolean; + /** The base URL of the provider backend */ + baseUrl: string; + /** Model identifier in use */ + model: string; + /** Error message if not connected */ + error?: string; +} + +// ============================================================================= +// Chat Types +// ============================================================================= + +/** + * Chat request sent to a provider. + * + * @public + */ +export interface ChatRequest { + /** User message text */ + message: string; + /** Model to use (overrides default) */ + model?: string; + /** Session/conversation identifier */ + sessionId?: string; + /** Previous response ID for conversation continuity */ + previousResponseId?: string; + /** User identity reference */ + userRef?: string; + /** Agent identifier for agent-scoped chat */ + agentId?: string; +} + +/** + * Chat response from a provider. + * + * @public + */ +export interface ChatResponse { + /** The assistant's response text */ + message: string; + /** Response identifier for conversation threading */ + responseId?: string; + /** Token usage information */ + usage?: ResponseUsage; +} + +/** + * Token usage statistics for a response. + * + * @public + */ +export interface ResponseUsage { + /** Total input tokens */ + input_tokens: number; + /** Total output tokens */ + output_tokens: number; + /** Sum of input + output tokens */ + total_tokens: number; +} + +// ============================================================================= +// Conversation Types +// ============================================================================= + +/** + * Summary of a conversation for list views. + * + * @public + */ +export interface ConversationSummary { + /** Response ID — use to continue this conversation */ + responseId: string; + /** Preview of the conversation (first user message or assistant response) */ + preview: string; + /** When the conversation was created */ + createdAt: Date; + /** Model used */ + model: string; + /** Conversation status */ + status: 'completed' | 'failed' | 'in_progress'; + /** Conversation container ID */ + conversationId?: string; + /** Previous response ID for chain deduplication */ + previousResponseId?: string; +} + +/** + * Full conversation details. + * + * @public + */ +export interface ConversationDetails { + /** Conversation/response identifier */ + id: string; + /** Model used */ + model: string; + /** Conversation status */ + status: string; + /** When the conversation was created */ + createdAt: Date; + /** Input data */ + input: unknown; + /** Output items (messages, tool calls, etc.) */ + output: Array<{ + type: string; + id?: string; + role?: string; + content?: Array<{ type: string; text: string }>; + status?: string; + name?: string; + call_id?: string; + arguments?: string; + output?: string; + error?: string; + server_label?: string; + results?: Array<{ + text: string; + filename?: string; + file_id?: string; + score?: number; + attributes?: Record; + }>; + }>; + /** Token usage */ + usage?: ResponseUsage; + /** Previous response in the chain */ + previousResponseId?: string; + /** Container conversation ID */ + conversationId?: string; +} + +/** + * An input item from a conversation's input list. + * + * @public + */ +export interface InputItem { + /** Item type (e.g., 'message', 'function_call', 'function_call_output') */ + type: string; + /** Item identifier */ + id?: string; + /** Role of the item author */ + role?: string; + /** Item content */ + content?: unknown; + /** Item status */ + status?: string; + /** Function call identifier */ + call_id?: string; + /** Function/tool name */ + name?: string; + /** Function call arguments (JSON string) */ + arguments?: string; + /** Function call output */ + output?: string; +} + +// ============================================================================= +// Normalized Streaming Events +// ============================================================================= +// +// These events form the contract between the backend provider and the frontend. +// Each provider adapter maps its native streaming format to these events. +// The frontend reducer ONLY processes normalized events — never raw provider events. +// +// Design principles: +// - Discriminated union on `type` for exhaustive switch handling +// - Each event carries only the data needed for that event +// - Event names use dot notation: stream.. +// - Provider-specific data is normalized away before reaching the frontend + +/** Response has been created, streaming is starting. @public */ +export interface StreamStartedEvent { + type: 'stream.started'; + responseId: string; + model?: string; + /** Server-side creation timestamp (Unix epoch seconds) */ + createdAt?: number; +} + +/** A chunk of generated text. @public */ +export interface StreamTextDeltaEvent { + type: 'stream.text.delta'; + delta: string; +} + +/** Text generation is complete for this response. @public */ +export interface StreamTextDoneEvent { + type: 'stream.text.done'; + text: string; +} + +/** A chunk of reasoning/thinking text. @public */ +export interface StreamReasoningDeltaEvent { + type: 'stream.reasoning.delta'; + delta: string; +} + +/** Reasoning text is complete. @public */ +export interface StreamReasoningDoneEvent { + type: 'stream.reasoning.done'; + text: string; +} + +/** Provider is discovering available tools. @public */ +export interface StreamToolDiscoveryEvent { + type: 'stream.tool.discovery'; + serverLabel?: string; + status: 'in_progress' | 'completed'; + toolCount?: number; +} + +/** A tool call has started. @public */ +export interface StreamToolStartedEvent { + type: 'stream.tool.started'; + callId: string; + name: string; + serverLabel?: string; +} + +/** Tool call arguments are streaming in. @public */ +export interface StreamToolDeltaEvent { + type: 'stream.tool.delta'; + callId: string; + delta: string; +} + +/** Tool call completed with output. @public */ +export interface StreamToolCompletedEvent { + type: 'stream.tool.completed'; + callId: string; + name: string; + serverLabel?: string; + output?: string; + error?: string; +} + +/** Tool call failed. @public */ +export interface StreamToolFailedEvent { + type: 'stream.tool.failed'; + callId: string; + name: string; + serverLabel?: string; + error: string; +} + +/** Tool call requires human approval (HITL). @public */ +export interface StreamToolApprovalEvent { + type: 'stream.tool.approval'; + callId: string; + name: string; + serverLabel?: string; + arguments?: string; + /** Response ID for this approval request */ + responseId?: string; +} + +/** RAG search results arrived. @public */ +export interface StreamRagResultsEvent { + type: 'stream.rag.results'; + sources: Array<{ + filename: string; + fileId?: string; + text?: string; + score?: number; + title?: string; + sourceUrl?: string; + contentType?: string; + attributes?: Record; + }>; + filesSearched?: string[]; +} + +/** An agent handoff occurred during multi-agent streaming. @public */ +export interface StreamAgentHandoffEvent { + type: 'stream.agent.handoff'; + fromAgent?: string; + toAgent: string; + reason?: string; +} + +/** Agent requests structured form input from the user. @public */ +export interface StreamFormRequestEvent { + type: 'stream.form.request'; + taskId?: string; + contextId?: string; + form: StreamFormDescriptor; +} + +/** A form field descriptor for input-required forms. @public */ +export interface StreamFormField { + name: string; + type?: string; + label?: string; + description?: string; + required?: boolean; + defaultValue?: unknown; + options?: Array<{ label: string; value: string }>; + [key: string]: unknown; +} + +/** Shape of a form render request. @public */ +export interface StreamFormDescriptor { + title?: string; + description?: string; + fields?: StreamFormField[]; + [key: string]: unknown; +} + +/** Agent requires authentication. @public */ +export interface StreamAuthRequiredEvent { + type: 'stream.auth.required'; + taskId?: string; + authType: 'oauth' | 'secret'; + url?: string; + demands?: { secrets?: StreamSecretDemand[]; [key: string]: unknown }; +} + +/** Shape of a secret demand from an agent. @public */ +export interface StreamSecretDemand { + name: string; + description?: string; + [key: string]: unknown; +} + +/** Agent is streaming an artifact (code, file, document). @public */ +export interface StreamArtifactEvent { + type: 'stream.artifact'; + artifactId: string; + name?: string; + description?: string; + content: string; + append?: boolean; + lastChunk?: boolean; +} + +/** A single citation reference. @public */ +export interface StreamCitationReference { + title?: string; + url?: string; + snippet?: string; + [key: string]: unknown; +} + +/** Agent provides source citations for its response. @public */ +export interface StreamCitationEvent { + type: 'stream.citation'; + citations: StreamCitationReference[]; +} + +/** The response is fully complete. @public */ +export interface StreamCompletedEvent { + type: 'stream.completed'; + responseId?: string; + usage?: ResponseUsage; + /** Display name of the agent that produced the final response */ + agentName?: string; +} + +/** An error occurred during streaming. @public */ +export interface StreamErrorEvent { + type: 'stream.error'; + error: string; + code?: string; + title?: string; + context?: Record; +} + +/** + * Union of all normalized streaming events. + * + * This is the single source of truth for the streaming contract + * between the backend and frontend. Both the backend's stream normalizer + * and the frontend's streaming reducer use this type. + * + * @public + */ +export type NormalizedStreamEvent = + | StreamStartedEvent + | StreamTextDeltaEvent + | StreamTextDoneEvent + | StreamReasoningDeltaEvent + | StreamReasoningDoneEvent + | StreamToolDiscoveryEvent + | StreamToolStartedEvent + | StreamToolDeltaEvent + | StreamToolCompletedEvent + | StreamToolFailedEvent + | StreamToolApprovalEvent + | StreamRagResultsEvent + | StreamAgentHandoffEvent + | StreamFormRequestEvent + | StreamAuthRequiredEvent + | StreamArtifactEvent + | StreamCitationEvent + | StreamCompletedEvent + | StreamErrorEvent; diff --git a/workspaces/boost/plugins/boost-node/.eslintrc.js b/workspaces/boost/plugins/boost-node/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/workspaces/boost/plugins/boost-node/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/workspaces/boost/plugins/boost-node/package.json b/workspaces/boost/plugins/boost-node/package.json new file mode 100644 index 0000000000..806b6be5f6 --- /dev/null +++ b/workspaces/boost/plugins/boost-node/package.json @@ -0,0 +1,55 @@ +{ + "name": "@red-hat-developer-hub/backstage-plugin-boost-node", + "version": "0.1.0", + "license": "Apache-2.0", + "description": "Node.js library for the boost plugin", + "main": "src/index.ts", + "types": "src/index.ts", + "publishConfig": { + "access": "public" + }, + "backstage": { + "role": "node-library", + "pluginId": "boost", + "pluginPackages": [ + "@red-hat-developer-hub/backstage-plugin-boost-common", + "@red-hat-developer-hub/backstage-plugin-boost-node" + ] + }, + "exports": { + ".": "./src/index.ts", + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "package.json": [ + "package.json" + ] + } + }, + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-plugin-api": "^1.9.1", + "@red-hat-developer-hub/backstage-plugin-boost-common": "workspace:^" + }, + "devDependencies": { + "@backstage/cli": "^0.34.5" + }, + "files": [ + "dist" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/redhat-developer/rhdh-plugins.git", + "directory": "workspaces/boost/plugins/boost-node" + }, + "homepage": "https://red.ht/rhdh", + "bugs": "https://github.com/redhat-developer/rhdh-plugins/issues" +} diff --git a/workspaces/boost/plugins/boost-node/src/index.test.ts b/workspaces/boost/plugins/boost-node/src/index.test.ts new file mode 100644 index 0000000000..f4a63c29f1 --- /dev/null +++ b/workspaces/boost/plugins/boost-node/src/index.test.ts @@ -0,0 +1,29 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { boostAiProviderServiceRef } from './index'; + +describe('boost-node', () => { + describe('boostAiProviderServiceRef', () => { + it('has the correct service ID', () => { + expect(boostAiProviderServiceRef.id).toBe('boost.ai-provider'); + }); + + it('has plugin scope', () => { + expect(boostAiProviderServiceRef.scope).toBe('plugin'); + }); + }); +}); diff --git a/workspaces/boost/plugins/boost-node/src/index.ts b/workspaces/boost/plugins/boost-node/src/index.ts new file mode 100644 index 0000000000..31d5f56b43 --- /dev/null +++ b/workspaces/boost/plugins/boost-node/src/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Node.js library for the boost plugin. + * + * @packageDocumentation + */ + +export { boostAiProviderServiceRef } from './services'; diff --git a/workspaces/boost/plugins/boost-node/src/services.ts b/workspaces/boost/plugins/boost-node/src/services.ts new file mode 100644 index 0000000000..1fcc98ea10 --- /dev/null +++ b/workspaces/boost/plugins/boost-node/src/services.ts @@ -0,0 +1,37 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createServiceRef } from '@backstage/backend-plugin-api'; +import type { AgenticProvider } from '@red-hat-developer-hub/backstage-plugin-boost-common'; + +/** + * Service ref for the active AI provider. + * + * Other Backstage plugins can declare a dependency on this ref + * to consume the currently active `AgenticProvider` instance. + * The core `boost-backend` plugin registers the default factory + * that resolves to the `ProviderManager`'s active provider. + * + * Lives in `boost-node` (not `boost-common`) because `createServiceRef` + * depends on `@backstage/backend-plugin-api`, which must not be pulled + * into browser bundles via a common-library package. + * + * @public + */ +export const boostAiProviderServiceRef = createServiceRef({ + id: 'boost.ai-provider', + scope: 'plugin', +}); diff --git a/workspaces/boost/yarn.lock b/workspaces/boost/yarn.lock index 103dbcbe85..85bacfbb23 100644 --- a/workspaces/boost/yarn.lock +++ b/workspaces/boost/yarn.lock @@ -3700,11 +3700,24 @@ __metadata: languageName: node linkType: hard -"@red-hat-developer-hub/backstage-plugin-boost-common@workspace:plugins/boost-common": +"@red-hat-developer-hub/backstage-plugin-boost-common@workspace:^, @red-hat-developer-hub/backstage-plugin-boost-common@workspace:plugins/boost-common": version: 0.0.0-use.local resolution: "@red-hat-developer-hub/backstage-plugin-boost-common@workspace:plugins/boost-common" dependencies: "@backstage/cli": "npm:^0.34.5" + "@backstage/plugin-permission-common": "npm:^0.9.9" + peerDependencies: + "@backstage/plugin-permission-common": ^0.9.9 + languageName: unknown + linkType: soft + +"@red-hat-developer-hub/backstage-plugin-boost-node@workspace:plugins/boost-node": + version: 0.0.0-use.local + resolution: "@red-hat-developer-hub/backstage-plugin-boost-node@workspace:plugins/boost-node" + dependencies: + "@backstage/backend-plugin-api": "npm:^1.9.1" + "@backstage/cli": "npm:^0.34.5" + "@red-hat-developer-hub/backstage-plugin-boost-common": "workspace:^" languageName: unknown linkType: soft From 06148729ccb40196bfe18ef0ec80d21de5d1a255 Mon Sep 17 00:00:00 2001 From: fullsend-fix <278716306+fullsend-ai-coder[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 21:40:29 +0000 Subject: [PATCH 2/3] fix: generate API reports for boost-common and boost-node - Escape '>' in TSDoc comments for boostAgentPromotePermission and boostAgentApprovePermission to fix tsdoc-escape-greater-than warnings that blocked API report generation - Regenerate boost-common/report.api.md with full API surface - Add missing boost-node/report.api.md Addresses review feedback on #3402 --- .../boost/plugins/boost-common/report.api.md | 632 +++++++++++++++++- .../plugins/boost-common/src/permissions.ts | 4 +- .../boost/plugins/boost-node/report.api.md | 15 + 3 files changed, 648 insertions(+), 3 deletions(-) create mode 100644 workspaces/boost/plugins/boost-node/report.api.md diff --git a/workspaces/boost/plugins/boost-common/report.api.md b/workspaces/boost/plugins/boost-common/report.api.md index 888eb42de8..7e712e7ef4 100644 --- a/workspaces/boost/plugins/boost-common/report.api.md +++ b/workspaces/boost/plugins/boost-common/report.api.md @@ -3,8 +3,638 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { BasicPermission } from '@backstage/plugin-permission-common'; +import { ResourcePermission } from '@backstage/plugin-permission-common'; + +// @public +export interface AgenticProvider { + chat(request: ChatRequest): Promise; + chatStream( + request: ChatRequest, + onEvent: (event: NormalizedStreamEvent) => void, + signal?: AbortSignal, + ): Promise; + readonly displayName: string; + getEffectiveConfig?(): Promise>; + getStatus(): Promise; + readonly id: string; + initialize(): Promise; + invalidateRuntimeConfig?(): void; + listModels?(): Promise< + Array<{ + id: string; + owned_by?: string; + model_type?: string; + }> + >; + postInitialize(): Promise; + refreshDynamicConfig?(): Promise; + setUserContext?(userRef: string): void; + shutdown?(): Promise; + testModel?( + model?: string, + baseUrl?: string, + ): Promise<{ + connected: boolean; + modelFound: boolean; + canGenerate: boolean; + error?: string; + }>; +} + +// @public +export interface AgenticProviderStatus { + capabilities?: { + chat: boolean; + rag: { + available: boolean; + reason?: string; + }; + mcpTools: { + available: boolean; + reason?: string; + }; + agentCatalog?: boolean; + }; + configurationErrors: string[]; + provider: ProviderStatus; + ready: boolean; + timestamp: string; +} + // @public export const BOOST_PLUGIN_ID = 'boost'; -// (No @packageDocumentation comment for this package) +// @public +export const BOOST_RULE_HAS_LIFECYCLE_STAGE = 'HAS_LIFECYCLE_STAGE'; + +// @public +export const BOOST_RULE_IS_NOT_CREATOR = 'IS_NOT_CREATOR'; + +// @public +export const BOOST_RULE_IS_OWNER = 'IS_OWNER'; + +// @public +export const boostAgentApprovePermission: ResourcePermission<'boost-agent'>; + +// @public +export const boostAgentConfigurePermission: BasicPermission; + +// @public +export const boostAgentDeletePermission: ResourcePermission<'boost-agent'>; + +// @public +export const boostAgentDemotePermission: ResourcePermission<'boost-agent'>; + +// @public +export const boostAgentListPermission: BasicPermission; + +// @public +export const boostAgentPromotePermission: ResourcePermission<'boost-agent'>; + +// @public +export const boostAgentPublishPermission: ResourcePermission<'boost-agent'>; + +// @public +export const boostAgentRegisterPermission: BasicPermission; + +// @public +export const boostAgentUnpublishPermission: ResourcePermission<'boost-agent'>; + +// @public +export const boostAgentWithdrawPermission: ResourcePermission<'boost-agent'>; + +// @public +export const boostChatCreatePermission: BasicPermission; + +// @public +export const boostChatReadPermission: BasicPermission; + +// @public +export const boostConfigManagePermission: BasicPermission; + +// @public +export const boostDocumentsManagePermission: BasicPermission; + +// @public +export const boostFunctionalPermissions: readonly [ + BasicPermission, + BasicPermission, + BasicPermission, + BasicPermission, + BasicPermission, +]; + +// @public +export const boostKagentiAdminPermission: BasicPermission; + +// @public +export const boostMcpManagePermission: BasicPermission; + +// @public +export const boostPermissions: readonly [ + BasicPermission, + BasicPermission, + ResourcePermission<'boost-agent'>, + ResourcePermission<'boost-agent'>, + ResourcePermission<'boost-agent'>, + ResourcePermission<'boost-agent'>, + ResourcePermission<'boost-agent'>, + ResourcePermission<'boost-agent'>, + ResourcePermission<'boost-agent'>, + BasicPermission, + ResourcePermission<'boost-tool'>, + ResourcePermission<'boost-tool'>, + ResourcePermission<'boost-tool'>, + ResourcePermission<'boost-tool'>, + ResourcePermission<'boost-tool'>, + BasicPermission, + BasicPermission, + BasicPermission, + BasicPermission, + BasicPermission, + BasicPermission, +]; + +// @public +export const boostResourcePermissions: readonly [ + BasicPermission, + BasicPermission, + ResourcePermission<'boost-agent'>, + ResourcePermission<'boost-agent'>, + ResourcePermission<'boost-agent'>, + ResourcePermission<'boost-agent'>, + ResourcePermission<'boost-agent'>, + ResourcePermission<'boost-agent'>, + ResourcePermission<'boost-agent'>, + BasicPermission, + ResourcePermission<'boost-tool'>, + ResourcePermission<'boost-tool'>, + ResourcePermission<'boost-tool'>, + ResourcePermission<'boost-tool'>, + ResourcePermission<'boost-tool'>, + BasicPermission, +]; + +// @public +export const boostToolApprovePermission: ResourcePermission<'boost-tool'>; + +// @public +export const boostToolDemotePermission: ResourcePermission<'boost-tool'>; + +// @public +export const boostToolPromotePermission: ResourcePermission<'boost-tool'>; + +// @public +export const boostToolPublishPermission: ResourcePermission<'boost-tool'>; + +// @public +export const boostToolUnpublishPermission: ResourcePermission<'boost-tool'>; + +// @public +export interface ChatRequest { + agentId?: string; + message: string; + model?: string; + previousResponseId?: string; + sessionId?: string; + userRef?: string; +} + +// @public +export interface ChatResponse { + message: string; + responseId?: string; + usage?: ResponseUsage; +} + +// @public +export interface ConversationDetails { + conversationId?: string; + createdAt: Date; + id: string; + input: unknown; + model: string; + output: Array<{ + type: string; + id?: string; + role?: string; + content?: Array<{ + type: string; + text: string; + }>; + status?: string; + name?: string; + call_id?: string; + arguments?: string; + output?: string; + error?: string; + server_label?: string; + results?: Array<{ + text: string; + filename?: string; + file_id?: string; + score?: number; + attributes?: Record; + }>; + }>; + previousResponseId?: string; + status: string; + usage?: ResponseUsage; +} + +// @public +export interface ConversationSummary { + conversationId?: string; + createdAt: Date; + model: string; + preview: string; + previousResponseId?: string; + responseId: string; + status: 'completed' | 'failed' | 'in_progress'; +} + +// @public +export interface InputItem { + arguments?: string; + call_id?: string; + content?: unknown; + id?: string; + name?: string; + output?: string; + role?: string; + status?: string; + type: string; +} + +// @public +export type NormalizedStreamEvent = + | StreamStartedEvent + | StreamTextDeltaEvent + | StreamTextDoneEvent + | StreamReasoningDeltaEvent + | StreamReasoningDoneEvent + | StreamToolDiscoveryEvent + | StreamToolStartedEvent + | StreamToolDeltaEvent + | StreamToolCompletedEvent + | StreamToolFailedEvent + | StreamToolApprovalEvent + | StreamRagResultsEvent + | StreamAgentHandoffEvent + | StreamFormRequestEvent + | StreamAuthRequiredEvent + | StreamArtifactEvent + | StreamCitationEvent + | StreamCompletedEvent + | StreamErrorEvent; + +// @public +export interface ProviderCapabilities { + readonly agentLifecycle: boolean; + readonly chat: boolean; + readonly contextHydration: boolean; + readonly conversations: boolean; + readonly devSpaces: boolean; + readonly evaluation: boolean; + readonly mcpTools: boolean; + readonly providerRoutes: boolean; + readonly rag: boolean; + readonly safety: boolean; + readonly toolLifecycle: boolean; + readonly tools: boolean; +} + +// @public +export interface ProviderConfigField { + readonly description?: string; + readonly key: string; + readonly label: string; + readonly options?: readonly string[]; + readonly placeholder?: string; + readonly required: boolean; + readonly sensitive?: boolean; + readonly type: 'string' | 'boolean' | 'number' | 'select'; +} + +// @public +export interface ProviderDescriptor { + readonly capabilities: ProviderCapabilities; + readonly configFields: readonly ProviderConfigField[]; + readonly description: string; + readonly displayName: string; + readonly id: string; + readonly implemented: boolean; +} + +// @public +export interface ProviderStatus { + baseUrl: string; + connected: boolean; + error?: string; + model: string; +} + +// @public +export const RESOURCE_TYPE_BOOST_AGENT = 'boost-agent'; + +// @public +export const RESOURCE_TYPE_BOOST_TOOL = 'boost-tool'; + +// @public +export interface ResponseUsage { + input_tokens: number; + output_tokens: number; + total_tokens: number; +} + +// @public +export interface StreamAgentHandoffEvent { + // (undocumented) + fromAgent?: string; + // (undocumented) + reason?: string; + // (undocumented) + toAgent: string; + // (undocumented) + type: 'stream.agent.handoff'; +} + +// @public +export interface StreamArtifactEvent { + // (undocumented) + append?: boolean; + // (undocumented) + artifactId: string; + // (undocumented) + content: string; + // (undocumented) + description?: string; + // (undocumented) + lastChunk?: boolean; + // (undocumented) + name?: string; + // (undocumented) + type: 'stream.artifact'; +} + +// @public +export interface StreamAuthRequiredEvent { + // (undocumented) + authType: 'oauth' | 'secret'; + // (undocumented) + demands?: { + secrets?: StreamSecretDemand[]; + [key: string]: unknown; + }; + // (undocumented) + taskId?: string; + // (undocumented) + type: 'stream.auth.required'; + // (undocumented) + url?: string; +} + +// @public +export interface StreamCitationEvent { + // (undocumented) + citations: StreamCitationReference[]; + // (undocumented) + type: 'stream.citation'; +} + +// @public +export interface StreamCitationReference { + // (undocumented) + [key: string]: unknown; + // (undocumented) + snippet?: string; + // (undocumented) + title?: string; + // (undocumented) + url?: string; +} + +// @public +export interface StreamCompletedEvent { + agentName?: string; + // (undocumented) + responseId?: string; + // (undocumented) + type: 'stream.completed'; + // (undocumented) + usage?: ResponseUsage; +} + +// @public +export interface StreamErrorEvent { + // (undocumented) + code?: string; + // (undocumented) + context?: Record; + // (undocumented) + error: string; + // (undocumented) + title?: string; + // (undocumented) + type: 'stream.error'; +} + +// @public +export interface StreamFormDescriptor { + // (undocumented) + [key: string]: unknown; + // (undocumented) + description?: string; + // (undocumented) + fields?: StreamFormField[]; + // (undocumented) + title?: string; +} + +// @public +export interface StreamFormField { + // (undocumented) + [key: string]: unknown; + // (undocumented) + defaultValue?: unknown; + // (undocumented) + description?: string; + // (undocumented) + label?: string; + // (undocumented) + name: string; + // (undocumented) + options?: Array<{ + label: string; + value: string; + }>; + // (undocumented) + required?: boolean; + // (undocumented) + type?: string; +} + +// @public +export interface StreamFormRequestEvent { + // (undocumented) + contextId?: string; + // (undocumented) + form: StreamFormDescriptor; + // (undocumented) + taskId?: string; + // (undocumented) + type: 'stream.form.request'; +} + +// @public +export interface StreamRagResultsEvent { + // (undocumented) + filesSearched?: string[]; + // (undocumented) + sources: Array<{ + filename: string; + fileId?: string; + text?: string; + score?: number; + title?: string; + sourceUrl?: string; + contentType?: string; + attributes?: Record; + }>; + // (undocumented) + type: 'stream.rag.results'; +} + +// @public +export interface StreamReasoningDeltaEvent { + // (undocumented) + delta: string; + // (undocumented) + type: 'stream.reasoning.delta'; +} + +// @public +export interface StreamReasoningDoneEvent { + // (undocumented) + text: string; + // (undocumented) + type: 'stream.reasoning.done'; +} + +// @public +export interface StreamSecretDemand { + // (undocumented) + [key: string]: unknown; + // (undocumented) + description?: string; + // (undocumented) + name: string; +} + +// @public +export interface StreamStartedEvent { + createdAt?: number; + // (undocumented) + model?: string; + // (undocumented) + responseId: string; + // (undocumented) + type: 'stream.started'; +} + +// @public +export interface StreamTextDeltaEvent { + // (undocumented) + delta: string; + // (undocumented) + type: 'stream.text.delta'; +} + +// @public +export interface StreamTextDoneEvent { + // (undocumented) + text: string; + // (undocumented) + type: 'stream.text.done'; +} + +// @public +export interface StreamToolApprovalEvent { + // (undocumented) + arguments?: string; + // (undocumented) + callId: string; + // (undocumented) + name: string; + responseId?: string; + // (undocumented) + serverLabel?: string; + // (undocumented) + type: 'stream.tool.approval'; +} + +// @public +export interface StreamToolCompletedEvent { + // (undocumented) + callId: string; + // (undocumented) + error?: string; + // (undocumented) + name: string; + // (undocumented) + output?: string; + // (undocumented) + serverLabel?: string; + // (undocumented) + type: 'stream.tool.completed'; +} + +// @public +export interface StreamToolDeltaEvent { + // (undocumented) + callId: string; + // (undocumented) + delta: string; + // (undocumented) + type: 'stream.tool.delta'; +} + +// @public +export interface StreamToolDiscoveryEvent { + // (undocumented) + serverLabel?: string; + // (undocumented) + status: 'in_progress' | 'completed'; + // (undocumented) + toolCount?: number; + // (undocumented) + type: 'stream.tool.discovery'; +} + +// @public +export interface StreamToolFailedEvent { + // (undocumented) + callId: string; + // (undocumented) + error: string; + // (undocumented) + name: string; + // (undocumented) + serverLabel?: string; + // (undocumented) + type: 'stream.tool.failed'; +} + +// @public +export interface StreamToolStartedEvent { + // (undocumented) + callId: string; + // (undocumented) + name: string; + // (undocumented) + serverLabel?: string; + // (undocumented) + type: 'stream.tool.started'; +} ``` diff --git a/workspaces/boost/plugins/boost-common/src/permissions.ts b/workspaces/boost/plugins/boost-common/src/permissions.ts index eebda4c7a0..def957e409 100644 --- a/workspaces/boost/plugins/boost-common/src/permissions.ts +++ b/workspaces/boost/plugins/boost-common/src/permissions.ts @@ -67,7 +67,7 @@ export const boostAgentRegisterPermission = createPermission({ }); /** - * Permission to submit a draft agent for review (draft -> pending). + * Permission to submit a draft agent for review (draft -\> pending). * Conditional rules: IS_OWNER, HAS_LIFECYCLE_STAGE * @public */ @@ -81,7 +81,7 @@ export const boostAgentPromotePermission: ResourcePermission<'boost-agent'> = }); /** - * Permission to approve a pending agent (pending -> published). + * Permission to approve a pending agent (pending -\> published). * Conditional rules: IS_NOT_CREATOR, HAS_LIFECYCLE_STAGE * @public */ diff --git a/workspaces/boost/plugins/boost-node/report.api.md b/workspaces/boost/plugins/boost-node/report.api.md new file mode 100644 index 0000000000..b01234c8d0 --- /dev/null +++ b/workspaces/boost/plugins/boost-node/report.api.md @@ -0,0 +1,15 @@ +## API Report File for "@red-hat-developer-hub/backstage-plugin-boost-node" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import type { AgenticProvider } from '@red-hat-developer-hub/backstage-plugin-boost-common'; +import { ServiceRef } from '@backstage/backend-plugin-api'; + +// @public +export const boostAiProviderServiceRef: ServiceRef< + AgenticProvider, + 'plugin', + 'singleton' +>; +``` From 49687121e8a9875ea30597460c42721a259032cf Mon Sep 17 00:00:00 2001 From: fullsend-fix <278716306+fullsend-ai-coder[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 22:17:46 +0000 Subject: [PATCH 3/3] refactor: split boostResourcePermissions into agent/tool/infra arrays Reorganize the monolithic boostResourcePermissions (16 entries) into three focused arrays per review feedback: - boostAgentPermissions (10 agent permissions) - boostToolPermissions (5 tool permissions) - boostInfraPermissions (1 kagenti infra permission) The top-level boostPermissions now spreads all three plus boostFunctionalPermissions, preserving the same 21-entry total. Addresses review feedback on #3402 --- .../boost/plugins/boost-common/report.api.md | 30 +++++++++++-------- .../plugins/boost-common/src/index.test.ts | 16 ++++++++-- .../boost/plugins/boost-common/src/index.ts | 4 ++- .../plugins/boost-common/src/permissions.ts | 27 +++++++++++++---- 4 files changed, 55 insertions(+), 22 deletions(-) diff --git a/workspaces/boost/plugins/boost-common/report.api.md b/workspaces/boost/plugins/boost-common/report.api.md index 7e712e7ef4..6be868d9a6 100644 --- a/workspaces/boost/plugins/boost-common/report.api.md +++ b/workspaces/boost/plugins/boost-common/report.api.md @@ -89,6 +89,20 @@ export const boostAgentDemotePermission: ResourcePermission<'boost-agent'>; // @public export const boostAgentListPermission: BasicPermission; +// @public +export const boostAgentPermissions: readonly [ + BasicPermission, + BasicPermission, + ResourcePermission<'boost-agent'>, + ResourcePermission<'boost-agent'>, + ResourcePermission<'boost-agent'>, + ResourcePermission<'boost-agent'>, + ResourcePermission<'boost-agent'>, + ResourcePermission<'boost-agent'>, + ResourcePermission<'boost-agent'>, + BasicPermission, +]; + // @public export const boostAgentPromotePermission: ResourcePermission<'boost-agent'>; @@ -125,6 +139,9 @@ export const boostFunctionalPermissions: readonly [ BasicPermission, ]; +// @public +export const boostInfraPermissions: readonly [BasicPermission]; + // @public export const boostKagentiAdminPermission: BasicPermission; @@ -157,23 +174,12 @@ export const boostPermissions: readonly [ ]; // @public -export const boostResourcePermissions: readonly [ - BasicPermission, - BasicPermission, - ResourcePermission<'boost-agent'>, - ResourcePermission<'boost-agent'>, - ResourcePermission<'boost-agent'>, - ResourcePermission<'boost-agent'>, - ResourcePermission<'boost-agent'>, - ResourcePermission<'boost-agent'>, - ResourcePermission<'boost-agent'>, - BasicPermission, +export const boostToolPermissions: readonly [ ResourcePermission<'boost-tool'>, ResourcePermission<'boost-tool'>, ResourcePermission<'boost-tool'>, ResourcePermission<'boost-tool'>, ResourcePermission<'boost-tool'>, - BasicPermission, ]; // @public diff --git a/workspaces/boost/plugins/boost-common/src/index.test.ts b/workspaces/boost/plugins/boost-common/src/index.test.ts index daa5553821..d3dc5338ea 100644 --- a/workspaces/boost/plugins/boost-common/src/index.test.ts +++ b/workspaces/boost/plugins/boost-common/src/index.test.ts @@ -18,7 +18,9 @@ import { BOOST_PLUGIN_ID, RESOURCE_TYPE_BOOST_AGENT, RESOURCE_TYPE_BOOST_TOOL, - boostResourcePermissions, + boostAgentPermissions, + boostToolPermissions, + boostInfraPermissions, boostFunctionalPermissions, boostPermissions, boostAgentListPermission, @@ -73,8 +75,16 @@ describe('boost-common', () => { }); describe('permissions', () => { - it('exports exactly 16 resource permissions', () => { - expect(boostResourcePermissions).toHaveLength(16); + it('exports exactly 10 agent permissions', () => { + expect(boostAgentPermissions).toHaveLength(10); + }); + + it('exports exactly 5 tool permissions', () => { + expect(boostToolPermissions).toHaveLength(5); + }); + + it('exports exactly 1 infra permission', () => { + expect(boostInfraPermissions).toHaveLength(1); }); it('exports exactly 5 functional permissions', () => { diff --git a/workspaces/boost/plugins/boost-common/src/index.ts b/workspaces/boost/plugins/boost-common/src/index.ts index 740a9e7be0..812476fd8f 100644 --- a/workspaces/boost/plugins/boost-common/src/index.ts +++ b/workspaces/boost/plugins/boost-common/src/index.ts @@ -106,7 +106,9 @@ export { BOOST_RULE_IS_NOT_CREATOR, BOOST_RULE_HAS_LIFECYCLE_STAGE, // Aggregations - boostResourcePermissions, + boostAgentPermissions, + boostToolPermissions, + boostInfraPermissions, boostFunctionalPermissions, boostPermissions, } from './permissions'; diff --git a/workspaces/boost/plugins/boost-common/src/permissions.ts b/workspaces/boost/plugins/boost-common/src/permissions.ts index def957e409..49e7e0fc6b 100644 --- a/workspaces/boost/plugins/boost-common/src/permissions.ts +++ b/workspaces/boost/plugins/boost-common/src/permissions.ts @@ -354,12 +354,11 @@ export const BOOST_RULE_HAS_LIFECYCLE_STAGE = 'HAS_LIFECYCLE_STAGE'; // ============================================================================= /** - * All 16 resource permissions (10 agent + 5 tool + 1 kagenti-infra). + * All 10 agent permissions (3 basic + 7 resource-scoped). * * @public */ -export const boostResourcePermissions = [ - // Agent permissions (10) +export const boostAgentPermissions = [ boostAgentListPermission, boostAgentRegisterPermission, boostAgentPromotePermission, @@ -370,13 +369,27 @@ export const boostResourcePermissions = [ boostAgentWithdrawPermission, boostAgentDeletePermission, boostAgentConfigurePermission, - // Tool permissions (5) +] as const; + +/** + * All 5 tool permissions (all resource-scoped). + * + * @public + */ +export const boostToolPermissions = [ boostToolPromotePermission, boostToolApprovePermission, boostToolDemotePermission, boostToolPublishPermission, boostToolUnpublishPermission, - // Kagenti infra (1) +] as const; + +/** + * Infrastructure permissions (kagenti). + * + * @public + */ +export const boostInfraPermissions = [ boostKagentiAdminPermission, ] as const; @@ -400,6 +413,8 @@ export const boostFunctionalPermissions = [ * @public */ export const boostPermissions = [ - ...boostResourcePermissions, + ...boostAgentPermissions, + ...boostToolPermissions, + ...boostInfraPermissions, ...boostFunctionalPermissions, ] as const;