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}