1use std::collections::{HashMap, HashSet};
8
9use serde::{Deserialize, Serialize};
10
11pub const CHIO_EXTENSION_INVENTORY_SCHEMA: &str = "chio.extension-inventory.v1";
12pub const CHIO_EXTENSION_MANIFEST_SCHEMA: &str = "chio.extension-manifest.v1";
13pub const CHIO_EXTENSION_NEGOTIATION_SCHEMA: &str = "chio.extension-negotiation.v1";
14pub const CHIO_OFFICIAL_STACK_SCHEMA: &str = "chio.official-stack.v1";
15pub const CHIO_EXTENSION_QUALIFICATION_MATRIX_SCHEMA: &str =
16 "chio.extension-qualification-matrix.v1";
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum CanonicalContractKind {
21 Capability,
22 Receipt,
23 Policy,
24 ArtifactFamily,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29pub enum ExtensionPointKind {
30 Authority,
31 Store,
32 ToolServerConnection,
33 ResourceProvider,
34 PromptProvider,
35 Adapter,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "snake_case")]
40pub enum ExtensionStability {
41 Supported,
42 Experimental,
43 Internal,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
47#[serde(rename_all = "snake_case")]
48pub enum ExtensionIsolation {
49 InProcess,
50 Subprocess,
51 RemoteService,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
55#[serde(rename_all = "snake_case")]
56pub enum ExtensionEvidenceMode {
57 None,
58 ImportOnly,
59 DispatchOnly,
60 ImportAndDispatch,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
64#[serde(rename_all = "snake_case")]
65pub enum ExtensionPrivilege {
66 FilesystemRead,
67 FilesystemWrite,
68 NetworkEgress,
69 ProcessExecution,
70 OperatorSecrets,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(rename_all = "snake_case")]
75pub enum ExtensionDistribution {
76 OfficialFirstParty,
77 CustomFirstParty,
78 ThirdPartyCustom,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(rename_all = "snake_case")]
83pub enum OfficialImplementationSource {
84 FirstParty,
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(rename_all = "snake_case")]
89pub enum ExtensionNegotiationOutcome {
90 Accepted,
91 Rejected,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
95#[serde(rename_all = "snake_case")]
96pub enum QualificationMode {
97 OfficialToOfficial,
98 OfficialToCustom,
99 CustomToOfficial,
100 CustomToCustom,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
104#[serde(rename_all = "snake_case")]
105pub enum QualificationOutcome {
106 Pass,
107 FailClosed,
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
111#[serde(rename_all = "snake_case")]
112pub enum QualificationInvariant {
113 PreservesCanonicalTruth,
114 RequiresLocalPolicyActivation,
115 RejectsVersionMismatch,
116 RejectsPrivilegeEscalation,
117 RejectsTruthMutation,
118 RejectsUnsignedEvidence,
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
122#[serde(rename_all = "snake_case")]
123pub enum ExtensionNegotiationRejectionCode {
124 MalformedInventory,
125 MalformedOfficialStack,
126 MalformedManifest,
127 UnknownExtensionPoint,
128 UnsupportedOfficialStack,
129 UnsupportedChioContract,
130 UnsupportedProfile,
131 UnsupportedComponent,
132 UnsupportedIsolation,
133 UnsupportedEvidenceMode,
134 UnsupportedPrivilege,
135 OfficialOnlyPoint,
136 InternalOnlyPoint,
137 LocalPolicyActivationRequired,
138 MissingSubjectBinding,
139 MissingSignerVerification,
140 MissingFreshnessCheck,
141 TruthMutationNotAllowed,
142 TrustWideningNotAllowed,
143}
144
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146#[serde(deny_unknown_fields)]
147pub struct CanonicalTruthSurface {
148 pub id: String,
149 pub name: String,
150 pub crate_path: String,
151 pub contract_kind: CanonicalContractKind,
152 pub artifact_schemas: Vec<String>,
153 pub notes: String,
154 pub extensions_may_write: bool,
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
158#[serde(deny_unknown_fields)]
159pub struct ChioExtensionPoint {
160 pub id: String,
161 pub name: String,
162 pub point_kind: ExtensionPointKind,
163 pub owner: String,
164 pub contract_path: String,
165 pub stability: ExtensionStability,
166 pub allowed_isolations: Vec<ExtensionIsolation>,
167 pub allowed_evidence_modes: Vec<ExtensionEvidenceMode>,
168 pub allowed_privileges: Vec<ExtensionPrivilege>,
169 pub custom_implementations_allowed: bool,
170 pub policy_activation_required: bool,
171 pub official_component_ids: Vec<String>,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
175#[serde(deny_unknown_fields)]
176pub struct ChioExtensionInventory {
177 pub schema: String,
178 pub chio_contract_version: String,
179 pub canonical_truth: Vec<CanonicalTruthSurface>,
180 pub extension_points: Vec<ChioExtensionPoint>,
181}
182
183#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184#[serde(deny_unknown_fields)]
185pub struct OfficialStackComponent {
186 pub id: String,
187 pub name: String,
188 pub extension_point_ids: Vec<String>,
189 pub crate_path: String,
190 pub implementation_source: OfficialImplementationSource,
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
194#[serde(deny_unknown_fields)]
195pub struct OfficialStackProfile {
196 pub id: String,
197 pub name: String,
198 pub description: String,
199 pub component_ids: Vec<String>,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
203#[serde(deny_unknown_fields)]
204pub struct OfficialStackPackage {
205 pub schema: String,
206 pub package_id: String,
207 pub version: String,
208 pub chio_contract_version: String,
209 pub components: Vec<OfficialStackComponent>,
210 pub profiles: Vec<OfficialStackProfile>,
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
214#[serde(deny_unknown_fields)]
215pub struct ExtensionCompatibility {
216 pub chio_contract_version: String,
217 pub official_stack_package_id: String,
218 pub supported_component_ids: Vec<String>,
219 pub supported_contract_schemas: Vec<String>,
220}
221
222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
223#[serde(deny_unknown_fields)]
224pub struct ExtensionRuntimeEnvelope {
225 pub isolation: ExtensionIsolation,
226 pub allowed_privileges: Vec<ExtensionPrivilege>,
227 pub evidence_mode: ExtensionEvidenceMode,
228 pub requires_subject_binding: bool,
229 pub requires_signer_verification: bool,
230 pub requires_freshness_check: bool,
231 pub requires_local_policy_activation: bool,
232 pub allows_truth_mutation: bool,
233 pub allows_trust_widening: bool,
234}
235
236#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
237#[serde(deny_unknown_fields)]
238pub struct ChioExtensionManifest {
239 pub schema: String,
240 pub extension_id: String,
241 pub display_name: String,
242 pub version: String,
243 pub distribution: ExtensionDistribution,
244 pub extension_point_id: String,
245 pub capabilities: Vec<String>,
246 pub supported_profiles: Vec<String>,
247 pub compatibility: ExtensionCompatibility,
248 pub runtime: ExtensionRuntimeEnvelope,
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252#[serde(deny_unknown_fields)]
253pub struct ExtensionNegotiationRejection {
254 pub code: ExtensionNegotiationRejectionCode,
255 pub detail: String,
256}
257
258#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
259#[serde(deny_unknown_fields)]
260pub struct ExtensionNegotiationReport {
261 pub schema: String,
262 pub official_stack_package_id: String,
263 pub extension_id: String,
264 pub extension_point_id: String,
265 pub outcome: ExtensionNegotiationOutcome,
266 pub reasons: Vec<ExtensionNegotiationRejection>,
267}
268
269#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
270#[serde(deny_unknown_fields)]
271pub struct ExtensionQualificationCase {
272 pub id: String,
273 pub name: String,
274 pub extension_point_id: String,
275 pub supported_component_id: String,
276 pub candidate_extension_id: String,
277 pub mode: QualificationMode,
278 pub expected_outcome: QualificationOutcome,
279 pub observed_outcome: QualificationOutcome,
280 pub rejection_codes: Vec<ExtensionNegotiationRejectionCode>,
281 pub invariants: Vec<QualificationInvariant>,
282}
283
284#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
285#[serde(deny_unknown_fields)]
286pub struct ExtensionQualificationMatrix {
287 pub schema: String,
288 pub official_stack_package_id: String,
289 pub chio_contract_version: String,
290 pub cases: Vec<ExtensionQualificationCase>,
291}
292
293#[derive(Debug, thiserror::Error, PartialEq, Eq)]
294pub enum ExtensionContractError {
295 #[error("unsupported schema: {0}")]
296 UnsupportedSchema(String),
297
298 #[error("missing field: {0}")]
299 MissingField(&'static str),
300
301 #[error("duplicate id or value: {0}")]
302 DuplicateValue(String),
303
304 #[error("unknown reference: {0}")]
305 UnknownReference(String),
306
307 #[error("invalid guardrail: {0}")]
308 InvalidGuardrail(String),
309
310 #[error("invalid profile: {0}")]
311 InvalidProfile(String),
312
313 #[error("invalid qualification case: {0}")]
314 InvalidQualificationCase(String),
315}
316
317pub fn validate_extension_inventory(
318 inventory: &ChioExtensionInventory,
319) -> Result<(), ExtensionContractError> {
320 if inventory.schema != CHIO_EXTENSION_INVENTORY_SCHEMA {
321 return Err(ExtensionContractError::UnsupportedSchema(
322 inventory.schema.clone(),
323 ));
324 }
325 ensure_non_empty(&inventory.chio_contract_version, "chio_contract_version")?;
326 if inventory.canonical_truth.is_empty() {
327 return Err(ExtensionContractError::MissingField("canonical_truth"));
328 }
329 if inventory.extension_points.is_empty() {
330 return Err(ExtensionContractError::MissingField("extension_points"));
331 }
332
333 let mut ids = HashSet::new();
334 for surface in &inventory.canonical_truth {
335 ensure_non_empty(&surface.id, "canonical_truth.id")?;
336 ensure_non_empty(&surface.name, "canonical_truth.name")?;
337 ensure_non_empty(&surface.crate_path, "canonical_truth.crate_path")?;
338 ensure_non_empty(&surface.notes, "canonical_truth.notes")?;
339 if surface.artifact_schemas.is_empty() {
340 return Err(ExtensionContractError::MissingField(
341 "canonical_truth.artifact_schemas",
342 ));
343 }
344 if surface.extensions_may_write {
345 return Err(ExtensionContractError::InvalidGuardrail(format!(
346 "canonical truth surface {} must not be writable by extensions",
347 surface.id
348 )));
349 }
350 if !ids.insert(surface.id.as_str()) {
351 return Err(ExtensionContractError::DuplicateValue(surface.id.clone()));
352 }
353 ensure_unique_strings(
354 &surface.artifact_schemas,
355 "canonical_truth.artifact_schemas",
356 )?;
357 }
358
359 for point in &inventory.extension_points {
360 ensure_non_empty(&point.id, "extension_points.id")?;
361 ensure_non_empty(&point.name, "extension_points.name")?;
362 ensure_non_empty(&point.owner, "extension_points.owner")?;
363 ensure_non_empty(&point.contract_path, "extension_points.contract_path")?;
364 if !ids.insert(point.id.as_str()) {
365 return Err(ExtensionContractError::DuplicateValue(point.id.clone()));
366 }
367 if point.allowed_isolations.is_empty() {
368 return Err(ExtensionContractError::MissingField(
369 "extension_points.allowed_isolations",
370 ));
371 }
372 if point.allowed_evidence_modes.is_empty() {
373 return Err(ExtensionContractError::MissingField(
374 "extension_points.allowed_evidence_modes",
375 ));
376 }
377 if point.allowed_privileges.is_empty() {
378 return Err(ExtensionContractError::MissingField(
379 "extension_points.allowed_privileges",
380 ));
381 }
382 if point.official_component_ids.is_empty() {
383 return Err(ExtensionContractError::MissingField(
384 "extension_points.official_component_ids",
385 ));
386 }
387 ensure_unique_copy_values(
388 &point.allowed_isolations,
389 "extension_points.allowed_isolations",
390 )?;
391 ensure_unique_copy_values(
392 &point.allowed_evidence_modes,
393 "extension_points.allowed_evidence_modes",
394 )?;
395 ensure_unique_copy_values(
396 &point.allowed_privileges,
397 "extension_points.allowed_privileges",
398 )?;
399 ensure_unique_strings(
400 &point.official_component_ids,
401 "extension_points.official_component_ids",
402 )?;
403 if point.policy_activation_required
404 && point.allowed_evidence_modes == [ExtensionEvidenceMode::None]
405 {
406 return Err(ExtensionContractError::InvalidGuardrail(format!(
407 "extension point {} requires policy activation but admits no evidence-capable mode",
408 point.id
409 )));
410 }
411 }
412
413 Ok(())
414}
415
416pub fn validate_official_stack_package(
417 inventory: &ChioExtensionInventory,
418 package: &OfficialStackPackage,
419) -> Result<(), ExtensionContractError> {
420 validate_extension_inventory(inventory)?;
421 if package.schema != CHIO_OFFICIAL_STACK_SCHEMA {
422 return Err(ExtensionContractError::UnsupportedSchema(
423 package.schema.clone(),
424 ));
425 }
426 ensure_non_empty(&package.package_id, "official_stack.package_id")?;
427 ensure_non_empty(&package.version, "official_stack.version")?;
428 ensure_non_empty(
429 &package.chio_contract_version,
430 "official_stack.chio_contract_version",
431 )?;
432 if package.components.is_empty() {
433 return Err(ExtensionContractError::MissingField(
434 "official_stack.components",
435 ));
436 }
437 if package.profiles.is_empty() {
438 return Err(ExtensionContractError::MissingField(
439 "official_stack.profiles",
440 ));
441 }
442
443 let points_by_id: HashMap<_, _> = inventory
444 .extension_points
445 .iter()
446 .map(|point| (point.id.as_str(), point))
447 .collect();
448
449 let mut component_ids = HashSet::new();
450 for component in &package.components {
451 ensure_non_empty(&component.id, "official_stack.components.id")?;
452 ensure_non_empty(&component.name, "official_stack.components.name")?;
453 ensure_non_empty(
454 &component.crate_path,
455 "official_stack.components.crate_path",
456 )?;
457 if !component_ids.insert(component.id.as_str()) {
458 return Err(ExtensionContractError::DuplicateValue(component.id.clone()));
459 }
460 if component.extension_point_ids.is_empty() {
461 return Err(ExtensionContractError::MissingField(
462 "official_stack.components.extension_point_ids",
463 ));
464 }
465 ensure_unique_strings(
466 &component.extension_point_ids,
467 "official_stack.components.extension_point_ids",
468 )?;
469 for point_id in &component.extension_point_ids {
470 if !points_by_id.contains_key(point_id.as_str()) {
471 return Err(ExtensionContractError::UnknownReference(point_id.clone()));
472 }
473 }
474 }
475
476 let components_by_id: HashMap<_, _> = package
477 .components
478 .iter()
479 .map(|component| (component.id.as_str(), component))
480 .collect();
481 let mut profile_ids = HashSet::new();
482 for profile in &package.profiles {
483 ensure_non_empty(&profile.id, "official_stack.profiles.id")?;
484 ensure_non_empty(&profile.name, "official_stack.profiles.name")?;
485 ensure_non_empty(&profile.description, "official_stack.profiles.description")?;
486 if !profile_ids.insert(profile.id.as_str()) {
487 return Err(ExtensionContractError::DuplicateValue(profile.id.clone()));
488 }
489 if profile.component_ids.is_empty() {
490 return Err(ExtensionContractError::MissingField(
491 "official_stack.profiles.component_ids",
492 ));
493 }
494 ensure_unique_strings(
495 &profile.component_ids,
496 "official_stack.profiles.component_ids",
497 )?;
498
499 let mut covered_points = HashSet::new();
500 for component_id in &profile.component_ids {
501 let component = components_by_id
502 .get(component_id.as_str())
503 .ok_or_else(|| ExtensionContractError::UnknownReference(component_id.clone()))?;
504 for point_id in &component.extension_point_ids {
505 if !covered_points.insert(point_id.as_str()) {
506 return Err(ExtensionContractError::InvalidProfile(format!(
507 "profile {} selects multiple components for extension point {}",
508 profile.id, point_id
509 )));
510 }
511 }
512 }
513 }
514
515 for point in &inventory.extension_points {
516 for component_id in &point.official_component_ids {
517 if !components_by_id.contains_key(component_id.as_str()) {
518 return Err(ExtensionContractError::UnknownReference(
519 component_id.clone(),
520 ));
521 }
522 }
523 }
524
525 Ok(())
526}
527
528pub fn validate_extension_manifest(
529 manifest: &ChioExtensionManifest,
530) -> Result<(), ExtensionContractError> {
531 if manifest.schema != CHIO_EXTENSION_MANIFEST_SCHEMA {
532 return Err(ExtensionContractError::UnsupportedSchema(
533 manifest.schema.clone(),
534 ));
535 }
536 ensure_non_empty(&manifest.extension_id, "extension_manifest.extension_id")?;
537 ensure_non_empty(&manifest.display_name, "extension_manifest.display_name")?;
538 ensure_non_empty(&manifest.version, "extension_manifest.version")?;
539 ensure_non_empty(
540 &manifest.extension_point_id,
541 "extension_manifest.extension_point_id",
542 )?;
543 if manifest.capabilities.is_empty() {
544 return Err(ExtensionContractError::MissingField(
545 "extension_manifest.capabilities",
546 ));
547 }
548 if manifest.supported_profiles.is_empty() {
549 return Err(ExtensionContractError::MissingField(
550 "extension_manifest.supported_profiles",
551 ));
552 }
553 ensure_unique_strings(&manifest.capabilities, "extension_manifest.capabilities")?;
554 ensure_unique_strings(
555 &manifest.supported_profiles,
556 "extension_manifest.supported_profiles",
557 )?;
558
559 ensure_non_empty(
560 &manifest.compatibility.chio_contract_version,
561 "extension_manifest.compatibility.chio_contract_version",
562 )?;
563 ensure_non_empty(
564 &manifest.compatibility.official_stack_package_id,
565 "extension_manifest.compatibility.official_stack_package_id",
566 )?;
567 if manifest.compatibility.supported_component_ids.is_empty() {
568 return Err(ExtensionContractError::MissingField(
569 "extension_manifest.compatibility.supported_component_ids",
570 ));
571 }
572 if manifest.compatibility.supported_contract_schemas.is_empty() {
573 return Err(ExtensionContractError::MissingField(
574 "extension_manifest.compatibility.supported_contract_schemas",
575 ));
576 }
577 ensure_unique_strings(
578 &manifest.compatibility.supported_component_ids,
579 "extension_manifest.compatibility.supported_component_ids",
580 )?;
581 ensure_unique_strings(
582 &manifest.compatibility.supported_contract_schemas,
583 "extension_manifest.compatibility.supported_contract_schemas",
584 )?;
585 if !manifest
586 .compatibility
587 .supported_contract_schemas
588 .iter()
589 .any(|schema| schema == CHIO_EXTENSION_MANIFEST_SCHEMA)
590 {
591 return Err(ExtensionContractError::InvalidGuardrail(
592 "extension manifest compatibility must list chio.extension-manifest.v1".to_string(),
593 ));
594 }
595
596 ensure_unique_copy_values(
597 &manifest.runtime.allowed_privileges,
598 "extension_manifest.runtime.allowed_privileges",
599 )?;
600 if manifest.runtime.allows_truth_mutation {
601 return Err(ExtensionContractError::InvalidGuardrail(
602 "extensions must not claim truth mutation".to_string(),
603 ));
604 }
605 if manifest.runtime.allows_trust_widening {
606 return Err(ExtensionContractError::InvalidGuardrail(
607 "extensions must not claim trust widening".to_string(),
608 ));
609 }
610 if manifest.runtime.evidence_mode != ExtensionEvidenceMode::None {
611 if !manifest.runtime.requires_subject_binding {
612 return Err(ExtensionContractError::InvalidGuardrail(
613 "evidence-capable extensions must require subject binding".to_string(),
614 ));
615 }
616 if !manifest.runtime.requires_signer_verification {
617 return Err(ExtensionContractError::InvalidGuardrail(
618 "evidence-capable extensions must require signer verification".to_string(),
619 ));
620 }
621 if !manifest.runtime.requires_freshness_check {
622 return Err(ExtensionContractError::InvalidGuardrail(
623 "evidence-capable extensions must require freshness checks".to_string(),
624 ));
625 }
626 if !manifest.runtime.requires_local_policy_activation {
627 return Err(ExtensionContractError::InvalidGuardrail(
628 "evidence-capable extensions must require local policy activation".to_string(),
629 ));
630 }
631 }
632
633 Ok(())
634}
635
636pub fn negotiate_extension(
637 inventory: &ChioExtensionInventory,
638 package: &OfficialStackPackage,
639 manifest: &ChioExtensionManifest,
640) -> ExtensionNegotiationReport {
641 let mut reasons = Vec::new();
642
643 if let Err(error) = validate_extension_inventory(inventory) {
644 reasons.push(negotiation_rejection(
645 ExtensionNegotiationRejectionCode::MalformedInventory,
646 error.to_string(),
647 ));
648 }
649 if let Err(error) = validate_official_stack_package(inventory, package) {
650 reasons.push(negotiation_rejection(
651 ExtensionNegotiationRejectionCode::MalformedOfficialStack,
652 error.to_string(),
653 ));
654 }
655 if let Err(error) = validate_extension_manifest(manifest) {
656 reasons.push(negotiation_rejection(
657 ExtensionNegotiationRejectionCode::MalformedManifest,
658 error.to_string(),
659 ));
660 }
661 if !reasons.is_empty() {
662 return ExtensionNegotiationReport {
663 schema: CHIO_EXTENSION_NEGOTIATION_SCHEMA.to_string(),
664 official_stack_package_id: package.package_id.clone(),
665 extension_id: manifest.extension_id.clone(),
666 extension_point_id: manifest.extension_point_id.clone(),
667 outcome: ExtensionNegotiationOutcome::Rejected,
668 reasons,
669 };
670 }
671
672 let points_by_id: HashMap<_, _> = inventory
673 .extension_points
674 .iter()
675 .map(|point| (point.id.as_str(), point))
676 .collect();
677 let profiles: HashSet<_> = package
678 .profiles
679 .iter()
680 .map(|profile| profile.id.as_str())
681 .collect();
682 let components: HashSet<_> = package
683 .components
684 .iter()
685 .map(|component| component.id.as_str())
686 .collect();
687
688 if package.package_id != manifest.compatibility.official_stack_package_id {
689 reasons.push(negotiation_rejection(
690 ExtensionNegotiationRejectionCode::UnsupportedOfficialStack,
691 format!(
692 "manifest targets {}, expected {}",
693 manifest.compatibility.official_stack_package_id, package.package_id
694 ),
695 ));
696 }
697 if package.chio_contract_version != manifest.compatibility.chio_contract_version {
698 reasons.push(negotiation_rejection(
699 ExtensionNegotiationRejectionCode::UnsupportedChioContract,
700 format!(
701 "manifest targets Chio {}, expected {}",
702 manifest.compatibility.chio_contract_version, package.chio_contract_version
703 ),
704 ));
705 }
706
707 let Some(point) = points_by_id.get(manifest.extension_point_id.as_str()) else {
708 reasons.push(negotiation_rejection(
709 ExtensionNegotiationRejectionCode::UnknownExtensionPoint,
710 format!(
711 "extension point {} is not registered",
712 manifest.extension_point_id
713 ),
714 ));
715 return ExtensionNegotiationReport {
716 schema: CHIO_EXTENSION_NEGOTIATION_SCHEMA.to_string(),
717 official_stack_package_id: package.package_id.clone(),
718 extension_id: manifest.extension_id.clone(),
719 extension_point_id: manifest.extension_point_id.clone(),
720 outcome: ExtensionNegotiationOutcome::Rejected,
721 reasons,
722 };
723 };
724
725 if manifest.distribution != ExtensionDistribution::OfficialFirstParty
726 && !point.custom_implementations_allowed
727 {
728 reasons.push(negotiation_rejection(
729 ExtensionNegotiationRejectionCode::OfficialOnlyPoint,
730 format!(
731 "extension point {} is reserved for official components",
732 point.id
733 ),
734 ));
735 }
736 if manifest.distribution != ExtensionDistribution::OfficialFirstParty
737 && point.stability == ExtensionStability::Internal
738 {
739 reasons.push(negotiation_rejection(
740 ExtensionNegotiationRejectionCode::InternalOnlyPoint,
741 format!("extension point {} is internal-only", point.id),
742 ));
743 }
744
745 for profile_id in &manifest.supported_profiles {
746 if !profiles.contains(profile_id.as_str()) {
747 reasons.push(negotiation_rejection(
748 ExtensionNegotiationRejectionCode::UnsupportedProfile,
749 format!(
750 "profile {} is not part of {}",
751 profile_id, package.package_id
752 ),
753 ));
754 }
755 }
756 for component_id in &manifest.compatibility.supported_component_ids {
757 if !components.contains(component_id.as_str()) {
758 reasons.push(negotiation_rejection(
759 ExtensionNegotiationRejectionCode::UnsupportedComponent,
760 format!(
761 "component {} is not part of {}",
762 component_id, package.package_id
763 ),
764 ));
765 }
766 }
767 if !manifest
768 .compatibility
769 .supported_component_ids
770 .iter()
771 .any(|component_id| {
772 point
773 .official_component_ids
774 .iter()
775 .any(|official| official == component_id)
776 })
777 {
778 reasons.push(negotiation_rejection(
779 ExtensionNegotiationRejectionCode::UnsupportedComponent,
780 format!(
781 "extension {} does not target an official component for point {}",
782 manifest.extension_id, point.id
783 ),
784 ));
785 }
786
787 if !point
788 .allowed_isolations
789 .contains(&manifest.runtime.isolation)
790 {
791 reasons.push(negotiation_rejection(
792 ExtensionNegotiationRejectionCode::UnsupportedIsolation,
793 format!(
794 "extension point {} does not allow {:?} isolation",
795 point.id, manifest.runtime.isolation
796 ),
797 ));
798 }
799 if !point
800 .allowed_evidence_modes
801 .contains(&manifest.runtime.evidence_mode)
802 {
803 reasons.push(negotiation_rejection(
804 ExtensionNegotiationRejectionCode::UnsupportedEvidenceMode,
805 format!(
806 "extension point {} does not allow {:?} evidence mode",
807 point.id, manifest.runtime.evidence_mode
808 ),
809 ));
810 }
811 for privilege in &manifest.runtime.allowed_privileges {
812 if !point.allowed_privileges.contains(privilege) {
813 reasons.push(negotiation_rejection(
814 ExtensionNegotiationRejectionCode::UnsupportedPrivilege,
815 format!(
816 "extension point {} does not allow {:?}",
817 point.id, privilege
818 ),
819 ));
820 }
821 }
822
823 if point.policy_activation_required && !manifest.runtime.requires_local_policy_activation {
824 reasons.push(negotiation_rejection(
825 ExtensionNegotiationRejectionCode::LocalPolicyActivationRequired,
826 format!(
827 "extension point {} requires local policy activation",
828 point.id
829 ),
830 ));
831 }
832 if manifest.runtime.evidence_mode != ExtensionEvidenceMode::None
833 && !manifest.runtime.requires_subject_binding
834 {
835 reasons.push(negotiation_rejection(
836 ExtensionNegotiationRejectionCode::MissingSubjectBinding,
837 format!(
838 "extension {} omitted subject binding",
839 manifest.extension_id
840 ),
841 ));
842 }
843 if manifest.runtime.evidence_mode != ExtensionEvidenceMode::None
844 && !manifest.runtime.requires_signer_verification
845 {
846 reasons.push(negotiation_rejection(
847 ExtensionNegotiationRejectionCode::MissingSignerVerification,
848 format!(
849 "extension {} omitted signer verification",
850 manifest.extension_id
851 ),
852 ));
853 }
854 if manifest.runtime.evidence_mode != ExtensionEvidenceMode::None
855 && !manifest.runtime.requires_freshness_check
856 {
857 reasons.push(negotiation_rejection(
858 ExtensionNegotiationRejectionCode::MissingFreshnessCheck,
859 format!(
860 "extension {} omitted freshness checks",
861 manifest.extension_id
862 ),
863 ));
864 }
865 if manifest.runtime.allows_truth_mutation {
866 reasons.push(negotiation_rejection(
867 ExtensionNegotiationRejectionCode::TruthMutationNotAllowed,
868 format!("extension {} claims truth mutation", manifest.extension_id),
869 ));
870 }
871 if manifest.runtime.allows_trust_widening {
872 reasons.push(negotiation_rejection(
873 ExtensionNegotiationRejectionCode::TrustWideningNotAllowed,
874 format!("extension {} claims trust widening", manifest.extension_id),
875 ));
876 }
877
878 ExtensionNegotiationReport {
879 schema: CHIO_EXTENSION_NEGOTIATION_SCHEMA.to_string(),
880 official_stack_package_id: package.package_id.clone(),
881 extension_id: manifest.extension_id.clone(),
882 extension_point_id: manifest.extension_point_id.clone(),
883 outcome: if reasons.is_empty() {
884 ExtensionNegotiationOutcome::Accepted
885 } else {
886 ExtensionNegotiationOutcome::Rejected
887 },
888 reasons,
889 }
890}
891
892pub fn validate_qualification_matrix(
893 matrix: &ExtensionQualificationMatrix,
894) -> Result<(), ExtensionContractError> {
895 if matrix.schema != CHIO_EXTENSION_QUALIFICATION_MATRIX_SCHEMA {
896 return Err(ExtensionContractError::UnsupportedSchema(
897 matrix.schema.clone(),
898 ));
899 }
900 ensure_non_empty(
901 &matrix.official_stack_package_id,
902 "qualification_matrix.official_stack_package_id",
903 )?;
904 ensure_non_empty(
905 &matrix.chio_contract_version,
906 "qualification_matrix.chio_contract_version",
907 )?;
908 if matrix.cases.is_empty() {
909 return Err(ExtensionContractError::MissingField(
910 "qualification_matrix.cases",
911 ));
912 }
913
914 let mut case_ids = HashSet::new();
915 for case in &matrix.cases {
916 ensure_non_empty(&case.id, "qualification_matrix.case.id")?;
917 ensure_non_empty(&case.name, "qualification_matrix.case.name")?;
918 ensure_non_empty(
919 &case.extension_point_id,
920 "qualification_matrix.case.extension_point_id",
921 )?;
922 ensure_non_empty(
923 &case.supported_component_id,
924 "qualification_matrix.case.supported_component_id",
925 )?;
926 ensure_non_empty(
927 &case.candidate_extension_id,
928 "qualification_matrix.case.candidate_extension_id",
929 )?;
930 if !case_ids.insert(case.id.as_str()) {
931 return Err(ExtensionContractError::DuplicateValue(case.id.clone()));
932 }
933 if case.invariants.is_empty() {
934 return Err(ExtensionContractError::InvalidQualificationCase(format!(
935 "case {} must record at least one invariant",
936 case.id
937 )));
938 }
939 ensure_unique_copy_values(&case.invariants, "qualification_matrix.case.invariants")?;
940 ensure_unique_copy_values(
941 &case.rejection_codes,
942 "qualification_matrix.case.rejection_codes",
943 )?;
944 let must_have_rejections = case.expected_outcome == QualificationOutcome::FailClosed
945 || case.observed_outcome == QualificationOutcome::FailClosed;
946 if must_have_rejections && case.rejection_codes.is_empty() {
947 return Err(ExtensionContractError::InvalidQualificationCase(format!(
948 "case {} must record rejection codes for fail-closed outcomes",
949 case.id
950 )));
951 }
952 if !must_have_rejections && !case.rejection_codes.is_empty() {
953 return Err(ExtensionContractError::InvalidQualificationCase(format!(
954 "case {} recorded rejection codes for a passing outcome",
955 case.id
956 )));
957 }
958 }
959
960 Ok(())
961}
962
963fn ensure_non_empty(value: &str, field: &'static str) -> Result<(), ExtensionContractError> {
964 if value.trim().is_empty() {
965 Err(ExtensionContractError::MissingField(field))
966 } else {
967 Ok(())
968 }
969}
970
971fn ensure_unique_strings(
972 values: &[String],
973 field: &'static str,
974) -> Result<(), ExtensionContractError> {
975 let mut seen = HashSet::new();
976 for value in values {
977 ensure_non_empty(value, field)?;
978 if !seen.insert(value.as_str()) {
979 return Err(ExtensionContractError::DuplicateValue(value.clone()));
980 }
981 }
982 Ok(())
983}
984
985fn ensure_unique_copy_values<T>(
986 values: &[T],
987 field: &'static str,
988) -> Result<(), ExtensionContractError>
989where
990 T: Eq + std::hash::Hash + Copy + std::fmt::Debug,
991{
992 let mut seen = HashSet::new();
993 for value in values {
994 if !seen.insert(*value) {
995 return Err(ExtensionContractError::DuplicateValue(format!(
996 "{field}:{value:?}"
997 )));
998 }
999 }
1000 Ok(())
1001}
1002
1003fn negotiation_rejection(
1004 code: ExtensionNegotiationRejectionCode,
1005 detail: impl Into<String>,
1006) -> ExtensionNegotiationRejection {
1007 ExtensionNegotiationRejection {
1008 code,
1009 detail: detail.into(),
1010 }
1011}
1012
1013#[cfg(test)]
1014#[allow(clippy::unwrap_used, clippy::expect_used)]
1015mod tests {
1016 use super::*;
1017
1018 fn sample_inventory() -> ChioExtensionInventory {
1019 ChioExtensionInventory {
1020 schema: CHIO_EXTENSION_INVENTORY_SCHEMA.to_string(),
1021 chio_contract_version: "2.0".to_string(),
1022 canonical_truth: vec![CanonicalTruthSurface {
1023 id: "chio.canonical.receipt".to_string(),
1024 name: "Signed receipts and checkpoints".to_string(),
1025 crate_path: "crates/chio-core/src/receipt.rs".to_string(),
1026 contract_kind: CanonicalContractKind::Receipt,
1027 artifact_schemas: vec!["chio.receipt.v1".to_string(), "chio.checkpoint.v1".to_string()],
1028 notes: "Extensions may project evidence around receipts, but they must not mutate signed receipt or checkpoint truth."
1029 .to_string(),
1030 extensions_may_write: false,
1031 }],
1032 extension_points: vec![
1033 ChioExtensionPoint {
1034 id: "chio.kernel.receipt_store".to_string(),
1035 name: "Receipt store backend".to_string(),
1036 point_kind: ExtensionPointKind::Store,
1037 owner: "kernel".to_string(),
1038 contract_path: "crates/chio-kernel/src/receipt_store.rs::ReceiptStore".to_string(),
1039 stability: ExtensionStability::Supported,
1040 allowed_isolations: vec![
1041 ExtensionIsolation::InProcess,
1042 ExtensionIsolation::RemoteService,
1043 ],
1044 allowed_evidence_modes: vec![ExtensionEvidenceMode::None],
1045 allowed_privileges: vec![
1046 ExtensionPrivilege::FilesystemRead,
1047 ExtensionPrivilege::FilesystemWrite,
1048 ExtensionPrivilege::NetworkEgress,
1049 ],
1050 custom_implementations_allowed: true,
1051 policy_activation_required: false,
1052 official_component_ids: vec![
1053 "chio.sqlite-receipt-store".to_string(),
1054 "chio.remote-receipt-store".to_string(),
1055 ],
1056 },
1057 ChioExtensionPoint {
1058 id: "chio.kernel.tool_server_connection".to_string(),
1059 name: "Tool server connection".to_string(),
1060 point_kind: ExtensionPointKind::ToolServerConnection,
1061 owner: "kernel".to_string(),
1062 contract_path: "crates/chio-kernel/src/runtime.rs::ToolServerConnection".to_string(),
1063 stability: ExtensionStability::Supported,
1064 allowed_isolations: vec![
1065 ExtensionIsolation::InProcess,
1066 ExtensionIsolation::Subprocess,
1067 ExtensionIsolation::RemoteService,
1068 ],
1069 allowed_evidence_modes: vec![
1070 ExtensionEvidenceMode::None,
1071 ExtensionEvidenceMode::ImportOnly,
1072 ExtensionEvidenceMode::DispatchOnly,
1073 ExtensionEvidenceMode::ImportAndDispatch,
1074 ],
1075 allowed_privileges: vec![
1076 ExtensionPrivilege::FilesystemRead,
1077 ExtensionPrivilege::NetworkEgress,
1078 ExtensionPrivilege::ProcessExecution,
1079 ExtensionPrivilege::OperatorSecrets,
1080 ],
1081 custom_implementations_allowed: true,
1082 policy_activation_required: true,
1083 official_component_ids: vec!["chio.native-chio-service".to_string()],
1084 },
1085 ],
1086 }
1087 }
1088
1089 fn sample_official_stack() -> OfficialStackPackage {
1090 OfficialStackPackage {
1091 schema: CHIO_OFFICIAL_STACK_SCHEMA.to_string(),
1092 package_id: "chio.official-stack".to_string(),
1093 version: "0.1.0".to_string(),
1094 chio_contract_version: "2.0".to_string(),
1095 components: vec![
1096 OfficialStackComponent {
1097 id: "chio.sqlite-receipt-store".to_string(),
1098 name: "SQLite receipt store".to_string(),
1099 extension_point_ids: vec!["chio.kernel.receipt_store".to_string()],
1100 crate_path: "crates/chio-store-sqlite/src/receipt_store.rs::SqliteReceiptStore"
1101 .to_string(),
1102 implementation_source: OfficialImplementationSource::FirstParty,
1103 },
1104 OfficialStackComponent {
1105 id: "chio.remote-receipt-store".to_string(),
1106 name: "Remote receipt store".to_string(),
1107 extension_point_ids: vec!["chio.kernel.receipt_store".to_string()],
1108 crate_path: "crates/chio-cli/src/trust_control.rs::RemoteReceiptStore"
1109 .to_string(),
1110 implementation_source: OfficialImplementationSource::FirstParty,
1111 },
1112 OfficialStackComponent {
1113 id: "chio.native-chio-service".to_string(),
1114 name: "Native Chio service".to_string(),
1115 extension_point_ids: vec!["chio.kernel.tool_server_connection".to_string()],
1116 crate_path: "crates/chio-mcp-adapter/src/native.rs::NativeChioService"
1117 .to_string(),
1118 implementation_source: OfficialImplementationSource::FirstParty,
1119 },
1120 ],
1121 profiles: vec![
1122 OfficialStackProfile {
1123 id: "local_default".to_string(),
1124 name: "Local default".to_string(),
1125 description: "Local stores with native Chio service".to_string(),
1126 component_ids: vec![
1127 "chio.sqlite-receipt-store".to_string(),
1128 "chio.native-chio-service".to_string(),
1129 ],
1130 },
1131 OfficialStackProfile {
1132 id: "shared_control_plane".to_string(),
1133 name: "Shared control plane".to_string(),
1134 description: "Remote store components with first-party service adapters"
1135 .to_string(),
1136 component_ids: vec![
1137 "chio.remote-receipt-store".to_string(),
1138 "chio.native-chio-service".to_string(),
1139 ],
1140 },
1141 ],
1142 }
1143 }
1144
1145 fn sample_manifest() -> ChioExtensionManifest {
1146 ChioExtensionManifest {
1147 schema: CHIO_EXTENSION_MANIFEST_SCHEMA.to_string(),
1148 extension_id: "sample.pg-receipt-store".to_string(),
1149 display_name: "Sample Postgres Receipt Store".to_string(),
1150 version: "1.0.0".to_string(),
1151 distribution: ExtensionDistribution::ThirdPartyCustom,
1152 extension_point_id: "chio.kernel.receipt_store".to_string(),
1153 capabilities: vec![
1154 "receipt_append".to_string(),
1155 "receipt_query".to_string(),
1156 "checkpoint_replay_safe".to_string(),
1157 ],
1158 supported_profiles: vec!["shared_control_plane".to_string()],
1159 compatibility: ExtensionCompatibility {
1160 chio_contract_version: "2.0".to_string(),
1161 official_stack_package_id: "chio.official-stack".to_string(),
1162 supported_component_ids: vec!["chio.remote-receipt-store".to_string()],
1163 supported_contract_schemas: vec![
1164 CHIO_EXTENSION_MANIFEST_SCHEMA.to_string(),
1165 "chio.receipt.v1".to_string(),
1166 "chio.checkpoint.v1".to_string(),
1167 ],
1168 },
1169 runtime: ExtensionRuntimeEnvelope {
1170 isolation: ExtensionIsolation::RemoteService,
1171 allowed_privileges: vec![
1172 ExtensionPrivilege::NetworkEgress,
1173 ExtensionPrivilege::FilesystemRead,
1174 ],
1175 evidence_mode: ExtensionEvidenceMode::None,
1176 requires_subject_binding: false,
1177 requires_signer_verification: false,
1178 requires_freshness_check: false,
1179 requires_local_policy_activation: false,
1180 allows_truth_mutation: false,
1181 allows_trust_widening: false,
1182 },
1183 }
1184 }
1185
1186 fn sample_qualification_matrix() -> ExtensionQualificationMatrix {
1187 ExtensionQualificationMatrix {
1188 schema: CHIO_EXTENSION_QUALIFICATION_MATRIX_SCHEMA.to_string(),
1189 official_stack_package_id: "chio.official-stack".to_string(),
1190 chio_contract_version: "2.0".to_string(),
1191 cases: vec![ExtensionQualificationCase {
1192 id: "tool-server-pass".to_string(),
1193 name: "Supported tool-server extension remains bounded".to_string(),
1194 extension_point_id: "chio.kernel.tool_server_connection".to_string(),
1195 supported_component_id: "chio.native-chio-service".to_string(),
1196 candidate_extension_id: "sample.tool-server".to_string(),
1197 mode: QualificationMode::OfficialToCustom,
1198 expected_outcome: QualificationOutcome::Pass,
1199 observed_outcome: QualificationOutcome::Pass,
1200 rejection_codes: vec![],
1201 invariants: vec![
1202 QualificationInvariant::PreservesCanonicalTruth,
1203 QualificationInvariant::RequiresLocalPolicyActivation,
1204 ],
1205 }],
1206 }
1207 }
1208
1209 fn rejection_codes(
1210 report: &ExtensionNegotiationReport,
1211 ) -> HashSet<ExtensionNegotiationRejectionCode> {
1212 report.reasons.iter().map(|reason| reason.code).collect()
1213 }
1214
1215 #[test]
1216 fn rejects_duplicate_inventory_ids() {
1217 let mut inventory = sample_inventory();
1218 inventory
1219 .extension_points
1220 .push(inventory.extension_points[0].clone());
1221 assert!(matches!(
1222 validate_extension_inventory(&inventory),
1223 Err(ExtensionContractError::DuplicateValue(_))
1224 ));
1225 }
1226
1227 #[test]
1228 fn inventory_validation_rejects_remaining_shape_and_guardrail_errors() {
1229 let mut inventory = sample_inventory();
1230 inventory.schema = "chio.extension-inventory.v9".to_string();
1231 assert!(matches!(
1232 validate_extension_inventory(&inventory),
1233 Err(ExtensionContractError::UnsupportedSchema(_))
1234 ));
1235
1236 let mut inventory = sample_inventory();
1237 inventory.chio_contract_version.clear();
1238 assert!(matches!(
1239 validate_extension_inventory(&inventory),
1240 Err(ExtensionContractError::MissingField(
1241 "chio_contract_version"
1242 ))
1243 ));
1244
1245 let mut inventory = sample_inventory();
1246 inventory.canonical_truth.clear();
1247 assert!(matches!(
1248 validate_extension_inventory(&inventory),
1249 Err(ExtensionContractError::MissingField("canonical_truth"))
1250 ));
1251
1252 let mut inventory = sample_inventory();
1253 inventory.extension_points.clear();
1254 assert!(matches!(
1255 validate_extension_inventory(&inventory),
1256 Err(ExtensionContractError::MissingField("extension_points"))
1257 ));
1258
1259 let mut inventory = sample_inventory();
1260 inventory.canonical_truth[0].artifact_schemas.clear();
1261 assert!(matches!(
1262 validate_extension_inventory(&inventory),
1263 Err(ExtensionContractError::MissingField(
1264 "canonical_truth.artifact_schemas"
1265 ))
1266 ));
1267
1268 let mut inventory = sample_inventory();
1269 inventory.canonical_truth[0].extensions_may_write = true;
1270 assert!(matches!(
1271 validate_extension_inventory(&inventory),
1272 Err(ExtensionContractError::InvalidGuardrail(_))
1273 ));
1274
1275 let mut inventory = sample_inventory();
1276 inventory.canonical_truth[0]
1277 .artifact_schemas
1278 .push("chio.receipt.v1".to_string());
1279 assert!(matches!(
1280 validate_extension_inventory(&inventory),
1281 Err(ExtensionContractError::DuplicateValue(_))
1282 ));
1283
1284 let mut inventory = sample_inventory();
1285 inventory.extension_points[0].allowed_isolations.clear();
1286 assert!(matches!(
1287 validate_extension_inventory(&inventory),
1288 Err(ExtensionContractError::MissingField(
1289 "extension_points.allowed_isolations"
1290 ))
1291 ));
1292
1293 let mut inventory = sample_inventory();
1294 inventory.extension_points[0].allowed_evidence_modes.clear();
1295 assert!(matches!(
1296 validate_extension_inventory(&inventory),
1297 Err(ExtensionContractError::MissingField(
1298 "extension_points.allowed_evidence_modes"
1299 ))
1300 ));
1301
1302 let mut inventory = sample_inventory();
1303 inventory.extension_points[0].allowed_privileges.clear();
1304 assert!(matches!(
1305 validate_extension_inventory(&inventory),
1306 Err(ExtensionContractError::MissingField(
1307 "extension_points.allowed_privileges"
1308 ))
1309 ));
1310
1311 let mut inventory = sample_inventory();
1312 inventory.extension_points[0].official_component_ids.clear();
1313 assert!(matches!(
1314 validate_extension_inventory(&inventory),
1315 Err(ExtensionContractError::MissingField(
1316 "extension_points.official_component_ids"
1317 ))
1318 ));
1319
1320 let mut inventory = sample_inventory();
1321 inventory.extension_points[0]
1322 .allowed_isolations
1323 .push(ExtensionIsolation::InProcess);
1324 assert!(matches!(
1325 validate_extension_inventory(&inventory),
1326 Err(ExtensionContractError::DuplicateValue(_))
1327 ));
1328
1329 let mut inventory = sample_inventory();
1330 inventory.extension_points[0]
1331 .allowed_evidence_modes
1332 .push(ExtensionEvidenceMode::None);
1333 assert!(matches!(
1334 validate_extension_inventory(&inventory),
1335 Err(ExtensionContractError::DuplicateValue(_))
1336 ));
1337
1338 let mut inventory = sample_inventory();
1339 inventory.extension_points[0]
1340 .allowed_privileges
1341 .push(ExtensionPrivilege::FilesystemRead);
1342 assert!(matches!(
1343 validate_extension_inventory(&inventory),
1344 Err(ExtensionContractError::DuplicateValue(_))
1345 ));
1346
1347 let mut inventory = sample_inventory();
1348 inventory.extension_points[0]
1349 .official_component_ids
1350 .push("chio.sqlite-receipt-store".to_string());
1351 assert!(matches!(
1352 validate_extension_inventory(&inventory),
1353 Err(ExtensionContractError::DuplicateValue(_))
1354 ));
1355
1356 let mut inventory = sample_inventory();
1357 inventory.extension_points[1].allowed_evidence_modes = vec![ExtensionEvidenceMode::None];
1358 assert!(matches!(
1359 validate_extension_inventory(&inventory),
1360 Err(ExtensionContractError::InvalidGuardrail(_))
1361 ));
1362 }
1363
1364 #[test]
1365 fn accepts_supported_custom_store_extension() {
1366 let report = negotiate_extension(
1367 &sample_inventory(),
1368 &sample_official_stack(),
1369 &sample_manifest(),
1370 );
1371 assert_eq!(report.outcome, ExtensionNegotiationOutcome::Accepted);
1372 assert!(report.reasons.is_empty());
1373 }
1374
1375 #[test]
1376 fn official_stack_validation_rejects_remaining_reference_and_profile_errors() {
1377 let inventory = sample_inventory();
1378
1379 let mut package = sample_official_stack();
1380 package.schema = "chio.official-stack.v9".to_string();
1381 assert!(matches!(
1382 validate_official_stack_package(&inventory, &package),
1383 Err(ExtensionContractError::UnsupportedSchema(_))
1384 ));
1385
1386 let mut package = sample_official_stack();
1387 package.package_id.clear();
1388 assert!(matches!(
1389 validate_official_stack_package(&inventory, &package),
1390 Err(ExtensionContractError::MissingField(
1391 "official_stack.package_id"
1392 ))
1393 ));
1394
1395 let mut package = sample_official_stack();
1396 package.components.clear();
1397 assert!(matches!(
1398 validate_official_stack_package(&inventory, &package),
1399 Err(ExtensionContractError::MissingField(
1400 "official_stack.components"
1401 ))
1402 ));
1403
1404 let mut package = sample_official_stack();
1405 package.profiles.clear();
1406 assert!(matches!(
1407 validate_official_stack_package(&inventory, &package),
1408 Err(ExtensionContractError::MissingField(
1409 "official_stack.profiles"
1410 ))
1411 ));
1412
1413 let mut package = sample_official_stack();
1414 package.components[0].extension_point_ids.clear();
1415 assert!(matches!(
1416 validate_official_stack_package(&inventory, &package),
1417 Err(ExtensionContractError::MissingField(
1418 "official_stack.components.extension_point_ids"
1419 ))
1420 ));
1421
1422 let mut package = sample_official_stack();
1423 package.components[0]
1424 .extension_point_ids
1425 .push("chio.kernel.receipt_store".to_string());
1426 assert!(matches!(
1427 validate_official_stack_package(&inventory, &package),
1428 Err(ExtensionContractError::DuplicateValue(_))
1429 ));
1430
1431 let mut package = sample_official_stack();
1432 package.components[0].extension_point_ids = vec!["chio.kernel.unknown".to_string()];
1433 assert!(matches!(
1434 validate_official_stack_package(&inventory, &package),
1435 Err(ExtensionContractError::UnknownReference(_))
1436 ));
1437
1438 let mut package = sample_official_stack();
1439 package.components[1].id = package.components[0].id.clone();
1440 assert!(matches!(
1441 validate_official_stack_package(&inventory, &package),
1442 Err(ExtensionContractError::DuplicateValue(_))
1443 ));
1444
1445 let mut package = sample_official_stack();
1446 package.profiles[0].component_ids.clear();
1447 assert!(matches!(
1448 validate_official_stack_package(&inventory, &package),
1449 Err(ExtensionContractError::MissingField(
1450 "official_stack.profiles.component_ids"
1451 ))
1452 ));
1453
1454 let mut package = sample_official_stack();
1455 package.profiles[1].id = package.profiles[0].id.clone();
1456 assert!(matches!(
1457 validate_official_stack_package(&inventory, &package),
1458 Err(ExtensionContractError::DuplicateValue(_))
1459 ));
1460
1461 let mut package = sample_official_stack();
1462 package.profiles[0]
1463 .component_ids
1464 .push("chio.sqlite-receipt-store".to_string());
1465 assert!(matches!(
1466 validate_official_stack_package(&inventory, &package),
1467 Err(ExtensionContractError::DuplicateValue(_))
1468 ));
1469
1470 let mut package = sample_official_stack();
1471 package.profiles[0].component_ids = vec!["chio.unknown-component".to_string()];
1472 assert!(matches!(
1473 validate_official_stack_package(&inventory, &package),
1474 Err(ExtensionContractError::UnknownReference(_))
1475 ));
1476
1477 let mut package = sample_official_stack();
1478 package.profiles[0]
1479 .component_ids
1480 .push("chio.remote-receipt-store".to_string());
1481 assert!(matches!(
1482 validate_official_stack_package(&inventory, &package),
1483 Err(ExtensionContractError::InvalidProfile(_))
1484 ));
1485
1486 let mut inventory = sample_inventory();
1487 inventory.extension_points[0]
1488 .official_component_ids
1489 .push("chio.unknown-component".to_string());
1490 assert!(matches!(
1491 validate_official_stack_package(&inventory, &sample_official_stack()),
1492 Err(ExtensionContractError::UnknownReference(_))
1493 ));
1494 }
1495
1496 #[test]
1497 fn manifest_validation_rejects_remaining_shape_and_runtime_guardrails() {
1498 let mut manifest = sample_manifest();
1499 manifest.schema = "chio.extension-manifest.v9".to_string();
1500 assert!(matches!(
1501 validate_extension_manifest(&manifest),
1502 Err(ExtensionContractError::UnsupportedSchema(_))
1503 ));
1504
1505 let mut manifest = sample_manifest();
1506 manifest.extension_id.clear();
1507 assert!(matches!(
1508 validate_extension_manifest(&manifest),
1509 Err(ExtensionContractError::MissingField(
1510 "extension_manifest.extension_id"
1511 ))
1512 ));
1513
1514 let mut manifest = sample_manifest();
1515 manifest.capabilities.clear();
1516 assert!(matches!(
1517 validate_extension_manifest(&manifest),
1518 Err(ExtensionContractError::MissingField(
1519 "extension_manifest.capabilities"
1520 ))
1521 ));
1522
1523 let mut manifest = sample_manifest();
1524 manifest.supported_profiles.clear();
1525 assert!(matches!(
1526 validate_extension_manifest(&manifest),
1527 Err(ExtensionContractError::MissingField(
1528 "extension_manifest.supported_profiles"
1529 ))
1530 ));
1531
1532 let mut manifest = sample_manifest();
1533 manifest.capabilities.push("receipt_append".to_string());
1534 assert!(matches!(
1535 validate_extension_manifest(&manifest),
1536 Err(ExtensionContractError::DuplicateValue(_))
1537 ));
1538
1539 let mut manifest = sample_manifest();
1540 manifest
1541 .supported_profiles
1542 .push("shared_control_plane".to_string());
1543 assert!(matches!(
1544 validate_extension_manifest(&manifest),
1545 Err(ExtensionContractError::DuplicateValue(_))
1546 ));
1547
1548 let mut manifest = sample_manifest();
1549 manifest.compatibility.supported_component_ids.clear();
1550 assert!(matches!(
1551 validate_extension_manifest(&manifest),
1552 Err(ExtensionContractError::MissingField(
1553 "extension_manifest.compatibility.supported_component_ids"
1554 ))
1555 ));
1556
1557 let mut manifest = sample_manifest();
1558 manifest.compatibility.supported_contract_schemas.clear();
1559 assert!(matches!(
1560 validate_extension_manifest(&manifest),
1561 Err(ExtensionContractError::MissingField(
1562 "extension_manifest.compatibility.supported_contract_schemas"
1563 ))
1564 ));
1565
1566 let mut manifest = sample_manifest();
1567 manifest
1568 .compatibility
1569 .supported_component_ids
1570 .push("chio.remote-receipt-store".to_string());
1571 assert!(matches!(
1572 validate_extension_manifest(&manifest),
1573 Err(ExtensionContractError::DuplicateValue(_))
1574 ));
1575
1576 let mut manifest = sample_manifest();
1577 manifest
1578 .compatibility
1579 .supported_contract_schemas
1580 .push("chio.receipt.v1".to_string());
1581 assert!(matches!(
1582 validate_extension_manifest(&manifest),
1583 Err(ExtensionContractError::DuplicateValue(_))
1584 ));
1585
1586 let mut manifest = sample_manifest();
1587 manifest.compatibility.supported_contract_schemas = vec![
1588 "chio.receipt.v1".to_string(),
1589 "chio.checkpoint.v1".to_string(),
1590 ];
1591 assert!(matches!(
1592 validate_extension_manifest(&manifest),
1593 Err(ExtensionContractError::InvalidGuardrail(_))
1594 ));
1595
1596 let mut manifest = sample_manifest();
1597 manifest
1598 .runtime
1599 .allowed_privileges
1600 .push(ExtensionPrivilege::FilesystemRead);
1601 assert!(matches!(
1602 validate_extension_manifest(&manifest),
1603 Err(ExtensionContractError::DuplicateValue(_))
1604 ));
1605
1606 let mut manifest = sample_manifest();
1607 manifest.runtime.allows_truth_mutation = true;
1608 assert!(matches!(
1609 validate_extension_manifest(&manifest),
1610 Err(ExtensionContractError::InvalidGuardrail(_))
1611 ));
1612
1613 let mut manifest = sample_manifest();
1614 manifest.runtime.allows_trust_widening = true;
1615 assert!(matches!(
1616 validate_extension_manifest(&manifest),
1617 Err(ExtensionContractError::InvalidGuardrail(_))
1618 ));
1619
1620 let mut manifest = sample_manifest();
1621 manifest.runtime.evidence_mode = ExtensionEvidenceMode::ImportOnly;
1622 manifest.runtime.requires_signer_verification = true;
1623 manifest.runtime.requires_freshness_check = true;
1624 manifest.runtime.requires_local_policy_activation = true;
1625 assert!(matches!(
1626 validate_extension_manifest(&manifest),
1627 Err(ExtensionContractError::InvalidGuardrail(_))
1628 ));
1629
1630 let mut manifest = sample_manifest();
1631 manifest.runtime.evidence_mode = ExtensionEvidenceMode::ImportOnly;
1632 manifest.runtime.requires_subject_binding = true;
1633 manifest.runtime.requires_freshness_check = true;
1634 manifest.runtime.requires_local_policy_activation = true;
1635 assert!(matches!(
1636 validate_extension_manifest(&manifest),
1637 Err(ExtensionContractError::InvalidGuardrail(_))
1638 ));
1639
1640 let mut manifest = sample_manifest();
1641 manifest.runtime.evidence_mode = ExtensionEvidenceMode::ImportOnly;
1642 manifest.runtime.requires_subject_binding = true;
1643 manifest.runtime.requires_signer_verification = true;
1644 manifest.runtime.requires_local_policy_activation = true;
1645 assert!(matches!(
1646 validate_extension_manifest(&manifest),
1647 Err(ExtensionContractError::InvalidGuardrail(_))
1648 ));
1649
1650 let mut manifest = sample_manifest();
1651 manifest.runtime.evidence_mode = ExtensionEvidenceMode::ImportOnly;
1652 manifest.runtime.requires_subject_binding = true;
1653 manifest.runtime.requires_signer_verification = true;
1654 manifest.runtime.requires_freshness_check = true;
1655 assert!(matches!(
1656 validate_extension_manifest(&manifest),
1657 Err(ExtensionContractError::InvalidGuardrail(_))
1658 ));
1659 }
1660
1661 #[test]
1662 fn rejects_policy_bypass_for_evidence_capable_extension() {
1663 let mut manifest = sample_manifest();
1664 manifest.extension_id = "sample.web3-oracle".to_string();
1665 manifest.extension_point_id = "chio.kernel.tool_server_connection".to_string();
1666 manifest.compatibility.supported_component_ids =
1667 vec!["chio.native-chio-service".to_string()];
1668 manifest.runtime.evidence_mode = ExtensionEvidenceMode::ImportAndDispatch;
1669 manifest.runtime.requires_subject_binding = true;
1670 manifest.runtime.requires_signer_verification = false;
1671 manifest.runtime.requires_freshness_check = true;
1672 manifest.runtime.requires_local_policy_activation = false;
1673 manifest.runtime.allowed_privileges = vec![
1674 ExtensionPrivilege::NetworkEgress,
1675 ExtensionPrivilege::OperatorSecrets,
1676 ];
1677
1678 let report = negotiate_extension(&sample_inventory(), &sample_official_stack(), &manifest);
1679 assert_eq!(report.outcome, ExtensionNegotiationOutcome::Rejected);
1680 assert!(report.reasons.iter().any(|reason| {
1681 reason.code == ExtensionNegotiationRejectionCode::MalformedManifest
1682 || reason.code == ExtensionNegotiationRejectionCode::LocalPolicyActivationRequired
1683 }));
1684 }
1685
1686 #[test]
1687 fn negotiation_rejects_malformed_and_mismatched_inputs() {
1688 let mut inventory = sample_inventory();
1689 inventory.canonical_truth.clear();
1690 let mut package = sample_official_stack();
1691 package.components.clear();
1692 let mut manifest = sample_manifest();
1693 manifest.capabilities.clear();
1694
1695 let report = negotiate_extension(&inventory, &package, &manifest);
1696 let codes = rejection_codes(&report);
1697 assert_eq!(report.outcome, ExtensionNegotiationOutcome::Rejected);
1698 assert!(codes.contains(&ExtensionNegotiationRejectionCode::MalformedInventory));
1699 assert!(codes.contains(&ExtensionNegotiationRejectionCode::MalformedOfficialStack));
1700 assert!(codes.contains(&ExtensionNegotiationRejectionCode::MalformedManifest));
1701
1702 let mut manifest = sample_manifest();
1703 manifest.compatibility.official_stack_package_id = "chio.other-stack".to_string();
1704 manifest.compatibility.chio_contract_version = "9.9".to_string();
1705 let report = negotiate_extension(&sample_inventory(), &sample_official_stack(), &manifest);
1706 let codes = rejection_codes(&report);
1707 assert!(codes.contains(&ExtensionNegotiationRejectionCode::UnsupportedOfficialStack));
1708 assert!(codes.contains(&ExtensionNegotiationRejectionCode::UnsupportedChioContract));
1709
1710 let mut manifest = sample_manifest();
1711 manifest.extension_point_id = "chio.kernel.unknown".to_string();
1712 let report = negotiate_extension(&sample_inventory(), &sample_official_stack(), &manifest);
1713 let codes = rejection_codes(&report);
1714 assert!(codes.contains(&ExtensionNegotiationRejectionCode::UnknownExtensionPoint));
1715 }
1716
1717 #[test]
1718 fn negotiation_rejects_reserved_and_incompatible_extension_claims() {
1719 let mut inventory = sample_inventory();
1720 inventory.extension_points[0].custom_implementations_allowed = false;
1721 inventory.extension_points[0].stability = ExtensionStability::Internal;
1722 let report = negotiate_extension(&inventory, &sample_official_stack(), &sample_manifest());
1723 let codes = rejection_codes(&report);
1724 assert!(codes.contains(&ExtensionNegotiationRejectionCode::OfficialOnlyPoint));
1725 assert!(codes.contains(&ExtensionNegotiationRejectionCode::InternalOnlyPoint));
1726
1727 let mut manifest = sample_manifest();
1728 manifest.supported_profiles = vec!["missing-profile".to_string()];
1729 manifest.compatibility.supported_component_ids = vec![
1730 "chio.native-chio-service".to_string(),
1731 "chio.unknown-component".to_string(),
1732 ];
1733 manifest.runtime.isolation = ExtensionIsolation::Subprocess;
1734 manifest.runtime.evidence_mode = ExtensionEvidenceMode::ImportOnly;
1735 manifest
1736 .runtime
1737 .allowed_privileges
1738 .push(ExtensionPrivilege::OperatorSecrets);
1739 manifest.runtime.requires_subject_binding = true;
1740 manifest.runtime.requires_signer_verification = true;
1741 manifest.runtime.requires_freshness_check = true;
1742 manifest.runtime.requires_local_policy_activation = true;
1743
1744 let report = negotiate_extension(&sample_inventory(), &sample_official_stack(), &manifest);
1745 let codes = rejection_codes(&report);
1746 assert!(codes.contains(&ExtensionNegotiationRejectionCode::UnsupportedProfile));
1747 assert!(codes.contains(&ExtensionNegotiationRejectionCode::UnsupportedComponent));
1748 assert!(codes.contains(&ExtensionNegotiationRejectionCode::UnsupportedIsolation));
1749 assert!(codes.contains(&ExtensionNegotiationRejectionCode::UnsupportedEvidenceMode));
1750 assert!(codes.contains(&ExtensionNegotiationRejectionCode::UnsupportedPrivilege));
1751 }
1752
1753 #[test]
1754 fn qualification_matrix_requires_rejection_codes_for_fail_closed_cases() {
1755 let matrix = ExtensionQualificationMatrix {
1756 schema: CHIO_EXTENSION_QUALIFICATION_MATRIX_SCHEMA.to_string(),
1757 official_stack_package_id: "chio.official-stack".to_string(),
1758 chio_contract_version: "2.0".to_string(),
1759 cases: vec![ExtensionQualificationCase {
1760 id: "missing-reasons".to_string(),
1761 name: "Broken case".to_string(),
1762 extension_point_id: "chio.kernel.receipt_store".to_string(),
1763 supported_component_id: "chio.sqlite-receipt-store".to_string(),
1764 candidate_extension_id: "sample.bad".to_string(),
1765 mode: QualificationMode::OfficialToCustom,
1766 expected_outcome: QualificationOutcome::FailClosed,
1767 observed_outcome: QualificationOutcome::FailClosed,
1768 rejection_codes: vec![],
1769 invariants: vec![QualificationInvariant::RejectsVersionMismatch],
1770 }],
1771 };
1772 assert!(matches!(
1773 validate_qualification_matrix(&matrix),
1774 Err(ExtensionContractError::InvalidQualificationCase(_))
1775 ));
1776 }
1777
1778 #[test]
1779 fn qualification_matrix_rejects_remaining_shape_and_outcome_errors() {
1780 let mut matrix = sample_qualification_matrix();
1781 matrix.schema = "chio.extension-qualification-matrix.v9".to_string();
1782 assert!(matches!(
1783 validate_qualification_matrix(&matrix),
1784 Err(ExtensionContractError::UnsupportedSchema(_))
1785 ));
1786
1787 let mut matrix = sample_qualification_matrix();
1788 matrix.official_stack_package_id.clear();
1789 assert!(matches!(
1790 validate_qualification_matrix(&matrix),
1791 Err(ExtensionContractError::MissingField(
1792 "qualification_matrix.official_stack_package_id"
1793 ))
1794 ));
1795
1796 let mut matrix = sample_qualification_matrix();
1797 matrix.chio_contract_version.clear();
1798 assert!(matches!(
1799 validate_qualification_matrix(&matrix),
1800 Err(ExtensionContractError::MissingField(
1801 "qualification_matrix.chio_contract_version"
1802 ))
1803 ));
1804
1805 let mut matrix = sample_qualification_matrix();
1806 matrix.cases.clear();
1807 assert!(matches!(
1808 validate_qualification_matrix(&matrix),
1809 Err(ExtensionContractError::MissingField(
1810 "qualification_matrix.cases"
1811 ))
1812 ));
1813
1814 let mut matrix = sample_qualification_matrix();
1815 matrix.cases[0].id.clear();
1816 assert!(matches!(
1817 validate_qualification_matrix(&matrix),
1818 Err(ExtensionContractError::MissingField(
1819 "qualification_matrix.case.id"
1820 ))
1821 ));
1822
1823 let mut matrix = sample_qualification_matrix();
1824 matrix.cases[0].name.clear();
1825 assert!(matches!(
1826 validate_qualification_matrix(&matrix),
1827 Err(ExtensionContractError::MissingField(
1828 "qualification_matrix.case.name"
1829 ))
1830 ));
1831
1832 let mut matrix = sample_qualification_matrix();
1833 matrix.cases[0].extension_point_id.clear();
1834 assert!(matches!(
1835 validate_qualification_matrix(&matrix),
1836 Err(ExtensionContractError::MissingField(
1837 "qualification_matrix.case.extension_point_id"
1838 ))
1839 ));
1840
1841 let mut matrix = sample_qualification_matrix();
1842 matrix.cases[0].supported_component_id.clear();
1843 assert!(matches!(
1844 validate_qualification_matrix(&matrix),
1845 Err(ExtensionContractError::MissingField(
1846 "qualification_matrix.case.supported_component_id"
1847 ))
1848 ));
1849
1850 let mut matrix = sample_qualification_matrix();
1851 matrix.cases[0].candidate_extension_id.clear();
1852 assert!(matches!(
1853 validate_qualification_matrix(&matrix),
1854 Err(ExtensionContractError::MissingField(
1855 "qualification_matrix.case.candidate_extension_id"
1856 ))
1857 ));
1858
1859 let mut matrix = sample_qualification_matrix();
1860 matrix.cases.push(matrix.cases[0].clone());
1861 assert!(matches!(
1862 validate_qualification_matrix(&matrix),
1863 Err(ExtensionContractError::DuplicateValue(_))
1864 ));
1865
1866 let mut matrix = sample_qualification_matrix();
1867 matrix.cases[0].invariants.clear();
1868 assert!(matches!(
1869 validate_qualification_matrix(&matrix),
1870 Err(ExtensionContractError::InvalidQualificationCase(_))
1871 ));
1872
1873 let mut matrix = sample_qualification_matrix();
1874 matrix.cases[0]
1875 .invariants
1876 .push(QualificationInvariant::PreservesCanonicalTruth);
1877 assert!(matches!(
1878 validate_qualification_matrix(&matrix),
1879 Err(ExtensionContractError::DuplicateValue(_))
1880 ));
1881
1882 let mut matrix = sample_qualification_matrix();
1883 matrix.cases[0].expected_outcome = QualificationOutcome::FailClosed;
1884 matrix.cases[0].observed_outcome = QualificationOutcome::FailClosed;
1885 matrix.cases[0].rejection_codes = vec![
1886 ExtensionNegotiationRejectionCode::UnsupportedProfile,
1887 ExtensionNegotiationRejectionCode::UnsupportedProfile,
1888 ];
1889 assert!(matches!(
1890 validate_qualification_matrix(&matrix),
1891 Err(ExtensionContractError::DuplicateValue(_))
1892 ));
1893
1894 let mut matrix = sample_qualification_matrix();
1895 matrix.cases[0].rejection_codes =
1896 vec![ExtensionNegotiationRejectionCode::UnsupportedProfile];
1897 assert!(matches!(
1898 validate_qualification_matrix(&matrix),
1899 Err(ExtensionContractError::InvalidQualificationCase(_))
1900 ));
1901 }
1902
1903 #[test]
1904 fn reference_artifacts_parse_and_validate() {
1905 let inventory: ChioExtensionInventory = serde_json::from_str(include_str!(
1906 "../../../docs/standards/CHIO_EXTENSION_INVENTORY.json"
1907 ))
1908 .unwrap();
1909 let official_stack: OfficialStackPackage = serde_json::from_str(include_str!(
1910 "../../../docs/standards/CHIO_OFFICIAL_STACK.json"
1911 ))
1912 .unwrap();
1913 let manifest: ChioExtensionManifest = serde_json::from_str(include_str!(
1914 "../../../docs/standards/CHIO_EXTENSION_MANIFEST_EXAMPLE.json"
1915 ))
1916 .unwrap();
1917 let matrix: ExtensionQualificationMatrix = serde_json::from_str(include_str!(
1918 "../../../docs/standards/CHIO_EXTENSION_QUALIFICATION_MATRIX.json"
1919 ))
1920 .unwrap();
1921
1922 validate_extension_inventory(&inventory).unwrap();
1923 validate_official_stack_package(&inventory, &official_stack).unwrap();
1924 validate_extension_manifest(&manifest).unwrap();
1925 validate_qualification_matrix(&matrix).unwrap();
1926
1927 let report = negotiate_extension(&inventory, &official_stack, &manifest);
1928 assert_eq!(report.outcome, ExtensionNegotiationOutcome::Accepted);
1929 }
1930}