Skip to content

Commit 174a46e

Browse files
feat(#3299): runtime configuration engine with Zod validation
Implements the runtime configuration engine for boost-backend: - RuntimeConfigResolver: two-layer config resolution (DB override → YAML baseline) with cacheService (30s TTL, immediate invalidation on write). Single cache layer, no duplicate wrappers. - AdminConfigService: DB-backed config overrides using the boost_admin_config table. Validates all writes against Zod schemas and enforces configScope (yaml-only fields rejected for DB writes). - Zod schemas as single source of truth: all 15 admin-configurable fields defined with schema, configScope annotation (yaml-only, db-overridable, db-only), and descriptions. config.d.ts generated from the same schema definitions. - Credential encryption: AES-256-GCM encryption for sensitive DB-stored values (e.g., DevSpaces credentials) with configurable encryption secret. - Schema version tracking: stores schema version alongside DB values. On startup, re-validates all stored values against current schemas and removes invalid overrides (restoring YAML baseline). - Plugin wired with coreServices.cache and coreServices.database dependencies, satisfying the cache-from-day-one architecture rule. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2e647e2 commit 174a46e

15 files changed

Lines changed: 2053 additions & 3 deletions
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* Configuration schema for the boost backend plugin.
19+
*
20+
* Generated from Zod schemas in `src/config/schemas.ts`.
21+
* Do not edit manually — update the Zod schemas and regenerate.
22+
*/
23+
export interface Config {
24+
boost?: {
25+
/** Model connection configuration. */
26+
model?: {
27+
/**
28+
* Base URL for the AI model endpoint.
29+
* @visibility frontend
30+
* @configScope db-overridable
31+
*/
32+
baseUrl?: string;
33+
/**
34+
* Name of the AI model to use.
35+
* @visibility frontend
36+
* @configScope db-overridable
37+
*/
38+
name?: string;
39+
};
40+
41+
/**
42+
* System prompt for AI conversations.
43+
* @configScope db-overridable
44+
*/
45+
systemPrompt?: string;
46+
47+
/** Security configuration. */
48+
security?: {
49+
/**
50+
* Security mode for the boost plugin.
51+
* @configScope yaml-only
52+
*/
53+
mode?: 'development-only-no-auth' | 'plugin-only' | 'full';
54+
};
55+
56+
/** Feature flags. */
57+
features?: {
58+
/**
59+
* Enable agent creation feature.
60+
* @visibility frontend
61+
* @configScope db-overridable
62+
*/
63+
agentCreation?: boolean;
64+
/**
65+
* Enable skills marketplace feature.
66+
* @visibility frontend
67+
* @configScope db-overridable
68+
*/
69+
skillsMarketplace?: boolean;
70+
};
71+
72+
/** Agent approval configuration. */
73+
agentApproval?: {
74+
/**
75+
* Agent approval mode: built-in or SonataFlow-managed.
76+
* @configScope db-overridable
77+
*/
78+
mode?: 'built-in' | 'sonataflow';
79+
/** SonataFlow integration. */
80+
sonataflow?: {
81+
/**
82+
* SonataFlow workflow endpoint for agent approval.
83+
* @configScope yaml-only
84+
*/
85+
endpoint?: string;
86+
};
87+
};
88+
89+
/** Skills marketplace configuration. */
90+
skillsMarketplace?: {
91+
/**
92+
* Skills catalog backend URL.
93+
* @configScope yaml-only
94+
*/
95+
endpoint?: string;
96+
/**
97+
* Enable or disable skills marketplace.
98+
* @visibility frontend
99+
* @configScope db-overridable
100+
*/
101+
enabled?: boolean;
102+
};
103+
104+
/** Kagenti provider configuration. */
105+
kagenti?: {
106+
/** Authentication configuration. */
107+
auth?: {
108+
/** RFC 8693 token exchange. */
109+
tokenExchange?: {
110+
/**
111+
* Enable RFC 8693 token exchange for Kagenti.
112+
* @configScope yaml-only
113+
*/
114+
enabled?: boolean;
115+
/**
116+
* Target audience for exchanged token.
117+
* @configScope yaml-only
118+
*/
119+
audience?: string;
120+
/**
121+
* Header containing user OIDC token.
122+
* @configScope yaml-only
123+
*/
124+
userTokenHeader?: string;
125+
};
126+
};
127+
};
128+
129+
/** DevSpaces integration. */
130+
devSpaces?: {
131+
/**
132+
* DevSpaces integration credentials.
133+
* @visibility secret
134+
* @configScope db-overridable
135+
*/
136+
credentials?: string;
137+
};
138+
};
139+
}

workspaces/boost/plugins/boost-backend/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@
3636
"@backstage/plugin-permission-node": "^0.10.11",
3737
"@red-hat-developer-hub/backstage-plugin-boost-common": "workspace:^",
3838
"@red-hat-developer-hub/backstage-plugin-boost-node": "workspace:^",
39-
"express": "^4.21.1"
39+
"express": "^4.21.1",
40+
"knex": "^3.1.0",
41+
"zod": "^3.23.8"
4042
},
4143
"devDependencies": {
4244
"@backstage/cli": "^0.34.5",

workspaces/boost/plugins/boost-backend/report.api.md

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,36 @@
66
import type { AgenticProvider } from '@red-hat-developer-hub/backstage-plugin-boost-common';
77
import { BackendFeature } from '@backstage/backend-plugin-api';
88
import { BasicPermission } from '@backstage/plugin-permission-common';
9+
import type { CacheService } from '@backstage/backend-plugin-api';
10+
import type { DatabaseService } from '@backstage/backend-plugin-api';
911
import type { HttpAuthService } from '@backstage/backend-plugin-api';
1012
import type { LoggerService } from '@backstage/backend-plugin-api';
1113
import type { PermissionsService } from '@backstage/backend-plugin-api';
1214
import type { ProviderDescriptor } from '@red-hat-developer-hub/backstage-plugin-boost-common';
1315
import type { Request as Request_2 } from 'express';
1416
import type { RequestHandler } from 'express';
17+
import type { RootConfigService } from '@backstage/backend-plugin-api';
1518
import { ServiceFactory } from '@backstage/backend-plugin-api';
19+
import { z } from 'zod';
20+
21+
// @public
22+
export class AdminConfigService {
23+
constructor(options: AdminConfigServiceOptions);
24+
getAllOverrides(): Promise<Map<string, unknown>>;
25+
getOverride(key: BoostConfigKey): Promise<unknown | undefined>;
26+
removeOverride(key: BoostConfigKey): Promise<void>;
27+
setOverride(key: BoostConfigKey, value: unknown): Promise<void>;
28+
validateStoredValues(): Promise<string[]>;
29+
}
30+
31+
// @public
32+
export interface AdminConfigServiceOptions {
33+
// (undocumented)
34+
database: DatabaseService;
35+
encryptionSecret?: string;
36+
// (undocumented)
37+
logger: LoggerService;
38+
}
1639

1740
// @public
1841
export function authorizeLifecycleAction(
@@ -27,23 +50,129 @@ export interface AuthorizeLifecycleActionOptions {
2750
permissions: PermissionsService;
2851
}
2952

53+
// @public
54+
export const BOOST_CONFIG_SCHEMA_VERSION = 1;
55+
3056
// @public
3157
export const boostAiProviderServiceFactory: ServiceFactory<
3258
AgenticProvider,
3359
'plugin',
3460
'singleton'
3561
>;
3662

63+
// @public
64+
export const boostConfigFields: {
65+
readonly 'boost.model.baseUrl': {
66+
readonly schema: z.ZodString;
67+
readonly configScope: ConfigScope;
68+
readonly description: 'Base URL for the AI model endpoint';
69+
};
70+
readonly 'boost.model.name': {
71+
readonly schema: z.ZodString;
72+
readonly configScope: ConfigScope;
73+
readonly description: 'Name of the AI model to use';
74+
};
75+
readonly 'boost.systemPrompt': {
76+
readonly schema: z.ZodOptional<z.ZodString>;
77+
readonly configScope: ConfigScope;
78+
readonly description: 'System prompt for AI conversations';
79+
};
80+
readonly 'boost.security.mode': {
81+
readonly schema: z.ZodEnum<
82+
['development-only-no-auth', 'plugin-only', 'full']
83+
>;
84+
readonly configScope: ConfigScope;
85+
readonly description: 'Security mode for the boost plugin';
86+
};
87+
readonly 'boost.features.agentCreation': {
88+
readonly schema: z.ZodOptional<z.ZodBoolean>;
89+
readonly configScope: ConfigScope;
90+
readonly description: 'Enable agent creation feature';
91+
};
92+
readonly 'boost.features.skillsMarketplace': {
93+
readonly schema: z.ZodOptional<z.ZodBoolean>;
94+
readonly configScope: ConfigScope;
95+
readonly description: 'Enable skills marketplace feature';
96+
};
97+
readonly 'boost.agentApproval.mode': {
98+
readonly schema: z.ZodOptional<z.ZodEnum<['built-in', 'sonataflow']>>;
99+
readonly configScope: ConfigScope;
100+
readonly description: 'Agent approval mode: built-in or SonataFlow-managed';
101+
};
102+
readonly 'boost.agentApproval.sonataflow.endpoint': {
103+
readonly schema: z.ZodOptional<z.ZodString>;
104+
readonly configScope: ConfigScope;
105+
readonly description: 'SonataFlow workflow endpoint for agent approval';
106+
};
107+
readonly 'boost.skillsMarketplace.endpoint': {
108+
readonly schema: z.ZodOptional<z.ZodString>;
109+
readonly configScope: ConfigScope;
110+
readonly description: 'Skills catalog backend URL';
111+
};
112+
readonly 'boost.skillsMarketplace.enabled': {
113+
readonly schema: z.ZodOptional<z.ZodBoolean>;
114+
readonly configScope: ConfigScope;
115+
readonly description: 'Enable or disable skills marketplace';
116+
};
117+
readonly 'boost.kagenti.auth.tokenExchange.enabled': {
118+
readonly schema: z.ZodOptional<z.ZodBoolean>;
119+
readonly configScope: ConfigScope;
120+
readonly description: 'Enable RFC 8693 token exchange for Kagenti';
121+
};
122+
readonly 'boost.kagenti.auth.tokenExchange.audience': {
123+
readonly schema: z.ZodOptional<z.ZodString>;
124+
readonly configScope: ConfigScope;
125+
readonly description: 'Target audience for exchanged token';
126+
};
127+
readonly 'boost.kagenti.auth.tokenExchange.userTokenHeader': {
128+
readonly schema: z.ZodOptional<z.ZodString>;
129+
readonly configScope: ConfigScope;
130+
readonly description: 'Header containing user OIDC token';
131+
};
132+
readonly 'boost.devSpaces.credentials': {
133+
readonly schema: z.ZodOptional<z.ZodString>;
134+
readonly configScope: ConfigScope;
135+
readonly description: 'DevSpaces integration credentials';
136+
readonly sensitive: true;
137+
};
138+
};
139+
140+
// @public
141+
export type BoostConfigKey = keyof typeof boostConfigFields;
142+
37143
// @public
38144
const boostPlugin: BackendFeature;
39145
export default boostPlugin;
40146

147+
// @public
148+
export interface ConfigFieldMeta<T extends z.ZodTypeAny = z.ZodTypeAny> {
149+
configScope: ConfigScope;
150+
description: string;
151+
schema: T;
152+
sensitive?: boolean;
153+
}
154+
155+
// @public
156+
export type ConfigScope = 'yaml-only' | 'db-overridable' | 'db-only';
157+
41158
// @public
42159
export function createAgentResourceLoader(): ResourceLoader;
43160

44161
// @public
45162
export function createToolResourceLoader(): ResourceLoader;
46163

164+
// @public
165+
export function decryptValue(encrypted: string, secret: string): string;
166+
167+
// @public
168+
export function encryptValue(plaintext: string, secret: string): string;
169+
170+
// @public
171+
export function isDbWritable(key: BoostConfigKey): boolean;
172+
173+
// @public
174+
export function isSensitiveField(key: BoostConfigKey): boolean;
175+
47176
// @public
48177
export class ProviderManager {
49178
getActiveProvider(): AgenticProvider;
@@ -62,9 +191,37 @@ export type ResourceLoader = (req: Request_2) => Promise<
62191
| undefined
63192
>;
64193

194+
// @public
195+
export class RuntimeConfigResolver {
196+
constructor(options: RuntimeConfigResolverOptions);
197+
invalidate(): Promise<void>;
198+
remove(key: BoostConfigKey): Promise<void>;
199+
resolve(key: BoostConfigKey): Promise<unknown | undefined>;
200+
resolveAll(): Promise<Map<string, unknown>>;
201+
set(key: BoostConfigKey, value: unknown): Promise<void>;
202+
}
203+
204+
// @public
205+
export interface RuntimeConfigResolverOptions {
206+
// (undocumented)
207+
adminConfigService: AdminConfigService;
208+
// (undocumented)
209+
cache: CacheService;
210+
// (undocumented)
211+
config: RootConfigService;
212+
// (undocumented)
213+
logger: LoggerService;
214+
}
215+
65216
// @public
66217
export type SecurityMode = 'development-only-no-auth' | 'plugin-only' | 'full';
67218

219+
// @public
220+
export function validateConfigValue(
221+
key: BoostConfigKey,
222+
value: unknown,
223+
): unknown;
224+
68225
// @public
69226
export function validateSecurityMode(
70227
mode: string | undefined,

0 commit comments

Comments
 (0)