Skip to main content

aae/
models.rs

1//! Serde-derived types for AAE protocol schemas.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// Blast radius classification for proposed effects.
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum BlastRadius {
11    /// No mutating side effects.
12    ReadOnly,
13    /// Affects a single service on a single host.
14    SingleService,
15    /// Affects a single host (multiple services possible).
16    SingleHost,
17    /// Affects multiple hosts.
18    MultiHost,
19    /// Effects cannot be cleanly undone.
20    Irreversible,
21}
22
23/// Policy engine decision values.
24///
25/// v0.8: the enum is closed at exactly these three values. Unknown decision
26/// values — including the two refined v0.3 values retired in v0.8 — MUST
27/// fail deserialization; consumers reject rather than coerce.
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum Decision {
31    /// Plan may proceed; capability token MUST be minted.
32    Allow,
33    /// Plan is rejected; lifecycle terminates.
34    Deny,
35    /// Plan requires human approval before proceeding.
36    RequireApproval,
37}
38
39/// v0.3: whether a tool's effects can be undone.
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(rename_all = "snake_case")]
42pub enum Reversibility {
43    /// Effects can be fully undone.
44    Reversible,
45    /// Some effects can be undone.
46    PartiallyReversible,
47    /// Effects cannot be undone.
48    Irreversible,
49}
50
51/// v0.7: the standard `ext.confidence` shape on policy decisions.
52///
53/// Answers "how sure is the engine?" without new decision values: a
54/// low-confidence allow is still an allow (G-2-safe); hosts MAY route it to
55/// spot checks.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct Confidence {
58    /// Confidence in [0, 1].
59    pub score: f64,
60    /// What the score is derived from.
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub basis: Option<String>,
63}
64
65/// v0.3: the standard `ext.cost_estimate` shape in step previews.
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct CostEstimate {
68    /// ISO 4217 code or deployment-defined unit.
69    pub currency: String,
70    /// Best estimate.
71    pub amount: f64,
72    /// Upper bound if known; cost-aware policies should use it conservatively.
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub amount_max: Option<f64>,
75    /// What the estimate is derived from.
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub basis: Option<String>,
78    /// Confidence in [0, 1].
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub confidence: Option<f64>,
81}
82
83/// Core strictness mode: gateway executes exactly the approved steps in
84/// order; deviation aborts. This is the REQUIRED default (SPEC §5.3).
85pub const STRICTNESS_STRICT_LITERAL: &str = "strict_literal";
86
87/// Core strictness mode: gateway executes steps matching templates with
88/// parameter substitution (SPEC §5.3, OPTIONAL).
89pub const STRICTNESS_STRICT_TEMPLATE: &str = "strict_template";
90
91fn default_strictness() -> String {
92    STRICTNESS_STRICT_LITERAL.to_string()
93}
94
95/// A single tool invocation within a proposal.
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct Step {
98    /// Name of a registered tool.
99    pub tool: String,
100    /// Arguments matching the tool's `plan_schema`.
101    pub args: serde_json::Value,
102    /// Optional success criteria.
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub expected: Option<serde_json::Value>,
105    /// Blast radius classification.
106    pub blast_radius: BlastRadius,
107}
108
109/// Context fields on a proposal. Open shape; rationale is required.
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct Context {
112    /// Human-readable explanation of why this plan was proposed.
113    pub rationale: String,
114    /// Optional reference to upstream cause.
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub triggered_by: Option<String>,
117    /// If this is a re-proposal, the previous `proposal_id`. Also the carrier
118    /// of the v0.8 negotiation idiom (SPEC §5.5): a proposal submitted in
119    /// response to a guided `deny` references the original here.
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub derived_from: Option<String>,
122    /// Host-specific extensions (use namespaced keys).
123    #[serde(flatten)]
124    pub extra: HashMap<String, serde_json::Value>,
125}
126
127/// A typed plan submitted by an agent.
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct Proposal {
130    /// Protocol version.
131    pub aae_version: String,
132    /// ULID uniquely identifying this proposal.
133    pub proposal_id: String,
134    /// Stable identifier for the agent submitting this proposal.
135    pub agent_id: String,
136    /// v0.3, optional (G-1): ordered provenance for proposals submitted on
137    /// behalf of other agents. Last entry must equal `agent_id`.
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub agent_chain: Option<Vec<String>>,
140    /// Tenant scope for multitenant hosts.
141    pub tenant_id: String,
142    /// Slug describing the agent's intent.
143    pub intent: String,
144    /// Context fields.
145    pub context: Context,
146    /// Steps to execute.
147    pub steps: Vec<Step>,
148    /// Submission timestamp.
149    pub submitted_at: DateTime<Utc>,
150}
151
152/// A predicted effect of executing a step.
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct Effect {
155    /// Effect type (e.g., `service_state_change`, `file_write`, `read_only`).
156    #[serde(rename = "type")]
157    pub effect_type: String,
158    /// Resource being affected.
159    pub target: String,
160    /// Prior state, if applicable.
161    #[serde(skip_serializing_if = "Option::is_none", rename = "from")]
162    pub from_state: Option<String>,
163    /// Predicted state, if applicable.
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub to: Option<String>,
166    /// Effect-specific details.
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub details: Option<serde_json::Value>,
169}
170
171/// Preview output for a single step.
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct StepPreview {
174    /// Index of this step within the proposal's steps array.
175    pub step_index: usize,
176    /// Predicted effects.
177    pub predicted_effects: Vec<Effect>,
178    /// Estimated duration in milliseconds.
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub estimated_duration_ms: Option<u64>,
181    /// Optional human-readable diff.
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub diff: Option<String>,
184    /// Warnings produced during preview.
185    #[serde(default)]
186    pub warnings: Vec<String>,
187    /// If this step's preview was unsupported, the reason.
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub preview_unsupported_reason: Option<String>,
190    /// v0.3: non-normative extension fields (`ext.*` namespace).
191    /// Standardized shapes: `cost_estimate` ([`CostEstimate`]).
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub ext: Option<serde_json::Value>,
194}
195
196/// Predicted effects of executing a proposal.
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct Preview {
199    /// Protocol version.
200    pub aae_version: String,
201    /// ULID of this preview.
202    pub preview_id: String,
203    /// ULID of the proposal this previews.
204    pub proposal_id: String,
205    /// Per-step preview outputs.
206    pub step_previews: Vec<StepPreview>,
207    /// Aggregate (worst-case) blast radius.
208    pub aggregate_blast_radius: BlastRadius,
209    /// True if any step's preview was unsupported.
210    pub preview_unsupported: bool,
211    /// Generation timestamp.
212    pub generated_at: DateTime<Utc>,
213}
214
215/// Output of policy engine evaluation.
216#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct PolicyDecision {
218    /// Protocol version.
219    pub aae_version: String,
220    /// ULID of this decision.
221    pub decision_id: String,
222    /// ULID of the proposal this evaluates.
223    pub proposal_id: String,
224    /// The decision value.
225    pub decision: Decision,
226    /// Identifier of the policy bundle/version used.
227    pub policy_version: String,
228    /// Identifiers of rules consulted during evaluation.
229    pub rules_evaluated: Vec<String>,
230    /// Human-readable explanation.
231    pub reason: String,
232    /// Required approvers, if decision is `RequireApproval`.
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub required_approvers: Option<Vec<String>>,
235    /// Expiration time, if decision is Allow.
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub expires_at: Option<DateTime<Utc>>,
238    /// Plan-to-execution binding strictness. v0.8: an open string. Core
239    /// normatively defines [`STRICTNESS_STRICT_LITERAL`] (default) and
240    /// [`STRICTNESS_STRICT_TEMPLATE`]; any other value is a
241    /// companion/extension mode, and a host that does not implement the
242    /// requested mode MUST fail closed (C-16), never downgrade.
243    #[serde(default = "default_strictness")]
244    pub strictness: String,
245    /// Decision timestamp.
246    pub decided_at: DateTime<Utc>,
247    /// v0.7: non-normative extension fields (`ext.*`). Standardized shape:
248    /// `confidence` ([`Confidence`]).
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub ext: Option<serde_json::Value>,
251}
252
253/// Capability token scope.
254#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct CapabilityScope {
256    /// Tool name authorized by this token.
257    pub tool: String,
258    /// SHA-256 hash of approved steps (for plan integrity verification).
259    pub approved_steps_hash: String,
260    /// Tool-specific constraints (e.g., for `ssh_exec`: host, user).
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub target_constraints: Option<serde_json::Value>,
263}
264
265/// Capability token claims (signed externally as JWT/PASETO/biscuit).
266#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct CapabilityToken {
268    /// Protocol version.
269    pub aae_version: String,
270    /// ULID of this token.
271    pub token_id: String,
272    /// ULID of the proposal this authorizes.
273    pub proposal_id: String,
274    /// ULID of the policy decision this derives from.
275    pub decision_id: String,
276    /// Token scope.
277    pub scope: CapabilityScope,
278    /// Issued-at (Unix seconds).
279    pub iat: i64,
280    /// Expiration (Unix seconds).
281    pub exp: i64,
282    /// Issuer identifier.
283    pub iss: String,
284    /// Subject (`agent_id`).
285    #[serde(skip_serializing_if = "Option::is_none")]
286    pub sub: Option<String>,
287    /// Maximum number of times this token may be presented.
288    pub max_uses: u32,
289}
290
291/// Optional digital signature on an audit event.
292#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct EventSignature {
294    /// Signature algorithm.
295    pub alg: String,
296    /// Signing key identifier.
297    pub key_id: String,
298    /// Base64-encoded signature value.
299    pub value: String,
300}
301
302/// Event type binding an out-of-chain artifact to a chain event by content
303/// hash (v0.8). Required payload fields: `artifact_hash` (algorithm-prefixed
304/// canonical hash), `bound_event_id`, `artifact_kind` (free-form string,
305/// e.g. `legible_record`, `session_recording`). Artifacts are advisory; the
306/// chain event is authoritative.
307pub const EVENT_TYPE_ARTIFACT_ATTESTED: &str = "artifact_attested";
308
309/// A single hash-chained audit log record.
310#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct AuditEvent {
312    /// Protocol version.
313    pub aae_version: String,
314    /// ULID of this event.
315    pub event_id: String,
316    /// Canonical event type (or namespaced extension). Open string; core
317    /// v0.8 adds [`EVENT_TYPE_ARTIFACT_ATTESTED`]. Consumers MUST ignore
318    /// unknown event types.
319    pub event_type: String,
320    /// ULID of the proposal this relates to.
321    pub proposal_id: String,
322    /// Tenant scope.
323    pub tenant_id: String,
324    /// Agent identifier.
325    pub agent_id: String,
326    /// Who emitted this event (agent, host, `policy_engine`, gateway, `audit_sink`, human:<id>).
327    pub actor: String,
328    /// Event timestamp.
329    pub ts: DateTime<Utc>,
330    /// Event-specific payload.
331    pub payload: serde_json::Value,
332    /// Hash of the previous event in the chain (None for genesis).
333    pub prev_event_hash: Option<String>,
334    /// SHA-256 of `canonicalize(event_without_this_hash)` || `prev_event_hash`.
335    pub this_event_hash: String,
336    /// Optional digital signature.
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub signature: Option<EventSignature>,
339}
340
341/// Tool registration declaring the tool's plan/preview contract.
342#[derive(Debug, Clone, Serialize, Deserialize)]
343pub struct ToolRegistration {
344    /// Protocol version.
345    pub aae_version: String,
346    /// Tool name (matched against Step.tool).
347    pub name: String,
348    /// Optional human-readable description.
349    #[serde(skip_serializing_if = "Option::is_none")]
350    pub description: Option<String>,
351    /// JSON Schema for the args object of a step using this tool.
352    pub plan_schema: serde_json::Value,
353    /// Optional JSON Schema for `predicted_effects` produced by this tool.
354    #[serde(skip_serializing_if = "Option::is_none")]
355    pub preview_schema: Option<serde_json::Value>,
356    /// Whether this tool supports preview.
357    pub preview_supported: bool,
358    /// Default blast radius if step does not declare one.
359    pub default_blast_radius: BlastRadius,
360    /// Whether this tool requires the AAE lifecycle.
361    pub aae_required: bool,
362    /// v0.3 optional (G-4): opaque data-class identifiers this tool may touch.
363    #[serde(default, skip_serializing_if = "Vec::is_empty")]
364    pub data_classes_touched: Vec<String>,
365    /// v0.3 optional (G-4): whether operations are compliance-relevant.
366    #[serde(default)]
367    pub compliance_relevant: bool,
368    /// v0.3 optional (G-4): systems of record this tool can modify.
369    #[serde(default, skip_serializing_if = "Vec::is_empty")]
370    pub systems_of_record: Vec<String>,
371    /// v0.3 optional (G-4): whether this tool's effects can be undone.
372    #[serde(skip_serializing_if = "Option::is_none")]
373    pub reversibility: Option<Reversibility>,
374}