Skip to content

feat(#3299): runtime configuration engine with Zod validation#3521

Merged
gabemontero merged 6 commits into
mainfrom
feat/3299-runtime-config-engine
Jun 23, 2026
Merged

feat(#3299): runtime configuration engine with Zod validation#3521
gabemontero merged 6 commits into
mainfrom
feat/3299-runtime-config-engine

Conversation

@fullsend-ai-coder

Copy link
Copy Markdown
Contributor

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


Closes #3299

Post-script verification

  • Branch is not main/master (feat/3299-runtime-config-engine)
  • Secret scan passed (gitleaks — 2e647e201781a7b120cb2e71fab2e8740be418e9..HEAD)
  • Pre-commit hooks passed (authoritative run on runner)
  • Tests ran inside sandbox

@fullsend-ai-coder fullsend-ai-coder Bot requested review from a team, durandom and gabemontero as code owners June 22, 2026 03:28
@rhdh-gh-app

rhdh-gh-app Bot commented Jun 22, 2026

Copy link
Copy Markdown

Missing Changesets

The following package(s) are changed by this PR but do not have a changeset:

  • @red-hat-developer-hub/backstage-plugin-boost-backend

See CONTRIBUTING.md for more information about how to add changesets.

Changed Packages

Package Name Package Path Changeset Bump Current Version
@red-hat-developer-hub/backstage-plugin-boost-backend workspaces/boost/plugins/boost-backend none v0.1.1

@codecov

codecov Bot commented Jun 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.87649% with 48 lines in your changes missing coverage. Please review.
✅ Project coverage is 53.71%. Comparing base (2985dab) to head (b35c5b3).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3521      +/-   ##
==========================================
+ Coverage   53.63%   53.71%   +0.07%     
==========================================
  Files        2260     2264       +4     
  Lines       85976    86227     +251     
  Branches    24147    24183      +36     
==========================================
+ Hits        46117    46320     +203     
- Misses      39687    39735      +48     
  Partials      172      172              
Flag Coverage Δ *Carryforward flag
adoption-insights 83.70% <ø> (ø) Carriedforward from 5f91eb5
ai-integrations 67.95% <ø> (ø) Carriedforward from 5f91eb5
app-defaults 69.79% <ø> (ø) Carriedforward from 5f91eb5
augment 46.39% <ø> (ø) Carriedforward from 5f91eb5
boost 78.62% <80.87%> (+3.97%) ⬆️
bulk-import 72.46% <ø> (ø) Carriedforward from 5f91eb5
cost-management 14.10% <ø> (ø) Carriedforward from 5f91eb5
dcm 61.79% <ø> (ø) Carriedforward from 5f91eb5
extensions 61.53% <ø> (ø) Carriedforward from 5f91eb5
global-floating-action-button 71.18% <ø> (ø) Carriedforward from 5f91eb5
global-header 59.71% <ø> (ø) Carriedforward from 5f91eb5
homepage 49.92% <ø> (ø) Carriedforward from 5f91eb5
install-dynamic-plugins 56.23% <ø> (ø) Carriedforward from 5f91eb5
konflux 91.49% <ø> (ø) Carriedforward from 5f91eb5
lightspeed 68.57% <ø> (ø) Carriedforward from 5f91eb5
mcp-integrations 85.46% <ø> (ø) Carriedforward from 5f91eb5
orchestrator 37.79% <ø> (ø) Carriedforward from 5f91eb5
quickstart 63.76% <ø> (ø) Carriedforward from 5f91eb5
sandbox 79.56% <ø> (ø) Carriedforward from 5f91eb5
scorecard 83.96% <ø> (ø) Carriedforward from 5f91eb5
theme 61.26% <ø> (ø) Carriedforward from 5f91eb5
translations 7.25% <ø> (ø) Carriedforward from 5f91eb5
x2a 78.68% <ø> (ø) Carriedforward from 5f91eb5

*This pull request uses carry forward flags. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2985dab...b35c5b3. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@fullsend-ai-review

fullsend-ai-review Bot commented Jun 22, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 3:30 AM UTC · Completed 3:48 AM UTC
Commit: 2e647e2 · View workflow run →

@fullsend-ai-review

fullsend-ai-review Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review

Findings

Medium

  • [Weak Key Derivation] workspaces/boost/plugins/boost-backend/src/config/encryption.ts:71 — The encryption key is derived using a single pass of SHA-256. While acceptable for high-entropy secrets, a proper KDF (HKDF or PBKDF2) would be more robust against brute-force if a weak secret is configured.
    Remediation: Replace deriveKey with crypto.hkdfSync('sha256', secret, salt, 'boost-config-encryption', 32) or similar.

  • [Fail-Open Credential Storage] workspaces/boost/plugins/boost-backend/src/config/AdminConfigService.ts:93 — When encryptionSecret is not configured, sensitive fields become silently unreadable (getOverride returns undefined, getAllOverrides skips them). A warning is logged during validateStoredValues, but the plugin does not fail initialization.
    Remediation: If encryptionSecret is absent and sensitive field overrides exist in DB at startup, fail plugin initialization with a clear error.

  • [error handling gap] workspaces/boost/plugins/boost-backend/src/config/RuntimeConfigResolver.ts:133 — In getEffectiveConfig Layer 2, DB overrides are applied unconditionally without checking if the key is in boostConfigFields. Stale or directly-inserted DB rows with unknown keys could appear in resolveAll() results.
    Remediation: Add guard: if (!(key in boostConfigFields)) continue; before effective.set(key, value).

  • [architecture-coherence] workspaces/boost/plugins/boost-backend/config.d.ts:26 — Design document says config.d.ts is generated from Zod schemas, but the file is hand-written with a comment "Keep both in sync." Risk of divergence over time.
    Remediation: Implement code generation from Zod, or update design doc and add a CI sync check.

  • [architecture-coherence] workspaces/boost/plugins/boost-backend/src/config/AdminConfigService.test.ts:1 — AGENTS.md states integration tests use real database and cache backends, not mocks. All tests in this PR use hand-built mock Knex clients and mock CacheService.
    Remediation: Add integration tests using @backstage/backend-test-utils with real SQLite/PostgreSQL alongside the existing unit tests.

  • [api-contract] workspaces/boost/plugins/boost-backend/src/config/AdminConfigService.ts:46AdminConfigServiceOptions public interface omits encryptionSecret, but the implementation relies on it for encryption. External consumers cannot provide it through the documented API.
    Remediation: Either make encryptionSecret part of public AdminConfigServiceOptions or restructure encryption injection.

  • [architecture-coherence] workspaces/boost/plugins/boost-backend/src/config/AdminConfigService.ts:89AdminConfigService creates the boost_admin_config table via inline DDL (ensureTable). Backstage plugins conventionally use the DatabaseService migration system for versioned schema management.
    Remediation: Use Backstage knex migrations directory for table creation.

  • [stale-doc] workspaces/boost/openspec/changes/platform-operations-deployment/proposal.md:72 — Proposal says RuntimeConfigResolver lives at src/services/ but PR places it at src/config/. See also: wrong config.d.ts path at line 74 (says boost-common/src/ but actual is boost-backend/).
    Remediation: Update path references in proposal.md.

  • [stale-doc] workspaces/boost/specifications/prd/pluggable-ai-platform-architecture.md:214 — Cache inventory references services/RuntimeConfigResolver.ts but actual path is src/config/RuntimeConfigResolver.ts.
    Remediation: Update path in cache inventory table.

  • [missing-doc] workspaces/boost/openspec/changes/platform-operations-deployment/specs/runtime-config/spec.md:115 — Credential encryption scenario doesn't mention the required boost.encryptionSecret config key. Deployers won't know to set it.
    Remediation: Add boost.encryptionSecret to the spec as a yaml-only, secret-visibility config field.

Low

  • [No Minimum Entropy Enforcement] workspaces/boost/plugins/boost-backend/src/plugin.ts:135 — Encryption secret read with no validation of length. config.d.ts says "32+ random characters" but not enforced in code.

  • [DB Override Bypasses yaml-only Scope] workspaces/boost/plugins/boost-backend/src/config/RuntimeConfigResolver.ts:197 — Layer 2 overwrites Layer 1 for all keys without checking configScope. Direct DB writes could override yaml-only fields like boost.security.mode. Defense-in-depth improvement.

  • [Sensitive Config Value Logged] workspaces/boost/plugins/boost-backend/src/plugin.ts:226 — The /config/status catch block logs full Error object. Minor data exposure risk if error message contains sensitive values.

  • [logic error] workspaces/boost/plugins/boost-backend/src/config/RuntimeConfigResolver.ts:127 — On cache hit, getEffectiveConfig calls getAllOverrides() for sensitive fields fresh from DB on every call, even when no sensitive fields are being resolved. Performance concern.

  • [race condition] workspaces/boost/plugins/boost-backend/src/config/RuntimeConfigResolver.ts:104getEffectiveConfig has no concurrency guard. Concurrent cache-miss calls perform redundant rebuilds. Consider promise deduplication pattern (as used in getDb()).

  • [error handling gap] workspaces/boost/plugins/boost-backend/src/config/AdminConfigService.ts:69getOverride does not validate key at runtime. While TypeScript prevents invalid keys at compile time, runtime callers via JSON deserialization could pass arbitrary strings.

  • [api-contract] workspaces/boost/plugins/boost-backend/src/config/RuntimeConfigResolver.ts:149set() and remove() methods marked @internal on a @public class. Consider clarifying the intended write path.

  • [error-handling-idiom] workspaces/boost/plugins/boost-backend/src/plugin.ts:192/config/status uses inline try/catch with manual status codes rather than Backstage error classes (NotAllowedError) and Express error middleware.

  • [scope-creep] workspaces/boost/plugins/boost-backend/src/plugin.ts:190 — The /config/status endpoint is not explicitly listed in issue boost-backend — Runtime configuration engine with Zod validation (issue 3 of 15) #3299's task breakdown, though it supports the admin onboarding scenario.

  • [No Key Rotation Support] workspaces/boost/plugins/boost-backend/src/config/encryption.ts:86 — No key version in ciphertext envelope. Key rotation makes values unreadable with no re-encryption path.

  • [edge-case] workspaces/boost/plugins/boost-backend/src/config/encryption.ts:67decryptValue doesn't validate minimum buffer length before slicing.

  • [test-inadequate] workspaces/boost/plugins/boost-backend/src/config/RuntimeConfigResolver.test.ts:125 — No test for cache-hit path verifying sensitive fields are merged from fresh DB data.

  • [naming-convention] workspaces/boost/plugins/boost-backend/src/plugin.ts:119_permissions uses underscore prefix (conventionally unused) but is actively used in /config/status handler.

  • [stale-doc] workspaces/boost/specifications/prd/platform-operations-deployment.md:173 — PRD describes Zod validation as aspirational "target architecture" but this PR implements it.

Info

  • [pattern-inconsistency] workspaces/boost/plugins/boost-backend/src/config/schemas.ts:192 — Individual as ConfigScope casts are redundant given as const satisfies already enforces type correctness.

  • [auth-review] workspaces/boost/plugins/boost-backend/src/plugin.ts:237/config/status uses user-cookie auth + boostAdminPermission check. Defense-in-depth pattern is correct.

Previous run

Review

Findings

Medium

  • [fail-open] src/config/AdminConfigService.ts:89 — When encryptionSecret is absent, encrypted sensitive fields become silently unreadable (getOverride returns undefined with a warn log, getAllOverrides skips them). The write path correctly fails closed (setOverride throws InputError), but the read path silently degrades, masking data loss after secret rotation or accidental config removal. validateStoredValues warns on startup but does not escalate to ERROR or fail startup.
    Remediation: On startup, if encryptionSecret is undefined but sensitive fields exist in DB, log at ERROR level and surface the misconfiguration in /config/status.

  • [weak-key-derivation] src/config/encryption.ts:31deriveKey uses a single-pass SHA-256 hash to derive the AES-256 encryption key. SHA-256 provides no brute-force resistance for low-entropy secrets. Industry practice is to use HKDF, PBKDF2, or scrypt with a salt.
    Remediation: Replace with crypto.hkdf('sha256', secret, salt, 'boost-config-encryption', 32) or crypto.pbkdf2Sync(). At minimum, document that encryptionSecret must be a high-entropy random string of 32+ characters.

  • [logic-error] src/config/RuntimeConfigResolver.ts:161getEffectiveConfig() applies all DB overrides unconditionally without checking configScope. If a yaml-only key exists in the DB (e.g., via direct SQL), it silently overrides the YAML value. The write path (setOverride) guards against this, but the resolver lacks defense-in-depth.
    Remediation: In getEffectiveConfig(), check isDbWritable(key as BoostConfigKey) before calling effective.set(key, value).

  • [edge-case] src/config/AdminConfigService.ts:123getOverride deletes rows with corrupt JSON but does NOT delete rows that fail decryption. Undecryptable rows persist permanently, causing error-level log spam on every access. validateStoredValues removes Zod validation failures but does not clean up rows that are unreadable due to a missing or rotated encryption secret.
    Remediation: Either delete rows that consistently fail decryption, or have validateStoredValues remove sensitive rows when no encryption secret is configured.

  • [authorization] src/plugin.ts:193 — No httpRouter.addAuthPolicy for /config/status. Only /health has an explicit auth policy. The endpoint correctly checks httpAuth.credentials and permissions.authorize, but Backstage best practice is to declare explicit auth policies rather than relying on implicit defaults.
    Remediation: Add httpRouter.addAuthPolicy({ path: '/config/status', allow: 'user-cookie' }) or equivalent.

  • [missing-test] src/plugin.ts:193 — No tests for the /config/status endpoint covering: (1) unauthenticated request returns 403, (2) non-admin user returns 403, (3) admin user gets redacted config, (4) sensitive fields are properly redacted.
    Remediation: Add integration tests for the endpoint.

  • [stale-doc] workspaces/boost/README.md — README states plugins/ contains "not yet created" (line 31) and the Plugins table says "coming soon" (line 44), but three plugin packages already exist (boost-backend, boost-common, boost-node). This pre-dates this PR but is worth fixing.
    Remediation: Update README directory structure and Plugins table.

Low

  • [logic-error] src/config/schemas.ts:63ConfigScope type defines 'db-only' but no field uses it. readYamlValue in RuntimeConfigResolver would incorrectly read YAML fallback values for future db-only fields.

  • [edge-case] src/config/RuntimeConfigResolver.ts:141isSensitiveField is called with key as BoostConfigKey where keys come from the DB and may not be valid BoostConfigKey values. Unknown keys silently bypass sensitivity checks.

  • [edge-case] src/config/schemas.ts:192validateConfigValue would throw a confusing TypeError for non-existent keys rather than a descriptive error.

  • [data-exposure] src/plugin.ts:206 — Redaction logic casts arbitrary DB keys as BoostConfigKey. Unknown keys bypass isSensitiveField and would be returned unredacted in /config/status responses.

  • [api-contract] src/config/AdminConfigService.ts:76encryptionSecret property is not in the public AdminConfigServiceOptions interface in report.api.md (intentionally @internal, but may confuse consumers).

Info

  • [permission-expansion] src/plugin.ts:195boostAdminPermission has action: 'update' but is used to gate a read-only GET endpoint (/config/status). Semantic mismatch between permission intent and usage.

  • [encryption] src/config/AdminConfigService.ts:90 — No re-encryption migration path after secret rotation. Previously encrypted values become permanently unreadable with no tooling to re-encrypt.

  • [sub-agent-failure] The style-conventions review dimension could not run because the PR branch files were not available on disk (repo checked out on main).

Previous run (2)

Review

Findings

High

  • [error handling / data loss] workspaces/boost/plugins/boost-backend/src/config/AdminConfigService.ts:288 — In validateStoredValues(), if the encryption secret is changed or rotated between deployments, decryptValue() will throw for all previously-encrypted sensitive fields. The catch block then deletes these rows from the database, permanently destroying the encrypted config overrides. The warning log says "Removing invalid config override" which does not clearly indicate that an encryption secret mismatch is the root cause.
    Remediation: Catch the decryption error specifically and log a distinct, more alarming message (e.g., "encryption secret may have changed") without deleting the row. Only delete rows that fail Zod schema validation after successful decryption.

  • [architectural coherence] workspaces/boost/plugins/boost-backend/src/config/AdminConfigService.test.ts — AGENTS.md states: "Integration tests use real database and cache backends, not mocks." The AdminConfigService tests use an elaborate hand-built mock Knex client (createMockKnex). The RuntimeConfigResolver tests similarly mock CacheService and AdminConfigService. This directly violates the project's stated testing convention.
    Remediation: Use Backstage's test database utilities (TestDatabases from @backstage/backend-test-utils) and real cache backends for integration tests, as mandated by AGENTS.md.

Medium

  • [Missing Auth Policy Declaration] workspaces/boost/plugins/boost-backend/src/plugin.ts:193 — The /config/status endpoint performs its own permission check (boostAdminPermission) but is not registered with httpRouter.addAuthPolicy(). The existing /health endpoint has an explicit auth policy. This omission means the Backstage framework cannot report this route in auth policy audits, and the codebase convention requires explicit declaration for every route.
    Remediation: Add an explicit auth policy entry for /config/status alongside the existing /health policy.

  • [scope alignment] workspaces/boost/plugins/boost-backend/config.d.ts — Task 2.2 specifies "Generate config.d.ts types from Zod schemas", but the added config.d.ts is hand-written with the comment "Keep both in sync". This contradicts the design decision that Zod schemas are the single source of truth and creates a synchronization risk the spec aimed to prevent.
    Remediation: Either generate config.d.ts programmatically from the Zod schemas (e.g., via a build script), or document why generation was deferred and open a follow-up issue.

Low

  • [logic error] workspaces/boost/plugins/boost-backend/src/config/RuntimeConfigResolver.ts:150 — The YAML baseline loop iterates over all boostConfigFields keys including fields with configScope: 'db-only'. No db-only fields exist today, but the code will silently include YAML values for db-only fields when one is added.

  • [cache effectiveness] workspaces/boost/plugins/boost-backend/src/config/RuntimeConfigResolver.ts:137 — On a cache hit, getEffectiveConfig() calls getAllOverrides() to fetch sensitive fields, which reads ALL rows and decrypts. The table is small (~14 rows max), so this is a minor concern.

  • [race condition] workspaces/boost/plugins/boost-backend/src/config/RuntimeConfigResolver.ts:111 — The set() method has a small race window between DB write and cache invalidation. Acceptable for infrequent admin config writes with a 30s TTL.

  • [test integrity] workspaces/boost/plugins/boost-backend/src/config/AdminConfigService.test.ts:163 — The "encrypts sensitive fields" test assertion expect(rawValue).not.toBe('my-secret-token') is weak; it would pass for any transformation, not just encryption.

  • [naming-conventions] workspaces/boost/plugins/boost-backend/src/plugin.ts:116_permissions is prefixed with underscore (indicating unused) but is now actively used via _permissions.authorize(). Should be renamed to permissions.

  • [Weak Key Derivation] workspaces/boost/plugins/boost-backend/src/config/encryption.ts:37deriveKey uses unsalted SHA-256. Acceptable for high-entropy secrets (documented as "32+ random characters"), but consider HKDF for defense in depth.

  • [scope creep] workspaces/boost/plugins/boost-backend/src/plugin.ts:190 — The /config/status endpoint is not explicitly listed in the authorized tasks for issue boost-backend — Runtime configuration engine with Zod validation (issue 3 of 15) #3299, though it is a natural companion to the config engine.

  • [code-organization] workspaces/boost/plugins/boost-backend/src/plugin.ts:240 — The /config/status route handler is inlined (~25 lines) rather than extracted into a middleware helper per the existing authorizeLifecycleAction pattern.

  • [api-shape-patterns] workspaces/boost/plugins/boost-backend/src/config/AdminConfigService.ts:50AdminConfigServiceOptions has inconsistent TSDoc: database and logger lack comments while encryptionSecret has one.

  • [api-shape-patterns] workspaces/boost/plugins/boost-backend/src/config/RuntimeConfigResolver.ts:55RuntimeConfigResolverOptions fields all lack TSDoc comments.

  • [code-organization] workspaces/boost/plugins/boost-backend/src/plugin.ts:239 — Imports use direct sub-module paths (./config/AdminConfigService) rather than the barrel ./config index created in the same PR.

Info

  • [Encryption/Decryption Functions Exported as Public API] workspaces/boost/plugins/boost-backend/src/index.ts:46encryptValue and decryptValue are exported from the package's public API surface. Consider keeping them internal.

  • [Sensitive Field Detection Bypass via Unknown DB Keys] workspaces/boost/plugins/boost-backend/src/config/RuntimeConfigResolver.ts:141 — Unknown DB keys bypass isSensitiveField due to type casting. Mitigated by validateStoredValues() cleanup on startup.

Previous run (3)

Review

Findings

High

  • [nil-deref] workspaces/boost/plugins/boost-backend/src/config/RuntimeConfigResolver.ts:148 — In getEffectiveConfig, the cache-hit path calls isSensitiveField(key as BoostConfigKey) for every key in the dbOverrides Map (line ~148) and again for every key in cacheSafe (line ~175). If the database contains a key not present in boostConfigFields, boostConfigFields[key] evaluates to undefined, and accessing .sensitive on undefined throws a TypeError at runtime. AdminConfigService.getAllOverrides correctly guards with key in boostConfigFields before calling isSensitiveField, but RuntimeConfigResolver does not. This also surfaces as a 500 error in the /config/status endpoint (plugin.ts:203).
    Remediation: Guard with key in boostConfigFields before calling isSensitiveField, or make isSensitiveField defensive by returning false for unknown keys.

Medium

  • [error-handling] workspaces/boost/plugins/boost-backend/src/config/AdminConfigService.ts:142JSON.parse(row.value) in getOverride and getAllOverrides is not wrapped in try/catch. If a DB row contains malformed JSON, this throws an unhandled exception. In getAllOverrides, this aborts the entire loop losing subsequent overrides. (validateStoredValues already has a try/catch.)
    Remediation: Wrap JSON.parse(row.value) in try/catch. In getAllOverrides, skip the row and log a warning.

  • [weak-kdf] workspaces/boost/plugins/boost-backend/src/config/encryption.ts:33deriveKey uses a single unsalted SHA-256 hash to derive the AES-256 key. While the config requires a high-entropy secret (32+ random characters), this is advisory only and not enforced. A proper KDF (PBKDF2/HKDF with salt) would provide defense-in-depth.
    Remediation: Replace with crypto.pbkdf2Sync or crypto.hkdf with a random salt, or enforce minimum entropy on encryptionSecret at startup.

  • [fail-open] workspaces/boost/plugins/boost-backend/src/plugin.ts:132encryptionSecret is optional. When absent, writing sensitive fields throws InputError (good), but reading a previously encrypted sensitive field returns undefined with only a warning log. getAllOverrides skips sensitive fields silently. While the encrypted data itself is not exposed, this silent degradation could cause operational confusion.
    Remediation: Surface the missing-secret condition explicitly (e.g., throw at startup if DB contains encrypted values but no secret is configured).

  • [public-api-dependency-leak] workspaces/boost/plugins/boost-backend/report.api.md:13 — The public API re-exports Zod types directly (z.ZodString, z.ZodEnum, etc.) in boostConfigFields and ConfigFieldMeta. This creates hard coupling to Zod v3. A future Zod major version bump would be a breaking change for all consumers.
    Remediation: Wrap Zod schemas behind an opaque validation interface.

  • [scope-alignment] workspaces/boost/plugins/boost-backend/config.d.ts:27 — Issue boost-backend — Runtime configuration engine with Zod validation (issue 3 of 15) #3299 task 2.2 specifies "Generate config.d.ts types from Zod schemas," and AGENTS.md states "TypeScript types are generated from Zod. No hand-written validators." The PR introduces a hand-written config.d.ts with the comment "Keep both in sync." Multiple spec documents (design.md, proposal.md, tasks.md, PRD) also reference generation. See also: [stale-doc] findings for these documents.
    Remediation: Add a code generation step or document why generation was deferred and open a follow-up issue.

  • [architectural-conflict] workspaces/boost/plugins/boost-backend/src/config/AdminConfigService.test.ts:1 — AGENTS.md testing rule states: "Integration tests use real database and cache backends, not mocks." All test files use hand-built mock Knex clients and mock CacheService objects. Unit tests with mocks are fine, but integration tests with real backends are also required per project conventions.
    Remediation: Add integration tests using @backstage/backend-test-utils (TestDatabases/createTestDatabaseService) alongside the existing unit tests.

Low

  • [edge-case] AdminConfigService.ts:146 — If a sensitive field's stored value is non-string after JSON.parse, decryption is skipped and the raw value is returned.
  • [race-condition] RuntimeConfigResolver.ts:140getEffectiveConfig has no concurrency guard; stale value could be cached after invalidation (mitigated by 30s TTL).
  • [test-inadequate] AdminConfigService.test.ts:94 — Mock Knex insert has an unreachable then fallback that creates duplicate rows, masking potential upsert bugs.
  • [logic-error] RuntimeConfigResolver.ts:143 — On cache hit, getAllOverrides() is still called for sensitive fields, incurring a full DB query on every request.
  • [auth-config] plugin.ts:190 — No explicit addAuthPolicy for /config/status; relies on Backstage's implicit default (authenticated by default + explicit permission check).
  • [api-surface] index.ts:44encryptValue/decryptValue exported as public API; should be @internal.
  • [naming-convention] AdminConfigService.ts:65logger.child({ service: 'ClassName' }) pattern is new to this codebase.
  • [error-handling-idiom] plugin.ts:214 — Manual try/catch with 500 JSON response instead of Express error propagation.
  • [api-shape] plugin.ts:190 — Inline permission checking instead of using a middleware abstraction.
  • [code-organization] plugin.ts:40 — Direct submodule imports instead of barrel import from ./config.
  • [api-shape] AdminConfigService.ts:44encryptionSecret passed as constructor option instead of read from config service.
  • [scope-creep] plugin.ts:190/config/status endpoint not listed in issue boost-backend — Runtime configuration engine with Zod validation (issue 3 of 15) #3299 tasks.
  • [naming-coherence] plugin.ts:111_permissions (underscore prefix suggesting unused) is actually used on line 196.
  • [stale-doc] proposal.md:30 — States config.d.ts is generated and at wrong location.
  • [missing-doc] prd.md:190boost.encryptionSecret config key not documented.
  • [missing-doc] prd.md:161/config/status endpoint not documented.
  • [unversioned-format] index.ts:45 — Encryption wire format has no version prefix for future migration.

Info

  • [positive-finding] schemas.ts:100boost.security.mode correctly scoped as yaml-only, preventing runtime admin panel override.
  • [stale-doc] README.md:43 — Plugins table still shows "coming soon" (pre-existing).
Previous run (4)

Review

Reason: stale-head

The review agent reviewed commit 606196005424908fbde62ab04edf93f319edc085 but the PR HEAD is now 4ad38b459d7db12c60f2cce49d47fec5ec9b35cd. This review was discarded to avoid approving unreviewed code.

Previous run (5)

Review

Findings

High

  • [data-exposure] src/plugin.ts:184 — The /config/status endpoint exposes all resolved configuration values (including decrypted sensitive values like boost.devSpaces.credentials) to any authenticated Backstage user. RuntimeConfigResolver.resolveAll() calls AdminConfigService.getAllOverrides() which decrypts sensitive fields when encryptionSecret is set, and the plaintext values are serialized directly into the JSON response. There is no permission check or role-based authorization, and no filtering of sensitive fields.
    Remediation: Add a Backstage permission check (e.g., boostAdminConfigRead) to the /config/status handler, gating access to administrator roles only. Additionally, filter out or redact fields marked sensitive: true from the response payload.

Medium

  • [fail-open] src/config/AdminConfigService.ts:86 — In getOverride() and getAllOverrides(), decryption is conditional on this.encryptionSecret being truthy. If a sensitive field was written with encryption and the encryption secret is later removed, the raw ciphertext (base64 string) is returned as-is and treated as the config value downstream. Same issue affects validateStoredValues() where ciphertext passes Zod string validation but is semantically invalid.
    Remediation: When isSensitiveField(key) is true and this.encryptionSecret is absent, log a warning and return undefined rather than silently returning ciphertext.

  • [auth-bypass] src/plugin.ts:184 — The /config/status route has no in-handler permission check. While Backstage defaults to requiring authentication, there is no authorization (permission) check, so any authenticated user can read admin config. See also: [data-exposure] finding at this location.
    Remediation: Add explicit permission check within the handler to restrict to admin users.

  • [error-handling-gap] src/config/AdminConfigService.ts:83 — The getDb() method caches the Knex connection promise. If database.getClient() or ensureTable() throws on the first call, the rejected promise is permanently cached, making the service permanently broken with no recovery path.
    Remediation: Clear this.knexPromise in the catch path so that a subsequent call can retry.

  • [race-condition] src/config/AdminConfigService.ts:108 — The upsert in setOverride uses a check-then-act pattern (select then insert/update). Two concurrent calls for the same key can both see no existing row and both attempt insert, causing a primary-key constraint violation.
    Remediation: Use knex(TABLE_NAME).insert({...}).onConflict('key').merge() for atomic upsert, or wrap in a transaction.

  • [api-contract] config.d.ts:29plugin.ts reads config.getOptionalString('boost.encryptionSecret') but this key is not declared in config.d.ts. Backstage's config schema validation may reject or warn about undeclared config keys.
    Remediation: Add encryptionSecret to the Config interface in config.d.ts under the boost object, with @visibility secret annotation.

  • [insecure-deserialization] src/config/encryption.ts:49deriveKey() uses single-pass SHA-256 to derive an AES-256 key from the encryption secret. SHA-256 is a hash, not a KDF, and provides no protection against brute-force on low-entropy secrets.
    Remediation: Replace with PBKDF2, scrypt, or HKDF for key derivation.

  • [data-exposure] src/config/RuntimeConfigResolver.ts:152getEffectiveConfig() caches all resolved values — including decrypted credentials — in the shared CacheService for up to 30 seconds. If the cache backend is Redis or Memcached, plaintext credentials are stored in an additional location beyond the database.
    Remediation: Exclude sensitive fields from the cache payload, or encrypt them before caching.

  • [data-exposure] src/plugin.ts:197 — The /config/status error handler returns String(error) directly in the JSON response body, which could leak stack traces, database connection strings, or encryption error details to clients.
    Remediation: Return a generic error message to the client and log the full error server-side.

  • [logic-error] src/config/schemas.ts:117boost.model.baseUrl uses z.string().url() (required) and boost.model.name uses z.string().min(1) (required), but both are declared optional in config.d.ts (baseUrl?: string, name?: string). validateConfigValue('boost.model.baseUrl', undefined) would throw, inconsistent with the TypeScript declaration.
    Remediation: Align Zod schemas with TypeScript declarations — either make schemas optional or make config.d.ts fields required.

Low / Info

  • [code-quality] src/config/encryption.ts:51deriveKey() uses require('crypto') despite createCipheriv, createDecipheriv, and randomBytes being statically imported at the top of the file. createHash should be added to the static import.

  • [test-inadequate] src/config/AdminConfigService.test.ts:55 — The mock Knex implementation creates independent query builder instances per call, so the mock cannot detect the TOCTOU race condition in the upsert pattern.

  • [weak-config] src/plugin.ts:128 — Encryption secret read from config with no validation of length or entropy. An operator could set a single-character secret, rendering encryption weak.

  • [api-shape] src/index.ts:45encryptValue and decryptValue are exported as @public API. Exposing raw encryption primitives increases misuse risk; these are internal implementation details.

  • [sub-agent-failure] N/A — The style-conventions, intent-coherence, and docs-currency sub-agents could not complete due to model unavailability. These dimensions were not reviewed.

Previous run (6)

Review

Findings

High

  • [data-exposure] workspaces/boost/plugins/boost-backend/src/plugin.ts:184 — The /config/status endpoint exposes all resolved configuration values including decrypted sensitive fields (e.g., boost.devSpaces.credentials) without any authorization check. Any authenticated Backstage user (not just admins) can retrieve decrypted credentials via GET /api/boost/config/status. No httpAuth.credentials() call or permission check is performed, and no addAuthPolicy is registered for this path.
    Remediation: (1) Add httpAuth.credentials(req) to enforce authentication. (2) Add a permission check (e.g., boost.config.read) to restrict to admin users. (3) Redact sensitive field values — never return decrypted credentials over the wire.

  • [data-exposure] workspaces/boost/plugins/boost-backend/src/config/RuntimeConfigResolver.ts:148resolveAll() returns all config values including decrypted sensitive credentials. These are then serialized to the Backstage cache in plaintext via cache.set('effective-config', cacheObj). Sensitive values should not be bulk-resolved or cached in cleartext.
    Remediation: Exclude sensitive fields from resolveAll() and the cache entirely, or keep them encrypted/redacted in the cache and only decrypt on authorized single-key retrieval.

  • [logic-error] workspaces/boost/plugins/boost-backend/src/config/AdminConfigService.ts:148 — Race condition (TOCTOU) in setOverride. Between the SELECT (.where({ key }).first()) and the subsequent UPDATE or INSERT, a concurrent request could insert a row with the same key, causing a primary-key collision, or a concurrent DELETE could cause the UPDATE to silently affect zero rows.
    Remediation: Replace the manual check-then-insert/update with knex(TABLE_NAME).insert({...}).onConflict('key').merge() for an atomic upsert, or wrap in knex.transaction().

  • [fail-open] workspaces/boost/plugins/boost-backend/src/config/AdminConfigService.ts:70 — In getOverride and getAllOverrides, when encryptionSecret is undefined but a sensitive field contains encrypted ciphertext, the method silently returns the raw base64 ciphertext to the caller. An operator who removes the encryption secret from config will unknowingly serve garbage data to downstream consumers without any error signal.
    Remediation: When isSensitiveField(key) is true and this.encryptionSecret is undefined, throw an error or return undefined with a logged warning.

Medium

  • [config-gap] workspaces/boost/plugins/boost-backend/src/plugin.ts:126boost.encryptionSecret is read from config via getOptionalString() but is not declared in config.d.ts or boostConfigFields. Without the @visibility secret annotation, Backstage may expose this value to the frontend bundle, leaking the encryption key.

  • [logic-error] workspaces/boost/plugins/boost-backend/src/config/RuntimeConfigResolver.ts:171 — The DB override layer in getEffectiveConfig applies all overrides without checking configScope. If a yaml-only key is inserted directly into the database, RuntimeConfigResolver would honor it, defeating the yaml-only scope guarantee.

  • [weak-crypto] workspaces/boost/plugins/boost-backend/src/config/encryption.ts:34deriveKey() uses unsalted SHA-256 to derive the AES-256 key. SHA-256 is not a key derivation function and provides no defense against brute-force on low-entropy secrets.

  • [data-exposure] workspaces/boost/plugins/boost-backend/src/plugin.ts:196 — The /config/status error handler returns raw String(error) in the JSON response, potentially leaking internal details such as DB connection strings or stack traces.

  • [design-direction] workspaces/boost/plugins/boost-backend/config.d.ts — The file header claims "Generated from Zod schemas" but no generation script or build step exists. Without a generation mechanism, config.d.ts can drift from the Zod schemas silently.

  • [pattern-inconsistency] workspaces/boost/plugins/boost-backend/src/config/encryption.ts:52deriveKey uses require('crypto') while other crypto imports use ES import. The codebase exclusively uses ES imports.

  • [error-handling-idiom] workspaces/boost/plugins/boost-backend/src/plugin.ts:193 — The /config/status error handler uses raw res.status(500).json() instead of forwarding via next(error), inconsistent with the existing codebase error-handling pattern.

  • [test-inadequate] workspaces/boost/plugins/boost-backend/src/config/AdminConfigService.test.ts — No test covers the upsert-update path of setOverride (calling it twice for the same key with different values).

  • [logic-error] workspaces/boost/plugins/boost-backend/src/config/RuntimeConfigResolver.ts:114readYamlValue speculatively calls getOptionalString/getOptionalNumber which throw on type mismatch. The outer try/catch silently swallows these errors, masking config problems.

  • [missing-doc] workspaces/boost/README.md — Plugins table says "coming soon" and directory structure says "not yet created" despite plugins existing.

  • [missing-doc] workspaces/boost/plugins/boost-backend/README.md — No README.md exists for the boost-backend plugin. Other backend plugins in the repo have READMEs.

Low / Info

  • [scope-creep] workspaces/boost/plugins/boost-backend/src/plugin.ts — Task 1.2 (cache infrastructure) from task group 1 included in this task group 2 PR. Pragmatic but crosses task group authorization.
  • [naming-convention] workspaces/boost/plugins/boost-backend/src/config/AdminConfigService.ts:43AdminConfigRow uses snake_case properties while the codebase uses camelCase for TS interfaces.
  • [pattern-inconsistency] workspaces/boost/plugins/boost-backend/src/config/schemas.ts:106 — Unnecessary cast in isSensitiveField; isDbWritable accesses .configScope without a cast.
  • [edge-case] workspaces/boost/plugins/boost-backend/src/config/encryption.ts:79decryptValue does not validate input length; short/empty input produces cryptic errors.
  • [missing-doc] workspaces/boost/README.md:38 — "pre-implementation" claim is outdated.
  • [api-shape] workspaces/boost/plugins/boost-backend/src/index.ts:45encryptValue/decryptValue exported as @public API; these low-level crypto primitives increase misuse risk.
  • [sub-agent-failure] N/A — Challenger sub-agent verified findings against main branch instead of PR diff. Pre-challenger finding set used as fallback.

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review comment for full details.

Comment thread workspaces/boost/plugins/boost-backend/src/plugin.ts
Comment thread workspaces/boost/plugins/boost-backend/src/plugin.ts
Comment thread workspaces/boost/plugins/boost-backend/config.d.ts
Comment thread workspaces/boost/plugins/boost-backend/src/config/encryption.ts
Comment thread workspaces/boost/plugins/boost-backend/src/plugin.ts
@gabemontero gabemontero force-pushed the feat/3299-runtime-config-engine branch from 174a46e to 6f9de3c Compare June 22, 2026 13:23
@fullsend-ai-review

fullsend-ai-review Bot commented Jun 22, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 1:25 PM UTC · Completed 1:36 PM UTC
Commit: a25f33d · View workflow run →

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review comment for full details.

Comment thread workspaces/boost/plugins/boost-backend/src/plugin.ts
Comment thread workspaces/boost/plugins/boost-backend/src/plugin.ts
Comment thread workspaces/boost/plugins/boost-backend/config.d.ts
Comment thread workspaces/boost/plugins/boost-backend/src/config/encryption.ts
Comment thread workspaces/boost/plugins/boost-backend/src/plugin.ts
Comment thread workspaces/boost/plugins/boost-backend/src/config/schemas.ts
@christoph-jerolimov christoph-jerolimov added the workspace/boost Boost workspace (Backstage AI plugin) label Jun 22, 2026
@gabemontero gabemontero force-pushed the feat/3299-runtime-config-engine branch from 6f9de3c to 6061960 Compare June 22, 2026 18:07
@rhdh-qodo-merge

Copy link
Copy Markdown

CI Feedback 🧐

A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

Action: Workspace boost, CI step for node 22

Failed stage: check api reports and generate API reference [❌]

Failed test name: ""

Failure summary:

The action failed during the API report validation step because there were uncommitted/generated
changes to the package public API reports:
- The log shows: You have uncommitted changes to the
public API or reports of a package.
- The workflow expects API report .md files to be up to date and
committed, and instructs to run yarn build:api-reports and commit the resulting changes.
- The job
exited with Process completed with exit code 1 after this validation, causing the workflow to fail.

Relevant error logs:
1:  ##[group]Runner Image Provisioner
2:  Hosted Compute Agent
...

252:  �[93m➤�[39m YN0002: │ �[38;5;166m@red-hat-developer-hub/�[39m�[38;5;173mbackstage-plugin-boost-backend�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:plugins/boost-backend�[39m doesn't provide �[38;5;173mwebpack�[39m (�[38;5;111mp405fc5�[39m), requested by �[38;5;166m@backstage/�[39m�[38;5;173mcli�[39m.
253:  �[93m➤�[39m YN0002: │ �[38;5;166m@red-hat-developer-hub/�[39m�[38;5;173mbackstage-plugin-boost-common�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:plugins/boost-common [c2cab]�[39m doesn't provide �[38;5;173mwebpack�[39m (�[38;5;111mp8e1e5a�[39m), requested by �[38;5;166m@backstage/�[39m�[38;5;173mcli�[39m.
254:  �[93m➤�[39m YN0002: │ �[38;5;166m@red-hat-developer-hub/�[39m�[38;5;173mbackstage-plugin-boost-common�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:plugins/boost-common�[39m doesn't provide �[38;5;173mwebpack�[39m (�[38;5;111mpe50ee4�[39m), requested by �[38;5;166m@backstage/�[39m�[38;5;173mcli�[39m.
255:  �[93m➤�[39m YN0002: │ �[38;5;166m@red-hat-developer-hub/�[39m�[38;5;173mbackstage-plugin-boost-node�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:plugins/boost-node�[39m doesn't provide �[38;5;173mwebpack�[39m (�[38;5;111mp456cbf�[39m), requested by �[38;5;166m@backstage/�[39m�[38;5;173mcli�[39m.
256:  �[93m➤�[39m YN0086: │ Some peer dependencies are incorrectly met by your project; run �[38;5;111myarn explain peer-requirements <hash>�[39m for details, where �[38;5;111m<hash>�[39m is the six-letter p-prefixed code.
257:  �[93m➤�[39m YN0086: │ Some peer dependencies are incorrectly met by dependencies; run �[38;5;111myarn explain peer-requirements�[39m for details.
258:  ##[endgroup]
259:  �[94m➤�[39m �[90mYN0000�[39m: └ Completed
260:  �[94m➤�[39m �[90mYN0000�[39m: ┌ Fetch step
261:  ##[group]Fetch step
262:  �[94m➤�[39m YN0013: │ �[38;5;220m1670�[39m packages were added to the project (�[38;5;160m+ 514.79 MiB�[39m).
263:  ##[endgroup]
264:  �[94m➤�[39m �[90mYN0000�[39m: └ Completed in 7s 180ms
265:  �[94m➤�[39m �[90mYN0000�[39m: ┌ Link step
266:  ##[group]Link step
267:  �[94m➤�[39m YN0007: │ �[38;5;166m@fission-ai/�[39m�[38;5;173mopenspec�[39m�[38;5;111m@�[39m�[38;5;111mnpm:1.4.1�[39m must be built because it never has been before or the last one failed
268:  �[94m➤�[39m YN0007: │ �[38;5;166m@swc/�[39m�[38;5;173mcore�[39m�[38;5;111m@�[39m�[38;5;111mnpm:1.15.40 [486b9]�[39m must be built because it never has been before or the last one failed
269:  �[94m➤�[39m YN0007: │ �[38;5;173mesbuild�[39m�[38;5;111m@�[39m�[38;5;111mnpm:0.25.12�[39m must be built because it never has been before or the last one failed
270:  �[94m➤�[39m YN0007: │ �[38;5;166m@nestjs/�[39m�[38;5;173mcore�[39m�[38;5;111m@�[39m�[38;5;111mnpm:11.1.21 [26b97]�[39m must be built because it never has been before or the last one failed
271:  �[94m➤�[39m YN0007: │ �[38;5;166m@openapitools/�[39m�[38;5;173mopenapi-generator-cli�[39m�[38;5;111m@�[39m�[38;5;111mnpm:2.34.0�[39m must be built because it never has been before or the last one failed
272:  �[94m➤�[39m YN0007: │ �[38;5;166m@internal/�[39m�[38;5;173mboost�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:.�[39m must be built because it never has been before or the last one failed
273:  �[94m➤�[39m �[90mYN0000�[39m: │ �[38;5;166m@internal/�[39m�[38;5;173mboost�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:.�[39m �[32mSTDOUT�[39m �[93m➤�[39m YN0000: Yarn detected that the current workflow is executed from a public pull request. For safety the hardened mode has been enabled.
...

281:  �[94m➤�[39m �[90mYN0000�[39m: │ �[38;5;166m@internal/�[39m�[38;5;173mboost�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:.�[39m �[32mSTDOUT�[39m �[94m➤�[39m �[90mYN0000�[39m: ┌ Post-resolution validation
282:  �[94m➤�[39m �[90mYN0000�[39m: │ �[38;5;166m@internal/�[39m�[38;5;173mboost�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:.�[39m �[32mSTDOUT�[39m ::group::Post-resolution validation
283:  �[94m➤�[39m �[90mYN0000�[39m: │ �[38;5;166m@internal/�[39m�[38;5;173mboost�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:.�[39m �[32mSTDOUT�[39m �[93m➤�[39m YN0060: │ �[38;5;173mprettier�[39m is listed by your project with version �[38;5;111m3.7.4�[39m (�[38;5;111mpc2ecd8�[39m), which doesn't satisfy what �[38;5;166m@spotify/�[39m�[38;5;173mprettier-config�[39m and other dependencies request (�[38;5;37m^2.0.0�[39m).
284:  �[94m➤�[39m �[90mYN0000�[39m: │ �[38;5;166m@internal/�[39m�[38;5;173mboost�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:.�[39m �[32mSTDOUT�[39m �[93m➤�[39m YN0002: │ �[38;5;166m@redhat-developer/�[39m�[38;5;173mrhdh-plugins�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:.�[39m doesn't provide �[38;5;166m@typescript-eslint/�[39m�[38;5;173mparser�[39m (�[38;5;111mp8d7c5c�[39m), requested by �[38;5;166m@spotify/�[39m�[38;5;173meslint-plugin�[39m.
285:  �[94m➤�[39m �[90mYN0000�[39m: │ �[38;5;166m@internal/�[39m�[38;5;173mboost�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:.�[39m �[32mSTDOUT�[39m �[93m➤�[39m YN0086: │ Some peer dependencies are incorrectly met by your project; run �[38;5;111myarn explain peer-requirements <hash>�[39m for details, where �[38;5;111m<hash>�[39m is the six-letter p-prefixed code.
286:  �[94m➤�[39m �[90mYN0000�[39m: │ �[38;5;166m@internal/�[39m�[38;5;173mboost�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:.�[39m �[32mSTDOUT�[39m �[93m➤�[39m YN0086: │ Some peer dependencies are incorrectly met by dependencies; run �[38;5;111myarn explain peer-requirements�[39m for details.
287:  �[94m➤�[39m �[90mYN0000�[39m: │ �[38;5;166m@internal/�[39m�[38;5;173mboost�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:.�[39m �[32mSTDOUT�[39m ::endgroup::
288:  �[94m➤�[39m �[90mYN0000�[39m: │ �[38;5;166m@internal/�[39m�[38;5;173mboost�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:.�[39m �[32mSTDOUT�[39m �[94m➤�[39m �[90mYN0000�[39m: └ Completed
289:  �[94m➤�[39m �[90mYN0000�[39m: │ �[38;5;166m@internal/�[39m�[38;5;173mboost�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:.�[39m �[32mSTDOUT�[39m �[94m➤�[39m �[90mYN0000�[39m: ┌ Fetch step
290:  �[94m➤�[39m �[90mYN0000�[39m: │ �[38;5;166m@internal/�[39m�[38;5;173mboost�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:.�[39m �[32mSTDOUT�[39m ::group::Fetch step
291:  �[94m➤�[39m �[90mYN0000�[39m: │ �[38;5;166m@internal/�[39m�[38;5;173mboost�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:.�[39m �[32mSTDOUT�[39m �[94m➤�[39m YN0013: │ �[38;5;220m690�[39m packages were added to the project (�[38;5;160m+ 288.05 MiB�[39m).
292:  �[94m➤�[39m �[90mYN0000�[39m: │ �[38;5;166m@internal/�[39m�[38;5;173mboost�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:.�[39m �[32mSTDOUT�[39m ::endgroup::
293:  �[94m➤�[39m �[90mYN0000�[39m: │ �[38;5;166m@internal/�[39m�[38;5;173mboost�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:.�[39m �[32mSTDOUT�[39m �[94m➤�[39m �[90mYN0000�[39m: └ Completed in 4s 436ms
294:  �[94m➤�[39m �[90mYN0000�[39m: │ �[38;5;166m@internal/�[39m�[38;5;173mboost�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:.�[39m �[32mSTDOUT�[39m �[94m➤�[39m �[90mYN0000�[39m: ┌ Link step
295:  �[94m➤�[39m �[90mYN0000�[39m: │ �[38;5;166m@internal/�[39m�[38;5;173mboost�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:.�[39m �[32mSTDOUT�[39m ::group::Link step
296:  �[94m➤�[39m �[90mYN0000�[39m: │ �[38;5;166m@internal/�[39m�[38;5;173mboost�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:.�[39m �[32mSTDOUT�[39m �[94m➤�[39m YN0007: │ �[38;5;173mesbuild�[39m�[38;5;111m@�[39m�[38;5;111mnpm:0.21.5�[39m must be built because it never has been before or the last one failed
297:  �[94m➤�[39m �[90mYN0000�[39m: │ �[38;5;166m@internal/�[39m�[38;5;173mboost�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:.�[39m �[32mSTDOUT�[39m �[94m➤�[39m YN0007: │ �[38;5;166m@swc/�[39m�[38;5;173mcore�[39m�[38;5;111m@�[39m�[38;5;111mnpm:1.4.13 [366d3]�[39m must be built because it never has been before or the last one failed
298:  �[94m➤�[39m �[90mYN0000�[39m: │ �[38;5;166m@internal/�[39m�[38;5;173mboost�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:.�[39m �[32mSTDOUT�[39m �[94m➤�[39m YN0007: │ �[38;5;173mesbuild�[39m�[38;5;111m@�[39m�[38;5;111mnpm:0.23.1�[39m must be built because it never has been before or the last one failed
299:  �[94m➤�[39m �[90mYN0000�[39m: │ �[38;5;166m@internal/�[39m�[38;5;173mboost�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:.�[39m �[32mSTDOUT�[39m �[94m➤�[39m YN0007: │ �[38;5;173mesbuild�[39m�[38;5;111m@�[39m�[38;5;111mnpm:0.20.2�[39m must be built because it never has been before or the last one failed
300:  �[94m➤�[39m �[90mYN0000�[39m: │ �[38;5;166m@internal/�[39m�[38;5;173mboost�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:.�[39m �[32mSTDOUT�[39m �[94m➤�[39m YN0007: │ �[38;5;173mcore-js-pure�[39m�[38;5;111m@�[39m�[38;5;111mnpm:3.36.1�[39m must be built because it never has been before or the last one failed
301:  �[94m➤�[39m �[90mYN0000�[39m: │ �[38;5;166m@internal/�[39m�[38;5;173mboost�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:.�[39m �[32mSTDOUT�[39m �[94m➤�[39m YN0007: │ �[38;5;166m@redhat-developer/�[39m�[38;5;173mrhdh-plugins�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:.�[39m must be built because it never has been before or the last one failed
302:  �[94m➤�[39m �[90mYN0000�[39m: │ �[38;5;166m@internal/�[39m�[38;5;173mboost�[39m�[38;5;111m@�[39m�[38;5;111mworkspace:.�[39m �[32mSTDOUT�[39m ::endgroup::
...

391:  import { ServiceFactory } from '@backstage/backend-plugin-api';
392:  import { z } from 'zod';
393:  // @public
394:  export class AdminConfigService {
395:  constructor(options: AdminConfigServiceOptions);
396:  getAllOverrides(): Promise<Map<string, unknown>>;
397:  getOverride(key: BoostConfigKey): Promise<unknown | undefined>;
398:  removeOverride(key: BoostConfigKey): Promise<void>;
399:  setOverride(key: BoostConfigKey, value: unknown): Promise<void>;
400:  validateStoredValues(): Promise<string[]>;
401:  }
402:  // @public
403:  export interface AdminConfigServiceOptions {
404:  // (undocumented)
405:  database: DatabaseService;
406:  Error: API Extractor completed with 0 errors and 1 warnings
407:  encryptionSecret?: string;
...

570:  // @public
571:  export function validateConfigValue(
572:  key: BoostConfigKey,
573:  value: unknown,
574:  ): unknown;
575:  // @public
576:  export function validateSecurityMode(
577:  mode: string | undefined,
578:  logger: LoggerService,
579:  ): SecurityMode;
580:  ```
581:  *************************************************************************************
582:  * You have uncommitted changes to the public API or reports of a package.           *
583:  * To solve this, run `yarn build:api-reports` and commit all md file changes.       *
584:  *************************************************************************************
585:  ##[error]Process completed with exit code 1.
586:  Node 20 is being deprecated. This workflow is running with Node 24 by default. If you need to temporarily use Node 20, you can set the ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true environment variable. For more information see: https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/

@fullsend-ai-review

fullsend-ai-review Bot commented Jun 22, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 6:09 PM UTC · Completed 6:22 PM UTC
Commit: ed84b21 · View workflow run →

@fullsend-ai-review

Copy link
Copy Markdown

/fs-review

@fullsend-ai-review

fullsend-ai-review Bot commented Jun 22, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 6:24 PM UTC · Completed 6:43 PM UTC
Commit: a32a667 · View workflow run →

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review comment for full details.

Comment thread workspaces/boost/plugins/boost-backend/src/config/encryption.ts
Comment thread workspaces/boost/plugins/boost-backend/src/plugin.ts
Comment thread workspaces/boost/plugins/boost-backend/report.api.md
Comment thread workspaces/boost/plugins/boost-backend/config.d.ts
@gabemontero gabemontero force-pushed the feat/3299-runtime-config-engine branch from 4ad38b4 to d12c7c7 Compare June 22, 2026 23:44
@fullsend-ai-review

fullsend-ai-review Bot commented Jun 22, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 11:47 PM UTC · Completed 12:02 AM UTC
Commit: 2985dab · View workflow run →

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review comment for full details.

Comment thread workspaces/boost/plugins/boost-backend/src/plugin.ts
fullsend-ai-coder Bot and others added 5 commits June 22, 2026 20:04
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>
- 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>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 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>
- 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>
@gabemontero gabemontero force-pushed the feat/3299-runtime-config-engine branch from d12c7c7 to 5f91eb5 Compare June 23, 2026 00:04
@fullsend-ai-review

fullsend-ai-review Bot commented Jun 23, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 12:07 AM UTC · Completed 12:23 AM UTC
Commit: 2985dab · View workflow run →

@fullsend-ai-review fullsend-ai-review Bot added the requires-manual-review Review requires human judgment label Jun 23, 2026
- 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>
@fullsend-ai-review

fullsend-ai-review Bot commented Jun 23, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 12:30 AM UTC · Completed 12:47 AM UTC
Commit: 2985dab · View workflow run →

@fullsend-ai-review fullsend-ai-review Bot added requires-manual-review Review requires human judgment and removed requires-manual-review Review requires human judgment labels Jun 23, 2026
@gabemontero gabemontero merged commit 112eb22 into main Jun 23, 2026
26 checks passed
@gabemontero gabemontero deleted the feat/3299-runtime-config-engine branch June 23, 2026 01:12
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

requires-manual-review Review requires human judgment workspace/boost Boost workspace (Backstage AI plugin)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

boost-backend — Runtime configuration engine with Zod validation (issue 3 of 15)

2 participants