Gunnar

nordisk/heimdall

crates/heimdall-core/src/runner.rs

// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (C) 2026 Ignalina ApS and Vetra Automation AB

//! The job API every runner implements (milestone 0, stable from 0.1.0:
//! additions only). Heimdall is a defensive sandbox: a runner runs one
//! untrusted job so that it can reach only its own input and output, and
//! answers with a result signed by a per-boot key that never enters a job.

use serde::{Deserialize, Serialize};

use crate::signed::{ResultError, SignedResult};
use crate::types::{Features, Job};

pub use ed25519_dalek::VerifyingKey;

/// Something that runs jobs: today `heimdall_plain::PlainYoukiRunner`, later
/// the protected Heimdall runner. Callers code against this trait only.
pub trait JobRunner: Send + Sync {
    /// Run one job to the end (blocking; callers put it on a job queue).
    ///
    /// `Ok` is a signed result, Accepted or Refused. `Err` means nothing ran
    /// to a verdict (bad job, unsupported kind, broken host): nothing is signed.
    fn run(&self, job: Job) -> Result<SignedResult, RunError>;

    /// The per-boot verifying key. The caller pins it and passes it to
    /// [`crate::verify`].
    fn verifying_key(&self) -> VerifyingKey;

    /// What protects this runner's jobs, stated truthfully. Every signed
    /// result carries the same value in `runner`.
    fn features(&self) -> Features;
}

/// Why a job did not run to a verdict. Nothing is signed.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
pub enum RunError {
    /// This runner does not run this kind of job (e.g. wasm on youki-plain)
    /// or this policy (e.g. a network peer without a broker).
    #[error("unsupported by this runner: {what}")]
    Unsupported { what: String },
    /// The job itself is malformed (bad digest, bad paths, bad limits,
    /// `output_dir` not empty).
    #[error("invalid job: {reason}")]
    InvalidJob { reason: String },
    /// The pinned image could not be found or does not match its digest.
    #[error("image: {reason}")]
    Image { reason: String },
    /// The host could not set the job up (namespaces, cgroups, runtime).
    #[error("setup: {reason}")]
    Setup { reason: String },
    /// A host I/O error outside the job.
    #[error("i/o: {reason}")]
    Io { reason: String },
    /// The result could not be signed.
    #[error("the result could not be signed: {0}")]
    Sign(#[from] ResultError),
}