feat(test): write down the safety invariants and test them as invariants - #71
feat(test): write down the safety invariants and test them as invariants#71mkoushni wants to merge 10 commits into
Conversation
The fail-closed promise lived in comments and per-seam judgment. Codify it as a catalog and contain panics in every awaited phase the way concurrent already did. Fixes praxis-proxy#24 Signed-off-by: mkoushni <mkoushni@redhat.com>
A one-plugin catalog could not fail if a contained serial panic still skipped later audit, and several cells only asserted "not allow". Name the decision per cell and cover ignore/disable outside concurrent. Signed-off-by: mkoushni <mkoushni@redhat.com>
praxis-bot
left a comment
There was a problem hiding this comment.
Review: feat(test): write down the safety invariants and test them as invariants
Strong work. The containment model is sound: serial, transform, and audit panics now route through on_error the same way concurrent already did. The catalog approach -- exhaustive matches that fail the build when a phase or dialect is added -- is exactly the right mechanism for these invariants. The fault-injection infrastructure is well-factored and the test matrix is thorough.
Two findings, both medium.
| Severity | File | Finding |
|---|---|---|
| Medium | executor.rs |
ContainedOutcome::Lost non-blocking paths skip OnError::Disable |
| Medium | plugin.rs |
is_dispatch_phase takes self by value; all other PluginMode methods take &self |
A non-blocking Lost outcome recorded the error but skipped the Disable circuit breaker that Error, Timeout, and Panic already trip. Match the other PluginMode predicates on the receiver too. Signed-off-by: mkoushni <mkoushni@redhat.com>
praxis-bot
left a comment
There was a problem hiding this comment.
PR Review
Both previous findings (Lost/Disable gap and is_dispatch_phase receiver) are addressed. One new finding.
| Severity | File | Finding |
|---|---|---|
| Medium | evaluator.rs |
evaluate_pdp_contained leaks spawned tasks on timeout |
Keep the safety-invariants changelog under Unreleased; take 0.2.0 wording for assertions. Fold the unreadable-result tests into FaultHandler. Signed-off-by: mkoushni <mkoushni@redhat.com>
Wrapping timeout around the JoinHandle detached the spawned task, so a resolver that never returned kept running. Put the budget inside the spawn, matching invoke_contained, so dropping the future cancels it. Signed-off-by: mkoushni <mkoushni@redhat.com>
Signed-off-by: mkoushni <mkoushni@redhat.com>
Signed-off-by: mkoushni <mkoushni@redhat.com>
Keep the safety-invariants changelog under Unreleased alongside the PPE documentation entry from praxis-proxy#82. Signed-off-by: mkoushni <mkoushni@redhat.com>
araujof
left a comment
There was a problem hiding this comment.
Thanks for this PR!
A few findings and suggestions to address before merging:
-
make publish-dryfails becauseppe-coreuses a versioned workspace self-dependency. Use the same path-only self-dependency already used byppe-apl-core. -
A transform panic can let the original, unredacted payload continue through the pipeline. Either stop processing when
on_errorisFail, or document and test this behavior explicitly. -
The spawned plugin and PDP tasks outlive a dropped request. Give both join handles abort-on-drop ownership so timeout, disconnect, and shutdown cancel the work.
-
A contained serial or transform panic removes the plugin's existing
local_state. Preserve and restore the previous state when the task returns no context. -
InjectedFailure::all()omitsWrongType, so the matrix never checks it. Add it and correct the audit expectation, since audit results are intentionally discarded.
A dropped request left plugin and PDP work running, and a serial panic cleared the plugin's prior local_state. Path-only self-dep so publish-dry packages; the catalog now drives WrongType. Signed-off-by: mkoushni <mkoushni@redhat.com>
|
make publish-dry — ppe-core now uses a path-only self-dev-dependency, same as ppe-apl-core. cargo package -p praxis-policy-core succeeds. Transform panic + original payload — Transform still cannot halt (on_error: fail is not an enforcement point). That is documented: the original unredacted payload continues, payload_modified stays false, and the failure is recorded. Pinned by a_transform_fail_panic_keeps_the_original_payload. Abort-on-drop — Contained plugin tasks use AbortOnDropHandle; PDP tasks abort if the request future is dropped, so timeout/disconnect/shutdown cancel the work. local_state — Serial/transform snapshot context into the task instead of taking it. A contained panic leaves the previous map in the table. Pinned by a_contained_serial_panic_keeps_prior_local_state. WrongType — InjectedFailure::all() includes it. Audit discards results, so Audit × WrongType is Allow, not a recorded error. |
terylt
left a comment
There was a problem hiding this comment.
Nice work! My review uncovered three small items.
1. Say what the containment costs
crates/ppe-core/src/executor.rs, run_serial_phase and run_ref_phase
Every entry now does payload.clone_boxed() plus a task spawn. Audit used to take
&payload and spawn nothing, so a hook with N sequential + M transform + K audit
plugins went from zero clones to N+M+K per request.
The spawn is load-bearing, since it carries the timeout and the abort-on-drop
cancellation as well as the panic containment, so this is a trade rather than a
regression. But the trade is currently unstated. Add a line to the PR body and to
the module comment saying containment costs a payload clone per plugin per hook,
and that audit is no longer free.
If the clone turns out to matter for CMF payloads, catch_unwind over
AssertUnwindSafe contains panics without the clone or the spawn, at the cost of
losing the timeout and cancellation on those phases. Worth measuring before
changing anything.
2. Fix or drop the coverage test
crates/ppe-core/tests/safety_invariants.rs, plugin_fault_catalog_covers_every_dispatch_mode
dispatch_modes() is all_plugin_modes().filter(PluginMode::is_dispatch_phase),
so asserting modes.contains(&mode) == mode.is_dispatch_phase() restates the
filter and cannot fail. The exhaustive match in all_plugin_modes() is what
actually guards I12, and it does so at compile time.
Either delete the test and point the I12 doc line at the exhaustive match, or
replace it with something the filter does not already guarantee, for example that
every mode in dispatch_modes() has a non-Allow cell for at least one injected
failure.
3. Document the Tokio requirement
crates/ppe-apl-core
tokio moved from a dev-dependency to a dependency, which is the right call and
is already explained in Cargo.toml. The gap is the failure shape: tokio::spawn
panics outside a runtime, so a host driving the evaluator on a non-Tokio executor
unwinds out of evaluate_effects on every Effect::Pdp instead of failing
closed. One line in the crate-level docs saying a Tokio runtime is required.
Audit is no longer a free &payload path. The I12 catalog test restated the dispatch-mode filter; replace it with a non-Allow cell check. Signed-off-by: mkoushni <mkoushni@redhat.com>
|
Summary
docs/safety-invariants.md(linked from CONTRIBUTING) and drive them with a reusable fault-injection plugin and PDP resolver.on_errorinstead of unwindingexecute(). Halt vs continue still followscan_block; that difference is documented with a reason.clone_boxedintotokio::spawn). Audit used to take&payloadand spawn nothing; it is no longer free. The spawn also carries the timeout and abort-on-drop cancellation, so this is a trade rather than a regression. If the clone matters for CMF payloads,catch_unwindoverAssertUnwindSafewould contain panics without the clone or the spawn, at the cost of losing timeout and cancellation on those phases — measure before changing.{panic, error, timeout}underon_error: fail, plus ignore/disable for serial, transform, and audit. A sequential panic under ignore still runs later audit; under fail it does not.on_errorin YAML isFail. Missingsubject.idpins the differential allowlist row (Cedar dispatch error, CEL eval error, OPA default deny). Malformed policy pins Cedar dispatch / CEL compile / OPA compile-or-dispatch.{panic, error, timeout}cells drive the evaluator wrapper (FaultPdp), not the Cedar/CEL/OPA resolvers themselves. Real dialects are covered for missing attribute and malformed policy. A new phase or shipped dialect fails the build until a cell is added.ppe-apl-corerequires a Tokio runtime:tokio::spawnpanics outside one, so a host driving the evaluator on a non-Tokio executor unwinds out ofevaluate_effectson everyEffect::Pdpinstead of failing closed.Closes #24
Test plan
cargo test -p praxis-policy-core --test safety_invariantscargo test -p praxis-policy-apl-core --test safety_invariantscargo test -p praxis-policy-pdp-diff safety