Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/dwarves

crates/dwarves-slsa/src/release_mode.rs

//! ★ Release mode (lane RELBUILD-ACCT, owner 2026-10-29): the release runner runs as
//! its own account on dwarf's host, never as the lanes' account.
//!
//! The lanes' account (`rickard`) could otherwise alter the dwarves binary or a run
//! directory before the owner signs. In release mode:
//!
//! * the runner runs only as [`Config::account`] (`relbuild`): never as root, never as
//!   the lanes' account, never as any account its config refuses ([`check_account`]);
//! * it runs only as the installed binary, root-owned and read-only, with the sha256
//!   its config names ([`check_binary_meta`]);
//! * it takes requests only from the inbox ([`Config::inbox`]), which a lane can write.
//!   A request is DATA: a release at pinned commits ([`Request`]). The runner's config,
//!   not the request, decides the image and where the sources are cloned from
//!   ([`parse_request`]);
//! * it writes runs only under [`Config::runs`], which it owns and nobody else can
//!   write ([`check_runs_meta`], [`run_dir_for`]); each request's bytes and status sit in
//!   `runs/requests/<sha256>.{request.toml,status}`;
//! * the provenance's builder details name the account, its uid, the mode, the binary
//!   and the request's sha256 ([`release_builder_version`]); `runnerSha256` is the
//!   binary's sha256 (every run records it).
//!
//! The account is made on the host by `script-relbuild-user.sh` (the owner runs it as
//! root); signing stays on the owner's YubiKey.

use std::collections::BTreeMap;
use std::io::Read as _;
use std::os::unix::fs::MetadataExt as _;
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};

use crate::runner::{self, Executor, RunOptions, DEFAULT_IMAGE};
use crate::spec::{ReleaseSpec, DEFAULT_FORGE, DEFAULT_WEB};
use crate::subjects::{sha256_file, sha256_hex};
use crate::{bail, Error, Result};

/// The account the release runner runs as.
pub const RELEASE_ACCOUNT: &str = "relbuild";
/// The lanes' account: never the release runner.
pub const LANES_ACCOUNT: &str = "rickard";
/// The runner's config on the host (root-owned; written by `script-relbuild-user.sh`).
pub const DEFAULT_CONFIG: &str = "/etc/dwarves-release/runner.toml";
/// Where lanes write requests (data only).
pub const DEFAULT_INBOX: &str = "/srv/relbuild/inbox";
/// Where the runner writes runs (the lanes can read, not write).
pub const DEFAULT_RUNS: &str = "/srv/relbuild/runs";
/// Public repositories, cloned without a key.
pub const DEFAULT_RELEASE_FORGE: &str = "ssh://anonymous@git.gunnar.rs:2222";
/// The release runner's container engine (its own podman API socket).
pub const DEFAULT_ENGINE: &str = "unix:///run/dwarves-release/podman.sock";
/// Where the owner's signed bundles come back (`<inbox>/bundles/<request sha256>.intoto.jsonl`).
pub const BUNDLES: &str = "bundles";
/// The release runner's Runa history (the forge's warehouse layout) under the runs.
pub const WAREHOUSE: &str = "warehouse";
/// The Runa project every release run is recorded under.
pub const PROJECT: &str = "release";

/// ★ The Runa run id of a request: `<release>-<first 12 hex of the request's sha256>`,
/// known as soon as the request is (the forge's history shows it from then on).
pub fn run_id_for(release: &str, request_sha256: &str) -> String {
    format!("{release}-{}", &request_sha256[..request_sha256.len().min(12)])
}

/// The `releaseMode` the builder details record.
pub const MODE_DEDICATED_ACCOUNT: &str = "dedicated-account";

/// An account: uid and name.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Identity {
    pub uid: u32,
    pub name: String,
}

/// The owner and mode (`st_mode`, type bits included) of a file or directory.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FileMeta {
    pub uid: u32,
    pub mode: u32,
}

impl FileMeta {
    pub fn of(p: &Path) -> Result<Self> {
        let m = std::fs::symlink_metadata(p).map_err(|e| Error(format!("{}: {e}", p.display())))?;
        Ok(Self {
            uid: m.uid(),
            mode: m.mode(),
        })
    }
    fn is_dir(&self) -> bool {
        self.mode & 0o170000 == 0o040000
    }
    fn is_file(&self) -> bool {
        self.mode & 0o170000 == 0o100000
    }
    fn others_can_write(&self) -> bool {
        self.mode & 0o022 != 0
    }
}

