Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ async-openai = "0.14"
tokio = { version = "1.35", features = ["full"] }
futures = "0.3"

[target.'cfg(target_os = "linux")'.dependencies]
seccompiler = "0.4.0"
libc = "0.2"

[features]
hardware-wallet = ["dep:hidapi", "dep:trezor-client"]

Expand Down
118 changes: 101 additions & 17 deletions src/commands/plugin.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use crate::plugins::interface::CORE_VERSION;
use crate::plugins::manifest;
use crate::plugins::registry::{self, RegisteredCommand, TrustLevel, UninstallOptions};
use crate::plugins::{PluginLoadError, PluginManager};
use crate::plugins::{Capability, PluginLoadError, PluginManager};
use crate::utils::print as p;
use anyhow::{Context, Result};
use anyhow::Result;
use clap::Subcommand;
use std::path::Path;
use std::path::PathBuf;
Expand Down Expand Up @@ -151,13 +151,35 @@ fn install(name: String, path: Option<PathBuf>, source: Option<String>, force: b
}
};

let meta = match get_plugin_metadata(&lib_path) {
Ok(m) => m,
Err(error) => {
p::warn(&format!(
"Plugin registered without capability discovery: {}",
error
));
crate::plugins::loader::PluginMetadataDump {
name: name.clone(),
version: plugin_manifest.version.clone(),
description: "".to_string(),
capabilities: Vec::new(),
commands: discovered_commands.clone(),
}
}
};

let approved_caps = audit_and_approve_capabilities(&name, &meta.capabilities)?;
let content_hash = crate::plugins::loader::calculate_sha256(&lib_path).ok();

registry::install_plugin(
&name,
&lib_path,
source_str,
&plugin_manifest.starforge_version,
&plugin_manifest.version,
discovered_commands.clone(),
approved_caps,
content_hash,
)?;

