1use std::path::PathBuf;
9
10use anyhow::{Context, Result};
11use async_trait::async_trait;
12use base64::Engine;
13use clap::{Args, Subcommand};
14use serde::Serialize;
15
16use super::discovery::DiscoveryCommand;
17use super::fingerprint::FingerprintCommand;
18use crate::core::{Command, CommandContext, CommandResult, ComponentType};
19
20#[derive(Args, Debug)]
21pub struct RegistryArgs {
22 #[command(subcommand)]
23 pub command: RegistryCommand,
24}
25
26#[derive(Subcommand, Debug)]
27pub enum RegistryCommand {
28 Discover(DiscoveryCommand),
30 Publish(RegistryPublishArgs),
32 Fingerprint(FingerprintCommand),
34}
35
36#[async_trait]
37impl Command for RegistryArgs {
38 async fn execute(&self, ctx: &CommandContext) -> Result<CommandResult> {
39 match &self.command {
40 RegistryCommand::Discover(cmd) => {
41 let command = DiscoveryCommand::from_args(cmd);
42 {
43 let container = ctx.container.lock().unwrap();
44 container.validate(&command.required_components())?;
45 }
46 command.execute(ctx).await
47 }
48 RegistryCommand::Fingerprint(cmd) => cmd.execute(ctx).await,
49 RegistryCommand::Publish(args) => {
50 execute_publish(args).await?;
51 Ok(CommandResult::Success(String::new()))
52 }
53 }
54 }
55
56 fn required_components(&self) -> Vec<ComponentType> {
57 match &self.command {
58 RegistryCommand::Discover(_) => vec![
59 ComponentType::ServiceDiscovery,
60 ComponentType::UserInterface,
61 ComponentType::ConfigManager,
62 ComponentType::DependencyResolver,
63 ComponentType::NetworkValidator,
64 ComponentType::FingerprintValidator,
65 ],
66 RegistryCommand::Fingerprint(_) | RegistryCommand::Publish(_) => vec![],
67 }
68 }
69
70 fn name(&self) -> &str {
71 "registry"
72 }
73
74 fn description(&self) -> &str {
75 "Interact with the remote service registry (discover, publish, fingerprint)"
76 }
77}
78
79#[derive(Args, Debug)]
82pub struct RegistryPublishArgs {
83 #[arg(long, short = 'p', value_name = "FILE")]
85 pub package: PathBuf,
86
87 #[arg(long, short = 'k', value_name = "FILE")]
89 pub keychain: PathBuf,
90
91 #[arg(long, short = 'e', value_name = "URL")]
93 pub endpoint: String,
94}
95
96#[derive(Serialize)]
97struct SignablePublishBody<'a> {
98 manufacturer: &'a str,
99 name: &'a str,
100 version: &'a str,
101 target: &'a str,
102 manifest: &'a str,
103 signature: &'a str,
104 #[serde(skip_serializing_if = "Option::is_none")]
105 proto_files: Option<&'a serde_json::Value>,
106 nonce: &'a str,
107}
108
109#[derive(Serialize)]
110struct FinalPublishBody<'a> {
111 manufacturer: &'a str,
112 name: &'a str,
113 version: &'a str,
114 target: &'a str,
115 manifest: &'a str,
116 signature: &'a str,
117 #[serde(skip_serializing_if = "Option::is_none")]
118 proto_files: Option<&'a serde_json::Value>,
119 nonce: &'a str,
120 nonce_sig: &'a str,
121}
122
123async fn execute_publish(args: &RegistryPublishArgs) -> Result<()> {
124 tracing::debug!("reading .actr package: {:?}", args.package);
125 let package_bytes = std::fs::read(&args.package)
126 .with_context(|| format!("Failed to read package: {}", args.package.display()))?;
127
128 let manifest_str = actr_pack::read_manifest_raw(&package_bytes)
129 .with_context(|| "Failed to read manifest from .actr package")?;
130 let manifest = actr_pack::PackageManifest::from_toml(&manifest_str)
131 .with_context(|| "Failed to parse manifest TOML")?;
132 let sig_raw = actr_pack::read_signature(&package_bytes)
133 .with_context(|| "Failed to read manifest.sig from .actr package")?;
134
135 tracing::debug!(
136 "loading keychain for identity verification: {:?}",
137 args.keychain
138 );
139 let keychain_content = std::fs::read_to_string(&args.keychain)
140 .with_context(|| format!("Failed to read keychain: {}", args.keychain.display()))?;
141 let keychain: serde_json::Value =
142 serde_json::from_str(&keychain_content).with_context(|| "Invalid keychain JSON")?;
143
144 let kc_privkey_b64 = keychain["private_key"]
145 .as_str()
146 .ok_or_else(|| anyhow::anyhow!("Keychain missing 'private_key' field"))?;
147 let kc_privkey_bytes = base64::engine::general_purpose::STANDARD
148 .decode(kc_privkey_b64)
149 .with_context(|| "Invalid private key in keychain")?;
150 let kc_arr: [u8; 32] = kc_privkey_bytes
151 .try_into()
152 .map_err(|_| anyhow::anyhow!("Private key must be 32 bytes"))?;
153 let kc_signing_key = ed25519_dalek::SigningKey::from_bytes(&kc_arr);
154 let kc_key_id = actr_pack::compute_key_id(&kc_signing_key.verifying_key().to_bytes());
155
156 match manifest.signing_key_id {
157 Some(ref manifest_key_id) if manifest_key_id == &kc_key_id => {}
158 Some(ref manifest_key_id) => {
159 anyhow::bail!(
160 "Key mismatch: package was built with '{}' but keychain key is '{}'. \
161 Rebuild with the correct MFR key, or use the matching keychain.",
162 manifest_key_id,
163 kc_key_id
164 );
165 }
166 None => {
167 anyhow::bail!(
168 "Package manifest has no 'signing_key_id'. \
169 Rebuild with the latest 'actr build' to embed a signing_key_id."
170 );
171 }
172 }
173
174 let sig_b64 = base64::engine::general_purpose::STANDARD.encode(&sig_raw);
175
176 println!("📦 Publishing package: {}", manifest.actr_type_str());
177 println!(" manufacturer: {}", manifest.manufacturer);
178 println!(" name: {}", manifest.name);
179 println!(" version: {}", manifest.version);
180 println!(" target: {}", manifest.binary.target);
181 println!(" signing_key: {}", kc_key_id);
182 println!("✅ Identity verified (keychain matches package)");
183 println!("🔐 Forwarding original signature (no re-signing)");
184
185 let proto_files = actr_pack::read_proto_files(&package_bytes).unwrap_or_default();
186 let proto_filing = if !proto_files.is_empty() {
187 let proto_entries: Vec<serde_json::Value> = proto_files
188 .iter()
189 .map(|(name, content)| {
190 serde_json::json!({
191 "name": name,
192 "content": String::from_utf8_lossy(content),
193 })
194 })
195 .collect();
196 println!("📋 Proto files for filing: {} file(s)", proto_entries.len());
197 Some(serde_json::json!({ "protobufs": proto_entries }))
198 } else {
199 None
200 };
201
202 let endpoint = args
203 .endpoint
204 .trim_end_matches("/ais")
205 .trim_end_matches('/')
206 .to_string();
207
208 let base_url = endpoint.trim_end_matches('/');
209 let nonce_url = format!("{}/mfr/pkg/nonce", base_url);
210 let client = reqwest::Client::new();
211
212 println!("🔑 Requesting publish nonce...");
213 let nonce_resp = client
214 .post(&nonce_url)
215 .json(&serde_json::json!({ "manufacturer": manifest.manufacturer }))
216 .send()
217 .await
218 .with_context(|| format!("Failed to request nonce from: {}", nonce_url))?;
219
220 if !nonce_resp.status().is_success() {
221 let status = nonce_resp.status();
222 let body = nonce_resp.text().await.unwrap_or_default();
223 anyhow::bail!("Nonce request failed (HTTP {}): {}", status, body);
224 }
225
226 let nonce_json: serde_json::Value = nonce_resp
227 .json()
228 .await
229 .with_context(|| "Failed to parse nonce response")?;
230 let nonce_b64 = nonce_json["nonce"]
231 .as_str()
232 .ok_or_else(|| anyhow::anyhow!("Nonce response missing 'nonce' field"))?
233 .to_string();
234
235 let nonce_bytes = base64::engine::general_purpose::STANDARD
236 .decode(&nonce_b64)
237 .with_context(|| "Invalid nonce base64 from server")?;
238
239 let signable_body = SignablePublishBody {
240 manufacturer: &manifest.manufacturer,
241 name: &manifest.name,
242 version: &manifest.version,
243 target: &manifest.binary.target,
244 manifest: &manifest_str,
245 signature: &sig_b64,
246 proto_files: proto_filing.as_ref(),
247 nonce: &nonce_b64,
248 };
249 let signable_body_bytes = serde_json::to_vec(&signable_body)
250 .with_context(|| "Failed to serialize signable publish body")?;
251
252 let nonce_sig_b64 = {
253 use ed25519_dalek::Signer;
254 use sha2::{Digest, Sha256};
255
256 let body_hash = hex::encode(Sha256::digest(&signable_body_bytes));
257 let nonce_hex = hex::encode(&nonce_bytes);
258 let payload = format!(
259 "ACTR-PUBLISH-V1\nmanufacturer={}\nmethod=POST\npath=/mfr/pkg/publish\nnonce={}\nbody_sha256={}",
260 manifest.manufacturer, nonce_hex, body_hash
261 );
262 let sig = kc_signing_key.sign(payload.as_bytes());
263 base64::engine::general_purpose::STANDARD.encode(sig.to_bytes())
264 };
265 println!("✅ Nonce signed (challenge-response)");
266
267 let publish_url = format!("{}/mfr/pkg/publish", base_url);
268 println!("📡 Publishing to: {}", publish_url);
269
270 let publish_body_bytes = serde_json::to_vec(&FinalPublishBody {
271 manufacturer: &manifest.manufacturer,
272 name: &manifest.name,
273 version: &manifest.version,
274 target: &manifest.binary.target,
275 manifest: &manifest_str,
276 signature: &sig_b64,
277 proto_files: proto_filing.as_ref(),
278 nonce: &nonce_b64,
279 nonce_sig: &nonce_sig_b64,
280 })
281 .with_context(|| "Failed to serialize publish request body")?;
282
283 let resp = client
284 .post(&publish_url)
285 .header(reqwest::header::CONTENT_TYPE, "application/json")
286 .body(publish_body_bytes)
287 .send()
288 .await
289 .with_context(|| format!("Failed to connect to MFR endpoint: {}", publish_url))?;
290
291 let status = resp.status();
292 let body = resp.text().await.unwrap_or_default();
293
294 if !status.is_success() {
295 eprintln!("❌ Publish failed (HTTP {})", status);
296 eprintln!(" Response: {}", body);
297 anyhow::bail!("MFR publish failed with status {}: {}", status, body);
298 }
299
300 if let Ok(result) = serde_json::from_str::<serde_json::Value>(&body) {
301 let type_str = result["type_str"].as_str().unwrap_or("unknown");
302 let pkg_id = result["id"].as_i64().unwrap_or(0);
303 println!();
304 println!("✅ Package published successfully!");
305 println!(" type_str: {}", type_str);
306 println!(" pkg_id: {}", pkg_id);
307 println!(" status: active");
308 } else {
309 println!();
310 println!("✅ Package published successfully!");
311 println!(" Response: {}", body);
312 }
313
314 Ok(())
315}