/// ★ The runner's config (`/etc/dwarves-release/runner.toml`). Unknown keys are refused.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Config {
    /// The only account the runner runs as.
    pub account: String,
    /// Accounts refused even if named in `account` (the lanes' account and root).
    pub refuse_accounts: Vec<String>,
    pub inbox: PathBuf,
    pub runs: PathBuf,
    /// The build image, by digest: every request must name exactly this one.
    pub image: String,
    /// The installed runner binary (release mode refuses any other).
    pub binary: Option<PathBuf>,
    /// Its sha256, as the install script printed it.
    pub binary_sha256: Option<String>,
    /// Where the sources are cloned from (the request cannot choose).
    pub forge: String,
    /// Per org (private repositories: the runner's own read key).
    pub org_forge: BTreeMap<String, String>,
    /// The container engine's socket (the release runner's own rootless podman API,
    /// `unix:///run/dwarves-release/podman.sock`): every sealed container of a release
    /// pipeline runs there. Exported as `DOCKER_HOST` for the forge's engine.
    pub engine: String,
    pub poll_seconds: u64,
    pub jobs: u32,
    pub max_request_bytes: u64,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            account: RELEASE_ACCOUNT.into(),
            refuse_accounts: vec![LANES_ACCOUNT.into(), "root".into()],
            inbox: DEFAULT_INBOX.into(),
            runs: DEFAULT_RUNS.into(),
            image: DEFAULT_IMAGE.into(),
            binary: None,
            binary_sha256: None,
            forge: DEFAULT_RELEASE_FORGE.into(),
            org_forge: BTreeMap::new(),
            engine: DEFAULT_ENGINE.into(),
            poll_seconds: 30,
            jobs: 4,
            max_request_bytes: 1 << 20,
        }
    }
}

impl Config {
    pub fn parse(text: &str) -> Result<Self> {
        let c: Self =
            toml::from_str(text).map_err(|e| Error(format!("release-mode config: {e}")))?;
        for f in std::iter::once(&c.forge).chain(c.org_forge.values()) {
            if !f.starts_with("ssh://") && !f.starts_with("https://") {
                bail!("release-mode config: forge {f:?}: ssh:// or https://");
            }
        }
        if !c.engine.starts_with("unix:///") {
            bail!("release-mode config: engine {:?}: a unix:/// socket", c.engine);
        }
        Ok(c)
    }

    pub fn load(path: &Path) -> Result<Self> {
        Self::parse(
            &std::fs::read_to_string(path)
                .map_err(|e| Error(format!("{}: {e}", path.display())))?,
        )
    }
}

/// This process's account: the effective uid (equal to the real one) and its name.
pub fn current_identity() -> Result<Identity> {
    let status = std::fs::read_to_string("/proc/self/status")?;
    let uids: Vec<u32> = status
        .lines()
        .find_map(|l| l.strip_prefix("Uid:"))
        .map(|l| {
            l.split_whitespace()
                .filter_map(|x| x.parse().ok())
                .collect()
        })
        .unwrap_or_default();
    let (real, eff) = match uids.as_slice() {
        [r, e, ..] => (*r, *e),
        _ => bail!("/proc/self/status names no uid"),
    };
    if real != eff {
        bail!("real uid {real} and effective uid {eff} differ");
    }
    let name = std::fs::read_to_string("/etc/passwd")
        .ok()
        .and_then(|p| {
            p.lines().find_map(|l| {
                let f: Vec<&str> = l.split(':').collect();
                (f.len() > 2 && f[2].parse() == Ok(eff)).then(|| f[0].to_string())
            })
        })
        .unwrap_or_else(|| eff.to_string());
    Ok(Identity { uid: eff, name })
}

/// ★ Release mode runs as the configured account only: never root (uid 0), never the
/// lanes' account, never one the config refuses, and the config cannot name those.
pub fn check_account(cfg: &Config, id: &Identity) -> Result<()> {
    if id.uid == 0 {
        bail!(
            "release mode never runs as root (uid 0); it runs as {}",
            cfg.account
        );
    }
    for a in [cfg.account.as_str(), id.name.as_str()] {
        if a == LANES_ACCOUNT || a == "root" || cfg.refuse_accounts.iter().any(|r| r == a) {
            bail!(
                "release mode refuses account {a:?} (the lanes' account {LANES_ACCOUNT:?}, root and the config's refused accounts never run release builds); it runs as {RELEASE_ACCOUNT:?}"
            );
        }
    }
    if id.name != cfg.account {
        bail!(
            "release mode runs as {:?} only; this is {:?} (uid {})",
            cfg.account,
            id.name,
            id.uid
        );
    }
    Ok(())
}

