macp_runtime/policy_engine.rs
1//! Pluggable ingress policy engine (E3, master plan §4.6).
2//!
3//! This is the identity-aware, async authorization surface for external
4//! engines (OPA, Cedar, org-specific services). It is deliberately distinct
5//! from [`macp_core::policy::PolicyEvaluator`]:
6//!
7//! - `PolicyEvaluator` governs **commitment evaluation** and must be a pure,
8//! deterministic function of bound rules + accepted history (RFC-MACP-0012
9//! §6.3) — it replays.
10//! - `PolicyEngine` governs **ingress**: whether an authenticated identity may
11//! start a session, send a message, or observe a session. Rejected traffic
12//! never enters accepted history, so replay only ever sees engine-approved
13//! envelopes — an async, non-deterministic external engine here cannot
14//! diverge replay, by the same reasoning that keeps authentication outside
15//! the replay boundary (RFC-MACP-0003).
16//!
17//! Failure semantics are **deny-on-error**: an engine that cannot answer is a
18//! denial, never an allow.
19
20use crate::security::AuthIdentity;
21use macp_core::policy::PolicyDecision;
22use macp_core::session::Session;
23use macp_pb::pb::Envelope;
24
25/// Decision points an external engine may govern at ingress.
26#[async_trait::async_trait]
27pub trait PolicyEngine: Send + Sync {
28 /// May `identity` start a session in `mode`? Runs after authentication
29 /// and the security layer's own checks, before the kernel accepts the
30 /// SessionStart.
31 async fn evaluate_session_start(
32 &self,
33 identity: &AuthIdentity,
34 mode: &str,
35 env: &Envelope,
36 ) -> PolicyDecision;
37
38 /// May `identity` send this session-scoped envelope? Runs after mode
39 /// binding is known, before kernel acceptance.
40 async fn evaluate_message(
41 &self,
42 identity: &AuthIdentity,
43 session: &Session,
44 env: &Envelope,
45 ) -> PolicyDecision;
46
47 /// May `identity` observe this session (GetSession / StreamSession
48 /// subscribe)? Purely a read gate; never replayed.
49 async fn evaluate_session_access(
50 &self,
51 identity: &AuthIdentity,
52 session: &Session,
53 ) -> PolicyDecision;
54}
55
56/// Convert an engine decision into a transport error, fail closed.
57pub fn require_allow(decision: PolicyDecision, what: &str) -> Result<(), tonic::Status> {
58 match decision {
59 PolicyDecision::Allow { .. } => Ok(()),
60 PolicyDecision::Deny { reasons } => Err(tonic::Status::permission_denied(format!(
61 "policy engine denied {what}: {}",
62 reasons.join("; ")
63 ))),
64 other => Err(tonic::Status::permission_denied(format!(
65 "policy engine returned unrecognized decision for {what} (fail closed): {other:?}"
66 ))),
67 }
68}