Skip to content

[BUG]: PluginResult.executions missing ControlExecutionRecord for denying plugin when violations_as_exceptions=True #147

Description

@prakhar-singh1928

Current Behavior

When violations_as_exceptions=True is set on an invoke_hook() call and a plugin denies the invocation, CPEX raises PluginViolationError immediately without appending a ControlExecutionRecord for the denying plugin to PluginResult.executions (see cpex/framework/manager.py:680-682).

As a result, PluginResult.executions only contains records for plugins that ran before the denial. The denying plugin itself has no record. ContextForge consumers cannot identify which specific control blocked the invocation from telemetry — only that some control in the chain denied it.

Expected Behavior

PluginResult.executions should contain a ControlExecutionRecord for the denying plugin even when violations_as_exceptions=True. The record should be appended before the exception is raised, with:

  • status = completed (or an appropriate terminal status)
  • effective_allow = false
  • All other standard identity and duration fields populated

Steps to Reproduce

  1. Configure a CPEX plugin chain with at least two plugins where the second plugin denies the invocation.
  2. Call invoke_hook() with violations_as_exceptions=True.
  3. Catch the raised PluginViolationError.
  4. Inspect PluginResult.executions — only the first plugin's record is present. The denying plugin has no record.

Logs / Error Output

No stack trace. The gap is silent — PluginResult.executions returns an incomplete list with no indication that a record is missing. ContextForge documents this inline:

Note: when violations_as_exceptions=True, CPEX raises PluginViolationError

before appending a ControlExecutionRecord for the denying plugin to the

executions list (see cpex/framework/manager.py:680-682 — "propagate

immediately, no record needed"). This means _ctl_acc may only contain

records from earlier plugins in the chain that ran before the denial.

Environment

Additional Context

ContextForge works around this via ControlTelemetryAccumulator.mark_denied(hook="pre/post"), which correctly sets cpex.control.result.allowed=false on the emitted summary span. However, downstream consumers cannot determine which control denied the invocation from telemetry alone.

The fix belongs in cpex/framework/manager.py around line 680-682 — the ControlExecutionRecord for the denying plugin should be appended to executions before the exception is raised, not skipped. Related: #130.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    Status
    In progress

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions