Skip to content

Fix/remote plugin already loaded - #5749

Closed
vvidovic wants to merge 3 commits into
maximhq:devfrom
vvidovic:fix/remote-plugin-already-loaded
Closed

Fix/remote plugin already loaded#5749
vvidovic wants to merge 3 commits into
maximhq:devfrom
vvidovic:fix/remote-plugin-already-loaded

Conversation

@vvidovic

Copy link
Copy Markdown

Summary

Fixes a "plugin already loaded" error when updating a custom plugin loaded from a remote URL. When a remote plugin was updated (e.g. config change, enable/disable), Bifrost re-downloaded the same .so bytes, wrote them to a new temp path, and called plugin.Open again. Go's plugin system treats any binary with the same checksum as already loaded, failing with "plugin already loaded".

Changes

  • framework/plugins/utils.go: DownloadPlugin now computes SHA-256 of plugin bytes during download, derives a stable path as /tmp/bifrost-plugin-<hash>.so, and atomically renames the temp file. Subsequent downloads of identical content return the existing stable path from an in-memory cache with no file I/O.

  • framework/plugins/soloader.go: After download, openPlugin checks the in-memory cache. If cached.loadedPlugin != nil, the cached *plugin.Plugin is returned directly — plugin.Open is skipped entirely. This is the core fix: the same binary content is only opened once per process.

  • framework/plugins/soplugin.go: Added contentHash field to DynamicPlugin to track the binary's SHA-256.

  • framework/plugins/soplugin_test.go: Removed 3 tests (TestLoadPlugin_CacheHitSkipsOpen, TestLoadPlugin_ConfigUpdateReusesPlugin, TestLoadPlugin_ContentChangeLoadsNewPlugin) that attempted to load the same plugin binary twice in a single test process. Go's plugin system does not support unloading or re-loading a module, so these tests were testing unsupported behavior.

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

Describe the steps to validate this change. Include commands and expected outcomes.

cd framework
go test ./plugins/... -v -count=1
All 23 tests pass. Existing tests (TestLoadPlugins_MultiplePlugins, TestDownloadPlugin_StablePathByContentHash, TestDownloadPlugin_IdenticalContentDifferentURLs) cover the behavior.

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

Closes #5741

## Security considerations

No.

## Checklist

- [x] 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)
- [x] I verified the CI pipeline passes locally if applicable

BearTS and others added 3 commits July 31, 2026 13:01
…hq#5705)

## Summary

Fixes a Helm schema validation failure introduced in v2.1.32 where multi-profile OTEL configs (`bifrost.plugins.otel.config.profiles`) would fail with `Additional property export_timeout is not allowed`, blocking Helm render and deploy.

## Changes

- Added `export_timeout` as an explicitly allowed (but deprecated) property in `values.schema.json` so that Helm's default map merge does not reject values when transitioning from the legacy flat config shape to the `profiles` wrapper shape.
- Commented out the `export_timeout: 5` default in `values.yaml` to prevent it from being injected into the config and triggering the schema conflict.
- Added a warning to the v2.1.32 changelog directing users to use v2.1.33 instead.
- Published the v2.1.33 changelog documenting the fix.
- Bumped the Helm chart version to `2.1.33`.

## Type of change

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

## Affected areas

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

## How to test

```sh
# Render the Helm chart with a multi-profile OTEL config to confirm schema validation passes
helm template bifrost ./helm-charts/bifrost \
  --set bifrost.plugins.otel.config.profiles[0].endpoint="http://otel-collector:4318" \
  --set bifrost.plugins.otel.config.profiles[0].export_timeout=10

# Should render without errors; previously would fail with:
# "Additional property export_timeout is not allowed"
```

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

Regression introduced in v2.1.32 by the addition of the `export_timeout` default value.

## Security considerations

None.

## Checklist

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

