Skip to content

fix: force file-wins sync for plugins, governance entities, and client config when source_of_truth=config.json - #4381

Merged
akshaydeo merged 1 commit into
devfrom
06-14-fix_use_updateplugin_when_source_of_truth_is_config_json
Jun 15, 2026
Merged

fix: force file-wins sync for plugins, governance entities, and client config when source_of_truth=config.json#4381
akshaydeo merged 1 commit into
devfrom
06-14-fix_use_updateplugin_when_source_of_truth_is_config_json

Conversation

@BearTS

@BearTS BearTS commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Summary

When source_of_truth=config.json, the config file should always be the authoritative source for plugins and governance entities, regardless of what is stored in the database. Previously, UI/API edits could cause drift that persisted across restarts because hash comparisons incorrectly treated a matching ConfigHash as proof the DB row was unchanged — even though ConfigHash is not updated on UI/API edits. Additionally, UpsertPlugin was being called during file sync, allowing a higher-versioned DB record to silently win over the file definition. This PR introduces a forceFileSync / forceClientSync flag for the source_of_truth=config.json path and switches to UpdatePlugin for the file sync path to enforce file authority unconditionally.

Changes

  • Introduced forceClientSync in loadClientConfig so that when source_of_truth=config.json and the client section is present in the file, the file always wins regardless of the stored ConfigHash.
  • Introduced forceFileSync in mergeGovernanceConfig so that all governance entities (budgets, rate limits, customers, teams, virtual keys, routing rules, pricing overrides, model configs) are re-synced from the file when source_of_truth=config.json, bypassing hash comparison.
  • Replaced UpsertPlugin with UpdatePlugin in syncPluginsFromFile so that file-defined plugins always overwrite DB state without version comparison.
  • Switched Update to UpdateColumn for customer_id budget unlinking in updateGovernanceConfigInStore and linkCustomerBudgetID to avoid unintended GORM hook side effects.
  • Implemented UpdatePlugin in MockConfigStore to properly replace the matching plugin by name.
  • Implemented UpsertPlugin in MockConfigStore with correct version-gating logic (skips update if the incoming version is lower than the stored version), making it accurate for non-file-sync paths.
  • Added tests covering file-overrides-DB scenarios for plugins under source_of_truth=config.json: lower file version, higher file version, equal version, plugin only in file, and plugin only in DB.

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Chore/CI

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Providers/Integrations
  • Plugins
  • UI (React)
  • Docs

How to test

go test ./transports/bifrost-http/lib/... -run TestSourceOfTruth

The new tests confirm that file-defined plugins and governance entities always override DB state when source_of_truth=config.json, regardless of version numbers or hash matches.

Screenshots/Recordings

N/A

Breaking changes

  • Yes
  • No

Related issues

Security considerations

No security implications. This change only affects how configuration authority is resolved between the config file and the database.

Checklist

  • I read docs/contributing/README.md and followed the guidelines
  • I added/updated tests where appropriate
  • I updated documentation where needed
  • I verified builds succeed (Go and UI)
  • I verified the CI pipeline passes locally if applicable

@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e04078aa-d87c-4b6c-b419-6e553209f603

📥 Commits

Reviewing files that changed from the base of the PR and between 0250183 and fefb0f2.

📒 Files selected for processing (2)
  • transports/bifrost-http/lib/config.go
  • transports/bifrost-http/lib/config_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • transports/bifrost-http/lib/config.go
  • transports/bifrost-http/lib/config_test.go

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved configuration file synchronization to ensure all settings apply from config.json even when internal checksums remain unchanged.
    • Fixed governance entity reconciliation (budgets, rate limits, customers, teams, keys, routing rules, pricing, and models) to properly update when config.json is the source of truth.
    • Enhanced plugin reconciliation with proper version comparison handling.
  • Tests

    • Added comprehensive test coverage for config.json source-of-truth reconciliation scenarios.

Walkthrough

This PR extends the config.json source-of-truth reconciliation by introducing a forced-sync pattern that overrides stored entity state from file data despite matching ConfigHash values. It applies this pattern across client config, governance entities (budgets, rate limits, customers, teams, virtual keys, routing rules, pricing overrides, model configs, and provider bindings), refines database update queries for budget unlinking, switches plugin sync to update-only semantics, and adds comprehensive tests for plugin override behavior.

Changes

Config reconciliation with forced sync and refined persistence

