use crate::message::ToolSpec;
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Fidelity {
Matches,
Differs,
Unknown,
}
impl Fidelity {
pub fn of(recorded: Option<&str>, live: &[ToolSpec]) -> Fidelity {
match recorded {
None => Fidelity::Unknown,
Some(h) if h == fingerprint(live) => Fidelity::Matches,
Some(_) => Fidelity::Differs,
}
}
pub fn caveat(self) -> Option<&'static str> {
match self {
Fidelity::Matches => None,
Fidelity::Differs => Some(
"the tool surface has changed since this was recorded, so the replay sends \
different bytes ahead of the system prompt",
),
Fidelity::Unknown => Some(
"this was recorded before the tool surface was kept, so how faithfully it \
replays is unknown",
),
}
}
}
pub fn fingerprint(specs: &[ToolSpec]) -> String {
let mut rendered = String::new();
for spec in specs {
rendered.push_str(&spec.name);
rendered.push('\0');
rendered.push_str(&spec.description);
rendered.push('\0');
rendered.push_str(&spec.input_schema.to_string());
rendered.push('\0');
}
crate::learning::rules_hash(&rendered)
}
pub struct SurfaceStore {
root: PathBuf,
}
impl SurfaceStore {
pub fn default_root() -> Result<PathBuf> {
Ok(crate::work::mecha_home()?.join("surfaces"))
}
pub fn open(root: impl Into<PathBuf>) -> Result<Self> {
let root = root.into();
crate::create_private_dir(&root).with_context(|| format!("creating {}", root.display()))?;
Ok(SurfaceStore { root })
}
pub fn open_default() -> Option<Self> {
Self::default_root().ok().and_then(|r| Self::open(r).ok())
}
fn path(&self, hash: &str) -> PathBuf {
self.root.join(format!("{hash}.json"))
}
pub fn record(&self, specs: &[ToolSpec]) -> Result<String> {
let hash = fingerprint(specs);
let path = self.path(&hash);
if path.exists() {
return Ok(hash);
}
let tmp = self.root.join(format!("{hash}.json.tmp"));
std::fs::write(&tmp, serde_json::to_vec_pretty(specs)?)
.with_context(|| format!("writing {}", tmp.display()))?;
std::fs::rename(&tmp, &path)?;
Ok(hash)
}
pub fn load(&self, hash: &str) -> Option<Vec<ToolSpec>> {
let text = std::fs::read_to_string(self.path(hash)).ok()?;
serde_json::from_str(&text).ok()
}
pub fn root(&self) -> &Path {
&self.root
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn spec(name: &str, description: &str) -> ToolSpec {
ToolSpec {
name: name.into(),
description: description.into(),
input_schema: json!({"type": "object"}),
}
}
fn scratch() -> SurfaceStore {
let dir = std::env::temp_dir().join(format!(
"mecha-surface-test-{}-{}",
std::process::id(),
uuid::Uuid::new_v4()
));
SurfaceStore::open(dir).unwrap()
}
#[test]
fn the_surface_store_directory_is_owner_only() {
use std::os::unix::fs::PermissionsExt;
let dir =
std::env::temp_dir().join(format!("mecha-surface-perms-{}", uuid::Uuid::new_v4()));
SurfaceStore::open(&dir).unwrap();
let mode = std::fs::metadata(&dir).unwrap().permissions().mode();
assert_eq!(mode & 0o777, 0o700);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_re_described_tool_is_a_different_surface() {
let before = [spec("fs_read", "Read a file.")];
let after = [spec(
"fs_read",
"Read a file. Paths are workspace-relative.",
)];
assert_ne!(fingerprint(&before), fingerprint(&after));
assert_eq!(
before.iter().map(|s| &s.name).collect::<Vec<_>>(),
after.iter().map(|s| &s.name).collect::<Vec<_>>(),
"…and the names are identical, which is the whole problem"
);
}
#[test]
fn a_changed_schema_is_a_different_surface_too() {
let mut other = spec("todo", "Keep a plan.");
other.input_schema = json!({"type": "object", "properties": {"serves": {}}});
assert_ne!(
fingerprint(&[spec("todo", "Keep a plan.")]),
fingerprint(&[other])
);
}
#[test]
fn the_same_surface_hashes_the_same_twice() {
let s = [spec("a", "x"), spec("b", "y")];
assert_eq!(fingerprint(&s), fingerprint(&s));
let flipped = [spec("b", "y"), spec("a", "x")];
assert_ne!(fingerprint(&s), fingerprint(&flipped));
}
#[test]
fn fingerprint_uses_the_stable_hasher_not_the_std_one() {
let one = spec("build", "Build it.");
let expected = crate::learning::rules_hash("build\0Build it.\0{\"type\":\"object\"}\0");
assert_eq!(fingerprint(&[one]), expected);
}
#[test]
fn a_recording_with_no_hash_is_unknown_and_never_a_match() {
let live = [spec("fs_read", "Read a file.")];
assert_eq!(Fidelity::of(None, &live), Fidelity::Unknown);
assert!(Fidelity::Unknown.caveat().is_some());
assert_ne!(Fidelity::of(None, &live), Fidelity::Matches);
}
#[test]
fn drift_is_detectable_with_nothing_but_the_hash() {
let recorded = fingerprint(&[spec("fs_read", "Read a file.")]);
let live = [spec(
"fs_read",
"Read a file. Paths are workspace-relative.",
)];
assert_eq!(Fidelity::of(Some(&recorded), &live), Fidelity::Differs);
assert!(Fidelity::Differs.caveat().unwrap().contains("changed"));
let same = [spec("fs_read", "Read a file.")];
assert_eq!(Fidelity::of(Some(&recorded), &same), Fidelity::Matches);
assert!(Fidelity::Matches.caveat().is_none());
}
#[test]
fn a_surface_round_trips_and_a_second_record_writes_nothing_new() {
let store = scratch();
let specs = vec![spec("a", "one"), spec("b", "two")];
let hash = store.record(&specs).unwrap();
assert_eq!(fingerprint(&store.load(&hash).unwrap()), hash);
let again = store.record(&specs).unwrap();
assert_eq!(again, hash);
let files = std::fs::read_dir(store.root()).unwrap().count();
assert_eq!(files, 1, "one blob per distinct surface, not per record");
}
#[test]
fn a_missing_blob_is_absent_and_not_empty() {
let store = scratch();
assert!(store.load("0000000000000000").is_none());
}
}