Adds documentation and OpenAPI specs for budget overrides on virtual-key budgets. An override temporarily raises a budget's effective spending limit by adding an `override_amount` on top of the base `max_limit`, without touching the base limit, current usage, or reset schedule. Overrides can be granted for a finite number of reset cycles or indefinitely, and are managed via `PUT`/`DELETE` on `/api/governance/virtual-keys/{vk_id}/budgets/{budget_id}/override`.

## Changes

- Added a **Budget overrides** section to `budget-and-limits.mdx` explaining the `effective_max_limit = max_limit + override_amount` enforcement model and linking to the virtual keys walkthrough.
- Expanded the override section in `virtual-keys.mdx` with full API usage examples for `PUT` (both `cycles` and `forever` modes) and `DELETE`, a sample JSON response, and behavioral notes covering immutability of the base limit, cycle anchoring, and cluster consistency.
- Registered `PUT` and `DELETE` endpoints for `/api/governance/virtual-keys/{vk_id}/budgets/{budget_id}/override` in the OpenAPI spec (both JSON and YAML), including request/response schemas (`BudgetOverrideRequest`, `BudgetOverrideResponse`) and new override fields on the `Budget` schema (`override_amount`, `override_mode`, `override_cycles_remaining`, `override_cycles_total`, `override_anchor_reset`).

## Type of change

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

## Affected areas

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

## How to test

1. Navigate to the **Budget overrides** section in `budget-and-limits.mdx` and confirm the formula and cross-link render correctly.
2. Navigate to the override API section in `virtual-keys.mdx` and verify all code blocks, the sample response, and the behavioral notes display as expected.
3. Load the OpenAPI spec and confirm the two new endpoints appear under the **Governance** tag with correct request/response schemas and that the `Budget` schema includes the five new override fields.

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

## Security considerations

The new endpoints require `ManagementBearerAuth`, consistent with all other governance management endpoints. No new auth surface is introduced.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [ ] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [ ] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable
fix(framework/plugins): prevent "plugin already loaded" on remote plugin update

When a custom plugin loaded from a remote URL was updated via the API (e.g.
config change, enable/disable), Bifrost re-downloaded the same .so bytes,
wrote them to a new temp path, and called plugin.Open again. Go's plugin
system treats any plugin binary with the same checksum as already loaded,
failing with "plugin already loaded".

Fix uses SHA-256 content hash as the stable on-disk filename and an
in-memory cache keyed by content hash (not URL):

- DownloadPlugin (utils.go): computes SHA-256 while streaming the response
  body, derives the stable path as /tmp/bifrost-plugin-<hash>.so, and
  atomically renames the temp file to it. Subsequent downloads of the same
  content return the existing stable path from cache with no file I/O.

- openPlugin (soloader.go): after download, checks the in-memory cache. If
  cached.loadedPlugin != nil, returns it directly — plugin.Open is skipped
  entirely. This is the key fix: the same binary is only opened once.

- DynamicPlugin (soplugin.go): added contentHash field to track the binary's
  SHA-256.

- Removed 3 tests (soplugin_test.go) that attempted to load the same plugin
  binary twice in a single test process. This triggers Go's "plugin already
  loaded" error and is unsupported — plugins are loaded once per process.

Fixes maximhq#5741.
@vvidovic
vvidovic requested a review from a team as a code owner July 31, 2026 14:44
@CLAassistant

CLAassistant commented Jul 31, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3929d61f-0533-4ff1-b34d-925065e61f58

📥 Commits

Reviewing files that changed from the base of the PR and between 29e49c9 and 365fade.

