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