Skip to main content

ferrum_kernels/
native_ops.rs

1//! Kernel-side adapter for Ferrum native operator artifacts.
2//!
3//! This module is intentionally path/config driven. It does not read
4//! environment variables or select an accelerator path by itself; product
5//! entrypoints must pass typed manifest/artifact choices into this layer.
6
7use std::path::PathBuf;
8use std::sync::OnceLock;
9
10use ferrum_native_ops::{
11    NativeOperatorArtifactFormat, NativeOperatorResolveError, NativeOperatorResolveRequest,
12    NativeOperatorResolver,
13};
14use ferrum_types::{
15    resolve_native_operator_manifest, NativeOperatorBackend, NativeOperatorBinding,
16    NativeOperatorLinkage, NativeOperatorProviderCatalog, NativeOperatorRequirement,
17    NATIVE_OPERATOR_MANIFEST_SCHEMA_VERSION,
18};
19
20pub use ferrum_types::CompiledNativeOperatorIdentity as CompiledNativeOperatorArtifact;
21
22pub const FA2_NATIVE_OPERATOR: &str = "fa2";
23pub const CUDA_NATIVE_SOURCE_BUNDLE_ID: &str = "ferrum-native-cuda-v1+sha256.\
247f6f35f91a85df6ea5374d5597f7f8ca4c159b5e567c1c1ef122a0ab88657613";
25
26pub fn compiled_native_operator_artifacts() -> &'static [CompiledNativeOperatorArtifact] {
27    static COMPILED: OnceLock<Vec<CompiledNativeOperatorArtifact>> = OnceLock::new();
28    COMPILED
29        .get_or_init(|| {
30            serde_json::from_str(
31                option_env!("FERRUM_COMPILED_NATIVE_OPERATOR_SET_JSON").unwrap_or("[]"),
32            )
33            .expect("build.rs emitted invalid native operator inventory JSON")
34        })
35        .as_slice()
36}
37
38pub fn validate_compiled_native_operator_provider_catalog(
39    catalog: &NativeOperatorProviderCatalog,
40    artifacts: &[CompiledNativeOperatorArtifact],
41) -> Result<(), String> {
42    catalog.validate()?;
43    if artifacts.is_empty() {
44        return Ok(());
45    }
46    let catalog_sha256 = catalog.canonical_sha256()?;
47    let mut binding_count = 0_usize;
48    let mut provenance_change_count = 0_usize;
49    let mut first_provenance_sha256 = None;
50    for artifact in artifacts {
51        if artifact.schema_version != NATIVE_OPERATOR_MANIFEST_SCHEMA_VERSION {
52            return Err(format!(
53                "compiled native operator {} uses schema {}, expected {}",
54                artifact.operator, artifact.schema_version, NATIVE_OPERATOR_MANIFEST_SCHEMA_VERSION
55            ));
56        }
57        if artifact.backend != catalog.backend {
58            return Err(format!(
59                "compiled native operator {} backend {:?} differs from live catalog {:?}",
60                artifact.operator, artifact.backend, catalog.backend
61            ));
62        }
63        let provenance_sha256 = artifact.g03_catalog_sha256.as_deref().ok_or_else(|| {
64            format!(
65                "compiled native operator {} has no provider catalog provenance",
66                artifact.operator
67            )
68        })?;
69        if !is_lowercase_sha256(provenance_sha256) {
70            return Err(format!(
71                "compiled native operator {} has invalid provider catalog provenance",
72                artifact.operator
73            ));
74        }
75        // The full catalog digest records build provenance. Runtime compatibility is
76        // binding-scoped so an unrelated provider cannot invalidate every artifact.
77        if provenance_sha256 != catalog_sha256 {
78            provenance_change_count = provenance_change_count.checked_add(1).ok_or_else(|| {
79                "compiled native operator provenance change count overflows usize".to_string()
80            })?;
81            first_provenance_sha256.get_or_insert(provenance_sha256);
82        }
83        for binding in &artifact.operation_bindings {
84            binding_count = binding_count.checked_add(1).ok_or_else(|| {
85                "compiled native operator binding count overflows usize".to_string()
86            })?;
87            let live = catalog
88                .providers
89                .iter()
90                .find(|provider| {
91                    provider.operation_id == binding.operation_id
92                        && provider.provider_id == binding.provider_id
93                })
94                .ok_or_else(|| {
95                    format!(
96                        "compiled native operator {} binds missing live provider {}/{}",
97                        artifact.operator, binding.operation_id, binding.provider_id
98                    )
99                })?;
100            if !contract_version_satisfies(
101                live.operation_contract_version,
102                binding.operation_contract_version,
103            ) || !contract_version_satisfies(live.provider_version, binding.provider_version)
104            {
105                return Err(format!(
106                    "compiled native operator {} binding {}/{} is incompatible with the live versioned contract",
107                    artifact.operator, binding.operation_id, binding.provider_id
108                ));
109            }
110        }
111    }
112    if binding_count == 0 {
113        return Err(
114            "compiled native operator set does not bind any live G03 operation/provider"
115                .to_string(),
116        );
117    }
118    if provenance_change_count > 0 {
119        tracing::debug!(
120            target: "ferrum::native_ops",
121            event = "native_operator_catalog_provenance_changed",
122            artifact_count = artifacts.len(),
123            provenance_change_count,
124            validated_binding_count = binding_count,
125            artifact_catalog_sha256 = first_provenance_sha256.unwrap_or_default(),
126            live_catalog_sha256 = catalog_sha256,
127            "native operator catalog provenance changed; declared provider bindings remain compatible"
128        );
129    }
130    Ok(())
131}
132
133fn contract_version_satisfies(
134    available: ferrum_types::NativeOperatorContractVersion,
135    required: ferrum_types::NativeOperatorContractVersion,
136) -> bool {
137    available.major == required.major && available.minor >= required.minor
138}
139
140fn is_lowercase_sha256(value: &str) -> bool {
141    value.len() == 64
142        && value
143            .bytes()
144            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct CompiledFa2NativeOperatorArtifact {
149    pub manifest_path: String,
150    pub artifact_path: String,
151    pub source_package_sha256: String,
152    pub inputs_sha256: String,
153    pub binary_sha256: String,
154}
155
156pub fn compiled_fa2_native_operator_artifact_linked() -> bool {
157    compiled_fa2_native_operator_artifact().is_some()
158}
159
160pub fn compiled_fa2_native_operator_artifact_state() -> &'static str {
161    option_env!("FERRUM_FA2_NATIVE_ARTIFACT_COMPILE").unwrap_or("not_configured")
162}
163
164pub fn compiled_fa2_native_operator_artifact() -> Option<CompiledFa2NativeOperatorArtifact> {
165    Some(CompiledFa2NativeOperatorArtifact {
166        manifest_path: option_env!("FERRUM_COMPILED_FA2_NATIVE_MANIFEST")?.to_string(),
167        artifact_path: option_env!("FERRUM_COMPILED_FA2_NATIVE_ARTIFACT")?.to_string(),
168        source_package_sha256: option_env!("FERRUM_COMPILED_FA2_NATIVE_SOURCE_SHA256")?.to_string(),
169        inputs_sha256: option_env!("FERRUM_COMPILED_FA2_NATIVE_INPUTS_SHA256")?.to_string(),
170        binary_sha256: option_env!("FERRUM_COMPILED_FA2_NATIVE_BINARY_SHA256")?.to_string(),
171    })
172}
173
174#[derive(Debug, Clone, PartialEq, Eq)]
175pub struct NativeOperatorArtifactSpec {
176    pub operator: String,
177    pub backend: NativeOperatorBackend,
178    pub compute_capability: Option<String>,
179    pub manifest_path: PathBuf,
180    pub artifact_path: PathBuf,
181    pub source_package_sha256: Option<String>,
182    pub inputs_sha256: Option<String>,
183    pub binary_sha256: Option<String>,
184}
185
186impl NativeOperatorArtifactSpec {
187    pub fn cuda_fa2(
188        manifest_path: impl Into<PathBuf>,
189        artifact_path: impl Into<PathBuf>,
190        compute_capability: impl Into<String>,
191    ) -> Self {
192        Self {
193            operator: FA2_NATIVE_OPERATOR.to_string(),
194            backend: NativeOperatorBackend::Cuda,
195            compute_capability: Some(compute_capability.into()),
196            manifest_path: manifest_path.into(),
197            artifact_path: artifact_path.into(),
198            source_package_sha256: None,
199            inputs_sha256: None,
200            binary_sha256: None,
201        }
202    }
203
204    pub fn with_source_package_sha256(mut self, sha256: impl Into<String>) -> Self {
205        self.source_package_sha256 = Some(sha256.into());
206        self
207    }
208
209    pub fn with_inputs_sha256(mut self, sha256: impl Into<String>) -> Self {
210        self.inputs_sha256 = Some(sha256.into());
211        self
212    }
213
214    pub fn with_binary_sha256(mut self, sha256: impl Into<String>) -> Self {
215        self.binary_sha256 = Some(sha256.into());
216        self
217    }
218}
219
220#[derive(Debug, Clone, PartialEq, Eq)]
221pub struct NativeOperatorRuntimeSelection {
222    pub schema_version: u32,
223    pub operator: String,
224    pub operator_abi_version: String,
225    pub ferrum_native_abi_version: String,
226    pub backend: NativeOperatorBackend,
227    pub compute_capability: Option<String>,
228    pub linkage: NativeOperatorLinkage,
229    pub manifest_path: PathBuf,
230    pub artifact_path: PathBuf,
231    pub binary_sha256: String,
232    pub source_package_sha256: String,
233    pub inputs_sha256: String,
234    pub g03_catalog_sha256: Option<String>,
235    pub abi_contract_sha256: Option<String>,
236    pub descriptor_export: Option<String>,
237    pub operation_bindings: Vec<NativeOperatorBinding>,
238    pub artifact_format: NativeOperatorArtifactFormat,
239    pub archive_members: Vec<String>,
240    pub required_exports: Vec<String>,
241    pub matched_exports: Vec<String>,
242}
243
244pub fn resolve_native_operator_artifact(
245    spec: &NativeOperatorArtifactSpec,
246) -> Result<NativeOperatorRuntimeSelection, NativeOperatorResolveError> {
247    let mut request = NativeOperatorResolveRequest::new(
248        spec.operator.clone(),
249        spec.backend,
250        spec.manifest_path.clone(),
251        spec.artifact_path.clone(),
252    );
253    if let Some(compute_capability) = spec.compute_capability.clone() {
254        request = request.with_compute_capability(compute_capability);
255    }
256
257    let resolved = NativeOperatorResolver.resolve(&request)?;
258    let mut requirement = NativeOperatorRequirement {
259        operator: spec.operator.clone(),
260        backend: spec.backend,
261        operator_abi_version: resolved.manifest.operator_abi_version.clone(),
262        ferrum_native_abi_version: resolved.manifest.ferrum_native_abi_version.clone(),
263        compute_capability: spec.compute_capability.clone(),
264        source_package_sha256: spec.source_package_sha256.clone(),
265        inputs_sha256: spec.inputs_sha256.clone(),
266        binary_sha256: spec
267            .binary_sha256
268            .clone()
269            .or_else(|| Some(resolved.artifact_sha256.clone())),
270        g03_catalog_sha256: resolved.manifest.g03_catalog_sha256.clone(),
271        abi_contract_sha256: resolved.manifest.abi_contract_sha256.clone(),
272        descriptor_export: resolved.manifest.descriptor_export.clone(),
273        required_exports: resolved.manifest.exports.clone(),
274        operation_bindings: Some(resolved.manifest.operation_bindings.clone()),
275    };
276    if requirement.source_package_sha256.is_none() {
277        requirement.source_package_sha256 = Some(resolved.manifest.source_package.sha256.clone());
278    }
279    if requirement.inputs_sha256.is_none() {
280        requirement.inputs_sha256 = Some(resolved.manifest.inputs_sha256.clone());
281    }
282    resolve_native_operator_manifest(Some(&resolved.manifest), &requirement)
283        .map_err(NativeOperatorResolveError::ManifestInvalid)?;
284
285    Ok(NativeOperatorRuntimeSelection {
286        schema_version: resolved.manifest.schema_version,
287        operator: resolved.manifest.operator.clone(),
288        operator_abi_version: resolved.manifest.operator_abi_version.clone(),
289        ferrum_native_abi_version: resolved.manifest.ferrum_native_abi_version.clone(),
290        backend: resolved.manifest.backend,
291        compute_capability: spec.compute_capability.clone(),
292        linkage: resolved.manifest.linkage,
293        manifest_path: resolved.manifest_path,
294        artifact_path: resolved.artifact_path,
295        binary_sha256: resolved.artifact_sha256,
296        source_package_sha256: resolved.manifest.source_package.sha256,
297        inputs_sha256: resolved.manifest.inputs_sha256,
298        g03_catalog_sha256: resolved.manifest.g03_catalog_sha256,
299        abi_contract_sha256: resolved.manifest.abi_contract_sha256,
300        descriptor_export: resolved.manifest.descriptor_export,
301        operation_bindings: resolved.manifest.operation_bindings,
302        artifact_format: resolved.binary_validation.format,
303        archive_members: resolved.binary_validation.archive_members,
304        required_exports: resolved.binary_validation.required_exports,
305        matched_exports: resolved.binary_validation.matched_exports,
306    })
307}
308
309pub fn resolve_cuda_fa2_native_operator(
310    spec: &NativeOperatorArtifactSpec,
311) -> Result<NativeOperatorRuntimeSelection, NativeOperatorResolveError> {
312    if spec.operator != FA2_NATIVE_OPERATOR || spec.backend != NativeOperatorBackend::Cuda {
313        return Err(NativeOperatorResolveError::ManifestInvalid(
314            "FA2 native operator selection requires operator=fa2 backend=cuda".to_string(),
315        ));
316    }
317    resolve_native_operator_artifact(spec)
318}