feat: structured control execution records - #134
Conversation
Signed-off-by: prakhar-singh1928 <prakhar.singh1928@ibm.com>
Signed-off-by: prakhar-singh1928 <prakhar.singh1928@ibm.com>
vishu-bh
left a comment
There was a problem hiding this comment.
The structured execution record looks well aligned with the observability use case. Could you please check and clarify the following contract semantics before we consume it in ContextForge?
-
Trigger semantics: What should count as a control being “triggered”? The record currently distinguishes invocation,
matched,applied, andeffective_allow=false. Should consumers treat these respectively as executed, condition matched, enforced, and denied? -
Stable identity: Is
plugin_idguaranteed to remain stable across process restarts, configuration reloads, and multiple replicas? Also, which fields should consumers use for the distinct artifact, control, and binding identities? -
Non-standard execution states: Could you check how
skipped,disabled, and fire-and-forget controls should be represented? In particular, should fire-and-forget initially use ascheduledstatus instead ofcompleted, since its final outcome is not yet known when the pipeline returns? -
Matched value:
matchedmay currently beNonewhen CPEX cannot determine whether a condition matched. Should matching become an explicit plugin-result field so consumers can reliably distinguish “not matched” from “unknown”? If not, could the expected interpretation ofNonebe documented?
Clarifying these points will help ensure CF derives trigger and enforcement metrics consistently without making assumptions that could produce misleading dashboard counts.
|
@vishu-bh to answer your questions
For the three identity concepts we will use: Artifact identity → plugin_kind (e.g. "builtin", "wasm://org/guard@1.2") plugin_id will only be used for within-request deduplication within a single invoke_hook call.
Fire-and-forget records use status = Completed at spawn time as an optimistic placeholder. The actual outcome is unknowable when the pipeline returns. duration_ns is 0, requested_allow is None, applied is false. We will identify FAF records by mode == "fire_and_forget" and exclude them from all enforcement metrics. We agree a Scheduled status variant would be cleaner and will file a follow-up CPEX ticket, but will not block CF work on it.
matched is tri-state:
None means unknown, not false. It does not mean the condition did not match. We will only count matched == True in our "condition matched" metric. For "was this control evaluated at all" we will count status in (completed, error, timeout) which is unconditional on match state. |
vishu-bh
left a comment
There was a problem hiding this comment.
Thanks @prakhar-singh1928 for clarifying the doubts . It looks a sound approach
LGTM 🚀
Summary
Adds structured control execution records to
PipelineResult. Every hookinvocation now returns a
Vec<ControlExecutionRecord>— one trusted record perplugin evaluated — carrying identity, execution health, the effective allow/deny
decision, modification flags, monotonic duration, and bounded diagnostics.
Records are populated entirely from framework-owned state; plugins cannot forge
them. Exposed through the FFI, Go binding, and Python PyO3 binding.
Closes: #130
Changes
Core (
crates/cpex-core)execution_recordmodule:ControlExecutionRecord,ControlExecutionStatus,ExecutionSummaryhelper, string/key cardinality bounds (MAX_STRING_LEN = 256,MAX_CONFIG_KEYS = 64), UTF-8-safe truncationexecutor: populate records across all 5 phases (SEQUENTIAL, TRANSFORM, AUDIT,CONCURRENT, FIRE_AND_FORGET) with
requested_allow/effective_allowdistinction,
payload_modified,extensions_modified, monotonicduration_ns,bounded
reasonanderror_codePipelineResult.executions: Vec<ControlExecutionRecord>+with_executions()FFI (
crates/cpex-ffi)FfiPipelineResult.executionspassthrough on bothcpex_invokeandcpex_invoke_resolvedpathsGo (
go/cpex)ControlExecutionStatusconstants andControlExecutionRecordstructPipelineResult.ExecutionsandTypedPipelineResult.ExecutionsfieldsExecutionsthroughInvoke[P]struct literal (was silently nil)Python (
bindings/python)PipelineResult.executionsgetter returnslist[dict]_lib.pyistub updatedBug fixes
the denial — synthesize a default violation, always halt the pipeline
original_modebeforeon_error: disablefiresso records show
"concurrent"not"disabled"Invoke[P]:Executionswas deserialized from the wire but never copiedinto
TypedPipelineResultChecks
make lintpasses —✅ lint passedmake testpasses - ✅## [Unreleased]section added)Notes
15 new Rust integration tests cover every executor phase and outcome path:
allow, deny, deny-without-violation, error+ignore, order preservation, audit
phase, concurrent allow/deny, FAF spawn, duration measurement, and
deny-stops-subsequent.
3 new Go tests cover field threading through
Invoke[P], msgpackround-trip for
ControlExecutionRecord(all pointer fields), andPipelineResult.Executionswire round-trip.Known follow-ups (non-blocking, tracked separately):
Disabled/Skippedstatus variants are declared but not yet emitted(disabled plugins are silently excluded by
group_by_mode)OnError::Disablearm has the same post-disable modesnapshot gap that was fixed in the concurrent phase
status = Completedat spawn time; a dedicatedSpawnedvariant would be cleaner