use std::borrow::Cow;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use sha2::{Digest, Sha256};
use crate::core::fs_secure;
use crate::error::{OlError, ERR_CLINE_PLUGIN_NOT_LOADED, ERR_HOOK_WRITE_FAILED};
use crate::hooks::hook_files::sha256_bytes;
include!(concat!(env!("OUT_DIR"), "/cline_bootstrap_files.rs"));
pub const TREE_DIR: &str = "cline-plugin-bootstrap";
pub const PROBE_HOST: &str = "openlatch-probe-host.mjs";
pub const PROBE_PLUGIN_DIR: &str = "openlatch-probe-plugin";
const PLATFORM_SLOT: &str = "{PLATFORM}";
const ARCH_SLOT: &str = "{ARCH}";
fn node_platform_arch() -> Option<(&'static str, &'static str)> {
let platform = if cfg!(target_os = "macos") {
"darwin"
} else if cfg!(windows) {
"win32"
} else if cfg!(target_os = "linux") {
"linux"
} else {
return None;
};
let arch = if cfg!(target_arch = "aarch64") {
"arm64"
} else if cfg!(target_arch = "x86_64") {
"x64"
} else {
return None;
};
Some((platform, arch))
}
pub fn tree_root(ol_dir: &Path) -> PathBuf {
ol_dir.join(TREE_DIR)
}
pub fn wrapper_path(ol_dir: &Path) -> PathBuf {
tree_root(ol_dir).join("wrapper")
}
pub fn bootstrap_path(ol_dir: &Path) -> Option<PathBuf> {
let (platform, arch) = node_platform_arch()?;
Some(join_rel(
&tree_root(ol_dir),
&format!(
"node_modules/@cline/cli-{platform}-{arch}/extensions/plugin-sandbox-bootstrap.js"
),
))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TreeState {
pub root: PathBuf,
pub signature: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TreeProblem {
UnsupportedPlatform,
Missing(String),
Mismatch(String),
Io(String),
}
impl std::fmt::Display for TreeProblem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TreeProblem::UnsupportedPlatform => {
write!(f, "this platform has no Cline plugin-sandbox package name")
}
TreeProblem::Missing(p) => write!(f, "bootstrap tree file missing: {p}"),
TreeProblem::Mismatch(p) => write!(f, "bootstrap tree file altered: {p}"),
TreeProblem::Io(e) => write!(f, "bootstrap tree unreadable: {e}"),
}
}
}
struct Expected {
bytes: Cow<'static, [u8]>,
sha: Cow<'static, str>,
}
fn expected_files() -> Option<BTreeMap<String, Expected>> {
let (platform, arch) = node_platform_arch()?;
let mut out = BTreeMap::new();
for (path, bytes, sha) in FILES {
let rel = path
.replace(PLATFORM_SLOT, platform)
.replace(ARCH_SLOT, arch);
out.insert(
rel,
Expected {
bytes: Cow::Borrowed(*bytes),
sha: Cow::Borrowed(*sha),
},
);
}
let pkg = format!("@cline/cli-{platform}-{arch}");
let stub = format!("{{\"name\":\"{pkg}\",\"version\":\"0.0.0-openlatch\",\"private\":true}}\n")
.into_bytes();
out.insert(
format!("node_modules/{pkg}/package.json"),
Expected {
sha: Cow::Owned(sha256_bytes(&stub)),
bytes: Cow::Owned(stub),
},
);
Some(out)
}
pub fn expected_signature() -> Option<String> {
expected_files().map(|expected| signature_of(&expected))
}
fn signature_of(expected: &BTreeMap<String, Expected>) -> String {
let mut hasher = Sha256::new();
for (path, file) in expected {
hasher.update(path.as_bytes());
hasher.update([0u8]);
hasher.update(file.sha.as_bytes());
hasher.update(b"\n");
}
hex::encode(hasher.finalize())
}
fn join_rel(root: &Path, rel: &str) -> PathBuf {
rel.split('/').fold(root.to_path_buf(), |p, c| p.join(c))
}
fn file_matches(path: &Path, file: &Expected) -> Result<bool, std::io::Error> {
match std::fs::symlink_metadata(path) {
Ok(meta) if meta.file_type().is_file() => Ok(meta.len() == file.bytes.len() as u64
&& sha256_bytes(&std::fs::read(path)?) == file.sha),
Ok(_) => Ok(false),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(e),
}
}
fn write_failed(path: &Path, e: &std::io::Error) -> OlError {
OlError::new(
ERR_HOOK_WRITE_FAILED,
format!(
"cannot write the Cline plugin bootstrap file {}: {e}",
crate::core::path_compat::display_path(path)
),
)
}
pub fn materialise(ol_dir: &Path) -> Result<TreeState, OlError> {
let Some(expected) = expected_files() else {
return Err(OlError::new(
ERR_CLINE_PLUGIN_NOT_LOADED,
"this platform has no Cline plugin-sandbox package name; the plugin cannot be delivered here",
));
};
let root = tree_root(ol_dir);
ensure_real_dir(&root).map_err(|e| write_failed(&root, &e))?;
for (rel, file) in &expected {
let dest = join_rel(&root, rel);
let mut dir = root.clone();
let parts: Vec<&str> = rel.split('/').collect();
for part in &parts[..parts.len().saturating_sub(1)] {
dir.push(part);
ensure_real_dir(&dir).map_err(|e| write_failed(&dir, &e))?;
}
if file_matches(&dest, file).unwrap_or(false) {
continue;
}
if let Ok(meta) = std::fs::symlink_metadata(&dest) {
let cleared = if meta.file_type().is_dir() {
std::fs::remove_dir_all(&dest)
} else if meta.file_type().is_symlink() {
std::fs::remove_file(&dest)
} else {
Ok(())
};
cleared.map_err(|e| write_failed(&dest, &e))?;
}
fs_secure::write_readable_bytes(&dest, &file.bytes).map_err(|e| write_failed(&dest, &e))?;
}
prune(&root, &root, &expected).map_err(|e| write_failed(&root, &e))?;
Ok(TreeState {
root,
signature: signature_of(&expected),
})
}
fn ensure_real_dir(path: &Path) -> std::io::Result<()> {
match std::fs::symlink_metadata(path) {
Ok(meta) if meta.file_type().is_dir() => return Ok(()),
Ok(_) => std::fs::remove_file(path).or_else(|_| std::fs::remove_dir(path))?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(e),
}
std::fs::create_dir_all(path)
}
fn prune(root: &Path, dir: &Path, expected: &BTreeMap<String, Expected>) -> std::io::Result<()> {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let file_type = entry.file_type()?;
let rel = path
.strip_prefix(root)
.map(|p| {
p.components()
.map(|c| c.as_os_str().to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join("/") })
.unwrap_or_default();
if file_type.is_dir() {
prune(root, &path, expected)?;
if std::fs::read_dir(&path)?.next().is_none() {
std::fs::remove_dir(&path)?;
}
} else if file_type.is_symlink() || !expected.contains_key(&rel) {
std::fs::remove_file(&path)?;
}
}
Ok(())
}
pub fn verify(ol_dir: &Path) -> Result<TreeState, TreeProblem> {
let expected = expected_files().ok_or(TreeProblem::UnsupportedPlatform)?;
let root = tree_root(ol_dir);
for (rel, file) in &expected {
let dest = join_rel(&root, rel);
match std::fs::symlink_metadata(&dest) {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Err(TreeProblem::Missing(rel.clone()))
}
Err(e) => return Err(TreeProblem::Io(format!("{rel}: {e}"))),
Ok(_) => {}
}
match file_matches(&dest, file) {
Ok(true) => {}
Ok(false) => return Err(TreeProblem::Mismatch(rel.clone())),
Err(e) => return Err(TreeProblem::Io(format!("{rel}: {e}"))),
}
}
Ok(TreeState {
root,
signature: signature_of(&expected),
})
}
pub fn remove(ol_dir: &Path) -> std::io::Result<()> {
match std::fs::remove_dir_all(tree_root(ol_dir)) {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
other => other,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn mtimes(root: &Path) -> BTreeMap<PathBuf, std::time::SystemTime> {
let mut out = BTreeMap::new();
let mut stack = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
for entry in std::fs::read_dir(&dir).expect("read_dir") {
let entry = entry.expect("entry");
let meta = entry.metadata().expect("metadata");
if meta.is_dir() {
stack.push(entry.path());
} else {
out.insert(entry.path(), meta.modified().expect("mtime"));
}
}
}
out
}
#[test]
fn materialise_is_idempotent_and_hash_checked() {
if node_platform_arch().is_none() {
return;
}
let ol = tempfile::tempdir().expect("tempdir");
let first = materialise(ol.path()).expect("first materialise");
let before = mtimes(&first.root);
assert!(!before.is_empty());
std::thread::sleep(std::time::Duration::from_millis(20));
let second = materialise(ol.path()).expect("second materialise");
assert_eq!(first, second);
assert_eq!(before, mtimes(&second.root), "no file was rewritten");
assert_eq!(verify(ol.path()), Ok(second.clone()));
let bootstrap = bootstrap_path(ol.path()).expect("deliverable");
let pinned = std::fs::read(&bootstrap).expect("read bootstrap");
std::fs::write(&bootstrap, b"tampered").expect("tamper");
assert!(matches!(verify(ol.path()), Err(TreeProblem::Mismatch(_))));
materialise(ol.path()).expect("heal");
assert_eq!(std::fs::read(&bootstrap).expect("read bootstrap"), pinned);
let stray = first.root.join("node_modules").join("stray.js");
let other_stub = first
.root
.join("node_modules")
.join("@cline")
.join("cli-plan9-mips")
.join("package.json");
std::fs::write(&stray, b"x").expect("stray");
std::fs::create_dir_all(other_stub.parent().expect("parent")).expect("mkdir");
std::fs::write(&other_stub, b"{}").expect("other stub");
materialise(ol.path()).expect("prune");
assert!(!stray.exists(), "a stray file is removed");
assert!(
!other_stub.parent().expect("parent").exists(),
"another platform's stub is removed with its directory"
);
assert_eq!(verify(ol.path()).map(|s| s.signature), Ok(first.signature));
std::fs::remove_file(&bootstrap).expect("remove");
assert!(matches!(verify(ol.path()), Err(TreeProblem::Missing(_))));
materialise(ol.path()).expect("restore");
assert!(bootstrap.is_file());
remove(ol.path()).expect("remove");
assert!(!tree_root(ol.path()).exists());
remove(ol.path()).expect("removing an absent tree is fine");
}
#[cfg(unix)]
#[test]
fn materialise_never_follows_a_symlinked_ancestor() {
if node_platform_arch().is_none() {
return;
}
let ol = tempfile::tempdir().expect("tempdir");
let outside = tempfile::tempdir().expect("outside");
std::fs::write(outside.path().join("keep.txt"), b"mine").expect("outside file");
let root = tree_root(ol.path());
std::fs::create_dir_all(&root).expect("mkdir root");
std::os::unix::fs::symlink(outside.path(), root.join("node_modules")).expect("symlink");
materialise(ol.path()).expect("materialise");
let listed: Vec<_> = std::fs::read_dir(outside.path())
.expect("read outside")
.flatten()
.map(|e| e.file_name())
.collect();
assert_eq!(listed, vec![std::ffi::OsString::from("keep.txt")]);
assert_eq!(
std::fs::read(outside.path().join("keep.txt")).expect("read"),
b"mine"
);
let node_modules = std::fs::symlink_metadata(root.join("node_modules")).expect("meta");
assert!(node_modules.file_type().is_dir(), "a real directory now");
assert!(root
.join("node_modules")
.join("jiti")
.join("package.json")
.is_file());
assert!(verify(ol.path()).is_ok());
remove(ol.path()).expect("remove");
std::os::unix::fs::symlink(outside.path(), &root).expect("symlink root");
materialise(ol.path()).expect("materialise");
assert!(std::fs::symlink_metadata(&root)
.expect("meta")
.file_type()
.is_dir());
assert_eq!(
std::fs::read_dir(outside.path()).expect("read").count(),
1,
"nothing written through a symlinked root"
);
}
#[test]
fn tree_layout_matches_the_resolver() {
let Some((platform, arch)) = node_platform_arch() else {
return;
};
let ol = tempfile::tempdir().expect("tempdir");
let state = materialise(ol.path()).expect("materialise");
let pkg = state
.root
.join("node_modules")
.join("@cline")
.join(format!("cli-{platform}-{arch}"));
let stub: serde_json::Value = serde_json::from_slice(
&std::fs::read(pkg.join("package.json")).expect("stub package.json"),
)
.expect("stub is JSON");
assert_eq!(stub["name"], format!("@cline/cli-{platform}-{arch}"));
assert!(pkg
.join("extensions")
.join("plugin-sandbox-bootstrap.js")
.is_file());
assert_eq!(
bootstrap_path(ol.path()),
Some(pkg.join("extensions").join("plugin-sandbox-bootstrap.js"))
);
assert!(state
.root
.join("node_modules")
.join("@cline")
.join("shared")
.join("dist")
.join("index.js")
.is_file());
assert!(state
.root
.join("node_modules")
.join("jiti")
.join("package.json")
.is_file());
assert!(state.root.join(PROBE_HOST).is_file());
assert!(state.root.join(PROBE_PLUGIN_DIR).join("index.js").is_file());
assert!(state.root.join("THIRD-PARTY-NOTICES").is_file());
assert_eq!(
wrapper_path(ol.path()).parent(),
Some(state.root.as_path()),
"createRequire resolves from the wrapper path's directory, the tree root"
);
assert!(
!wrapper_path(ol.path()).exists(),
"the wrapper path names no file"
);
}
#[test]
fn embedded_hashes_match_vendor_toml() {
let vendor = include_str!("../../assets/cline/plugin-bootstrap/VENDOR.toml");
let mut listed: BTreeMap<String, String> = BTreeMap::new();
let mut in_files = false;
for line in vendor.lines() {
let line = line.trim();
if line.starts_with('[') {
in_files = line == "[files]";
continue;
}
if !in_files || line.is_empty() || line.starts_with('#') {
continue;
}
let (rel, sha) = line
.strip_prefix('"')
.and_then(|r| r.split_once("\" = \""))
.and_then(|(rel, sha)| sha.strip_suffix('"').map(|sha| (rel, sha)))
.expect("a [files] line");
listed.insert(rel.to_string(), sha.to_string());
}
assert_eq!(
listed.len(),
FILES.len(),
"one embedded file per [files] line"
);
let unmap = |path: &str| -> String {
if let Some(rest) = path.strip_prefix("node_modules/@cline/shared/") {
format!("shared/{rest}")
} else if let Some(rest) = path.strip_prefix("node_modules/jiti/") {
format!("jiti/{rest}")
} else if path.ends_with("/extensions/plugin-sandbox-bootstrap.js") {
"plugin-sandbox-bootstrap.js".to_string()
} else {
path.to_string()
}
};
for (path, bytes, sha) in FILES {
let rel = unmap(path);
assert_eq!(listed.get(&rel).map(String::as_str), Some(*sha), "{rel}");
assert_eq!(sha256_bytes(bytes), *sha, "{rel}: embedded bytes match");
}
for pin in ["[core]", "[shared]", "[jiti]"] {
assert!(vendor.contains(pin), "VENDOR.toml pins {pin}");
}
assert!(vendor.contains("version = \"0.0.87\""));
assert!(vendor.contains("version = \"2.7.0\""));
}
}