p::header("Plugin Install");
Expand Down Expand Up @@ -435,13 +457,24 @@ fn update(name: Option<String>, yes: bool) -> Result<()> {

match status {
Ok(s) if s.success() => {
let path = std::path::Path::new(&pl.path);
let meta = get_plugin_metadata(path).ok();
let caps = if let Some(ref m) = meta {
audit_and_approve_capabilities(&pl.name, &m.capabilities)?
} else {
pl.capabilities.clone()
};
let content_hash = crate::plugins::loader::calculate_sha256(path).ok();

registry::install_plugin(
&pl.name,
std::path::Path::new(&pl.path),
path,
&pl.source,
&pl.starforge_version,
&pl.plugin_version,
pl.commands.clone(),
caps,
content_hash,
)?;
p::success(&format!(" '{}' updated via cargo install", pl.name));
updated += 1;
Expand Down Expand Up @@ -481,15 +514,26 @@ fn update(name: Option<String>, yes: bool) -> Result<()> {

if modified > installed_epoch {
// Library on disk is newer — refresh the registry entry.
let path = std::path::Path::new(&pl.path);
let meta = get_plugin_metadata(path).ok();
let caps = if let Some(ref m) = meta {
audit_and_approve_capabilities(&pl.name, &m.capabilities)?
} else {
pl.capabilities.clone()
};
let content_hash = crate::plugins::loader::calculate_sha256(path).ok();
let cmds = discover_commands_from_library(&pl.path)
.unwrap_or_else(|_| pl.commands.clone());

registry::install_plugin(
&pl.name,
std::path::Path::new(&pl.path),
path,
&pl.source,
&pl.starforge_version,
&pl.plugin_version,
cmds,
caps,
content_hash,
)?;
p::success(&format!(
" '{}' library on disk is newer — registry refreshed.",
Expand Down Expand Up @@ -883,19 +927,59 @@ fn print_audit_report(report: &AuditReport) {
}

fn discover_commands_from_library(path: &str) -> Result<Vec<RegisteredCommand>> {
let mut pm = PluginManager::new();
unsafe {
pm.load_plugin(path)
.with_context(|| format!("Failed to load plugin from {}", path))?;
}
Ok(pm
.list_commands()
.into_iter()
.map(|c| RegisteredCommand {
name: c.name,
description: c.description,
})
.collect())
let meta = get_plugin_metadata(Path::new(path))?;
Ok(meta.commands)
}

fn get_plugin_metadata(library_path: &Path) -> Result<crate::plugins::loader::PluginMetadataDump> {
let output = std::process::Command::new(std::env::current_exe()?)
.arg("__dump_plugin_metadata")
.arg(library_path)
.output()?;
if !output.status.success() {
let err = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("Failed to dump plugin metadata: {}", err);
}
let meta: crate::plugins::loader::PluginMetadataDump = serde_json::from_slice(&output.stdout)?;
Ok(meta)
}

fn audit_and_approve_capabilities(
name: &str,
capabilities: &[Capability],
) -> Result<Vec<Capability>> {
if capabilities.is_empty() {
return Ok(Vec::new());
}

p::header("Plugin Capability Audit");
p::warn(&format!(
"Plugin '{}' requests the following permissions/capabilities:",
name
));
for cap in capabilities {
p::warn(&format!(" • {}: {}", cap.name(), cap.description()));
}
println!();

use std::io::IsTerminal;
let approved = if std::io::stdin().is_terminal() {
let theme = dialoguer::theme::ColorfulTheme::default();
dialoguer::Confirm::with_theme(&theme)
.with_prompt("Do you want to grant these capabilities and load/install the plugin?")
.default(false)
.interact()
.unwrap_or(false)
} else {
p::warn("Non-interactive terminal detected: auto-approving capabilities.");
true
};

if !approved {
anyhow::bail!("Capability approval declined by user");
}

Ok(capabilities.to_vec())
}

fn commands(name: Option<String>) -> Result<()> {
Expand Down
21 changes: 9 additions & 12 deletions src/commands/telemetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,16 @@ fn handle_status() -> Result<()> {

p::header("Telemetry Status");
p::separator();
p::kv("Collection", if enabled { "enabled" } else { "disabled" });
p::kv(
"Collection",
if enabled { "enabled" } else { "disabled" },
"Schema Version",
&telemetry::TELEMETRY_SCHEMA_VERSION.to_string(),
);
p::kv("Schema Version", &telemetry::TELEMETRY_SCHEMA_VERSION.to_string());
p::kv("Events Stored", &count.to_string());
p::kv("Log Size", &format!("{:.1} KB", size as f64 / 1024.0));
p::kv(
"Limits",
&format!(
"{} entries / 5 MB",
telemetry::MAX_ENTRIES
),
&format!("{} entries / 5 MB", telemetry::MAX_ENTRIES),
);
if let Some(val) = env_override {
p::kv("Env Override (STARFORGE_TELEMETRY)", &val);
Expand Down Expand Up @@ -97,10 +94,7 @@ fn handle_show(limit: usize) -> Result<()> {
println!(" {}", "─".repeat(72).dimmed());

for ev in &events {
let ts = ev
.timestamp
.format("%Y-%m-%d %H:%M:%S")
.to_string();
let ts = ev.timestamp.format("%Y-%m-%d %H:%M:%S").to_string();
let status = if ev.success {
"✓ ok".green().to_string()
} else {
Expand Down Expand Up @@ -155,6 +149,9 @@ fn handle_clear(yes: bool) -> Result<()> {
}

telemetry::clear_log()?;
p::success(&format!("Telemetry log cleared ({} events removed).", count));
p::success(&format!(
"Telemetry log cleared ({} events removed).",
count
));
Ok(())
}
11 changes: 8 additions & 3 deletions src/commands/upgrade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,8 @@ fn handle_propose(args: ProposeArgs) -> Result<()> {

// ── Upgrade simulation + auth display ─────────────────────────────────
p::step(4, 5, "Simulating upgrade transaction…");
match soroban::simulate_upgrade_transaction(&args.contract_id, &new_hash, wallet, &args.network) {
match soroban::simulate_upgrade_transaction(&args.contract_id, &new_hash, wallet, &args.network)
{
Ok(sim) => {
p::kv("Estimated fee", &format!("{} stroops", sim.fee));
if !sim.auth_entries.is_empty() {
Expand Down Expand Up @@ -667,7 +668,8 @@ fn handle_execute(args: ExecuteArgs) -> Result<()> {
p::step(2, 3, "Validating multisig signer weights…");
match horizon::fetch_account_signers(&wallet.public_key, &args.network) {
Ok(signer_info) => {
let local_keys: Vec<&str> = cfg.wallets.iter().map(|w| w.public_key.as_str()).collect();
let local_keys: Vec<&str> =
cfg.wallets.iter().map(|w| w.public_key.as_str()).collect();
let available_weight: u32 = signer_info
.signers
.iter()
Expand All @@ -677,7 +679,10 @@ fn handle_execute(args: ExecuteArgs) -> Result<()> {
let required = signer_info.thresholds.high;

p::kv("On-chain high threshold", &required.to_string());
p::kv("Available local signer weight", &available_weight.to_string());
p::kv(
"Available local signer weight",
&available_weight.to_string(),
);

if required > 0 && available_weight < required {
let missing: Vec<String> = signer_info
Expand Down
29 changes: 16 additions & 13 deletions src/commands/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1941,14 +1941,15 @@ fn multisig_create(
p::kv("Signers", &account.signers.len().to_string());
if let Some(path) = xdr_output {
p::step(1, 2, "Fetching source account sequence...");
let source_account = horizon::fetch_account(&account.account_id, &network).map_err(|e| {
anyhow::anyhow!(
"Multi-sig account not found on {}: {}\nFund it with: starforge wallet fund {}",
network,
e,
account.name
)
})?;
let source_account =
horizon::fetch_account(&account.account_id, &network).map_err(|e| {
anyhow::anyhow!(
"Multi-sig account not found on {}: {}\nFund it with: starforge wallet fund {}",
network,
e,
account.name
)
})?;
p::step(2, 2, "Building unsigned SetOptions transaction XDR...");
let setup_xdr =
multisig::build_account_setup_transaction_xdr(&account, &source_account.sequence)?;
Expand All @@ -1967,8 +1968,7 @@ fn multisig_create(
" {}",
format!(
"starforge wallet multisig sign {} --transaction tx.xdr --network {}",
account.name,
network
account.name, network
)
.cyan()
);
Expand All @@ -1977,8 +1977,7 @@ fn multisig_create(
" {}",
format!(
"starforge wallet multisig submit {} --transaction tx.xdr --network {}",
account.name,
network
account.name, network
)
.cyan()
);
Expand Down Expand Up @@ -2150,7 +2149,11 @@ fn multisig_submit(name: String, transaction: PathBuf, network: Option<String>)
);
}

p::step(1, 1, &format!("Submitting signed envelope to Horizon ({})...", network));
p::step(
1,
1,
&format!("Submitting signed envelope to Horizon ({})...", network),
);
let result = horizon::submit_multisig_transaction(&signed_xdr, &network)?;

println!();
Expand Down
Loading
Loading