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.
84 kB · 2478 lines
Rust
at dev
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479use 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>, /// Scope the export to a single session ID #[arg(long)] session: 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, /// Filter by session ID #[arg(long)] session: Option<String>, /// Filter by execution mode (wasmbox, via, blocked, native) #[arg(long)] mode: Option<String>, /// Filter by binary hash #[arg(long)] binary_hash: Option<String>, }, /// 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, }, /// Proxy a single command: log it, then exec the real binary Via { /// Pipe and count stdin/stdout/stderr bytes (loses interactive TTY behaviour) #[arg(long)] capture_streams: bool, /// Command name to proxy cmd: String, /// Arguments to pass to the command #[arg(trailing_var_arg = true, allow_hyphen_values = true)] args: Vec<String>, }, /// Manage PATH shims that route commands through `wasmbox via` Shell { #[command(subcommand)] action: ShellAction, }, /// Launch an agent inside the recorded environment Wrap { /// Agent command to launch agent: String, /// Start a fresh session even if WASMBOX_SESSION_ID is set #[arg(long)] new_session: bool, /// Arguments to pass to the agent #[arg(trailing_var_arg = true, allow_hyphen_values = true)] args: Vec<String>, }, /// Launch Claude Code in the recorded environment (alias for `wrap claude`) Claude { #[arg(trailing_var_arg = true, allow_hyphen_values = true)] args: Vec<String>, }, /// Launch OpenCode in the recorded environment (alias for `wrap opencode`) Opencode { #[arg(trailing_var_arg = true, allow_hyphen_values = true)] args: Vec<String>, }, /// Launch Aider in the recorded environment (alias for `wrap aider`) Aider { #[arg(trailing_var_arg = true, allow_hyphen_values = true)] args: Vec<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 ShellAction { /// Generate PATH shims for commands Init { /// Shim every executable in standard bin directories #[arg(long)] all: bool, /// Extra commands to shim (comma-separated) #[arg(long, value_delimiter = ',')] include: Vec<String>, /// Commands to skip (comma-separated) #[arg(long, value_delimiter = ',')] exclude: Vec<String>, /// Regenerate all shims #[arg(long)] rebuild: bool, /// Append the PATH export line to the user's shell rc file #[arg(long)] auto_add_to_shell: bool, }, /// Show shim directory status Status, /// Regenerate all shims (alias for `init --rebuild`) Rebuild,}
#[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,}
#[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, ref session, } => cmd_audit( &paths, cli.json, export, format, sign, init_key, verify.as_deref(), from.as_deref(), to.as_deref(), session.as_deref(), ), Commands::Log { last, ref tool, blocked, ref after, ref before, ref export, rotate, stats, ref session, ref mode, ref binary_hash, } => cmd_log( &paths, cli.json, last, tool.as_deref(), blocked, after.as_deref(), before.as_deref(), export.as_deref(), rotate, stats, session.as_deref(), mode.as_deref(), binary_hash.as_deref(), ), 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), Commands::Via { capture_streams, ref cmd, ref args, } => cmd_via(&paths, cmd, args, capture_streams), Commands::Shell { ref action } => cmd_shell(&paths, action, cli.json), Commands::Wrap { ref agent, new_session, ref args, } => cmd_wrap(&paths, agent, new_session, args), Commands::Claude { ref args } => cmd_wrap(&paths, "claude", false, args), Commands::Opencode { ref args } => cmd_wrap(&paths, "opencode", false, args), Commands::Aider { ref args } => cmd_wrap(&paths, "aider", false, args), };
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, session_id: std::env::var("WASMBOX_SESSION_ID").ok(), mode: "wasmbox".to_string(), path: None, binary_hash: None, args: None, killed: None, stdin_bytes: None, stdout_bytes: None, stderr_bytes: 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()), session_id: std::env::var("WASMBOX_SESSION_ID").ok(), mode: "wasmbox".to_string(), path: None, binary_hash: None, args: None, killed: None, stdin_bytes: None, stdout_bytes: None, stderr_bytes: None, }; 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), session_id: std::env::var("WASMBOX_SESSION_ID").ok(), mode: "wasmbox".to_string(), path: None, binary_hash: None, args: None, killed: None, stdin_bytes: None, stdout_bytes: None, stderr_bytes: None, }; 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), session_id: std::env::var("WASMBOX_SESSION_ID").ok(), mode: "wasmbox".to_string(), path: None, binary_hash: None, args: None, killed: None, stdin_bytes: None, stdout_bytes: None, stderr_bytes: None, }; 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>, session: 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}"))?; // Detect report shape: session reports carry `report_type: // "session_audit"` at the top level; compliance reports do not. let probe: serde_json::Value = serde_json::from_str(&content)?; let is_session = probe.get("report_type").and_then(|v| v.as_str()) == Some("session_audit"); let result = if is_session { let report: wasmbox_compliance::SessionReport = serde_json::from_str(&content)?; wasmbox_compliance::verify_session_report(&report) .map_err(|e| anyhow::anyhow!("{e}"))? } else { let report: wasmbox_compliance::AuditReport = serde_json::from_str(&content)?; 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 { // Session-scoped export. if let Some(session_id) = session { let mut report = wasmbox_compliance::generate_session_report( paths.home(), env!("CARGO_PKG_VERSION"), session_id, ) .map_err(|e| anyhow::anyhow!("{e}"))?; if sign { wasmbox_compliance::sign_session_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_session_markdown(&report)), other => anyhow::bail!("unsupported format for session export: {other}"), } return Ok(()); }
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, session: Option<&str>, mode: Option<&str>, binary_hash: Option<&str>,) -> 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 = if session.is_some() || mode.is_some() || binary_hash.is_some() { run_log .read_advanced(session, mode, binary_hash) .map_err(|e| anyhow::anyhow!("{e}"))? } else { 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()); // Sandboxed-tool entries carry a version + hash check; proxied // (`via`) and other entries show their execution mode instead. let tag = if entry.mode == "wasmbox" { format!( "v{} [{}]", entry.version, if entry.hash_verified { "ok" } else { "FAIL" }, ) } else { format!("[{}]", entry.mode) }; eprintln!( " {} {} {} exit={} dur={} policy={}", entry.ts.format("%Y-%m-%dT%H:%M:%S"), entry.tool.bold(), tag, 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)?); } }
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 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(())}
// --- Via ---
fn cmd_via(paths: &WasmBoxPaths, cmd: &str, args: &[String], capture_streams: bool) -> Result<()> { let shim_dir = paths.home().join("proxy"); let policy_store = wasmbox_compliance::PolicyStore::load(paths.home()).map_err(|e| anyhow::anyhow!("{e}"))?; let proxy_policy = resolve_proxy_policy(&policy_store);
let req = wasmbox_compliance::ViaRequest { cmd: cmd.to_string(), args: args.to_vec(), home: paths.home().to_path_buf(), shim_dir, session_id: std::env::var("WASMBOX_SESSION_ID").ok(), };
let outcome = if capture_streams { wasmbox_compliance::run_via_capture(&req) } else { wasmbox_compliance::run_via_with_policy(req, proxy_policy.as_ref()) } .map_err(|e| anyhow::anyhow!("{e}"))?;
// Mirror the child's outcome exactly. std::process::exit(outcome.exit_code);}
/// Resolve the effective proxy policy: the policy file's `[proxy]` section,/// with the mode overridden by `WASMBOX_MODE` if that env var is set.fn resolve_proxy_policy( store: &wasmbox_compliance::PolicyStore,) -> Option<wasmbox_compliance::ProxyPolicy> { let mut policy = store.proxy_policy().cloned(); if let Ok(mode_str) = std::env::var("WASMBOX_MODE") { let mode = match mode_str.as_str() { "record" => wasmbox_compliance::ProxyMode::Record, "suggest" => wasmbox_compliance::ProxyMode::Suggest, "enforce" => wasmbox_compliance::ProxyMode::Enforce, _ => return policy, }; let mut p = policy.unwrap_or_default(); p.mode = mode; policy = Some(p); } policy}
// --- Shell ---
fn cmd_shell(paths: &WasmBoxPaths, action: &ShellAction, json: bool) -> Result<()> { match action { ShellAction::Status => { let status = wasmbox_compliance::shim_status(paths.home()) .map_err(|e| anyhow::anyhow!("{e}"))?; if json { println!("{}", serde_json::to_string_pretty(&status)?); } else { eprintln!("Shim directory: {}", status.proxy_dir); eprintln!("Shims: {}", status.shim_count); eprintln!("On PATH: {}", if status.active { "yes" } else { "no" }); } Ok(()) } ShellAction::Rebuild => cmd_shell_init(paths, json, false, &[], &[], false), ShellAction::Init { all, include, exclude, rebuild: _, auto_add_to_shell, } => cmd_shell_init(paths, json, *all, include, exclude, *auto_add_to_shell), }}
fn cmd_shell_init( paths: &WasmBoxPaths, json: bool, all: bool, include: &[String], exclude: &[String], auto_add_to_shell: bool,) -> Result<()> { let mut commands: Vec<String> = if all { scan_all_executables() } else { wasmbox_compliance::curated_commands() .into_iter() .map(String::from) .collect() }; for c in include { if !commands.contains(c) { commands.push(c.clone()); } } commands.retain(|c| !exclude.contains(c));
let report = wasmbox_compliance::generate_shims(paths.home(), &commands) .map_err(|e| anyhow::anyhow!("{e}"))?;
let export_line = format!("export PATH=\"{}:$PATH\"", report.proxy_dir); if auto_add_to_shell { append_to_shell_rc(&export_line)?; }
if json { println!("{}", serde_json::to_string_pretty(&report)?); } else { eprintln!( "{} {} shims in {}", "generated".green().bold(), report.created, report.proxy_dir, ); if !auto_add_to_shell { eprintln!("\nTo activate, add to ~/.zshrc or ~/.bashrc:"); eprintln!(" {export_line}"); } } Ok(())}
/// Scan standard bin directories for executables (for `--all`).fn scan_all_executables() -> Vec<String> { use std::os::unix::fs::PermissionsExt; let mut found = std::collections::BTreeSet::new(); let dirs = ["/usr/bin", "/usr/local/bin"]; let mut all_dirs: Vec<std::path::PathBuf> = dirs.iter().map(std::path::PathBuf::from).collect(); if let Some(home) = std::env::var_os("HOME") { all_dirs.push(std::path::PathBuf::from(home).join(".local/bin")); } for dir in all_dirs { if let Ok(rd) = std::fs::read_dir(&dir) { for entry in rd.filter_map(|e| e.ok()) { let path = entry.path(); if let Ok(meta) = std::fs::metadata(&path) && meta.is_file() && meta.permissions().mode() & 0o111 != 0 && let Some(name) = path.file_name().and_then(|n| n.to_str()) { found.insert(name.to_string()); } } } } found.into_iter().collect()}
/// Append the PATH export line to the user's shell rc file (~/.zshrc or ~/.bashrc).fn append_to_shell_rc(export_line: &str) -> Result<()> { let home = std::env::var_os("HOME").ok_or_else(|| anyhow::anyhow!("HOME is not set"))?; let shell = std::env::var("SHELL").unwrap_or_default(); let rc = if shell.ends_with("zsh") { ".zshrc" } else { ".bashrc" }; let rc_path = std::path::PathBuf::from(home).join(rc); let mut file = std::fs::OpenOptions::new() .create(true) .append(true) .open(&rc_path) .with_context(|| format!("opening {}", rc_path.display()))?; use std::io::Write; writeln!(file, "\n# Added by wasmbox shell init\n{export_line}")?; eprintln!( "{} PATH export to {}", "added".green().bold(), rc_path.display() ); Ok(())}
// --- Wrap ---
fn cmd_wrap(paths: &WasmBoxPaths, agent: &str, new_session: bool, args: &[String]) -> Result<()> { // Resolve the agent binary up front for a friendly not-installed error. let shim_dir = paths.home().join("proxy"); if wasmbox_compliance::resolve_binary_path(agent, &shim_dir).is_none() { anyhow::bail!("agent '{agent}' not found on PATH. Install it, or check the name."); }
let session_id = wasmbox_compliance::resolve_session_id(new_session); let mut entries = vec![shim_dir.clone()]; entries.extend(std::env::split_paths( &std::env::var_os("PATH").unwrap_or_default(), )); let path_var = std::env::join_paths(entries).map_err(|e| anyhow::anyhow!("building PATH: {e}"))?;
eprintln!( "{} session {} starting", "WasmBox".cyan().bold(), session_id, );
let status = std::process::Command::new(agent) .args(args) .env("WASMBOX_SESSION_ID", &session_id) .env("PATH", &path_var) .status() .with_context(|| format!("launching {agent}"))?;
// Print the session summary unless quieted. if std::env::var("WASMBOX_QUIET").as_deref() != Ok("1") { let summary = wasmbox_compliance::build_summary(paths.home(), &session_id) .map_err(|e| anyhow::anyhow!("{e}"))?; eprintln!("\n{}", summary.render()); }
std::process::exit(status.code().unwrap_or(1));}
// --- 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(", ")); }}