Skip to main content

chio_core/
extension.rs

1//! Chio extension and official-stack contract types.
2//!
3//! These types freeze which Chio surfaces are canonical truth, which seams are
4//! replaceable, how custom implementations negotiate against the official
5//! stack, and which fail-closed conditions must be preserved.
6
7use 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}