canic-host 0.111.0

Host-side App build and desired-state Fleet reconciliation library for Canic workspaces
Documentation
//! Module: role_contract
//!
//! Responsibility: collect exact local Cargo package evidence for core role policy.
//! Does not own: feature implications, allocation policy, descriptors, or rendering.
//! Boundary: exposes only supported package evidence or one blocking finding.

mod descriptor;
mod package;
#[cfg(test)]
mod tests;

use canic_core::{
    bootstrap::compiled::ConfigModel,
    role_contract::{
        BuiltInRoleKind, CanicFeatureKey, RoleContractFinding, RoleContractInput,
        RoleContractResolution, RoleContractSource, required_features_for_role,
        resolve_role_contract,
    },
};
use std::{collections::BTreeSet, path::Path};

pub use crate::cargo_metadata::CargoFeatureSelection;
pub(crate) use package::package_manifest_path;

pub use descriptor::{
    StateDescriptorRegistry, materialize_state_manifest, validate_state_descriptor_registry,
};

pub use package::{
    PackageValidationMode, RoleCargoGraphEvidence, RolePackageValidation,
    declared_role_manifest_path, validate_built_in_fleet_coordinator_package,
    validate_built_in_wasm_store_package, validate_declared_role_package,
    validate_declared_role_packages, validate_internal_test_wasm_packages,
};

/// Resolve one role inventory using fresh workspace evidence and isolated role trees.
#[must_use]
pub(crate) fn resolve_declared_role_contracts(
    config_path: &Path,
    config: &ConfigModel,
    roles: &[canic_contracts::ids::CanisterRole],
    mode: PackageValidationMode,
) -> Vec<RoleContractResolution> {
    // Canonical Root projection must not require a generated Cargo package.
    let package_roles = roles
        .iter()
        .filter(|role| !role.is_root())
        .cloned()
        .collect::<Vec<_>>();
    let mut packages =
        validate_declared_role_packages(config_path, config, &package_roles, mode).into_iter();
    roles
        .iter()
        .map(|role| {
            if role.is_root() {
                return resolve_canonical_root_contract(config);
            }
            match packages
                .next()
                .expect("one validation per package-backed role")
            {
                RolePackageValidation::Supported(evidence) => {
                    resolve_declared_role_package_contract(config, &evidence)
                }
                RolePackageValidation::Unsupported(finding) => RoleContractResolution::Rejected {
                    errors: vec![finding],
                },
            }
        })
        .collect()
}

/// Project the configured canonical Root contract without requiring build artifacts.
/// Artifact construction separately validates the materialized Cargo package.
#[must_use]
pub fn resolve_canonical_root_contract(config: &ConfigModel) -> RoleContractResolution {
    let role = canic_contracts::ids::CanisterRole::ROOT;
    let requirements = match required_features_for_role(config, &role) {
        Ok(requirements) => requirements,
        Err(finding) => {
            return RoleContractResolution::Rejected {
                errors: vec![finding],
            };
        }
    };
    resolve_role_contract(RoleContractInput {
        source: RoleContractSource::Declared {
            config,
            role: &role,
        },
        declared_features: requirements
            .into_iter()
            .map(|requirement| requirement.feature)
            .collect(),
        default_features_enabled: false,
    })
}

#[must_use]
pub fn resolve_declared_role_package_contract(
    config: &ConfigModel,
    evidence: &RoleCargoGraphEvidence,
) -> RoleContractResolution {
    resolve_role_contract(RoleContractInput {
        source: RoleContractSource::Declared {
            config,
            role: &evidence.role,
        },
        declared_features: evidence.direct_features.clone(),
        default_features_enabled: evidence.default_features_enabled,
    })
}

#[must_use]
pub fn resolve_built_in_fleet_coordinator_contract(
    evidence: &RoleCargoGraphEvidence,
) -> RoleContractResolution {
    resolve_role_contract(RoleContractInput {
        source: RoleContractSource::BuiltIn(BuiltInRoleKind::FleetCoordinator),
        declared_features: evidence.direct_features.clone(),
        default_features_enabled: evidence.default_features_enabled,
    })
}

