Something went wrong. Try again.
A safe, simple, extensible, and fast agent harness
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203// SPDX-License-Identifier: Apache-2.0// Copyright 2026 Mason Stallmo
//! Installing the bundled WASM plugins and reporting whether they are there.
use anyhow::{Context, Result};use std::{io, path::Path};use tar::Archive;use tokio::{fs, task};use tracing::info;use xz2::read::XzDecoder;
/// The tool plugins a default install is expected to have.////// This and [`DEFAULT_MODEL_CLIENT_PLUGINS`] name every plugin the release/// bundle ships, because [`check_default_plugins`] decides whether the client/// offers an install. A plugin left out is reported as installed when it is/// missing, and the session runs without it with nothing to say why.////// What ships is set by `[package.metadata.ein] plugin_type` in each plugin's/// `Cargo.toml`, and a test holds both lists to it.const DEFAULT_TOOL_PLUGINS: &[&str] = &[ "ein_bash", "ein_read", "ein_write", "ein_edit", "ein_grep", "ein_webfetch",];const DEFAULT_MODEL_CLIENT_PLUGINS: &[&str] = &[ "ein_openrouter", "ein_anthropic", "ein_openai", "ein_ollama",];
/// Downloads and installs the Ein plugin bundle from GitHub releases.////// If `version` is `None`, installs the version matching the current binary's/// `CARGO_PKG_VERSION`. Plugins are extracted to `~/.ein/plugins/`.pub async fn install_plugins(version: Option<String>) -> Result<()> { let ver = version.unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string()); let ver = ver.trim_start_matches('v'); let tag = format!("v{ver}"); let url = format!("https://github.com/mstallmo/ein/releases/download/{tag}/ein-plugins-{tag}.tar.xz");
let plugins_dir = dirs::home_dir() .context("Failed to find home directory")? .join(".ein") .join("plugins");
fs::create_dir_all(plugins_dir.join("tools")).await?; fs::create_dir_all(plugins_dir.join("model_clients")).await?;
info!("downloading plugins from {url}");
let response = reqwest::get(&url) .await .with_context(|| format!("Failed to download {url}"))?;
if !response.status().is_success() { anyhow::bail!("Download failed: HTTP {}", response.status()); }
let bytes = response .bytes() .await .context("Failed to read response body")?;
task::spawn_blocking(move || { let xz = XzDecoder::new(io::Cursor::new(bytes)); Archive::new(xz) .unpack(&plugins_dir) .context("Failed to extract plugin archive") }) .await??;
info!("plugins installed successfully"); Ok(())}
/// Returns `true` if all default tool and model-client plugins are present.pub async fn check_default_plugins(plugin_dir: &Path, model_client_dir: &Path) -> bool { all_present(plugin_dir, DEFAULT_TOOL_PLUGINS).await && all_present(model_client_dir, DEFAULT_MODEL_CLIENT_PLUGINS).await}
/// Returns whether every one of `names` is present in `dir` as a `.wasm`.////// Uses `tokio::fs` rather than the blocking `Path::exists`, because this runs/// from a gRPC handler on a runtime worker, and blocking `stat`s there stall/// every other session on that thread. A path that cannot be stat'd counts as/// missing, so the user is offered an install rather than told they have one.async fn all_present(dir: &Path, names: &[&str]) -> bool { for name in names { let path = dir.join(format!("{name}.wasm")); if !fs::try_exists(&path).await.unwrap_or(false) { return false; } }
true}
/// Returns `true` if the tool plugin directory has no `.wasm` files.pub async fn plugins_missing(plugin_dir: &Path) -> bool { let Ok(mut entries) = fs::read_dir(plugin_dir).await else { return true; };
while let Ok(Some(entry)) = entries.next_entry().await { if entry.path().extension().is_some_and(|e| e == "wasm") { return false; } }
true}
#[cfg(test)]mod tests { use super::*;
/// The default plugin lists name exactly the plugins the workspace ships. /// /// Without this, a plugin missing from the lists is reported as present on /// an incomplete install, and the client never offers to fix it. /// /// Each plugin's kind comes from `[package.metadata.ein] plugin_type` in /// its manifest, the marker the release workflow bundles by. The manifest /// is read as text to avoid a TOML dependency for two string comparisons. #[test] fn the_default_lists_name_every_plugin_the_workspace_ships() { let plugins_dir = Path::new(env!("CARGO_MANIFEST_DIR")) .parent() .expect("the crate directory has a parent") .join("plugins");
let mut tools = Vec::new(); let mut model_clients = Vec::new();
for entry in std::fs::read_dir(&plugins_dir).expect("the plugins directory exists") { let dir = entry.expect("a readable directory entry").path(); let (Ok(manifest), Some(name)) = ( std::fs::read_to_string(dir.join("Cargo.toml")), dir.file_name().and_then(|n| n.to_str()), ) else { continue; };
if manifest.contains(r#"plugin_type = "tool""#) { tools.push(name.to_string()); } else if manifest.contains(r#"plugin_type = "model_client""#) { model_clients.push(name.to_string()); } }
let sorted = |names: &[&str]| { let mut owned: Vec<String> = names.iter().map(|n| (*n).to_string()).collect(); owned.sort_unstable(); owned };
tools.sort_unstable(); model_clients.sort_unstable();
assert_eq!( tools, sorted(DEFAULT_TOOL_PLUGINS), "DEFAULT_TOOL_PLUGINS does not match the tool plugins in the workspace" ); assert_eq!( model_clients, sorted(DEFAULT_MODEL_CLIENT_PLUGINS), "DEFAULT_MODEL_CLIENT_PLUGINS does not match the model clients in the workspace" ); }
#[tokio::test] async fn a_directory_missing_one_plugin_is_not_complete() { let dir = tempfile::TempDir::new().unwrap(); for name in DEFAULT_TOOL_PLUGINS.iter().skip(1) { std::fs::write(dir.path().join(format!("{name}.wasm")), b"").unwrap(); }
assert!(!all_present(dir.path(), DEFAULT_TOOL_PLUGINS).await);
std::fs::write( dir.path().join(format!("{}.wasm", DEFAULT_TOOL_PLUGINS[0])), b"", ) .unwrap();
assert!(all_present(dir.path(), DEFAULT_TOOL_PLUGINS).await); }
#[tokio::test] async fn a_directory_that_does_not_exist_is_not_complete() { assert!(!all_present(Path::new("/no/such/directory"), DEFAULT_TOOL_PLUGINS).await); }}