Conversation
Allow external plugins to override built-in step, module, trigger, handler, deploy target, and sidecar provider types. When a duplicate type is encountered via LoadPluginWithOverride, a warning is logged instead of returning an error. The original LoadPlugin behavior (rejecting duplicates) is preserved. Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds an opt-in “override” plugin loading path so external plugins can intentionally replace built-in module/step/trigger/handler (and related) registrations without failing on duplicate type names—supporting scenarios like overriding the built-in step.authz_check.
Changes:
- Refactors
PluginLoader.LoadPlugininternals intoloadPlugin(p, allowOverride)and addsLoadPluginWithOverridethat replaces duplicates while logging warnings. - Adds
StdEngine.LoadPluginWithOverride, delegating to the loader’s override behavior. - Adds unit tests validating override behavior for modules, steps, and a multi-type case.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
plugin/loader.go |
Introduces LoadPluginWithOverride and shared loadPlugin(..., allowOverride) to replace factories on duplicates with warnings. |
engine.go |
Adds StdEngine.LoadPluginWithOverride and internal helper to select override vs strict loader path. |
plugin/loader_test.go |
Adds tests for override behavior (module override, step.authz_check override, and a combined-types override scenario). |
engine.go
Outdated
| // override existing module, step, trigger, and handler type registrations. | ||
| // When a duplicate type is encountered, the new factory replaces the previous | ||
| // one and a warning is logged, instead of returning an error. | ||
| // This is intended for external plugins that intentionally replace built-in | ||
| // defaults (e.g., replacing a mock step with a production implementation). |
There was a problem hiding this comment.
Engine.LoadPluginWithOverride doc says it only overrides module/step/trigger/handler registrations, but it delegates to PluginLoader.LoadPluginWithOverride which also overrides deploy targets and sidecar providers. Please either update this doc to mention deploy targets/sidecar providers too, or constrain the override behavior to match the doc so callers aren’t surprised.
| // override existing module, step, trigger, and handler type registrations. | |
| // When a duplicate type is encountered, the new factory replaces the previous | |
| // one and a warning is logged, instead of returning an error. | |
| // This is intended for external plugins that intentionally replace built-in | |
| // defaults (e.g., replacing a mock step with a production implementation). | |
| // override existing module, step, trigger, handler, deploy target, and sidecar | |
| // provider registrations. When a duplicate type is encountered, the new factory | |
| // replaces the previous one and a warning is logged, instead of returning an | |
| // error. This is intended for external plugins that intentionally replace | |
| // built-in defaults (e.g., replacing a mock step with a production | |
| // implementation, or swapping out deploy targets/sidecar providers). |
| func (l *PluginLoader) LoadPluginWithOverride(p EnginePlugin) error { | ||
| return l.loadPlugin(p, true) | ||
| } | ||
|
|
There was a problem hiding this comment.
PluginLoader has LoadBinaryPlugin() which currently always calls LoadPlugin() (duplicate types rejected). With the new override capability, consider adding a LoadBinaryPluginWithOverride() (or an override flag) so binary-loaded external plugins can intentionally replace built-ins as well.
| // LoadBinaryPluginWithOverride is the override-capable counterpart to loading | |
| // binary or externally provided plugins. It behaves like LoadPluginWithOverride, | |
| // allowing a binary-loaded plugin to intentionally replace built-in module, | |
| // step, trigger, handler, deploy target, and sidecar provider registrations. | |
| func (l *PluginLoader) LoadBinaryPluginWithOverride(p EnginePlugin) error { | |
| return l.loadPlugin(p, true) | |
| } |
| if err := loader.LoadPlugin(p1); err != nil { | ||
| t.Fatalf("first load should succeed: %v", err) | ||
| } | ||
| if err := loader.LoadPluginWithOverride(p2); err != nil { | ||
| t.Fatalf("LoadPluginWithOverride should succeed: %v", err) | ||
| } |
There was a problem hiding this comment.
This test validates override behavior, but it doesn’t assert that the non-override path still rejects duplicate step types (only the module override test covers the non-override behavior for modules). Adding a LoadPlugin(p2) failure assertion here would better protect against regressions in the step duplicate check.
| func TestPluginLoader_LoadPluginWithOverride_AllTypes(t *testing.T) { | ||
| loader := newTestEngineLoader() | ||
|
|
||
| p1 := &modulePlugin{ | ||
| BaseEnginePlugin: *makeEnginePlugin("builtin", "1.0.0", nil), | ||
| modules: map[string]ModuleFactory{ | ||
| "mod.type": func(name string, cfg map[string]any) modular.Module { return nil }, | ||
| }, | ||
| steps: map[string]StepFactory{ | ||
| "step.type": func(name string, cfg map[string]any, _ modular.Application) (any, error) { return nil, nil }, | ||
| }, | ||
| triggers: map[string]TriggerFactory{ | ||
| "trigger.type": func() any { return nil }, | ||
| }, | ||
| handlers: map[string]WorkflowHandlerFactory{ | ||
| "handler.type": func() any { return nil }, | ||
| }, | ||
| } |
There was a problem hiding this comment.
Test name/intent says "AllTypes", but it only covers modules/steps/triggers/handlers. The new override path also includes deploy targets and sidecar providers; either extend this test to include those types (and/or their duplicate rejection in LoadPlugin) or rename/split the test so coverage matches what’s being claimed.
|
@copilot apply changes based on the comments in this thread |
…ded tests - Add LoadBinaryPluginWithOverride to PluginLoader for override-capable binary plugin loading - Fix engine.go doc to mention deploy targets and sidecar providers - Add LoadPlugin duplicate rejection assertion in step override test - Extend AllTypes test to cover deploy targets and sidecar providers; add fullPlugin, mockDeployTarget, mockSidecarProvider test helpers Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
Applied all changes from the review thread in commit
|
LoadPlugin()rejects duplicate step/module/trigger/handler type names, preventing external plugins from overriding built-in defaults (e.g., replacing the mockstep.authz_checkwith a production Casbin-backed implementation).Changes
plugin/loader.go: ExtractLoadPluginlogic intoloadPlugin(p, allowOverride). AddLoadPluginWithOverride(p)— on duplicate, logs a warning viaslog.Warnand replaces the factory instead of returning an error. Covers all factory types: modules, steps, triggers, handlers, deploy targets, sidecar providers. Also addsLoadBinaryPluginWithOverride(p, binaryPath, sigPath, certPath)as the override-capable counterpart toLoadBinaryPlugin, so binary-loaded external plugins can also intentionally replace built-ins.engine.go: Mirror withStdEngine.LoadPluginWithOverride(p)delegating to the loader's override path. Doc updated to accurately reflect that deploy targets and sidecar providers are also overridable.plugin/loader_test.go: Tests for module override, step override (thestep.authz_checkscenario, including assertion thatLoadPluginstill rejects the duplicate), and an all-types override scenario covering modules, steps, triggers, handlers, deploy targets, and sidecar providers (withLoadPluginduplicate rejection verified for each).Usage
Original
LoadPluginandLoadBinaryPluginbehavior is unchanged — callers must opt in to override semantics.Original prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.