Skip to content

Commit 112eb22

Browse files
fullsend-ai-coder[bot]claudegabemontero
authored
feat(#3299): runtime configuration engine with Zod validation (#3521)
* 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> * fix(boost): address security review findings for runtime config engine - Add boost.admin permission check to /config/status endpoint - Redact sensitive fields in /config/status response - Return generic error message, log details server-side - Exclude sensitive fields from effective-config cache - Fetch sensitive fields fresh from DB on cache hit - Replace SELECT-then-INSERT/UPDATE with atomic onConflict().merge() - Return undefined + log warning when encryptionSecret missing on read - Clear cached knexPromise on rejection for retry - Add encryptionSecret to config.d.ts with @visibility secret - Replace require('crypto') with ES import for createHash - Fix misleading codegen comment in config.d.ts - Add upsert-update test case Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: gabemontero <gmontero@redhat.com> * fix(boost): regenerate API report for CI compatibility Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(boost): address round-3 security findings - Guard isSensitiveField against unknown DB keys (nil-deref) - Wrap JSON.parse in try/catch in getOverride/getAllOverrides - Warn at startup when encrypted DB values exist without encryptionSecret Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(boost): tighten API surface and harden error paths - Remove encryptValue/decryptValue from public exports (internal only) - Mark encryptionSecret, set(), remove() as @internal - Auto-delete corrupt JSON rows in getOverride - Handle decryption failures from rotated secrets gracefully - Use knex.fn.now() for DB timestamps instead of client-side Date - Add tests for corrupt JSON and rotated-secret error paths Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: gabemontero <gmontero@redhat.com> * fix(boost): prevent data loss on secret rotation and add auth policy - validateStoredValues: separate decryption errors from validation errors. Rows that fail decryption (rotated secret) are kept intact with a warning instead of being deleted. Only rows that fail Zod schema validation after successful decryption are removed. - Add httpRouter.addAuthPolicy for /config/status endpoint to match the pattern used by all other endpoints in the codebase. - Add tests for both new behaviors (2 new test cases, 79 total). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: gabemontero <gmontero@redhat.com> --------- Signed-off-by: gabemontero <gmontero@redhat.com> Co-authored-by: fullsend-code <278716306+fullsend-ai-coder[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: gabemontero <gmontero@redhat.com>
1 parent 2985dab commit 112eb22

15 files changed

Lines changed: 2298 additions & 4 deletions
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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+
* Mirrors the field definitions in `src/config/schemas.ts`.
21+
* Keep both in sync when adding or changing config fields.
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+
/**
140+
* Secret used for encrypting sensitive config values stored in the database.
141+
* Must be a high-entropy string (e.g., 32+ random characters).
142+
* @visibility secret
143+
*/
144+
encryptionSecret?: string;
145+
};
146+
}

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: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,35 @@
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+
// (undocumented)
36+
logger: LoggerService;
37+
}
1638

1739
// @public
1840
export function authorizeLifecycleAction(
@@ -27,23 +49,123 @@ export interface AuthorizeLifecycleActionOptions {
2749
permissions: PermissionsService;
2850
}
2951

52+
// @public
53+
export const BOOST_CONFIG_SCHEMA_VERSION = 1;
54+
3055
// @public
3156
export const boostAiProviderServiceFactory: ServiceFactory<
3257
AgenticProvider,
3358
'plugin',
3459
'singleton'
3560
>;
3661

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

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

44160
// @public
45161
export function createToolResourceLoader(): ResourceLoader;
46162

163+
// @public
164+
export function isDbWritable(key: BoostConfigKey): boolean;
165+
166+
// @public
167+
export function isSensitiveField(key: BoostConfigKey): boolean;
168+
47169
// @public
48170
export class ProviderManager {
49171
getActiveProvider(): AgenticProvider;
@@ -62,9 +184,35 @@ export type ResourceLoader = (req: Request_2) => Promise<
62184
| undefined
63185
>;
64186

187+
// @public
188+
export class RuntimeConfigResolver {
189+
constructor(options: RuntimeConfigResolverOptions);
190+
invalidate(): Promise<void>;
191+
resolve(key: BoostConfigKey): Promise<unknown | undefined>;
192+
resolveAll(): Promise<Map<string, unknown>>;
193+
}
194+
195+
// @public
196+
export interface RuntimeConfigResolverOptions {
197+
// (undocumented)
198+
adminConfigService: AdminConfigService;
199+
// (undocumented)
200+
cache: CacheService;
201+
// (undocumented)
202+
config: RootConfigService;
203+
// (undocumented)
204+
logger: LoggerService;
205+
}
206+
65207
// @public
66208
export type SecurityMode = 'development-only-no-auth' | 'plugin-only' | 'full';
67209

210+
// @public
211+
export function validateConfigValue(
212+
key: BoostConfigKey,
213+
value: unknown,
214+
): unknown;
215+
68216
// @public
69217
export function validateSecurityMode(
70218
mode: string | undefined,

0 commit comments

Comments
 (0)