/// ★ The running binary is the installed one: the config's path, a regular file owned
/// by root that only root can write, in a directory only root can write, with the
/// config's sha256.
pub fn check_binary_meta(
    cfg: &Config,
    exe: &Path,
    file: &FileMeta,
    dir: &FileMeta,
    sha256: &str,
) -> Result<()> {
    let Some(want) = &cfg.binary else {
        bail!("release mode needs `binary` (the installed runner) in its config");
    };
    if exe != want {
        bail!(
            "this runner is {}, not the installed release runner {}",
            exe.display(),
            want.display()
        );
    }
    if !file.is_file() || file.uid != 0 || file.others_can_write() {
        bail!(
            "{}: the release runner must be a regular file owned by root and writable by root only (uid {}, mode {:o})",
            exe.display(),
            file.uid,
            file.mode
        );
    }
    if !dir.is_dir() || dir.uid != 0 || dir.others_can_write() {
        bail!(
            "{}'s directory must be owned by root and writable by root only (uid {}, mode {:o})",
            exe.display(),
            dir.uid,
            dir.mode
        );
    }
    if let Some(h) = &cfg.binary_sha256 {
        if h != sha256 {
            bail!(
                "{}: sha256 {sha256} is not the installed runner's {h}",
                exe.display()
            );
        }
    }
    Ok(())
}

/// ★ The runs directory is a directory the runner owns and nobody else can write.
pub fn check_runs_meta(id: &Identity, m: &FileMeta) -> Result<()> {
    if !m.is_dir() || m.uid != id.uid || m.others_can_write() {
        bail!(
            "the runs directory must be a directory owned by {} (uid {}) and writable by it only (uid {}, mode {:o})",
            id.name,
            id.uid,
            m.uid,
            m.mode
        );
    }
    Ok(())
}

/// ★ A request: build release X at commits Y. Data only.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Request {
    #[serde(default)]
    pub rebuild_published: bool,
    pub spec: ReleaseSpec,
}

impl Request {
    pub fn to_toml(&self) -> Result<String> {
        toml::to_string(self).map_err(|e| Error(format!("request: {e}")))
    }
}

/// ★ Parse a request against the runner's config: text, small, only the request's
/// keys, a valid spec, the runner's image; the forge and web are the defaults or
/// unset, and the runner sets the forge from its config.
pub fn parse_request(bytes: &[u8], cfg: &Config) -> Result<Request> {
    if bytes.len() as u64 > cfg.max_request_bytes {
        bail!(
            "the request is {} bytes; at most {}",
            bytes.len(),
            cfg.max_request_bytes
        );
    }
    let text =
        std::str::from_utf8(bytes).map_err(|_| Error("the request is not UTF-8 text".into()))?;
    let table: toml::Table = toml::from_str(text).map_err(|e| Error(format!("request: {e}")))?;
    if let Some(spec) = table.get("spec").and_then(|s| s.as_table()) {
        for (key, default) in [("forge", DEFAULT_FORGE), ("web", DEFAULT_WEB)] {
            if let Some(v) = spec.get(key) {
                if v.as_str() != Some(default) {
                    bail!("the request sets {key} = {v}: the runner's config decides the {key}");
                }
            }
        }
        if spec.contains_key("forge_by_org") {
            bail!("the request sets forge_by_org: the runner's config decides the forge");
        }
    }
    let mut r: Request = toml::from_str(text).map_err(|e| Error(format!("request: {e}")))?;
    r.spec.validate()?;
    if r.spec.image != cfg.image {
        bail!(
            "the request's image {:?} is not the runner's image {:?}",
            r.spec.image,
            cfg.image
        );
    }
    r.spec.forge = cfg.forge.clone();
    r.spec.forge_by_org = cfg.org_forge.clone();
    r.spec.validate()?;
    Ok(r)
}

/// ★ A request file: a regular file (never a link or a directory), the file that was
/// checked is the file that is read, at most `max` bytes.
pub fn read_request_file(path: &Path, max: u64) -> Result<Vec<u8>> {
    let l =
        std::fs::symlink_metadata(path).map_err(|e| Error(format!("{}: {e}", path.display())))?;
    if !l.file_type().is_file() {
        bail!("{}: not a regular file", path.display());
    }
    let f = std::fs::File::open(path)?;
    let m = f.metadata()?;
    if m.dev() != l.dev() || m.ino() != l.ino() || !m.is_file() {
        bail!(
            "{}: not a regular file (it changed while opened)",
            path.display()
        );
    }
    let mut b = Vec::new();
    f.take(max + 1).read_to_end(&mut b)?;
    if b.len() as u64 > max {
        bail!("{}: more than {max} bytes", path.display());
    }
    Ok(b)
}

fn requests_dir(cfg: &Config) -> PathBuf {
    cfg.runs.join("requests")
}

