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}