Skip to main content

rightkit_ort/
lib.rs

1//! Product-neutral ONNX Runtime dynamic-library resolution, environment and
2//! session setup for Right Suite apps.
3//!
4//! Merged from ScrapeRight `ort_common` (installed-bundle layout, system-DLL
5//! hazard) and HeardRight `heardright-onnx-asr` (environment, execution
6//! providers, session builder). Product environment variables and data roots
7//! stay in app adapters.
8
9/// The pinned `ort` crate, re-exported so apps that run their own inference reach
10/// `rightkit_ort::ort::{value::Tensor, session::Session, ...}` through this crate.
11/// They never declare a direct `ort` dependency, which keeps the release ownership
12/// scanner from seeing one and keeps the whole suite on a single `ort` pin.
13///
14/// ```no_run
15/// use rightkit_ort::ort::{session::Session, value::Tensor};
16///
17/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
18/// // Configure the runtime and environment first (see `configure_runtime`).
19/// let mut session = Session::builder()?.commit_from_file("model.onnx")?;
20/// let x = Tensor::from_array(([1usize, 4], vec![0.0f32; 4]))?;
21/// let _outputs = session.run(rightkit_ort::ort::inputs!["x" => x])?;
22/// # Ok(())
23/// # }
24/// ```
25#[cfg(feature = "session")]
26pub use ort;
27
28/// `half` (f16/bf16) as used by ort's `half` feature: `rightkit_ort::half::f16`.
29#[cfg(feature = "half")]
30pub use half;
31
32/// `ndarray` as used by ort's `ndarray` feature: `rightkit_ort::ndarray::Array2`.
33#[cfg(feature = "ndarray")]
34pub use ndarray;
35
36#[cfg(feature = "session")]
37pub mod environment;
38#[cfg(feature = "session")]
39pub mod probe;
40#[cfg(feature = "session")]
41pub mod session;
42
43#[cfg(feature = "session")]
44pub use environment::{
45    cpu_thread_budget, init_environment, init_environment_with_options, shared_pool_active,
46    EnvironmentInitOptions, EnvironmentInitReport, EnvironmentOptions, EnvironmentReport,
47    EnvironmentStatus, GlobalPool,
48};
49#[cfg(feature = "session")]
50pub use probe::{probe_providers, ProviderDiagnostic, ProviderKind};
51#[cfg(feature = "session")]
52pub use session::{BuiltSession, ExecutionProvider, SessionError, SessionOptions};
53
54use std::fmt;
55use std::path::{Path, PathBuf};
56use std::sync::Mutex;
57
58static CONFIGURED_RUNTIME: Mutex<Option<PathBuf>> = Mutex::new(None);
59
60/// Clear inherited `ORT_DYLIB_PATH` in release builds; no-op in debug builds.
61/// This keeps shell-exported development paths from steering shipped apps,
62/// consistent with the policy that inherited `ORT_DYLIB_PATH` fails closed.
63///
64/// Call at process startup, before any thread spawns, runtime binding or ORT
65/// use. Removing a process environment variable requires exclusive access to
66/// the environment; this function does not unload an already-loaded runtime.
67pub fn clear_inherited_runtime_for_release() {
68    clear_inherited_runtime_with(|| std::env::remove_var("ORT_DYLIB_PATH"));
69}
70
71// Injected removal keeps release/debug tests free of process env mutation.
72fn clear_inherited_runtime_with(remove_path: impl FnOnce()) {
73    if cfg!(not(debug_assertions)) {
74        remove_path();
75    }
76}
77
78/// Read a caller-named development override as an explicit runtime candidate.
79/// Unset or empty values return `None`; release builds always return `None`
80/// without reading the variable. Path validation remains in runtime resolution.
81pub fn developer_override_candidate(env_name: &str) -> Option<RuntimeCandidate> {
82    if cfg!(debug_assertions) {
83        nonempty_env_path(env_name)
84            .map(|path| RuntimeCandidate::new(path, CandidateSource::Explicit))
85    } else {
86        None
87    }
88}
89
90/// Return the last path successfully bound by this crate for this process,
91/// falling back to a non-empty `ORT_DYLIB_PATH` when no binding is recorded.
92/// Read-only: no resolution, file check, environment mutation or ORT loading.
93/// Missing-mode bindings are included; this is not proof of a loaded library.
94pub fn configured_runtime() -> Option<PathBuf> {
95    let recorded = CONFIGURED_RUNTIME
96        .lock()
97        .unwrap_or_else(|poisoned| poisoned.into_inner())
98        .clone();
99    recorded.or_else(|| nonempty_env_path("ORT_DYLIB_PATH"))
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum CandidateSource {
104    Explicit,
105    Bundled,
106    AppData,
107    /// Caller-opted-in `<workspace>/tools/bin/<runtime filename>`.
108    LegacyDefault,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct RuntimeCandidate {
113    pub path: PathBuf,
114    pub source: CandidateSource,
115}
116
117impl RuntimeCandidate {
118    pub fn new(path: impl Into<PathBuf>, source: CandidateSource) -> Self {
119        Self {
120            path: path.into(),
121            source,
122        }
123    }
124}
125
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct CandidateDiagnostic {
128    pub path: PathBuf,
129    pub source: CandidateSource,
130    pub absolute: bool,
131    pub expected_filename: bool,
132    pub exists: bool,
133    pub is_file: bool,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct RuntimeSelection {
138    pub path: PathBuf,
139    pub source: CandidateSource,
140    pub diagnostics: Vec<CandidateDiagnostic>,
141}
142
143/// Binding policy; defaults retain strict file validation & conflict refusal.
144#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
145pub struct RuntimeBindingOptions {
146    /// Overwrite ORT_DYLIB_PATH, including an invalid existing value. This does
147    /// not replace a library already loaded by ort. Use before any ORT use.
148    pub force_replace: bool,
149    /// If no regular file resolves, bind the first missing candidate. Existing
150    /// directories, invalid filenames & unsafe paths remain errors.
151    pub allow_missing: bool,
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum RuntimeBindingStatus {
156    Ready,
157    /// Path recorded & bound, but absent. Callers own degraded-mode messaging.
158    Missing,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct RuntimeBinding {
163    pub selection: RuntimeSelection,
164    pub status: RuntimeBindingStatus,
165}
166
167#[derive(Debug, Clone, PartialEq, Eq)]
168pub enum RuntimeError {
169    NoCandidates,
170    UnsafePath {
171        path: PathBuf,
172    },
173    UnexpectedFilename {
174        path: PathBuf,
175        expected: &'static str,
176    },
177    NotFound {
178        diagnostics: Vec<CandidateDiagnostic>,
179    },
180    Canonicalize {
181        path: PathBuf,
182        message: String,
183    },
184    AlreadyConfigured {
185        configured: PathBuf,
186        selected: PathBuf,
187    },
188    /// The Windows-ML copy in `System32` hangs at session init; apps must
189    /// supply their bundled runtime instead.
190    SystemRuntimeRejected {
191        path: PathBuf,
192    },
193}
194
195impl fmt::Display for RuntimeError {
196    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197        match self {
198            Self::NoCandidates => write!(f, "no ONNX Runtime candidates were supplied"),
199            Self::UnsafePath { path } => {
200                write!(f, "runtime path must be absolute: {}", path.display())
201            }
202            Self::UnexpectedFilename { path, expected } => {
203                write!(f, "runtime path {} must end in {expected}", path.display())
204            }
205            Self::NotFound { .. } => {
206                write!(f, "no supplied ONNX Runtime candidate is a regular file")
207            }
208            Self::Canonicalize { path, message } => {
209                write!(f, "cannot canonicalize {}: {message}", path.display())
210            }
211            Self::SystemRuntimeRejected { path } => write!(
212                f,
213                "runtime {} is the OS-provided copy; bundle and supply an app runtime",
214                path.display()
215            ),
216            Self::AlreadyConfigured {
217                configured,
218                selected,
219            } => write!(
220                f,
221                "ORT_DYLIB_PATH is already {}, refusing replacement with {}",
222                configured.display(),
223                selected.display()
224            ),
225        }
226    }
227}
228
229impl std::error::Error for RuntimeError {}
230
231pub fn runtime_filename() -> &'static str {
232    #[cfg(target_os = "windows")]
233    {
234        "onnxruntime.dll"
235    }
236    #[cfg(target_os = "macos")]
237    {
238        "libonnxruntime.dylib"
239    }
240    #[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
241    {
242        "libonnxruntime.so"
243    }
244}
245
246pub fn inspect_candidate(candidate: &RuntimeCandidate) -> CandidateDiagnostic {
247    CandidateDiagnostic {
248        path: candidate.path.clone(),
249        source: candidate.source,
250        absolute: candidate.path.is_absolute(),
251        expected_filename: candidate.path.file_name().and_then(|name| name.to_str())
252            == Some(runtime_filename()),
253        exists: candidate.path.exists(),
254        is_file: candidate.path.is_file(),
255    }
256}
257
258pub fn resolve_runtime(
259    candidates: impl IntoIterator<Item = RuntimeCandidate>,
260) -> Result<RuntimeSelection, RuntimeError> {
261    let candidates: Vec<_> = candidates.into_iter().collect();
262    if candidates.is_empty() {
263        return Err(RuntimeError::NoCandidates);
264    }
265    let mut diagnostics = Vec::with_capacity(candidates.len());
266    for candidate in candidates {
267        let diagnostic = inspect_candidate(&candidate);
268        if !diagnostic.absolute {
269            return Err(RuntimeError::UnsafePath {
270                path: candidate.path,
271            });
272        }
273        if !diagnostic.expected_filename {
274            return Err(RuntimeError::UnexpectedFilename {
275                path: candidate.path,
276                expected: runtime_filename(),
277            });
278        }
279        if is_system_runtime(&candidate.path) {
280            return Err(RuntimeError::SystemRuntimeRejected {
281                path: candidate.path,
282            });
283        }
284        diagnostics.push(diagnostic.clone());
285        if diagnostic.is_file {
286            let path =
287                candidate
288                    .path
289                    .canonicalize()
290                    .map_err(|error| RuntimeError::Canonicalize {
291                        path: candidate.path,
292                        message: error.to_string(),
293                    })?;
294            return Ok(RuntimeSelection {
295                path,
296                source: candidate.source,
297                diagnostics,
298            });
299        }
300    }
301    Err(RuntimeError::NotFound { diagnostics })
302}
303
304/// True when the path sits under a Windows `System32`/`SysWOW64` directory.
305/// The OS copy of `onnxruntime.dll` (Windows ML) hangs at session init, so it
306/// is never an acceptable candidate (ScrapeRight `ort_common` finding).
307pub fn is_system_runtime(path: &Path) -> bool {
308    path.components().any(|component| {
309        component
310            .as_os_str()
311            .to_str()
312            .map(|name| {
313                name.eq_ignore_ascii_case("system32") || name.eq_ignore_ascii_case("syswow64")
314            })
315            .unwrap_or(false)
316    })
317}
318
319/// Resources directory of an installed application bundle, derived from its
320/// executable only (no checkout or working-directory fallback):
321/// macOS `Foo.app/Contents/MacOS/foo` -> `Contents/Resources`; Windows the
322/// executable directory; elsewhere `<exe dir>/resources`.
323pub fn installed_resource_dir(executable: &Path) -> Option<PathBuf> {
324    if !executable.is_absolute() {
325        return None;
326    }
327    let dir = executable.parent()?.to_path_buf();
328    let is = |p: &Path, name: &str| {
329        p.file_name()
330            .and_then(|n| n.to_str())
331            .map(|n| n.eq_ignore_ascii_case(name))
332            .unwrap_or(false)
333    };
334    #[cfg(target_os = "macos")]
335    {
336        if is(&dir, "MacOS") {
337            if let Some(contents) = dir.parent() {
338                if is(contents, "Contents") {
339                    return Some(contents.join("Resources"));
340                }
341            }
342        }
343        if is(&dir, "Resources") {
344            return Some(dir);
345        }
346        Some(dir.join("Resources"))
347    }
348    #[cfg(target_os = "windows")]
349    {
350        let _ = is;
351        Some(dir)
352    }
353    #[cfg(not(any(target_os = "macos", target_os = "windows")))]
354    {
355        if is(&dir, "resources") {
356            Some(dir)
357        } else {
358            Some(dir.join("resources"))
359        }
360    }
361}
362
363/// Bundled-runtime candidate for an installed app: `<resources>/<subdir>/<runtime file>`.
364/// Apps pass their own `subdir` (ScrapeRight/HeardRight/CodeRight all use `runtime`).
365pub fn installed_runtime_candidate(executable: &Path, subdir: &str) -> Option<RuntimeCandidate> {
366    let dir = installed_resource_dir(executable)?;
367    Some(RuntimeCandidate::new(
368        dir.join(subdir).join(runtime_filename()),
369        CandidateSource::Bundled,
370    ))
371}
372
373/// Explicit legacy candidate; never searched unless the caller supplies it.
374/// Relative roots are rejected rather than resolved against process cwd.
375pub fn legacy_runtime_candidate(workspace_root: &Path) -> Result<RuntimeCandidate, RuntimeError> {
376    if !workspace_root.is_absolute() {
377        return Err(RuntimeError::UnsafePath {
378            path: workspace_root.to_path_buf(),
379        });
380    }
381    Ok(RuntimeCandidate::new(
382        workspace_root
383            .join("tools")
384            .join("bin")
385            .join(runtime_filename()),
386        CandidateSource::LegacyDefault,
387    ))
388}
389
390/// Configure ORT only after resolving a caller-supplied, absolute regular file.
391/// Existing configuration is preserved and must canonicalize to the same file.
392pub fn configure_runtime(
393    candidates: impl IntoIterator<Item = RuntimeCandidate>,
394) -> Result<RuntimeSelection, RuntimeError> {
395    configure_runtime_with_options(candidates, &RuntimeBindingOptions::default())
396        .map(|binding| binding.selection)
397}
398
399/// Bind explicit candidates under caller-selected policy. Available regular
400/// files win in caller order; missing mode falls back to the first absent path
401/// only when no file resolves. Missing paths retain their supplied absolute
402/// spelling because they cannot be canonicalized.
403///
404/// Call before starting worker threads or using any ORT API: this mutates a
405/// process environment variable, not ort's already-loaded library handle.
406pub fn configure_runtime_with_options(
407    candidates: impl IntoIterator<Item = RuntimeCandidate>,
408    options: &RuntimeBindingOptions,
409) -> Result<RuntimeBinding, RuntimeError> {
410    let binding = configure_runtime_with_env(
411        candidates,
412        options,
413        nonempty_env_path("ORT_DYLIB_PATH"),
414        |path| std::env::set_var("ORT_DYLIB_PATH", path),
415    )?;
416    *CONFIGURED_RUNTIME
417        .lock()
418        .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(binding.selection.path.clone());
419    Ok(binding)
420}
421
422// Injected environment writer avoids process-global mutation in unit tests.
423fn configure_runtime_with_env(
424    candidates: impl IntoIterator<Item = RuntimeCandidate>,
425    options: &RuntimeBindingOptions,
426    configured: Option<PathBuf>,
427    set_path: impl FnOnce(&Path),
428) -> Result<RuntimeBinding, RuntimeError> {
429    let binding = match resolve_runtime(candidates) {
430        Ok(selection) => RuntimeBinding {
431            selection,
432            status: RuntimeBindingStatus::Ready,
433        },
434        Err(RuntimeError::NotFound { diagnostics }) if options.allow_missing => {
435            let Some(missing) = diagnostics.iter().find(|candidate| !candidate.exists) else {
436                return Err(RuntimeError::NotFound { diagnostics });
437            };
438            RuntimeBinding {
439                selection: RuntimeSelection {
440                    path: missing.path.clone(),
441                    source: missing.source,
442                    diagnostics,
443                },
444                status: RuntimeBindingStatus::Missing,
445            }
446        }
447        Err(error) => return Err(error),
448    };
449    if let Some(configured) = configured.filter(|_| !options.force_replace) {
450        if options.allow_missing && configured == binding.selection.path {
451            return Ok(binding);
452        }
453        let configured = canonical_if_file(&configured)?;
454        if configured != binding.selection.path {
455            return Err(RuntimeError::AlreadyConfigured {
456                configured,
457                selected: binding.selection.path,
458            });
459        }
460        return Ok(binding);
461    }
462    set_path(&binding.selection.path);
463    Ok(binding)
464}
465
466fn nonempty_env_path(name: &str) -> Option<PathBuf> {
467    std::env::var_os(name)
468        .filter(|value| !value.is_empty())
469        .map(PathBuf::from)
470}
471
472fn canonical_if_file(path: &Path) -> Result<PathBuf, RuntimeError> {
473    if !path.is_absolute() {
474        return Err(RuntimeError::UnsafePath {
475            path: path.to_path_buf(),
476        });
477    }
478    path.canonicalize()
479        .map_err(|error| RuntimeError::Canonicalize {
480            path: path.to_path_buf(),
481            message: error.to_string(),
482        })
483}
484
485#[cfg(test)]
486mod binding_tests {
487    use super::*;
488    use std::sync::atomic::{AtomicUsize, Ordering};
489
490    #[cfg(debug_assertions)]
491    #[test]
492    fn clearing_inherited_runtime_is_noop_in_debug() {
493        clear_inherited_runtime_with(|| panic!("debug must preserve inherited runtime"));
494    }
495
496    #[cfg(not(debug_assertions))]
497    #[test]
498    fn clearing_inherited_runtime_removes_it_in_release() {
499        let mut inherited = Some(PathBuf::from("shell-exported-dev-runtime"));
500        clear_inherited_runtime_with(|| inherited = None);
501        assert_eq!(inherited, None);
502    }
503
504    struct Fixture(PathBuf);
505
506    impl Fixture {
507        fn new() -> Self {
508            static NEXT: AtomicUsize = AtomicUsize::new(0);
509            let root = std::env::temp_dir().join(format!(
510                "rightkit-ort-binding-{}-{}",
511                std::process::id(),
512                NEXT.fetch_add(1, Ordering::Relaxed)
513            ));
514            std::fs::create_dir_all(&root).unwrap();
515            Self(root.canonicalize().unwrap())
516        }
517
518        fn candidate(&self, directory: &str, exists: bool) -> RuntimeCandidate {
519            let path = self.0.join(directory).join(runtime_filename());
520            if exists {
521                std::fs::create_dir_all(path.parent().unwrap()).unwrap();
522                std::fs::write(&path, b"not a real ORT binary").unwrap();
523            }
524            RuntimeCandidate::new(path, CandidateSource::Explicit)
525        }
526    }
527
528    impl Drop for Fixture {
529        fn drop(&mut self) {
530            let _ = std::fs::remove_dir_all(&self.0);
531        }
532    }
533
534    #[test]
535    fn binding_defaults_preserve_conflicts_and_require_files() {
536        let options = RuntimeBindingOptions::default();
537        assert!(!options.force_replace);
538        assert!(!options.allow_missing);
539        let fixture = Fixture::new();
540        let first = fixture.candidate("first", true);
541        let second = fixture.candidate("second", true);
542        let error = configure_runtime_with_env([second], &options, Some(first.path), |_| {
543            panic!("must not overwrite")
544        })
545        .unwrap_err();
546        assert!(matches!(error, RuntimeError::AlreadyConfigured { .. }));
547        let error = configure_runtime_with_env(
548            [fixture.candidate("missing", false)],
549            &options,
550            None,
551            |_| panic!("must not bind missing path by default"),
552        )
553        .unwrap_err();
554        assert!(matches!(error, RuntimeError::NotFound { .. }));
555    }
556
557    #[test]
558    fn forced_replacement_overwrites_existing_or_invalid_configuration() {
559        let fixture = Fixture::new();
560        let first = fixture.candidate("first", true);
561        let selected = fixture.candidate("second", true);
562        for configured in [first.path, PathBuf::from("invalid-relative-path")] {
563            let mut written = None;
564            let report = configure_runtime_with_env(
565                [selected.clone()],
566                &RuntimeBindingOptions {
567                    force_replace: true,
568                    ..Default::default()
569                },
570                Some(configured),
571                |path| written = Some(path.to_path_buf()),
572            )
573            .unwrap();
574            let expected = selected.path.canonicalize().unwrap();
575            assert_eq!(written, Some(expected.clone()));
576            assert_eq!(report.selection.path, expected);
577            assert_eq!(report.status, RuntimeBindingStatus::Ready);
578        }
579    }
580
581    #[test]
582    fn missing_mode_binds_records_and_reuses_first_absent_candidate() {
583        let fixture = Fixture::new();
584        let first = fixture.candidate("missing-first", false);
585        let second = fixture.candidate("missing-second", false);
586        let options = RuntimeBindingOptions {
587            allow_missing: true,
588            ..Default::default()
589        };
590        let mut written = None;
591        let report = configure_runtime_with_env([first.clone(), second], &options, None, |path| {
592            written = Some(path.to_path_buf())
593        })
594        .unwrap();
595        assert_eq!(report.status, RuntimeBindingStatus::Missing);
596        assert_eq!(report.selection.path, first.path);
597        assert_eq!(written, Some(first.path.clone()));
598        assert_eq!(report.selection.diagnostics.len(), 2);
599        assert!(report
600            .selection
601            .diagnostics
602            .iter()
603            .all(|d| !d.exists && !d.is_file));
604        let repeated =
605            configure_runtime_with_env([first.clone()], &options, Some(first.path), |_| {
606                panic!("matching binding must be preserved")
607            })
608            .unwrap();
609        assert_eq!(repeated.status, RuntimeBindingStatus::Missing);
610    }
611
612    #[test]
613    fn missing_mode_prefers_available_files_and_never_binds_directories() {
614        let fixture = Fixture::new();
615        let available = fixture.candidate("available", true);
616        let options = RuntimeBindingOptions {
617            allow_missing: true,
618            ..Default::default()
619        };
620        let report = configure_runtime_with_env(
621            [fixture.candidate("absent", false), available.clone()],
622            &options,
623            None,
624            |_| {},
625        )
626        .unwrap();
627        assert_eq!(report.status, RuntimeBindingStatus::Ready);
628        assert_eq!(
629            report.selection.path,
630            available.path.canonicalize().unwrap()
631        );
632        let directory = fixture.candidate("directory", false);
633        std::fs::create_dir_all(&directory.path).unwrap();
634        let error = configure_runtime_with_env([directory], &options, None, |_| {
635            panic!("directory must not be bound")
636        })
637        .unwrap_err();
638        assert!(matches!(error, RuntimeError::NotFound { .. }));
639    }
640
641    #[test]
642    fn forced_missing_binding_overwrites_existing_configuration() {
643        let fixture = Fixture::new();
644        let previous = fixture.candidate("previous", true);
645        let missing = fixture.candidate("missing", false);
646        let mut written = None;
647        let report = configure_runtime_with_env(
648            [missing.clone()],
649            &RuntimeBindingOptions {
650                force_replace: true,
651                allow_missing: true,
652            },
653            Some(previous.path),
654            |path| written = Some(path.to_path_buf()),
655        )
656        .unwrap();
657        assert_eq!(report.status, RuntimeBindingStatus::Missing);
658        assert_eq!(written, Some(missing.path));
659    }
660
661    #[test]
662    fn permissive_options_still_reject_unsafe_paths_and_system_runtime() {
663        let fixture = Fixture::new();
664        let options = RuntimeBindingOptions {
665            force_replace: true,
666            allow_missing: true,
667        };
668        for (candidate, expected) in [
669            (
670                RuntimeCandidate::new(runtime_filename(), CandidateSource::Explicit),
671                "relative",
672            ),
673            (
674                RuntimeCandidate::new(fixture.0.join("wrong.bin"), CandidateSource::Explicit),
675                "filename",
676            ),
677            (fixture.candidate("System32", false), "system"),
678        ] {
679            let error = configure_runtime_with_env([candidate], &options, None, |_| {
680                panic!("unsafe candidate must not be bound")
681            })
682            .unwrap_err();
683            assert!(match expected {
684                "relative" => matches!(error, RuntimeError::UnsafePath { .. }),
685                "filename" => matches!(error, RuntimeError::UnexpectedFilename { .. }),
686                _ => matches!(error, RuntimeError::SystemRuntimeRejected { .. }),
687            });
688        }
689    }
690}