refactor(dashboard): simplify guardrails and workflow UI#207
refactor(dashboard): simplify guardrails and workflow UI#207SantiagoDePolonia merged 8 commits intomainfrom
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a Guardrails subsystem (types, stores, service, factory, admin APIs), integrates it into app/compiler/execution-plans, adds dashboard UI/JS/CSS and tests, flattens execution-plan markup and consolidates node classes, and implements managed-default execution-plan reconciliation across stores. Changes
Sequence DiagramsequenceDiagram
participant Browser as Browser
participant DashboardJS as /admin/static/js/dashboard.js
participant GuardrailsModule as /admin/static/js/modules/guardrails.js
participant AdminAPI as Admin API (/admin/api/v1)
participant Service as guardrails.Service
participant Store as Store (SQLite/PG/Mongo)
participant DB as Database
Browser->>DashboardJS: navigate to /admin/dashboard/guardrails
DashboardJS->>DashboardJS: _applyRoute('guardrails', null)
DashboardJS->>GuardrailsModule: load/initialize module
GuardrailsModule->>AdminAPI: GET /guardrails/types
AdminAPI->>Service: TypeDefinitions()
Service-->>AdminAPI: type schema + defaults
AdminAPI-->>GuardrailsModule: type definitions
GuardrailsModule->>AdminAPI: GET /guardrails
AdminAPI->>Service: ListViews()
Service->>Store: List()
Store->>DB: query
DB-->>Store: rows
Store-->>Service: Definitions
Service-->>AdminAPI: Views[]
AdminAPI-->>GuardrailsModule: list response
GuardrailsModule->>Browser: render list/editor
Browser->>GuardrailsModule: submit create/edit
GuardrailsModule->>AdminAPI: PUT /guardrails/:name
AdminAPI->>Service: Upsert(def)
Service->>Store: Upsert(def)
Store->>DB: INSERT/UPDATE
DB-->>Store: success
Store-->>Service: persisted
Service->>Service: Refresh()
Service-->>AdminAPI: updated view
AdminAPI-->>GuardrailsModule: 200 + updated view
GuardrailsModule->>Browser: update UI
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~65 minutes Possibly Related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/executionplans/compiler.go (1)
67-74:⚠️ Potential issue | 🟠 MajorReturn typed
core.GatewayErrorinstead offmt.Errorf.The error at line 72 violates the coding guideline requiring all errors returned to clients to be
core.GatewayErrorinstances. Although this error is eventually normalized in the server layer, it should be typed at the source. Usecore.NewInvalidRequestErrororcore.NewProviderErrorwith appropriate error category.The nil check itself is correct and properly handles both nil compiler and nil registry.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/executionplans/compiler.go` around lines 67 - 74, In compileGuardrails, replace the fmt.Errorf call that returns a plain error when c==nil || c.registry==nil with a typed core.GatewayError (use core.NewProviderError) so callers receive a core.GatewayError; specifically change the error return in function compileGuardrails to return core.NewProviderError("guardrails are enabled but no guardrail registry is configured") (keeping the same nil, "", error return shape) and ensure imports/reference to core.NewProviderError are used instead of fmt.Errorf.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/admin/dashboard/static/js/dashboard.js`:
- Around line 206-209: The guardrailsPageVisible() helper currently falls back
to false when executionPlanRuntimeBooleanFlag isn't available; change the
fallback to true so it matches the execution-plans module behavior — update
guardrailsPageVisible() to return
this.executionPlanRuntimeBooleanFlag('GUARDRAILS_ENABLED', true) when the
function exists (and otherwise default to true) so the top-level Guardrails nav
uses the same GUARDRAILS_ENABLED fallback as
internal/admin/dashboard/static/js/modules/execution-plans.js.
In `@internal/admin/dashboard/static/js/modules/guardrails.test.js`:
- Around line 40-53: Add an edge-case unit test for normalizeGuardrailConfig to
ensure it behaves defensively when the requested type isn't present in
guardrailTypes: in a new test (e.g., "normalizeGuardrailConfig returns empty
config for unknown type") create a module via createGuardrailsModule(), set
guardrailTypes to include only 'system_prompt', call
module.normalizeGuardrailConfig({ content: 'test' }, 'unknown_type'), and assert
the returned config equals the input ({ content: 'test' }) rather than
attempting to merge non-existent defaults.
In `@internal/admin/dashboard/templates/index.html`:
- Around line 309-312: The new guardrail filter input (the element with
x-model="guardrailFilter" and class "filter-input") lacks an accessible name;
add one by giving the input a unique id (e.g., guardrail-filter) and either (a)
add a visible or screen-reader-only <label for="guardrail-filter">Guardrail
filter</label> placed before the input, or (b) add an aria-label="Guardrail
filter" (or aria-labelledby pointing to a label element) so screen readers have
a stable, descriptive name for the control.
- Around line 263-350: The page hides all status alerts when authError is true,
causing expired sessions to show the empty-state instead of prompting
re-authentication; update the guardrails template to surface auth errors by
either rendering the shared auth-banner component on this page or removing the
"!authError" suppression on the alert blocks that use authError (those
referencing authError, guardrailError, guardrailNotice, guardrailsAvailable).
Specifically, ensure the auth-banner (or an equivalent alert) is rendered when
authError is truthy and that the existing alerts (x-show expressions around
guardrailsRuntimeEnabled/guardrailsAvailable/guardrailError/guardrailNotice) do
not blanket-suppress messages by checking authError; adjust the x-show
conditions so authError triggers the auth-banner instead of hiding all alerts.
In `@internal/admin/handler_guardrails_test.go`:
- Around line 17-59: The guardrailTestStore (and newGuardrailTestStore) in this
file duplicates test storage logic already present as testStore in
internal/guardrails/service_test.go; extract the shared in-memory store into a
single test helper (e.g., internal/guardrails/testutil_test.go) that exposes a
reusable constructor and methods matching List, Get, Upsert, Delete, Close, then
replace the local guardrailTestStore/newGuardrailTestStore usages with the
shared helper to avoid duplication and keep behavior consistent across tests.
In `@internal/admin/handler.go`:
- Around line 1025-1043: The handler currently does a racy check-then-delete
(activeWorkflowGuardrailReferences(...) then guardrailDefs.Delete(...)); move
the consistency boundary so the check-and-delete is atomic by implementing the
reference check inside the guardrail deletion logic (e.g., add an atomic
DeleteIfNotReferenced or make guardrailDefs.Delete perform the active-workflow
reference query within the same storage transaction/lock and return a clear
ErrGuardrailInUse/ErrReferenced if found), then simplify the handler to call
guardrailDefs.Delete and handle ErrGuardrailInUse (or existing guardrail write
errors) and continue to call refreshExecutionPlansAfterGuardrailChange on
successful delete. Ensure the unique symbols touched are
activeWorkflowGuardrailReferences, guardrailDefs.Delete, and
refreshExecutionPlansAfterGuardrailChange so callers and error handling are
updated accordingly.
- Around line 1175-1185: The code currently treats any view.Payload.Guardrails
entry as active; update the loop to skip checking guardrail refs for a view when
its feature flag is disabled by first checking view.Payload.Features.Guardrails
(or equivalent Features struct) and continuing if it's false. In other words,
inside the outer loop over views, add a guard like "if
!view.Payload.Features.Guardrails { continue }" before iterating
view.Payload.Guardrails so stale refs are ignored (consistent with
validateExecutionPlanGuardrails), then proceed to append view.ScopeDisplay and
sort as before.
In `@internal/app/app.go`:
- Around line 190-204: EnsureSeedDefinitions currently only seeds when store is
empty so config guardrail updates are ignored after first run; change startup
logic to persist the config-managed subset as an upsert instead of one-time
seed. Replace the call to guardrailResult.Service.EnsureSeedDefinitions(ctx,
seedGuardrails) with an upsert operation (e.g., add/implement
Service.UpsertDefinitions(ctx, seedGuardrails) or change EnsureSeedDefinitions
to perform upserts) so configGuardrailDefinitions are merged/updated into the DB
on every startup; additionally adjust the default-plan bootstrap logic that
reads Service.Names() (the code deriving the global workflow from runtime
config) to include names from the current configGuardrailDefinitions when
computing defaults so config-backed guardrails are not silently dropped once the
DB becomes authoritative.
In `@internal/guardrails/catalog.go`:
- Around line 1-8: The BuildPipeline method's second return value (the string)
is undocumented; update the Catalog interface comment to document
BuildPipeline's return values by describing the string as a configuration/hash
used for cache validation or change detection, and mention semantics for
empty/unchanged values so implementers of BuildPipeline (which accepts steps
[]StepReference and returns (*Pipeline, string, error)) know how to compute and
return the config hash alongside the Pipeline and error.
In `@internal/guardrails/definitions.go`:
- Around line 151-156: The normalization currently calls
effectiveSystemPromptMode(cfg.Mode) but does not reject unknown values, allowing
invalid modes to be persisted; update the normalization in the system_prompt
path (e.g., where systemPromptDefinitionConfig is created) to validate the mode
after calling effectiveSystemPromptMode and return a validation error for
unsupported modes instead of silently defaulting—ensure Service.Upsert paths
that call buildDefinition/NewSystemPromptGuardrail cannot persist unknown modes
by performing this check in the same normalization/validation function,
returning newValidationError("system_prompt mode is invalid", ...) when the mode
is not one of the allowed enums.
In `@internal/guardrails/factory.go`:
- Around line 103-109: createStore currently passes ctx only to
NewPostgreSQLStore causing SQLite and MongoDB initialization to ignore caller
cancellation; update createStore to pass the incoming ctx into NewSQLiteStore
and NewMongoDBStore calls (i.e., call NewSQLiteStore(ctx, db) and
NewMongoDBStore(ctx, db) or whatever constructor signature you create), then
modify the SQLite constructor/function (NewSQLiteStore) to use
ExecContext/QueryContext for schema/setup instead of Exec, and change the
MongoDB constructor (NewMongoDBStore) to derive its timeout/operations from the
provided ctx (do not use context.Background()) and avoid launching detached
goroutines; also ensure error returns are converted to the typed
core.GatewayError where appropriate so startup failures propagate synchronously.
In `@internal/guardrails/service.go`:
- Around line 205-235: StartBackgroundRefresh currently spawns a detached
goroutine rooted in context.Background() and swallows Refresh errors; change its
signature to accept a parent context (e.g., ctx context.Context) and stop using
context.Background(), run the ticker loop tied to that parent context, and
surface Refresh failures instead of dropping them — for example return a cancel
function plus an error channel or return typed core.GatewayError values from
StartBackgroundRefresh so callers can observe failures; ensure you call
s.Refresh with a derived context (e.g., context.WithTimeout(ctx,
30*time.Second)) and propagate any error from Refresh rather than ignoring it,
and keep the once/done semantics but driven by the provided context.
---
Outside diff comments:
In `@internal/executionplans/compiler.go`:
- Around line 67-74: In compileGuardrails, replace the fmt.Errorf call that
returns a plain error when c==nil || c.registry==nil with a typed
core.GatewayError (use core.NewProviderError) so callers receive a
core.GatewayError; specifically change the error return in function
compileGuardrails to return core.NewProviderError("guardrails are enabled but no
guardrail registry is configured") (keeping the same nil, "", error return
shape) and ensure imports/reference to core.NewProviderError are used instead of
fmt.Errorf.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 1d378ab4-5e25-4dbd-aa1e-8a230536ed56
📒 Files selected for processing (30)
internal/admin/dashboard/dashboard_test.gointernal/admin/dashboard/static/css/dashboard.cssinternal/admin/dashboard/static/js/dashboard.jsinternal/admin/dashboard/static/js/modules/dashboard-layout.test.jsinternal/admin/dashboard/static/js/modules/execution-plans-layout.test.jsinternal/admin/dashboard/static/js/modules/execution-plans.jsinternal/admin/dashboard/static/js/modules/execution-plans.test.jsinternal/admin/dashboard/static/js/modules/guardrails.jsinternal/admin/dashboard/static/js/modules/guardrails.test.jsinternal/admin/dashboard/static/js/modules/timezone-layout.test.jsinternal/admin/dashboard/templates/execution-plan-chart.htmlinternal/admin/dashboard/templates/index.htmlinternal/admin/dashboard/templates/layout.htmlinternal/admin/handler.gointernal/admin/handler_guardrails_test.gointernal/app/app.gointernal/app/app_test.gointernal/executionplans/compiler.gointernal/guardrails/catalog.gointernal/guardrails/definitions.gointernal/guardrails/factory.gointernal/guardrails/service.gointernal/guardrails/service_test.gointernal/guardrails/store.gointernal/guardrails/store_mongodb.gointernal/guardrails/store_postgresql.gointernal/guardrails/store_sqlite.gointernal/guardrails/store_sqlite_test.gointernal/guardrails/system_prompt.gointernal/server/http.go
| type guardrailTestStore struct { | ||
| definitions map[string]guardrails.Definition | ||
| } | ||
|
|
||
| func newGuardrailTestStore(definitions ...guardrails.Definition) *guardrailTestStore { | ||
| store := &guardrailTestStore{definitions: make(map[string]guardrails.Definition, len(definitions))} | ||
| for _, definition := range definitions { | ||
| store.definitions[definition.Name] = definition | ||
| } | ||
| return store | ||
| } | ||
|
|
||
| func (s *guardrailTestStore) List(context.Context) ([]guardrails.Definition, error) { | ||
| result := make([]guardrails.Definition, 0, len(s.definitions)) | ||
| for _, definition := range s.definitions { | ||
| result = append(result, definition) | ||
| } | ||
| return result, nil | ||
| } | ||
|
|
||
| func (s *guardrailTestStore) Get(_ context.Context, name string) (*guardrails.Definition, error) { | ||
| definition, ok := s.definitions[name] | ||
| if !ok { | ||
| return nil, guardrails.ErrNotFound | ||
| } | ||
| copy := definition | ||
| return ©, nil | ||
| } | ||
|
|
||
| func (s *guardrailTestStore) Upsert(_ context.Context, definition guardrails.Definition) error { | ||
| s.definitions[definition.Name] = definition | ||
| return nil | ||
| } | ||
|
|
||
| func (s *guardrailTestStore) Delete(_ context.Context, name string) error { | ||
| if _, ok := s.definitions[name]; !ok { | ||
| return guardrails.ErrNotFound | ||
| } | ||
| delete(s.definitions, name) | ||
| return nil | ||
| } | ||
|
|
||
| func (s *guardrailTestStore) Close() error { return nil } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider extracting shared test store to reduce duplication.
The guardrailTestStore implementation is nearly identical to testStore in internal/guardrails/service_test.go. Consider creating a shared internal/guardrails/testutil_test.go or exporting a test helper.
However, keeping them separate is acceptable for test isolation and simplicity.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/admin/handler_guardrails_test.go` around lines 17 - 59, The
guardrailTestStore (and newGuardrailTestStore) in this file duplicates test
storage logic already present as testStore in
internal/guardrails/service_test.go; extract the shared in-memory store into a
single test helper (e.g., internal/guardrails/testutil_test.go) that exposes a
reusable constructor and methods matching List, Get, Upsert, Delete, Close, then
replace the local guardrailTestStore/newGuardrailTestStore usages with the
shared helper to avoid duplication and keep behavior consistent across tests.
| func createStore(ctx context.Context, store storage.Storage) (Store, error) { | ||
| return storage.ResolveBackend[Store]( | ||
| store, | ||
| func(db *sql.DB) (Store, error) { return NewSQLiteStore(db) }, | ||
| func(pool *pgxpool.Pool) (Store, error) { return NewPostgreSQLStore(ctx, pool) }, | ||
| func(db *mongo.Database) (Store, error) { return NewMongoDBStore(db) }, | ||
| ) |
There was a problem hiding this comment.
Store initialization ignores caller cancellation on SQLite and MongoDB.
Only the PostgreSQL branch receives ctx here. The SQLite and MongoDB constructors still do schema/index setup with db.Exec or context.Background() internally, so startup can block well past the app's timeout budget on those backends.
Suggested direction
func createStore(ctx context.Context, store storage.Storage) (Store, error) {
return storage.ResolveBackend[Store](
store,
- func(db *sql.DB) (Store, error) { return NewSQLiteStore(db) },
+ func(db *sql.DB) (Store, error) { return NewSQLiteStore(ctx, db) },
func(pool *pgxpool.Pool) (Store, error) { return NewPostgreSQLStore(ctx, pool) },
- func(db *mongo.Database) (Store, error) { return NewMongoDBStore(db) },
+ func(db *mongo.Database) (Store, error) { return NewMongoDBStore(ctx, db) },
)
}That needs matching constructor updates so store_sqlite.go uses ExecContext and store_mongodb.go derives its timeout from the caller context instead of context.Background().
Based on learnings: Applies to **/*.go : Do not hide work in detached goroutines; respect context synchronously and return typed core.GatewayError values.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/guardrails/factory.go` around lines 103 - 109, createStore currently
passes ctx only to NewPostgreSQLStore causing SQLite and MongoDB initialization
to ignore caller cancellation; update createStore to pass the incoming ctx into
NewSQLiteStore and NewMongoDBStore calls (i.e., call NewSQLiteStore(ctx, db) and
NewMongoDBStore(ctx, db) or whatever constructor signature you create), then
modify the SQLite constructor/function (NewSQLiteStore) to use
ExecContext/QueryContext for schema/setup instead of Exec, and change the
MongoDB constructor (NewMongoDBStore) to derive its timeout/operations from the
provided ctx (do not use context.Background()) and avoid launching detached
goroutines; also ensure error returns are converted to the typed
core.GatewayError where appropriate so startup failures propagate synchronously.
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/app/app.go (1)
562-584: 🧹 Nitpick | 🔵 TrivialFix inconsistent shutdown step numbering.
The shutdown comments have duplicate step numbers (two "8" comments at lines 570 and 578).
Suggested fix
- // 7. Close batch store (flushes pending entries) + // 7. Close batch store (flushes pending entries) if a.batch != nil { ... } - // 8. Close usage tracking (flushes pending entries) + // 8. Close usage tracking (flushes pending entries) if a.usage != nil { ... } - // 8. Close audit logging (flushes pending logs) + // 9. Close audit logging (flushes pending logs) if a.audit != nil {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/app/app.go` around lines 562 - 584, The shutdown comments in the Close sequence are misnumbered (two "8." comments). Update the inline step comments so they are sequential (e.g., keep "// 7. Close batch store...", "// 8. Close usage tracking..." and change "// 8. Close audit logging..." to "// 9. Close audit logging...") near the code that calls a.batch.Close(), a.usage.Close(), and a.audit.Close() so the shutdown steps read in order.
♻️ Duplicate comments (3)
internal/admin/dashboard/templates/index.html (1)
350-352:⚠️ Potential issue | 🟡 MinorAdd
!authErrorto the empty-state condition.When authentication fails, this empty-state message could still display alongside the auth-banner, showing "No guardrails defined yet." instead of prompting re-authentication clearly.
Suggested fix
- <p class="empty-state" x-show="filteredGuardrails.length === 0 && !guardrailsLoading && guardrailsAvailable && !guardrailError"> + <p class="empty-state" x-show="filteredGuardrails.length === 0 && !guardrailsLoading && guardrailsAvailable && !guardrailError && !authError"> No guardrails defined yet. </p>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/admin/dashboard/templates/index.html` around lines 350 - 352, The empty-state paragraph (class "empty-state" with the x-show expression) should also check authentication by adding a "!authError" clause to its condition so the message only shows when there is no authentication error; update the x-show expression on the <p class="empty-state"> element to include "&& !authError" alongside the existing checks (filteredGuardrails.length === 0 && !guardrailsLoading && guardrailsAvailable && !guardrailError).internal/admin/handler.go (1)
1025-1037:⚠️ Potential issue | 🟠 MajorFold the in-use check into
guardrailDefs.Delete.This is still a check-then-delete across two services. A workflow can become active after
activeWorkflowGuardrailReferences(...)returns and beforeguardrailDefs.Delete(...)runs, leaving an active plan pointing at a deleted guardrail. Make the reference check part of the delete write path and return a dedicated in-use error from there.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/admin/handler.go` around lines 1025 - 1037, The current flow does a race-prone check-then-delete by calling activeWorkflowGuardrailReferences(...) before guardrailDefs.Delete; instead move that in-use check into the delete write path so guardrailDefs.Delete performs the reference lookup and returns a specific in-use error (e.g. define and return guardrails.ErrInUse or similar) when active workflows reference the guardrail; update the Delete implementation to call activeWorkflowGuardrailReferences or equivalent within the same transactional/write operation and have this handler only call guardrailDefs.Delete and handle errors by mapping guardrails.ErrNotFound to core.NewNotFoundError and the new guardrails.ErrInUse to core.NewInvalidRequestError (using guardrailWriteError for other write errors).internal/guardrails/service.go (1)
196-240:⚠️ Potential issue | 🟠 MajorMove refresh orchestration out of
Service.This still hides a long-lived ticker worker inside the service. Even with the returned stop function and error channel, refresh failures remain asynchronous and later failures are still dropped once
errsis full, so callers still do not own the work synchronously. Keep the scheduling loop in app/factory lifecycle code and leaveServiceresponsible only forRefresh(...). As per coding guidelines, "Do not hide work in detached goroutines; respect context synchronously and return typedcore.GatewayErrorvalues".#!/bin/bash set -euo pipefail # Show where the service-owned background worker is started and how callers handle it. rg -n -C5 '\bStartBackgroundRefresh\s*\('🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/guardrails/service.go` around lines 196 - 240, Remove the internal background worker from Service: delete or deprecate StartBackgroundRefresh so Service no longer starts a detached goroutine or owns a ticker; instead keep only the synchronous Refresh(ctx) method on Service and ensure Refresh returns the typed core.GatewayError on failure. Move the scheduling/loop (ticker, context cancellation, and error propagation) into the application/factory lifecycle code where callers can synchronously call s.Refresh(ctx) on each tick and handle/aggregate errors; do not spawn detached goroutines inside Service and ensure callers own the retry/scheduling logic and error handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/executionplans/compiler.go`:
- Around line 11-22: The compiler currently only nil-checks the catalog but does
not handle an empty catalog: when registry.Len() == 0 BuildPipeline() can return
a plain error instead of a core.GatewayError. Update NewCompilerWithFeatureCaps
(and any call-sites that build the pipeline) to check registry.Len() == 0 and
return a core.GatewayError (wrap the underlying error or create a new
core.GatewayError with a clear message) instead of returning fmt.Errorf; also
ensure any error returned from BuildPipeline() is wrapped with core.GatewayError
before propagating.
In `@internal/executionplans/service.go`:
- Around line 226-229: isManagedDefaultGlobal currently infers "managed" status
from user-editable Version fields (version.Name/version.Description), which lets
users accidentally create workflows that will be treated as managed by
EnsureDefaultGlobal; change this by adding an explicit persisted marker (e.g.,
Version.Managed bool or Version.Metadata["managed"]="true") or by enforcing
reservation of ManagedDefaultGlobalName/ManagedDefaultGlobalDescription at
creation time; update isManagedDefaultGlobal to check the new persisted marker
(not Name/Description) and adjust creation/validation paths so user-created
Version objects cannot use the reserved name/description.
In `@internal/guardrails/service.go`:
- Around line 75-90: The current UpsertDefinitions (and similar Upsert/Delete)
persist changes to s.store before rebuilding s.snapshot, leaving in-memory state
stale if Refresh fails; fix this by making the operation atomic: prepare and
normalize all changes first, build the new in-memory snapshot (or compute the
snapshot delta) and apply it only if persistence succeeds, or run persistence
inside a store transaction and only swap s.snapshot after a successful commit;
specifically update Service.UpsertDefinitions (and the corresponding
Upsert/Delete methods) to either (a) use a transactional store API so you call
s.store.Upsert/Delete inside a transaction and call s.Refresh/snapshot swap only
after commit, or (b) compute the new snapshot in-memory, attempt persistence,
and on success assign s.snapshot = newSnapshot (and on failure leave snapshot
unchanged and return the error).
---
Outside diff comments:
In `@internal/app/app.go`:
- Around line 562-584: The shutdown comments in the Close sequence are
misnumbered (two "8." comments). Update the inline step comments so they are
sequential (e.g., keep "// 7. Close batch store...", "// 8. Close usage
tracking..." and change "// 8. Close audit logging..." to "// 9. Close audit
logging...") near the code that calls a.batch.Close(), a.usage.Close(), and
a.audit.Close() so the shutdown steps read in order.
---
Duplicate comments:
In `@internal/admin/dashboard/templates/index.html`:
- Around line 350-352: The empty-state paragraph (class "empty-state" with the
x-show expression) should also check authentication by adding a "!authError"
clause to its condition so the message only shows when there is no
authentication error; update the x-show expression on the <p
class="empty-state"> element to include "&& !authError" alongside the existing
checks (filteredGuardrails.length === 0 && !guardrailsLoading &&
guardrailsAvailable && !guardrailError).
In `@internal/admin/handler.go`:
- Around line 1025-1037: The current flow does a race-prone check-then-delete by
calling activeWorkflowGuardrailReferences(...) before guardrailDefs.Delete;
instead move that in-use check into the delete write path so
guardrailDefs.Delete performs the reference lookup and returns a specific in-use
error (e.g. define and return guardrails.ErrInUse or similar) when active
workflows reference the guardrail; update the Delete implementation to call
activeWorkflowGuardrailReferences or equivalent within the same
transactional/write operation and have this handler only call
guardrailDefs.Delete and handle errors by mapping guardrails.ErrNotFound to
core.NewNotFoundError and the new guardrails.ErrInUse to
core.NewInvalidRequestError (using guardrailWriteError for other write errors).
In `@internal/guardrails/service.go`:
- Around line 196-240: Remove the internal background worker from Service:
delete or deprecate StartBackgroundRefresh so Service no longer starts a
detached goroutine or owns a ticker; instead keep only the synchronous
Refresh(ctx) method on Service and ensure Refresh returns the typed
core.GatewayError on failure. Move the scheduling/loop (ticker, context
cancellation, and error propagation) into the application/factory lifecycle code
where callers can synchronously call s.Refresh(ctx) on each tick and
handle/aggregate errors; do not spawn detached goroutines inside Service and
ensure callers own the retry/scheduling logic and error handling.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: f3dade14-fcde-410b-88e7-dadea0fd3d8e
📒 Files selected for processing (21)
.env.templateinternal/admin/dashboard/static/js/dashboard.jsinternal/admin/dashboard/static/js/modules/dashboard-layout.test.jsinternal/admin/dashboard/static/js/modules/guardrails.test.jsinternal/admin/dashboard/templates/index.htmlinternal/admin/handler.gointernal/admin/handler_guardrails_test.gointernal/app/app.gointernal/app/app_test.gointernal/executionplans/compiler.gointernal/executionplans/service.gointernal/executionplans/service_test.gointernal/guardrails/catalog.gointernal/guardrails/definitions.gointernal/guardrails/factory.gointernal/guardrails/service.gointernal/guardrails/service_test.gointernal/guardrails/store_mongodb.gointernal/guardrails/store_sqlite.gointernal/guardrails/store_sqlite_test.gointernal/guardrails/system_prompt.go
| registry guardrails.Catalog | ||
| featureCaps core.ExecutionFeatures | ||
| } | ||
|
|
||
| // NewCompiler creates the default execution-plan compiler for the v1 payload. | ||
| func NewCompiler(registry *guardrails.Registry) Compiler { | ||
| func NewCompiler(registry guardrails.Catalog) Compiler { | ||
| return NewCompilerWithFeatureCaps(registry, core.DefaultExecutionFeatures()) | ||
| } | ||
|
|
||
| // NewCompilerWithFeatureCaps creates the default execution-plan compiler for the | ||
| // v1 payload with process-level feature caps applied at compile time. | ||
| func NewCompilerWithFeatureCaps(registry *guardrails.Registry, featureCaps core.ExecutionFeatures) Compiler { | ||
| func NewCompilerWithFeatureCaps(registry guardrails.Catalog, featureCaps core.ExecutionFeatures) Compiler { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd -t f -name "*.go" | xargs rg -l "guardrails.Catalog|interface.*Catalog"Repository: ENTERPILOT/GoModel
Length of output: 371
🏁 Script executed:
rg "type Catalog interface" -A 20Repository: ENTERPILOT/GoModel
Length of output: 1396
🏁 Script executed:
fd -t f -name "compiler.go" -path "*/executionplans/*"Repository: ENTERPILOT/GoModel
Length of output: 292
🏁 Script executed:
cat -n internal/executionplans/compiler.goRepository: ENTERPILOT/GoModel
Length of output: 2755
🏁 Script executed:
rg "func.*BuildPipeline" -B 2 -A 10Repository: ENTERPILOT/GoModel
Length of output: 1696
🏁 Script executed:
rg "func.*Len\(\)" -B 5 -A 2 internal/guardrails/Repository: ENTERPILOT/GoModel
Length of output: 1464
🏁 Script executed:
rg "type.*Catalog.*interface" -A 10 internal/guardrails/catalog.goRepository: ENTERPILOT/GoModel
Length of output: 182
🏁 Script executed:
rg "var.*Catalog|:= .*Catalog" internal/executionplans/compiler.goRepository: ENTERPILOT/GoModel
Length of output: 44
🏁 Script executed:
grep -n "func (s \*Service) BuildPipeline" internal/guardrails/service.goRepository: ENTERPILOT/GoModel
Length of output: 149
🏁 Script executed:
sed -n '182,210p' internal/guardrails/service.goRepository: ENTERPILOT/GoModel
Length of output: 899
🏁 Script executed:
rg "NewCompiler|NewCompilerWithFeatureCaps" -B 2 -A 2Repository: ENTERPILOT/GoModel
Length of output: 9770
🏁 Script executed:
rg "type Service struct" -A 15 internal/guardrails/service.goRepository: ENTERPILOT/GoModel
Length of output: 442
🏁 Script executed:
rg "BuildPipeline" internal/guardrails/registry.go -B 5 -A 20Repository: ENTERPILOT/GoModel
Length of output: 850
🏁 Script executed:
rg "core.GatewayError|core.NewProviderError" -B 2 -A 2 internal/core/Repository: ENTERPILOT/GoModel
Length of output: 44
🏁 Script executed:
grep -n "type.*Error" internal/core/*.go | head -20Repository: ENTERPILOT/GoModel
Length of output: 469
🏁 Script executed:
rg "GatewayError" internal/core/ -A 3 | head -40Repository: ENTERPILOT/GoModel
Length of output: 1912
🏁 Script executed:
rg "func NewProviderError" -A 10 internal/core/Repository: ENTERPILOT/GoModel
Length of output: 635
🏁 Script executed:
cat internal/core/errors.go | head -100Repository: ENTERPILOT/GoModel
Length of output: 2721
🏁 Script executed:
rg "BuildPipeline" internal/guardrails/service.go -A 15 | grep -E "fmt.Errorf|error"Repository: ENTERPILOT/GoModel
Length of output: 424
Wrap empty-catalog errors in core.GatewayError to comply with error handling guidelines.
The nil-check at line 71 correctly returns a GatewayError when the catalog is missing. However, when the catalog is empty (Len() == 0), it passes the nil check and calls BuildPipeline(), which returns a plain fmt.Errorf. This violates the guideline requiring all client-facing errors to be instances of core.GatewayError.
Add a check for empty catalogs and wrap any returned errors appropriately:
Suggested fix
func (c *compiler) compileGuardrails(steps []guardrails.StepReference) (*guardrails.Pipeline, string, error) {
if len(steps) == 0 {
return nil, "", nil
}
- if c == nil || c.registry == nil {
- return nil, "", core.NewProviderError("", http.StatusBadGateway, "guardrails are enabled but no guardrail registry is configured", nil)
+ if c == nil || c.registry == nil || c.registry.Len() == 0 {
+ return nil, "", core.NewProviderError("", http.StatusBadGateway, "guardrails are enabled but no guardrail catalog is configured", nil)
}
return c.registry.BuildPipeline(steps)
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/executionplans/compiler.go` around lines 11 - 22, The compiler
currently only nil-checks the catalog but does not handle an empty catalog: when
registry.Len() == 0 BuildPipeline() can return a plain error instead of a
core.GatewayError. Update NewCompilerWithFeatureCaps (and any call-sites that
build the pipeline) to check registry.Len() == 0 and return a core.GatewayError
(wrap the underlying error or create a new core.GatewayError with a clear
message) instead of returning fmt.Errorf; also ensure any error returned from
BuildPipeline() is wrapped with core.GatewayError before propagating.
# Conflicts: # internal/admin/dashboard/static/js/modules/execution-plans-layout.test.js
There was a problem hiding this comment.
Actionable comments posted: 11
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/admin/dashboard/static/css/dashboard.css`:
- Around line 2930-3011: The CSS is missing rules for the classes
ep-node-async-usage and ep-node-async-audit which the template
execution-plan-chart.html applies (classes on nodes: "ep-node-async
ep-node-async-usage" and "ep-node-async ep-node-async-audit"), causing styling
regression; add definitions for .ep-node-async-usage and .ep-node-async-audit
(and their child selectors .ep-node-icon, .ep-node-label, .ep-node-sub,
.ep-node-badge as needed) following the pattern used for
.ep-node-feature/.ep-node-success/.ep-node-error so Usage and Audit Log get
their distinct color mixes and border/background rules and restore the test
expectations in execution-plans-layout.test.js.
In `@internal/admin/dashboard/templates/execution-plan-chart.html`:
- Around line 65-66: The connector spacer currently uses x-show="{{.}}.showUsage
&& {{.}}.showAudit" so when executionPlanAuditChart() forces {{.}}.showAudit
true but {{.}}.showUsage is false the spacer disappears and the Audit node
shifts; change the condition on the spacer element with class ep-conn-async to
render whenever the audit node renders (e.g. x-show="{{.}}.showAudit" or the
equivalent OR logic), so the ep-conn ep-conn-async divider remains aligned with
the ep-node-async-audit node; update the template line for the spacer (and any
matching connector) accordingly to reference the same {{.}}.showAudit check used
by the ep-node-async-audit element.
In `@internal/admin/dashboard/templates/index.html`:
- Around line 292-297: The first warning banner is incorrectly bound to
guardrailsRuntimeEnabled() (which returns this.guardrailsAvailable) so it shows
when the runtime is unavailable; change its x-show to use the actual
feature-flag helper (the "GUARDRAILS_ENABLED" helper function used elsewhere,
e.g., guardrailsEnabled() or the helper that checks the feature flag) instead of
guardrailsRuntimeEnabled(), and keep the second banner bound to
guardrailsAvailable/authError; update the banner binding in index.html to
reference the real feature-flag helper (not
guardrailsRuntimeEnabled/guardrailsAvailable) so the message about
GUARDRAILS_ENABLED reflects the flag state only.
- Around line 384-387: The user_path field is rendered but never wired into the
module state or payload; update the guardrails module to persist it by adding
guardrailForm.user_path to the form defaults object, set guardrailForm.user_path
when hydrating the form for editing (e.g., in the function that populates the
form from an existing guardrail — assign guardrailForm.user_path =
guardrail.user_path || ''), and include user_path in the object sent by the
submit/save handler (e.g., add user_path: guardrailForm.user_path to the payload
built in the submitGuardrailForm/saveGuardrail function).
In `@internal/app/app.go`:
- Around line 711-732: The current configGuardrailDefinitions function always
marshals SystemPrompt.Mode and SystemPrompt.Content into the
guardrails.Definition.Config regardless of rule.Type; change
configGuardrailDefinitions to perform type-specific marshaling by switching on
rule.Type (and/or presence of rule.Config) and serializing the appropriate
fields for each type (e.g., for "system_prompt" marshal Mode/Content, for future
types marshal their required fields or forward existing rule.Config), returning
an error for unknown/unsupported rule.Type; update references to rule.Type,
rule.SystemPrompt, cfg.Rules, and guardrails.Definition to locate and implement
the type-aware switch and ensure Config contains the correct JSON payload per
type.
In `@internal/executionplans/compiler_test.go`:
- Around line 159-173: The test
TestCompilerCompile_WrapsBuildPipelineErrorsAsGatewayErrors currently creates an
empty registry so the compiler fails on the empty-catalog path instead of
exercising the missing-ref handling; populate the registry with at least one
dummy guardrail entry (not "missing") before calling
NewCompiler(registry).Compile so the compiler proceeds past the empty-registry
check and triggers the resolution of Ref: "missing" (e.g., register a minimal
guardrail/step into the registry variable created by guardrails.NewRegistry() so
the missing-ref wrapping path is exercised).
In `@internal/executionplans/service.go`:
- Around line 222-223: The seed path currently sets normalized.Managed = true
and calls s.store.Create(ctx, normalized) which bypasses
validateCreateCandidate, refreshMu, and the in-memory snapshot update (causing
Match() to potentially fail); change this to route the seed through the normal
creation flow by calling s.Create(ctx, normalized) (or the internal create entry
point used by s.Create) instead of s.store.Create so that
validateCreateCandidate is executed, refreshMu is updated, and the in-memory
snapshot is refreshed before returning.
In `@internal/guardrails/service.go`:
- Around line 223-236: The error path in Service.BuildPipeline currently returns
a plain fmt.Errorf when snapshot.registry is nil; change this to return a
client-facing core.GatewayError (e.g., use the project's constructor like
core.NewGatewayError or build a core.GatewayError instance) with the same
descriptive message ("guardrail catalog is not loaded"); update the return to
nil, "", core.GatewayError and add the core import if missing so the
BuildPipeline method and its callers return the proper typed gateway error
instead of fmt.Errorf.
In `@internal/guardrails/store_mongodb.go`:
- Around line 200-209: The function mongoConfigFromRaw can receive
empty/whitespace or JSON "null"/"{}" and downstream guardrail builders may not
handle these consistently; update mongoConfigFromRaw and the consumers to
normalize configs to a single representation (e.g., always return a non-nil
empty bson.M for empty/whitespace, for JSON "null" treat as empty, and ensure
any guardrail builder functions that consume the result accept an empty map
rather than nil). Specifically, in mongoConfigFromRaw handle the "null" JSON
case as empty, and audit the guardrail builder functions that read the config
(references to the guardrail builder constructors/usages) to accept and treat an
empty bson.M the same as no config so both "{}" and empty RawMessage behave
identically.
In `@internal/guardrails/store_sqlite.go`:
- Around line 71-77: Replace the direct sentinel comparison "err ==
sql.ErrNoRows" with errors.Is(err, sql.ErrNoRows) in the error handling block
(keep the same return behavior to return ErrNotFound when matched), and add the
"errors" package import if missing; target the block that checks err after
querying the store (the variables/functions involved are err, sql.ErrNoRows,
ErrNotFound and the returned definition).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: da87aee2-df60-47a6-934b-c610b5a107f6
📒 Files selected for processing (25)
internal/admin/dashboard/static/css/dashboard.cssinternal/admin/dashboard/static/js/modules/execution-plans-layout.test.jsinternal/admin/dashboard/static/js/modules/execution-plans.jsinternal/admin/dashboard/static/js/modules/execution-plans.test.jsinternal/admin/dashboard/templates/execution-plan-chart.htmlinternal/admin/dashboard/templates/index.htmlinternal/admin/handler.gointernal/admin/handler_guardrails_test.gointernal/app/app.gointernal/executionplans/compiler.gointernal/executionplans/compiler_test.gointernal/executionplans/service.gointernal/executionplans/service_test.gointernal/executionplans/store_mongodb.gointernal/executionplans/store_postgresql.gointernal/executionplans/store_sqlite.gointernal/executionplans/types.gointernal/executionplans/types_test.gointernal/guardrails/factory.gointernal/guardrails/service.gointernal/guardrails/service_test.gointernal/guardrails/store.gointernal/guardrails/store_mongodb.gointernal/guardrails/store_postgresql.gointernal/guardrails/store_sqlite.go
| <div class="alert alert-warning" x-show="!guardrailsRuntimeEnabled()"> | ||
| Runtime guardrail execution is currently off because <code>GUARDRAILS_ENABLED</code> is disabled. You can still manage definitions here. | ||
| </div> | ||
| <div class="alert alert-warning" x-show="!authError && !guardrailsAvailable"> | ||
| Guardrails feature is unavailable. | ||
| </div> |
There was a problem hiding this comment.
This banner is checking availability, not GUARDRAILS_ENABLED.
internal/admin/dashboard/static/js/modules/guardrails.js:131-134 defines guardrailsRuntimeEnabled() as return this.guardrailsAvailable;. A 503/401 will therefore show this "GUARDRAILS_ENABLED is disabled" copy and the separate unavailable banner together. Bind this banner to the actual feature-flag helper instead.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/admin/dashboard/templates/index.html` around lines 292 - 297, The
first warning banner is incorrectly bound to guardrailsRuntimeEnabled() (which
returns this.guardrailsAvailable) so it shows when the runtime is unavailable;
change its x-show to use the actual feature-flag helper (the
"GUARDRAILS_ENABLED" helper function used elsewhere, e.g., guardrailsEnabled()
or the helper that checks the feature flag) instead of
guardrailsRuntimeEnabled(), and keep the second banner bound to
guardrailsAvailable/authError; update the banner binding in index.html to
reference the real feature-flag helper (not
guardrailsRuntimeEnabled/guardrailsAvailable) so the message about
GUARDRAILS_ENABLED reflects the flag state only.
| <label class="alias-form-field"> | ||
| <span>User Path</span> | ||
| <input type="text" class="filter-input" placeholder="/team/alpha" x-model="guardrailForm.user_path" aria-label="Guardrail user path"> | ||
| <small class="alias-form-hint">Reserved for future UI visibility scoping. It does not affect runtime execution yet.</small> |
There was a problem hiding this comment.
user_path is UI-only here.
internal/admin/dashboard/static/js/modules/guardrails.js:1-23, internal/admin/dashboard/static/js/modules/guardrails.js:131-168, and internal/admin/dashboard/static/js/modules/guardrails.js:253-310 still omit user_path from the form defaults, edit hydration, and submit payload. The field renders, but its value is dropped on open/save. Wire it through the module before exposing it.
🧰 Tools
🪛 HTMLHint (1.9.2)
[warning] 386-386: No matching [ label ] tag found.
(input-requires-label)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/admin/dashboard/templates/index.html` around lines 384 - 387, The
user_path field is rendered but never wired into the module state or payload;
update the guardrails module to persist it by adding guardrailForm.user_path to
the form defaults object, set guardrailForm.user_path when hydrating the form
for editing (e.g., in the function that populates the form from an existing
guardrail — assign guardrailForm.user_path = guardrail.user_path || ''), and
include user_path in the object sent by the submit/save handler (e.g., add
user_path: guardrailForm.user_path to the payload built in the
submitGuardrailForm/saveGuardrail function).
| if input.Managed && (scope.Provider != "" || scope.Model != "" || scope.UserPath != "") { | ||
| return CreateInput{}, "", "", newValidationError("managed default workflow must use global scope", nil) | ||
| } | ||
| if !input.Managed && | ||
| input.Name == ManagedDefaultGlobalName && | ||
| input.Description == ManagedDefaultGlobalDescription { | ||
| return CreateInput{}, "", "", newValidationError("managed default workflow name/description is reserved", nil) |
There was a problem hiding this comment.
Managed is still assignable to non-default globals.
isManagedDefaultGlobal() now trusts only Version.Managed, but this validation only constrains the scope. Any caller of Service.Create can still persist Managed: true on an arbitrary global plan, and a later EnsureDefaultGlobal() run will treat it as the managed default and replace it. Require the reserved name/description pair when Managed is set.
Proposed fix
if input.Managed && (scope.Provider != "" || scope.Model != "" || scope.UserPath != "") {
return CreateInput{}, "", "", newValidationError("managed default workflow must use global scope", nil)
}
+ if input.Managed &&
+ (input.Name != ManagedDefaultGlobalName || input.Description != ManagedDefaultGlobalDescription) {
+ return CreateInput{}, "", "", newValidationError("managed default workflow identity is reserved", nil)
+ }
if !input.Managed &&
input.Name == ManagedDefaultGlobalName &&
input.Description == ManagedDefaultGlobalDescription {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if input.Managed && (scope.Provider != "" || scope.Model != "" || scope.UserPath != "") { | |
| return CreateInput{}, "", "", newValidationError("managed default workflow must use global scope", nil) | |
| } | |
| if !input.Managed && | |
| input.Name == ManagedDefaultGlobalName && | |
| input.Description == ManagedDefaultGlobalDescription { | |
| return CreateInput{}, "", "", newValidationError("managed default workflow name/description is reserved", nil) | |
| if input.Managed && (scope.Provider != "" || scope.Model != "" || scope.UserPath != "") { | |
| return CreateInput{}, "", "", newValidationError("managed default workflow must use global scope", nil) | |
| } | |
| if input.Managed && | |
| (input.Name != ManagedDefaultGlobalName || input.Description != ManagedDefaultGlobalDescription) { | |
| return CreateInput{}, "", "", newValidationError("managed default workflow identity is reserved", nil) | |
| } | |
| if !input.Managed && | |
| input.Name == ManagedDefaultGlobalName && | |
| input.Description == ManagedDefaultGlobalDescription { | |
| return CreateInput{}, "", "", newValidationError("managed default workflow name/description is reserved", nil) |
| // BuildPipeline resolves named steps through the current in-memory guardrail registry. | ||
| func (s *Service) BuildPipeline(steps []StepReference) (*Pipeline, string, error) { | ||
| if len(steps) == 0 { | ||
| return nil, "", nil | ||
| } | ||
|
|
||
| s.mu.RLock() | ||
| registry := s.snapshot.registry | ||
| s.mu.RUnlock() | ||
| if registry == nil { | ||
| return nil, "", fmt.Errorf("guardrail catalog is not loaded") | ||
| } | ||
| return registry.BuildPipeline(steps) | ||
| } |
There was a problem hiding this comment.
Return typed core.GatewayError instead of fmt.Errorf.
Line 233 returns a plain fmt.Errorf which violates the coding guideline requiring client-facing errors to be core.GatewayError instances. Since BuildPipeline is part of the Catalog interface used by the execution plans compiler, this error can propagate to clients.
🛠️ Suggested fix
func (s *Service) BuildPipeline(steps []StepReference) (*Pipeline, string, error) {
if len(steps) == 0 {
return nil, "", nil
}
s.mu.RLock()
registry := s.snapshot.registry
s.mu.RUnlock()
if registry == nil {
- return nil, "", fmt.Errorf("guardrail catalog is not loaded")
+ return nil, "", core.NewProviderError("", http.StatusServiceUnavailable, "guardrail catalog is not loaded", nil)
}
return registry.BuildPipeline(steps)
}As per coding guidelines: "All errors returned to clients must be instances of core.GatewayError"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/guardrails/service.go` around lines 223 - 236, The error path in
Service.BuildPipeline currently returns a plain fmt.Errorf when
snapshot.registry is nil; change this to return a client-facing
core.GatewayError (e.g., use the project's constructor like core.NewGatewayError
or build a core.GatewayError instance) with the same descriptive message
("guardrail catalog is not loaded"); update the return to nil, "",
core.GatewayError and add the core import if missing so the BuildPipeline method
and its callers return the proper typed gateway error instead of fmt.Errorf.
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/executionplans/service.go (1)
190-223:⚠️ Potential issue | 🔴 CriticalMake the managed-default reconciliation atomic.
The read at Line 190 and the writes at Lines 212/223 are separate store operations. Another writer can install a different active global in between, and this path will still seed/update the managed default from stale state, deactivating the newer plan. This needs a transactional compare-and-set or a recheck inside the same storage write path; a process-local mutex alone will not protect multi-instance deployments.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/executionplans/service.go` around lines 190 - 223, The current reconciliation reads active plans via s.store.ListActive then later calls s.Create with modified normalized, which is racy; replace this read-then-write with an atomic store-side check-and-set (e.g., add and call a transactional method on the store such as CreateIfNoActiveGlobal/CreateOrUpdateManagedDefault or UpdateManagedDefaultWithCompare) that performs the recheck of active global execution plans and only inserts/updates the managed default when the checked state still permits it; ensure the method invoked from service.go (instead of s.Create) accepts the normalized execution plan and the planHash/identity and returns a clear error if a competing active global exists so callers (the loop using s.store.ListActive, normalizeScope, isManagedDefaultGlobal, normalized) do not overwrite newer active plans.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/app/app.go`:
- Around line 752-773: The code builds an availability set using trimmed
guardrail names but then writes the untrimmed rule.Name into the payload.Ref;
update the loop that constructs payload.Guardrails so that it uses the trimmed
name (e.g., assign name := strings.TrimSpace(rule.Name) and use that name for
both the availability check and for GuardrailStep.Ref) when appending
executionplans.GuardrailStep to payload.Guardrails, ensuring consistency between
the filter and the written Ref.
In `@internal/guardrails/store_mongodb.go`:
- Around line 133-183: The UpsertMany method currently issues many UpdateOne
calls inside a transaction (using StartSession and WithTransaction) which is
inefficient for large batches; replace the per-definition UpdateOne loop with a
single BulkWrite call: build a slice of mongo.WriteModel (e.g., UpdateOneModel
or ReplaceOneModel) for each normalized definition using
mongoDefinitionIDFilter{ID: normalized.Name} as the filter and the same update
document (including $set and $setOnInsert), set each model to Upsert true, then
call s.collection.BulkWrite(sessionCtx, models,
options.BulkWrite().SetOrdered(true)) inside the existing session transaction;
keep normalization, mongoConfigFromRaw, created_at/updated_at handling and
preserve error wrapping around the BulkWrite call.
In `@internal/guardrails/store_sqlite.go`:
- Around line 197-203: The current isSQLiteDuplicateColumnError uses loose
substring checks which can false-positive on generic "already exists" messages;
update the function to normalize the error string (strings.ToLower(err.Error()))
and then return true only when it matches more specific SQLite patterns such as
"duplicate column", "duplicate column name", or an "already exists" phrase that
also contains "column" (e.g., strings.Contains(msg,"already exists") &&
strings.Contains(msg,"column")); reference the isSQLiteDuplicateColumnError
function and replace the two simple contains checks with these stricter combined
checks to reduce false positives while keeping pragmatic string-based detection.
---
Outside diff comments:
In `@internal/executionplans/service.go`:
- Around line 190-223: The current reconciliation reads active plans via
s.store.ListActive then later calls s.Create with modified normalized, which is
racy; replace this read-then-write with an atomic store-side check-and-set
(e.g., add and call a transactional method on the store such as
CreateIfNoActiveGlobal/CreateOrUpdateManagedDefault or
UpdateManagedDefaultWithCompare) that performs the recheck of active global
execution plans and only inserts/updates the managed default when the checked
state still permits it; ensure the method invoked from service.go (instead of
s.Create) accepts the normalized execution plan and the planHash/identity and
returns a clear error if a competing active global exists so callers (the loop
using s.store.ListActive, normalizeScope, isManagedDefaultGlobal, normalized) do
not overwrite newer active plans.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: c9bc313d-68ae-4aa6-beae-a8a013ac69db
📒 Files selected for processing (13)
internal/admin/dashboard/static/css/dashboard.cssinternal/admin/dashboard/static/js/modules/execution-plans-layout.test.jsinternal/admin/dashboard/templates/execution-plan-chart.htmlinternal/app/app.gointernal/app/app_test.gointernal/executionplans/compiler_test.gointernal/executionplans/service.gointernal/executionplans/service_test.gointernal/guardrails/service.gointernal/guardrails/service_test.gointernal/guardrails/store_mongodb.gointernal/guardrails/store_mongodb_test.gointernal/guardrails/store_sqlite.go
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/app/app.go (1)
294-317:⚠️ Potential issue | 🟠 MajorRestore the server-level guardrail hash wiring.
internal/server/http.goLine 98 still copiescfg.GuardrailsHashinto the handler, but this config literal no longer sets that field. That leaveshandler.guardrailsHashempty, so the server path that still depends on the config hash stops varying by guardrail state. Either keep populatingserver.Config.GuardrailsHashhere, or remove that stale server-side field in the same change.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/app/app.go` around lines 294 - 317, The server Config literal removed populating server.Config.GuardrailsHash leaving handler.guardrailsHash empty; restore wiring by assigning cfg.GuardrailsHash into the Config (set Server.Config.GuardrailsHash = cfg.GuardrailsHash) so that internal/server/http.go's handler.guardrailsHash continues to receive the guardrails hash, or alternatively remove the stale handler.guardrailsHash usage in internal/server/http.go alongside this change—update the code paths referencing GuardrailsHash (server.Config.GuardrailsHash, handler.guardrailsHash, cfg.GuardrailsHash) to be consistent.
♻️ Duplicate comments (1)
internal/executionplans/service.go (1)
197-217:⚠️ Potential issue | 🔴 CriticalValidate the managed default before the store mutation commits.
Line 197 now runs the mutating reconcile path before Line 210 compiles the candidate. The stores added in this PR deactivate/insert inside
EnsureManagedDefaultGlobalbefore returning, so a compile failure here leaves the previous good global disabled and the new invalid one active in storage. This reintroduces the old seed-path problem in a new place; the reconciliation needs a preview-validation step before the durable write (or the store API needs to split “check” from “commit” so compilation can happen first).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/executionplans/service.go` around lines 197 - 217, The current flow calls s.store.EnsureManagedDefaultGlobal (which mutates storage) before validating the candidate plan; move the compile/validation step so it happens prior to any store mutation. Concretely: obtain the candidate (normalized, planHash) and call s.compiler.Compile on the candidate version (or change EnsureManagedDefaultGlobal to provide a non-mutating "preview" version) and verify compiled != nil and compiled.Policy != nil before invoking s.store.EnsureManagedDefaultGlobal; after successful compile/validation call s.storeActivatedCompiledLocked(compiledPlanForVersion(compiled, *version)) as before, and keep the refreshLocked/snapshot logic unchanged for the nil-version path. Ensure changes reference EnsureManagedDefaultGlobal, s.compiler.Compile, compiledPlanForVersion, and s.storeActivatedCompiledLocked.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/admin/handler_executionplans_test.go`:
- Around line 101-114: The reconciliation check in EnsureManagedDefaultGlobal
currently short-circuits creation when a managed global version matches only
Name and Description; update the condition to also compare PlanHash so the fake
store only no-ops when version.Name == input.Name && version.Description ==
input.Description && version.PlanHash == input.PlanHash (using the existing
version.PlanHash and input.PlanHash fields), otherwise proceed to call
s.Create(ctx, input) to mirror real-store behavior.
In `@internal/app/app.go`:
- Around line 716-736: Trim and validate rule.Name and rule.Type before building
the guardrails.Definition: canonicalize rule.Type and rule.Name with
strings.TrimSpace (and e.g. strings.ToLower if you want canonical casing) and
reject empty/blank names or types with a clear error; use the normalized values
when creating rawConfig and when appending guardrails.Definition (set
Definition.Name and Definition.Type to the trimmed/normalized strings) so
downstream dispatch sees the same canonical type as workflow refs rather than
the original raw input.
In `@internal/executionplans/service.go`:
- Around line 221-223: The helper function isManagedDefaultGlobal (taking
Version) is unused and causing a lint failure; remove the entire function
declaration (the isManagedDefaultGlobal function) from the file and any related
unused imports or references to Version that only existed for this helper so the
linter stops flagging it.
In `@internal/guardrails/store_sqlite_test.go`:
- Around line 15-19: The tests open an in-memory SQLite DB with
sql.Open("sqlite", ":memory:") but rely on a single connection; make this
explicit by calling db.SetMaxOpenConns(1) immediately after the sql.Open(...)
success check (i.e., after the db variable is created and before any setup or
assertions) so the connection pool cannot create multiple connections and split
the in-memory database; apply the same change wherever sql.Open("sqlite",
":memory:") and db are used in this test file.
---
Outside diff comments:
In `@internal/app/app.go`:
- Around line 294-317: The server Config literal removed populating
server.Config.GuardrailsHash leaving handler.guardrailsHash empty; restore
wiring by assigning cfg.GuardrailsHash into the Config (set
Server.Config.GuardrailsHash = cfg.GuardrailsHash) so that
internal/server/http.go's handler.guardrailsHash continues to receive the
guardrails hash, or alternatively remove the stale handler.guardrailsHash usage
in internal/server/http.go alongside this change—update the code paths
referencing GuardrailsHash (server.Config.GuardrailsHash,
handler.guardrailsHash, cfg.GuardrailsHash) to be consistent.
---
Duplicate comments:
In `@internal/executionplans/service.go`:
- Around line 197-217: The current flow calls s.store.EnsureManagedDefaultGlobal
(which mutates storage) before validating the candidate plan; move the
compile/validation step so it happens prior to any store mutation. Concretely:
obtain the candidate (normalized, planHash) and call s.compiler.Compile on the
candidate version (or change EnsureManagedDefaultGlobal to provide a
non-mutating "preview" version) and verify compiled != nil and compiled.Policy
!= nil before invoking s.store.EnsureManagedDefaultGlobal; after successful
compile/validation call
s.storeActivatedCompiledLocked(compiledPlanForVersion(compiled, *version)) as
before, and keep the refreshLocked/snapshot logic unchanged for the nil-version
path. Ensure changes reference EnsureManagedDefaultGlobal, s.compiler.Compile,
compiledPlanForVersion, and s.storeActivatedCompiledLocked.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 90d8dca6-2b22-4ef4-aa66-6eb3c524c1e7
📒 Files selected for processing (12)
internal/admin/handler_executionplans_test.gointernal/app/app.gointernal/app/app_test.gointernal/executionplans/service.gointernal/executionplans/service_test.gointernal/executionplans/store.gointernal/executionplans/store_mongodb.gointernal/executionplans/store_postgresql.gointernal/executionplans/store_sqlite.gointernal/guardrails/store_mongodb.gointernal/guardrails/store_sqlite.gointernal/guardrails/store_sqlite_test.go
Summary
Testing
Summary by CodeRabbit
New Features
Updates