Skip to content
Open
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
19 changes: 10 additions & 9 deletions PLUGIN_TRUST.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,21 @@ and the full plugin lifecycle.

Every installed plugin is assigned one of three trust levels at install time:

| Level | When assigned | StarForge behaviour |
|---|---|---|
| `local` | Plugin installed via `--path` (no source URL) | Always loaded without warnings |
| `trusted` | Source URL matches a known trusted prefix | Loaded without warnings |
| Level | When assigned | StarForge behaviour |
| --------- | --------------------------------------------- | ---------------------------------------------------- |
| `local` | Plugin installed via `--path` (no source URL) | Always loaded without warnings |
| `trusted` | Source URL matches a known trusted prefix | Loaded without warnings |
| `unknown` | Source URL provided but not in the allow-list | Warning shown on load; `--force` required to install |

### Trusted source prefixes
### Trusted sources

The following URL prefixes are automatically classified as `trusted`:
Trusted sources are defined in the StarForge configuration (`~/.starforge/config.toml`). By default, it includes:

- `https://github.com/Nanle-code/starforge-*`
- `https://github.com/StarForge-Labs/*`
- `https://crates.io/crates/starforge-plugin-*`

Any other source is `unknown`.
Any other source is `unknown` unless explicitly added to the `plugin_trust.trusted_sources` list in your configuration.

---

Expand All @@ -34,11 +34,11 @@ Plugins are native shared libraries loaded at runtime via `libloading`.
Two compatibility checks run before a plugin is executed:

1. **rustc ABI check** — the plugin must be compiled with the same Rust
toolchain version as StarForge. Mismatches cause undefined behaviour
toolchain version as StarForge. Mismatches cause undefined behaviour
and are rejected immediately.

2. **Core version check** — the plugin's declared `core_version` major
number must match the running StarForge major version. A plugin built
number must match the running StarForge major version. A plugin built
for StarForge `0.x.y` is incompatible with StarForge `1.x.y`.