#[must_use]
pub(crate) fn resolve_host_generated_fleet_coordinator_contract() -> RoleContractResolution {
    resolve_role_contract(RoleContractInput {
        source: RoleContractSource::BuiltIn(BuiltInRoleKind::FleetCoordinator),
        declared_features: BTreeSet::from([CanicFeatureKey::FleetCoordinatorCanister]),
        default_features_enabled: false,
    })
}
#[must_use]
pub(crate) fn resolve_host_generated_wasm_store_contract() -> RoleContractResolution {
    resolve_role_contract(RoleContractInput {
        source: RoleContractSource::BuiltIn(BuiltInRoleKind::WasmStore),
        declared_features: BTreeSet::from([CanicFeatureKey::WasmStoreCanister]),
        default_features_enabled: false,
    })
}

#[must_use]
pub fn resolve_built_in_wasm_store_contract(
    evidence: &RoleCargoGraphEvidence,
) -> RoleContractResolution {
    resolve_role_contract(RoleContractInput {
        source: RoleContractSource::BuiltIn(BuiltInRoleKind::WasmStore),
        declared_features: evidence.direct_features.clone(),
        default_features_enabled: evidence.default_features_enabled,
    })
}

#[must_use]
pub fn finding_detail(finding: &RoleContractFinding) -> String {
    match finding {
        RoleContractFinding::AllocationDescriptorDuplicate { key } => {
            format!("allocation {key:?} has more than one state descriptor")
        }
        RoleContractFinding::AllocationDescriptorKeyMismatch {
            key,
            expected,
            actual,
        } => format!(
            "allocation {key:?} descriptor keys {actual:?} do not match canonical keys {expected:?}"
        ),
        RoleContractFinding::AllocationDescriptorMissing { key } => {
            format!("allocation {key:?} has no state descriptor")
        }
        RoleContractFinding::BuiltInPackageUnavailable { role } => {
            format!("built-in package evidence is unavailable for {role:?}")
        }
        RoleContractFinding::CanicVersionMismatch { expected, actual } => {
            format!("resolved Canic version {actual}, expected {expected}")
        }
        RoleContractFinding::CargoCatalogDrift { reason }
        | RoleContractFinding::CatalogInvalid { reason }
        | RoleContractFinding::DependencyShapeUnsupported { reason } => reason.clone(),
        RoleContractFinding::CargoEvidenceUnavailable { phase, cause } => {
            format!("Cargo evidence phase {phase} failed: {cause}")
        }
        RoleContractFinding::MemoryKeyCollision {
            stable_key,
            first,
            second,
        } => format!("memory key {stable_key} is claimed by {first:?} and {second:?}"),
        RoleContractFinding::MultipleCanicPackages { packages } => format!(
            "the wasm runtime graph reaches multiple Canic packages: {}",
            packages.join(", ")
        ),
        RoleContractFinding::MultipleMemoryRuntimes { packages } => format!(
            "the selected Wasm role reaches multiple ic-memory package identities: {}. Align the application's framework and database dependencies to one ic-memory package identity before building; matching version labels from different sources is insufficient",
            packages.join(", ")
        ),
        RoleContractFinding::MultipleTimerRuntimes { packages } => format!(
            "the selected Wasm role reaches multiple ic-timers package identities: {}. Select one timer runtime and its shared metrics types before building",
            packages.join(", ")
        ),
        RoleContractFinding::PackageAmbiguous { role } => {
            format!("multiple Cargo packages resolve for role {role}")
        }
        RoleContractFinding::PackageMetadataMismatch {
            expected_app,
            expected_role,
            actual_app,
            actual_role,
        } => format!(
            "package metadata declares app={} role={}, expected app={expected_app} role={expected_role}",
            actual_app.as_deref().unwrap_or("<missing>"),
            actual_role.as_deref().unwrap_or("<missing>")
        ),
        RoleContractFinding::PackageMissing { role } => {
            format!("Cargo package for role {role} is missing")
        }
        RoleContractFinding::RequiredFeatureMissing {
            capability,
            feature,
        } => format!(
            "capability {capability:?} requires Canic feature `{}`",
            feature.cargo_name()
        ),
        RoleContractFinding::SurplusCryptoFeature { feature } => format!(
            "Canic cryptographic feature `{}` is enabled without a role capability that uses it",
            feature.cargo_name()
        ),
        RoleContractFinding::RoleUnknown { role } => {
            format!("role {role} is not declared")
        }
        RoleContractFinding::RuntimeCanicDependencyMissing { role } => {
            format!("role {role} has no direct normal runtime dependency on package `canic`")
        }
    }
}