Files
buzz/crates/buzz-swarm/src/cli.rs
Eva fed802fa7d fix(swarm): route declared env sources and default private workdirs
A {env: VAR} source reached every harness, not only the agents that
declare it: sibling processes inherited a per-agent credential from the
parent environment. Remove every declared source (agent env, auth_tag,
and raw defaults, including disabled agents) from the inherited
environment; declarers still receive the value under the declared name.

Agents defaulted to the config directory as workdir, which is where
generated keys/ live, so each shell-capable agent started beside its
siblings' private keys. Default to workspaces/<name> beside the config,
created 0700 on start and reported by validate. The default now lives
only in the plan.

README: state exactly what shutdown cleans up. Swarm kills the harness's
own process group; buzz-acp moves agents into their own groups and stops
them on SIGTERM, so a forced shutdown or harness crash relies on host
containment.

Also build Secret directly and format executable().

Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
2026-09-23 14:09:29 -04:00

106 lines
3.4 KiB
Rust

use std::os::unix::fs::DirBuilderExt;
use std::path::PathBuf;
use std::process::ExitCode;
use std::time::Duration;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use crate::config::{expand_tilde, SwarmFile, DEFAULT_CONFIG_FILE};
use crate::env::Env;
use crate::plan::Plan;
use crate::supervisor::{self, Options};
#[derive(Parser)]
#[command(version, about = "Run Buzz agent identities on any number of relays")]
struct Cli {
/// YAML swarm file. Paths inside it are relative to its directory.
#[arg(short, long, default_value = DEFAULT_CONFIG_FILE, global = true)]
config: PathBuf,
/// Run only these agent names.
#[arg(long, value_delimiter = ',', global = true)]
only: Vec<String>,
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Subcommand)]
enum Command {
/// Start and supervise the swarm (default).
Start {
/// Grace period in seconds before forcibly stopping harness groups.
#[arg(long, default_value_t = 15)]
grace: u64,
},
/// Check swarm wiring and identities; write no keys and start no processes.
/// Runtime-specific settings are validated by the runtime when it starts.
Validate,
}
#[tokio::main]
pub async fn main() -> ExitCode {
match run(Cli::parse()).await {
Ok(failed) if !failed => ExitCode::SUCCESS,
Ok(_) => ExitCode::FAILURE,
Err(error) => {
eprintln!("buzz-swarm: {error:#}");
ExitCode::FAILURE
}
}
}
async fn run(cli: Cli) -> Result<bool> {
let env = Env::from_process();
let file = SwarmFile::load(&expand_tilde(&cli.config, &env))?;
let plan = Plan::build(&file, &cli.only, &env)?;
match cli.command.unwrap_or(Command::Start { grace: 15 }) {
Command::Validate => {
println!("owner {}", plan.owner_pubkey);
for agent in &plan.agents {
println!(
"{} · relay {} · identity {} · {} · workdir {}",
agent.name,
agent.relay_url,
agent.pubkey,
agent.program.display(),
agent.workdir.display()
);
}
for key in &plan.generated_keys {
println!(
"{}: created on start (preview identity is temporary)",
key.path.display()
);
}
for workdir in &plan.new_workdirs {
println!("{}: created on start", workdir.display());
}
Ok(false)
}
Command::Start { grace } => {
// Validate the whole file before creating keys or starting anything.
for key in &plan.generated_keys {
key.persist()?;
}
for workdir in &plan.new_workdirs {
std::fs::DirBuilder::new()
.recursive(true)
.mode(0o700)
.create(workdir)
.with_context(|| format!("creating workdir {}", workdir.display()))?;
}
let summary = supervisor::run(
plan,
Options {
grace: Duration::from_secs(grace),
},
)
.await?;
for (name, outcome) in &summary.outcomes {
eprintln!("{name}: {outcome:?}");
}
Ok(summary.failed())
}
}
}