use std::fs;
use std::path::Path;
use std::process::ExitCode;
pub const DEFAULT_ORCHESTRATOR_URL: &str = "https://api.peeramid.xyz";
pub const DEFAULT_ROOM: &str = "demo";
pub const DEFAULT_TOKEN_ENV: &str = "QUORUM_DEMO_TOKEN";
pub fn render_workspace_yaml(
orchestrator_url: &str,
room: &str,
token_env: &str,
agents: &[String],
) -> String {
let agents_yaml = if agents.is_empty() {
String::from(
" # Edit: list the agent names this room dispatches to, e.g.\n # agents: [\"CortexA\", \"CortexB\"]\n agents: []\n",
)
} else {
let quoted: Vec<String> = agents.iter().map(|a| format!("\"{a}\"")).collect();
format!(" agents: [{}]\n", quoted.join(", "))
};
format!(
r#"# =============================================================================
# quorum workspace — generated by `quorum init`
# =============================================================================
#
# This is a minimal workspace pointing at one remote orchestrator and
# one room. Edit freely — the file format matches the schema documented
# at https://docs.rs/quorum-cli (WorkspaceConfig).
#
# To run a deliberation:
# export {token_env}=<your bearer token>
# quorum run --room {room} "<your topic>"
orchestrators:
primary:
mode: remote
address: "{orchestrator_url}"
# Bearer token resolves from the env var at runtime. Never commit
# the raw secret here.
token: "${{{token_env}}}"
policies:
default:
{agents_yaml} max_rounds: 3
effort: 0.7
rooms:
{room}:
policy: default
orchestrator: primary
default_room: {room}
"#,
)
}
pub fn run(
target: &Path,
orchestrator_url: &str,
room: &str,
token_env: &str,
agents: &[String],
force: bool,
) -> ExitCode {
if target.exists() && !force {
eprintln!(
"error: {} already exists. Re-run with --force to overwrite.",
target.display()
);
return ExitCode::FAILURE;
}
let body = render_workspace_yaml(orchestrator_url, room, token_env, agents);
if let Some(parent) = target.parent() {
if !parent.as_os_str().is_empty() {
if let Err(e) = fs::create_dir_all(parent) {
eprintln!("error: failed to create parent directory {parent:?}: {e}");
return ExitCode::FAILURE;
}
}
}
if let Err(e) = fs::write(target, &body) {
eprintln!("error: failed to write {}: {e}", target.display());
return ExitCode::FAILURE;
}
println!(
"Wrote workspace config to {}\n\nNext steps:\n export {}=<your bearer token>\n quorum run --room {} \"<your topic>\"",
target.display(),
token_env,
room,
);
ExitCode::SUCCESS
}
fn is_valid_agent_name(name: &str) -> bool {
!name.is_empty()
&& name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
}
fn render_fleet_yaml(agents: &[String]) -> String {
let agent_names: Vec<String> = if agents.is_empty() {
vec!["cortex-a".to_string()]
} else {
agents.iter().map(|s| s.to_string()).collect()
};
let mut agent_entries = String::new();
for name in &agent_names {
agent_entries.push_str(&format!(
" - name: {name}\n provider_id: openai\n model_name: gpt-4o-mini\n",
));
}
format!(
r#"# =============================================================================
# quorum agent fleet — generated by `quorum init --agent-fleet`
# =============================================================================
#
# This file configures the AGENT PROCESS that `quorum serve` runs.
# Each entry under `agents:` becomes one worker connected to the
# orchestrator's NATS bus.
#
# Distinct from `nsed.yaml` (which configures task SUBMISSION via
# `quorum run`/`status`/`trace`/`tui`).
#
# Run:
# quorum serve --nats-url <url-from-quorum-redeem>
#
# By default `quorum serve` reads `./agent.yml` + `~/.nsed/agent.creds`
# from `quorum redeem`. Override with --config / --nats-creds.
providers:
# ── OpenAI-compatible LLMs ───────────────────────────────────────────
# Default option. Works for OpenAI itself, plus any provider that
# speaks the OpenAI wire format (Groq, DeepSeek, Together,
# local llama.cpp, etc.) — just change `base_url`.
openai:
type: openai
base_url: "https://api.openai.com/v1"
api_key: "${{OPENAI_API_KEY}}" # resolved from your shell env at runtime
# ── Claude CLI ───────────────────────────────────────────────────────
# Uncomment if you have `claude` on $PATH and want the CLI to be the
# agent runtime. No api_key here — Claude CLI handles its own auth.
# claude_cli:
# type: claude
# ── Subprocess agent (any language) via the exec protocol ────────────
# Uncomment to drive an agent from Python/TypeScript/etc. via
# stdin/stdout framing. See docs/reference/exec-agent-protocol.md.
# exec_local:
# type: exec
# ── Subprocess agent via Model Context Protocol ──────────────────────
# Uncomment for an MCP-server-backed agent (Claude Code, generic MCP).
# See docs/reference/mcp-agent-protocol.md.
# mcp_local:
# type: mcp
agents:
{agent_entries}"#,
)
}
pub fn run_agent_fleet(target: &Path, agents: &[String], force: bool) -> ExitCode {
for name in agents {
if !is_valid_agent_name(name) {
eprintln!(
"error: invalid agent name {name:?}. Allowed characters: ASCII letters, digits, `-`, `_`, `.` (and non-empty)."
);
return ExitCode::FAILURE;
}
}
if target.exists() && !force {
eprintln!(
"error: {} already exists. Re-run with --force to overwrite.",
target.display()
);
return ExitCode::FAILURE;
}
let body = render_fleet_yaml(agents);
if let Some(parent) = target.parent()
&& !parent.as_os_str().is_empty()
&& let Err(e) = fs::create_dir_all(parent)
{
eprintln!("error: failed to create parent directory {parent:?}: {e}");
return ExitCode::FAILURE;
}
if let Err(e) = fs::write(target, &body) {
eprintln!("error: failed to write {}: {e}", target.display());
return ExitCode::FAILURE;
}
println!(
"Wrote agent fleet config to {}\n\n\
Next steps:\n\
\x20 1. Edit {} — pick a provider section (only the first is active),\n\
\x20 set the api_key env var (or remove it if your provider doesn't need one).\n\
\x20 2. Make sure ~/.nsed/agent.creds exists (run `quorum redeem <invite>` first).\n\
\x20 3. Run: quorum serve --nats-url <url-from-quorum-redeem>",
target.display(),
target.display(),
);
ExitCode::SUCCESS
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
fn agents(names: &[&str]) -> Vec<String> {
names.iter().map(|s| s.to_string()).collect()
}
#[test]
fn renders_with_provided_inputs() {
let yaml = render_workspace_yaml("https://example.test", "myroom", "MY_TOKEN", &[]);
assert!(yaml.contains(r#"address: "https://example.test""#));
assert!(yaml.contains("token: \"${MY_TOKEN}\""));
assert!(yaml.contains("\n myroom:\n policy: default"));
assert!(yaml.contains("default_room: myroom"));
}
#[test]
fn renders_with_defaults() {
let yaml = render_workspace_yaml(
DEFAULT_ORCHESTRATOR_URL,
DEFAULT_ROOM,
DEFAULT_TOKEN_ENV,
&[],
);
assert!(yaml.contains("https://api.peeramid.xyz"));
assert!(yaml.contains("${QUORUM_DEMO_TOKEN}"));
assert!(yaml.contains("\n demo:\n policy: default"));
assert!(yaml.contains("default_room: demo"));
}
#[test]
fn rendered_output_parses_as_yaml() {
let yaml = render_workspace_yaml(
DEFAULT_ORCHESTRATOR_URL,
DEFAULT_ROOM,
DEFAULT_TOKEN_ENV,
&agents(&["CortexA", "CortexB"]),
);
let parsed: serde_yaml::Value =
serde_yaml::from_str(&yaml).expect("rendered yaml must round-trip through serde");
assert!(parsed["orchestrators"]["primary"]["address"].is_string());
assert_eq!(parsed["default_room"].as_str(), Some("demo"));
let agents_node = &parsed["policies"]["default"]["agents"];
assert!(agents_node.is_sequence(), "agents must serialise as a list");
let names: Vec<&str> = agents_node
.as_sequence()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect();
assert_eq!(names, vec!["CortexA", "CortexB"]);
}
#[test]
fn renders_with_named_agents() {
let yaml =
render_workspace_yaml("https://example.test", "demo", "TOK", &agents(&["A", "B"]));
assert!(
yaml.contains(r#"agents: ["A", "B"]"#),
"agents list must appear in the rendered policy"
);
}
#[test]
fn empty_agents_emits_editable_placeholder() {
let yaml = render_workspace_yaml("https://example.test", "demo", "TOK", &[]);
assert!(
yaml.contains("agents: []"),
"empty agents must still emit a parseable agents: [] line so the file round-trips through serde"
);
assert!(
yaml.contains("Edit: list the agent names"),
"empty agents must include an inline edit hint"
);
}
#[test]
fn run_writes_file_and_succeeds_when_target_absent() {
let dir = tempdir().unwrap();
let target = dir.path().join("nsed.yaml");
let exit = run(
&target,
DEFAULT_ORCHESTRATOR_URL,
DEFAULT_ROOM,
DEFAULT_TOKEN_ENV,
&[],
false,
);
assert_eq!(exit, ExitCode::SUCCESS);
let body = fs::read_to_string(&target).unwrap();
assert!(body.contains("https://api.peeramid.xyz"));
}
#[test]
fn run_refuses_to_overwrite_without_force() {
let dir = tempdir().unwrap();
let target = dir.path().join("nsed.yaml");
fs::write(&target, "existing content").unwrap();
let exit = run(
&target,
DEFAULT_ORCHESTRATOR_URL,
DEFAULT_ROOM,
DEFAULT_TOKEN_ENV,
&[],
false,
);
assert_eq!(exit, ExitCode::FAILURE);
let body = fs::read_to_string(&target).unwrap();
assert_eq!(body, "existing content", "file must NOT be overwritten");
}
#[test]
fn run_overwrites_with_force() {
let dir = tempdir().unwrap();
let target = dir.path().join("nsed.yaml");
fs::write(&target, "existing content").unwrap();
let exit = run(
&target,
DEFAULT_ORCHESTRATOR_URL,
DEFAULT_ROOM,
DEFAULT_TOKEN_ENV,
&[],
true,
);
assert_eq!(exit, ExitCode::SUCCESS);
let body = fs::read_to_string(&target).unwrap();
assert!(body.contains("https://api.peeramid.xyz"));
assert!(!body.contains("existing content"));
}
#[test]
fn run_creates_parent_directory_if_missing() {
let dir = tempdir().unwrap();
let target = dir.path().join("nested/path/nsed.yaml");
let exit = run(
&target,
DEFAULT_ORCHESTRATOR_URL,
DEFAULT_ROOM,
DEFAULT_TOKEN_ENV,
&[],
false,
);
assert_eq!(exit, ExitCode::SUCCESS);
assert!(target.exists());
}
#[test]
fn rendered_output_uses_provided_token_env() {
let yaml = render_workspace_yaml("https://example.test", "demo", "CUSTOM_TOK", &[]);
assert!(yaml.contains("${CUSTOM_TOK}"));
assert!(!yaml.contains("${QUORUM_DEMO_TOKEN}"));
}
#[test]
fn fleet_yaml_includes_active_openai_provider() {
let yaml = render_fleet_yaml(&[]);
assert!(yaml.contains("providers:"));
assert!(yaml.contains("openai:"));
assert!(
yaml.contains("api_key: \"${OPENAI_API_KEY}\""),
"default template references env-var; operator overrides"
);
assert!(
yaml.contains("# claude_cli:"),
"claude provider must ship commented"
);
assert!(
yaml.contains("# exec_local:"),
"exec provider must ship commented"
);
assert!(
yaml.contains("# mcp_local:"),
"mcp provider must ship commented"
);
}
#[test]
fn fleet_yaml_defaults_to_one_agent_when_none_provided() {
let yaml = render_fleet_yaml(&[]);
assert!(yaml.contains("- name: cortex-a"));
assert!(yaml.contains("model_name: gpt-4o-mini"));
}
#[test]
fn fleet_yaml_emits_one_entry_per_named_agent() {
let names = agents(&["one", "two", "three"]);
let yaml = render_fleet_yaml(&names);
for name in &names {
assert!(
yaml.contains(&format!("- name: {name}")),
"expected entry for {name}, got:\n{yaml}"
);
}
}
#[test]
fn fleet_yaml_parses_as_valid_yaml() {
let yaml = render_fleet_yaml(&agents(&["cortex-a", "cortex-b"]));
let parsed: serde_yaml::Value =
serde_yaml::from_str(&yaml).expect("fleet yaml must round-trip via serde_yaml");
assert!(parsed["providers"]["openai"]["type"].is_string());
assert_eq!(
parsed["providers"]["openai"]["type"].as_str(),
Some("openai")
);
let agents_seq = parsed["agents"].as_sequence().expect("agents is a list");
assert_eq!(agents_seq.len(), 2);
assert_eq!(agents_seq[0]["name"].as_str(), Some("cortex-a"));
}
#[test]
fn run_agent_fleet_writes_file() {
let dir = tempdir().unwrap();
let target = dir.path().join("agent.yml");
let exit = run_agent_fleet(&target, &agents(&["cortex-a"]), false);
assert_eq!(exit, ExitCode::SUCCESS);
assert!(target.exists());
let body = std::fs::read_to_string(&target).unwrap();
assert!(body.contains("providers:"));
assert!(body.contains("- name: cortex-a"));
}
#[test]
fn run_agent_fleet_refuses_to_overwrite_without_force() {
let dir = tempdir().unwrap();
let target = dir.path().join("agent.yml");
std::fs::write(&target, "stale").unwrap();
let exit = run_agent_fleet(&target, &[], false);
assert_eq!(exit, ExitCode::FAILURE);
assert_eq!(std::fs::read_to_string(&target).unwrap(), "stale");
}
#[test]
fn run_agent_fleet_overwrites_with_force() {
let dir = tempdir().unwrap();
let target = dir.path().join("agent.yml");
std::fs::write(&target, "stale").unwrap();
let exit = run_agent_fleet(&target, &[], true);
assert_eq!(exit, ExitCode::SUCCESS);
let body = std::fs::read_to_string(&target).unwrap();
assert!(!body.contains("stale"));
assert!(body.contains("providers:"));
}
#[test]
fn run_agent_fleet_creates_parent_directory() {
let dir = tempdir().unwrap();
let target = dir.path().join("nested/agent.yml");
let exit = run_agent_fleet(&target, &[], false);
assert_eq!(exit, ExitCode::SUCCESS);
assert!(target.exists());
}
#[test]
fn is_valid_agent_name_accepts_documented_alphabet() {
for ok in ["cortex-a", "Cortex_B", "agent.v1", "a", "0", "A1_b.2-x"] {
assert!(is_valid_agent_name(ok), "expected {ok:?} valid");
}
}
#[test]
fn is_valid_agent_name_rejects_yaml_breakers() {
for bad in [
"",
"foo: bar",
"name#with hash",
"has\"quote",
"has'quote",
"line\nbreak",
"tab\there",
"back\\slash",
"space here",
"uni¢ode",
] {
assert!(!is_valid_agent_name(bad), "expected {bad:?} invalid");
}
}
#[test]
fn run_agent_fleet_rejects_invalid_name_and_does_not_write_file() {
let dir = tempdir().unwrap();
let target = dir.path().join("agent.yml");
let exit = run_agent_fleet(&target, &agents(&["bad: name"]), false);
assert_eq!(exit, ExitCode::FAILURE);
assert!(
!target.exists(),
"must not leave a broken agent.yml on disk when validation rejects"
);
}
}