feat: split plugin execution mode from error handling; add observe mode#1
Merged
feat: split plugin execution mode from error handling; add observe mode#1
Conversation
…n mode refactor: drop unused config plugin_settings feat: add execution_pool for enforce tasks tests: fix grpc tests Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
2378b87 to
d259f62
Compare
Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
terylt
reviewed
Mar 4, 2026
terylt
reviewed
Mar 4, 2026
terylt
reviewed
Mar 4, 2026
terylt
reviewed
Mar 4, 2026
terylt
reviewed
Mar 4, 2026
terylt
reviewed
Mar 4, 2026
…e PERMISSIVE as AUDIT mode Signed-off-by: Frederico Araujo <frederico.araujo@ibm.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR refactors
PluginModeto cleanly separate two concerns that were previously conflated in the now-removedENFORCE_IGNORE_ERRORmode:PluginMode— defines the plugin's scheduling authority: how and when it runs relative to the pipelineOnError— defines error handling behaviour independently, per pluginThe refactor also clarifies scheduling semantics by replacing the ambiguous
ENFORCEname with explicit mode names:ENFORCEis renamed toSEQUENTIAL(parallel, fail-fast — the scheduling behaviour it always had)CONCURRENTmode is added (parallel, fail-fast — the scheduling behaviour it always had)PERMISSIVEis renamed toAUDIT(sequential, chained, cannot halt)FIRE_AND_FORGETmode is added (fire-and-forget background)The
PluginSettingsPydantic model is also removed frommodels.py; its live runtime fields were migrated tosettings.pyalongside the existing env-var–backed settings.Closes #2
Changes
New
OnErrorenum (models.py)Replaces the blunt
enforce_ignore_errorescape hatch with a first-class, per-plugin error policy:failignoredisableUpdated
PluginModeenum (models.py)PluginModenow usesStrEnumand contains five values. Execution order: SEQUENTIAL → AUDIT → CONCURRENT → FIRE_AND_FORGET.sequentialpermissiveconcurrentfire_and_forgetdisabledENFORCE_IGNORE_ERRORis removed andENFORCEis replaced bySEQUENTIALfor backwards-compat YAML configs (see Migration Guide). Existing YAML configs usingmode: enforceormode: enforce_ignore_errorare automatically migrated via amodel_validator— no YAML changes required for existing deployments.The default mode is
SEQUENTIAL.New
PluginConfig.on_errorfield (models.py)A
model_validator(_migrate_legacy_modes) converts legacy configs at parse time:Removed
PluginSettingsmodel; fields migrated tosettings.pyThe
PluginSettingsPydantic model (previously embedded inConfigand populated from YAML) is removed. Its unused fields (parallel_execution_within_band,plugin_timeout,enable_plugin_api,plugin_health_check_interval,include_user_info) are dropped. The two fields still in active use are migrated to the env-var–backedsettings.py:fail_on_plugin_errorboolFalsePLUGINS_FAIL_ON_PLUGIN_ERRORexecution_poolint | NonePLUGINS_EXECUTION_POOLBoth fields are added to
PluginsSettings(full settings) andPluginsStartupSettings(lightweight early-read class used at executor init), with corresponding lazy properties onLazySettingsWrapper. The executor reads them viasettings.execution_poolandsettings.fail_on_plugin_error.Config.plugin_settingsis removed andConfiggainsmodel_config = ConfigDict(extra="ignore")so that YAML files that still carry aplugin_settings:section parse without error.execution_pool— FIRE_AND_FORGET and CONCURRENT concurrency capsFIRE_AND_FORGET- and CONCURRENT-mode tasks are dispatched concurrently. The
execution_poolsetting bounds concurrent tasks through two independentasyncio.Semaphoreobjects (one per mode):# Cap tasks to 5 workers per FIRE_AND_FORGET/CONCURRENT mode (max of 10 concurrent workers combined) PLUGINS_EXECUTION_POOL=5Refactored
PluginExecutor(manager.py)The previously monolithic execution loop is replaced with a four-phase scheduling model, executed in strict order:
SEQUENTIAL phase — plugins run one-at-a-time in priority order. Each plugin receives
current_payload(the output of the previous plugin). Any plugin returningcontinue_processing=Falsehalts the pipeline immediately; FIRE_AND_FORGET tasks are fired before returning.AUDIT phase — plugins run sequentially, each receiving the output of the previous plugin (chained transformation semantics). A
continue_processing=Falseresult is swallowed — the violation is logged but the pipeline continues.CONCURRENT phase — all CONCURRENT plugins receive a snapshot of
current_payloadat phase start and execute in parallel viaasyncio.as_completed, bounded bySemaphore(execution_pool). Any plugin returningcontinue_processing=Falsecancels remaining tasks, fires FIRE_AND_FORGET tasks, and returns early.FIRE_AND_FORGET phase — each plugin gets an isolated payload snapshot and is dispatched as a fire-and-forget
asyncio.Task, bounded bySemaphore(execution_pool). The pipeline does not await these tasks. Errors are swallowed (with logging);on_error: disableadds the plugin to a_runtime_disabledset. FIRE_AND_FORGET always fires last — using the final payload state after all foreground phases complete.New and updated private methods:
_group_by_mode()— now returns a 4-tuple(sequential_refs, permissive_refs, concurrent_refs, fire_and_forget_refs), buckets and priority-sorts refs, respectingDISABLEDand_runtime_disabled_fire_fire_and_forget_tasks()— extracted helper that schedules FIRE_AND_FORGET plugins; called from both early-exit paths (SEQUENTIAL/CONCURRENT halt) and the normal completion path, ensuring FIRE_AND_FORGET always fires exactly once_run_fire_and_forget_task()— wraps individual background execution with error handling and optional auto-disable_apply_payload_modification()— extracted helper for payload-merge policy logicError handling in
execute_plugin()now dispatches onhook_ref.plugin_ref.on_errorrather than mode. The halt check inexecute_plugin()now covers bothCONCURRENTandSEQUENTIALmodes (both can halt; AUDIT cannot).Global state isolation
Only
CONCURRENTandSEQUENTIALplugins merge their localglobal_contextmutations back to the shared context.AUDITandFIRE_AND_FORGETplugins operate on isolated copy-on-write snapshots. CONCURRENT and SEQUENTIAL plugins are authoritative checks whose side-effects are intentional; AUDIT plugins are transformers that chain payload changes only.Exports (
__init__.py)OnErroris exported fromcpex.frameworkalongside the existingPluginMode.Breaking Changes
PluginMode.ENFORCE_IGNORE_ERRORis removed from the enum. Python code that references it directly will raiseAttributeError. YAML configs are migrated automatically.PluginMode.ENFORCEis removed. Python code that referencesPluginMode.ENFORCEmust be updated toPluginMode.CONCURRENT(parallel, fail-fast) orPluginMode.SEQUENTIAL(sequential, can halt) depending on intent.PluginMode.PERMISSIVEis renamedPluginMode.AUDIT. Python code that referencesPluginMode.PERMISSIVEmust be updated toPluginMode.AUDIT.PluginSettingsis removed frommodels.py. Code that imports or instantiates it must be updated.Config.plugin_settingsis removed. Any code that reads fields from it (e.g.config.plugin_settings.plugin_timeout) must be updated to read fromsettings.*instead.mode: sequentialormode: permissive.Migration Guide
Plugin mode — Python code
YAML plugin config
Runtime settings (previously in YAML
plugin_settings:)