Layer / File(s) Summary
Client config forced sync despite hash match
transports/bifrost-http/lib/config.go
Client config reconciliation now introduces a forceClientSync flag when config.json is the source of truth and the client section is present, bypassing the hash-match early exit to force DB rows to resync from file even when ConfigHash matches.
Governance entity forced sync across budget, rate limit, customer, team, and other entity types
transports/bifrost-http/lib/config.go
Governance config merging applies a forceFileSync condition when config.json is the source of truth, forcing file-derived updates across all entity types (budgets, rate limits, customers, teams, virtual keys, routing rules, pricing overrides, model configs, provider governance bindings) despite stored ConfigHash matches.
Refined database updates for budget unlink operations
transports/bifrost-http/lib/config.go
Budget unlink operations in governance transactional updates and the linkCustomerBudgetID clearStale path now use UpdateColumn("customer_id", nil) instead of the generic Update method, targeting column-level updates with explicit nil values.
Plugin sync persistence from update-only semantics
transports/bifrost-http/lib/config.go
In syncPluginsFromFile, the per-plugin write switches from UpsertPlugin to UpdatePlugin, making the config.json sync path update-only for existing plugin rows.
Mock implementations and plugin reconciliation tests
transports/bifrost-http/lib/config_test.go
MockConfigStore.UpdatePlugin now replaces plugins by name; MockConfigStore.UpsertPlugin gains version-aware semantics. A comprehensive test suite verifies that file-declared plugins override DB state for enabled and config fields under source_of_truth=config.json, including when DB plugin versions are higher or equal, and asserts creation of file-only plugins and removal of DB-only plugins when the file provides a plugins section.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • maximhq/bifrost#3968: Both PRs implement/modify the source_of_truth=config.json reconciliation flow in transports/bifrost-http/lib/config.go, especially the authoritative plugin sync/reconciliation logic (e.g., syncPluginsFromFile behavior and related tests).

Suggested reviewers

  • danpiths
  • roroghost17

Poem

🐇 The config file speaks with force anew,
Hash matches yield to what's true—
File overrides rule the day,
UpdateColumn's better way,
Plugin syncs from old to new! 🌟

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: forcing file-wins synchronization for plugins, governance entities, and client config when source_of_truth=config.json.
Description check ✅ Passed The description is comprehensive and follows the template structure with all critical sections completed: Summary, Changes, Type of change, Affected areas, How to test, Breaking changes, and Checklist items appropriately marked.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 06-14-fix_use_updateplugin_when_source_of_truth_is_config_json

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Comment @coderabbitai help to get the list of available commands and usage tips.

BearTS commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

@BearTS
BearTS marked this pull request as ready for review June 15, 2026 05:29
@BearTS BearTS changed the title fix: use updatePlugin when source of truth is config json fix: use UpdatePlugin instead of UpsertPlugin when syncing plugins from config.json source of truth Jun 15, 2026
@greptile-apps

greptile-apps Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Confidence Score: 5/5

Safe to merge — the change is narrowly scoped to the source_of_truth=config.json boot path, the flag logic is correctly guarded, and mock implementations faithfully mirror the real store semantics.

The forceFileSync/forceClientSync flags are introduced with correct guards: governance uses isConfigJSONSourceOfTruth() (safe because mergeGovernanceConfig is already gated on configData.Governance != nil at the call site), and client config adds the extra sectionPresent("client") guard (redundant but harmless). UpdatePlugin in the real store does delete-then-create and correctly falls through to Create when ErrRecordNotFound, handling the new-plugin case. The five new tests exercise all materially distinct scenarios.

No files require special attention.

Important Files Changed

Filename Overview
transports/bifrost-http/lib/config.go Adds forceFileSync/forceClientSync flags to bypass hash comparison when source_of_truth=config.json, and switches syncPluginsFromFile from UpsertPlugin to UpdatePlugin; also switches two Update calls to UpdateColumn for customer_id unlinking. Logic is correct and well-guarded.
transports/bifrost-http/lib/config_test.go Implements previously stub UpdatePlugin and UpsertPlugin in MockConfigStore accurately mirroring real semantics; adds five new table-driven tests covering file-overrides-DB scenarios for plugins under source_of_truth=config.json.

Reviews (3): Last reviewed commit: "fix: use updatePlugin when source of tru..." | Re-trigger Greptile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
transports/bifrost-http/lib/config.go (1)

3844-3857: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve DB-backed plugin metadata before replacing the row.

UpdatePlugin deletes/recreates the row, so this now overwrites DB-backed version, placement, and order with config.json/default values. Under source_of_truth=config.json, only file-owned fields like enabled and config should override DB state; preserve existing metadata when building tablePlugin.

