1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::BTreeMap;
4
5use crate::error::{Result, SdkError};
6use crate::identity::{verify_envelope, AgentId, Envelope, Event};
7
8pub const PROTOCOL: &str = "agent-profile/1.0";
9pub const PROFILE_UPDATE: &str = "profile.update";
10
11#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
12pub struct ServiceEndpoint {
13 #[serde(rename = "type")]
14 pub kind: String,
15 pub url: String,
16 #[serde(default, skip_serializing_if = "Vec::is_empty")]
17 pub protocols: Vec<String>,
18}
19
20#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
21pub struct ProfileUpdatePayload {
22 pub agent_id: AgentId,
23 pub name: String,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub description: Option<String>,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
27 pub avatar_url: Option<String>,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub provider: Option<String>,
30 #[serde(default, skip_serializing_if = "Vec::is_empty")]
31 pub capabilities: Vec<String>,
32 #[serde(default, skip_serializing_if = "Vec::is_empty")]
33 pub service_endpoints: Vec<ServiceEndpoint>,
34 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
35 pub links: BTreeMap<String, String>,
36 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
37 pub metadata: BTreeMap<String, Value>,
38}
39
40impl ProfileUpdatePayload {
41 pub fn new(agent_id: AgentId, name: impl Into<String>) -> Self {
42 Self {
43 agent_id,
44 name: name.into(),
45 description: None,
46 avatar_url: None,
47 provider: None,
48 capabilities: Vec::new(),
49 service_endpoints: Vec::new(),
50 links: BTreeMap::new(),
51 metadata: BTreeMap::new(),
52 }
53 }
54}
55
56#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
57pub struct AgentProfile {
58 pub agent_id: AgentId,
59 pub name: String,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub description: Option<String>,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub avatar_url: Option<String>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub provider: Option<String>,
66 #[serde(default, skip_serializing_if = "Vec::is_empty")]
67 pub capabilities: Vec<String>,
68 #[serde(default, skip_serializing_if = "Vec::is_empty")]
69 pub service_endpoints: Vec<ServiceEndpoint>,
70 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
71 pub links: BTreeMap<String, String>,
72 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
73 pub metadata: BTreeMap<String, Value>,
74 pub updated_at: i64,
75 pub profile_event_id: String,
76}
77
78#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
79pub struct ProfileReadResponse {
80 pub profile: AgentProfile,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub profile_event: Option<Envelope<ProfileUpdatePayload>>,
83}
84
85#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
86pub struct ProfileServiceDiscovery {
87 pub protocol: String,
88 pub service: String,
89 pub endpoints: ProfileServiceEndpoints,
90 #[serde(default, skip_serializing_if = "Vec::is_empty")]
91 pub features: Vec<String>,
92}
93
94#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
95pub struct ProfileServiceEndpoints {
96 pub profiles: String,
97}
98
99pub fn profile_update_event(
100 actor: AgentId,
101 created_at: i64,
102 nonce: impl Into<String>,
103 payload: ProfileUpdatePayload,
104) -> Event<ProfileUpdatePayload> {
105 Event::new(PROTOCOL, PROFILE_UPDATE, actor, created_at, nonce, payload)
106}
107
108pub fn validate_profile_update(envelope: &Envelope<ProfileUpdatePayload>) -> Result<()> {
109 verify_envelope(envelope)?;
110 if envelope.event.protocol != PROTOCOL {
111 return Err(SdkError::InvalidEventProtocol {
112 expected: PROTOCOL.to_owned(),
113 actual: envelope.event.protocol.clone(),
114 });
115 }
116 if envelope.event.kind != PROFILE_UPDATE {
117 return Err(SdkError::InvalidEventType {
118 expected: PROFILE_UPDATE.to_owned(),
119 actual: envelope.event.kind.clone(),
120 });
121 }
122 if envelope.event.actor != envelope.event.payload.agent_id {
123 return Err(SdkError::InvalidActor(
124 "profile update actor must match payload.agent_id".to_owned(),
125 ));
126 }
127 Ok(())
128}
129
130pub fn materialize_profile(envelope: &Envelope<ProfileUpdatePayload>) -> Result<AgentProfile> {
131 validate_profile_update(envelope)?;
132 let payload = &envelope.event.payload;
133 Ok(AgentProfile {
134 agent_id: payload.agent_id.clone(),
135 name: payload.name.clone(),
136 description: payload.description.clone(),
137 avatar_url: payload.avatar_url.clone(),
138 provider: payload.provider.clone(),
139 capabilities: payload.capabilities.clone(),
140 service_endpoints: payload.service_endpoints.clone(),
141 links: payload.links.clone(),
142 metadata: payload.metadata.clone(),
143 updated_at: envelope.event.created_at,
144 profile_event_id: envelope.event_id.clone(),
145 })
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use crate::identity::AgentSigner;
152
153 #[test]
154 fn materializes_valid_profile_update() {
155 let signer = AgentSigner::from_seed([11; 32]);
156 let mut payload = ProfileUpdatePayload::new(signer.agent_id(), "ResearchAgent-v3");
157 payload.capabilities.push("research".to_owned());
158 let event =
159 profile_update_event(signer.agent_id(), 1_779_753_600_000, "n_profile", payload);
160 let envelope = signer.sign_event(event).unwrap();
161
162 let profile = materialize_profile(&envelope).unwrap();
163
164 assert_eq!(profile.name, "ResearchAgent-v3");
165 assert_eq!(profile.updated_at, 1_779_753_600_000);
166 assert_eq!(profile.profile_event_id, envelope.event_id);
167 }
168
169 #[test]
170 fn rejects_actor_payload_mismatch() {
171 let signer = AgentSigner::from_seed([12; 32]);
172 let other = AgentSigner::from_seed([13; 32]);
173 let payload = ProfileUpdatePayload::new(other.agent_id(), "Imposter");
174 let event =
175 profile_update_event(signer.agent_id(), 1_779_753_600_000, "n_profile", payload);
176 let envelope = signer.sign_event(event).unwrap();
177
178 assert!(matches!(
179 validate_profile_update(&envelope),
180 Err(SdkError::InvalidActor(_))
181 ));
182 }
183}