supercode-harness 0.4.16

The optional native Supercode agent and tool harness
Documentation
//! ONT-4: the world doors, in one implementation.
//!
//! `harness.v1.world.load|save|compile|decompile|import|export` and
//! `supercode world <verb>` are two transports over the functions here, which
//! are themselves a thin wrapper over the ONT-3 codecs
//! (`supercode_interchange::world::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::world::codec::folder::OWNED_FILES;
use supercode_interchange::world::codec::{
    carry_unmodeled, from_hermes, from_openclaw, load_home, save_home, to_hermes, to_openclaw,
    Flavor, LoadedHome, Refusal,
};
use supercode_interchange::world::World;

use crate::Result;

/// Which layout a folder is read as (`harness.v1.world.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 world 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 a world is compiled from or decompiled back to.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WorldHarness {
    /// 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 world, and the vault's key names.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorldRead {
    /// The world value.
    pub world: World,
    /// The `.env` names the world'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 WorldSaved {
    /// Always true; a failure is an error, never a `false`.
    pub written: bool,
    /// The folder the world was written to.
    pub root: PathBuf,
}

/// What a decompile did.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorldDecompiled {
    /// 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 world as saved, the vault's key names, and the
/// unmodeled files carried by path.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorldImported {
    /// The world value, as written into `root`.
    pub world: World,
    /// The `.env` names the world's secret refs point at — names only.
    pub vault_keys: Vec<String>,
    /// Our folder.
    pub root: PathBuf,
    /// Files the world 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 a world 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(world: &mut World, root: &Path) {
    world.root = root.to_path_buf();
    for (name, profile) in world.profiles.iter_mut() {
        profile.dir = if name == "default" {
            root.to_path_buf()
        } else {
            root.join("profiles").join(name)
        };
    }
}

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

/// `harness.v1.world.save`: write a world 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 world
/// 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, world: World, vault: BTreeMap<String, String>) -> Result<WorldSaved> {
    let mut loaded = if root.is_dir() {
        load_home(root, Flavor::Orchestrator)?
    } else {
        LoadedHome {
            world: world.clone(),
            vault: BTreeMap::new(),
            io: BTreeMap::new(),
        }
    };
    loaded.world = world;
    repoint(&mut loaded.world, root);
    loaded.vault.extend(vault);
    save_home(&mut loaded, Some(root))?;
    Ok(WorldSaved {
        written: true,
        root: root.to_path_buf(),
    })
}

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

/// `harness.v1.world.decompile`: write a world back as the source harness's home.
///
/// `source` is the home the world 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 UNI-22
/// gate refuse a live `state.db` instead of guessing at one.
pub fn decompile(
    to: WorldHarness,
    world: World,
    source: &Path,
    source_flavor: SourceFlavor,
    dest: &Path,
    vault: BTreeMap<String, String>,
) -> Result<WorldDecompiled> {
    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
        (WorldHarness::Hermes, SourceFlavor::Orchestrator) => {
            let mut loaded = load_home(source, HomeFlavor::Orchestrator.into())?;
            loaded.world = world;
            loaded.vault.extend(vault);
            let report = to_hermes(&loaded, dest, None)?;
            WorldDecompiled {
                written: report.written,
                refused: report.refused.iter().map(RefusalRow::from).collect(),
                ..WorldDecompiled::default()
            }
        }
        (WorldHarness::Openclaw, SourceFlavor::Orchestrator) => {
            let loaded = supercode_interchange::world::codec::OpenclawLoaded::from_world(world, {
                let mut v = load_home(source, HomeFlavor::Orchestrator.into())?.vault;
                v.extend(vault);
                v
            });
            let report = to_openclaw(&loaded, dest)?;
            WorldDecompiled {
                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),
            }
        }
        (WorldHarness::Hermes, SourceFlavor::Native) => {
            let mut loaded = from_hermes(source)?;
            loaded.world = world;
            loaded.vault.extend(vault);
            let report = to_hermes(&loaded, dest, None)?;
            WorldDecompiled {
                written: report.written,
                refused: report.refused.iter().map(RefusalRow::from).collect(),
                ..WorldDecompiled::default()
            }
        }
        (WorldHarness::Openclaw, SourceFlavor::Native) => {
            let mut loaded = from_openclaw(source)?;
            loaded.world = world;
            loaded.vault.extend(vault);
            let report = to_openclaw(&loaded, dest)?;
            WorldDecompiled {
                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.world.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 world does not model
/// (`MEMORY.md`, `skills/`, an agent's transcripts) are carried by path.
pub fn import(from: WorldHarness, home: &Path, into: &Path) -> Result<WorldImported> {
    let (world, vault, sources): (World, BTreeMap<String, String>, BTreeMap<String, PathBuf>) =
        match from {
            WorldHarness::Hermes => {
                let loaded = from_hermes(home)?;
                let sources = loaded
                    .io
                    .iter()
                    .filter_map(|(name, io)| Some((name.clone(), io.source_dir.clone()?)))
                    .collect();
                (loaded.world, loaded.vault, sources)
            }
            WorldHarness::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.world, loaded.vault, sources)
            }
        };
    let mut loaded = LoadedHome {
        world,
        vault,
        io: BTreeMap::new(),
    };
    repoint(&mut loaded.world, into);
    save_home(&mut loaded, Some(into))?;
    let mut carried = Vec::new();
    for (name, profile) in &loaded.world.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(WorldImported {
        vault_keys: keys(&loaded.vault),
        world: loaded.world,
        root: into.to_path_buf(),
        carried,
    })
}

/// `harness.v1.world.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 the codec's to refuse (UNI-22).
pub fn export(to: WorldHarness, root: &Path, dest: &Path) -> Result<WorldDecompiled> {
    let loaded = load_home(root, Flavor::Orchestrator)?;
    decompile(
        to,
        loaded.world,
        root,
        SourceFlavor::Orchestrator,
        dest,
        loaded.vault,
    )
}