Both checks produce actionable error messages that tell you exactly what
Expand Down Expand Up @@ -87,6 +87,7 @@ starforge plugin verify my-plugin # verify a specific plugin
```

Checks:

- Library file exists on disk at the registered path
- Trust level is `local` or `trusted`

Expand Down
143 changes: 138 additions & 5 deletions src/commands/config.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
use crate::utils::{config, crypto, print as p};
use crate::utils::{config, print as p};
use anyhow::Result;
use clap::Subcommand;

#[derive(Subcommand)]
pub enum ConfigCommands {
/// Show current global configuration
Show,
/// Set a scalar configuration value
Set {
/// Configuration key, e.g. telemetry.enabled or network
key: String,
/// New value
value: String,
},
/// Manage trusted plugin source allowlist
#[command(subcommand)]
PluginTrust(PluginTrustCommands),
/// Set global wallet encryption parameters (Argon2id)
SetEncryption {
/// Argon2 memory cost in KiB (e.g. 65536)
Expand All @@ -23,9 +33,29 @@ pub enum ConfigCommands {
},
}

#[derive(Subcommand)]
pub enum PluginTrustCommands {
/// List trusted plugin sources
List,
/// Add a trusted plugin domain or URL prefix
Add {
/// Domain or URL prefix to trust
source: String,
},
/// Remove a trusted plugin source
Remove {
/// Domain or URL prefix to remove
source: String,
},
/// Reset trusted plugin sources to StarForge defaults
Reset,
}

pub fn handle(cmd: ConfigCommands) -> Result<()> {
match cmd {
ConfigCommands::Show => show(),
ConfigCommands::Set { key, value } => set_value(&key, &value),
ConfigCommands::PluginTrust(cmd) => plugin_trust(cmd),
ConfigCommands::SetEncryption {
mem,
iterations,
Expand All @@ -42,7 +72,20 @@ fn show() -> Result<()> {

p::kv("Config file", &config::config_path().display().to_string());
p::kv("Active network", &cfg.network);
p::kv("Telemetry", if cfg.telemetry_enabled.unwrap_or(false) { "enabled" } else { "disabled" });
p::kv(
"telemetry.enabled",
&cfg.telemetry_enabled.unwrap_or(false).to_string(),
);

println!();
p::header("Plugin Trust");
if cfg.plugin_trust.trusted_sources.is_empty() {
p::warn("No trusted remote plugin sources configured.");
} else {
for source in &cfg.plugin_trust.trusted_sources {
p::kv("trusted source", source);
}
}

println!();
p::header("Wallet Encryption (Argon2id)");
Expand All @@ -61,6 +104,90 @@ fn show() -> Result<()> {
Ok(())
}

fn set_value(key: &str, value: &str) -> Result<()> {
let mut cfg = config::load()?;
match key {
"telemetry" | "telemetry.enabled" => {
cfg.telemetry_enabled = Some(parse_bool(value)?);
}
"network" => {
config::validate_network_exists(&cfg, value)?;
cfg.network = value.to_string();
}
_ => {
anyhow::bail!(
"Unsupported config key '{}'. Supported keys: telemetry.enabled, network",
key
);
}
}
config::save(&cfg)?;
p::success(&format!("{} set to '{}'", key, value));
Ok(())
}

fn parse_bool(value: &str) -> Result<bool> {
match value.to_ascii_lowercase().as_str() {
"true" | "1" | "yes" | "on" | "enabled" => Ok(true),
"false" | "0" | "no" | "off" | "disabled" => Ok(false),
_ => anyhow::bail!(
"Expected boolean value for telemetry.enabled, got '{}'",
value
),
}
}

fn plugin_trust(cmd: PluginTrustCommands) -> Result<()> {
match cmd {
PluginTrustCommands::List => {
let cfg = config::load()?;
print_plugin_trust_sources(&cfg);
}
PluginTrustCommands::Add { source } => {
let mut cfg = config::load()?;
let added = config::add_trusted_plugin_source(&mut cfg, source.clone())?;
config::save(&cfg)?;
if added {
p::success(&format!("Trusted plugin source added: {}", source.trim()));
} else {
p::info(&format!(
"Trusted plugin source already exists: {}",
source.trim()
));
}
print_plugin_trust_sources(&cfg);
}
PluginTrustCommands::Remove { source } => {
let mut cfg = config::load()?;
if !config::remove_trusted_plugin_source(&mut cfg, &source) {
anyhow::bail!("Trusted plugin source not found: {}", source.trim());
}
config::save(&cfg)?;
p::success(&format!("Trusted plugin source removed: {}", source.trim()));
print_plugin_trust_sources(&cfg);
}
PluginTrustCommands::Reset => {
let mut cfg = config::load()?;
config::reset_trusted_plugin_sources(&mut cfg);
config::save(&cfg)?;
p::success("Trusted plugin sources reset to defaults.");
print_plugin_trust_sources(&cfg);
}
}
Ok(())
}

fn print_plugin_trust_sources(cfg: &config::Config) {
p::header("Trusted Plugin Sources");
if cfg.plugin_trust.trusted_sources.is_empty() {
p::warn("No trusted remote plugin sources configured.");
return;
}
for source in &cfg.plugin_trust.trusted_sources {
p::info(&format!("- {}", source));
}
}

fn set_encryption(
mem: Option<u32>,
iterations: Option<u32>,
Expand All @@ -81,9 +208,15 @@ fn set_encryption(
}

let mut kdf = cfg.wallet_encryption.unwrap_or_default();
if let Some(m) = mem { kdf.mem = Some(m); }
if let Some(i) = iterations { kdf.iterations = Some(i); }
if let Some(p) = parallelism { kdf.parallelism = Some(p); }
if let Some(m) = mem {
kdf.mem = Some(m);
}
if let Some(i) = iterations {
kdf.iterations = Some(i);
}
if let Some(p) = parallelism {
kdf.parallelism = Some(p);
}

cfg.wallet_encryption = Some(kdf);
config::save(&cfg)?;
Expand Down
23 changes: 19 additions & 4 deletions src/commands/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ fn run_dry_run(
}
// Verify the bytes are non-empty and start with the WASM magic header.
if wasm_bytes.len() < 4 || &wasm_bytes[..4] != b"\0asm" {
warnings.push("File does not appear to be a valid WASM binary (missing magic header).".to_string());
warnings.push(
"File does not appear to be a valid WASM binary (missing magic header).".to_string(),
);
} else {
checks_passed += 1;
p::success(" Artifact is a valid WASM binary");
Expand Down Expand Up @@ -161,7 +163,10 @@ fn run_dry_run(
p::info("[ 4/4 ] Estimating Soroban fees via RPC simulation...");
match soroban::simulate_deploy_transaction(wasm_hash, network, wallet) {
Ok(simulation) => {
p::kv(" Estimated fee", &format!("{} stroops", simulation.fee));
p::kv(
" Estimated fee",
&format!("{} stroops", simulation.fee),
);
if !simulation.errors.is_empty() {
for error in &simulation.errors {
warnings.push(format!("RPC simulation warning: {}", error));
Expand All @@ -186,7 +191,10 @@ fn run_dry_run(
// ── Summary ───────────────────────────────────────────────────────────
p::separator();
p::header("Deployment Plan Summary");
p::kv("Checks passed", &format!("{}/{}", checks_passed, checks_total));
p::kv(
"Checks passed",
&format!("{}/{}", checks_passed, checks_total),
);
p::kv("Network", network);
p::kv("Wallet", &wallet.name);
p::kv("WASM", &wasm_path.display().to_string());
Expand Down Expand Up @@ -312,7 +320,14 @@ pub fn handle(args: DeployArgs) -> Result<()> {

// --dry-run: validate everything and print deployment plan, then exit.
if args.dry_run {
return run_dry_run(&wasm_path, &wasm_bytes, &wasm_hash, wasm_size_kb, wallet, &args.network);
return run_dry_run(
&wasm_path,
&wasm_bytes,
&wasm_hash,
wasm_size_kb,
wallet,
&args.network,
);
}

if args.simulate {
Expand Down
29 changes: 18 additions & 11 deletions src/commands/diagnostics.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use clap::Args;
use std::process::Command;
use anyhow::{Context, Result};
use clap::Args;
use colored::*;
use std::process::Command;

#[derive(Args, Debug)]
pub struct DiagnosticsArgs {
Expand All @@ -13,12 +13,13 @@ pub struct DiagnosticsArgs {
/// Handles the `starforge diagnostics` command by bridging execution
/// to the internal TypeScript/JavaScript hardware utility layer.
pub fn handle(args: DiagnosticsArgs) -> Result<()> {
println!("{}", "🔍 Checking system environment for Node.js runtime...".dimmed());
println!(
"{}",
"🔍 Checking system environment for Node.js runtime...".dimmed()
);

// 1. Verify Node.js is installed on the user's machine to run TS diagnostics
let node_check = Command::new("node")
.arg("-v")
.output();
let node_check = Command::new("node").arg("-v").output();

if node_check.is_err() {
anyhow::bail!(
Expand All @@ -31,16 +32,22 @@ pub fn handle(args: DiagnosticsArgs) -> Result<()> {
// 2. Prepare arguments to pass downstream to the TypeScript runner
// Assumes your built runner script is located in the distribution path or run via ts-node/bundler
let mut runner = Command::new("node");

// Path points to your project's diagnostics script executor
runner.arg("./dist/diagnostics/run.js");
runner.arg("./dist/diagnostics/run.js");

if let Some(wallet_type) = args.wallet {
runner.arg("--wallet").arg(wallet_type);
}

println!("{}", "🚀 Running hardware wallet connectivity utility...".cyan());
println!("{}\n", "--------------------------------------------------".dimmed());
println!(
"{}",
"🚀 Running hardware wallet connectivity utility...".cyan()
);
println!(
"{}\n",
"--------------------------------------------------".dimmed()
);

// 3. Execute the process and inherit standard output streams so colors/formatting are preserved
let status = runner
Expand All @@ -52,4 +59,4 @@ pub fn handle(args: DiagnosticsArgs) -> Result<()> {
}

Ok(())
}
}
5 changes: 4 additions & 1 deletion src/commands/invoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,10 @@ pub fn handle(args: InvokeArgs) -> Result<()> {
.add("Contract ID", &args.contract_id)
.add("Function", &args.function)
.add("Wallet", &args.wallet)
.add("Estimated Fee", &format!("{} stroops", outcome.simulation.fee))
.add(
"Estimated Fee",
&format!("{} stroops", outcome.simulation.fee),
)
.add("Return Value", &outcome.simulation.return_value);

// Add arguments to summary if present
Expand Down
2 changes: 1 addition & 1 deletion src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod completions;
pub mod config;
pub mod contract;
pub mod deploy;
pub mod diagnostics;
pub mod gas;
pub mod info;
pub mod inspect;
Expand All @@ -22,4 +23,3 @@ pub mod tutorial;
pub mod tx;
pub mod upgrade;
pub mod wallet;
pub mod diagnostics;
5 changes: 4 additions & 1 deletion src/commands/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,9 @@ fn rename_network(old_name: String, new_name: String) -> Result<()> {
config::rename_custom_network(&mut cfg, &old_name, &new_name)?;
config::save(&cfg)?;

p::success(&format!("Network renamed from '{}' to '{}'", old_name, new_name));
p::success(&format!(
"Network renamed from '{}' to '{}'",
old_name, new_name
));
Ok(())
}
Loading