1use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
34use serde::{Deserialize, Serialize};
35use sha2::{Digest, Sha256};
36use std::collections::HashMap;
37use std::sync::atomic::{AtomicI64, Ordering};
38use std::time::{SystemTime, UNIX_EPOCH};
39use thiserror::Error;
40
41pub const SPEC: &str = "moss-0001";
42pub const VERSION: i32 = 1;
43pub const ALGORITHM: &str = "ML-DSA-44";
44pub const DEFAULT_BASE_URL: &str = "https://moss-api-837703369688.us-central1.run.app";
45
46#[derive(Error, Debug)]
48pub enum MossError {
49 #[error("API key is required")]
50 NoApiKey,
51
52 #[error("Invalid envelope")]
53 InvalidEnvelope,
54
55 #[error("Verification failed: {0}")]
56 VerificationFailed(String),
57
58 #[error("HTTP error: {0}")]
59 HttpError(#[from] reqwest::Error),
60
61 #[error("JSON error: {0}")]
62 JsonError(#[from] serde_json::Error),
63
64 #[error("API error: {0}")]
65 ApiError(String),
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct Envelope {
71 pub spec: String,
72 pub version: i32,
73 pub alg: String,
74 pub subject: String,
75 pub key_version: i32,
76 pub seq: i64,
77 pub issued_at: i64,
78 pub payload_hash: String,
79 pub signature: String,
80}
81
82#[derive(Debug, Clone)]
84pub struct SignRequest {
85 pub payload: serde_json::Value,
86 pub agent_id: String,
87 pub action: Option<String>,
88 pub context: Option<HashMap<String, serde_json::Value>>,
89}
90
91#[derive(Debug, Clone)]
93pub struct SignResult {
94 pub envelope: Envelope,
95 pub allowed: bool,
96 pub blocked: bool,
97 pub held: bool,
98 pub decision: String,
99 pub reason: Option<String>,
100 pub action_id: Option<String>,
101 pub evidence_id: Option<String>,
102 pub signature_valid: bool,
103}
104
105#[derive(Debug, Clone)]
107pub struct VerifyResult {
108 pub valid: bool,
109 pub subject: Option<String>,
110 pub issued_at: Option<i64>,
111 pub sequence: Option<i64>,
112 pub error: Option<String>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct Agent {
118 pub id: String,
119 pub agent_id: String,
120 pub display_name: Option<String>,
121 pub status: String,
122 pub tags: Option<Vec<String>>,
123 pub metadata: Option<HashMap<String, serde_json::Value>>,
124 pub policy_id: Option<String>,
125 pub total_signatures: i64,
126 pub active_key_id: Option<String>,
127 pub created_at: Option<String>,
128 pub last_seen_at: Option<String>,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct RegisterAgentRequest {
134 pub agent_id: String,
135 pub display_name: Option<String>,
136 pub tags: Option<Vec<String>>,
137 pub metadata: Option<HashMap<String, serde_json::Value>>,
138 pub policy_id: Option<String>,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct RegisterAgentResult {
144 pub id: String,
145 pub agent_id: String,
146 pub display_name: Option<String>,
147 pub status: String,
148 pub key_id: String,
149 pub signing_secret: String,
150 pub created_at: Option<String>,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct RotateKeyResult {
156 pub agent_id: String,
157 pub key_id: String,
158 pub signing_secret: String,
159 pub rotated_at: String,
160}
161
162#[derive(Debug, Clone)]
164pub struct MossConfig {
165 pub api_key: Option<String>,
166 pub base_url: String,
167}
168
169impl Default for MossConfig {
170 fn default() -> Self {
171 Self {
172 api_key: std::env::var("MOSS_API_KEY").ok(),
173 base_url: DEFAULT_BASE_URL.to_string(),
174 }
175 }
176}
177
178pub struct MossClient {
180 config: MossConfig,
181 http_client: reqwest::Client,
182 sequence: AtomicI64,
183}
184
185impl MossClient {
186 pub fn new(api_key: Option<String>) -> Result<Self, MossError> {
188 let config = MossConfig {
189 api_key: api_key.or_else(|| std::env::var("MOSS_API_KEY").ok()),
190 ..Default::default()
191 };
192
193 Ok(Self {
194 config,
195 http_client: reqwest::Client::new(),
196 sequence: AtomicI64::new(0),
197 })
198 }
199
200 pub fn with_config(config: MossConfig) -> Result<Self, MossError> {
202 Ok(Self {
203 config,
204 http_client: reqwest::Client::new(),
205 sequence: AtomicI64::new(0),
206 })
207 }
208
209 pub async fn sign(&self, req: SignRequest) -> Result<SignResult, MossError> {
211 if self.config.api_key.is_none() {
212 return self.sign_local(req);
213 }
214 self.sign_enterprise(req).await
215 }
216
217 fn sign_local(&self, req: SignRequest) -> Result<SignResult, MossError> {
218 let payload_json = serde_json::to_string(&req.payload)?;
219 let payload_hash = compute_hash(&payload_json);
220
221 let seq = self.sequence.fetch_add(1, Ordering::SeqCst) + 1;
222 let now = SystemTime::now()
223 .duration_since(UNIX_EPOCH)
224 .unwrap()
225 .as_secs() as i64;
226
227 let subject = if req.agent_id.is_empty() {
228 "moss:local:default".to_string()
229 } else {
230 req.agent_id
231 };
232
233 let envelope = Envelope {
234 spec: SPEC.to_string(),
235 version: VERSION,
236 alg: ALGORITHM.to_string(),
237 subject: subject.clone(),
238 key_version: 1,
239 seq,
240 issued_at: now,
241 payload_hash,
242 signature: String::new(),
243 };
244
245 Ok(SignResult {
246 envelope,
247 allowed: true,
248 blocked: false,
249 held: false,
250 decision: "allow".to_string(),
251 reason: None,
252 action_id: None,
253 evidence_id: None,
254 signature_valid: true,
255 })
256 }
257
258 async fn sign_enterprise(&self, req: SignRequest) -> Result<SignResult, MossError> {
259 let api_key = self.config.api_key.as_ref().ok_or(MossError::NoApiKey)?;
260
261 let mut eval_req: HashMap<String, serde_json::Value> = HashMap::new();
262 eval_req.insert("subject".to_string(), serde_json::json!(req.agent_id));
263 eval_req.insert("payload".to_string(), req.payload.clone());
264 if let Some(action) = &req.action {
265 eval_req.insert("action".to_string(), serde_json::json!(action));
266 }
267 if let Some(context) = &req.context {
268 eval_req.insert("context".to_string(), serde_json::to_value(context)?);
269 }
270
271 let response = self
272 .http_client
273 .post(format!("{}/v1/evaluate", self.config.base_url))
274 .header("Authorization", format!("Bearer {}", api_key))
275 .json(&eval_req)
276 .send()
277 .await?;
278
279 if !response.status().is_success() {
280 let status = response.status();
281 let text = response.text().await.unwrap_or_default();
282 return Err(MossError::ApiError(format!(
283 "status {}: {}",
284 status, text
285 )));
286 }
287
288 let result: serde_json::Value = response.json().await?;
289
290 let envelope = if let Some(env) = result.get("envelope") {
291 serde_json::from_value(env.clone())?
292 } else {
293 let payload_json = serde_json::to_string(&req.payload)?;
294 let payload_hash = compute_hash(&payload_json);
295 let seq = self.sequence.fetch_add(1, Ordering::SeqCst) + 1;
296 let now = SystemTime::now()
297 .duration_since(UNIX_EPOCH)
298 .unwrap()
299 .as_secs() as i64;
300
301 Envelope {
302 spec: SPEC.to_string(),
303 version: VERSION,
304 alg: ALGORITHM.to_string(),
305 subject: req.agent_id.clone(),
306 key_version: 1,
307 seq,
308 issued_at: now,
309 payload_hash,
310 signature: String::new(),
311 }
312 };
313
314 let decision = result
315 .get("decision")
316 .and_then(|v| v.as_str())
317 .unwrap_or("allow")
318 .to_string();
319
320 Ok(SignResult {
321 envelope,
322 allowed: decision == "allow",
323 blocked: decision == "block",
324 held: decision == "hold",
325 decision,
326 reason: result.get("reason").and_then(|v| v.as_str()).map(String::from),
327 action_id: result.get("action_id").and_then(|v| v.as_str()).map(String::from),
328 evidence_id: result.get("evidence_id").and_then(|v| v.as_str()).map(String::from),
329 signature_valid: result.get("signature_valid").and_then(|v| v.as_bool()).unwrap_or(true),
330 })
331 }
332
333 pub fn verify(&self, payload: &serde_json::Value, envelope: &Envelope) -> VerifyResult {
335 if envelope.spec != SPEC {
336 return VerifyResult {
337 valid: false,
338 subject: None,
339 issued_at: None,
340 sequence: None,
341 error: Some(format!("Unknown spec: {}", envelope.spec)),
342 };
343 }
344
345 let payload_json = match serde_json::to_string(payload) {
346 Ok(j) => j,
347 Err(e) => {
348 return VerifyResult {
349 valid: false,
350 subject: None,
351 issued_at: None,
352 sequence: None,
353 error: Some(format!("Failed to encode payload: {}", e)),
354 };
355 }
356 };
357
358 let computed_hash = compute_hash(&payload_json);
359
360 if computed_hash != envelope.payload_hash {
361 return VerifyResult {
362 valid: false,
363 subject: None,
364 issued_at: None,
365 sequence: None,
366 error: Some("Payload hash mismatch".to_string()),
367 };
368 }
369
370 VerifyResult {
371 valid: true,
372 subject: Some(envelope.subject.clone()),
373 issued_at: Some(envelope.issued_at),
374 sequence: Some(envelope.seq),
375 error: None,
376 }
377 }
378
379 pub async fn register_agent(
381 &self,
382 req: RegisterAgentRequest,
383 ) -> Result<RegisterAgentResult, MossError> {
384 let api_key = self.config.api_key.as_ref().ok_or(MossError::NoApiKey)?;
385
386 let response = self
387 .http_client
388 .post(format!("{}/v1/agents", self.config.base_url))
389 .header("Authorization", format!("Bearer {}", api_key))
390 .json(&req)
391 .send()
392 .await?;
393
394 if !response.status().is_success() {
395 let status = response.status();
396 let text = response.text().await.unwrap_or_default();
397 return Err(MossError::ApiError(format!(
398 "status {}: {}",
399 status, text
400 )));
401 }
402
403 Ok(response.json().await?)
404 }
405
406 pub async fn get_agent(&self, agent_id: &str) -> Result<Option<Agent>, MossError> {
408 let api_key = self.config.api_key.as_ref().ok_or(MossError::NoApiKey)?;
409
410 let response = self
411 .http_client
412 .get(format!("{}/v1/agents/{}", self.config.base_url, agent_id))
413 .header("Authorization", format!("Bearer {}", api_key))
414 .send()
415 .await?;
416
417 if response.status() == reqwest::StatusCode::NOT_FOUND {
418 return Ok(None);
419 }
420
421 if !response.status().is_success() {
422 let status = response.status();
423 let text = response.text().await.unwrap_or_default();
424 return Err(MossError::ApiError(format!(
425 "status {}: {}",
426 status, text
427 )));
428 }
429
430 Ok(Some(response.json().await?))
431 }
432
433 pub async fn rotate_agent_key(
435 &self,
436 agent_id: &str,
437 reason: Option<&str>,
438 ) -> Result<RotateKeyResult, MossError> {
439 let api_key = self.config.api_key.as_ref().ok_or(MossError::NoApiKey)?;
440
441 let mut body: HashMap<String, String> = HashMap::new();
442 if let Some(r) = reason {
443 body.insert("reason".to_string(), r.to_string());
444 }
445
446 let response = self
447 .http_client
448 .post(format!(
449 "{}/v1/agents/{}/rotate",
450 self.config.base_url, agent_id
451 ))
452 .header("Authorization", format!("Bearer {}", api_key))
453 .json(&body)
454 .send()
455 .await?;
456
457 if !response.status().is_success() {
458 let status = response.status();
459 let text = response.text().await.unwrap_or_default();
460 return Err(MossError::ApiError(format!(
461 "status {}: {}",
462 status, text
463 )));
464 }
465
466 Ok(response.json().await?)
467 }
468
469 pub async fn suspend_agent(
471 &self,
472 agent_id: &str,
473 reason: Option<&str>,
474 ) -> Result<(), MossError> {
475 let api_key = self.config.api_key.as_ref().ok_or(MossError::NoApiKey)?;
476
477 let mut body: HashMap<String, String> = HashMap::new();
478 if let Some(r) = reason {
479 body.insert("reason".to_string(), r.to_string());
480 }
481
482 let response = self
483 .http_client
484 .post(format!(
485 "{}/v1/agents/{}/suspend",
486 self.config.base_url, agent_id
487 ))
488 .header("Authorization", format!("Bearer {}", api_key))
489 .json(&body)
490 .send()
491 .await?;
492
493 if !response.status().is_success() {
494 let status = response.status();
495 let text = response.text().await.unwrap_or_default();
496 return Err(MossError::ApiError(format!(
497 "status {}: {}",
498 status, text
499 )));
500 }
501
502 Ok(())
503 }
504
505 pub async fn reactivate_agent(&self, agent_id: &str) -> Result<(), MossError> {
507 let api_key = self.config.api_key.as_ref().ok_or(MossError::NoApiKey)?;
508
509 let response = self
510 .http_client
511 .post(format!(
512 "{}/v1/agents/{}/reactivate",
513 self.config.base_url, agent_id
514 ))
515 .header("Authorization", format!("Bearer {}", api_key))
516 .send()
517 .await?;
518
519 if !response.status().is_success() {
520 let status = response.status();
521 let text = response.text().await.unwrap_or_default();
522 return Err(MossError::ApiError(format!(
523 "status {}: {}",
524 status, text
525 )));
526 }
527
528 Ok(())
529 }
530
531 pub async fn revoke_agent(&self, agent_id: &str, reason: &str) -> Result<(), MossError> {
533 let api_key = self.config.api_key.as_ref().ok_or(MossError::NoApiKey)?;
534
535 let body: HashMap<String, String> =
536 [("reason".to_string(), reason.to_string())].into_iter().collect();
537
538 let response = self
539 .http_client
540 .post(format!(
541 "{}/v1/agents/{}/revoke",
542 self.config.base_url, agent_id
543 ))
544 .header("Authorization", format!("Bearer {}", api_key))
545 .json(&body)
546 .send()
547 .await?;
548
549 if !response.status().is_success() {
550 let status = response.status();
551 let text = response.text().await.unwrap_or_default();
552 return Err(MossError::ApiError(format!(
553 "status {}: {}",
554 status, text
555 )));
556 }
557
558 Ok(())
559 }
560
561 pub fn is_enterprise_enabled(&self) -> bool {
563 self.config.api_key.is_some()
564 }
565}
566
567fn compute_hash(data: &str) -> String {
568 let mut hasher = Sha256::new();
569 hasher.update(data.as_bytes());
570 let result = hasher.finalize();
571 URL_SAFE_NO_PAD.encode(result)
572}
573
574#[cfg(test)]
575mod tests {
576 use super::*;
577
578 #[test]
579 fn test_new_client() {
580 let client = MossClient::new(None).unwrap();
581 assert!(!client.is_enterprise_enabled());
582 }
583
584 #[test]
585 fn test_new_client_with_api_key() {
586 let client = MossClient::new(Some("test_key".to_string())).unwrap();
587 assert!(client.is_enterprise_enabled());
588 }
589
590 #[tokio::test]
591 async fn test_sign_local() {
592 let client = MossClient::new(None).unwrap();
593
594 let result = client
595 .sign(SignRequest {
596 payload: serde_json::json!({"action": "test", "amount": 100}),
597 agent_id: "test-agent".to_string(),
598 action: None,
599 context: None,
600 })
601 .await
602 .unwrap();
603
604 assert_eq!(result.envelope.spec, SPEC);
605 assert_eq!(result.envelope.subject, "test-agent");
606 assert!(!result.envelope.payload_hash.is_empty());
607 assert!(result.allowed);
608 }
609
610 #[tokio::test]
611 async fn test_sign_sequence_increment() {
612 let client = MossClient::new(None).unwrap();
613
614 let result1 = client
615 .sign(SignRequest {
616 payload: serde_json::json!("test1"),
617 agent_id: "agent".to_string(),
618 action: None,
619 context: None,
620 })
621 .await
622 .unwrap();
623
624 let result2 = client
625 .sign(SignRequest {
626 payload: serde_json::json!("test2"),
627 agent_id: "agent".to_string(),
628 action: None,
629 context: None,
630 })
631 .await
632 .unwrap();
633
634 assert!(result2.envelope.seq > result1.envelope.seq);
635 }
636
637 #[tokio::test]
638 async fn test_verify() {
639 let client = MossClient::new(None).unwrap();
640
641 let payload = serde_json::json!({"action": "test", "value": 42});
642
643 let sign_result = client
644 .sign(SignRequest {
645 payload: payload.clone(),
646 agent_id: "test-agent".to_string(),
647 action: None,
648 context: None,
649 })
650 .await
651 .unwrap();
652
653 let verify_result = client.verify(&payload, &sign_result.envelope);
654
655 assert!(verify_result.valid);
656 assert_eq!(verify_result.subject, Some("test-agent".to_string()));
657 }
658
659 #[tokio::test]
660 async fn test_verify_tampered_payload() {
661 let client = MossClient::new(None).unwrap();
662
663 let payload = serde_json::json!({"action": "test", "value": 42});
664
665 let sign_result = client
666 .sign(SignRequest {
667 payload: payload.clone(),
668 agent_id: "test-agent".to_string(),
669 action: None,
670 context: None,
671 })
672 .await
673 .unwrap();
674
675 let tampered = serde_json::json!({"action": "test", "value": 9999});
676 let verify_result = client.verify(&tampered, &sign_result.envelope);
677
678 assert!(!verify_result.valid);
679 }
680}