use std::{net::SocketAddr, path::PathBuf};
use clap::Parser;
use figment::{
Figment,
providers::{Env, Format, Serialized, Toml},
};
use polyc_rpc_client::Sensitive;
use serde::{Deserialize, Serialize};
#[derive(Debug, Parser)]
#[command(name = "polychrome-a2a", version, about = "polychrome A2A edge")]
pub struct Cli {
#[arg(long)]
pub config: Option<PathBuf>,
#[arg(long)]
pub bind: Option<SocketAddr>,
#[arg(long)]
pub side_addr: Option<SocketAddr>,
#[arg(long)]
pub agent_addr: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub bind: SocketAddr,
pub side_addr: SocketAddr,
pub agent_addr: String,
pub agent_name: String,
pub agent_description: String,
pub public_url: String,
#[serde(default = "default_signing_seed", skip_serializing)]
pub signing_seed: Sensitive<u64>,
pub max_concurrent_turns: usize,
#[serde(default = "empty_peer_credentials", skip_serializing)]
pub peer_credentials: Sensitive<String>,
#[serde(default)]
pub edge_id: Option<String>,
#[serde(default, skip_serializing)]
pub edge_bearer_key: Option<Sensitive<String>>,
#[serde(default, skip_serializing)]
pub edge_signing_key_hex: Option<Sensitive<String>>,
}
const fn empty_peer_credentials() -> Sensitive<String> {
Sensitive::new(String::new())
}
const fn default_signing_seed() -> Sensitive<u64> {
Sensitive::new(1)
}
impl Default for Config {
fn default() -> Self {
Self {
bind: SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 0], 8080)),
side_addr: SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 0], 9090)),
agent_addr: String::new(),
agent_name: "Polychrome".to_owned(),
agent_description: "A polychrome agent reachable over A2A.".to_owned(),
public_url: "http://localhost:8080/".to_owned(),
signing_seed: default_signing_seed(),
max_concurrent_turns: 64,
peer_credentials: empty_peer_credentials(),
edge_id: None,
edge_bearer_key: None,
edge_signing_key_hex: None,
}
}
}
pub fn load(cli: &Cli) -> Result<Config, Box<figment::Error>> {
let mut fig = Figment::from(Serialized::defaults(Config::default()));
if let Some(path) = &cli.config {
fig = fig.merge(Toml::file(path));
}
fig = fig.merge(Env::prefixed("POLYCHROME_A2A_"));
fig = fig.merge(Env::prefixed("POLYCHROME_").only(&[
"AGENT_ADDR",
"EDGE_ID",
"EDGE_BEARER_KEY",
"EDGE_SIGNING_KEY_HEX",
]));
if let Some(addr) = cli.bind {
fig = fig.merge(Serialized::default("bind", addr));
}
if let Some(addr) = cli.side_addr {
fig = fig.merge(Serialized::default("side_addr", addr));
}
if let Some(addr) = &cli.agent_addr {
fig = fig.merge(Serialized::default("agent_addr", addr));
}
fig.extract().map_err(Box::new)
}
#[cfg(test)]
#[allow(
clippy::pedantic,
clippy::nursery,
clippy::result_large_err,
missing_docs
)]
mod tests {
use super::*;
fn cli() -> Cli {
Cli {
config: None,
bind: None,
side_addr: None,
agent_addr: None,
}
}
#[test]
fn defaults_apply_when_env_empty() {
figment::Jail::expect_with(|_jail| {
let cfg = load(&cli()).map_err(|e| *e)?;
assert_eq!(cfg.bind, SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 0], 8080)));
assert!(cfg.agent_addr.is_empty());
assert_eq!(cfg.agent_name, "Polychrome");
assert_eq!(*cfg.signing_seed.expose(), 1);
assert!(cfg.peer_credentials.expose().is_empty());
Ok(())
});
}
#[test]
fn env_overrides_win() {
figment::Jail::expect_with(|jail| {
jail.set_env("POLYCHROME_A2A_BIND", "127.0.0.1:7100");
jail.set_env("POLYCHROME_A2A_PUBLIC_URL", "https://agent.example/");
jail.set_env("POLYCHROME_A2A_AGENT_NAME", "Edge One");
jail.set_env("POLYCHROME_A2A_SIGNING_SEED", "42");
jail.set_env("POLYCHROME_A2A_PEER_CREDENTIALS", "weather=s3cr3t");
jail.set_env("POLYCHROME_AGENT_ADDR", "http://agent:8080");
let cfg = load(&cli()).map_err(|e| *e)?;
assert_eq!(cfg.bind, SocketAddr::from(([127, 0, 0, 1], 7100)));
assert_eq!(cfg.public_url, "https://agent.example/");
assert_eq!(cfg.agent_name, "Edge One");
assert_eq!(*cfg.signing_seed.expose(), 42);
assert_eq!(cfg.agent_addr, "http://agent:8080");
assert_eq!(cfg.peer_credentials.expose(), &"weather=s3cr3t".to_owned());
Ok(())
});
}
#[test]
fn shared_edge_credential_env_names_load_unprefixed() {
figment::Jail::expect_with(|jail| {
jail.set_env("POLYCHROME_EDGE_ID", "a2a");
jail.set_env("POLYCHROME_EDGE_BEARER_KEY", "pc_a2a_secret");
jail.set_env("POLYCHROME_EDGE_SIGNING_KEY_HEX", "ab12");
let cfg = load(&cli()).map_err(|e| *e)?;
assert_eq!(cfg.edge_id.as_deref(), Some("a2a"));
assert_eq!(
cfg.edge_bearer_key.as_ref().map(Sensitive::expose),
Some(&"pc_a2a_secret".to_owned())
);
assert_eq!(
cfg.edge_signing_key_hex.as_ref().map(Sensitive::expose),
Some(&"ab12".to_owned())
);
Ok(())
});
}
#[test]
fn cli_flag_beats_env_for_bind() {
figment::Jail::expect_with(|jail| {
jail.set_env("POLYCHROME_A2A_BIND", "127.0.0.1:7100");
let cli = Cli {
config: None,
bind: Some(SocketAddr::from(([10, 0, 0, 1], 9999))),
side_addr: None,
agent_addr: None,
};
let cfg = load(&cli).map_err(|e| *e)?;
assert_eq!(cfg.bind, SocketAddr::from(([10, 0, 0, 1], 9999)));
Ok(())
});
}
#[test]
fn config_debug_never_prints_peer_credentials() {
let raw_token = "weather=s3cr3t-bearer-value";
let raw_seed = 424_242_424_242_u64;
let cfg = Config {
peer_credentials: Sensitive::new(raw_token.to_owned()),
signing_seed: Sensitive::new(raw_seed),
..Config::default()
};
polyc_rpc_client::assert_redacted(&cfg, &[raw_token, &raw_seed.to_string()]);
}
}