📒 Files selected for processing (19)
  • docker-volume/config.json
  • docs/changelogs/helm-v2.1.32.mdx
  • docs/changelogs/helm-v2.1.33.mdx
  • docs/docs.json
  • docs/features/governance/budget-and-limits.mdx
  • docs/features/governance/virtual-keys.mdx
  • docs/openapi/openapi.json
  • docs/openapi/openapi.yaml
  • docs/openapi/paths/management/governance.yaml
  • docs/openapi/schemas/management/governance.yaml
  • framework/plugins/soloader.go
  • framework/plugins/soplugin.go
  • framework/plugins/soplugin_test.go
  • framework/plugins/utils.go
  • framework/plugins/utils_test.go
  • helm-charts/bifrost/Chart.yaml
  • helm-charts/bifrost/README.md
  • helm-charts/bifrost/values.schema.json
  • helm-charts/bifrost/values.yaml

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added virtual-key budget overrides with temporary or indefinite duration, replacement and removal support, and effective-limit visibility.
    • Added Docker configuration for SQLite-backed settings and logs, plus user-information plugin support.
  • Bug Fixes

    • Fixed Helm validation failures for multi-profile OpenTelemetry configurations.
    • Updated the Helm chart to version 2.1.33.
  • Documentation

    • Added API reference and usage guidance for budget overrides.
    • Documented the Helm 2.1.33 fix and warning for version 2.1.32.

Walkthrough

The change adds virtual-key budget override API contracts and documentation, content-hash caching for remote plugins, Helm 2.1.33 OTEL schema updates, and Docker volume configuration for SQLite stores and the user-info plugin.

Changes

Virtual-key budget overrides

Layer / File(s) Summary
Budget override schemas
docs/openapi/schemas/management/governance.yaml, docs/openapi/openapi.json
Budget schemas now expose override details. Request and response schemas define validation and effective limits.
Management endpoints
docs/openapi/paths/management/governance.yaml, docs/openapi/openapi.yaml, docs/openapi/openapi.json
Authenticated PUT and DELETE operations manage virtual-key budget overrides.
Governance documentation
docs/features/governance/budget-and-limits.mdx, docs/features/governance/virtual-keys.mdx
Documentation covers duration, replacement, removal, validation, reset behavior, and API examples.

Content-hash plugin caching

Layer / File(s) Summary
Stable plugin downloads
framework/plugins/utils.go
Plugin downloads now return SHA-256 hashes and stable hash-based paths. Cache reuse and atomic file replacement are implemented.
Hashed plugin loading
framework/plugins/soloader.go, framework/plugins/soplugin.go
Plugin loading reuses cached instances or paths for identical binaries. DynamicPlugin stores the content hash.
Download and cache tests
framework/plugins/utils_test.go, framework/plugins/soplugin_test.go
Tests cover hash generation, stable paths, cache reuse, distinct content, errors, redirects, and extensions.

Helm 2.1.33 correction

Layer / File(s) Summary
OTEL configuration and chart version
helm-charts/bifrost/Chart.yaml, helm-charts/bifrost/values.schema.json, helm-charts/bifrost/values.yaml
The chart version becomes 2.1.33. The OTEL wrapper schema accepts deprecated export_timeout, while the active default is unset.
Release documentation and navigation
docs/changelogs/*, docs/docs.json, helm-charts/bifrost/README.md
The changelogs, README, and documentation navigation describe the v2.1.32 warning and v2.1.33 fix.

Docker volume configuration

Layer / File(s) Summary
SQLite and user-info plugin configuration
docker-volume/config.json
Docker volume configuration adds SQLite config and log stores and enables the user-info plugin.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: madhuvod, akshaydeo

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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"

🔧 oasdiff (1.26.0)
docs/openapi/openapi.yaml

Error: failed to load base spec from "/tmp/coderabbit-oasdiff-base.GtdVHi": encountered disallowed external reference: "./paths/inference/async.yaml#/components/parameters/AsyncJobId"


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.

❤️ Share

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

Warning

⚠️ This pull request shows signs of AI-generated slop (description_diff_mismatch). It has been flagged by CodeRabbit slop detection and should be reviewed carefully.

@vvidovic vvidovic closed this Jul 31, 2026
@coderabbitai
coderabbitai Bot requested review from Madhuvod and akshaydeo July 31, 2026 14:46
@vvidovic

Copy link
Copy Markdown
Author

dev branch was rewritten in the meantime, rebasing to the proper latest dev branch.

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.

5 participants