supercode-harness 0.4.20

The optional native Supercode agent and tool harness
Documentation
//! ONT-4: the orchestration doors, in one implementation.
//!
//! `harness.v1.orchestration.load|save|compile|decompile|import|export` and
//! `supercode orchestration <verb>` are two transports over the functions here, which
//! are themselves a thin wrapper over the ONT-3 codecs
//! (`supercode_interchange::orchestration::codec`). Nothing in this module decides
//! anything a codec does not: it picks the codec the caller named, keeps the
//! io bookkeeping a decompile needs, and shapes the answer for the wire.
//!
//! One rule the wire adds: a vault VALUE never leaves. A load or a compile
//! answers with the vault's KEY NAMES only — the caller that needs a value
//! reads the home's own `.env`. That rule is why `import` and `export` exist
//! as verbs of their own: a migration moves credentials between homes, and
//! composed from the value-level verbs by a client it could not — the
//! credential would have to cross the wire. Here it stays in this process.

use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};

use supercode_interchange::ontology::ArtifactFidelity;
use supercode_interchange::orchestration::codec::folder::OWNED_FILES;
use supercode_interchange::orchestration::codec::{
    carry_unmodeled, from_hermes, from_openclaw, load_home, save_home, to_hermes, to_openclaw,
    Flavor, LoadedHome, Refusal,
};
use supercode_interchange::orchestration::Orchestration;

use crate::Result;

/// Which layout a folder is read as (`harness.v1.orchestration.load`'s `flavor`).
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HomeFlavor {
    /// Our own folder.
    #[default]
    Orchestrator,
    /// A Hermes home read in place.
    Hermes,
}

impl From<HomeFlavor> for Flavor {
    fn from(flavor: HomeFlavor) -> Self {
        match flavor {
            HomeFlavor::Orchestrator => Flavor::Orchestrator,
            HomeFlavor::Hermes => Flavor::Hermes,
        }
    }
}

/// What kind of home `decompile`'s `source` is.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SourceFlavor {
    /// The target harness's own home, the one the orchestration was compiled from.
    #[default]
    Native,
    /// Our own folder: refs where the harness reads values, and no session store of the target's.
    Orchestrator,
}

/// Which source harness an orchestration is compiled from or decompiled back to.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OrchestrationHarness {
    /// A Hermes home.
    Hermes,
    /// An OpenClaw state directory.
    Openclaw,
}

/// A refused write, on the wire.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RefusalRow {
    /// The file, relative to the destination home.
    pub file: String,
    /// Why, naming the gate.
    pub reason: String,
}

impl From<&Refusal> for RefusalRow {
    fn from(refusal: &Refusal) -> Self {
        Self {
            file: refusal.file.clone(),
            reason: refusal.reason.clone(),
        }
    }
}

/// What a load or a compile answers: the orchestration, and the vault's key names.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OrchestrationRead {
    /// The orchestration value.
    pub orchestration: Orchestration,
    /// The `.env` names the orchestration's secret refs point at — names only.
    pub vault_keys: Vec<String>,
}

/// What a save answers.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OrchestrationSaved {
    /// Always true; a failure is an error, never a `false`.
    pub written: bool,
    /// The folder the orchestration was written to.
    pub root: PathBuf,
}

/// What a decompile did.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct OrchestrationDecompiled {
    /// Every artifact written, with its tier.
    pub written: Vec<ArtifactFidelity>,
    /// Every write refused, with the gate named.
    pub refused: Vec<RefusalRow>,
    /// What a semantic write gave up (OpenClaw only).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub notes: Vec<String>,
    /// Store rows written back column for column (OpenClaw only).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub rows_byte: Option<usize>,
    /// Store rows re-encoded (OpenClaw only).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub rows_emitted: Option<usize>,
}

/// What an import did: the orchestration as saved, the vault's key names, and the
/// unmodeled files carried by path.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OrchestrationImported {
    /// The orchestration value, as written into `root`.
    pub orchestration: Orchestration,
    /// The `.env` names the orchestration's secret refs point at — names only.
    pub vault_keys: Vec<String>,
    /// Our folder.
    pub root: PathBuf,
    /// Files the orchestration does not model, copied byte for byte (relative to `root`).
    pub carried: Vec<String>,
}

fn keys(vault: &BTreeMap<String, String>) -> Vec<String> {
    vault.keys().cloned().collect()
}

