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 live.operation_contract_version != binding.operation_contract_version
101                || live.provider_version != binding.provider_version
102                || live.provider_implementation_fingerprint
103                    != binding.provider_implementation_fingerprint
104            {
105                return Err(format!(
106                    "compiled native operator {} binding {}/{} differs from the live provider identity",
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 is_lowercase_sha256(value: &str) -> bool {
134    value.len() == 64
135        && value
136            .bytes()
137            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
138}
139
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct CompiledFa2NativeOperatorArtifact {
142    pub manifest_path: String,
143    pub artifact_path: String,
144    pub source_package_sha256: String,
145    pub inputs_sha256: String,
146    pub binary_sha256: String,
147}
148
149pub fn compiled_fa2_native_operator_artifact_linked() -> bool {
150    compiled_fa2_native_operator_artifact().is_some()
151}
152
153pub fn compiled_fa2_native_operator_artifact_state() -> &'static str {
154    option_env!("FERRUM_FA2_NATIVE_ARTIFACT_COMPILE").unwrap_or("not_configured")
155}
156
157pub fn compiled_fa2_native_operator_artifact() -> Option<CompiledFa2NativeOperatorArtifact> {
158    Some(CompiledFa2NativeOperatorArtifact {
159        manifest_path: option_env!("FERRUM_COMPILED_FA2_NATIVE_MANIFEST")?.to_string(),
160        artifact_path: option_env!("FERRUM_COMPILED_FA2_NATIVE_ARTIFACT")?.to_string(),
161        source_package_sha256: option_env!("FERRUM_COMPILED_FA2_NATIVE_SOURCE_SHA256")?.to_string(),
162        inputs_sha256: option_env!("FERRUM_COMPILED_FA2_NATIVE_INPUTS_SHA256")?.to_string(),
163        binary_sha256: option_env!("FERRUM_COMPILED_FA2_NATIVE_BINARY_SHA256")?.to_string(),
164    })
165}
166
167#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct NativeOperatorArtifactSpec {
169    pub operator: String,
170    pub backend: NativeOperatorBackend,
171    pub compute_capability: Option<String>,
172    pub manifest_path: PathBuf,
173    pub artifact_path: PathBuf,
174    pub source_package_sha256: Option<String>,
175    pub inputs_sha256: Option<String>,
176    pub binary_sha256: Option<String>,
177}
178
179impl NativeOperatorArtifactSpec {
180    pub fn cuda_fa2(
181        manifest_path: impl Into<PathBuf>,
182        artifact_path: impl Into<PathBuf>,
183        compute_capability: impl Into<String>,
184    ) -> Self {
185        Self {
186            operator: FA2_NATIVE_OPERATOR.to_string(),
187            backend: NativeOperatorBackend::Cuda,
188            compute_capability: Some(compute_capability.into()),
189            manifest_path: manifest_path.into(),
190            artifact_path: artifact_path.into(),
191            source_package_sha256: None,
192            inputs_sha256: None,
193            binary_sha256: None,
194        }
195    }
196
197    pub fn with_source_package_sha256(mut self, sha256: impl Into<String>) -> Self {
198        self.source_package_sha256 = Some(sha256.into());
199        self
200    }
201
202    pub fn with_inputs_sha256(mut self, sha256: impl Into<String>) -> Self {
203        self.inputs_sha256 = Some(sha256.into());
204        self
205    }
206
207    pub fn with_binary_sha256(mut self, sha256: impl Into<String>) -> Self {
208        self.binary_sha256 = Some(sha256.into());
209        self
210    }
211}
212
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct NativeOperatorRuntimeSelection {
215    pub schema_version: u32,
216    pub operator: String,
217    pub operator_abi_version: String,
218    pub ferrum_native_abi_version: String,
219    pub backend: NativeOperatorBackend,
220    pub compute_capability: Option<String>,
221    pub linkage: NativeOperatorLinkage,
222    pub manifest_path: PathBuf,
223    pub artifact_path: PathBuf,
224    pub binary_sha256: String,
225    pub source_package_sha256: String,
226    pub inputs_sha256: String,
227    pub g03_catalog_sha256: Option<String>,
228    pub abi_contract_sha256: Option<String>,
229    pub descriptor_export: Option<String>,
230    pub operation_bindings: Vec<NativeOperatorBinding>,
231    pub artifact_format: NativeOperatorArtifactFormat,
232    pub archive_members: Vec<String>,
233    pub required_exports: Vec<String>,
234    pub matched_exports: Vec<String>,
235}
236
237pub fn resolve_native_operator_artifact(
238    spec: &NativeOperatorArtifactSpec,
239) -> Result<NativeOperatorRuntimeSelection, NativeOperatorResolveError> {
240    let mut request = NativeOperatorResolveRequest::new(
241        spec.operator.clone(),
242        spec.backend,
243        spec.manifest_path.clone(),
244        spec.artifact_path.clone(),
245    );
246    if let Some(compute_capability) = spec.compute_capability.clone() {
247        request = request.with_compute_capability(compute_capability);
248    }
249
250    let resolved = NativeOperatorResolver.resolve(&request)?;
251    let mut requirement = NativeOperatorRequirement {
252        operator: spec.operator.clone(),
253        backend: spec.backend,
254        operator_abi_version: resolved.manifest.operator_abi_version.clone(),
255        ferrum_native_abi_version: resolved.manifest.ferrum_native_abi_version.clone(),
256        compute_capability: spec.compute_capability.clone(),
257        source_package_sha256: spec.source_package_sha256.clone(),
258        inputs_sha256: spec.inputs_sha256.clone(),
259        binary_sha256: spec
260            .binary_sha256
261            .clone()
262            .or_else(|| Some(resolved.artifact_sha256.clone())),
263        g03_catalog_sha256: resolved.manifest.g03_catalog_sha256.clone(),
264        abi_contract_sha256: resolved.manifest.abi_contract_sha256.clone(),
265        descriptor_export: resolved.manifest.descriptor_export.clone(),
266        required_exports: resolved.manifest.exports.clone(),
267        operation_bindings: Some(resolved.manifest.operation_bindings.clone()),
268    };
269    if requirement.source_package_sha256.is_none() {
270        requirement.source_package_sha256 = Some(resolved.manifest.source_package.sha256.clone());
271    }
272    if requirement.inputs_sha256.is_none() {
273        requirement.inputs_sha256 = Some(resolved.manifest.inputs_sha256.clone());
274    }
275    resolve_native_operator_manifest(Some(&resolved.manifest), &requirement)
276        .map_err(NativeOperatorResolveError::ManifestInvalid)?;
277
278    Ok(NativeOperatorRuntimeSelection {
279        schema_version: resolved.manifest.schema_version,
280        operator: resolved.manifest.operator.clone(),
281        operator_abi_version: resolved.manifest.operator_abi_version.clone(),
282        ferrum_native_abi_version: resolved.manifest.ferrum_native_abi_version.clone(),
283        backend: resolved.manifest.backend,
284        compute_capability: spec.compute_capability.clone(),
285        linkage: resolved.manifest.linkage,
286        manifest_path: resolved.manifest_path,
287        artifact_path: resolved.artifact_path,
288        binary_sha256: resolved.artifact_sha256,
289        source_package_sha256: resolved.manifest.source_package.sha256,
290        inputs_sha256: resolved.manifest.inputs_sha256,
291        g03_catalog_sha256: resolved.manifest.g03_catalog_sha256,
292        abi_contract_sha256: resolved.manifest.abi_contract_sha256,
293        descriptor_export: resolved.manifest.descriptor_export,
294        operation_bindings: resolved.manifest.operation_bindings,
295        artifact_format: resolved.binary_validation.format,
296        archive_members: resolved.binary_validation.archive_members,
297        required_exports: resolved.binary_validation.required_exports,
298        matched_exports: resolved.binary_validation.matched_exports,
299    })
300}
301
302pub fn resolve_cuda_fa2_native_operator(
303    spec: &NativeOperatorArtifactSpec,
304) -> Result<NativeOperatorRuntimeSelection, NativeOperatorResolveError> {
305    if spec.operator != FA2_NATIVE_OPERATOR || spec.backend != NativeOperatorBackend::Cuda {
306        return Err(NativeOperatorResolveError::ManifestInvalid(
307            "FA2 native operator selection requires operator=fa2 backend=cuda".to_string(),
308        ));
309    }
310    resolve_native_operator_artifact(spec)
311}