fn status_path(cfg: &Config, sha: &str) -> PathBuf {
    requests_dir(cfg).join(format!("{sha}.status"))
}

fn request_name_ok(n: &str) -> bool {
    n.ends_with(".toml")
        && !n.starts_with('.')
        && n.bytes()
            .all(|c| c.is_ascii_alphanumeric() || b"-_.+".contains(&c))
}

/// ★ The inbox's new requests, by name: `*.toml` (not hidden, plain names), whose bytes
/// have no status yet.
pub fn pending(cfg: &Config) -> Result<Vec<PathBuf>> {
    let mut out = Vec::new();
    for e in
        std::fs::read_dir(&cfg.inbox).map_err(|e| Error(format!("{}: {e}", cfg.inbox.display())))?
    {
        let e = e?;
        let name = e.file_name();
        let Some(n) = name.to_str() else { continue };
        if !request_name_ok(n) {
            continue;
        }
        let p = e.path();
        let Ok(b) = read_request_file(&p, cfg.max_request_bytes) else {
            continue;
        };
        if !status_path(cfg, &sha256_hex(&b)).exists() {
            out.push(p);
        }
    }
    out.sort();
    Ok(out)
}

/// ★ A run's directory: `<runs>/<release>-<utc>`, always directly under the runs directory.
pub fn run_dir_for(cfg: &Config, spec: &ReleaseSpec, now: u64) -> PathBuf {
    cfg.runs
        .join(format!("{}-{}", spec.name, crate::time::compact(now)))
}

/// ★ Release mode's builder details: the caller's (`base`: dwarves' version and
/// commit), the mode, the account and uid, the installed binary and the request's
/// sha256. The runner adds `runnerSha256` (the binary's sha256).
pub fn release_builder_version(
    cfg: &Config,
    id: &Identity,
    request_sha256: &str,
    base: &BTreeMap<String, String>,
) -> BTreeMap<String, String> {
    let mut v = base.clone();
    v.insert("releaseMode".into(), MODE_DEDICATED_ACCOUNT.into());
    v.insert("runnerAccount".into(), id.name.clone());
    v.insert("runnerUid".into(), id.uid.to_string());
    if let Some(b) = &cfg.binary {
        v.insert("runnerBinary".into(), b.display().to_string());
    }
    v.insert("requestSha256".into(), request_sha256.into());
    v
}

/// What became of one request.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RequestState {
    /// Built: the unsigned statement.
    Built(PathBuf),
    /// Not a request the runner takes (nothing built).
    Refused(String),
    /// Taken, and the build went red.
    Red(String),
}

/// One request handled.
#[derive(Debug, Clone)]
pub struct Outcome {
    pub file: PathBuf,
    pub sha256: String,
    pub state: RequestState,
}

/// ★ A request's status file (`runs/requests/<sha256>.status`). `state`: `refused`,
/// `building`, `built` (waiting for the owner's signature), `red`, `signed`.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct Status {
    pub state: String,
    pub request: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub run_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub release: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub run: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub statement: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub statement_sha256: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
    /// Why the last bundle that came back was not taken.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub bundle_error: Option<String>,
    pub at: String,
}

impl Status {
    pub fn parse(text: &str) -> Result<Self> {
        toml::from_str(text).map_err(|e| Error(format!("status: {e}")))
    }
}

/// A request's status, if the runner wrote one.
pub fn read_status(runs: &Path, sha: &str) -> Option<Status> {
    std::fs::read_to_string(runs.join("requests").join(format!("{sha}.status")))
        .ok()
        .and_then(|t| Status::parse(&t).ok())
}

fn write_status(cfg: &Config, sha: &str, s: &Status) -> Result<()> {
    let text = toml::to_string(s).map_err(|e| Error(format!("status: {e}")))?;
    let p = status_path(cfg, sha);
    let tmp = p.with_extension("status.tmp");
    std::fs::write(&tmp, text)?;
    std::fs::rename(&tmp, &p)?;
    Ok(())
}