/// Point an orchestration at the folder it is about to be written to, so the record and
/// the disk agree afterwards. `dir` is bookkeeping, not part of any artifact's
/// record, so this never forces a re-emit.
fn repoint(orchestration: &mut Orchestration, root: &Path) {
    orchestration.root = root.to_path_buf();
    for (name, profile) in orchestration.profiles.iter_mut() {
        profile.dir = if name == "default" {
            root.to_path_buf()
        } else {
            root.join("profiles").join(name)
        };
    }
}

/// `harness.v1.orchestration.load`: read a home folder as one orchestration value.
pub fn load(root: &Path, flavor: HomeFlavor) -> Result<OrchestrationRead> {
    let loaded = load_home(root, flavor.into())?;
    Ok(OrchestrationRead {
        vault_keys: keys(&loaded.vault),
        orchestration: loaded.orchestration,
    })
}

/// `harness.v1.orchestration.save`: write an orchestration into our own folder.
///
/// An existing root is loaded first: its `io` bookkeeping is what tells the
/// encoder which artifacts are unchanged, so a save of an unmodified orchestration
/// leaves every byte alone. `vault` is merged into the loaded one — a caller
/// that sends no secrets keeps the home's own `.env`.
pub fn save(
    root: &Path,
    orchestration: Orchestration,
    vault: BTreeMap<String, String>,
) -> Result<OrchestrationSaved> {
    let mut loaded = if root.is_dir() {
        load_home(root, Flavor::Orchestrator)?
    } else {
        LoadedHome {
            orchestration: orchestration.clone(),
            vault: BTreeMap::new(),
            io: BTreeMap::new(),
        }
    };
    loaded.orchestration = orchestration;
    repoint(&mut loaded.orchestration, root);
    loaded.vault.extend(vault);
    save_home(&mut loaded, Some(root))?;
    Ok(OrchestrationSaved {
        written: true,
        root: root.to_path_buf(),
    })
}

/// `harness.v1.orchestration.compile`: read another harness's home as one orchestration value.
pub fn compile(from: OrchestrationHarness, home: &Path) -> Result<OrchestrationRead> {
    Ok(match from {
        OrchestrationHarness::Hermes => {
            let loaded = from_hermes(home)?;
            OrchestrationRead {
                vault_keys: keys(&loaded.vault),
                orchestration: loaded.orchestration,
            }
        }
        OrchestrationHarness::Openclaw => {
            let loaded = from_openclaw(home)?;
            OrchestrationRead {
                vault_keys: keys(&loaded.vault),
                orchestration: loaded.orchestration,
            }
        }
    })
}

/// `harness.v1.orchestration.decompile`: write an orchestration back as the source harness's home.
///
/// `source` is the home the orchestration was compiled from. It is re-compiled here
/// for one reason: the io bookkeeping. That is what lets an artifact whose
/// record has not changed be reused byte for byte, and what lets the codec
/// refuse a live `state.db` (UNI-18's write) instead of guessing at one.
pub fn decompile(
    to: OrchestrationHarness,
    orchestration: Orchestration,
    source: &Path,
    source_flavor: SourceFlavor,
    dest: &Path,
    vault: BTreeMap<String, String>,
) -> Result<OrchestrationDecompiled> {
    Ok(match (to, source_flavor) {
        // our own folder on its way out: its bytes are ours (refs, not values),
        // so the codec re-emits credentials from the vault and refuses the
        // session half by construction
        (OrchestrationHarness::Hermes, SourceFlavor::Orchestrator) => {
            let mut loaded = load_home(source, HomeFlavor::Orchestrator.into())?;
            loaded.orchestration = orchestration;
            loaded.vault.extend(vault);
            let report = to_hermes(&loaded, dest, None)?;
            OrchestrationDecompiled {
                written: report.written,
                refused: report.refused.iter().map(RefusalRow::from).collect(),
                ..OrchestrationDecompiled::default()
            }
        }
        (OrchestrationHarness::Openclaw, SourceFlavor::Orchestrator) => {
            let loaded =
                supercode_interchange::orchestration::codec::OpenclawLoaded::from_orchestration(
                    orchestration,
                    {
                        let mut v = load_home(source, HomeFlavor::Orchestrator.into())?.vault;
                        v.extend(vault);
                        v
                    },
                );
            let report = to_openclaw(&loaded, dest)?;
            OrchestrationDecompiled {
                written: report.written,
                refused: report.refused.iter().map(RefusalRow::from).collect(),
                notes: report.notes,
                rows_byte: Some(report.rows_byte),
                rows_emitted: Some(report.rows_emitted),
            }
        }
        (OrchestrationHarness::Hermes, SourceFlavor::Native) => {
            let mut loaded = from_hermes(source)?;
            loaded.orchestration = orchestration;
            loaded.vault.extend(vault);
            let report = to_hermes(&loaded, dest, None)?;
            OrchestrationDecompiled {
                written: report.written,
                refused: report.refused.iter().map(RefusalRow::from).collect(),
                ..OrchestrationDecompiled::default()
            }
        }
        (OrchestrationHarness::Openclaw, SourceFlavor::Native) => {
            let mut loaded = from_openclaw(source)?;
            loaded.orchestration = orchestration;
            loaded.vault.extend(vault);
            let report = to_openclaw(&loaded, dest)?;
            OrchestrationDecompiled {
                written: report.written,
                refused: report.refused.iter().map(RefusalRow::from).collect(),
                notes: report.notes,
                rows_byte: Some(report.rows_byte),
                rows_emitted: Some(report.rows_emitted),
            }
        }
    })
}

