diff --git a/backend/core/config_test.go b/backend/core/config_test.go index 79b317e8..826b3609 100644 --- a/backend/core/config_test.go +++ b/backend/core/config_test.go @@ -72,3 +72,21 @@ func TestConfigGetRejectsNilView(t *testing.T) { _, err := core.ConfigGet[float64](nil, "otel.sampling_rate") require.ErrorIs(t, err, extpoints.ErrConfigNotResolved) } + +func TestContextConfigIsSharedAcrossForks(t *testing.T) { + ctx := core.NewContext(nil) + child := ctx.Fork() + + require.NotNil(t, ctx.Config()) + assert.Same(t, ctx.Config(), child.Config(), "configuration declarations are process-wide facts") + + require.NoError(t, child.Config().Declare("cache", + extpoints.ConfigBinding{Prefix: "otel", Target: &otelConfig{}})) + + declared := false + for _, entry := range ctx.Config().Entries() { + declared = declared || entry.Key == "otel.sampling_rate" + } + assert.True(t, declared, "a declaration made in a plugin scope must be visible to the root") + assert.False(t, ctx.Config().Resolved()) +} diff --git a/backend/core/context.go b/backend/core/context.go index 617e860c..671fddde 100644 --- a/backend/core/context.go +++ b/backend/core/context.go @@ -27,6 +27,7 @@ type Context struct { tasks extpoints.TaskExtension schedules extpoints.ScheduleExtension settings extpoints.SettingExtension + config extpoints.ConfigExtension mu sync.RWMutex children []*Context @@ -56,6 +57,7 @@ func NewContext(base context.Context) *Context { tasks: extpoints.NewTaskRegistry(), schedules: extpoints.NewScheduleRegistry(), settings: extpoints.NewSettingRegistry(), + config: extpoints.NewConfigRegistry(nil), values: make(map[any]any), } } @@ -167,6 +169,7 @@ func (c *Context) ForkWithContext(base context.Context) *Context { tasks: c.tasks, schedules: c.schedules, settings: c.settings, + config: c.config, values: make(map[any]any), } @@ -235,6 +238,13 @@ func (c *Context) Setting() extpoints.SettingExtension { return c.Settings() } +// Config returns the process-level configuration extension point. The registry is +// shared by every fork because configuration declarations are global facts, and it +// carries no per-scope disposers: values are resolved once before Apply runs. +func (c *Context) Config() extpoints.ConfigExtension { + return c.config +} + // OnDispose registers a cleanup callback function to be executed when this Context is disposed. // It accepts func() error, func(), or Disposer. func (c *Context) OnDispose(fn any) { diff --git a/backend/core/types.go b/backend/core/types.go index f8baae9d..5fb521fc 100644 --- a/backend/core/types.go +++ b/backend/core/types.go @@ -154,3 +154,32 @@ type SettingExtension = extpoints.SettingExtension // SettingSchema re-exports extpoints.SettingSchema. type SettingSchema = extpoints.SettingSchema + +// ConfigExtension re-exports extpoints.ConfigExtension. +type ConfigExtension = extpoints.ConfigExtension + +// ConfigSource re-exports extpoints.ConfigSource. +type ConfigSource = extpoints.ConfigSource + +// ConfigBinding re-exports extpoints.ConfigBinding. +type ConfigBinding = extpoints.ConfigBinding + +// ConfigView re-exports extpoints.ConfigView. +type ConfigView = extpoints.ConfigView + +// ConfigEntry re-exports extpoints.ConfigEntry. +type ConfigEntry = extpoints.ConfigEntry + +// ConfigGatedPlugin is an optional interface for plugins whose activation depends on +// configuration. The kernel evaluates the gate during reconciliation, before the +// plugin's Apply runs, so every key read by ConfigEnabled must be published through +// DeclareConfig. +type ConfigGatedPlugin interface { + Plugin + + // DeclareConfig publishes the configuration bindings consumed by ConfigEnabled. + DeclareConfig() []extpoints.ConfigBinding + + // ConfigEnabled reports whether this plugin should activate for the resolved values. + ConfigEnabled(view extpoints.ConfigView) bool +}