/// ★ One pass over the inbox: each new request is copied beside its status, refused
/// or built (one at a time), and its status written. Builds go only under the runs
/// directory, from the runner's forge, with the runner's image and container program.
pub fn serve_once(
    cfg: &Config,
    id: &Identity,
    exe_sha256: &str,
    base: &BTreeMap<String, String>,
    exec: &dyn Executor,
) -> Result<Vec<Outcome>> {
    check_account(cfg, id)?;
    std::fs::create_dir_all(requests_dir(cfg))?;
    let mut out = Vec::new();
    for file in pending(cfg)? {
        let bytes = read_request_file(&file, cfg.max_request_bytes)?;
        let sha = sha256_hex(&bytes);
        std::fs::write(
            requests_dir(cfg).join(format!("{sha}.request.toml")),
            &bytes,
        )?;
        let request = file
            .file_name()
            .map(|n| n.to_string_lossy().into_owned())
            .unwrap_or_default();
        let now = || crate::time::rfc3339_secs(crate::time::now_secs());
        let mut status = Status {
            state: "refused".into(),
            request,
            at: now(),
            ..Default::default()
        };
        let state = match parse_request(&bytes, cfg) {
            Err(e) => {
                status.error = Some(e.0.clone());
                write_status(cfg, &sha, &status)?;
                RequestState::Refused(e.0)
            }
            Ok(r) => {
                let run_dir = run_dir_for(cfg, &r.spec, crate::time::now_secs());
                let run_id = run_id_for(&r.spec.name, &sha);
                status.state = "building".into();
                status.run_id = Some(run_id.clone());
                status.release = Some(r.spec.name.clone());
                status.run = Some(run_dir.display().to_string());
                write_status(cfg, &sha, &status)?;
                // The binary serve checked (the runner records the same sha256 itself).
                let mut builder_version = release_builder_version(cfg, id, &sha, base);
                builder_version.insert("runnerSha256".into(), exe_sha256.into());
                let opts = RunOptions {
                    run_dir,
                    from: None,
                    jobs: cfg.jobs,
                    rebuild_published: r.rebuild_published,
                    keep: false,
                    run_id: Some(run_id),
                    builder_version,
                };
                let res = runner::run(&r.spec, &opts, exec);
                status.at = now();
                match res {
                    Ok(st) => {
                        status.state = "built".into();
                        status.statement = Some(st.display().to_string());
                        status.statement_sha256 = sha256_file(&st).ok();
                        write_status(cfg, &sha, &status)?;
                        RequestState::Built(st)
                    }
                    Err(e) => {
                        status.state = "red".into();
                        status.error = Some(e.0.clone());
                        write_status(cfg, &sha, &status)?;
                        RequestState::Red(e.0)
                    }
                }
            }
        };
        out.push(Outcome {
            file,
            sha256: sha,
            state,
        });
    }
    take_bundles(cfg, &mut |m| println!("{m}"))?;
    Ok(out)
}

/// ★ The owner's signed bundles that came back (`<inbox>/bundles/<request sha256>.intoto.jsonl`,
/// put there by the forge after it verified them): each is verified AGAIN here against
/// the release key and the run's own statement and `out/`, then stored as the run's
/// `release.intoto.jsonl`, and the request is `signed`. A bundle that does not verify is
/// not taken; its status says why.
pub fn take_bundles(cfg: &Config, say: &mut dyn FnMut(&str)) -> Result<usize> {
    take_bundles_with(cfg, &crate::release_key::spki()?, say)
}

/// [`take_bundles`] against `spki` (a test's key).
pub fn take_bundles_with(cfg: &Config, spki: &[u8], say: &mut dyn FnMut(&str)) -> Result<usize> {
    let dir = cfg.inbox.join(BUNDLES);
    let Ok(rd) = std::fs::read_dir(&dir) else {
        return Ok(0);
    };
    let mut n = 0;
    for e in rd {
        let p = e?.path();
        let Some(sha) = p
            .file_name()
            .and_then(|f| f.to_str())
            .and_then(|f| f.strip_suffix(".intoto.jsonl"))
            .filter(|s| s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit()))
            .map(str::to_string)
        else {
            continue;
        };
        let Some(mut st) = read_status(&cfg.runs, &sha) else { continue };
        if st.state != "built" {
            continue;
        }
        let Some(run) = st.run.clone().map(PathBuf::from) else { continue };
        let bytes = match read_request_file(&p, cfg.max_request_bytes) {
            Ok(b) => b,
            Err(e) => {
                st.bundle_error = Some(e.0);
                write_status(cfg, &sha, &st)?;
                continue;
            }
        };
        let fp = std::fs::read_to_string(run.join("release.intoto.jsonl")).ok();
        if fp.as_deref().map(str::as_bytes) == Some(&bytes[..]) {
            continue;
        }
        match crate::publish::verify_bundle_for_run(&run, &bytes, spki) {
            Ok(v) => {
                std::fs::write(run.join("release.intoto.jsonl"), &bytes)?;
                st.state = "signed".into();
                st.bundle_error = None;
                st.at = crate::time::rfc3339_secs(crate::time::now_secs());
                write_status(cfg, &sha, &st)?;
                say(&format!("signed  {} ({sha}): release key {}", run.display(), v.keyid));
                n += 1;
            }
            Err(e) => {
                if st.bundle_error.as_deref() != Some(e.0.as_str()) {
                    st.bundle_error = Some(e.0.clone());
                    write_status(cfg, &sha, &st)?;
                    say(&format!("bundle  {} ({sha}) not taken: {e}", p.display()));
                }
            }
        }
    }
    Ok(n)
}

