Skip to content

feat: dynamic config registry for extensible package-level configs#117

Open
gaarutyunov wants to merge 2 commits into
mainfrom
claude/issue-113-20260415-0755
Open

feat: dynamic config registry for extensible package-level configs#117
gaarutyunov wants to merge 2 commits into
mainfrom
claude/issue-113-20260415-0755

Conversation

@gaarutyunov

@gaarutyunov gaarutyunov commented Apr 15, 2026

Copy link
Copy Markdown
Owner

Implements the dynamic config registry described in #113.

Adds a DynamicConfig wrapper over ProxyConfig that allows packages to register their own config sections via init(), eliminating conflicts when multiple features edit the monolithic config file. Operator CRDs gain Extensions fields for passing arbitrary package config through Kubernetes.

Closes #113

Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Added support for extensible proxy and upstream configurations with custom extension sections.
    • Configuration system now supports registering and loading additional configuration extensions at proxy and upstream levels.
    • Enhanced configuration loader with support for dynamic extension population during startup and reload operations.

…configs

Adds a DynamicConfig wrapper over ProxyConfig that allows packages to
register their own config sections under specific keys via init(). This
enables SDK consumers to extend the proxy without modifying the core
config package — eliminating conflicts when multiple features edit the
same file and enabling true SDK-first extensibility.

Key changes:
- pkg/config/registry.go: New file with RegisterProxySection and
  RegisterUpstreamSection for proxy and upstream extension points.
  DynamicConfig wraps ProxyConfig and holds dynamically populated
  extension sections. GetProxySection[T] and GetUpstreamSection[T]
  provide typed access. mapProvider implements koanf.Provider for
  building scoped koanf instances from raw upstream map data.
- pkg/config/loader.go: Load() now returns *DynamicConfig. Loader
  stores *DynamicConfig and its callback accepts *DynamicConfig.
- pkg/mcp/manager.go: Rebuild() accepts *config.DynamicConfig.
- pkg/mcpanything/proxy.go: Proxy.cfg and LoadConfig/New updated to
  DynamicConfig; functions expecting *ProxyConfig receive &cfg.ProxyConfig.
- pkg/caddy/caddy.go: Updated for new Load() return type.
- pkg/crd/v1alpha1/types.go: Added Extensions map[string]apiextensionsv1.JSON
  to MCPProxySpec and MCPUpstreamSpec for CRD-level extension.
- pkg/crd/v1alpha1/zz_generated.deepcopy.go: Regenerated via controller-gen
  with deepcopy for new Extensions fields.
- pkg/operator/configgen/generator.go: Extensions from CRD specs are
  decoded from JSON and inlined into the generated YAML using yaml:",inline".
- charts/mcp-anything/crds/: Regenerated CRD manifests.

Closes #113

Co-authored-by: German Arutyunov <gaarutyunov@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Apr 15, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

The PR introduces a dynamic configuration registry pattern across the system. It adds a DynamicConfig type that embeds ProxyConfig and stores extension sections, implements concurrent registries allowing packages to register configuration factory functions keyed by paths, updates the Loader to work with DynamicConfig, and modifies callers to use the new type. Similar registry infrastructure is added to operator config generation for CRD-based extensions.

Changes

Cohort / File(s) Summary
Core Config Registry Infrastructure
pkg/config/registry.go
New module implementing init-style dynamic configuration registry with SectionFactory type, RegisterProxySection/RegisterUpstreamSection functions, DynamicConfig struct embedding ProxyConfig, and generic accessors GetProxySection/GetUpstreamSection. Includes loadExtensions for populating proxy and upstream sections from registered factories with error wrapping.
Config Loader Updates
pkg/config/loader.go
Updated Loader type to store/return atomic.Pointer[DynamicConfig] and accept func(*DynamicConfig) error callback. Changed Load() to return *DynamicConfig, construct DynamicConfig{ProxyConfig: cfg}, and populate extension sections via loadExtensions(). Reload control flow remains unchanged but operates on DynamicConfig.
Config API Updates
pkg/caddy/caddy.go, pkg/mcpanything/proxy.go, pkg/mcp/manager.go
Updated callers to use *config.DynamicConfig instead of *config.ProxyConfig. Changed function signatures for LoadConfig() and New() in proxy.go, Rebuild() in manager.go, and loader callback types. Adjusted internal calls to pass &cfg.ProxyConfig where proxy-specific config is needed.
Operator Config Generation Registry
pkg/operator/configgen/registry.go
New module defining ProxySectionFactory and UpstreamSectionFactory types for operator-level extension registration, with concurrent registries and RegisterProxySectionFactory/RegisterUpstreamSectionFactory functions. Includes applyProxyExtensions and applyUpstreamExtensions helpers for invoking factories and collecting results.
Operator Config Generation Logic
pkg/operator/configgen/generator.go
Extended generatedProxyConfig and generatedUpstreamConfig with Extensions map[string]interface{} fields. Updated Generate() to apply registered proxy section factories via applyProxyExtensions(). Modified buildUpstreamConfig() to accept context and apply registered upstream section factories via applyUpstreamExtensions().
Generated Code
pkg/crd/v1alpha1/zz_generated.deepcopy.go
Updated import alias from unaliased v1 to metav1 and adjusted DeepCopyInto logic for MCPProxyStatus and MCPUpstreamStatus to allocate Conditions slices as []metav1.Condition.

