pub enum Effect {
Allow,
Deny {
reason: Option<String>,
code: Option<String>,
},
Plugin {
name: String,
},
Delegate(DelegateStep),
Elicit(ElicitStep),
Taint {
label: String,
scopes: Vec<TaintScope>,
},
FieldOp {
path: String,
stages: Vec<Stage>,
},
Sequential(Vec<Effect>),
Parallel(Vec<Effect>),
When {
condition: Expression,
body: Vec<Effect>,
source: String,
},
Pdp {
call: PdpCall,
on_allow: Vec<Effect>,
on_deny: Vec<Effect>,
},
}Expand description
One thing a matching rule does. Mirrors DSL spec §3 effect classes:
- Control —
Allow,Deny - Label —
Taint - Host —
Plugin,Delegate
Content effects (redact, mask, omit, hash) and orchestration
(Sequential, Parallel) land in later slices (E2 / E3).
PDP calls (cedar:(…), opa(…), …) remain top-level Step
variants for now; folding them into Effect is an E4 cleanup.
§Inside a Vec<Effect> (a rule’s effects body)
Allowis a no-op — lets evaluation continue to the next effect in the list, then to the next step inpolicy:.Denyshort-circuits the rest of the list, the rest of thepolicy:block, and the route. Thereasonpropagates into the violation message.Plugin/Delegatedispatch identically to their top-levelStepcounterparts (same invoker traits).Taintaccumulates into the phase’s taint events.
Variants§
Allow
Deny
Fields
code: Option<String>Author-supplied stable violation code. When Some, it
overrides the rule’s auto-generated source-position code
(routes.tool:X.apl.policy[N]) downstream. Useful when
MCP clients want to dispatch on category (quota.exceeded,
delegation.depth_exceeded) rather than position, or when
multiple routes share a deny category that should
aggregate consistently in audit dashboards. When None,
the evaluator falls back to rule.source as the code —
matches the historical behavior.
Parser shape: deny('reason', 'code') (two positional
arguments) or the structured deny: { reason: ..., code: ... }
map form.
Plugin
Delegate(DelegateStep)
Elicit(ElicitStep)
Elicitation effect — dispatch a question to a human (approval,
confirmation, step-up, …) through a channel plugin, hold pending
state across the agent’s retries, validate the response, resume.
The elicitation analogue of Delegate. See
docs/apl-manager-approval-ciba-design.md and
crate::step::ElicitStep.
Taint
FieldOp
Content effect (DSL §3) — apply a pipe chain (redact, mask,
omit, hash, validators, transforms) to a field in the
route’s args or result. The author writes
result.salary | redact inside a do: body; the parser
splits the dotted path from the pipeline.
path must start with args. or result. — the evaluator
dispatches the lookup against RoutePayload.args or
RoutePayload.result. A FieldOp inside a Pre-phase route’s
do: that targets result.X is a no-op (the result hasn’t
been produced yet); same goes for a Post-phase rule that
targets args.X (the args are already on the wire). The
evaluator silently skips out-of-phase ops so the same
when:/do: shape can describe both phases without
branching.
Sequential(Vec<Effect>)
Run a list of effects in declaration order, stopping on the
first Deny. Semantically equivalent to inlining the list into
the enclosing scope; the variant exists to make grouping
explicit and to pair with Parallel.
Parallel(Vec<Effect>)
Run a list of effects concurrently. Any Deny → overall Deny.
Taints from all branches accumulate. Bag and payload mutations
inside parallel branches are discarded when the branch
completes — each branch gets a clone of the state, never the
shared mutable original. Plugins inside Parallel can still
emit taints (those merge); any other mutation they try to make
(bag writes, args/result rewrites) vanishes.
Config-load rejects FieldOp and Delegate directly inside
Parallel (recursively), since both would silently drop their
effect. The escape valve is Sequential.
When
Predicate-gated body. body runs in order when condition
evaluates to true; any Deny in the body halts the surrounding
phase. Replaces the historical Step::Rule(Rule) shape —
when: / do: directly desugars to this. A bare require(X)
or deny(X) shorthand compiles to When { condition: X, body: vec![Effect::Allow / Deny] }.
source is the human-readable origin (e.g. "routes.X.policy[2]")
surfaced in Decision::Deny.rule_source when the body denies
without supplying its own code.
Pdp
External PDP call. on_allow / on_deny are reaction effect
lists fired against the PDP’s decision (DSL §7.5). Replaces
Step::Pdp { ... } — args-shape stays identical.
Implementations§
Source§impl Effect
impl Effect
Sourcepub fn contains_mutation(&self) -> bool
pub fn contains_mutation(&self) -> bool
Walk this effect (and any nested effects) checking whether any
node would mutate route state. Used by the config-load
validator to reject FieldOp / Delegate inside Parallel
since both would silently drop their effect in a discarded
branch.
Sourcepub fn validate_parallel_purity(&self) -> Result<(), String>
pub fn validate_parallel_purity(&self) -> Result<(), String>
Walk the effect tree rejecting any FieldOp / Delegate that
lives directly or transitively under a Parallel node. Returns
the path string of the first violation found (or Ok(()) if
the tree is clean). Run at config-load.
Trait Implementations§
Source§impl<'de> Deserialize<'de> for Effect
impl<'de> Deserialize<'de> for Effect
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
Source§impl From<Rule> for Effect
Rule is structurally identical to Effect::When. The From impl lets
callers that already hold a Rule (notably the parser’s inner helpers
and the test fixtures) drop a .into() instead of re-spelling all
three fields. Bridges the few remaining producers while the migration
completes; will probably stay long-term because the parser still
builds Rule incrementally before deciding it’s an Effect::When.
impl From<Rule> for Effect
Rule is structurally identical to Effect::When. The From impl lets
callers that already hold a Rule (notably the parser’s inner helpers
and the test fixtures) drop a .into() instead of re-spelling all
three fields. Bridges the few remaining producers while the migration
completes; will probably stay long-term because the parser still
builds Rule incrementally before deciding it’s an Effect::When.