/// How `serve` runs.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServeMode {
    /// Every `poll_seconds`, for ever (the system unit).
    Loop,
    /// One pass over the inbox.
    Once,
    /// Only the checks (account, binary, runs directory, inbox); nothing is built.
    Check,
}

/// ★ The release runner (`dwarves release-build serve`): the account, the installed
/// binary and the runs directory are checked first; then the inbox is served, once or
/// every `poll_seconds`.
pub fn serve(cfg: &Config, mode: ServeMode, base: &BTreeMap<String, String>, exec: &dyn Executor) -> Result<()> {
    let id = current_identity()?;
    check_account(cfg, &id)?;
    let exe = std::env::current_exe()?;
    let exe = std::fs::canonicalize(&exe).unwrap_or(exe);
    let dir = exe
        .parent()
        .ok_or_else(|| Error("the runner has no directory".into()))?;
    let exe_sha = sha256_file(&exe)?;
    check_binary_meta(
        cfg,
        &exe,
        &FileMeta::of(&exe)?,
        &FileMeta::of(dir)?,
        &exe_sha,
    )?;
    check_runs_meta(&id, &FileMeta::of(&cfg.runs)?)?;
    if !cfg.inbox.is_dir() {
        bail!("the inbox {} is not a directory", cfg.inbox.display());
    }
    println!(
        "release runner: account {} (uid {}), binary {} sha256 {exe_sha}, inbox {}, runs {}",
        id.name,
        id.uid,
        exe.display(),
        cfg.inbox.display(),
        cfg.runs.display()
    );
    if mode == ServeMode::Check {
        println!("check: OK (nothing built)");
        return Ok(());
    }
    loop {
        for o in serve_once(cfg, &id, &exe_sha, base, exec)? {
            match &o.state {
                RequestState::Built(st) => println!(
                    "built   {} ({}): {}",
                    o.file.display(),
                    o.sha256,
                    st.display()
                ),
                RequestState::Refused(e) => {
                    println!("refused {} ({}): {e}", o.file.display(), o.sha256)
                }
                RequestState::Red(e) => {
                    println!("RED     {} ({}): {e}", o.file.display(), o.sha256)
                }
            }
        }
        if mode == ServeMode::Once {
            return Ok(());
        }
        std::thread::sleep(std::time::Duration::from_secs(cfg.poll_seconds.max(1)));
    }
}

/// ★ A lane writes a request into the inbox (`dwarves release-build request`): the
/// file appears whole (written hidden, then renamed). Returns its path.
pub fn write_request(inbox: &Path, r: &Request) -> Result<PathBuf> {
    let text = format!(
        "# dwarves release-build request: build release {} at the pinned commits (data only)\n{}",
        r.spec.name,
        r.to_toml()?
    );
    let mut rnd = [0u8; 4];
    if let Ok(mut f) = std::fs::File::open("/dev/urandom") {
        let _ = f.read_exact(&mut rnd);
    }
    let name = format!(
        "{}-{}-{}.toml",
        r.spec.name,
        crate::time::compact(crate::time::now_secs()),
        hex::encode(rnd)
    );
    let tmp = inbox.join(format!(".{name}.tmp"));
    std::fs::write(&tmp, text).map_err(|e| Error(format!("{}: {e}", tmp.display())))?;
    let p = inbox.join(&name);
    std::fs::rename(&tmp, &p)?;
    Ok(p)
}

// ───────────────────── the forge's side: queue, show, attach ─────────────────────

/// ★ A release run as the dwarves forge shows it (`dwarves release-build show`, the
/// Pipeline tab's run view). `state`: `queued`, `building`, `waiting for signature`,
/// `signed`, `failed`, `refused`.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReleaseView {
    pub run_id: String,
    pub release: String,
    pub state: String,
    pub request_sha256: String,
    pub run: Option<String>,
    pub statement: Option<String>,
    pub statement_sha256: Option<String>,
    pub error: Option<String>,
    pub bundle_error: Option<String>,
    pub at: String,
    /// `(name, sha256)` of every subject of the statement.
    pub artifacts: Vec<(String, String)>,
    /// `(repository, commit)`.
    pub sources: Vec<(String, String)>,
    pub builder_id: Option<String>,
    pub runner_account: Option<String>,
    pub runner_sha256: Option<String>,
    /// What the owner runs (on loki, with the YubiKey) while the run waits for him.
    pub sign: Option<String>,
}

