axonflow_sdk_rust/types/pep.rs
1//! Decision Mode PEP (Policy Enforcement Point) wire types (ADR-056, epic #2563).
2//!
3//! These mirror the platform Decision API DTOs (`platform/agent/decision_handler.go`)
4//! and the MCP request-redaction DTOs (`platform/agent/mcp_handler.go`). They are
5//! deliberately re-declared (not derived from a shared crate) so they stay light
6//! enough to vendor into a customer gateway; cross-SDK parity is enforced by the
7//! shared wire field names being byte-identical with the Go / Python / TypeScript
8//! / Java SDKs.
9//!
10//! Field names are snake_case on the wire (serde derives match the struct field
11//! names verbatim), which already matches the platform JSON contract.
12
13use serde::{Deserialize, Serialize};
14
15/// Names the engine call a PEP makes to discharge an obligation.
16///
17/// Fulfillment is a property of the contract, not of PEP-author discipline: a
18/// conforming PEP POSTs the obligation's source content to `endpoint` and
19/// forwards the engine-redacted content the endpoint returns.
20///
21/// `content_types` advertises the mime-types the endpoint's detectors can
22/// handle today. The contract is content-type-agnostic: a PEP holding content
23/// of a type NOT in this list must fail closed rather than forward it
24/// unredacted. Mirrors platform `ObligationFulfillment`.
25#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
26pub struct ObligationFulfillment {
27 /// Engine path, e.g. `"/api/v1/mcp/check-input"`.
28 pub endpoint: String,
29 /// HTTP method, e.g. `"POST"`.
30 #[serde(default)]
31 pub method: String,
32 /// `"request"` | `"response"`.
33 pub phase: String,
34 /// Mime-types the endpoint can redact today. Absent/empty means the PEP
35 /// must not assume a particular detector is registered.
36 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub content_types: Option<Vec<String>>,
38}
39
40/// A self-describing, engine-fulfillable PEP requirement on an allow verdict.
41///
42/// Obligations are SELF-DESCRIBING and ENGINE-FULFILLABLE (ADR-056, #2563):
43/// `/decide` is a pure PDP and never mutates content, so a `redact_pii`
44/// obligation is not "go redact this yourself with your own patterns" — it is
45/// "call the AxonFlow engine endpoint named in `fulfillment` to obtain
46/// engine-redacted content." There is no other blessed way to satisfy it;
47/// client-side redaction is forbidden. Mirrors platform `DecisionObligation`.
48#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
49pub struct Obligation {
50 /// Obligation type, e.g. `"redact_pii"`.
51 pub r#type: String,
52 /// Human-readable detail for audit logs.
53 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub detail: Option<String>,
55 /// How a PEP discharges this obligation via the engine.
56 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub fulfillment: Option<ObligationFulfillment>,
58}
59
60/// Gateway-asserted identity for a `/decide` request.
61///
62/// `org_id` / `tenant_id` are optional in the body — the auth-derived identity
63/// is authoritative; body-supplied values are accepted only when they match.
64/// Mirrors platform `DecisionCallerIdentity`.
65#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
66pub struct DecisionCallerIdentity {
67 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub gateway_id: Option<String>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub org_id: Option<String>,
71 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub tenant_id: Option<String>,
73}
74
75/// Describes what the gateway is about to call. Mirrors platform `DecisionTarget`.
76#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
77pub struct DecisionTarget {
78 /// `"llm"` | `"tool"` | `"agent"`.
79 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub r#type: Option<String>,
81 /// When `type=llm`.
82 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub model: Option<String>,
84 /// When `type=llm`.
85 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub provider: Option<String>,
87 /// When `type=tool`.
88 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub tool: Option<String>,
90}
91
92/// Inbound contract for `POST /api/v1/decide`. Mirrors platform `DecideRequest`.
93///
94/// Required: `stage` (one of `"llm"` | `"tool"` | `"agent"`) and `query`.
95/// `user_token` is optional — a PEP that supplies one gets the validated-user
96/// record on the audit row; one that doesn't gets a synthesized service user.
97#[derive(Debug, Clone, Serialize, Deserialize, Default)]
98pub struct DecideRequest {
99 pub stage: String,
100 pub query: String,
101 #[serde(default)]
102 pub caller_identity: DecisionCallerIdentity,
103 #[serde(default)]
104 pub target: DecisionTarget,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
106 pub user_token: Option<String>,
107 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub context: Option<serde_json::Value>,
109}
110
111impl DecideRequest {
112 /// Construct a request with the two required fields. `stage` is one of
113 /// `"llm"` | `"tool"` | `"agent"`.
114 pub fn new(stage: impl Into<String>, query: impl Into<String>) -> Self {
115 Self {
116 stage: stage.into(),
117 query: query.into(),
118 ..Default::default()
119 }
120 }
121}
122
123/// PDP verdict returned by `POST /api/v1/decide`. Mirrors platform `DecideResponse`.
124///
125/// `obligations` is always a list so PEP code can iterate without a None-check.
126/// `trace_id` is W3C-format (32 lowercase hex chars). `error` is set on the
127/// deny path when the request was malformed.
128#[derive(Debug, Clone, Serialize, Deserialize, Default)]
129pub struct DecideResponse {
130 pub verdict: String,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub decision_id: Option<String>,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub trace_id: Option<String>,
135 #[serde(default, skip_serializing_if = "Option::is_none")]
136 pub reasons: Option<Vec<String>>,
137 #[serde(default)]
138 pub obligations: Vec<Obligation>,
139 #[serde(default)]
140 pub evaluated_policies: Vec<String>,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
142 pub stage: Option<String>,
143 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub expires_at: Option<String>,
145 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub error: Option<String>,
147}
148
149/// Request to the MCP request-redaction endpoint (`POST /api/v1/mcp/check-input`).
150///
151/// Mirrors platform `MCPCheckInputRequest`. `content_type` selects the
152/// request-redaction detector (ADR-056 / #2563 addendum). When omitted the
153/// platform defaults to `text/plain`; a `content_type` with no registered
154/// detector is rejected (415) so a PEP fails closed rather than forwarding
155/// content the engine cannot govern.
156#[derive(Debug, Clone, Serialize, Deserialize, Default)]
157pub struct MCPCheckInputRequest {
158 pub connector_type: String,
159 pub statement: String,
160 #[serde(default, skip_serializing_if = "Option::is_none")]
161 pub operation: Option<String>,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub tenant_id: Option<String>,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub content_type: Option<String>,
166}
167
168/// Result of MCP request-redaction policy evaluation.
169///
170/// Mirrors platform `MCPCheckInputResponse`. The `redacted` / `redacted_statement`
171/// / `redaction_evaluated` fields (ADR-056 / #2563) are what make a `/decide`
172/// `redact_pii` obligation engine-fulfillable: when an allowed statement carries
173/// PII under a redact (not block) policy the engine returns the masked statement
174/// here so a PEP can forward redacted content WITHOUT hand-rolling its own
175/// patterns.
176#[derive(Debug, Clone, Serialize, Deserialize, Default)]
177pub struct MCPCheckInputResponse {
178 pub allowed: bool,
179 #[serde(default, skip_serializing_if = "Option::is_none")]
180 pub block_reason: Option<String>,
181 #[serde(default)]
182 pub policies_evaluated: u64,
183 #[serde(default, skip_serializing_if = "Option::is_none")]
184 pub decision_id: Option<String>,
185 /// Whether the engine actually masked something in `redacted_statement`.
186 #[serde(default)]
187 pub redacted: bool,
188 /// The engine-masked statement (present only when `redacted` is true).
189 #[serde(default, skip_serializing_if = "Option::is_none")]
190 pub redacted_statement: Option<String>,
191 /// Whether the redaction detector actually RAN (regardless of whether it
192 /// masked anything). A PEP fulfilling a `redact_pii` obligation MUST fail
193 /// closed when this is false — it means the redactor did not run (detection
194 /// disabled), so `redacted:false` would otherwise be indistinguishable from
195 /// "looked, found nothing" (#2563 B1). Defaults false so a PEP stays
196 /// fail-closed against a platform that predates the field.
197 #[serde(default)]
198 pub redaction_evaluated: bool,
199}
200
201/// Request to the MCP response-redaction endpoint (`POST /api/v1/mcp/check-output`).
202///
203/// Mirrors platform `MCPCheckOutputRequest`. Carried for completeness of the
204/// response-phase contract; the SDK PEP helper fulfills request-phase
205/// obligations only today.
206#[derive(Debug, Clone, Serialize, Deserialize, Default)]
207pub struct MCPCheckOutputRequest {
208 pub connector_type: String,
209 #[serde(default, skip_serializing_if = "Option::is_none")]
210 pub message: Option<String>,
211 #[serde(default, skip_serializing_if = "Option::is_none")]
212 pub tenant_id: Option<String>,
213}
214
215/// Result of MCP response-redaction policy evaluation.
216///
217/// Mirrors platform `MCPCheckOutputResponse`. `redaction_evaluated` mirrors the
218/// check-input field for the response phase (ADR-056 / #2563): a PEP fulfilling
219/// a response-phase `redact_pii` obligation MUST fail closed when this is false.
220#[derive(Debug, Clone, Serialize, Deserialize, Default)]
221pub struct MCPCheckOutputResponse {
222 pub allowed: bool,
223 #[serde(default, skip_serializing_if = "Option::is_none")]
224 pub block_reason: Option<String>,
225 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub redacted_message: Option<String>,
227 #[serde(default)]
228 pub policies_evaluated: u64,
229 #[serde(default, skip_serializing_if = "Option::is_none")]
230 pub decision_id: Option<String>,
231 /// Whether the response-phase redaction detector actually RAN. Defaults
232 /// false so a PEP stays fail-closed against a platform that predates it.
233 #[serde(default)]
234 pub redaction_evaluated: bool,
235}