Sequence Diagram(s)

sequenceDiagram
    participant Pkg as Extension Package
    participant Reg as Config Registry
    participant Loader as Config Loader
    participant Factory as Factory Function
    participant Store as DynamicConfig Storage
    
    Note over Pkg,Store: Extension Registration Phase (init-time)
    Pkg->>Reg: RegisterProxySection(key, factory)
    activate Reg
    Reg->>Reg: Lock registry
    Reg->>Reg: Add factory to registry[key]
    Reg->>Reg: Unlock registry
    deactivate Reg
    
    Note over Pkg,Store: Config Loading Phase (runtime)
    Loader->>Loader: Load(path) → parse YAML
    Loader->>Reg: loadExtensions(d, koanf, rawUpstreams)
    activate Reg
    Reg->>Reg: Read lock & snapshot registries
    loop For each registered proxy section key
        Reg->>Factory: factory(koanf.Cut(key))
        activate Factory
        Factory-->>Reg: extension value
        deactivate Factory
        Reg->>Store: proxySections[key] = value
    end
    loop For each upstream
        loop For each registered upstream section key
            Reg->>Factory: factory(scoped koanf)
            activate Factory
            Factory-->>Reg: extension value
            deactivate Factory
            Reg->>Store: upstreamSections[i][key] = value
        end
    end
    Reg->>Reg: Unlock registry
    deactivate Reg
    Loader->>Store: Return DynamicConfig with populated sections