/// The word a status file's state is shown as.
pub fn state_word(state: &str) -> &'static str {
    match state {
        "building" => "building",
        "built" => "waiting for signature",
        "signed" => "signed",
        "red" => "failed",
        "refused" => "refused",
        _ => "queued",
    }
}

/// The owner's signing command for a run that waits for his signature.
pub fn sign_command(run: &str, statement_sha256: &str, run_id: &str) -> String {
    format!(
        "on loki: scp dwarf:{run}/release.statement.json . && ~/script-sign-release.sh release.statement.json   (statement sha256 {statement_sha256}); then: dwarves release-build attach {run_id} release.intoto.jsonl"
    )
}

fn view_of(sha: &str, st: &Status) -> ReleaseView {
    let release = st.release.clone().unwrap_or_default();
    let mut v = ReleaseView {
        run_id: st.run_id.clone().unwrap_or_else(|| run_id_for(&release, sha)),
        release,
        state: state_word(&st.state).to_string(),
        request_sha256: sha.to_string(),
        run: st.run.clone(),
        statement: st.statement.clone(),
        statement_sha256: st.statement_sha256.clone(),
        error: st.error.clone(),
        bundle_error: st.bundle_error.clone(),
        at: st.at.clone(),
        ..Default::default()
    };
    if let Some(p) = &st.statement {
        if let Ok(bytes) = std::fs::read(p) {
            if let Ok(val) = serde_json::from_slice::<serde_json::Value>(&bytes) {
                if let Ok(s) = crate::statement::summary(&val) {
                    v.artifacts = s
                        .subjects
                        .iter()
                        .map(|x| (x.name.clone(), x.sha256_hex().unwrap_or("").to_string()))
                        .collect();
                    v.sources = s.repositories.iter().map(|(n, _, c)| (n.clone(), c.clone())).collect();
                    v.builder_id = Some(s.builder_id.clone());
                }
                let bv = val.pointer("/predicate/runDetails/builder/version");
                let get = |k: &str| bv.and_then(|b| b.get(k)).and_then(|x| x.as_str()).map(str::to_string);
                v.runner_account = get("runnerAccount");
                v.runner_sha256 = get("runnerSha256");
            }
        }
    }
    if st.state == "built" {
        if let (Some(run), Some(sum)) = (&st.run, &st.statement_sha256) {
            v.sign = Some(sign_command(run, sum, &v.run_id));
        }
    }
    v
}

/// ★ Every release request the forge knows, newest first: those the runner has taken
/// (their status) and those still in the inbox (`queued`).
pub fn release_views(inbox: &Path, runs: &Path) -> Vec<ReleaseView> {
    let mut out: Vec<ReleaseView> = Vec::new();
    let mut seen = std::collections::BTreeSet::new();
    if let Ok(rd) = std::fs::read_dir(runs.join("requests")) {
        for e in rd.flatten() {
            let n = e.file_name().to_string_lossy().into_owned();
            let Some(sha) = n.strip_suffix(".status") else { continue };
            if let Some(st) = read_status(runs, sha) {
                seen.insert(sha.to_string());
                out.push(view_of(sha, &st));
            }
        }
    }
    if let Ok(rd) = std::fs::read_dir(inbox) {
        for e in rd.flatten() {
            let n = e.file_name().to_string_lossy().into_owned();
            if !request_name_ok(&n) {
                continue;
            }
            let Ok(b) = read_request_file(&e.path(), 1 << 20) else { continue };
            let sha = sha256_hex(&b);
            if seen.contains(&sha) {
                continue;
            }
            let release = std::str::from_utf8(&b)
                .ok()
                .and_then(|t| toml::from_str::<Request>(t).ok())
                .map(|r| r.spec.name)
                .unwrap_or_default();
            let at = e
                .metadata()
                .ok()
                .and_then(|m| m.modified().ok())
                .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
                .map(|d| crate::time::rfc3339_secs(d.as_secs()))
                .unwrap_or_default();
            out.push(ReleaseView {
                run_id: run_id_for(&release, &sha),
                release,
                state: "queued".into(),
                request_sha256: sha,
                at,
                ..Default::default()
            });
        }
    }
    out.sort_by(|a, b| b.at.cmp(&a.at).then(a.run_id.cmp(&b.run_id)));
    out
}

/// One release run by its run id.
pub fn release_view(inbox: &Path, runs: &Path, run_id: &str) -> Option<ReleaseView> {
    release_views(inbox, runs).into_iter().find(|v| v.run_id == run_id)
}