/// `harness.v1.orchestration.import`: another harness's home becomes our folder.
///
/// A compile followed by a save, with the credentials along: the source's
/// secret values land in our `.env` and every other file carries a ref. Every
/// artifact is emitted canonically — the source's bytes are another
/// harness's, never reused as ours — and the files the orchestration does not model
/// (`MEMORY.md`, `skills/`, an agent's transcripts) are carried by path.
pub fn import(
    from: OrchestrationHarness,
    home: &Path,
    into: &Path,
) -> Result<OrchestrationImported> {
    let (orchestration, vault, sources): (
        Orchestration,
        BTreeMap<String, String>,
        BTreeMap<String, PathBuf>,
    ) = match from {
        OrchestrationHarness::Hermes => {
            let loaded = from_hermes(home)?;
            let sources = loaded
                .io
                .iter()
                .filter_map(|(name, io)| Some((name.clone(), io.source_dir.clone()?)))
                .collect();
            (loaded.orchestration, loaded.vault, sources)
        }
        OrchestrationHarness::Openclaw => {
            let loaded = from_openclaw(home)?;
            // the root profile's unmodeled files are listed from the state
            // dir; a named agent's from `agents/<id>/`
            let sources = loaded
                .profiles
                .iter()
                .map(|(name, io)| {
                    let src = if name == "default" {
                        loaded.root.state_dir.clone()
                    } else {
                        io.source_dir.clone()
                    };
                    (name.clone(), src)
                })
                .collect();
            (loaded.orchestration, loaded.vault, sources)
        }
    };
    let mut loaded = LoadedHome {
        orchestration,
        vault,
        io: BTreeMap::new(),
    };
    repoint(&mut loaded.orchestration, into);
    save_home(&mut loaded, Some(into))?;
    let mut carried = Vec::new();
    for (name, profile) in &loaded.orchestration.profiles {
        let Some(src) = sources.get(name) else {
            continue;
        };
        // a file the SOURCE does not model may share its name with an
        // artifact we own (OpenClaw's legacy `cron/jobs.json` is a store key
        // to it and a jobs file to us); a carried byte never overwrites an
        // owned artifact, and a re-import refreshes every other carried file
        let files: Vec<String> = profile
            .residue
            .files
            .iter()
            .filter(|rel| !OWNED_FILES.contains(&rel.as_str()))
            .cloned()
            .collect();
        for rel in carry_unmodeled(&files, src, &profile.dir)? {
            carried.push(if name == "default" {
                rel
            } else {
                format!("profiles/{name}/{rel}")
            });
        }
    }
    Ok(OrchestrationImported {
        vault_keys: keys(&loaded.vault),
        orchestration: loaded.orchestration,
        root: into.to_path_buf(),
        carried,
    })
}

/// `harness.v1.orchestration.export`: our folder becomes another harness's home.
///
/// A load followed by a decompile from our flavor, with the credentials
/// along: the values our `.env` holds are written where the harness reads
/// them. The session half is written into a fresh destination (ONT-8) and
/// refused into a live one (UNI-18).
pub fn export(
    to: OrchestrationHarness,
    root: &Path,
    dest: &Path,
) -> Result<OrchestrationDecompiled> {
    let loaded = load_home(root, Flavor::Orchestrator)?;
    decompile(
        to,
        loaded.orchestration,
        root,
        SourceFlavor::Orchestrator,
        dest,
        loaded.vault,
    )
}