Skip to main content

palladium_cli/commands/
federation.rs

1use clap::{Args, Subcommand};
2use schemars::JsonSchema;
3use serde::Serialize;
4use serde_json::{json, Value};
5
6use crate::client::{ControlPlaneClient, Endpoint};
7use crate::CliResult;
8
9#[derive(Subcommand, Debug, Serialize, JsonSchema)]
10#[serde(rename_all = "kebab-case")]
11pub enum FederationCommand {
12    /// Manage federation policy.
13    #[command(subcommand)]
14    Policy(FederationPolicyCommand),
15    /// Inspect the federated registry.
16    #[command(subcommand)]
17    Registry(FederationRegistryCommand),
18}
19
20#[derive(Subcommand, Debug, Serialize, JsonSchema)]
21#[serde(rename_all = "kebab-case")]
22pub enum FederationPolicyCommand {
23    /// Show current federation policy.
24    Get(FederationPolicyGetArgs),
25    /// Update federation policy.
26    Set(FederationPolicySetArgs),
27}
28
29#[derive(Subcommand, Debug, Serialize, JsonSchema)]
30#[serde(rename_all = "kebab-case")]
31pub enum FederationRegistryCommand {
32    /// List registry entries.
33    List(FederationRegistryListArgs),
34    /// Get a registry entry by path.
35    Get(FederationRegistryGetArgs),
36}
37
38#[derive(Args, Debug, Serialize, JsonSchema)]
39#[serde(rename_all = "kebab-case")]
40pub struct FederationPolicyGetArgs {
41    /// Output in JSON format.
42    #[arg(long)]
43    pub json: bool,
44}
45
46#[derive(Args, Debug, Serialize, JsonSchema)]
47#[serde(rename_all = "kebab-case")]
48pub struct FederationPolicySetArgs {
49    /// Policy mode: allow-all, deny-all, allow-prefixes, deny-prefixes.
50    #[arg(long)]
51    pub mode: String,
52    /// Prefix paths (repeatable) for allow-prefixes/deny-prefixes.
53    #[arg(long = "prefix")]
54    pub prefixes: Vec<String>,
55    /// Output in JSON format.
56    #[arg(long)]
57    pub json: bool,
58}
59
60#[derive(Args, Debug, Serialize, JsonSchema)]
61#[serde(rename_all = "kebab-case")]
62pub struct FederationRegistryListArgs {
63    /// Optional path prefix filter.
64    #[arg(long)]
65    pub prefix: Option<String>,
66    /// Output in JSON format.
67    #[arg(long)]
68    pub json: bool,
69}
70
71#[derive(Args, Debug, Serialize, JsonSchema)]
72#[serde(rename_all = "kebab-case")]
73pub struct FederationRegistryGetArgs {
74    /// Actor path.
75    pub path: String,
76    /// Output in JSON format.
77    #[arg(long)]
78    pub json: bool,
79}
80
81pub fn run(cmd: FederationCommand, endpoint: &Endpoint) -> CliResult {
82    let mut client = ControlPlaneClient::connect_endpoint(endpoint)?;
83    match cmd {
84        FederationCommand::Policy(policy) => match policy {
85            FederationPolicyCommand::Get(args) => {
86                let result = client.call("federation.policy.get", Value::Null)?;
87                if args.json {
88                    println!("{}", serde_json::to_string_pretty(&result)?);
89                } else {
90                    println!("Federation policy: {}", format_policy_summary(&result));
91                }
92            }
93            FederationPolicyCommand::Set(args) => {
94                let params = json!({
95                    "mode": args.mode,
96                    "prefixes": args.prefixes,
97                });
98                let result = client.call("federation.policy.set", params)?;
99                if args.json {
100                    println!("{}", serde_json::to_string_pretty(&result)?);
101                } else {
102                    println!(
103                        "Federation policy updated: {}",
104                        format_policy_summary(&result)
105                    );
106                }
107            }
108        },
109        FederationCommand::Registry(registry) => match registry {
110            FederationRegistryCommand::List(args) => {
111                let mut params = serde_json::Map::new();
112                if let Some(prefix) = &args.prefix {
113                    params.insert("prefix".into(), Value::String(prefix.clone()));
114                }
115                let result = client.call("federation.registry.list", Value::Object(params))?;
116                let entries = result.as_array().cloned().unwrap_or_default();
117                if args.json {
118                    println!("{}", serde_json::to_string_pretty(&Value::Array(entries))?);
119                } else if entries.is_empty() {
120                    println!("No registry entries.");
121                } else {
122                    for entry in entries {
123                        let path = entry.get("path").and_then(|v| v.as_str()).unwrap_or("?");
124                        let engine = entry
125                            .get("engine_id")
126                            .and_then(|v| v.as_str())
127                            .unwrap_or("?");
128                        println!("{path} -> {engine}");
129                    }
130                }
131            }
132            FederationRegistryCommand::Get(args) => {
133                let result = client.call("federation.registry.get", json!({"path": args.path}))?;
134                if args.json {
135                    println!("{}", serde_json::to_string_pretty(&result)?);
136                } else {
137                    let path = result.get("path").and_then(|v| v.as_str()).unwrap_or("?");
138                    let engine = result
139                        .get("engine_id")
140                        .and_then(|v| v.as_str())
141                        .unwrap_or("?");
142                    println!("{path} -> {engine}");
143                }
144            }
145        },
146    }
147    Ok(())
148}
149
150fn format_policy_summary(result: &Value) -> String {
151    let mode = result
152        .get("mode")
153        .and_then(|v| v.as_str())
154        .unwrap_or("unknown");
155    let prefixes = result
156        .get("prefixes")
157        .and_then(|v| v.as_array())
158        .map(|p| {
159            p.iter()
160                .filter_map(|v| v.as_str())
161                .collect::<Vec<_>>()
162                .join(", ")
163        })
164        .unwrap_or_default();
165    if prefixes.is_empty() {
166        mode.to_string()
167    } else {
168        format!("{mode} [{prefixes}]")
169    }
170}