Skip to main content

chio_governance/
generic.rs

1use serde::{Deserialize, Serialize};
2
3use crate::canonical_json_bytes;
4use crate::crypto::sha256_hex;
5use crate::listing::{
6    normalize_namespace, GenericListingActorKind, GenericRegistryPublisher, SignedGenericListing,
7    SignedGenericTrustActivation,
8};
9use crate::receipt::lineage::SignedExportEnvelope;
10use crate::validation::{is_sha256_hex, validate_non_empty};
11
12pub const GENERIC_GOVERNANCE_CHARTER_ARTIFACT_SCHEMA: &str = "chio.registry.governance-charter.v1";
13pub const GENERIC_GOVERNANCE_CASE_ARTIFACT_SCHEMA: &str = "chio.registry.governance-case.v1";
14
15#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
16#[serde(rename_all = "snake_case")]
17pub enum GenericGovernanceCaseKind {
18    Dispute,
19    Freeze,
20    Sanction,
21    Appeal,
22}
23
24#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
25#[serde(rename_all = "snake_case")]
26pub enum GenericGovernanceCaseState {
27    Open,
28    Escalated,
29    Enforced,
30    Resolved,
31    Denied,
32    Superseded,
33}
34
35#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
36#[serde(rename_all = "snake_case")]
37pub enum GenericGovernanceEffectiveState {
38    Clear,
39    Disputed,
40    Frozen,
41    Sanctioned,
42    Appealed,
43}
44
45#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
46#[serde(rename_all = "snake_case")]
47pub enum GenericGovernanceEvidenceKind {
48    Listing,
49    TrustActivation,
50    Certification,
51    RegistrySearch,
52    OperatorReport,
53    External,
54}
55
56#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
57#[serde(rename_all = "snake_case")]
58pub enum GenericGovernanceFindingCode {
59    ListingUnverifiable,
60    ActivationUnverifiable,
61    CharterUnverifiable,
62    CaseUnverifiable,
63    PriorCaseUnverifiable,
64    CharterExpired,
65    CaseExpired,
66    CharterScopeMismatch,
67    CharterKindUnsupported,
68    CaseMismatch,
69    MissingActivation,
70    ActivationMismatch,
71    AppealTargetMissing,
72    AppealTargetInvalid,
73    SupersessionTargetMissing,
74    SupersessionTargetInvalid,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
78#[serde(rename_all = "camelCase")]
79pub struct GenericGovernanceAuthorityScope {
80    pub namespace: String,
81    #[serde(default, skip_serializing_if = "Vec::is_empty")]
82    pub allowed_listing_operator_ids: Vec<String>,
83    #[serde(default, skip_serializing_if = "Vec::is_empty")]
84    pub allowed_actor_kinds: Vec<GenericListingActorKind>,
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub policy_reference: Option<String>,
87}
88
89impl GenericGovernanceAuthorityScope {
90    pub fn validate(&self) -> Result<(), String> {
91        validate_non_empty(&self.namespace, "authority_scope.namespace")?;
92        for (index, operator_id) in self.allowed_listing_operator_ids.iter().enumerate() {
93            validate_non_empty(
94                operator_id,
95                &format!("authority_scope.allowed_listing_operator_ids[{index}]"),
96            )?;
97        }
98        Ok(())
99    }
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
103#[serde(rename_all = "camelCase")]
104pub struct GenericGovernanceEvidenceReference {
105    pub kind: GenericGovernanceEvidenceKind,
106    pub reference_id: String,
107    #[serde(default, skip_serializing_if = "Option::is_none")]
108    pub uri: Option<String>,
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub sha256: Option<String>,
111}
112
113impl GenericGovernanceEvidenceReference {
114    pub fn validate(&self, field: &str) -> Result<(), String> {
115        validate_non_empty(&self.reference_id, &format!("{field}.reference_id"))?;
116        if let Some(uri) = self.uri.as_deref() {
117            validate_non_empty(uri, &format!("{field}.uri"))?;
118        }
119        if let Some(sha256) = self.sha256.as_deref() {
120            let sha256_field = format!("{field}.sha256");
121            if !is_sha256_hex(sha256) {
122                return Err(format!(
123                    "{sha256_field} must be a 64-character SHA-256 hex digest"
124                ));
125            }
126        }
127        Ok(())
128    }
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
132#[serde(rename_all = "camelCase")]
133pub struct GenericGovernanceCharterArtifact {
134    pub schema: String,
135    pub charter_id: String,
136    pub governing_operator_id: String,
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub governing_operator_name: Option<String>,
139    pub authority_scope: GenericGovernanceAuthorityScope,
140    pub allowed_case_kinds: Vec<GenericGovernanceCaseKind>,
141    #[serde(default, skip_serializing_if = "Vec::is_empty")]
142    pub escalation_operator_ids: Vec<String>,
143    pub issued_at: u64,
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    pub expires_at: Option<u64>,
146    pub issued_by: String,
147    #[serde(default, skip_serializing_if = "Option::is_none")]
148    pub note: Option<String>,
149}
150
151impl GenericGovernanceCharterArtifact {
152    pub fn validate(&self) -> Result<(), String> {
153        if self.schema != GENERIC_GOVERNANCE_CHARTER_ARTIFACT_SCHEMA {
154            return Err(format!(
155                "unsupported generic governance charter schema: {}",
156                self.schema
157            ));
158        }
159        validate_non_empty(&self.charter_id, "charter_id")?;
160        validate_non_empty(&self.governing_operator_id, "governing_operator_id")?;
161        validate_non_empty(&self.issued_by, "issued_by")?;
162        self.authority_scope.validate()?;
163        if normalize_namespace(&self.authority_scope.namespace).is_empty() {
164            return Err("authority_scope.namespace must not be empty".to_string());
165        }
166        if self.allowed_case_kinds.is_empty() {
167            return Err("allowed_case_kinds must not be empty".to_string());
168        }
169        for (index, operator_id) in self.escalation_operator_ids.iter().enumerate() {
170            validate_non_empty(operator_id, &format!("escalation_operator_ids[{index}]"))?;
171        }
172        if let Some(expires_at) = self.expires_at {
173            if expires_at <= self.issued_at {
174                return Err("expires_at must be greater than issued_at".to_string());
175            }
176        }
177        Ok(())
178    }
179}
180
181pub type SignedGenericGovernanceCharter = SignedExportEnvelope<GenericGovernanceCharterArtifact>;
182
183#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
184#[serde(rename_all = "camelCase")]
185pub struct GenericGovernanceCaseArtifact {
186    pub schema: String,
187    pub case_id: String,
188    pub charter_id: String,
189    pub governing_operator_id: String,
190    pub kind: GenericGovernanceCaseKind,
191    pub state: GenericGovernanceCaseState,
192    pub namespace: String,
193    pub listing_id: String,
194    #[serde(default, skip_serializing_if = "Option::is_none")]
195    pub activation_id: Option<String>,
196    #[serde(default, skip_serializing_if = "Option::is_none")]
197    pub subject_operator_id: Option<String>,
198    pub opened_at: u64,
199    pub updated_at: u64,
200    #[serde(default, skip_serializing_if = "Option::is_none")]
201    pub expires_at: Option<u64>,
202    #[serde(default, skip_serializing_if = "Vec::is_empty")]
203    pub escalated_to_operator_ids: Vec<String>,
204    pub evidence_refs: Vec<GenericGovernanceEvidenceReference>,
205    #[serde(default, skip_serializing_if = "Option::is_none")]
206    pub appeal_of_case_id: Option<String>,
207    #[serde(default, skip_serializing_if = "Option::is_none")]
208    pub supersedes_case_id: Option<String>,
209    pub issued_by: String,
210    #[serde(default, skip_serializing_if = "Option::is_none")]
211    pub note: Option<String>,
212}
213
214impl GenericGovernanceCaseArtifact {
215    pub fn validate(&self) -> Result<(), String> {
216        if self.schema != GENERIC_GOVERNANCE_CASE_ARTIFACT_SCHEMA {
217            return Err(format!(
218                "unsupported generic governance case schema: {}",
219                self.schema
220            ));
221        }
222        validate_non_empty(&self.case_id, "case_id")?;
223        validate_non_empty(&self.charter_id, "charter_id")?;
224        validate_non_empty(&self.governing_operator_id, "governing_operator_id")?;
225        validate_non_empty(&self.namespace, "namespace")?;
226        validate_non_empty(&self.listing_id, "listing_id")?;
227        validate_non_empty(&self.issued_by, "issued_by")?;
228        if self.updated_at < self.opened_at {
229            return Err("updated_at must be greater than or equal to opened_at".to_string());
230        }
231        if let Some(expires_at) = self.expires_at {
232            if expires_at <= self.opened_at {
233                return Err("expires_at must be greater than opened_at".to_string());
234            }
235        }
236        for (index, operator_id) in self.escalated_to_operator_ids.iter().enumerate() {
237            validate_non_empty(operator_id, &format!("escalated_to_operator_ids[{index}]"))?;
238        }
239        if self.evidence_refs.is_empty() {
240            return Err("evidence_refs must not be empty".to_string());
241        }
242        for (index, evidence_ref) in self.evidence_refs.iter().enumerate() {
243            evidence_ref.validate(&format!("evidence_refs[{index}]"))?;
244        }
245        if matches!(self.kind, GenericGovernanceCaseKind::Appeal) {
246            if self.appeal_of_case_id.as_deref().is_none() {
247                return Err("appeal case requires appeal_of_case_id".to_string());
248            }
249        } else if self.appeal_of_case_id.is_some() {
250            return Err("appeal_of_case_id is only valid for appeal cases".to_string());
251        }
252        if matches!(self.state, GenericGovernanceCaseState::Escalated)
253            && self.escalated_to_operator_ids.is_empty()
254        {
255            return Err("escalated case requires escalated_to_operator_ids".to_string());
256        }
257        Ok(())
258    }
259}
260
261pub type SignedGenericGovernanceCase = SignedExportEnvelope<GenericGovernanceCaseArtifact>;
262
263#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
264#[serde(rename_all = "camelCase")]
265pub struct GenericGovernanceCharterIssueRequest {
266    pub authority_scope: GenericGovernanceAuthorityScope,
267    pub allowed_case_kinds: Vec<GenericGovernanceCaseKind>,
268    #[serde(default, skip_serializing_if = "Vec::is_empty")]
269    pub escalation_operator_ids: Vec<String>,
270    pub issued_by: String,
271    #[serde(default, skip_serializing_if = "Option::is_none")]
272    pub issued_at: Option<u64>,
273    #[serde(default, skip_serializing_if = "Option::is_none")]
274    pub expires_at: Option<u64>,
275    #[serde(default, skip_serializing_if = "Option::is_none")]
276    pub note: Option<String>,
277}
278
279impl GenericGovernanceCharterIssueRequest {
280    pub fn validate(&self) -> Result<(), String> {
281        self.authority_scope.validate()?;
282        validate_non_empty(&self.issued_by, "issued_by")?;
283        if self.allowed_case_kinds.is_empty() {
284            return Err("allowed_case_kinds must not be empty".to_string());
285        }
286        Ok(())
287    }
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
291#[serde(rename_all = "camelCase")]
292pub struct GenericGovernanceCaseIssueRequest {
293    pub charter: SignedGenericGovernanceCharter,
294    pub listing: SignedGenericListing,
295    #[serde(default, skip_serializing_if = "Option::is_none")]
296    pub activation: Option<SignedGenericTrustActivation>,
297    pub kind: GenericGovernanceCaseKind,
298    pub state: GenericGovernanceCaseState,
299    #[serde(default, skip_serializing_if = "Option::is_none")]
300    pub subject_operator_id: Option<String>,
301    #[serde(default, skip_serializing_if = "Vec::is_empty")]
302    pub escalated_to_operator_ids: Vec<String>,
303    pub evidence_refs: Vec<GenericGovernanceEvidenceReference>,
304    #[serde(default, skip_serializing_if = "Option::is_none")]
305    pub appeal_of_case_id: Option<String>,
306    #[serde(default, skip_serializing_if = "Option::is_none")]
307    pub supersedes_case_id: Option<String>,
308    pub issued_by: String,
309    #[serde(default, skip_serializing_if = "Option::is_none")]
310    pub opened_at: Option<u64>,
311    #[serde(default, skip_serializing_if = "Option::is_none")]
312    pub updated_at: Option<u64>,
313    #[serde(default, skip_serializing_if = "Option::is_none")]
314    pub expires_at: Option<u64>,
315    #[serde(default, skip_serializing_if = "Option::is_none")]
316    pub note: Option<String>,
317}
318
319impl GenericGovernanceCaseIssueRequest {
320    pub fn validate(&self) -> Result<(), String> {
321        self.listing.body.validate()?;
322        if !self
323            .listing
324            .verify_signature()
325            .map_err(|error| error.to_string())?
326        {
327            return Err("governance case listing signature is invalid".to_string());
328        }
329        if !self
330            .charter
331            .verify_signature()
332            .map_err(|error| error.to_string())?
333        {
334            return Err("governance charter signature is invalid".to_string());
335        }
336        self.charter.body.validate()?;
337        if let Some(activation) = self.activation.as_ref() {
338            if !activation
339                .verify_signature()
340                .map_err(|error| error.to_string())?
341            {
342                return Err("trust activation signature is invalid".to_string());
343            }
344            activation
345                .body
346                .validate()
347                .map_err(|error| error.to_string())?;
348        }
349        validate_non_empty(&self.issued_by, "issued_by")?;
350        if self.evidence_refs.is_empty() {
351            return Err("evidence_refs must not be empty".to_string());
352        }
353        for (index, evidence_ref) in self.evidence_refs.iter().enumerate() {
354            evidence_ref.validate(&format!("evidence_refs[{index}]"))?;
355        }
356        Ok(())
357    }
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
361#[serde(rename_all = "camelCase")]
362pub struct GenericGovernanceCaseEvaluationRequest {
363    pub listing: SignedGenericListing,
364    pub current_publisher: GenericRegistryPublisher,
365    #[serde(default, skip_serializing_if = "Option::is_none")]
366    pub activation: Option<SignedGenericTrustActivation>,
367    pub charter: SignedGenericGovernanceCharter,
368    pub case: SignedGenericGovernanceCase,
369    #[serde(default, skip_serializing_if = "Option::is_none")]
370    pub prior_case: Option<SignedGenericGovernanceCase>,
371    #[serde(default, skip_serializing_if = "Option::is_none")]
372    pub evaluated_at: Option<u64>,
373}
374
375impl GenericGovernanceCaseEvaluationRequest {
376    pub fn validate(&self) -> Result<(), String> {
377        self.listing.body.validate()?;
378        self.current_publisher.validate()?;
379        Ok(())
380    }
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
384#[serde(rename_all = "camelCase")]
385pub struct GenericGovernanceFinding {
386    pub code: GenericGovernanceFindingCode,
387    pub message: String,
388}
389
390#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
391#[serde(rename_all = "camelCase")]
392pub struct GenericGovernanceCaseEvaluation {
393    pub listing_id: String,
394    pub namespace: String,
395    pub charter_id: String,
396    pub case_id: String,
397    pub governing_operator_id: String,
398    pub kind: GenericGovernanceCaseKind,
399    pub state: GenericGovernanceCaseState,
400    pub effective_state: GenericGovernanceEffectiveState,
401    pub evaluated_at: u64,
402    pub blocks_admission: bool,
403    #[serde(default, skip_serializing_if = "Vec::is_empty")]
404    pub findings: Vec<GenericGovernanceFinding>,
405}
406
407pub fn build_generic_governance_charter_artifact(
408    local_operator_id: &str,
409    local_operator_name: Option<String>,
410    request: &GenericGovernanceCharterIssueRequest,
411    issued_at: u64,
412) -> Result<GenericGovernanceCharterArtifact, String> {
413    request.validate()?;
414    validate_non_empty(local_operator_id, "local_operator_id")?;
415    let issued_at = request.issued_at.unwrap_or(issued_at);
416    let charter_id = format!(
417        "charter-{}",
418        sha256_hex(
419            &canonical_json_bytes(&(
420                local_operator_id,
421                normalize_namespace(&request.authority_scope.namespace),
422                &request.allowed_case_kinds,
423                issued_at,
424            ))
425            .map_err(|error| error.to_string())?
426        )
427    );
428    let artifact = GenericGovernanceCharterArtifact {
429        schema: GENERIC_GOVERNANCE_CHARTER_ARTIFACT_SCHEMA.to_string(),
430        charter_id,
431        governing_operator_id: local_operator_id.to_string(),
432        governing_operator_name: local_operator_name,
433        authority_scope: request.authority_scope.clone(),
434        allowed_case_kinds: request.allowed_case_kinds.clone(),
435        escalation_operator_ids: request.escalation_operator_ids.clone(),
436        issued_at,
437        expires_at: request.expires_at,
438        issued_by: request.issued_by.clone(),
439        note: request.note.clone(),
440    };
441    artifact.validate()?;
442    Ok(artifact)
443}
444
445pub fn build_generic_governance_case_artifact(
446    local_operator_id: &str,
447    request: &GenericGovernanceCaseIssueRequest,
448    issued_at: u64,
449) -> Result<GenericGovernanceCaseArtifact, String> {
450    request.validate()?;
451    validate_non_empty(local_operator_id, "local_operator_id")?;
452    if request.charter.body.governing_operator_id != local_operator_id {
453        return Err("governance case must be issued by the charter governing operator".to_string());
454    }
455    if request
456        .activation
457        .as_ref()
458        .is_some_and(|activation| activation.body.local_operator_id != local_operator_id)
459    {
460        return Err(
461            "governance cases must use a trust activation issued by the governing operator"
462                .to_string(),
463        );
464    }
465    let opened_at = request.opened_at.unwrap_or(issued_at);
466    let updated_at = request.updated_at.unwrap_or(opened_at);
467    let case_id = format!(
468        "case-{}",
469        sha256_hex(
470            &canonical_json_bytes(&(
471                local_operator_id,
472                &request.charter.body.charter_id,
473                &request.listing.body.listing_id,
474                request.kind,
475                request.state,
476                opened_at,
477                &request.appeal_of_case_id,
478                &request.supersedes_case_id,
479            ))
480            .map_err(|error| error.to_string())?
481        )
482    );
483    let artifact = GenericGovernanceCaseArtifact {
484        schema: GENERIC_GOVERNANCE_CASE_ARTIFACT_SCHEMA.to_string(),
485        case_id,
486        charter_id: request.charter.body.charter_id.clone(),
487        governing_operator_id: local_operator_id.to_string(),
488        kind: request.kind,
489        state: request.state,
490        namespace: request.listing.body.namespace.clone(),
491        listing_id: request.listing.body.listing_id.clone(),
492        activation_id: request
493            .activation
494            .as_ref()
495            .map(|activation| activation.body.activation_id.clone()),
496        subject_operator_id: request.subject_operator_id.clone(),
497        opened_at,
498        updated_at,
499        expires_at: request.expires_at,
500        escalated_to_operator_ids: request.escalated_to_operator_ids.clone(),
501        evidence_refs: request.evidence_refs.clone(),
502        appeal_of_case_id: request.appeal_of_case_id.clone(),
503        supersedes_case_id: request.supersedes_case_id.clone(),
504        issued_by: request.issued_by.clone(),
505        note: request.note.clone(),
506    };
507    artifact.validate()?;
508    Ok(artifact)
509}