Loading
sequenceDiagram
    participant Op as Operator ConfigGen
    participant Reg as ConfigGen Registry
    participant ProxyFac as Proxy Factory
    participant UpFac as Upstream Factory
    participant Out as Generated Config
    
    Note over Op,Out: Operator-driven Extension Generation
    Op->>Op: Generate(ctx, proxy, upstreams)
    Op->>Reg: applyProxyExtensions(ctx, proxy, upstreams, exts)
    activate Reg
    Reg->>Reg: Read lock & snapshot factories
    loop For each registered proxy factory
        Reg->>ProxyFac: factory(ctx, proxy, upstreams)
        activate ProxyFac
        ProxyFac-->>Reg: extension value
        deactivate ProxyFac
        Reg->>Out: exts[key] = value
    end
    Reg->>Reg: Unlock registry
    deactivate Reg
    
    loop For each upstream
        Op->>Reg: applyUpstreamExtensions(ctx, upstream, exts)
        activate Reg
        Reg->>Reg: Read lock & snapshot factories
        loop For each registered upstream factory
            Reg->>UpFac: factory(ctx, upstream)
            activate UpFac
            UpFac-->>Reg: extension value
            deactivate UpFac
            Reg->>Out: exts[key] = value
        end
        Reg->>Reg: Unlock registry
        deactivate Reg
    end
    
    Op->>Out: Return config with Extensions
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 A registry of dreams, where config flows free,
No more entangled branches on the proxy tree!
Extensions hop in, each in their place,
Dynamic and graceful, at their own pace.
The SDK breathes deep—no conflicts in sight,
Just organized harmony, configured just right. 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective: introducing a dynamic config registry pattern for extensible configurations at the package level.
Linked Issues check ✅ Passed The PR implements core registry infrastructure (#113): dynamic config wrapper, registration APIs for proxy/upstream sections, generic accessors, and operator integration with modular factory pattern for CRD-driven generation.
Out of Scope Changes check ✅ Passed All changes align with #113 scope: registry implementation, config type refactoring to DynamicConfig, operator integration, and deepcopy autogeneration updates are all required for the registry pattern.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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 claude/issue-113-20260415-0755

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 and usage tips.

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/config/registry.go`:
- Around line 23-27: The register helpers currently overwrite existing entries
in proxyRegistry and upstreamRegistry under sectionMu; change
RegisterProxySection and RegisterUpstreamSection to acquire the write lock,
check if the key already exists, and fail loudly instead of replacing it (either
return an error from the registration function or panic/log.Fatal with a
descriptive message that includes the duplicate key and registry type) so
duplicate init-time registrations cannot silently clobber each other; ensure the
check-and-set is done atomically while holding sectionMu to avoid races.
- Around line 117-126: The loop over proxyCopy always calls each proxy factory
and unconditionally sets d.proxySections[key], which can make absent sections
appear present; change the logic in the proxy-level section construction to
first check whether the section key exists in the config before invoking the
factory (use the same presence check semantics as GetProxySection), and only
call f(sub) and assign d.proxySections[key] when the section is present; keep
using k.Cut(sub) for the factory input and preserve the existing error wrapping
when f(sub) returns an error.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9d399b8c-7206-45e2-9d49-3f7f2edf4267

📥 Commits

Reviewing files that changed from the base of the PR and between a4a9284 and 9d2a2ea.

📒 Files selected for processing (10)
  • charts/mcp-anything/crds/mcpproxy.yaml
  • charts/mcp-anything/crds/mcpupstream.yaml
  • pkg/caddy/caddy.go
  • pkg/config/loader.go
  • pkg/config/registry.go
  • pkg/crd/v1alpha1/types.go
  • pkg/crd/v1alpha1/zz_generated.deepcopy.go
  • pkg/mcp/manager.go
  • pkg/mcpanything/proxy.go
  • pkg/operator/configgen/generator.go

Comment thread pkg/config/registry.go
Comment thread pkg/config/registry.go
@gaarutyunov

Copy link
Copy Markdown
Owner Author

@claude no extensions with additional properties. The typing will be regenerated with strict types for each proxy instance. The operator for this must be modular to in the same way proxy is.

@claude

claude Bot commented Apr 15, 2026

Copy link
Copy Markdown

Claude finished @gaarutyunov's task in 13m 19s —— View job


Changes pushed

  • Read current code to understand architecture
  • Understand request: remove additionalProperties from CRDs, make operator modular like proxy
  • Fix pkg/config/registry.go: panic on duplicate registrations + skip absent proxy sections
  • Remove Extensions map[string]apiextensionsv1.JSON from MCPProxySpec and MCPUpstreamSpec
  • Remove deepcopy for Extensions and apiextensionsv1 import from zz_generated.deepcopy.go
  • Remove extensions: / additionalProperties: from CRD YAML schemas
  • Create pkg/operator/configgen/registry.go with modular ProxySectionFactory / UpstreamSectionFactory pattern
  • Update generator.go: use registry, remove decodeExtensions, drop apiextensionsv1 import
  • make lint && make test — 0 issues, all tests pass
  • Committed and pushed

What changed

pkg/operator/configgen/registry.go (new file) — operator-side registry mirroring pkg/config/registry.go:

  • ProxySectionFactory — receives *v1alpha1.MCPProxy + []v1alpha1.MCPUpstream, returns a typed YAML section value
  • UpstreamSectionFactory — receives *v1alpha1.MCPUpstream, returns a typed YAML section value
  • RegisterProxySectionFactory / RegisterUpstreamSectionFactoryinit()-based registration, panics on duplicate
  • applyProxyExtensions / applyUpstreamExtensions — called by Generate to collect factory output into the inline Extensions maps on the generated structs

CRD typesExtensions map[string]apiextensionsv1.JSON removed from MCPProxySpec and MCPUpstreamSpec; apiextensionsv1 import dropped from both types.go and the deepcopy file. CRD YAML schemas updated accordingly (no more additionalProperties: x-kubernetes-preserve-unknown-fields: true).

pkg/config/registry.go — fixed two CodeRabbit issues: duplicate registrations now panic with a descriptive message; proxy factory loop skips keys absent from the config file.
|

@gaarutyunov

Copy link
Copy Markdown
Owner Author

@claude you also haven’t implemented anything. Just the registry. This is not what I meant. I meant migrating existing configs too, because all our code is modular. Let’s not implement it, but decompose the tasks into refactoring tasks: runtime configs, inbound and outbound configs, and others. Propose the decomposition first.

@claude

claude Bot commented Apr 15, 2026

Copy link
Copy Markdown

Claude finished @gaarutyunov's task in 3m 46s —— View job


Task decomposition: migrate existing configs to the registry pattern

  • Gather context: explored all existing config types and packages
  • Map config types to registry pattern
  • Propose decomposed refactoring tasks

The current PR only built the registry infrastructure (DynamicConfig, RegisterProxySection, RegisterUpstreamSection). The 13 config types still embedded in the monolithic ProxyConfig / UpstreamConfig need to be extracted into their owning packages one group at a time. Below is the proposed decomposition.


Current state

ProxyConfig owns 13 top-level sections, several of which already have associated packages that have their own registries:

Config field(s) Owning package Registry?
Runtime (JS + Lua pool sizes) pkg/runtime no
InboundAuth + strategy sub-configs pkg/auth/inbound yes (for strategies)
Upstreams[i].OutboundAuth + sub-configs pkg/auth/outbound yes (for strategies)
RateLimits + RateLimitStore pkg/ratelimit yes (for stores)
Caches + CacheStore pkg/cache yes (for stores)
SessionStore pkg/session yes (for backends)
CircuitBreakers pkg/upstream/circuitbreaker no
ToolSearch + Embedding pkg/search / pkg/embedding yes (for providers)
TokenCounting pkg/tokencounter no
Server, Telemetry, Naming, Groups core (no sub-package) stays in ProxyConfig

Proposed refactoring tasks

Task 1 — Runtime config (pkg/runtime)

Move RuntimeConfig, JSRuntimeConfig, LuaRuntimeConfig from pkg/configpkg/runtime.
Register with config.RegisterProxySection("runtime", ...) in pkg/runtime's init().
Update NewRegistry(cfg config.RuntimeConfig)NewRegistry(cfg RuntimeConfig).
Consumers (pkg/mcpanything/proxy.go) switch to config.GetProxySection[runtime.Config](cfg,).

Task 2 — Inbound auth config (pkg/auth/inbound)

Move InboundAuthConfig, JWTAuthConfig, IntrospectionConfig, APIKeyAuthConfig, LuaAuthConfig, JSAuthConfigpkg/auth/inbound.
Register the inbound section at both proxy level (inbound_auth) and upstream level (inbound_auth_override) in pkg/auth/inbound's init().
Each strategy sub-package (jwt/, introspection/, apikey/, lua/, js/) already registers a ValidatorFactory; their strategy-specific config structs can stay co-located with each strategy.
Consumers that reference cfg.InboundAuth switch to config.GetProxySection[inbound.Config](cfg,).

Task 3 — Outbound auth config (pkg/auth/outbound)

Move OutboundAuthConfig, BearerOutboundConfig, APIKeyOutboundConfig, OAuth2CCConfig, OAuth2UserSessionConfig, LuaOutboundConfig, JSOutboundConfigpkg/auth/outbound.
Register as an upstream section (outbound_auth) in pkg/auth/outbound's init().
Each strategy sub-package owns its strategy-config struct (same as inbound).
Consumers that reference upstream.OutboundAuth switch to config.GetUpstreamSection[outbound.Config](cfg, i,).

Task 4 — Rate limiting config (pkg/ratelimit)

Move RateLimitConfig, RateLimitStoreConfig, RedisStoreConfigpkg/ratelimit.
StoreFactory already takes *config.ProxyConfig; change it to take *ratelimit.Config.
Register both sections as proxy sections (rate_limits, rate_limit_store).
The store sub-packages (memory/, redis/) continue registering via ratelimit.Register() but now own their backing config structs (e.g. redis.StoreConfig).

Task 5 — Cache config (pkg/cache)

Move CacheConfig, CacheStoreConfig, RedisCacheConfigpkg/cache.
Register as proxy sections (caches, cache_store).
Same pattern as rate limiting: store sub-packages own their connection configs.

Task 6 — Session store config (pkg/session)

Move SessionStoreConfig, PostgresSessionConfig, RedisSessionConfigpkg/session.
StoreFactory already takes *config.SessionStoreConfig; update signature.
Register as proxy section (session_store).
OAuthTokenStore and OAuthCallbackRegistrar interfaces can stay in pkg/config since they are part of the core contract consumed by OutboundAuthConfig.

Task 7 — Circuit breaker config (pkg/upstream/circuitbreaker)

Move CircuitBreakerConfigpkg/upstream/circuitbreaker.
Register as proxy section (circuit_breakers).
pkg/mcp/manager.go (the main consumer) switches to config.GetProxySection[circuitbreaker.Policies](cfg,).

Task 8 — Tool search + embedding config (pkg/search + pkg/embedding)

Move ToolSearchConfig, EmbeddingConfig, and all provider configs (OpenAIEmbedConfig, …, HugotEmbedConfig) → pkg/embedding (or split: EmbeddingConfigpkg/embedding, ToolSearchConfigpkg/search).
Register as proxy section (tool_search).
Embedding provider sub-packages already use embedding.Register(); their provider-specific config structs can move alongside each provider.

Task 9 — Token counting config (pkg/tokencounter)

Move TokenCountingConfigpkg/tokencounter.
Register as proxy section (token_counting).


Operator / CRD modularization (cross-cutting)

The current PR's Extensions map[string]apiextensionsv1.JSON is the wrong approach (strict typing required, as noted). For the operator side, each config migration task above should also include:

  • Adding strongly-typed fields to MCPProxySpec / MCPUpstreamSpec for that section (regenerated via make generate), rather than an open additionalProperties map.
  • Updating pkg/operator/configgen/generator.go to emit the new section when present.
  • Removing the Extensions fields added by this PR once all sections are migrated.

This can be tracked as a sub-item in each numbered task above, or as a single follow-up task after all config migrations are done.


Suggested order

Tasks 2 and 3 (inbound/outbound auth) are the most impactful since they affect every upstream. Tasks 1, 4–9 are relatively independent and can go in any order. The operator modularization should be done in lockstep with each migration or as a single final sweep.

@gaarutyunov

Copy link
Copy Markdown
Owner Author

@claude decomposition is good. You must add operator as a subtask to each task with integration tests.

@claude

claude Bot commented Apr 15, 2026

Copy link
Copy Markdown

Claude finished @gaarutyunov's task in 43s —— View job


Updated task decomposition

Each task below covers:

  1. Moving the config type(s) into the owning package and registering via init()
  2. Updating all consumers to use GetProxySection / GetUpstreamSection
  3. Operator sub-task: add strongly-typed CRD fields + update configgen generator
  4. Integration tests: add/update integration tests covering the migrated section

Task 1 — Runtime config (pkg/runtime)

Move RuntimeConfig, JSRuntimeConfig, LuaRuntimeConfig out of pkg/configpkg/runtime.
Register with config.RegisterProxySection("runtime", ...) in pkg/runtime's init().
Consumers switch to config.GetProxySection[runtime.Config](cfg,).

  • Operator: add spec.runtime typed fields to MCPProxySpec; update configgen/generator.go to emit runtime: section from the new struct.
  • Integration tests: verify proxy starts and applies JS/Lua pool sizes when runtime: is present and when it is absent.

Task 2 — Inbound auth config (pkg/auth/inbound)

Move InboundAuthConfig, JWTAuthConfig, IntrospectionConfig, APIKeyAuthConfig, LuaAuthConfig, JSAuthConfigpkg/auth/inbound.
Register inbound section at proxy level (inbound_auth) in pkg/auth/inbound's init().
Strategy sub-packages (jwt/, introspection/, apikey/, lua/, js/) keep their strategy-specific config structs co-located.
Consumers that reference cfg.InboundAuth switch to config.GetProxySection[inbound.Config](cfg,).

  • Operator: add spec.inboundAuth typed struct to MCPProxySpec; update configgen to emit inbound_auth: from the new struct.
  • Integration tests: verify JWT, API-key, and "no auth" paths still pass after the move; add a test for missing inbound_auth section (should default to no auth).

Task 3 — Outbound auth config (pkg/auth/outbound)

Move OutboundAuthConfig, BearerOutboundConfig, APIKeyOutboundConfig, OAuth2CCConfig, OAuth2UserSessionConfig, LuaOutboundConfig, JSOutboundConfigpkg/auth/outbound.
Register as upstream section (outbound_auth) in pkg/auth/outbound's init().
Strategy sub-packages own their strategy-config structs (same pattern as inbound).
Consumers that reference upstream.OutboundAuth switch to config.GetUpstreamSection[outbound.Config](cfg, i,).

  • Operator: add spec.outboundAuth typed struct to MCPUpstreamSpec; update configgen to emit per-upstream outbound_auth: from the new struct.
  • Integration tests: verify bearer, API-key, and OAuth2 client-credentials outbound flows pass; verify upstream with no outbound auth still proxies correctly.

Task 4 — Rate limiting config (pkg/ratelimit)

Move RateLimitConfig, RateLimitStoreConfig, RedisStoreConfigpkg/ratelimit.
StoreFactory signature changes from *config.ProxyConfig*ratelimit.Config.
Register as proxy section (rate_limits, rate_limit_store).
Store sub-packages (memory/, redis/) own their connection config structs.

  • Operator: add spec.rateLimits and spec.rateLimitStore typed structs to MCPProxySpec; update configgen accordingly.
  • Integration tests: verify in-memory and (if present) Redis store backends enforce limits; verify proxy starts cleanly with no rate-limit section.

Task 5 — Cache config (pkg/cache)

Move CacheConfig, CacheStoreConfig, RedisCacheConfigpkg/cache.
Register as proxy sections (caches, cache_store).
Store sub-packages own their connection config structs (mirrors rate limiting pattern).

  • Operator: add spec.caches and spec.cacheStore typed structs to MCPProxySpec; update configgen accordingly.
  • Integration tests: verify cache hit/miss behaviour and proxy start with no cache section.

Task 6 — Session store config (pkg/session)

Move SessionStoreConfig, PostgresSessionConfig, RedisSessionConfigpkg/session.
StoreFactory signature updated to accept *session.Config.
Register as proxy section (session_store).
OAuthTokenStore / OAuthCallbackRegistrar interfaces stay in pkg/config (core contract).

  • Operator: add spec.sessionStore typed struct to MCPProxySpec; update configgen accordingly.
  • Integration tests: verify OAuth callback flow works end-to-end; verify proxy starts with no session store configured.

Task 7 — Circuit breaker config (pkg/upstream/circuitbreaker)

Move CircuitBreakerConfigpkg/upstream/circuitbreaker.
Register as proxy section (circuit_breakers).
pkg/mcp/manager.go switches to config.GetProxySection[circuitbreaker.Policies](cfg,).

  • Operator: add spec.circuitBreakers typed struct to MCPProxySpec; update configgen accordingly.
  • Integration tests: verify circuit breaker trips and resets correctly; verify proxy works with no circuit-breaker section.

Task 8 — Tool search + embedding config (pkg/search + pkg/embedding)

Move ToolSearchConfigpkg/search; move EmbeddingConfig and all provider configs (OpenAIEmbedConfig, …, HugotEmbedConfig) → pkg/embedding.
Register tool_search as a proxy section in pkg/search's init().
Embedding provider sub-packages already use embedding.Register(); their provider-specific config structs move alongside each provider.

  • Operator: add spec.toolSearch typed struct to MCPProxySpec; update configgen accordingly.
  • Integration tests: verify tool-search returns results with and without embeddings configured; verify proxy starts with no tool-search section.

Task 9 — Token counting config (pkg/tokencounter)

Move TokenCountingConfigpkg/tokencounter.
Register as proxy section (token_counting).

  • Operator: add spec.tokenCounting typed struct to MCPProxySpec; update configgen accordingly.
  • Integration tests: verify token-count metadata is attached to MCP responses; verify proxy starts with no token-counting section.

Cross-cutting cleanup (after all tasks)

  • Remove the Extensions map[string]apiextensionsv1.JSON fields added in this PR from MCPProxySpec, MCPUpstreamSpec, CRD YAMLs, and configgen/generator.go once all sections have been migrated to strongly-typed structs.
  • Remove decodeExtensions helper from configgen/generator.go.
  • Run make generate to regenerate deepcopy files after all type changes.

…erator configgen registry

Remove Extensions map[string]apiextensionsv1.JSON from MCPProxySpec and
MCPUpstreamSpec (and their CRD YAML schemas), eliminating the
additionalProperties/x-kubernetes-preserve-unknown-fields approach.

Add pkg/operator/configgen/registry.go with ProxySectionFactory and
UpstreamSectionFactory types following the same init()-based registry
pattern as pkg/config.RegisterProxySection. Packages register their own
configgen factories that receive strongly-typed CRD values and return
YAML sections to merge into the generated config.

Also fix two CodeRabbit issues in pkg/config/registry.go:
- Duplicate section registrations now panic instead of silently
  overwriting, preventing import-order-dependent collisions.
- Proxy section factories are skipped when their key is absent from
  the config file, so absent sections cannot appear present via a
  zero-value factory return.

Co-authored-by: German Arutyunov <gaarutyunov@users.noreply.github.com>

@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: 5

♻️ Duplicate comments (1)
pkg/config/registry.go (1)

23-27: ⚠️ Potential issue | 🟠 Major

Move this registry out of package-global state and stop panicking from a library package.

This makes extension availability process-global and import-order dependent, and a duplicate registration crashes any embedding binary instead of returning a normal error path. Please hide the registry behind an explicit dependency passed into the loader and make registration/reporting non-panicking.

Based on learnings, "Applies to **/*.go : Avoid global mutable state; pass dependencies explicitly". As per coding guidelines, "Do not use panic() in library code; only in main() for unrecoverable startup failures".

Also applies to: 34-55

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/config/registry.go` around lines 23 - 27, The package-global registries
(proxyRegistry, upstreamRegistry) and sectionMu must be removed and replaced
with an explicit registry type (e.g., type Registry struct { mu sync.RWMutex;
proxy map[string]SectionFactory; upstream map[string]SectionFactory }) that is
constructed and passed into the loader instead of relying on import-time
globals; change any registration helpers (the code that currently writes to
proxyRegistry/upstreamRegistry) to be methods on that Registry (e.g.,
Registry.RegisterProxy(name string, f SectionFactory) error,
RegisterUpstream(...)) that return an error on duplicate registration instead of
calling panic, and update loader/consumer code to accept a *Registry parameter
so availability is not process-global or import-order dependent. Ensure all
accesses use the Registry's mu and maps and remove the package-level vars
sectionMu/proxyRegistry/upstreamRegistry.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/config/registry.go`:
- Around line 29-55: Prevent extension name collisions by rejecting reserved
core keys during registration: add a set/list of reserved keys (including
"server", "naming", "upstreams", "name", "type") and in both
RegisterProxySection and RegisterUpstreamSection check if the given key exists
in that reserved set before taking the sectionMu lock (or immediately after) and
panic with a clear message like "config: key %q is reserved by core config —
registration conflict" if it is reserved; otherwise proceed with the existing
duplicate-registration check and assignment to proxyRegistry/upstreamRegistry.
- Around line 152-158: The code currently treats a present but non-object
upstream entry as "absent" by doing `rawMap, _ :=
rawVal.(map[string]interface{})` and `if rawMap == nil { continue }`; instead,
in the function handling upstream parsing (the block that reads `rawUp`,
`rawVal`, `rawMap`), return a typed error when the value exists but is not a
map—e.g. construct and return an error like `fmt.Errorf("upstream %q: expected
object, got %T", key, rawVal)` or use a package-level sentinel error
(ErrMalformedUpstreamSection) so the caller can fail fast; replace the
`continue` with this error return and update callers/signatures as needed to
propagate the error.

In `@pkg/operator/configgen/registry.go`:
- Around line 36-59: The RegisterProxySectionFactory and
RegisterUpstreamSectionFactory functions should reject reserved YAML keys before
inserting into proxyGenRegistry or upstreamGenRegistry; add a small reserved-key
set (e.g., {"server","upstreams","name","transport"} or similar core fields)
and, while holding genMu, check if the provided key is in that set and panic (or
return an explicit error) with a clear message about reserved core keys to
prevent collisions; apply this same pre-check logic to both
RegisterProxySectionFactory and RegisterUpstreamSectionFactory so reserved names
are never registered.
- Around line 72-78: The loop over snapshot currently inserts entries even when
a factory returns nil; update the loop that iterates "for key, f := range
snapshot" (and the analogous loop at lines 92-98) to skip adding to exts when
the factory result v is nil by checking "if v == nil { continue }" before
"exts[key] = v", so that Generate omits sections instead of emitting key: null.
- Around line 30-34: The package currently exposes package-level globals genMu,
proxyGenRegistry and upstreamGenRegistry which mirror runtime global mutable
state and use panic on duplicate registration; instead create a Registry struct
(holding the maps and mutex) and change functions that register or look up
ProxySectionFactory and UpstreamSectionFactory to be methods on that Registry so
callers inject an instance into config generation, and replace any panic() on
duplicate registration with a returned error (or a boolean) from the
RegisterProxy/ RegisterUpstream methods so callers can handle/report duplicates
rather than crashing the process.

---

Duplicate comments:
In `@pkg/config/registry.go`:
- Around line 23-27: The package-global registries (proxyRegistry,
upstreamRegistry) and sectionMu must be removed and replaced with an explicit
registry type (e.g., type Registry struct { mu sync.RWMutex; proxy
map[string]SectionFactory; upstream map[string]SectionFactory }) that is
constructed and passed into the loader instead of relying on import-time
globals; change any registration helpers (the code that currently writes to
proxyRegistry/upstreamRegistry) to be methods on that Registry (e.g.,
Registry.RegisterProxy(name string, f SectionFactory) error,
RegisterUpstream(...)) that return an error on duplicate registration instead of
calling panic, and update loader/consumer code to accept a *Registry parameter
so availability is not process-global or import-order dependent. Ensure all
accesses use the Registry's mu and maps and remove the package-level vars
sectionMu/proxyRegistry/upstreamRegistry.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7c4687ed-1056-42d4-a51f-fcc630834d07

📥 Commits

Reviewing files that changed from the base of the PR and between 9d2a2ea and edc77ea.

📒 Files selected for processing (4)
  • pkg/config/registry.go
  • pkg/crd/v1alpha1/zz_generated.deepcopy.go
  • pkg/operator/configgen/generator.go
  • pkg/operator/configgen/registry.go
✅ Files skipped from review due to trivial changes (1)
  • pkg/crd/v1alpha1/zz_generated.deepcopy.go

Comment thread pkg/config/registry.go
Comment on lines +29 to +55
// RegisterProxySection registers a factory for a top-level proxy config section.
// key is the koanf path in the proxy config (e.g. "tool_search", "session_store").
// The factory is called with a koanf subtree scoped to that key on every Load.
// Typically called from init() in packages that define their own proxy-level config.
// Panics if a factory is already registered under the same key.
func RegisterProxySection(key string, f SectionFactory) {
sectionMu.Lock()
defer sectionMu.Unlock()
if _, exists := proxyRegistry[key]; exists {
panic(fmt.Sprintf("config: proxy section %q is already registered — import conflict", key))
}
proxyRegistry[key] = f
}

// RegisterUpstreamSection registers a factory for a per-upstream config section.
// key is the sub-key within each upstream config object in the YAML.
// The factory is called once per upstream per Load for upstreams that contain the key.
// Typically called from init() in packages that define upstream-level config.
// Panics if a factory is already registered under the same key.
func RegisterUpstreamSection(key string, f SectionFactory) {
sectionMu.Lock()
defer sectionMu.Unlock()
if _, exists := upstreamRegistry[key]; exists {
panic(fmt.Sprintf("config: upstream section %q is already registered — import conflict", key))
}
upstreamRegistry[key] = f
}

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.

⚠️ Potential issue | 🟠 Major

Reject reserved core keys at registration time.

Right now an extension can register a key like server, naming, upstreams, name, or type, and the runtime loader will start treating built-in config as an extension section. Add a reserved-key check here so extension namespaces cannot collide with the core config schema.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/config/registry.go` around lines 29 - 55, Prevent extension name
collisions by rejecting reserved core keys during registration: add a set/list
of reserved keys (including "server", "naming", "upstreams", "name", "type") and
in both RegisterProxySection and RegisterUpstreamSection check if the given key
exists in that reserved set before taking the sectionMu lock (or immediately
after) and panic with a clear message like "config: key %q is reserved by core
config — registration conflict" if it is reserved; otherwise proceed with the
existing duplicate-registration check and assignment to
proxyRegistry/upstreamRegistry.

Comment thread pkg/config/registry.go
Comment on lines +152 to +158
rawVal, ok := rawUp[key]
if !ok {
continue
}
rawMap, _ := rawVal.(map[string]interface{})
if rawMap == nil {
continue

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.

⚠️ Potential issue | 🟠 Major

Don't silently drop malformed upstream sections.

Once the key exists, a non-object value currently gets treated as “section absent”, which hides bad config and disables the extension without any load error. Return a typed error here instead of continue.

Suggested fix
 		for key, f := range upstreamCopy {
 			rawVal, ok := rawUp[key]
 			if !ok {
 				continue
 			}
 			rawMap, _ := rawVal.(map[string]interface{})
 			if rawMap == nil {
-				continue
+				return fmt.Errorf("upstream[%d] section %q: expected object, got %T", i, key, rawVal)
 			}
 			// Build a scoped koanf instance from the raw map so the factory can
 			// use the standard k.Unmarshal("", &target) pattern.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/config/registry.go` around lines 152 - 158, The code currently treats a
present but non-object upstream entry as "absent" by doing `rawMap, _ :=
rawVal.(map[string]interface{})` and `if rawMap == nil { continue }`; instead,
in the function handling upstream parsing (the block that reads `rawUp`,
`rawVal`, `rawMap`), return a typed error when the value exists but is not a
map—e.g. construct and return an error like `fmt.Errorf("upstream %q: expected
object, got %T", key, rawVal)` or use a package-level sentinel error
(ErrMalformedUpstreamSection) so the caller can fail fast; replace the
`continue` with this error return and update callers/signatures as needed to
propagate the error.

Comment on lines +30 to +34
var (
genMu sync.RWMutex
proxyGenRegistry = map[string]ProxySectionFactory{}
upstreamGenRegistry = map[string]UpstreamSectionFactory{}
)

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.

⚠️ Potential issue | 🟠 Major

This repeats the runtime registry's process-global mutable state and panic behavior in another library package.

The operator side now has the same import-order coupling and hard crash path as the runtime side. Please inject a registry instance into config generation instead of relying on package globals, and report duplicate registrations without panic().

As per coding guidelines, "Do not use panic() in library code; only in main() for unrecoverable startup failures". Based on learnings, "Applies to **/*.go : Avoid global mutable state; pass dependencies explicitly".

Also applies to: 40-59

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/operator/configgen/registry.go` around lines 30 - 34, The package
currently exposes package-level globals genMu, proxyGenRegistry and
upstreamGenRegistry which mirror runtime global mutable state and use panic on
duplicate registration; instead create a Registry struct (holding the maps and
mutex) and change functions that register or look up ProxySectionFactory and
UpstreamSectionFactory to be methods on that Registry so callers inject an
instance into config generation, and replace any panic() on duplicate
registration with a returned error (or a boolean) from the RegisterProxy/
RegisterUpstream methods so callers can handle/report duplicates rather than
crashing the process.

Comment on lines +36 to +59
// RegisterProxySectionFactory registers a factory for a top-level proxy config
// section that is injected by the operator when generating proxy config YAML.
// key is the top-level YAML key the returned value will be set at.
// Panics if a factory is already registered under the same key.
func RegisterProxySectionFactory(key string, f ProxySectionFactory) {
genMu.Lock()
defer genMu.Unlock()
if _, exists := proxyGenRegistry[key]; exists {
panic(fmt.Sprintf("configgen: proxy section factory %q is already registered — import conflict", key))
}
proxyGenRegistry[key] = f
}

// RegisterUpstreamSectionFactory registers a factory for a per-upstream config
// section that is injected by the operator when generating upstream config YAML.
// key is the YAML key within the upstream object the returned value will be set at.
// Panics if a factory is already registered under the same key.
func RegisterUpstreamSectionFactory(key string, f UpstreamSectionFactory) {
genMu.Lock()
defer genMu.Unlock()
if _, exists := upstreamGenRegistry[key]; exists {
panic(fmt.Sprintf("configgen: upstream section factory %q is already registered — import conflict", key))
}
upstreamGenRegistry[key] = f

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.

⚠️ Potential issue | 🟠 Major

Block extension keys that collide with generated core YAML fields.

Because these values are later inlined into the generated YAML, registering keys like server, upstreams, name, or transport can collide with the base schema and produce ambiguous output. Reject reserved keys here before they ever enter the registry.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/operator/configgen/registry.go` around lines 36 - 59, The
RegisterProxySectionFactory and RegisterUpstreamSectionFactory functions should
reject reserved YAML keys before inserting into proxyGenRegistry or
upstreamGenRegistry; add a small reserved-key set (e.g.,
{"server","upstreams","name","transport"} or similar core fields) and, while
holding genMu, check if the provided key is in that set and panic (or return an
explicit error) with a clear message about reserved core keys to prevent
collisions; apply this same pre-check logic to both RegisterProxySectionFactory
and RegisterUpstreamSectionFactory so reserved names are never registered.

Comment on lines +72 to +78
for key, f := range snapshot {
v, err := f(ctx, proxy, upstreams)
if err != nil {
return fmt.Errorf("proxy section factory %q: %w", key, err)
}
exts[key] = v
}

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.

⚠️ Potential issue | 🟠 Major

Treat nil factory results as “omit this section”.

Right now a factory that returns nil, nil still adds an entry to exts, so Generate emits inline key: null blocks instead of leaving the section absent. Skip insertion when the factory result is nil.

Suggested fix
 	for key, f := range snapshot {
 		v, err := f(ctx, proxy, upstreams)
 		if err != nil {
 			return fmt.Errorf("proxy section factory %q: %w", key, err)
 		}
+		if v == nil {
+			continue
+		}
 		exts[key] = v
 	}
@@
 	for key, f := range snapshot {
 		v, err := f(ctx, upstream)
 		if err != nil {
 			return fmt.Errorf("upstream section factory %q: %w", key, err)
 		}
+		if v == nil {
+			continue
+		}
 		exts[key] = v
 	}

Also applies to: 92-98

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/operator/configgen/registry.go` around lines 72 - 78, The loop over
snapshot currently inserts entries even when a factory returns nil; update the
loop that iterates "for key, f := range snapshot" (and the analogous loop at
lines 92-98) to skip adding to exts when the factory result v is nil by checking
"if v == nil { continue }" before "exts[key] = v", so that Generate omits
sections instead of emitting key: null.

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.

Dynamic configs

1 participant