1pub mod gossip;
4pub mod sync;
5
6pub use gossip::{
7 GossipConfig, GossipDigest, GossipDigestEntry, GossipSyncEngine as PushPullGossipSyncEngine,
8 GossipSyncReport, PeerAddress,
9};
10pub use sync::{
11 CapsuleDisposition, GossipSyncEngine, QuarantineEntry, QuarantineReason, QuarantineState,
12 QuarantineStore, RemoteCapsuleReceiver, SyncStats, PROMOTE_THRESHOLD,
13};
14
15use std::collections::BTreeSet;
16
17use chrono::Utc;
18use serde::{Deserialize, Serialize};
19use sha2::{Digest, Sha256};
20
21use oris_evolution::{Capsule, EvolutionEvent, Gene};
22
23#[derive(Clone, Debug, Serialize, Deserialize)]
24pub enum MessageType {
25 Publish,
26 Fetch,
27 Report,
28 Revoke,
29}
30
31#[derive(Clone, Debug, Serialize, Deserialize)]
32#[serde(tag = "kind", rename_all = "snake_case")]
33pub enum NetworkAsset {
34 Gene { gene: Gene },
35 Capsule { capsule: Capsule },
36 EvolutionEvent { event: EvolutionEvent },
37}
38
39#[derive(Clone, Debug, Serialize, Deserialize)]
40pub struct EvolutionEnvelope {
41 pub protocol: String,
42 pub protocol_version: String,
43 pub message_type: MessageType,
44 pub message_id: String,
45 pub sender_id: String,
46 pub timestamp: String,
47 pub assets: Vec<NetworkAsset>,
48 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub manifest: Option<EnvelopeManifest>,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub signature: Option<String>,
52 pub content_hash: String,
53}
54
55#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
56pub struct EnvelopeManifest {
57 pub publisher: String,
58 pub sender_id: String,
59 pub asset_ids: Vec<String>,
60 pub asset_hash: String,
61}
62
63#[derive(Clone, Debug, Serialize, Deserialize)]
64pub struct PublishRequest {
65 pub sender_id: String,
66 pub assets: Vec<NetworkAsset>,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub since_cursor: Option<String>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub resume_token: Option<String>,
71}
72
73#[derive(Clone, Debug, Serialize, Deserialize)]
74pub struct FetchQuery {
75 pub sender_id: String,
76 pub signals: Vec<String>,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub since_cursor: Option<String>,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub resume_token: Option<String>,
81}
82
83#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
84pub struct SyncAudit {
85 pub batch_id: String,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub requested_cursor: Option<String>,
88 pub scanned_count: usize,
89 pub applied_count: usize,
90 pub skipped_count: usize,
91 pub failed_count: usize,
92 #[serde(default, skip_serializing_if = "Vec::is_empty")]
93 pub failure_reasons: Vec<String>,
94}
95
96#[derive(Clone, Debug, Serialize, Deserialize)]
97pub struct FetchResponse {
98 pub sender_id: String,
99 pub assets: Vec<NetworkAsset>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub next_cursor: Option<String>,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub resume_token: Option<String>,
104 #[serde(default)]
105 pub sync_audit: SyncAudit,
106}
107
108#[derive(Clone, Debug, Serialize, Deserialize)]
109pub struct RevokeNotice {
110 pub sender_id: String,
111 pub asset_ids: Vec<String>,
112 pub reason: String,
113}
114
115impl EvolutionEnvelope {
116 pub fn publish(sender_id: impl Into<String>, assets: Vec<NetworkAsset>) -> Self {
117 let sender_id = sender_id.into();
118 let manifest = Some(Self::build_manifest(&sender_id, &assets));
119 let mut envelope = Self {
120 protocol: "oen".into(),
121 protocol_version: "0.1".into(),
122 message_type: MessageType::Publish,
123 message_id: format!(
124 "msg-{:x}",
125 Utc::now().timestamp_nanos_opt().unwrap_or_default()
126 ),
127 sender_id,
128 timestamp: Utc::now().to_rfc3339(),
129 assets,
130 manifest,
131 signature: None,
132 content_hash: String::new(),
133 };
134 envelope.content_hash = envelope.compute_content_hash();
135 envelope
136 }
137
138 fn build_manifest(sender_id: &str, assets: &[NetworkAsset]) -> EnvelopeManifest {
139 EnvelopeManifest {
140 publisher: sender_id.to_string(),
141 sender_id: sender_id.to_string(),
142 asset_ids: Self::manifest_asset_ids(assets),
143 asset_hash: Self::compute_assets_hash(assets),
144 }
145 }
146
147 fn normalize_manifest_ids(asset_ids: &[String]) -> Vec<String> {
148 let normalized = asset_ids
149 .iter()
150 .map(|value| value.trim())
151 .filter(|value| !value.is_empty())
152 .map(ToOwned::to_owned)
153 .collect::<BTreeSet<_>>();
154 normalized.into_iter().collect()
155 }
156
157 pub fn manifest_asset_ids(assets: &[NetworkAsset]) -> Vec<String> {
158 let ids = assets
159 .iter()
160 .map(Self::manifest_asset_id)
161 .collect::<BTreeSet<_>>();
162 ids.into_iter().collect()
163 }
164
165 fn manifest_asset_id(asset: &NetworkAsset) -> String {
166 match asset {
167 NetworkAsset::Gene { gene } => format!("gene:{}", gene.id),
168 NetworkAsset::Capsule { capsule } => format!("capsule:{}", capsule.id),
169 NetworkAsset::EvolutionEvent { event } => {
170 let payload = serde_json::to_vec(event).unwrap_or_default();
171 let mut hasher = Sha256::new();
172 hasher.update(payload);
173 format!("event:{}", hex::encode(hasher.finalize()))
174 }
175 }
176 }
177
178 pub fn compute_assets_hash(assets: &[NetworkAsset]) -> String {
179 let payload = serde_json::to_vec(assets).unwrap_or_default();
180 let mut hasher = Sha256::new();
181 hasher.update(payload);
182 hex::encode(hasher.finalize())
183 }
184
185 pub fn compute_content_hash(&self) -> String {
186 let payload = (
187 &self.protocol,
188 &self.protocol_version,
189 &self.message_type,
190 &self.message_id,
191 &self.sender_id,
192 &self.timestamp,
193 &self.assets,
194 &self.manifest,
195 &self.signature,
196 );
197 let json = serde_json::to_vec(&payload).unwrap_or_default();
198 let mut hasher = Sha256::new();
199 hasher.update(json);
200 hex::encode(hasher.finalize())
201 }
202
203 pub fn verify_content_hash(&self) -> bool {
204 self.compute_content_hash() == self.content_hash
205 }
206
207 pub fn verify_manifest(&self) -> Result<(), String> {
208 let Some(manifest) = self.manifest.as_ref() else {
209 return Err("missing manifest".into());
210 };
211 if manifest.publisher.trim().is_empty() {
212 return Err("missing manifest publisher".into());
213 }
214 if manifest.sender_id.trim().is_empty() {
215 return Err("missing manifest sender_id".into());
216 }
217 if manifest.sender_id.trim() != self.sender_id.trim() {
218 return Err("manifest sender_id mismatch".into());
219 }
220
221 let expected_asset_ids = Self::manifest_asset_ids(&self.assets);
222 let actual_asset_ids = Self::normalize_manifest_ids(&manifest.asset_ids);
223 if expected_asset_ids != actual_asset_ids {
224 return Err("manifest asset_ids mismatch".into());
225 }
226
227 let expected_hash = Self::compute_assets_hash(&self.assets);
228 if manifest.asset_hash != expected_hash {
229 return Err("manifest asset_hash mismatch".into());
230 }
231
232 Ok(())
233 }
234
235 pub fn verify_signature(&self) -> bool {
236 true
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use oris_evolution::{AssetState, Gene};
244
245 fn sample_gene(id: &str) -> Gene {
246 Gene {
247 id: id.to_string(),
248 signals: vec!["docs.fix".to_string()],
249 strategy: vec!["summary=docs fix".to_string()],
250 validation: vec!["cargo test".to_string()],
251 state: AssetState::Promoted,
252 task_class_id: None,
253 }
254 }
255
256 #[test]
257 fn publish_populates_manifest_and_verifies() {
258 let envelope = EvolutionEnvelope::publish(
259 "node-a",
260 vec![NetworkAsset::Gene {
261 gene: sample_gene("gene-a"),
262 }],
263 );
264 assert!(envelope.verify_content_hash());
265 assert!(envelope.verify_manifest().is_ok());
266 assert!(envelope.manifest.is_some());
267 }
268
269 #[test]
270 fn verify_manifest_detects_sender_mismatch() {
271 let mut envelope = EvolutionEnvelope::publish(
272 "node-a",
273 vec![NetworkAsset::Gene {
274 gene: sample_gene("gene-a"),
275 }],
276 );
277 envelope.sender_id = "node-b".to_string();
278 assert!(envelope.verify_manifest().is_err());
279 }
280
281 #[test]
282 fn verify_manifest_detects_asset_hash_drift() {
283 let mut envelope = EvolutionEnvelope::publish(
284 "node-a",
285 vec![NetworkAsset::Gene {
286 gene: sample_gene("gene-a"),
287 }],
288 );
289 if let Some(NetworkAsset::Gene { gene }) = envelope.assets.first_mut() {
290 gene.id = "gene-b".to_string();
291 }
292 assert!(envelope.verify_manifest().is_err());
293 }
294}