/// ★ The forge queues a request (the only writer of the inbox): checked as data first
/// (a valid spec, the pinned image, the forge left to the runner), then written whole.
/// Returns `(run id, the request file's sha256)`.
pub fn queue_request(inbox: &Path, r: &Request) -> Result<(String, String)> {
    r.spec.validate()?;
    if r.spec.image != DEFAULT_IMAGE {
        bail!("the request's image {:?} is not the release runner's image {DEFAULT_IMAGE:?}", r.spec.image);
    }
    let p = write_request(inbox, r)?;
    let sha = sha256_file(&p)?;
    Ok((run_id_for(&r.spec.name, &sha), sha))
}

/// ★ The owner's signed bundle for `run_id`, verified against `spki` (the release key),
/// the run's own statement and `out/` BEFORE it is stored for the runner
/// (`<inbox>/bundles/<request sha256>.intoto.jsonl`; the runner verifies it again and
/// marks the run signed). Returns the signing key id.
pub fn store_bundle(inbox: &Path, runs: &Path, run_id: &str, bundle: &[u8], spki: &[u8]) -> Result<String> {
    if bundle.len() > 1 << 20 {
        bail!("the bundle is {} bytes; at most 1 MiB", bundle.len());
    }
    let v = release_view(inbox, runs, run_id).ok_or_else(|| Error(format!("no release run {run_id}")))?;
    if v.state != "waiting for signature" {
        bail!("release run {run_id} is {}, not waiting for a signature", v.state);
    }
    let run = v.run.ok_or_else(|| Error(format!("release run {run_id} has no run directory")))?;
    let ok = crate::publish::verify_bundle_for_run(Path::new(&run), bundle, spki)?;
    let dir = inbox.join(BUNDLES);
    std::fs::create_dir_all(&dir)?;
    let name = format!("{}.intoto.jsonl", v.request_sha256);
    let tmp = dir.join(format!(".{name}.tmp"));
    std::fs::write(&tmp, bundle)?;
    std::fs::rename(&tmp, dir.join(&name))?;
    Ok(ok.keyid)
}

/// ★ A release request as the UI sends it: the same inputs as the command line's spec
/// flags, the files' TEXT instead of their paths. The forge turns it into a [`Request`]
/// (the UI holds no logic).
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct RequestInputs {
    /// `--name`
    pub name: String,
    /// The STEPS table: a steps file's text, or a `publish-*.sh` script's (`--steps` /
    /// `--steps-from`).
    pub steps: String,
    /// `--pins`: the pins file's text.
    pub pins: String,
    /// `--org repo=org`
    pub orgs: Vec<String>,
    /// `--lock repo/Cargo.toml`
    pub locks: Vec<String>,
    /// `--binary crate:bin[:feat,feat][:no-default]`
    pub binaries: Vec<String>,
    /// `--rebuild-published`
    pub rebuild_published: bool,
}

impl RequestInputs {
    /// The request these inputs name (the runner's image).
    pub fn to_request(&self) -> Result<Request> {
        let mut orgs = BTreeMap::new();
        for o in &self.orgs {
            let (r, org) = o
                .split_once('=')
                .ok_or_else(|| Error(format!("org {o:?}: repo=org")))?;
            orgs.insert(r.trim().to_string(), org.trim().to_string());
        }
        let steps = if self.steps.contains("STEPS=") {
            crate::cli::steps_of_script(&self.steps)?
        } else {
            self.steps.clone()
        };
        let bins = self
            .binaries
            .iter()
            .map(|b| crate::cli::binary_step(b))
            .collect::<Result<Vec<_>>>()?;
        Ok(Request {
            rebuild_published: self.rebuild_published,
            spec: ReleaseSpec::from_tables(&self.name, DEFAULT_IMAGE, &steps, &self.pins, &orgs, "nordisk", self.locks.clone(), bins)?,
        })
    }
}

/// The text a `Trigger` carries for `release-build`: a whole [`Request`] (`[spec]`), or
/// [`RequestInputs`] (`[inputs]`), in TOML; or `{"inputs": {…}}` in JSON (the UI's).
pub fn parse_inputs_or_request(text: &str) -> Result<Request> {
    if text.trim_start().starts_with('{') {
        #[derive(Deserialize)]
        #[serde(deny_unknown_fields)]
        struct J {
            inputs: RequestInputs,
        }
        let j: J = serde_json::from_str(text).map_err(|e| Error(format!("request inputs: {e}")))?;
        return j.inputs.to_request();
    }
    let table: toml::Table = toml::from_str(text).map_err(|e| Error(format!("request: {e}")))?;
    if let Some(i) = table.get("inputs") {
        let inputs: RequestInputs = i
            .clone()
            .try_into()
            .map_err(|e| Error(format!("request inputs: {e}")))?;
        return inputs.to_request();
    }
    toml::from_str(text).map_err(|e| Error(format!("request: {e}")))
}