//! `/api/egress/allowed-domains` and `/api/egress/denied-domains` — the destinations the swarm lets //! agents reach through their hive's egress proxy, and the ones it refuses. use serde::{Deserialize, Serialize}; /// One allowed destination and who may reach it. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "schema", derive(utoipa::ToSchema))] #[serde(deny_unknown_fields)] pub struct AllowedDomain { /// A DNS name of at least two labels, such as `github.com`, stored /// lower-cased. One leading dot, as in `.githubusercontent.com`, also /// allows every subdomain. IP addresses, wildcards, ports, paths and the /// proxy's own always-allowed names are refused. #[cfg_attr(feature = "schema", schema(example = "github.com"))] pub domain: String, /// The agent this entry is for. Absent means every agent in the swarm. #[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "schema", schema(example = "atlas"))] pub agent: Option, /// Set on an entry the controller keeps from the agent's backend /// environment (the host of its `ANTHROPIC_BASE_URL`) or from an account an /// operator linked to it. The list shows it as automatic; adding or /// removing one is refused. #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub derived: bool, } /// The swarm's allow-list: every entry, swarm-wide and per agent, sorted. #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "schema", derive(utoipa::ToSchema))] pub struct AllowedDomains { pub entries: Vec, } /// One refused destination and who it is refused for. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "schema", derive(utoipa::ToSchema))] #[serde(deny_unknown_fields)] pub struct DeniedDomain { /// A DNS name written as in [`AllowedDomain::domain`]: at least two /// labels, lower-cased, one leading dot for the name and its subdomains. #[cfg_attr(feature = "schema", schema(example = ".example.com"))] pub domain: String, /// The agent this entry is for. Absent means every agent in the swarm. An /// agent's entry adds to the swarm-wide ones and never lifts one. #[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "schema", schema(example = "atlas"))] pub agent: Option, } /// The swarm's deny list: every entry, swarm-wide and per agent, sorted, and /// whether every hive has taken it. #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "schema", derive(utoipa::ToSchema))] pub struct DeniedDomains { pub entries: Vec, /// The revision of the stored list, `0` when nobody has written one. A /// hive reports the revision it last rendered into its proxy. #[serde(default)] pub revision: u64, /// The hives whose proxy is not known to refuse these names yet: they have /// not reported the list's revision (a hive that predates the deny list /// never does, a new one takes a minute or so), or have not reported at /// all. Empty when the list is empty and no hive reports otherwise. #[serde(default)] pub behind: Vec, } #[cfg(test)] mod tests { use super::*; #[test] fn an_entry_leaves_out_an_unset_agent_and_refuses_unknown_fields() { let swarm_wide = AllowedDomain { domain: "github.com".to_owned(), agent: None, derived: false, }; let wire = r#"{"domain":"github.com"}"#; assert_eq!(serde_json::to_string(&swarm_wide).unwrap(), wire); assert_eq!( serde_json::from_str::(wire).unwrap(), swarm_wide ); let scoped = r#"{"domain":"crates.io","agent":"atlas"}"#; assert_eq!( serde_json::from_str::(scoped) .unwrap() .agent .as_deref(), Some("atlas") ); assert!(serde_json::from_str::(r#"{"domain":"a.io","hive":"x"}"#).is_err()); } #[test] fn a_denied_entry_has_no_derived_flag_and_refuses_unknown_fields() { let swarm_wide = DeniedDomain { domain: ".example.com".to_owned(), agent: None, }; let wire = r#"{"domain":".example.com"}"#; assert_eq!(serde_json::to_string(&swarm_wide).unwrap(), wire); assert_eq!( serde_json::from_str::(wire).unwrap(), swarm_wide ); // A deny is never automatic, so the allow list's `derived` is not a field. assert!( serde_json::from_str::(r#"{"domain":"a.io","derived":true}"#).is_err() ); assert!(serde_json::from_str::(r#"{"domain":"a.io","hive":"x"}"#).is_err()); } }