Something went wrong. Try again.
Flatpak-style launcher for sandboxed WebAssembly tools. Install, verify, and run .wasm binaries with explicit capability grants. Single binary, local-first, zero telemetry. wasmbox.orbiter.website
wasm rust wasmbox
Something went wrong. Try again.
74 kB · 2177 lines
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178use std::io::{IsTerminal, Write};use std::path::PathBuf;
use anyhow::{Context, Result};use chrono::Utc;use clap::{Parser, Subcommand};use colored::Colorize;use wasmbox_compliance::{PolicyDecision, RunLog, RunLogEntry};use wasmbox_shared::{Capability, WasmBoxConfig, WasmBoxPaths};
// Exit codes per CLAUDE.mdconst EXIT_ERROR: i32 = 1;const EXIT_PERMISSION_DENIED: i32 = 2;const EXIT_VERIFICATION_FAILED: i32 = 3;
#[derive(Parser)]#[command(name = "wasmbox", about = "Sandboxed Wasm tool launcher", version)]struct Cli { #[command(subcommand)] command: Commands,
/// Output in JSON format #[arg(long, global = true)] json: bool,
/// Override WasmBox home directory #[arg(long, global = true)] home: Option<String>,}
#[derive(Subcommand)]enum Commands { /// Run a tool (installed or local file) Run { /// Tool name (optionally with @version) name: Option<String>, /// Run a local .wasm file directly #[arg(long)] file: Option<String>, /// Manifest for local .wasm (declares capabilities) #[arg(long)] manifest: Option<String>, /// Force zero capabilities (full sandbox) #[arg(long)] sandbox: bool, /// Allow specific capabilities (comma-separated: stdin,stdout,clipboard) #[arg(long, value_delimiter = ',')] allow: Vec<String>, /// Allow all capabilities requested in manifest #[arg(long)] allow_all: bool, /// Arguments to pass to the tool (after --) #[arg(last = true)] tool_args: Vec<String>, }, /// Install a tool from a registry Install { /// Tool name name: String, /// Registry URL to install from #[arg(long)] registry: Option<String>, /// Pre-approve specific capabilities #[arg(long, value_delimiter = ',')] allow: Vec<String>, /// Pre-approve all manifest capabilities #[arg(long)] allow_all: bool, }, /// List installed tools List, /// Search registries for tools Search { /// Search query query: String, }, /// Show tool info and manifest Info { /// Tool name name: String, }, /// Verify tool hash against manifest Verify { /// Tool name name: String, }, /// Manage permissions for a tool Permissions { /// Tool name name: String, #[command(subcommand)] action: Option<PermissionAction>, }, /// Manage registries Registry { #[command(subcommand)] action: RegistryAction, }, /// Audit all granted permissions across all tools Audit { /// Export compliance report #[arg(long)] export: bool, /// Output format for export (json, md, csv) #[arg(long, default_value = "json")] format: String, /// Sign the exported report with Ed25519 #[arg(long)] sign: bool, /// Generate Ed25519 signing keypair #[arg(long)] init_key: bool, /// Verify a signed audit report #[arg(long)] verify: Option<String>, /// Start of audit period (ISO 8601 date) #[arg(long)] from: Option<String>, /// End of audit period (ISO 8601 date) #[arg(long)] to: Option<String>, }, /// View execution run log Log { /// Show last N entries #[arg(long)] last: Option<usize>, /// Filter by tool name #[arg(long)] tool: Option<String>, /// Show only blocked executions #[arg(long)] blocked: bool, /// Filter entries after this date (YYYY-MM-DD) #[arg(long)] after: Option<String>, /// Filter entries before this date (YYYY-MM-DD) #[arg(long)] before: Option<String>, /// Export log to file #[arg(long)] export: Option<String>, /// Rotate log (archive current, start fresh) #[arg(long)] rotate: bool, /// Show log statistics #[arg(long)] stats: bool, }, /// Manage compliance policy Policy { #[command(subcommand)] action: PolicyAction, }, /// Remove an installed tool Remove { /// Tool name name: String, }, /// Update a tool to the latest version Update { /// Tool name name: String, }, /// Revoke a specific capability for a tool Revoke { /// Tool name name: String, /// Capability to revoke (stdout, stdin, network, filesystem, clipboard, env) capability: String, }, /// Compute SHA-256 hash of a .wasm file Hash { /// Path to .wasm file file: String, },}
#[derive(Subcommand)]enum PermissionAction { /// Show granted permissions Show, /// Revoke all permissions Revoke,}
#[derive(Subcommand)]enum RegistryAction { /// Add a registry Add { /// Registry URL url: String, }, /// List configured registries List, /// Remove a registry Remove { /// Registry URL url: String, },}
#[derive(Subcommand)]enum PolicyAction { /// Initialize a new policy from currently installed tools Init { /// Policy name #[arg(long)] name: Option<String>, /// Approver email #[arg(long)] approved_by: Option<String>, /// Enforcement mode (enforce, warn, disabled) #[arg(long, default_value = "warn")] enforcement: String, }, /// Show current policy Show, /// Check all installed tools against policy Check, /// Add a tool to the policy Add { /// Tool name name: String, }, /// Remove a tool from the policy Remove { /// Tool name name: String, }, /// Set enforcement to block unapproved tools Enforce, /// Set enforcement to warn but allow Warn, /// Disable policy enforcement Disable, /// Show changes since last policy approval Diff, /// Export policy to stdout Export, /// Generate restricted shell wrapper for AI agents Shell, /// Configure agent shell enforcement Agent { /// Enable shell enforcement #[arg(long)] enable: bool, /// Disable shell enforcement #[arg(long)] disable: bool, },}
#[tokio::main]async fn main() { if std::env::var_os("NO_COLOR").is_some() { colored::control::set_override(false); }
let cli = Cli::parse(); let paths = WasmBoxPaths::resolve(cli.home.as_deref());
let result = match cli.command { Commands::Run { name, file, manifest, sandbox, allow, allow_all, tool_args, } => { cmd_run(&paths, name, file, manifest, sandbox, &allow, allow_all, tool_args) } Commands::Install { ref name, ref registry, ref allow, allow_all, } => { cmd_install(&paths, name, registry.as_deref(), allow, allow_all).await } Commands::List => cmd_list(&paths, cli.json), Commands::Search { ref query } => cmd_search(&paths, query, cli.json).await, Commands::Info { ref name } => cmd_info(&paths, name, cli.json), Commands::Verify { ref name } => cmd_verify(&paths, name), Commands::Permissions { ref name, ref action } => { cmd_permissions(&paths, name, action, cli.json) } Commands::Registry { ref action } => cmd_registry(&paths, action), Commands::Audit { export, ref format, sign, init_key, ref verify, ref from, ref to, } => cmd_audit(&paths, cli.json, export, format, sign, init_key, verify.as_deref(), from.as_deref(), to.as_deref()), Commands::Log { last, ref tool, blocked, ref after, ref before, ref export, rotate, stats, } => cmd_log(&paths, cli.json, last, tool.as_deref(), blocked, after.as_deref(), before.as_deref(), export.as_deref(), rotate, stats), Commands::Policy { ref action } => cmd_policy(&paths, action, cli.json), Commands::Remove { ref name } => cmd_remove(&paths, name), Commands::Update { ref name } => cmd_update(&paths, name).await, Commands::Revoke { ref name, ref capability, } => cmd_revoke(&paths, name, capability), Commands::Hash { ref file } => cmd_hash(file), };
if let Err(e) = result { let msg = format!("{e:#}"); if msg.contains("tool exited with non-zero status") { std::process::exit(1); } else if msg.contains("permission denied") || msg.contains("Permission denied") { eprintln!("{}: {e:#}", "error".red().bold()); std::process::exit(EXIT_PERMISSION_DENIED); } else if msg.contains("verification failed") || msg.contains("hash mismatch") { eprintln!("{}: {e:#}", "error".red().bold()); std::process::exit(EXIT_VERIFICATION_FAILED); } else { eprintln!("{}: {e:#}", "error".red().bold()); std::process::exit(EXIT_ERROR); } }}
// --- Run ---
#[allow(clippy::too_many_arguments)]fn cmd_run( paths: &WasmBoxPaths, name: Option<String>, file: Option<String>, manifest_path: Option<String>, sandbox: bool, allow: &[String], allow_all: bool, tool_args: Vec<String>,) -> Result<()> { let (wasm_path, manifest, tool_name) = resolve_run_target(paths, name, file, manifest_path)?;
let capabilities = resolve_capabilities( paths, &tool_name, &manifest, sandbox, allow, allow_all, )?;
let version = manifest .as_ref() .map(|m| m.tool.version.to_string()) .unwrap_or_else(|| "unknown".to_string());
let cap_strings: Vec<String> = capabilities.iter().map(format_capability).collect();
// Verify hash if we have a manifest let (hash, hash_verified) = if let Some(ref m) = manifest { let wasm_bytes = std::fs::read(&wasm_path) .with_context(|| format!("reading {}", wasm_path.display()))?; match wasmbox_verify::verify_hash(&wasm_bytes, &m.binary.hash) { Ok(()) => { eprintln!( "{} {} [{}]", "verified".green().bold(), tool_name, &m.binary.hash[..20], ); (m.binary.hash.clone(), true) } Err(e) => { // Log hash failure and bail let run_log = RunLog::new(paths.home()); let entry = RunLogEntry { ts: Utc::now(), tool: tool_name.clone(), version: version.clone(), hash: m.binary.hash.clone(), hash_verified: false, capabilities: cap_strings, exit_code: None, duration_ms: None, policy: "approved".to_string(), policy_reason: None, }; let _ = run_log.append(&entry); anyhow::bail!("verification failed for {}: {e}", tool_name); } } } else { ("unknown".to_string(), true) };
// Check policy before execution let policy_store = wasmbox_compliance::PolicyStore::load(paths.home()) .map_err(|e| anyhow::anyhow!("policy error: {e}"))?; let policy_decision = policy_store.check_tool(&tool_name, &hash, &cap_strings);
match &policy_decision { PolicyDecision::Blocked(reason) => { let run_log = RunLog::new(paths.home()); let entry = RunLogEntry { ts: Utc::now(), tool: tool_name.clone(), version: version.clone(), hash: hash.clone(), hash_verified, capabilities: cap_strings, exit_code: None, duration_ms: None, policy: "blocked".to_string(), policy_reason: Some(reason.clone()), }; let _ = run_log.append(&entry);
if let Some(p) = policy_store.policy() { eprintln!( "{} by policy: \"{}\" {}", "BLOCKED".red().bold(), tool_name, reason, ); eprintln!("Policy: {} v{}", p.policy.name, p.policy.version); eprintln!("Approved by: {}", p.policy.approved_by); eprintln!("Enforcement: {:?}", p.policy.enforcement); eprintln!("\nTo add this tool to the policy, run:"); eprintln!(" wasmbox policy add {tool_name}"); } anyhow::bail!("permission denied: blocked by policy"); } PolicyDecision::Warned(reason) => { if let Some(p) = policy_store.policy() { eprintln!( "{}: \"{}\" {}", "WARNING".yellow().bold(), tool_name, reason, ); eprintln!( "Policy: {} v{} (enforcement: {:?})", p.policy.name, p.policy.version, p.policy.enforcement, ); } } PolicyDecision::Approved | PolicyDecision::NoPolicy => {} }
eprintln!( "{} {} with {} capabilities", "running".cyan().bold(), tool_name, capabilities.len(), );
// Log before execution let run_log = RunLog::new(paths.home()); let start = std::time::Instant::now(); let pre_entry = RunLogEntry { ts: Utc::now(), tool: tool_name.clone(), version: version.clone(), hash: hash.clone(), hash_verified, capabilities: cap_strings.clone(), exit_code: None, duration_ms: None, policy: policy_decision.status_str().to_string(), policy_reason: policy_decision.reason().map(String::from), }; let _ = run_log.append(&pre_entry);
let config = wasmbox_runtime::RunConfig { wasm_path, capabilities, fuel_limit: None, args: tool_args, };
// Run wasm on a dedicated thread to avoid tokio runtime conflict // (wasmtime-wasi sync creates its own runtime internally) let result: Result<()> = std::thread::scope(|s| { s.spawn(|| { wasmbox_runtime::run_tool(&config).map_err(|e| anyhow::anyhow!("{e}")) }) .join() .expect("wasm thread panicked") });
// Log after execution with actual exit code and duration let duration = start.elapsed(); let exit_code = match &result { Ok(()) => Some(0), Err(_) => Some(1), }; let post_entry = RunLogEntry { ts: Utc::now(), tool: tool_name, version, hash, hash_verified, capabilities: cap_strings, exit_code, duration_ms: Some(duration.as_millis() as u64), policy: policy_decision.status_str().to_string(), policy_reason: policy_decision.reason().map(String::from), }; let _ = run_log.append(&post_entry);
result}
fn resolve_run_target( paths: &WasmBoxPaths, name: Option<String>, file: Option<String>, manifest_path: Option<String>,) -> Result<(PathBuf, Option<wasmbox_manifest::Manifest>, String)> { if let Some(file) = file { // Local file execution let wasm_path = PathBuf::from(&file); if !wasm_path.exists() { anyhow::bail!("file not found: {file}"); }
let manifest = manifest_path .map(|p| { wasmbox_manifest::Manifest::from_file(std::path::Path::new(&p)) .map_err(|e| anyhow::anyhow!("manifest error: {e}")) }) .transpose()?;
let tool_name = manifest .as_ref() .map(|m| m.tool.name.clone()) .unwrap_or_else(|| { wasm_path .file_stem() .map(|s| s.to_string_lossy().to_string()) .unwrap_or_else(|| "unknown".to_string()) });
Ok((wasm_path, manifest, tool_name)) } else if let Some(name) = name { // Installed tool let (tool_name, version) = parse_tool_spec(&name); let version = find_installed_version(paths, &tool_name, version.as_deref())?; let manifest_file = paths.tool_manifest_path(&tool_name, &version); let manifest = wasmbox_manifest::Manifest::from_file(&manifest_file) .map_err(|e| anyhow::anyhow!("manifest error for {tool_name}@{version}: {e}"))?;
let wasm_path = paths.tool_wasm_path(&tool_name, &version, &manifest.binary.wasm);
Ok((wasm_path, Some(manifest), tool_name)) } else { anyhow::bail!("provide a tool name or --file <path.wasm>"); }}
fn resolve_capabilities( paths: &WasmBoxPaths, tool_name: &str, manifest: &Option<wasmbox_manifest::Manifest>, sandbox: bool, allow: &[String], allow_all: bool,) -> Result<Vec<Capability>> { if sandbox { return Ok(Vec::new()); }
// Explicit --allow flags take priority if !allow.is_empty() { let caps = parse_allow_flags(allow)?; persist_if_installed(paths, tool_name, manifest, &caps)?; return Ok(caps); }
// --allow-all grants everything in manifest, or stdin+stdout if no manifest if allow_all { let caps = manifest .as_ref() .map(|m| m.requested_capabilities()) .unwrap_or_else(|| vec![Capability::Stdin, Capability::Stdout]); persist_if_installed(paths, tool_name, manifest, &caps)?; return Ok(caps); }
// Check stored permissions let perm_path = paths.permissions_file(); if perm_path.exists() { let store = wasmbox_permissions::PermissionStore::load(&perm_path) .map_err(|e| anyhow::anyhow!("{e}"))?; if store.get(tool_name).is_some() { return Ok(store.to_capabilities(tool_name)); } }
// No stored permissions — prompt or fail let requested = manifest .as_ref() .map(|m| m.requested_capabilities()) .unwrap_or_default();
if requested.is_empty() { return Ok(Vec::new()); }
// If not a TTY, fail with permission denied if !std::io::stdin().is_terminal() { anyhow::bail!( "permission denied: {} requests capabilities but no \ TTY for prompting. Use --allow or --allow-all", tool_name, ); }
prompt_capabilities(paths, tool_name, manifest, &requested)}
/// Persist granted capabilities for installed tools so future runs don't re-prompt.fn persist_if_installed( paths: &WasmBoxPaths, tool_name: &str, manifest: &Option<wasmbox_manifest::Manifest>, caps: &[Capability],) -> Result<()> { // Only persist for installed tools (those with a manifest on disk) let Some(m) = manifest else { return Ok(()) }; let manifest_path = paths.tool_manifest_path(tool_name, &m.tool.version.to_string()); if !manifest_path.exists() { return Ok(()); } let perm_path = paths.permissions_file(); let mut store = wasmbox_permissions::PermissionStore::load(&perm_path) .map_err(|e| anyhow::anyhow!("{e}"))?; store .grant(tool_name, &m.tool.version.to_string(), caps) .map_err(|e| anyhow::anyhow!("{e}"))?; Ok(())}
fn prompt_capabilities( paths: &WasmBoxPaths, tool_name: &str, manifest: &Option<wasmbox_manifest::Manifest>, requested: &[Capability],) -> Result<Vec<Capability>> { let version = manifest .as_ref() .map(|m| m.tool.version.to_string()) .unwrap_or_else(|| "unknown".to_string());
eprintln!(); if let Some(m) = manifest { eprintln!( " {} v{} - {}", m.tool.name.bold(), m.tool.version, m.tool.description, ); eprintln!( " Author: {} | License: {}", m.tool.author, m.tool.license, ); eprintln!(" Hash: {} [{}]", &m.binary.hash[..20], "VERIFIED".green()); } eprintln!(); eprintln!(" {}:", "Requested capabilities".bold());
for cap in requested { match cap { Capability::Stdin => eprintln!(" stdin: {}", "yes".yellow()), Capability::Stdout => eprintln!(" stdout: {}", "yes".yellow()), Capability::Clipboard => eprintln!(" clipboard: {}", "yes".yellow()), Capability::Network(hosts) => { eprintln!(" network: {}", hosts.join(", ").yellow()); } Capability::Filesystem(g) => { let mode = match (g.read, g.write) { (true, true) => "read+write", (true, false) => "read", (false, true) => "write", (false, false) => "none", }; eprintln!(" filesystem: {} ({})", g.path.yellow(), mode); } Capability::Env(vars) => { eprintln!(" env: {}", vars.join(", ").yellow()); } } }
eprintln!(); eprint!(" Allow? [Y/n] "); std::io::stderr().flush()?;
let mut input = String::new(); std::io::stdin().read_line(&mut input)?; let input = input.trim().to_lowercase();
if input.is_empty() || input == "y" || input == "yes" { // Store the approval paths.ensure_dirs()?; let perm_path = paths.permissions_file(); let mut store = wasmbox_permissions::PermissionStore::load(&perm_path) .map_err(|e| anyhow::anyhow!("{e}"))?; store .grant(tool_name, &version, requested) .map_err(|e| anyhow::anyhow!("{e}"))?; eprintln!(" {}", "Approved.".green()); Ok(requested.to_vec()) } else { anyhow::bail!("permission denied: user declined capabilities for {tool_name}"); }}
fn parse_allow_flags(allow: &[String]) -> Result<Vec<Capability>> { let mut caps = Vec::new(); for item in allow { match item.as_str() { "stdin" => caps.push(Capability::Stdin), "stdout" => caps.push(Capability::Stdout), "clipboard" => caps.push(Capability::Clipboard), other => anyhow::bail!( "unknown capability: {other}. Valid: stdin, stdout, clipboard" ), } } Ok(caps)}
fn parse_tool_spec(spec: &str) -> (String, Option<String>) { if let Some((name, version)) = spec.rsplit_once('@') { (name.to_string(), Some(version.to_string())) } else { (spec.to_string(), None) }}
fn find_installed_version( paths: &WasmBoxPaths, tool_name: &str, requested_version: Option<&str>,) -> Result<String> { let tool_cache = paths.cache_dir().join(tool_name); if !tool_cache.exists() { anyhow::bail!("tool not found: {tool_name}. Run: wasmbox install {tool_name}"); }
if let Some(version) = requested_version { let version_dir = tool_cache.join(version); if version_dir.exists() { return Ok(version.to_string()); } anyhow::bail!("version {version} not found for {tool_name}"); }
// Find the latest version (highest semver) let mut versions: Vec<semver::Version> = Vec::new(); for entry in std::fs::read_dir(&tool_cache)? { let entry = entry?; if entry.file_type()?.is_dir() && let Ok(v) = semver::Version::parse(&entry.file_name().to_string_lossy()) { versions.push(v); } }
versions.sort(); versions .last() .map(|v| v.to_string()) .ok_or_else(|| anyhow::anyhow!("no installed versions for {tool_name}"))}
// --- Install ---
async fn cmd_install( paths: &WasmBoxPaths, name: &str, registry_url: Option<&str>, allow: &[String], allow_all: bool,) -> Result<()> { paths.ensure_dirs()?;
let registries = get_registries(paths, registry_url)?; let client = wasmbox_registry::RegistryClient::new(registries.clone());
// Find the tool in registries eprintln!("{} {} in registries...", "searching".cyan().bold(), name); let mut found_registry = None; let mut found_entry = None;
for reg in ®istries { match client.fetch_index(reg).await { Ok(index) => { if let Some(entry) = index.tools.iter().find(|t| t.name == name) { found_registry = Some(reg.clone()); found_entry = Some(entry.clone()); break; } } Err(e) => { eprintln!( "{}: failed to fetch {}: {e}", "warning".yellow().bold(), reg.url, ); } } }
let registry = found_registry .ok_or_else(|| anyhow::anyhow!("tool not found in any registry: {name}"))?; let entry = found_entry.unwrap();
eprintln!( "{} {} v{} ({})", "found".green().bold(), entry.name, entry.version, human_size(entry.size), );
// Download the binary eprintln!("{} binary...", "downloading".cyan().bold()); let wasm_bytes = client .fetch_binary(®istry, name) .await .map_err(|e| anyhow::anyhow!("{e}"))?;
// Verify hash wasmbox_verify::verify_hash(&wasm_bytes, &entry.hash) .map_err(|e| anyhow::anyhow!("verification failed: {e}"))?; eprintln!("{} hash verified", "ok".green().bold());
// Fetch manifest let manifest_json = client .fetch_manifest(®istry, name) .await .map_err(|e| anyhow::anyhow!("{e}"))?;
// Parse manifest before writing anything let manifest = wasmbox_manifest::Manifest::parse(&manifest_json) .map_err(|e| anyhow::anyhow!("manifest error: {e}"))?;
// Atomic install: write to a temp dir, then rename into place. // This prevents partial installs from corrupting the cache. let cache_dir = paths.cache_dir(); std::fs::create_dir_all(&cache_dir)?; let staging_dir = tempfile::tempdir_in(&cache_dir)?;
std::fs::write(staging_dir.path().join("wasmbox.toml"), &manifest_json)?; std::fs::write(staging_dir.path().join(&manifest.binary.wasm), &wasm_bytes)?;
// Move into final location let tool_dir = paths.tool_dir(name, &entry.version); if tool_dir.exists() { std::fs::remove_dir_all(&tool_dir)?; } std::fs::create_dir_all(tool_dir.parent().unwrap())?; std::fs::rename(staging_dir.path(), &tool_dir)?; // Prevent tempdir cleanup from erroring on the now-moved dir let _ = staging_dir.keep();
eprintln!( "{} {} v{} to {}", "installed".green().bold(), name, entry.version, tool_dir.display(), );
// Pre-approve capabilities if requested if allow_all || !allow.is_empty() { let capabilities = if allow_all { manifest.requested_capabilities() } else { parse_allow_flags(allow)? };
if !capabilities.is_empty() { let perm_path = paths.permissions_file(); let mut store = wasmbox_permissions::PermissionStore::load(&perm_path) .map_err(|e| anyhow::anyhow!("{e}"))?; store .grant(name, &entry.version, &capabilities) .map_err(|e| anyhow::anyhow!("{e}"))?; eprintln!( "{} {} capabilities pre-approved", "ok".green().bold(), capabilities.len(), ); } }
Ok(())}
// --- Search ---
async fn cmd_search(paths: &WasmBoxPaths, query: &str, json: bool) -> Result<()> { let registries = get_registries(paths, None)?; let client = wasmbox_registry::RegistryClient::new(registries);
let results = client .search(query) .await .map_err(|e| anyhow::anyhow!("{e}"))?;
if json { println!("{}", serde_json::to_string_pretty(&results)?); return Ok(()); }
if results.is_empty() { eprintln!("no results for '{query}'"); return Ok(()); }
for tool in &results { eprintln!( " {} v{} - {}", tool.name.bold(), tool.version, tool.description, ); if !tool.categories.is_empty() { eprintln!(" categories: {}", tool.categories.join(", ")); } } eprintln!("\n{} result(s)", results.len());
Ok(())}
// --- Info ---
fn cmd_info(paths: &WasmBoxPaths, name: &str, json: bool) -> Result<()> { let (tool_name, version) = parse_tool_spec(name); let version = find_installed_version(paths, &tool_name, version.as_deref())?; let manifest_path = paths.tool_manifest_path(&tool_name, &version); let manifest = wasmbox_manifest::Manifest::from_file(&manifest_path) .map_err(|e| anyhow::anyhow!("{e}"))?;
if json { let mut value = serde_json::to_value(manifest.to_metadata())?; if let (Some(obj), Some(agent)) = (value.as_object_mut(), manifest.agent.as_ref()) { obj.insert("agent".to_string(), serde_json::to_value(agent)?); } println!("{}", serde_json::to_string_pretty(&value)?); return Ok(()); }
eprintln!(" {} v{}", manifest.tool.name.bold(), manifest.tool.version); eprintln!(" {}", manifest.tool.description); eprintln!( " Author: {} | License: {}", manifest.tool.author, manifest.tool.license, ); if let Some(ref hp) = manifest.tool.homepage { eprintln!(" Homepage: {hp}"); } eprintln!(" Hash: {}", manifest.binary.hash); eprintln!(" Type: {}", manifest.ui.tool_type);
let caps = manifest.requested_capabilities(); if caps.is_empty() { eprintln!(" Capabilities: none (pure sandbox)"); } else { eprintln!(" Capabilities:"); for cap in &caps { eprintln!(" - {}", format_capability(cap)); } }
Ok(())}
// --- Verify ---
fn cmd_verify(paths: &WasmBoxPaths, name: &str) -> Result<()> { let (tool_name, version) = parse_tool_spec(name); let version = find_installed_version(paths, &tool_name, version.as_deref())?; let manifest_path = paths.tool_manifest_path(&tool_name, &version); let manifest = wasmbox_manifest::Manifest::from_file(&manifest_path) .map_err(|e| anyhow::anyhow!("{e}"))?;
let wasm_path = paths.tool_wasm_path(&tool_name, &version, &manifest.binary.wasm); let wasm_bytes = std::fs::read(&wasm_path) .with_context(|| format!("reading {}", wasm_path.display()))?;
match wasmbox_verify::verify_hash(&wasm_bytes, &manifest.binary.hash) { Ok(()) => { eprintln!( "{} {} v{} - hash matches manifest", "verified".green().bold(), tool_name, version, ); Ok(()) } Err(e) => { anyhow::bail!("verification failed for {tool_name} v{version}: {e}"); } }}
// --- List ---
fn cmd_list(paths: &WasmBoxPaths, json: bool) -> Result<()> { let cache_dir = paths.cache_dir();
if !cache_dir.exists() { if json { println!("[]"); } else { eprintln!("no tools installed"); } return Ok(()); }
let mut tools: Vec<serde_json::Value> = Vec::new();
for entry in std::fs::read_dir(&cache_dir)? { let entry = entry?; if !entry.file_type()?.is_dir() { continue; } let tool_name = entry.file_name().to_string_lossy().to_string();
for ver_entry in std::fs::read_dir(entry.path())? { let ver_entry = ver_entry?; if !ver_entry.file_type()?.is_dir() { continue; } let version = ver_entry.file_name().to_string_lossy().to_string(); let manifest_path = ver_entry.path().join("wasmbox.toml");
if json { if let Ok(m) = wasmbox_manifest::Manifest::from_file(&manifest_path) { tools.push(serde_json::to_value(m.to_metadata())?); } } else if let Ok(m) = wasmbox_manifest::Manifest::from_file(&manifest_path) { eprintln!( " {} v{} - {}", m.tool.name.bold(), m.tool.version, m.tool.description, ); } else { eprintln!(" {} v{}", tool_name.bold(), version); } } }
if json { println!("{}", serde_json::to_string_pretty(&tools)?); }
Ok(())}
// --- Remove ---
fn cmd_remove(paths: &WasmBoxPaths, name: &str) -> Result<()> { let (tool_name, version) = parse_tool_spec(name); let tool_cache = paths.cache_dir().join(&tool_name);
if !tool_cache.exists() { anyhow::bail!("tool not installed: {tool_name}"); }
if let Some(version) = version { // Remove specific version let version_dir = tool_cache.join(&version); if !version_dir.exists() { anyhow::bail!("version {version} not found for {tool_name}"); } std::fs::remove_dir_all(&version_dir)?; eprintln!("{} {} v{}", "removed".green().bold(), tool_name, version);
// If no versions left, remove the tool directory if std::fs::read_dir(&tool_cache)?.next().is_none() { std::fs::remove_dir(&tool_cache)?; } } else { // Remove all versions std::fs::remove_dir_all(&tool_cache)?; eprintln!("{} {} (all versions)", "removed".green().bold(), tool_name); }
// Remove permissions let perm_path = paths.permissions_file(); if perm_path.exists() { let mut store = wasmbox_permissions::PermissionStore::load(&perm_path) .map_err(|e| anyhow::anyhow!("{e}"))?; store.revoke(&tool_name).map_err(|e| anyhow::anyhow!("{e}"))?; }
Ok(())}
// --- Update ---
async fn cmd_update(paths: &WasmBoxPaths, name: &str) -> Result<()> { let (tool_name, _) = parse_tool_spec(name); let current_version = find_installed_version(paths, &tool_name, None)?; let manifest_path = paths.tool_manifest_path(&tool_name, ¤t_version); let current_manifest = wasmbox_manifest::Manifest::from_file(&manifest_path) .map_err(|e| anyhow::anyhow!("{e}"))?;
let registries = get_registries(paths, None)?; let client = wasmbox_registry::RegistryClient::new(registries.clone());
// Find latest version in registries let mut latest_entry = None; for reg in ®istries { if let Ok(index) = client.fetch_index(reg).await && let Some(entry) = index.tools.iter().find(|t| t.name == tool_name) { latest_entry = Some((reg.clone(), entry.clone())); break; } }
let (registry, entry) = latest_entry .ok_or_else(|| anyhow::anyhow!("tool not found in registries: {tool_name}"))?;
if entry.version == current_version && entry.hash == current_manifest.binary.hash { eprintln!("{} {} is already up to date (v{})", "ok".green().bold(), tool_name, current_version); return Ok(()); }
eprintln!( "{} {} v{} -> v{}", "updating".cyan().bold(), tool_name, current_version, entry.version, ); eprintln!(" old hash: {}", current_manifest.binary.hash); eprintln!(" new hash: {}", entry.hash);
// Download new version let wasm_bytes = client .fetch_binary(®istry, &tool_name) .await .map_err(|e| anyhow::anyhow!("{e}"))?;
wasmbox_verify::verify_hash(&wasm_bytes, &entry.hash) .map_err(|e| anyhow::anyhow!("verification failed: {e}"))?;
let manifest_json = client .fetch_manifest(®istry, &tool_name) .await .map_err(|e| anyhow::anyhow!("{e}"))?;
// Store new version (keep old for rollback) — atomic write let new_manifest = wasmbox_manifest::Manifest::parse(&manifest_json) .map_err(|e| anyhow::anyhow!("{e}"))?;
let cache_dir = paths.cache_dir(); std::fs::create_dir_all(&cache_dir)?; let staging_dir = tempfile::tempdir_in(&cache_dir)?;
std::fs::write(staging_dir.path().join("wasmbox.toml"), &manifest_json)?; std::fs::write(staging_dir.path().join(&new_manifest.binary.wasm), &wasm_bytes)?;
let tool_dir = paths.tool_dir(&tool_name, &entry.version); if tool_dir.exists() { std::fs::remove_dir_all(&tool_dir)?; } std::fs::create_dir_all(tool_dir.parent().unwrap())?; std::fs::rename(staging_dir.path(), &tool_dir)?; let _ = staging_dir.keep();
eprintln!( "{} {} to v{} (v{} kept for rollback)", "updated".green().bold(), tool_name, entry.version, current_version, );
Ok(())}
// --- Permissions ---
fn cmd_permissions( paths: &WasmBoxPaths, name: &str, action: &Option<PermissionAction>, json: bool,) -> Result<()> { let perm_path = paths.permissions_file(); let mut store = wasmbox_permissions::PermissionStore::load(&perm_path) .map_err(|e| anyhow::anyhow!("{e}"))?;
match action { Some(PermissionAction::Revoke) => { store.revoke(name).map_err(|e| anyhow::anyhow!("{e}"))?; eprintln!("{} all permissions for {name}", "revoked".green().bold()); } Some(PermissionAction::Show) | None => { if let Some(perm) = store.get(name) { if json { println!("{}", serde_json::to_string_pretty(perm)?); } else { print_permissions(name, perm); } } else { eprintln!("no permissions granted for {name}"); } } }
Ok(())}
// --- Revoke ---
fn cmd_revoke(paths: &WasmBoxPaths, name: &str, capability: &str) -> Result<()> { let perm_path = paths.permissions_file(); let mut store = wasmbox_permissions::PermissionStore::load(&perm_path) .map_err(|e| anyhow::anyhow!("{e}"))?;
let changed = store .revoke_capability(name, capability) .map_err(|e| anyhow::anyhow!("{e}"))?;
if changed { eprintln!( "{} {capability} for {name}", "revoked".green().bold(), ); } else { eprintln!("no {capability} permission found for {name}"); }
Ok(())}
// --- Audit ---
#[allow(clippy::too_many_arguments)]fn cmd_audit( paths: &WasmBoxPaths, json: bool, export: bool, format: &str, sign: bool, init_key: bool, verify: Option<&str>, from: Option<&str>, to: Option<&str>,) -> Result<()> { // Key generation if init_key { paths.ensure_dirs()?; let pub_key = wasmbox_compliance::generate_signing_key(paths.home()) .map_err(|e| anyhow::anyhow!("{e}"))?; eprintln!( "{} Ed25519 keypair at {}", "generated".green().bold(), paths.home().join("audit_key.pem").display(), ); eprintln!("Public key: {pub_key}"); eprintln!("Register this public key with your compliance team."); return Ok(()); }
// Verify a signed report if let Some(report_path) = verify { let content = std::fs::read_to_string(report_path) .with_context(|| format!("reading {report_path}"))?; let report: wasmbox_compliance::AuditReport = serde_json::from_str(&content)?; let result = wasmbox_compliance::verify_report(&report) .map_err(|e| anyhow::anyhow!("{e}"))?; if result.valid { eprintln!("Signature: {}", "VALID".green().bold()); } else { eprintln!("Signature: {}", "INVALID".red().bold()); } eprintln!("Signed by: {}", result.public_key); eprintln!("Report generated: {}", result.generated_at); if !result.valid { anyhow::bail!("verification failed: signature is invalid"); } return Ok(()); }
// Export compliance report if export { let period_from = from .map(parse_date_start) .transpose()? .unwrap_or_else(|| Utc::now() - chrono::Duration::days(90)); let period_to = to .map(parse_date_end) .transpose()? .unwrap_or_else(Utc::now);
let installed_tools = collect_tool_audit_entries(paths)?;
let mut report = wasmbox_compliance::generate_report( paths.home(), env!("CARGO_PKG_VERSION"), period_from, period_to, installed_tools, ) .map_err(|e| anyhow::anyhow!("{e}"))?;
if sign { wasmbox_compliance::sign_report(&mut report, paths.home()) .map_err(|e| anyhow::anyhow!("{e}"))?; }
match format { "json" => println!("{}", serde_json::to_string_pretty(&report)?), "md" => println!("{}", wasmbox_compliance::format_markdown(&report)), "csv" => println!("{}", wasmbox_compliance::format_csv(&report)), _ => anyhow::bail!("unsupported format: {format}. Use json, md, or csv"), } return Ok(()); }
// Default: show permissions (backwards compatible) let perm_path = paths.permissions_file(); if !perm_path.exists() { if json { println!("{{}}"); } else { eprintln!("no permissions granted"); } return Ok(()); }
let store = wasmbox_permissions::PermissionStore::load(&perm_path) .map_err(|e| anyhow::anyhow!("{e}"))?;
let tools = store.list_all();
if json { // Include policy status per tool let policy_store = wasmbox_compliance::PolicyStore::load(paths.home()) .map_err(|e| anyhow::anyhow!("{e}"))?;
let mut output = serde_json::Map::new(); for (name, perm) in tools { let mut perm_val = serde_json::to_value(perm)?; if let Some(obj) = perm_val.as_object_mut() { let status = if policy_store.has_policy() { let caps: Vec<String> = store.to_capabilities(name).iter().map(format_capability).collect(); let hash = get_installed_hash(paths, name).unwrap_or_default(); policy_store.check_tool(name, &hash, &caps).status_str().to_string() } else { "no_policy".to_string() }; obj.insert("policy_status".to_string(), serde_json::Value::String(status)); } output.insert(name.clone(), perm_val); } println!("{}", serde_json::to_string_pretty(&output)?); } else if tools.is_empty() { eprintln!("no permissions granted"); } else { for (name, perm) in tools { print_permissions(name, perm); eprintln!(); } }
Ok(())}
// --- Log ---
#[allow(clippy::too_many_arguments)]fn cmd_log( paths: &WasmBoxPaths, json: bool, last: Option<usize>, tool: Option<&str>, blocked: bool, after: Option<&str>, before: Option<&str>, export: Option<&str>, rotate: bool, stats: bool,) -> Result<()> { let run_log = RunLog::new(paths.home());
if rotate { run_log.rotate().map_err(|e| anyhow::anyhow!("{e}"))?; eprintln!("{} log rotated", "ok".green().bold()); return Ok(()); }
if let Some(dest) = export { let count = run_log .export(std::path::Path::new(dest)) .map_err(|e| anyhow::anyhow!("{e}"))?; eprintln!("{} {count} entries to {dest}", "exported".green().bold()); return Ok(()); }
if stats { let s = run_log.stats().map_err(|e| anyhow::anyhow!("{e}"))?; if json { println!("{}", serde_json::to_string_pretty(&s)?); } else { eprintln!("Total runs: {}", s.total_runs); eprintln!("Unique tools: {}", s.unique_tools); eprintln!("Blocked: {}", s.blocked); eprintln!("Hash failures: {}", s.hash_failures); if let Some(first) = s.first_entry { eprintln!("First entry: {first}"); } if let Some(last) = s.last_entry { eprintln!("Last entry: {last}"); } } return Ok(()); }
let after_dt = after.map(parse_date_start).transpose()?; let before_dt = before.map(parse_date_end).transpose()?;
let entries = run_log .read_filtered(tool, blocked, after_dt, before_dt, last) .map_err(|e| anyhow::anyhow!("{e}"))?;
if json { println!("{}", serde_json::to_string_pretty(&entries)?); } else if entries.is_empty() { eprintln!("no log entries"); } else { for entry in &entries { let status = match entry.policy.as_str() { "blocked" => entry.policy.red().to_string(), "warned" => entry.policy.yellow().to_string(), _ => entry.policy.green().to_string(), }; let exit = entry .exit_code .map(|c| c.to_string()) .unwrap_or_else(|| "-".to_string()); let dur = entry .duration_ms .map(|d| format!("{d}ms")) .unwrap_or_else(|| "-".to_string()); eprintln!( " {} {} v{} [{}] exit={} dur={} policy={}", entry.ts.format("%Y-%m-%dT%H:%M:%S"), entry.tool.bold(), entry.version, if entry.hash_verified { "ok" } else { "FAIL" }, exit, dur, status, ); } eprintln!("\n{} entries", entries.len()); }
Ok(())}
// --- Policy ---
fn cmd_policy(paths: &WasmBoxPaths, action: &PolicyAction, json: bool) -> Result<()> { match action { PolicyAction::Init { name, approved_by, enforcement, } => { let mode = match enforcement.as_str() { "enforce" => wasmbox_compliance::EnforcementMode::Enforce, "warn" => wasmbox_compliance::EnforcementMode::Warn, "disabled" => wasmbox_compliance::EnforcementMode::Disabled, other => anyhow::bail!("invalid enforcement mode: {other}. Use enforce, warn, or disabled"), };
let policy_name = name .as_deref() .unwrap_or("WasmBox Policy"); let approver = approved_by .as_deref() .unwrap_or("unset");
// Collect currently installed tools let mut tools = std::collections::HashMap::new(); let cache_dir = paths.cache_dir(); if cache_dir.exists() { for entry in std::fs::read_dir(&cache_dir)? { let entry = entry?; if !entry.file_type()?.is_dir() { continue; } let tool_name = entry.file_name().to_string_lossy().to_string(); if let Ok(version) = find_installed_version(paths, &tool_name, None) { let manifest_path = paths.tool_manifest_path(&tool_name, &version); if let Ok(m) = wasmbox_manifest::Manifest::from_file(&manifest_path) { let caps: Vec<String> = m.requested_capabilities().iter().map(format_capability).collect(); tools.insert( tool_name, wasmbox_compliance::PolicyTool { approved_hash: m.binary.hash.clone(), max_version: format!( "{}.{}.x", m.tool.version.major, m.tool.version.minor, ), capabilities: caps, approved_date: Utc::now().date_naive(), risk_tier: None, notes: None, }, ); } } } }
paths.ensure_dirs()?; wasmbox_compliance::PolicyStore::init( paths.home(), policy_name, approver, mode, tools, ) .map_err(|e| anyhow::anyhow!("{e}"))?;
eprintln!( "{} policy at {}", "created".green().bold(), paths.home().join("policy.toml").display(), ); }
PolicyAction::Show => { let store = wasmbox_compliance::PolicyStore::load(paths.home()) .map_err(|e| anyhow::anyhow!("{e}"))?; let Some(policy) = store.policy() else { eprintln!("no policy file. Run: wasmbox policy init"); return Ok(()); };
if json { println!("{}", serde_json::to_string_pretty(policy)?); } else { eprintln!("Policy: {} v{}", policy.policy.name.bold(), policy.policy.version); eprintln!("Approved by: {}", policy.policy.approved_by); eprintln!("Approved date: {}", policy.policy.approved_date); eprintln!("Enforcement: {:?}", policy.policy.enforcement); eprintln!("Default action: {:?}", policy.policy.default.action); eprintln!("\nApproved tools ({}):", policy.tools.len()); for (name, tool) in &policy.tools { let risk = tool.risk_tier.as_deref().unwrap_or("-"); eprintln!( " {} [{}] caps={:?} risk={}", name.bold(), &tool.approved_hash[..20.min(tool.approved_hash.len())], tool.capabilities, risk, ); } } }
PolicyAction::Check => { let store = wasmbox_compliance::PolicyStore::load(paths.home()) .map_err(|e| anyhow::anyhow!("{e}"))?; if !store.has_policy() { eprintln!("no policy file. Run: wasmbox policy init"); return Ok(()); }
let cache_dir = paths.cache_dir(); if !cache_dir.exists() { eprintln!("no tools installed"); return Ok(()); }
let mut results = Vec::new(); for entry in std::fs::read_dir(&cache_dir)? { let entry = entry?; if !entry.file_type()?.is_dir() { continue; } let tool_name = entry.file_name().to_string_lossy().to_string(); if let Ok(version) = find_installed_version(paths, &tool_name, None) { let manifest_path = paths.tool_manifest_path(&tool_name, &version); if let Ok(m) = wasmbox_manifest::Manifest::from_file(&manifest_path) { let caps: Vec<String> = m.requested_capabilities().iter().map(format_capability).collect(); let decision = store.check_tool(&tool_name, &m.binary.hash, &caps); results.push((tool_name, version, m.binary.hash.clone(), decision)); } } }
if json { let json_results: Vec<serde_json::Value> = results .iter() .map(|(name, version, hash, decision)| { serde_json::json!({ "name": name, "version": version, "hash": hash, "status": decision.status_str(), "reason": decision.reason(), }) }) .collect(); println!("{}", serde_json::to_string_pretty(&json_results)?); } else { for (name, version, hash, decision) in &results { let status = match decision { PolicyDecision::Approved => "APPROVED".green().to_string(), PolicyDecision::Warned(r) => format!("{} ({})", "WARNED".yellow(), r), PolicyDecision::Blocked(r) => format!("{} ({})", "BLOCKED".red(), r), PolicyDecision::NoPolicy => "NO POLICY".yellow().to_string(), }; eprintln!( " {} v{} {} {}", name.bold(), version, &hash[..20.min(hash.len())], status, ); } } }
PolicyAction::Add { name } => { let mut store = wasmbox_compliance::PolicyStore::load(paths.home()) .map_err(|e| anyhow::anyhow!("{e}"))?; if !store.has_policy() { anyhow::bail!("no policy file. Run: wasmbox policy init"); }
let version = find_installed_version(paths, name, None)?; let manifest_path = paths.tool_manifest_path(name, &version); let m = wasmbox_manifest::Manifest::from_file(&manifest_path) .map_err(|e| anyhow::anyhow!("{e}"))?; let caps: Vec<String> = m.requested_capabilities().iter().map(format_capability).collect();
store .add_tool( name, wasmbox_compliance::PolicyTool { approved_hash: m.binary.hash.clone(), max_version: format!( "{}.{}.x", m.tool.version.major, m.tool.version.minor, ), capabilities: caps, approved_date: Utc::now().date_naive(), risk_tier: None, notes: None, }, ) .map_err(|e| anyhow::anyhow!("{e}"))?;
eprintln!( "{} {} v{} ({})", "added".green().bold(), name, version, &m.binary.hash[..20], ); }
PolicyAction::Remove { name } => { let mut store = wasmbox_compliance::PolicyStore::load(paths.home()) .map_err(|e| anyhow::anyhow!("{e}"))?; if !store.has_policy() { anyhow::bail!("no policy file. Run: wasmbox policy init"); } let removed = store .remove_tool(name) .map_err(|e| anyhow::anyhow!("{e}"))?; if removed { eprintln!("{} {} from policy", "removed".green().bold(), name); } else { eprintln!("{name} not in policy"); } }
PolicyAction::Enforce => { let mut store = wasmbox_compliance::PolicyStore::load(paths.home()) .map_err(|e| anyhow::anyhow!("{e}"))?; if !store.has_policy() { anyhow::bail!("no policy file. Run: wasmbox policy init"); } store .set_enforcement(wasmbox_compliance::EnforcementMode::Enforce) .map_err(|e| anyhow::anyhow!("{e}"))?; eprintln!("{} enforcement mode: {}", "set".green().bold(), "enforce".bold()); }
PolicyAction::Warn => { let mut store = wasmbox_compliance::PolicyStore::load(paths.home()) .map_err(|e| anyhow::anyhow!("{e}"))?; if !store.has_policy() { anyhow::bail!("no policy file. Run: wasmbox policy init"); } store .set_enforcement(wasmbox_compliance::EnforcementMode::Warn) .map_err(|e| anyhow::anyhow!("{e}"))?; eprintln!("{} enforcement mode: {}", "set".green().bold(), "warn".bold()); }
PolicyAction::Disable => { let mut store = wasmbox_compliance::PolicyStore::load(paths.home()) .map_err(|e| anyhow::anyhow!("{e}"))?; if !store.has_policy() { anyhow::bail!("no policy file. Run: wasmbox policy init"); } store .set_enforcement(wasmbox_compliance::EnforcementMode::Disabled) .map_err(|e| anyhow::anyhow!("{e}"))?; eprintln!("{} enforcement mode: {}", "set".green().bold(), "disabled".bold()); }
PolicyAction::Diff => { let store = wasmbox_compliance::PolicyStore::load(paths.home()) .map_err(|e| anyhow::anyhow!("{e}"))?; let Some(policy) = store.policy() else { eprintln!("no policy file. Run: wasmbox policy init"); return Ok(()); };
let cache_dir = paths.cache_dir(); let mut changes = Vec::new();
// Check installed tools against policy if cache_dir.exists() { for entry in std::fs::read_dir(&cache_dir)? { let entry = entry?; if !entry.file_type()?.is_dir() { continue; } let tool_name = entry.file_name().to_string_lossy().to_string(); if let Ok(version) = find_installed_version(paths, &tool_name, None) { let manifest_path = paths.tool_manifest_path(&tool_name, &version); if let Ok(m) = wasmbox_manifest::Manifest::from_file(&manifest_path) { if let Some(pt) = policy.tools.get(&tool_name) { if pt.approved_hash != m.binary.hash { changes.push(format!( "CHANGED: {} hash was {} now {}", tool_name, &pt.approved_hash[..20.min(pt.approved_hash.len())], &m.binary.hash[..20], )); } } else { changes.push(format!( "NEW: {} v{} installed but not in policy", tool_name, version, )); } } } } }
// Check policy tools no longer installed for name in policy.tools.keys() { if !cache_dir.join(name).exists() { changes.push(format!( "REMOVED: {} in policy but no longer installed", name, )); } }
if json { println!("{}", serde_json::to_string_pretty(&changes)?); } else if changes.is_empty() { eprintln!("no changes since last policy approval"); } else { for change in &changes { eprintln!(" {change}"); } } }
PolicyAction::Export => { let store = wasmbox_compliance::PolicyStore::load(paths.home()) .map_err(|e| anyhow::anyhow!("{e}"))?; let Some(policy) = store.policy() else { anyhow::bail!("no policy file. Run: wasmbox policy init"); }; println!("{}", toml::to_string_pretty(policy)?); }
PolicyAction::Shell => { let store = wasmbox_compliance::PolicyStore::load(paths.home()) .map_err(|e| anyhow::anyhow!("{e}"))?; let Some(policy) = store.policy() else { anyhow::bail!("no policy file. Run: wasmbox policy init"); };
// Ensure agent policy exists if policy.agent.is_none() || !policy .agent .as_ref() .is_some_and(|a| a.shell_enforcement) { eprintln!( "{}: agent shell enforcement not configured.", "warning".yellow().bold(), ); eprintln!("Run: wasmbox policy agent --enable"); eprintln!("This will add default blocked/allowed command lists to your policy."); }
// Collect installed tool names let installed_tools = collect_installed_tool_names(paths);
let script = wasmbox_compliance::generate_shell_wrapper(policy, &installed_tools); println!("{script}"); }
PolicyAction::Agent { enable, disable } => { let mut store = wasmbox_compliance::PolicyStore::load(paths.home()) .map_err(|e| anyhow::anyhow!("{e}"))?; if !store.has_policy() { anyhow::bail!("no policy file. Run: wasmbox policy init"); }
if *enable { let agent = wasmbox_compliance::AgentPolicy { shell_enforcement: true, blocked_commands: wasmbox_compliance::default_command_mappings() .iter() .map(|m| m.blocked.clone()) .collect(), allowed_commands: wasmbox_compliance::default_allowed_commands(), }; store .set_agent_policy(agent) .map_err(|e| anyhow::anyhow!("{e}"))?; eprintln!("{} agent shell enforcement", "enabled".green().bold()); eprintln!("\nGenerate the shell wrapper:"); eprintln!(" wasmbox policy shell > ~/.wasmbox/agent-shell.sh"); eprintln!(" chmod +x ~/.wasmbox/agent-shell.sh"); eprintln!("\nPoint your agent to use it:"); eprintln!(" export OPENCODE_SHELL=~/.wasmbox/agent-shell.sh"); } else if *disable { let agent = wasmbox_compliance::AgentPolicy { shell_enforcement: false, ..store .agent_policy() .cloned() .unwrap_or_default() }; store .set_agent_policy(agent) .map_err(|e| anyhow::anyhow!("{e}"))?; eprintln!("{} agent shell enforcement", "disabled".green().bold()); } else { // Show current agent policy if let Some(agent) = store.agent_policy() { if json { println!("{}", serde_json::to_string_pretty(agent)?); } else { eprintln!( "Shell enforcement: {}", if agent.shell_enforcement { "enabled".green().to_string() } else { "disabled".red().to_string() }, ); eprintln!("\nBlocked commands ({}):", agent.blocked_commands.len()); for cmd in &agent.blocked_commands { let mapping = wasmbox_compliance::default_command_mappings() .into_iter() .find(|m| m.blocked == *cmd); if let Some(m) = mapping { eprintln!(" {} -> {}", cmd.bold(), m.replacement); } else { eprintln!(" {}", cmd.bold()); } } eprintln!("\nAllowed commands ({}):", agent.allowed_commands.len()); eprintln!( " {}", agent.allowed_commands.join(", "), ); } } else { eprintln!("no agent policy configured. Run: wasmbox policy agent --enable"); } } } }
Ok(())}
/// Helper: parse a YYYY-MM-DD date as start-of-day UTC.fn parse_date_start(s: &str) -> Result<chrono::DateTime<Utc>> { let date = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") .with_context(|| format!("invalid date: {s}. Use YYYY-MM-DD"))?; Ok(date .and_hms_opt(0, 0, 0) .unwrap() .and_utc())}
/// Helper: parse a YYYY-MM-DD date as end-of-day UTC.fn parse_date_end(s: &str) -> Result<chrono::DateTime<Utc>> { let date = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") .with_context(|| format!("invalid date: {s}. Use YYYY-MM-DD"))?; Ok(date .and_hms_opt(23, 59, 59) .unwrap() .and_utc())}
/// Helper: get the hash of an installed tool's current version.fn get_installed_hash(paths: &WasmBoxPaths, tool_name: &str) -> Option<String> { let version = find_installed_version(paths, tool_name, None).ok()?; let manifest_path = paths.tool_manifest_path(tool_name, &version); let m = wasmbox_manifest::Manifest::from_file(&manifest_path).ok()?; Some(m.binary.hash.clone())}
/// Helper: collect installed tool names.fn collect_installed_tool_names(paths: &WasmBoxPaths) -> Vec<String> { let cache_dir = paths.cache_dir(); let mut names = Vec::new(); if cache_dir.exists() && let Ok(entries) = std::fs::read_dir(&cache_dir) { for entry in entries.flatten() { if entry.file_type().is_ok_and(|t| t.is_dir()) { names.push(entry.file_name().to_string_lossy().to_string()); } } } names.sort(); names}
/// Helper: collect all installed tools as ToolAuditEntry for audit export.fn collect_tool_audit_entries( paths: &WasmBoxPaths,) -> Result<Vec<wasmbox_compliance::ToolAuditEntry>> { let policy_store = wasmbox_compliance::PolicyStore::load(paths.home()) .map_err(|e| anyhow::anyhow!("{e}"))?;
let cache_dir = paths.cache_dir(); let mut entries = Vec::new();
if !cache_dir.exists() { return Ok(entries); }
for dir_entry in std::fs::read_dir(&cache_dir)? { let dir_entry = dir_entry?; if !dir_entry.file_type()?.is_dir() { continue; } let tool_name = dir_entry.file_name().to_string_lossy().to_string(); if let Ok(version) = find_installed_version(paths, &tool_name, None) { let manifest_path = paths.tool_manifest_path(&tool_name, &version); if let Ok(m) = wasmbox_manifest::Manifest::from_file(&manifest_path) { let wasm_path = paths.tool_wasm_path(&tool_name, &version, &m.binary.wasm); let hash_verified = wasm_path .exists() .then(|| { let data = std::fs::read(&wasm_path).ok()?; wasmbox_verify::verify_hash(&data, &m.binary.hash).ok() }) .flatten() .is_some();
let caps: Vec<String> = m.requested_capabilities().iter().map(format_capability).collect(); let decision = policy_store.check_tool(&tool_name, &m.binary.hash, &caps);
let policy_tool = policy_store .policy() .and_then(|p| p.tools.get(&tool_name));
entries.push(wasmbox_compliance::ToolAuditEntry { name: tool_name, version, hash: m.binary.hash.clone(), hash_verified, capabilities: caps, policy_status: decision.status_str().to_string(), risk_tier: policy_tool.and_then(|t| t.risk_tier.clone()), approved_date: policy_tool.map(|t| t.approved_date.to_string()), notes: policy_tool.and_then(|t| t.notes.clone()), }); } } }
Ok(entries)}
// --- Registry management ---
fn cmd_registry(paths: &WasmBoxPaths, action: &RegistryAction) -> Result<()> { paths.ensure_dirs()?; let config_path = paths.config_file(); let mut config = WasmBoxConfig::load(&config_path) .map_err(|e| anyhow::anyhow!("config error: {e}"))?;
match action { RegistryAction::Add { url } => { if config.registries.iter().any(|r| r.url == *url) { eprintln!("registry already configured: {url}"); return Ok(()); } config.registries.push(wasmbox_shared::RegistryConfig { url: url.clone(), name: None, }); config .save(&config_path) .map_err(|e| anyhow::anyhow!("{e}"))?; eprintln!("{} registry {url}", "added".green().bold()); } RegistryAction::List => { if config.registries.is_empty() { eprintln!("no registries configured"); eprintln!( "add one with: wasmbox registry add <url>" ); } else { for reg in &config.registries { if let Some(ref name) = reg.name { eprintln!(" {} ({})", reg.url, name); } else { eprintln!(" {}", reg.url); } } } } RegistryAction::Remove { url } => { let before = config.registries.len(); config.registries.retain(|r| r.url != *url); if config.registries.len() < before { config .save(&config_path) .map_err(|e| anyhow::anyhow!("{e}"))?; eprintln!("{} registry {url}", "removed".green().bold()); } else { eprintln!("registry not found: {url}"); } } }
Ok(())}
// --- Hash ---
fn cmd_hash(file: &str) -> Result<()> { let path = std::path::Path::new(file); if !path.exists() { anyhow::bail!("file not found: {file}"); } let hash = wasmbox_verify::hash_file(path).map_err(|e| anyhow::anyhow!("hash error: {e}"))?; println!("{hash}"); Ok(())}
// --- Helpers ---
fn get_registries( paths: &WasmBoxPaths, override_url: Option<&str>,) -> Result<Vec<wasmbox_registry::Registry>> { if let Some(url) = override_url { return Ok(vec![wasmbox_registry::Registry { url: url.to_string(), }]); }
let config_path = paths.config_file(); let config = WasmBoxConfig::load(&config_path) .map_err(|e| anyhow::anyhow!("config error: {e}"))?;
if config.registries.is_empty() { anyhow::bail!( "no registries configured. Add one with: wasmbox registry add <url>" ); }
Ok(config .registries .iter() .map(|r| wasmbox_registry::Registry { url: r.url.clone(), }) .collect())}
fn human_size(bytes: u64) -> String { if bytes < 1024 { format!("{bytes} B") } else if bytes < 1024 * 1024 { format!("{:.1} KB", bytes as f64 / 1024.0) } else { format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0)) }}
fn format_capability(cap: &Capability) -> String { match cap { Capability::Stdin => "stdin".to_string(), Capability::Stdout => "stdout".to_string(), Capability::Clipboard => "clipboard".to_string(), Capability::Network(hosts) => format!("network: {}", hosts.join(", ")), Capability::Filesystem(g) => { let mode = match (g.read, g.write) { (true, true) => "rw", (true, false) => "r", (false, true) => "w", (false, false) => "none", }; format!("filesystem: {} ({})", g.path, mode) } Capability::Env(vars) => format!("env: {}", vars.join(", ")), }}
fn print_permissions(name: &str, perm: &wasmbox_permissions::ToolPermissions) { eprintln!("{} v{}:", name.bold(), perm.version); if perm.stdout { eprintln!(" stdout: {}", "granted".green()); } if perm.stdin { eprintln!(" stdin: {}", "granted".green()); } if perm.clipboard { eprintln!(" clipboard: {}", "granted".green()); } if !perm.network.is_empty() { eprintln!(" network: {}", perm.network.join(", ")); } if !perm.filesystem.is_empty() { for fs in &perm.filesystem { eprintln!( " filesystem: {} (read={}, write={})", fs.path, fs.read, fs.write, ); } } if !perm.env.is_empty() { eprintln!(" env: {}", perm.env.join(", ")); }}