Summary
Add a framework-owned, structured execution record for every policy/plugin control evaluated by CPEX. Hosts must be able to derive enforcement telemetry—invocation counts, matched/applied counts, allow/deny outcomes, duration, stable reason/error codes, and per-control history—without parsing logs, exceptions, or plugin-specific metadata.
This feature should produce a language/runtime-neutral result contract. CPEX should collect execution facts centrally inside the executor; host applications remain responsible for mapping those facts into their own OpenTelemetry attributes, metrics, exporters, and dashboards.
This complements #43. Issue #43 discusses a general plugin telemetry protocol and export ownership. This issue is narrower: define and return a trustworthy control-execution ledger as part of pipeline execution, independent of any specific telemetry backend.
Problem
CPEX already exposes useful final state:
PipelineResult.continue_processing
- a blocking
PluginViolation
- non-fatal
PluginErrorRecord values
- plugin identity/configuration in
PluginRef
- hook, agent, MCP entity, request, trace, and span context in extensions
However, the host cannot reconstruct every control evaluation from the final result. Successful allow results are normally discarded, audit/transform denials may be suppressed by execution-mode semantics, per-plugin duration is not returned, and skipped/timed-out/cancelled controls do not share one result model.
This prevents reliable enforcement analytics such as:
- number of controls invoked for an operation
- number of controls that matched or took action
- final and per-control allow/deny results
- per-control latency
- top/least invoked controls
- control behavior by hook, entity, route, agent, and time range
- drill-down into structured reasons and stable error codes
Asking every plugin to emit custom metadata is not scalable and creates inconsistent semantics, cardinality, security, and performance behavior. Executor-owned facts must come from trusted framework state, not untrusted plugin claims.
User Story 1
- As a: host integrating CPEX
- I want: a structured record for every control evaluated during a hook invocation
- So that: I can emit consistent enforcement telemetry without parsing logs or implementing plugin-specific adapters
Acceptance Criteria
Scenario: Multiple controls allow an operation
Given multiple controls are selected for a hook
When every selected control completes and allows processing
Then PipelineResult contains one ordered execution record per evaluated control
And each record identifies the hook, control, outcome, status, and duration
And the final pipeline decision remains allowed
Scenario: A control denies an operation
Given a blocking control returns a structured violation
When the executor halts the pipeline
Then the denying control record contains allowed=false
And the record contains the framework-attributed control identity
And the record contains the stable violation code and bounded reason
And controls not evaluated after the halt are not reported as invoked
Scenario: A non-blocking mode reports a deny
Given an audit or transform control returns continue_processing=false
When its execution mode suppresses blocking
Then the record preserves that the control matched or requested denial
And the record shows whether enforcement was applied
And the final pipeline result reflects effective mode semantics
Scenario: A control errors or times out
Given a control errors or exceeds its timeout
When on_error policy is applied
Then the record contains a stable status and error code
And the final pipeline decision follows the configured on_error policy
And free-form exception text is not required to classify the outcome
User Story 2
- As a: CPEX plugin author
- I want: framework-provided execution telemetry
- So that: my plugin does not need direct OpenTelemetry dependencies or backend-specific metadata
Acceptance Criteria
Scenario: Plugin returns a normal result
Given a plugin returns PluginResult::allow, deny, modify, or modify_extensions
When the executor consumes the result
Then the framework creates the execution record automatically
And the plugin does not need to create spans or connect to a collector
Scenario: Plugin metadata is present
Given a plugin returns optional metadata
When the result crosses typed, erased, FFI, Python, or Go boundaries
Then framework execution facts remain separate from plugin-provided metadata
And plugin metadata cannot overwrite trusted identity, status, duration, or decision fields
Proposed Architecture
Add a neutral execution record to cpex-core, populated by Executor around every plugin invocation.
Illustrative model:
pub struct ControlExecutionRecord {
pub plugin_id: String,
pub plugin_name: String,
pub plugin_kind: String,
pub hook_name: String,
pub mode: PluginMode,
pub status: ControlExecutionStatus,
pub requested_allow: Option<bool>,
pub effective_allow: bool,
pub matched: Option<bool>,
pub applied: bool,
pub payload_modified: bool,
pub extensions_modified: bool,
pub duration_ns: u64,
pub reason: Option<String>,
pub error_code: Option<String>,
pub config_keys: Vec<String>,
}
Naming is illustrative and should be finalized during design. Important semantic separation:
requested_allow: what the plugin result requested
effective_allow: result after execution-mode and on_error policy
matched: whether the control condition matched, when known
applied: whether the control changed payload/extensions or effective decision
status: execution health, separate from allow/deny policy decision
Suggested stable statuses:
completed
skipped
error
timeout
cancelled
disabled
Expose records from PipelineResult, for example:
pub executions: Vec<ControlExecutionRecord>
Records should preserve deterministic execution order where phases are serial. Concurrent results need documented stable ordering, preferably resolved plan/priority order rather than completion order.
Alignment with Existing Architecture
Implementation should preserve current trust boundaries:
- Identity, kind, mode, priority, hook, timeout policy, and configuration keys come from manager-owned
PluginRef.trusted_config, never plugin-returned metadata.
- Violation plugin name remains framework-attributed.
- Payload and extension modification acceptance remains governed by execution mode and capability checks.
on_error behavior remains authoritative for effective outcome.
- Fire-and-forget execution remains represented separately when completion is unavailable at initial
PipelineResult return. BackgroundTasks.wait_for_background_tasks() may need a structured completion-record surface.
- Execution records remain data only. CPEX must not require an OpenTelemetry collector or application-specific dashboard.
- Request trace/span context continues through existing extensions; hosts decide how records are attached to spans.
Current Rust result type-erasure should also be reviewed: PluginResult.metadata is not part of ErasedResultFields, while PipelineResult.metadata is initialized as None. This feature should not use arbitrary metadata as the trusted execution ledger, and existing metadata behavior should either be preserved correctly across boundaries or explicitly deprecated/documented.
Aggregates
CPEX may expose convenience methods, but records should remain source of truth:
invocation_count = records with an actual invocation
matched_count = records where matched=true
applied_count = records where applied=true
result_count = number of returned execution records
control_duration = sum of measured control execution durations
Hosts can combine pre- and post-invocation hook results for one operation. CPEX should not flatten dynamic control names into backend-specific attribute keys.
Security and Reliability Requirements
Trusted attribution
- Plugins must not be able to forge
plugin_id, plugin_name, plugin_kind, hook, mode, status, duration, or effective decision.
- Framework-owned fields must be populated after plugin return using trusted registry/configuration data.
- Plugin-supplied violation/error data must not override framework attribution.
Sensitive data handling
- Never include configuration values in execution records; expose validated configuration key names only.
- Do not copy violation
details, payloads, headers, credentials, extension contents, or stack traces into telemetry records by default.
- Bound and sanitize free-form
reason values.
- Prefer stable low-cardinality error codes over exception messages.
- Document which fields may contain tenant/user-provided text.
Cardinality and memory bounds
- Bound records per pipeline invocation.
- Bound all string lengths, configuration key counts, and serialized record size.
- Define truncation/drop behavior and expose a non-sensitive overflow indicator.
- Avoid dynamic attribute-key generation inside CPEX.
- Avoid cloning payloads or large extension data into records.
Timing integrity
- Use monotonic time for durations.
- Define duration unit explicitly; nanoseconds are preferred internally for precision and conversion.
- Prevent integer overflow and use saturating conversion where necessary.
- Duration measurement must include plugin execution but clearly document whether queue/semaphore wait and framework overhead are included.
Failure isolation
- Failure to construct, serialize, or export an execution record must not alter policy outcome.
- Telemetry/recording failures must be observable without recursively generating unbounded records.
- Panic, timeout, cancellation, short-circuit,
on_error=ignore, and on_error=disable paths must preserve existing fail-open/fail-closed semantics.
Concurrency
- Concurrent and short-circuited phases must not report controls as completed when tasks were cancelled or never started.
- No shared mutable result object may be written without synchronization.
- Record ordering and duplicate handling must be deterministic.
- Fire-and-forget records must not outlive or mutate an immutable
PipelineResult through unsafe shared state.
Language and Boundary Requirements
Expose the same semantic fields through:
- Rust
cpex-core
- Python PyO3 binding
- C FFI
- Go binding
- future WASM/plugin-host boundaries where applicable
Serialization must be versioned or backward-compatible. Adding records must not break consumers deserializing older PipelineResult payloads.
Conformance fixtures should prove equivalent outcomes across supported bindings for:
- allow
- deny
- payload modification
- extension modification
- suppressed audit/transform denial
- non-fatal error
- fatal error
- timeout
- disabled plugin
- concurrent deny with cancellation
- fire-and-forget completion/error
Performance Requirements
- Measure one monotonic start/end pair per invoked control.
- Avoid payload serialization and deep copies.
- Avoid per-plugin exporter/network calls.
- Keep record allocation bounded.
- Add benchmarks for long chains and concurrent execution.
- Document overhead target before declaring contract stable.
Out of Scope
- Grafana dashboards or application-specific queries
- vendor-specific names such as
controls.*
- flattening control names into dynamic OpenTelemetry attribute keys
- direct plugin connections to OpenTelemetry collectors
- host-specific agent/tool/binding lookup
- changing policy decisions or execution-mode semantics
Compatibility and Delivery
CPEX main is Rust 0.2+; ContextForge currently consumes Python 0.1.1 from the 0.1.x branch. Proposed delivery:
- Finalize language-neutral record semantics on
main.
- Decide whether a compatible subset should be backported to
0.1.x for current hosts.
- Expose equivalent result fields through Python/Go/FFI.
- Let hosts map records into their own OpenTelemetry semantic attributes.
Definition of Done
- Structured execution-record types and stable semantics documented.
- Executor populates trusted records for every relevant execution path.
PipelineResult exposes records without breaking existing consumers.
- Rust, Python, C FFI, and Go surfaces are aligned.
- Security/cardinality bounds are enforced and tested.
- Concurrency and fire-and-forget behavior is documented and tested.
- Benchmarks demonstrate bounded overhead.
- Host can derive per-control invocation, match, application, decision, error, and duration telemetry without parsing logs or exception strings.
Alternatives Considered
- Plugin-specific metadata: rejected because every plugin would define different fields and could forge framework facts.
- Parse logs/exceptions: rejected because text is unstable, incomplete for successful controls, and may leak sensitive data.
- Direct OpenTelemetry SDK in every plugin: rejected because it couples plugins to infrastructure and duplicates exporter configuration.
- Only emit framework spans: insufficient as a host-neutral result contract and difficult to preserve consistently across FFI/language boundaries.
Additional Context
Summary
Add a framework-owned, structured execution record for every policy/plugin control evaluated by CPEX. Hosts must be able to derive enforcement telemetry—invocation counts, matched/applied counts, allow/deny outcomes, duration, stable reason/error codes, and per-control history—without parsing logs, exceptions, or plugin-specific metadata.
This feature should produce a language/runtime-neutral result contract. CPEX should collect execution facts centrally inside the executor; host applications remain responsible for mapping those facts into their own OpenTelemetry attributes, metrics, exporters, and dashboards.
This complements #43. Issue #43 discusses a general plugin telemetry protocol and export ownership. This issue is narrower: define and return a trustworthy control-execution ledger as part of pipeline execution, independent of any specific telemetry backend.
Problem
CPEX already exposes useful final state:
PipelineResult.continue_processingPluginViolationPluginErrorRecordvaluesPluginRefHowever, the host cannot reconstruct every control evaluation from the final result. Successful allow results are normally discarded, audit/transform denials may be suppressed by execution-mode semantics, per-plugin duration is not returned, and skipped/timed-out/cancelled controls do not share one result model.
This prevents reliable enforcement analytics such as:
Asking every plugin to emit custom metadata is not scalable and creates inconsistent semantics, cardinality, security, and performance behavior. Executor-owned facts must come from trusted framework state, not untrusted plugin claims.
User Story 1
Acceptance Criteria
User Story 2
Acceptance Criteria
Proposed Architecture
Add a neutral execution record to
cpex-core, populated byExecutoraround every plugin invocation.Illustrative model:
Naming is illustrative and should be finalized during design. Important semantic separation:
requested_allow: what the plugin result requestedeffective_allow: result after execution-mode andon_errorpolicymatched: whether the control condition matched, when knownapplied: whether the control changed payload/extensions or effective decisionstatus: execution health, separate from allow/deny policy decisionSuggested stable statuses:
Expose records from
PipelineResult, for example:Records should preserve deterministic execution order where phases are serial. Concurrent results need documented stable ordering, preferably resolved plan/priority order rather than completion order.
Alignment with Existing Architecture
Implementation should preserve current trust boundaries:
PluginRef.trusted_config, never plugin-returned metadata.on_errorbehavior remains authoritative for effective outcome.PipelineResultreturn.BackgroundTasks.wait_for_background_tasks()may need a structured completion-record surface.Current Rust result type-erasure should also be reviewed:
PluginResult.metadatais not part ofErasedResultFields, whilePipelineResult.metadatais initialized asNone. This feature should not use arbitrary metadata as the trusted execution ledger, and existing metadata behavior should either be preserved correctly across boundaries or explicitly deprecated/documented.Aggregates
CPEX may expose convenience methods, but records should remain source of truth:
Hosts can combine pre- and post-invocation hook results for one operation. CPEX should not flatten dynamic control names into backend-specific attribute keys.
Security and Reliability Requirements
Trusted attribution
plugin_id,plugin_name,plugin_kind, hook, mode, status, duration, or effective decision.Sensitive data handling
details, payloads, headers, credentials, extension contents, or stack traces into telemetry records by default.reasonvalues.Cardinality and memory bounds
Timing integrity
Failure isolation
on_error=ignore, andon_error=disablepaths must preserve existing fail-open/fail-closed semantics.Concurrency
PipelineResultthrough unsafe shared state.Language and Boundary Requirements
Expose the same semantic fields through:
cpex-coreSerialization must be versioned or backward-compatible. Adding records must not break consumers deserializing older
PipelineResultpayloads.Conformance fixtures should prove equivalent outcomes across supported bindings for:
Performance Requirements
Out of Scope
controls.*Compatibility and Delivery
CPEX
mainis Rust0.2+; ContextForge currently consumes Python0.1.1from the0.1.xbranch. Proposed delivery:main.0.1.xfor current hosts.Definition of Done
PipelineResultexposes records without breaking existing consumers.Alternatives Considered
Additional Context
PipelineResult,PluginViolation, andPluginErrorRecord.