polyc-a2a 2026.8.3

polychrome A2A edge: serves a domain-signed Agent Card and drives message/send tasks onto a turn.
//! Deployment-configured A2A peer directory (`POLYCHROME_A2A_PEERS`).
//!
//! Two planes resolve peer names against this directory (`#760`):
//!
//! - the harness advertises the `peer_call` tool's `peer` enum from it, so the
//!   model can only ever name one of the deployment's configured peers — but
//!   NEVER dials with it; and
//! - the control plane independently re-resolves the named peer from its OWN
//!   copy of this directory before dialing — never trusting that the harness
//!   only ever offers a configured name, the same defence-in-depth stance the
//!   control plane already takes for every other mid-turn tool request it
//!   brokers.
//!
//! A peer is never a model-supplied URL — only a name the operator configured
//! — mirroring how `POLYCHROME_TOOL_SERVICES` scopes the MCP connector
//! surface elsewhere in this codebase.

use std::{collections::BTreeMap, sync::Arc};

/// Env var naming the deployment's callable A2A peers.
///
/// Whitespace-separated `name=base_url` entries, e.g.
/// `POLYCHROME_A2A_PEERS="finance=https://finance.example/ ops=https://ops.example/"`.
/// Mirrors the `POLYCHROME_TOOL_SERVICES` grammar (minus the `|token` suffix —
/// an A2A peer is authenticated by its domain-signed Agent Card, not a bearer
/// header).
pub const PEERS_ENV: &str = "POLYCHROME_A2A_PEERS";

/// The deployment's configured peer agents: a name → base-URL map resolved
/// once from [`PEERS_ENV`]. Immutable after construction, so cheap (an `Arc`)
/// to clone into each turn's proxy tool or dialer call.
#[derive(Debug, Clone, Default)]
pub struct PeerConfig {
    peers: Arc<BTreeMap<String, String>>,
}

impl PeerConfig {
    /// Resolves the configured peers from [`PEERS_ENV`].
    #[must_use]
    pub fn from_env() -> Self {
        Self::parse(&std::env::var(PEERS_ENV).unwrap_or_default())
    }

    /// Pure parser for the [`PEERS_ENV`] grammar, so it is unit-testable
    /// without touching the process environment. A malformed entry (missing
    /// `=`, empty name, or empty URL) is silently skipped rather than
    /// rejecting the whole list — an operator typo in one entry shouldn't take
    /// down every other configured peer.
    #[must_use]
    pub fn parse(raw: &str) -> Self {
        let peers = raw
            .split_whitespace()
            .filter_map(|entry| {
                let (name, url) = entry.split_once('=')?;
                (!name.is_empty() && !url.is_empty()).then(|| (name.to_owned(), url.to_owned()))
            })
            .collect();
        Self {
            peers: Arc::new(peers),
        }
    }

    /// Whether no peer is configured — the tool is not advertised at all.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.peers.is_empty()
    }

    /// The configured peer names, in a stable (sorted, `BTreeMap`) order —
    /// advertised as the tool schema's `peer` enum so prompt hashes stay
    /// reproducible.
    #[must_use]
    pub fn names(&self) -> Vec<String> {
        self.peers.keys().cloned().collect()
    }

    /// The configured base URL for `name`, or `None` when it names no
    /// configured peer. This is the ONLY place a peer name resolves to a
    /// dialable address — the control plane calls it independently of
    /// whatever the harness advertised.
    #[must_use]
    pub fn base_url(&self, name: &str) -> Option<&str> {
        self.peers.get(name).map(String::as_str)
    }
}

#[cfg(test)]
mod tests {
    #![allow(clippy::pedantic, clippy::nursery, missing_docs)]
    use super::*;

    #[test]
    fn parse_multiple_peers() {
        let cfg = PeerConfig::parse("finance=https://finance.example/ ops=https://ops.example/");
        assert_eq!(cfg.names(), vec!["finance".to_owned(), "ops".to_owned()]);
        assert_eq!(cfg.base_url("finance"), Some("https://finance.example/"));
        assert_eq!(cfg.base_url("ops"), Some("https://ops.example/"));
        assert!(!cfg.is_empty());
    }

    #[test]
    fn parse_skips_malformed_entries() {
        // No `=`, empty name, and empty URL are each dropped; a good entry
        // elsewhere in the list still parses.
        let cfg = PeerConfig::parse("bad-entry finance=https://finance.example/ =novalue nokey=");
        assert_eq!(cfg.names(), vec!["finance".to_owned()]);
    }

    #[test]
    fn empty_or_blank_env_yields_empty_config() {
        assert!(PeerConfig::parse("").is_empty());
        assert!(PeerConfig::parse("   ").is_empty());
        assert!(PeerConfig::default().is_empty());
    }

    #[test]
    fn unknown_name_resolves_to_none() {
        let cfg = PeerConfig::parse("finance=https://finance.example/");
        assert_eq!(cfg.base_url("ops"), None);
    }
}