1use std::collections::BTreeMap;
11
12use serde::{Deserialize, Serialize};
13
14use crate::decimal::Decimal;
15use crate::definition::{ParamSource, Predicate, ProvenanceClass, TypeDecl};
16use crate::grade::Grade;
17use crate::ids::ContentDigest;
18use crate::time::{DurationMs, Timestamp};
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
22#[serde(deny_unknown_fields)]
23pub struct Actionability {
24 pub caller_controlled: Vec<ProvenanceClass>,
27 pub remediations: Vec<RemediationDecl>,
28 pub questions: Vec<QuestionDecl>,
29 pub constraint_groups: Vec<ConstraintGroup>,
30 pub checks: Vec<CheckDecl>,
31}
32
33#[derive(
36 Debug,
37 Clone,
38 Copy,
39 PartialEq,
40 Eq,
41 PartialOrd,
42 Ord,
43 Hash,
44 Serialize,
45 Deserialize,
46 schemars::JsonSchema,
47)]
48#[serde(rename_all = "snake_case")]
49pub enum Party {
50 None,
51 Caller,
52 Host,
53 Upstream,
54}
55
56impl Party {
57 pub fn combine<I: IntoIterator<Item = Party>>(parties: I) -> Party {
60 parties.into_iter().max().unwrap_or(Party::None)
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
66#[serde(deny_unknown_fields)]
67pub struct RemediationDecl {
68 pub code: String,
70 #[serde(rename = "for")]
72 pub for_party: Party,
73 pub params: Vec<RemediationParam>,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
77#[serde(deny_unknown_fields)]
78pub struct RemediationParam {
79 pub name: String,
80 #[serde(rename = "type")]
82 pub ty: TypeDecl,
83}
84
85#[derive(
87 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
88)]
89#[serde(deny_unknown_fields)]
90pub struct QuestionDecl {
91 pub id: String,
92 pub fields: Vec<String>,
94 pub text: String,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
100#[serde(deny_unknown_fields)]
101pub struct ConstraintGroup {
102 pub name: String,
103 pub checks: Vec<String>,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
109#[serde(deny_unknown_fields)]
110pub struct CheckDecl {
111 pub id: String,
112 pub subject: String,
114 pub test: Predicate,
116 pub on_fail: OnFail,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
120#[serde(deny_unknown_fields)]
121pub struct OnFail {
122 pub by_provenance: BTreeMap<ProvenanceClass, Consequence>,
123 pub default: DefaultConsequence,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
128#[serde(rename_all = "snake_case")]
129pub enum DefaultConsequence {
130 None,
131 #[serde(untagged)]
132 Consequence(Consequence),
133}
134
135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
138#[serde(rename_all = "snake_case", deny_unknown_fields)]
139pub enum Consequence {
140 Refuse {
141 remediation: RemediationUse,
142 #[serde(default, skip_serializing_if = "Address::is_computed")]
143 address: Address,
144 },
145 DropField {
146 notice: String,
147 #[serde(default, skip_serializing_if = "Address::is_computed")]
148 address: Address,
149 },
150 Relax {
151 group: String,
152 notice: String,
153 #[serde(default, skip_serializing_if = "Address::is_computed")]
154 address: Address,
155 },
156 DowngradeGrade {
157 to: Grade,
159 notice: String,
160 #[serde(default, skip_serializing_if = "Address::is_computed")]
161 address: Address,
162 },
163 Abstain,
164 Escalate {
165 reason: String,
166 },
167 ContinueWithNotice {
168 notice: String,
169 #[serde(default, skip_serializing_if = "Address::is_computed")]
170 address: Address,
171 },
172}
173
174impl Consequence {
175 pub fn is_terminal(&self) -> bool {
177 matches!(
178 self,
179 Consequence::Refuse { .. } | Consequence::Abstain | Consequence::Escalate { .. }
180 )
181 }
182
183 pub fn kind(&self) -> &'static str {
185 match self {
186 Consequence::Refuse { .. } => "refuse",
187 Consequence::DropField { .. } => "drop_field",
188 Consequence::Relax { .. } => "relax",
189 Consequence::DowngradeGrade { .. } => "downgrade_grade",
190 Consequence::Abstain => "abstain",
191 Consequence::Escalate { .. } => "escalate",
192 Consequence::ContinueWithNotice { .. } => "continue_with_notice",
193 }
194 }
195}
196
197#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
200#[serde(rename_all = "snake_case")]
201pub enum Address {
202 #[default]
203 Computed,
204 #[serde(untagged)]
205 Override(AddressOverride),
206}
207
208impl Address {
209 pub fn is_computed(&self) -> bool {
210 matches!(self, Address::Computed)
211 }
212}
213
214#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
215#[serde(deny_unknown_fields)]
216pub struct AddressOverride {
217 pub to: Party,
219 pub reason: String,
221}
222
223#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
226#[serde(untagged)]
227pub enum RemediationUse {
228 Code(String),
229 WithParams(RemediationCall),
230}
231
232#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
233#[serde(deny_unknown_fields)]
234pub struct RemediationCall {
235 pub code: String,
236 pub params: BTreeMap<String, ParamSource>,
237}
238
239impl RemediationUse {
240 pub fn code(&self) -> &str {
241 match self {
242 RemediationUse::Code(c) => c,
243 RemediationUse::WithParams(c) => &c.code,
244 }
245 }
246}
247
248pub const BUILTIN_REMEDIATIONS: &[&str] = &[
250 "provide_fields",
251 "refresh_fields",
252 "correct_fields",
253 "resolve_conflict",
254 "improve_evidence",
255 "retry",
256 "report_backend",
257 "raise_budget",
258 "provide_capability",
259 "retry_after",
260];
261
262#[derive(
265 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
266)]
267#[serde(rename_all = "snake_case", deny_unknown_fields)]
268pub enum RemediationValue {
269 Bool(bool),
270 Integer(i64),
271 Decimal(Decimal),
272 Text(String),
273 Enum(String),
274 Timestamp(Timestamp),
275 Duration(DurationMs),
276 List(Vec<RemediationValue>),
277}
278
279#[derive(
282 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
283)]
284#[serde(rename_all = "snake_case")]
285pub enum Remediation {
286 None,
287 #[serde(untagged)]
288 Some {
289 code: String,
290 params: BTreeMap<String, RemediationValue>,
291 },
292}
293
294impl Remediation {
295 pub fn code(code: &str) -> Remediation {
296 Remediation::Some {
297 code: code.into(),
298 params: BTreeMap::new(),
299 }
300 }
301
302 pub fn with(
303 code: &str,
304 params: impl IntoIterator<Item = (String, RemediationValue)>,
305 ) -> Remediation {
306 Remediation::Some {
307 code: code.into(),
308 params: params.into_iter().collect(),
309 }
310 }
311
312 pub fn texts<I: IntoIterator<Item = String>>(items: I) -> RemediationValue {
314 RemediationValue::List(items.into_iter().map(RemediationValue::Text).collect())
315 }
316}
317
318#[derive(
320 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, schemars::JsonSchema,
321)]
322#[serde(rename_all = "snake_case", deny_unknown_fields)]
323pub enum CauseProvenance {
324 Observed { class: ProvenanceClass },
326 Accepted { classes: Vec<ProvenanceClass> },
328}
329
330#[derive(
332 Debug,
333 Clone,
334 Copy,
335 Default,
336 PartialEq,
337 Eq,
338 PartialOrd,
339 Ord,
340 Hash,
341 Serialize,
342 Deserialize,
343 schemars::JsonSchema,
344)]
345#[serde(rename_all = "snake_case")]
346pub enum Origin {
347 #[default]
348 Upstream,
349 Host,
350}
351
352#[derive(
354 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, schemars::JsonSchema,
355)]
356#[serde(rename_all = "snake_case", deny_unknown_fields)]
357pub enum Cause {
358 Input {
359 field: String,
360 provenance: CauseProvenance,
361 },
362 Backend {
363 step: String,
364 origin: Origin,
365 },
366 Budget {
367 resource: String,
368 },
369 Deadline,
370 Capability {
371 name: String,
372 },
373 Policy {
375 site: String,
376 },
377 #[serde(untagged)]
378 CapabilityStep {
379 capability: CapabilityStep,
380 },
381}
382
383#[derive(
385 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, schemars::JsonSchema,
386)]
387#[serde(deny_unknown_fields)]
388pub struct CapabilityStep {
389 pub step: String,
390}
391
392#[derive(
394 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
395)]
396#[serde(rename_all = "snake_case")]
397pub enum OverrideBasis {
398 Declared,
399 Remediation,
400}
401
402#[derive(
404 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
405)]
406#[serde(rename_all = "snake_case")]
407pub enum Override {
408 None,
409 #[serde(untagged)]
410 Applied {
411 to: Party,
412 basis: OverrideBasis,
413 reason: String,
414 },
415}
416
417#[derive(
420 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
421)]
422#[serde(deny_unknown_fields)]
423pub struct Action {
424 pub addressed_to: Party,
425 pub computed: Party,
426 #[serde(rename = "override")]
427 pub override_: Override,
428 pub remediation: Remediation,
429 pub caused_by: Vec<Cause>,
431 pub transient: bool,
432}
433
434#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
436#[serde(deny_unknown_fields)]
437pub struct Refusal {
438 pub check: String,
439 pub subject: String,
440 pub action: Action,
441}
442
443pub type Question = QuestionDecl;
446
447#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
449#[serde(deny_unknown_fields)]
450pub struct NeedsInput {
451 pub fields: Vec<String>,
452 pub questions: Vec<Question>,
453 pub continuation: ContentDigest,
455 pub action: Action,
456}
457
458#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
460pub struct ContinuationInputs<'a> {
461 pub plan_id: &'a str,
462 pub snapshot_id: &'a str,
463 pub evaluation_time_ms: i64,
464 pub site: &'a str,
465 pub fields: &'a [String],
466 pub questions: &'a [String],
467}
468
469pub const CONTINUATION_TAG: &str = "rustev.continuation/1";
471
472#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
474#[serde(rename_all = "snake_case", deny_unknown_fields)]
475pub enum CheckStatus {
476 Passed,
477 Failed {
478 class: ProvenanceClass,
479 consequence: String,
481 },
482 NotEvaluated {
483 reason: crate::judgment::Unresolved,
484 },
485 Unevaluable {
486 reason: crate::judgment::Unresolved,
487 },
488 Relaxed {
489 by: String,
490 },
491}
492
493#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
495#[serde(deny_unknown_fields)]
496pub struct CheckRecord {
497 pub check: String,
498 pub subject: String,
499 pub status: CheckStatus,
500}
501
502#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
505#[serde(deny_unknown_fields)]
506pub struct PlanActionability {
507 pub checks: Vec<StaticCheck>,
508 pub sites: Vec<StaticSite>,
509}
510
511#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
512#[serde(deny_unknown_fields)]
513pub struct StaticCheck {
514 pub check: String,
515 pub subject: String,
516 pub branches: Vec<StaticBranch>,
519}
520
521#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
522#[serde(deny_unknown_fields)]
523pub struct StaticBranch {
524 pub classes: Vec<ProvenanceClass>,
525 pub consequence: String,
526 pub causes: Vec<Cause>,
527 pub party: Party,
528}
529
530#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
532#[serde(deny_unknown_fields)]
533pub struct StaticSite {
534 pub site: String,
535 pub causes: Vec<Cause>,
536 pub party: Party,
537}
538
539pub const CALLER_CONTROLLABLE: [ProvenanceClass; 3] = [
541 ProvenanceClass::UserSupplied,
542 ProvenanceClass::AuthenticatedAppField,
543 ProvenanceClass::AttributedClaim,
544];
545
546pub fn is_code(s: &str) -> bool {
548 let b = s.as_bytes();
549 !b.is_empty()
550 && b.len() <= 64
551 && b[0].is_ascii_lowercase()
552 && b.iter()
553 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || *c == b'_')
554}