Skip to main content

remote_oracle_admission/
lib.rs

1//! Typed remote-oracle admission surface crate for lease, slice, replay, and
2//! re-admission artifacts.
3
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use stack_ids::{
7    AttestationEnvelopeId, AttestationRevocationId, AttestationSupersessionId,
8    CrossRuntimeReplayTicketId, DisputeBundleId, RemoteOracleLeaseId, RemoteSliceRequestId,
9    RemoteSliceResultId, TrustRootSetId,
10};
11
12pub use stack_ids::V25ConstitutionCitation;
13
14pub const ATTESTATION_REVOCATION_V1_SCHEMA: &str = "attestation_revocation_v1";
15pub const ATTESTATION_SUPERSESSION_V1_SCHEMA: &str = "attestation_supersession_v1";
16pub const REMOTE_ORACLE_LEASE_V1_SCHEMA: &str = "remote_oracle_lease_v1";
17pub const REMOTE_SLICE_REQUEST_V1_SCHEMA: &str = "remote_slice_request_v1";
18pub const REMOTE_SLICE_RESULT_V1_SCHEMA: &str = "remote_slice_result_v1";
19pub const CROSS_RUNTIME_REPLAY_TICKET_V1_SCHEMA: &str = "cross_runtime_replay_ticket_v1";
20
21// Canonical v25 citations embed ApplicabilityContextId, ProfileSetId,
22// CompositionReceiptId, EffectiveConstitutionId, and CompiledObligationSetId.
23
24fn require_non_empty(value: &str, field: &'static str) -> Result<(), &'static str> {
25    if value.trim().is_empty() {
26        return Err(field);
27    }
28    Ok(())
29}
30
31fn require_non_empty_slice<T>(values: &[T], field: &'static str) -> Result<(), &'static str> {
32    if values.is_empty() {
33        return Err(field);
34    }
35    Ok(())
36}
37
38fn require_schema_version(
39    found: &str,
40    expected: &'static str,
41    field: &'static str,
42) -> Result<(), &'static str> {
43    if found != expected {
44        return Err(field);
45    }
46    Ok(())
47}
48
49fn require_citation(citation: &V25ConstitutionCitation) -> Result<(), &'static str> {
50    require_non_empty(
51        citation.applicability_context_id.as_str(),
52        "applicability_context_id",
53    )?;
54    require_non_empty(citation.profile_set_id.as_str(), "profile_set_id")?;
55    require_non_empty(
56        citation.composition_receipt_id.as_str(),
57        "composition_receipt_id",
58    )?;
59    require_non_empty(
60        citation.effective_constitution_id.as_str(),
61        "effective_constitution_id",
62    )?;
63    require_non_empty(
64        citation.compiled_obligation_set_id.as_str(),
65        "compiled_obligation_set_id",
66    )?;
67    Ok(())
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
71#[serde(rename_all = "snake_case")]
72pub enum RemoteExactnessClassV1 {
73    Exact,
74    BoundedExact,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
78#[serde(rename_all = "snake_case")]
79pub enum RemoteDisclosureClassV1 {
80    #[serde(rename = "non-sensitive")]
81    NonSensitive,
82    #[serde(rename = "redacted_structured_only")]
83    RedactedStructuredOnly,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
87#[serde(rename_all = "snake_case")]
88pub enum RemoteReplayObligationV1 {
89    MustReturnReplayTicketOrNonreplayableReason,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
93#[serde(rename_all = "snake_case")]
94pub enum AttestationReplayImpactV1 {
95    #[serde(rename = "replay ticket unchanged")]
96    ReplayTicketUnchanged,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
100#[serde(rename_all = "snake_case")]
101pub enum LocalAdmissionRecommendationV1 {
102    Eligible,
103    AdmitIfTransparencyReceiptPresent,
104    AdmitWithDisclosureConstraints,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
108#[serde(rename_all = "snake_case")]
109pub enum ReplayFailureBehaviorV1 {
110    Retry,
111    DowngradeToAdvisoryAndEmitDisputeIfMandatory,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
115pub struct AttestationRevocationV1 {
116    pub schema_version: String,
117    pub attestation_revocation_id: AttestationRevocationId,
118    #[serde(default, skip_serializing_if = "Vec::is_empty")]
119    pub affected_refs: Vec<String>,
120    pub revocation_reason: String,
121    pub effective_time: String,
122    pub blast_radius: String,
123    pub required_local_invalidation_behavior: String,
124    #[serde(default, skip_serializing_if = "Option::is_none")]
125    pub dispute_linkage: Option<DisputeBundleId>,
126}
127
128impl AttestationRevocationV1 {
129    #[allow(clippy::too_many_arguments)]
130    pub fn new(
131        attestation_revocation_id: AttestationRevocationId,
132        affected_refs: Vec<String>,
133        revocation_reason: impl Into<String>,
134        effective_time: impl Into<String>,
135        blast_radius: impl Into<String>,
136        required_local_invalidation_behavior: impl Into<String>,
137        dispute_linkage: Option<DisputeBundleId>,
138    ) -> Result<Self, &'static str> {
139        let value = Self {
140            schema_version: ATTESTATION_REVOCATION_V1_SCHEMA.to_string(),
141            attestation_revocation_id,
142            affected_refs,
143            revocation_reason: revocation_reason.into(),
144            effective_time: effective_time.into(),
145            blast_radius: blast_radius.into(),
146            required_local_invalidation_behavior: required_local_invalidation_behavior.into(),
147            dispute_linkage,
148        };
149        value.validate()?;
150        Ok(value)
151    }
152
153    pub fn validate(&self) -> Result<(), &'static str> {
154        require_schema_version(
155            &self.schema_version,
156            ATTESTATION_REVOCATION_V1_SCHEMA,
157            "schema_version",
158        )?;
159        require_non_empty(
160            self.attestation_revocation_id.as_str(),
161            "attestation_revocation_id",
162        )?;
163        require_non_empty(&self.revocation_reason, "revocation_reason")?;
164        require_non_empty(&self.effective_time, "effective_time")?;
165        require_non_empty(&self.blast_radius, "blast_radius")?;
166        require_non_empty(
167            &self.required_local_invalidation_behavior,
168            "required_local_invalidation_behavior",
169        )?;
170        Ok(())
171    }
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
175pub struct AttestationSupersessionV1 {
176    pub schema_version: String,
177    pub attestation_supersession_id: AttestationSupersessionId,
178    pub prior_ref: String,
179    pub replacement_ref: String,
180    pub semantic_delta_summary: String,
181    pub effective_time: String,
182    pub replay_impact: AttestationReplayImpactV1,
183    pub requires_re_admission: bool,
184}
185
186impl AttestationSupersessionV1 {
187    #[allow(clippy::too_many_arguments)]
188    pub fn new(
189        attestation_supersession_id: AttestationSupersessionId,
190        prior_ref: impl Into<String>,
191        replacement_ref: impl Into<String>,
192        semantic_delta_summary: impl Into<String>,
193        effective_time: impl Into<String>,
194        replay_impact: AttestationReplayImpactV1,
195        requires_re_admission: bool,
196    ) -> Result<Self, &'static str> {
197        let value = Self {
198            schema_version: ATTESTATION_SUPERSESSION_V1_SCHEMA.to_string(),
199            attestation_supersession_id,
200            prior_ref: prior_ref.into(),
201            replacement_ref: replacement_ref.into(),
202            semantic_delta_summary: semantic_delta_summary.into(),
203            effective_time: effective_time.into(),
204            replay_impact,
205            requires_re_admission,
206        };
207        value.validate()?;
208        Ok(value)
209    }
210
211    pub fn validate(&self) -> Result<(), &'static str> {
212        require_schema_version(
213            &self.schema_version,
214            ATTESTATION_SUPERSESSION_V1_SCHEMA,
215            "schema_version",
216        )?;
217        require_non_empty(
218            self.attestation_supersession_id.as_str(),
219            "attestation_supersession_id",
220        )?;
221        require_non_empty(&self.prior_ref, "prior_ref")?;
222        require_non_empty(&self.replacement_ref, "replacement_ref")?;
223        require_non_empty(&self.semantic_delta_summary, "semantic_delta_summary")?;
224        require_non_empty(&self.effective_time, "effective_time")?;
225        if self.prior_ref == self.replacement_ref {
226            return Err("replacement_ref");
227        }
228        Ok(())
229    }
230}
231
232#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
233pub struct RemoteOracleLeaseV1 {
234    pub schema_version: String,
235    pub remote_oracle_lease_id: RemoteOracleLeaseId,
236    pub oracle_identity: String,
237    #[serde(default, skip_serializing_if = "Vec::is_empty")]
238    pub allowed_artifact_families: Vec<String>,
239    #[serde(default, skip_serializing_if = "Vec::is_empty")]
240    pub allowed_graph_or_slice_kinds: Vec<String>,
241    pub exactness_class_ceiling: RemoteExactnessClassV1,
242    pub budget_ceiling: String,
243    pub disclosure_ceiling: RemoteDisclosureClassV1,
244    pub replay_obligation: RemoteReplayObligationV1,
245    pub lease_expiry: String,
246    #[serde(default, skip_serializing_if = "Vec::is_empty")]
247    pub policy_owner_refs: Vec<String>,
248}
249
250impl RemoteOracleLeaseV1 {
251    #[allow(clippy::too_many_arguments)]
252    pub fn new(
253        remote_oracle_lease_id: RemoteOracleLeaseId,
254        oracle_identity: impl Into<String>,
255        allowed_artifact_families: Vec<String>,
256        allowed_graph_or_slice_kinds: Vec<String>,
257        exactness_class_ceiling: RemoteExactnessClassV1,
258        budget_ceiling: impl Into<String>,
259        disclosure_ceiling: RemoteDisclosureClassV1,
260        replay_obligation: RemoteReplayObligationV1,
261        lease_expiry: impl Into<String>,
262        policy_owner_refs: Vec<String>,
263    ) -> Result<Self, &'static str> {
264        let value = Self {
265            schema_version: REMOTE_ORACLE_LEASE_V1_SCHEMA.to_string(),
266            remote_oracle_lease_id,
267            oracle_identity: oracle_identity.into(),
268            allowed_artifact_families,
269            allowed_graph_or_slice_kinds,
270            exactness_class_ceiling,
271            budget_ceiling: budget_ceiling.into(),
272            disclosure_ceiling,
273            replay_obligation,
274            lease_expiry: lease_expiry.into(),
275            policy_owner_refs,
276        };
277        value.validate()?;
278        Ok(value)
279    }
280
281    pub fn validate(&self) -> Result<(), &'static str> {
282        require_schema_version(
283            &self.schema_version,
284            REMOTE_ORACLE_LEASE_V1_SCHEMA,
285            "schema_version",
286        )?;
287        require_non_empty(
288            self.remote_oracle_lease_id.as_str(),
289            "remote_oracle_lease_id",
290        )?;
291        require_non_empty(&self.oracle_identity, "oracle_identity")?;
292        require_non_empty_slice(&self.allowed_artifact_families, "allowed_artifact_families")?;
293        require_non_empty_slice(
294            &self.allowed_graph_or_slice_kinds,
295            "allowed_graph_or_slice_kinds",
296        )?;
297        require_non_empty(&self.budget_ceiling, "budget_ceiling")?;
298        require_non_empty(&self.lease_expiry, "lease_expiry")?;
299        require_non_empty_slice(&self.policy_owner_refs, "policy_owner_refs")?;
300        Ok(())
301    }
302}
303
304#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
305pub struct RemoteSliceRequestV1 {
306    pub schema_version: String,
307    pub remote_slice_request_id: RemoteSliceRequestId,
308    pub requested_slice_definition: String,
309    #[serde(default, skip_serializing_if = "Vec::is_empty")]
310    pub required_artifact_refs: Vec<String>,
311    pub allowed_disclosure_policy: String,
312    pub exactness_target: RemoteExactnessClassV1,
313    pub trust_root_set_id: TrustRootSetId,
314    #[serde(flatten)]
315    pub citation: V25ConstitutionCitation,
316    #[serde(default, skip_serializing_if = "Vec::is_empty")]
317    pub challenge_expectations: Vec<String>,
318    pub remote_oracle_lease_id: RemoteOracleLeaseId,
319}
320
321impl RemoteSliceRequestV1 {
322    #[allow(clippy::too_many_arguments)]
323    pub fn new(
324        remote_slice_request_id: RemoteSliceRequestId,
325        requested_slice_definition: impl Into<String>,
326        required_artifact_refs: Vec<String>,
327        allowed_disclosure_policy: impl Into<String>,
328        exactness_target: RemoteExactnessClassV1,
329        trust_root_set_id: TrustRootSetId,
330        citation: V25ConstitutionCitation,
331        challenge_expectations: Vec<String>,
332        remote_oracle_lease_id: RemoteOracleLeaseId,
333    ) -> Result<Self, &'static str> {
334        let value = Self {
335            schema_version: REMOTE_SLICE_REQUEST_V1_SCHEMA.to_string(),
336            remote_slice_request_id,
337            requested_slice_definition: requested_slice_definition.into(),
338            required_artifact_refs,
339            allowed_disclosure_policy: allowed_disclosure_policy.into(),
340            exactness_target,
341            trust_root_set_id,
342            citation,
343            challenge_expectations,
344            remote_oracle_lease_id,
345        };
346        value.validate()?;
347        Ok(value)
348    }
349
350    pub fn validate(&self) -> Result<(), &'static str> {
351        require_schema_version(
352            &self.schema_version,
353            REMOTE_SLICE_REQUEST_V1_SCHEMA,
354            "schema_version",
355        )?;
356        require_non_empty(
357            self.remote_slice_request_id.as_str(),
358            "remote_slice_request_id",
359        )?;
360        require_non_empty(
361            &self.requested_slice_definition,
362            "requested_slice_definition",
363        )?;
364        require_non_empty_slice(&self.required_artifact_refs, "required_artifact_refs")?;
365        require_non_empty(&self.allowed_disclosure_policy, "allowed_disclosure_policy")?;
366        require_non_empty(self.trust_root_set_id.as_str(), "trust_root_set_id")?;
367        require_citation(&self.citation)?;
368        require_non_empty(
369            self.remote_oracle_lease_id.as_str(),
370            "remote_oracle_lease_id",
371        )?;
372        Ok(())
373    }
374}
375
376#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
377pub struct RemoteSliceResultV1 {
378    pub schema_version: String,
379    pub remote_slice_result_id: RemoteSliceResultId,
380    pub remote_slice_request_id: RemoteSliceRequestId,
381    #[serde(flatten)]
382    pub citation: V25ConstitutionCitation,
383    #[serde(default, skip_serializing_if = "Vec::is_empty")]
384    pub returned_artifact_refs: Vec<String>,
385    pub exactness_class: RemoteExactnessClassV1,
386    pub remote_execution_evidence: String,
387    #[serde(default, skip_serializing_if = "Vec::is_empty")]
388    pub disclosure_markers: Vec<String>,
389    pub replay_handle: String,
390    pub local_admission_recommendation: LocalAdmissionRecommendationV1,
391    pub attestation_envelope_id: AttestationEnvelopeId,
392}
393
394impl RemoteSliceResultV1 {
395    #[allow(clippy::too_many_arguments)]
396    pub fn new(
397        remote_slice_result_id: RemoteSliceResultId,
398        remote_slice_request_id: RemoteSliceRequestId,
399        citation: V25ConstitutionCitation,
400        returned_artifact_refs: Vec<String>,
401        exactness_class: RemoteExactnessClassV1,
402        remote_execution_evidence: impl Into<String>,
403        disclosure_markers: Vec<String>,
404        replay_handle: impl Into<String>,
405        local_admission_recommendation: LocalAdmissionRecommendationV1,
406        attestation_envelope_id: AttestationEnvelopeId,
407    ) -> Result<Self, &'static str> {
408        let value = Self {
409            schema_version: REMOTE_SLICE_RESULT_V1_SCHEMA.to_string(),
410            remote_slice_result_id,
411            remote_slice_request_id,
412            citation,
413            returned_artifact_refs,
414            exactness_class,
415            remote_execution_evidence: remote_execution_evidence.into(),
416            disclosure_markers,
417            replay_handle: replay_handle.into(),
418            local_admission_recommendation,
419            attestation_envelope_id,
420        };
421        value.validate()?;
422        Ok(value)
423    }
424
425    pub fn validate(&self) -> Result<(), &'static str> {
426        require_schema_version(
427            &self.schema_version,
428            REMOTE_SLICE_RESULT_V1_SCHEMA,
429            "schema_version",
430        )?;
431        require_non_empty(
432            self.remote_slice_result_id.as_str(),
433            "remote_slice_result_id",
434        )?;
435        require_non_empty(
436            self.remote_slice_request_id.as_str(),
437            "remote_slice_request_id",
438        )?;
439        require_citation(&self.citation)?;
440        require_non_empty_slice(&self.returned_artifact_refs, "returned_artifact_refs")?;
441        require_non_empty(&self.remote_execution_evidence, "remote_execution_evidence")?;
442        require_non_empty(&self.replay_handle, "replay_handle")?;
443        require_non_empty(
444            self.attestation_envelope_id.as_str(),
445            "attestation_envelope_id",
446        )?;
447        Ok(())
448    }
449}
450
451#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
452pub struct CrossRuntimeReplayTicketV1 {
453    pub schema_version: String,
454    pub cross_runtime_replay_ticket_id: CrossRuntimeReplayTicketId,
455    #[serde(flatten)]
456    pub citation: V25ConstitutionCitation,
457    #[serde(default, skip_serializing_if = "Vec::is_empty")]
458    pub artifact_refs: Vec<String>,
459    pub time_coordinates: String,
460    #[serde(default, skip_serializing_if = "Vec::is_empty")]
461    pub required_trust_roots: Vec<TrustRootSetId>,
462    pub allowed_disclosure: RemoteDisclosureClassV1,
463    pub lease_window: String,
464    #[serde(default, skip_serializing_if = "Vec::is_empty")]
465    pub replay_expectations: Vec<String>,
466    pub failure_behavior: ReplayFailureBehaviorV1,
467}
468
469impl CrossRuntimeReplayTicketV1 {
470    #[allow(clippy::too_many_arguments)]
471    pub fn new(
472        cross_runtime_replay_ticket_id: CrossRuntimeReplayTicketId,
473        citation: V25ConstitutionCitation,
474        artifact_refs: Vec<String>,
475        time_coordinates: impl Into<String>,
476        required_trust_roots: Vec<TrustRootSetId>,
477        allowed_disclosure: RemoteDisclosureClassV1,
478        lease_window: impl Into<String>,
479        replay_expectations: Vec<String>,
480        failure_behavior: ReplayFailureBehaviorV1,
481    ) -> Result<Self, &'static str> {
482        let value = Self {
483            schema_version: CROSS_RUNTIME_REPLAY_TICKET_V1_SCHEMA.to_string(),
484            cross_runtime_replay_ticket_id,
485            citation,
486            artifact_refs,
487            time_coordinates: time_coordinates.into(),
488            required_trust_roots,
489            allowed_disclosure,
490            lease_window: lease_window.into(),
491            replay_expectations,
492            failure_behavior,
493        };
494        value.validate()?;
495        Ok(value)
496    }
497
498    pub fn validate(&self) -> Result<(), &'static str> {
499        require_schema_version(
500            &self.schema_version,
501            CROSS_RUNTIME_REPLAY_TICKET_V1_SCHEMA,
502            "schema_version",
503        )?;
504        require_non_empty(
505            self.cross_runtime_replay_ticket_id.as_str(),
506            "cross_runtime_replay_ticket_id",
507        )?;
508        require_citation(&self.citation)?;
509        require_non_empty_slice(&self.artifact_refs, "artifact_refs")?;
510        require_non_empty(&self.time_coordinates, "time_coordinates")?;
511        require_non_empty_slice(&self.required_trust_roots, "required_trust_roots")?;
512        require_non_empty(&self.lease_window, "lease_window")?;
513        Ok(())
514    }
515}