Suggested direction
+		existingByName := make(map[string]*configstoreTables.TablePlugin, len(existing))
 		for _, plugin := range existing {
+			if plugin != nil {
+				existingByName[plugin.Name] = plugin
+			}
 			if plugin != nil && !keep[plugin.Name] {
 				if err := config.ConfigStore.DeletePlugin(ctx, plugin.Name, tx); err != nil {
 					return fmt.Errorf("failed to delete plugin %s: %w", plugin.Name, err)
 				}
 			}
@@
-			if plugin.Version == nil {
-				plugin.Version = bifrost.Ptr(int16(1))
-			}
+			version := int16(1)
+			var placement *schemas.PluginPlacement
+			var order *int
+			if existingPlugin := existingByName[plugin.Name]; existingPlugin != nil {
+				version = existingPlugin.Version
+				placement = existingPlugin.Placement
+				order = existingPlugin.Order
+			}
 			tablePlugin := &configstoreTables.TablePlugin{
 				Name:      plugin.Name,
 				Enabled:   plugin.Enabled,
 				Config:    pluginConfigCopy,
 				Path:      plugin.Path,
-				Version:   *plugin.Version,
-				Placement: plugin.Placement,
-				Order:     plugin.Order,
+				Version:   version,
+				Placement: placement,
+				Order:     order,
 			}

As per coding guidelines, transports/config.schema.json is the source of truth and plugin version, placement, and order are DB-backed-only metadata while config.json overrides should focus on enabled and config.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@transports/bifrost-http/lib/config.go` around lines 3844 - 3857, The
UpdatePlugin call in the plugin configuration block is overwriting
database-backed metadata (version, placement, and order) with config.json
values, which violates the source of truth principle. Before constructing the
tablePlugin struct, fetch the existing plugin record from the database to
retrieve its current version, placement, and order values. When building the
tablePlugin struct around line 3848-3857, use the preserved database values for
the Version, Placement, and Order fields instead of the values from the
config.json plugin object; only allow config.json to override the Enabled and
Config fields. This ensures that DB-backed metadata is preserved while
config.json changes are correctly applied only to file-owned fields.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@transports/bifrost-http/lib/config_test.go`:
- Around line 13016-13051: Add four new test functions following the pattern of
TestSourceOfTruthConfigJSON_PluginsPresentFileOverridesDB to cover additional
edge cases for the source_of_truth=config.json reconciliation behavior. Create
test functions for: (1) file version greater than DB version to verify file
override still applies, (2) file version equal to DB version to ensure correct
behavior when versions match, (3) plugin existing in file but not in DB to
validate new plugin creation, and (4) plugin existing in DB but not in file to
confirm the plugin is preserved and not deleted. Each test should follow the
same setup pattern with loadPlugins and appropriate assertions to verify the
expected behavior in each scenario.
- Around line 1271-1278: Combine the two separate loops iterating through
m.plugins (the version check loop starting with the comparison of plugin.Version
and the subsequent filter-rebuild loop) into a single loop to reduce iteration
overhead. Within this unified loop, perform both the version check (returning
nil if plugin.Version is less than p.Version) and the filter-rebuild operations
in one pass through the m.plugins collection. This optimization maintains the
same logic and behavior while eliminating the duplicate iteration.

---

Outside diff comments:
In `@transports/bifrost-http/lib/config.go`:
- Around line 3844-3857: The UpdatePlugin call in the plugin configuration block
is overwriting database-backed metadata (version, placement, and order) with
config.json values, which violates the source of truth principle. Before
constructing the tablePlugin struct, fetch the existing plugin record from the
database to retrieve its current version, placement, and order values. When
building the tablePlugin struct around line 3848-3857, use the preserved
database values for the Version, Placement, and Order fields instead of the
values from the config.json plugin object; only allow config.json to override
the Enabled and Config fields. This ensures that DB-backed metadata is preserved
while config.json changes are correctly applied only to file-owned fields.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 053c73a1-c713-468c-b383-0ff6d8aba789

📥 Commits

Reviewing files that changed from the base of the PR and between a35a4c8 and 0810a22.

📒 Files selected for processing (2)
  • transports/bifrost-http/lib/config.go
  • transports/bifrost-http/lib/config_test.go

Comment thread transports/bifrost-http/lib/config_test.go Outdated
Comment thread transports/bifrost-http/lib/config_test.go
@BearTS
BearTS force-pushed the 06-14-fix_use_updateplugin_when_source_of_truth_is_config_json branch from 0810a22 to 0250183 Compare June 15, 2026 07:43
@BearTS BearTS changed the title fix: use UpdatePlugin instead of UpsertPlugin when syncing plugins from config.json source of truth fix: force file-wins sync for plugins, governance entities, and client config when source_of_truth=config.json Jun 15, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@transports/bifrost-http/lib/config_test.go`:
- Around line 13012-13176: Consolidate the five separate test functions
(TestSourceOfTruthConfigJSON_PluginsPresentFileOverridesDB,
TestSourceOfTruthConfigJSON_FileVersionGreaterThanDB,
TestSourceOfTruthConfigJSON_FileVersionEqualToDBVersion,
TestSourceOfTruthConfigJSON_PluginInFileNotInDB, and
TestSourceOfTruthConfigJSON_PluginInDBNotInFile) into a single table-driven
test. Define a test case struct containing the input parameters (DB plugins,
file plugin config, source of truth) and expected output assertions (enabled
status, config values, store updates). Create a slice of test cases representing
each scenario, then iterate through them with a loop that calls loadPlugins and
verifies all assertions for each case. This eliminates the repeated setup/assert
blocks and makes adding new scenarios simpler and safer.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9a3dd045-d276-4834-b7f9-826ba0a6c030

📥 Commits

Reviewing files that changed from the base of the PR and between 0810a22 and 0250183.

📒 Files selected for processing (2)
  • transports/bifrost-http/lib/config.go
  • transports/bifrost-http/lib/config_test.go

Comment thread transports/bifrost-http/lib/config_test.go
@BearTS
BearTS force-pushed the 06-14-fix_use_updateplugin_when_source_of_truth_is_config_json branch from 0250183 to fefb0f2 Compare June 15, 2026 07:54
@coderabbitai
coderabbitai Bot requested review from danpiths and roroghost17 June 15, 2026 07:55

akshaydeo commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Merge activity

  • Jun 15, 9:10 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Jun 15, 9:10 AM UTC: @akshaydeo merged this pull request with Graphite.

@akshaydeo
akshaydeo merged commit e9bf83f into dev Jun 15, 2026
16 checks passed
@akshaydeo
akshaydeo deleted the 06-14-fix_use_updateplugin_when_source_of_truth_is_config_json branch June 15, 2026 09:10
akshaydeo pushed a commit that referenced this pull request Jun 15, 2026
…t config when `source_of_truth=config.json` (#4381)

## Summary

When `source_of_truth=config.json`, the config file should always be the authoritative source for plugins and governance entities, regardless of what is stored in the database. Previously, UI/API edits could cause drift that persisted across restarts because hash comparisons incorrectly treated a matching `ConfigHash` as proof the DB row was unchanged — even though `ConfigHash` is not updated on UI/API edits. Additionally, `UpsertPlugin` was being called during file sync, allowing a higher-versioned DB record to silently win over the file definition. This PR introduces a `forceFileSync` / `forceClientSync` flag for the `source_of_truth=config.json` path and switches to `UpdatePlugin` for the file sync path to enforce file authority unconditionally.

## Changes

- Introduced `forceClientSync` in `loadClientConfig` so that when `source_of_truth=config.json` and the `client` section is present in the file, the file always wins regardless of the stored `ConfigHash`.
- Introduced `forceFileSync` in `mergeGovernanceConfig` so that all governance entities (budgets, rate limits, customers, teams, virtual keys, routing rules, pricing overrides, model configs) are re-synced from the file when `source_of_truth=config.json`, bypassing hash comparison.
- Replaced `UpsertPlugin` with `UpdatePlugin` in `syncPluginsFromFile` so that file-defined plugins always overwrite DB state without version comparison.
- Switched `Update` to `UpdateColumn` for `customer_id` budget unlinking in `updateGovernanceConfigInStore` and `linkCustomerBudgetID` to avoid unintended GORM hook side effects.
- Implemented `UpdatePlugin` in `MockConfigStore` to properly replace the matching plugin by name.
- Implemented `UpsertPlugin` in `MockConfigStore` with correct version-gating logic (skips update if the incoming version is lower than the stored version), making it accurate for non-file-sync paths.
- Added tests covering file-overrides-DB scenarios for plugins under `source_of_truth=config.json`: lower file version, higher file version, equal version, plugin only in file, and plugin only in DB.

## Type of change

- [x] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [x] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./transports/bifrost-http/lib/... -run TestSourceOfTruth
```

The new tests confirm that file-defined plugins and governance entities always override DB state when `source_of_truth=config.json`, regardless of version numbers or hash matches.

## Screenshots/Recordings

N/A

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

## Security considerations

No security implications. This change only affects how configuration authority is resolved between the config file and the database.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants