Skip to main content

PolicyEngine

Trait PolicyEngine 

Source
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:

actionresourceasked
ACTION_ADMITthe run’s capabilityonce, before a run starts
ACTION_PERFORMthe effect kindbefore 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§

Source

fn authorize(&self, request: &PolicyRequest<'_>) -> PolicyDecision

Source

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§

Source

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.

Source

fn digest(&self) -> Digest

Compact digest of bundle.

Dyn Compatibility§

This trait is dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§