Skip to main content

mnemo_deal/
discovery.rs

1//! Counterparty discovery (v0.4.1 P1-5).
2//!
3//! Anthropic's Project Deal (2026-04-25) opened up agent-on-agent
4//! commerce but published no built-in discovery surface. Mnemo
5//! ships the `/.well-known/mnemo-deal-agent.json` advertisement
6//! shape: each agent puts a small JSON document at a stable URL
7//! that says "I exist, here's my capabilities, here's my Ed25519
8//! public key, here's where my deal-ledger anchor is".
9
10use serde::{Deserialize, Serialize};
11
12/// Deal capability vocabulary. Open-ended; new verbs land as
13/// constants when the Project Deal catalog grows.
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub enum DealCapability {
16    DataLookup,
17    DataTransform,
18    Compute,
19    Verification,
20    /// Free-form capability the directory tags but does not
21    /// validate. Enables forward-compat with Project Deal's
22    /// evolving vocabulary without crate releases.
23    Custom(String),
24}
25
26impl DealCapability {
27    pub fn as_str(&self) -> &str {
28        match self {
29            DealCapability::DataLookup => "data_lookup",
30            DealCapability::DataTransform => "data_transform",
31            DealCapability::Compute => "compute",
32            DealCapability::Verification => "verification",
33            DealCapability::Custom(s) => s.as_str(),
34        }
35    }
36}
37
38/// Public-key bytes (32 bytes for Ed25519). Stored as hex on the
39/// wire so a curl + jq inspection is human-readable.
40pub type Ed25519PubBytes = [u8; 32];
41
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
43pub struct AgentAdvertisement {
44    pub agent: String,
45    pub capabilities: Vec<DealCapability>,
46    /// Hex-encoded 32-byte Ed25519 public key.
47    pub public_key_hex: String,
48    pub ledger_anchor_url: String,
49    pub terms_template: serde_json::Value,
50}
51
52impl AgentAdvertisement {
53    pub fn new(
54        agent: impl Into<String>,
55        capabilities: Vec<DealCapability>,
56        public_key: &Ed25519PubBytes,
57        ledger_anchor_url: impl Into<String>,
58    ) -> Self {
59        Self {
60            agent: agent.into(),
61            capabilities,
62            public_key_hex: hex::encode(public_key),
63            ledger_anchor_url: ledger_anchor_url.into(),
64            terms_template: serde_json::json!({}),
65        }
66    }
67
68    /// Serialize to the canonical `/.well-known/mnemo-deal-agent.json` body.
69    pub fn to_well_known(&self) -> Result<String, serde_json::Error> {
70        serde_json::to_string_pretty(self)
71    }
72
73    /// Parse a `/.well-known/mnemo-deal-agent.json` body.
74    pub fn from_well_known(body: &str) -> Result<Self, serde_json::Error> {
75        serde_json::from_str(body)
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn advertisement_round_trips_through_json() {
85        let pk = [7u8; 32];
86        let ad = AgentAdvertisement::new(
87            "agent-runner-42",
88            vec![DealCapability::DataLookup, DealCapability::Compute],
89            &pk,
90            "https://agent-42.example.com/deal-ledger",
91        );
92        let body = ad.to_well_known().unwrap();
93        let restored = AgentAdvertisement::from_well_known(&body).unwrap();
94        assert_eq!(restored, ad);
95    }
96
97    #[test]
98    fn capability_strings_round_trip() {
99        assert_eq!(DealCapability::DataLookup.as_str(), "data_lookup");
100        let custom = DealCapability::Custom("research".into());
101        assert_eq!(custom.as_str(), "research");
102    }
103
104    #[test]
105    fn body_includes_required_fields() {
106        let ad = AgentAdvertisement::new(
107            "x",
108            vec![DealCapability::DataLookup],
109            &[1u8; 32],
110            "https://x/y",
111        );
112        let body = ad.to_well_known().unwrap();
113        for required in [
114            "agent",
115            "capabilities",
116            "public_key_hex",
117            "ledger_anchor_url",
118        ] {
119            assert!(
120                body.contains(required),
121                "missing field {required} in: {body}"
122            );
123        }
124    }
125}