pub trait PolicyEngine:
Send
+ Sync
+ Debug {
// Required methods
fn authorize(&self, request: &PolicyRequest<'_>) -> PolicyDecision;
fn bundle(&self) -> PolicyBundleIdentity;
// Provided methods
fn preflight(&self, requests: &[PolicyRequest<'_>]) -> Vec<String> { ... }
fn digest(&self) -> Digest { ... }
}Expand description
Decides whether an action is allowed.
Implementations must be total — every request gets an answer — and pure: no I/O, no clock, no randomness. Two calls with the same request against the same policy set must return the same decision, or a run stops being replayable for reasons nobody can see.
§This is not where a content classifier hangs
A sink request carries the outbound value’s fields and its label, so
scanning here looks like one line of work. Two clauses above forbid it: a
classifier is I/O, and only denials are journaled — so a value a
classifier passed would leave no trace of having been examined.
Put the inspection behind an effect and let it produce a
Label: journaled, replayed rather than re-run, and
the structural gates then decide on it. A heuristic may describe a value;
only a rule may refuse one. Why, and what a false positive
costs.
§What you will be asked
An engine is consulted at more than one place, and a first one written for
effects alone refuses every run at admission — a failure that does not look
like a policy problem. The runtime tier asks exactly ACTIONS:
action | resource | asked |
|---|---|---|
ACTION_ADMIT | the run’s capability | once, before a run starts |
ACTION_PERFORM | the effect kind | before every live effect |
ACTION_RELEASE | "information_flow.label" | on each typed release |
A served surface adds its own vocabulary — see api::a2a::action::ALL — so
the set is closed per wired surface, not globally. Match on what you mean
and permit the rest, or scope with PolicyRequest::is_effect; a _ => Deny arm refuses admission and every release along with the effects it was
written for.
ACTION_DECLARED, ACTION_EGRESS and ACTION_CONTENT are never asked. They label
refusals the manifest and the flow gates already decided, so an engine that
matched on them would be writing rules for a question nobody puts to it.
use agentplane::core::{PolicyDecision, PolicyEngine, PolicyRequest};
#[derive(Debug)]
struct AmountCeiling;
impl PolicyEngine for AmountCeiling {
fn authorize(&self, request: &PolicyRequest<'_>) -> PolicyDecision {
// Scoped first. Without this the rule below reads `amount` out of
// the admission context and refuses the run before it starts.
if !request.is_effect() {
return PolicyDecision::Permit;
}
match request.context.get("amount_eur").and_then(serde_json::Value::as_u64) {
Some(amount) if amount > 5_000 => PolicyDecision::Deny {
reason: format!("{amount} EUR is over the 5000 ceiling"),
},
_ => PolicyDecision::Permit,
}
}
fn bundle(&self) -> agentplane::core::PolicyBundleIdentity {
agentplane::core::PolicyBundleIdentity::new(
agentplane::core::Digest::of(b"amount-ceiling-v1"),
"amount-ceiling/v1",
)
}
}Required Methods§
Sourcefn bundle(&self) -> PolicyBundleIdentity
fn bundle(&self) -> PolicyBundleIdentity
Identifies every static input that can affect evaluation.
Journaled at admission, so the rules, schema, static entities, configuration/extensions, and evaluator semantics that governed a run remain answerable after any of them change.
Provided Methods§
Sourcefn preflight(&self, requests: &[PolicyRequest<'_>]) -> Vec<String>
fn preflight(&self, requests: &[PolicyRequest<'_>]) -> Vec<String>
Can this engine evaluate the requests this plane is about to make?
Asked once, at build, with a canonical request of each shape the plane will issue. Each returned string is a problem an operator must fix before the plane runs; an empty vector means nothing to report.
The default reports nothing, and that is honest rather than lax: the
failure this exists to catch belongs to total evaluators. Cedar
answers every request, so a rule reading an attribute a request does
not carry does not fail to match — it errors, and an unevaluable rule
may be the forbid that would have stopped the call, so the gate
refuses. One unguarded rule therefore denies every effect of every run
from a policy set that compiled cleanly. An engine written as Rust code
has no such trap and has nothing to say here — about this trap. It has
its own, and the trait’s own docs name it: an engine that does not scope
on the action refuses admission for a rule written about effects.
Implementations must keep the contract the trait already demands —
total, pure, no I/O — because this runs during build, where a
blocking call would make assembling a plane depend on a network.
What a caller may not conclude from an empty answer: that the rules are correct. A set permitting everything reports nothing here.
Dyn Compatibility§
This trait is dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".