tenzro_types/knowledge.rs
1//! Knowledge registry types for Tenzro Network.
2//!
3//! Operator-curated, queryable data resources: vector DBs, RAG
4//! indices, document corpora, indexed historical datasets, real-time
5//! data feeds (prices, oracles, indexers, weather, news, regulatory
6//! filings, social signals), embedding stores. Discoverable via
7//! `tenzro_listKnowledge` / `tenzro_searchKnowledge`, invokable via
8//! `tenzro_useKnowledge`, settled in TNZO with the same commission
9//! split as tools (5% to treasury, 95% to operator's creator_wallet).
10//!
11//! Distinguished from MCPs in that an MCP is a *protocol* (the
12//! invocation method) and a knowledge resource is a *data resource*
13//! (what the operator brokers). A datasource MCP IS a knowledge
14//! resource — it can register under both registries. The
15//! distinction is intent: registering as a knowledge resource
16//! signals "this is queryable data with semantic discovery
17//! metadata"; registering as a tool signals "this is an actionable
18//! capability."
19
20use crate::primitives::Address;
21use serde::{Deserialize, Serialize};
22
23/// What kind of knowledge resource this is. Drives the suggested
24/// `params` schema and lets the discovery surface filter by purpose.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27pub enum KnowledgeKind {
28 /// Vector index — semantic similarity search over an embedded
29 /// corpus. Params: `{ query: string, top_k: u32, filter?: object }`.
30 VectorIndex,
31 /// Full-text document corpus (BM25 / Tantivy / Lucene). Params:
32 /// `{ query: string, top_k: u32, filter?: object }`.
33 DocumentCorpus,
34 /// Pre-indexed historical dataset queryable by structured filter
35 /// (SQL-ish / GraphQL / REST). Params: caller-defined.
36 IndexedDataset,
37 /// Real-time data feed (price feed, oracle, market data, news
38 /// stream). Params: `{ subject: string, window?: object }`.
39 Feed,
40 /// Embedding store for retrieval-augmented generation. Params:
41 /// `{ query: string | embedding: [f32], top_k: u32 }`.
42 EmbeddingStore,
43 /// Catch-all for resource shapes not yet captured. The discovery
44 /// surface still routes by capability_tags; the kind is advisory.
45 Other,
46}
47
48impl KnowledgeKind {
49 pub fn as_str(&self) -> &'static str {
50 match self {
51 KnowledgeKind::VectorIndex => "vector_index",
52 KnowledgeKind::DocumentCorpus => "document_corpus",
53 KnowledgeKind::IndexedDataset => "indexed_dataset",
54 KnowledgeKind::Feed => "feed",
55 KnowledgeKind::EmbeddingStore => "embedding_store",
56 KnowledgeKind::Other => "other",
57 }
58 }
59
60 pub fn parse_str(s: &str) -> Option<Self> {
61 match s {
62 "vector_index" => Some(KnowledgeKind::VectorIndex),
63 "document_corpus" => Some(KnowledgeKind::DocumentCorpus),
64 "indexed_dataset" => Some(KnowledgeKind::IndexedDataset),
65 "feed" => Some(KnowledgeKind::Feed),
66 "embedding_store" => Some(KnowledgeKind::EmbeddingStore),
67 "other" => Some(KnowledgeKind::Other),
68 _ => None,
69 }
70 }
71}
72
73/// Status of a knowledge resource. Mirrors `ToolStatus`.
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
75#[serde(rename_all = "snake_case")]
76pub enum KnowledgeStatus {
77 #[default]
78 Active,
79 Inactive,
80 Deprecated,
81}
82
83/// A knowledge resource published to the Tenzro Network knowledge
84/// registry. Operators register resources they broker; tenants
85/// discover via `tenzro_listKnowledge` and invoke via
86/// `tenzro_useKnowledge`. The protocol settles TNZO via the same
87/// commission split as the tools registry.
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct KnowledgeRecord {
90 /// Unique resource id (UUID v4).
91 pub knowledge_id: String,
92
93 /// Human-readable name (e.g. "us-equities-bloomberg-feed",
94 /// "openai-text-embedding-3-large", "kbv2-pinecone-rag").
95 pub name: String,
96
97 /// Semantic version.
98 pub version: String,
99
100 /// What kind of resource this is.
101 pub kind: KnowledgeKind,
102
103 /// Invocation endpoint. URL for HTTP-style resources, opaque
104 /// reference for stdio / MCP-tunnelled resources (the underlying
105 /// transport is resolved via the corresponding tool entry when
106 /// the operator co-registers).
107 pub endpoint: String,
108
109 /// Free-text description shown in the discovery surface.
110 pub description: String,
111
112 /// Capability tags for discovery filtering. E.g. `["prices",
113 /// "us-equities", "real-time"]`, `["rag", "legal", "us-case-law"]`,
114 /// `["embeddings", "text", "1024-dim"]`.
115 pub capabilities: Vec<String>,
116
117 /// Free-text category for the discovery UI. E.g. "finance",
118 /// "compliance", "scientific", "code", "ml".
119 pub category: String,
120
121 /// DID of the operator who registered this resource.
122 pub creator_did: Option<String>,
123
124 /// Payout wallet for the creator's share of paid invocations.
125 /// Required when `price_per_call > 0`.
126 pub creator_wallet: Option<Address>,
127
128 /// TNZO atto-token cost per invocation. The protocol takes 5% as
129 /// network commission; the remaining 95% goes to `creator_wallet`.
130 pub price_per_call: u128,
131
132 /// Resource status.
133 pub status: KnowledgeStatus,
134
135 /// Unix timestamp (seconds) at registration.
136 pub created_at: u64,
137
138 /// Monotonic invocation counter.
139 pub invocation_count: u64,
140
141 /// Last liveness heartbeat (seconds).
142 #[serde(default = "default_last_seen")]
143 pub last_seen_at: u64,
144
145 /// JSON-Schema (or schema-like description) of the expected
146 /// invocation params. The discovery surface returns this verbatim
147 /// so the caller (LLM agent or developer) can construct a valid
148 /// invocation without out-of-band docs. Optional — when `None`,
149 /// the caller infers from `kind`.
150 #[serde(default)]
151 pub params_schema: Option<serde_json::Value>,
152
153 /// JSON-Schema (or schema-like description) of the response shape
154 /// returned by an invocation. Same purpose as `params_schema`.
155 #[serde(default)]
156 pub response_schema: Option<serde_json::Value>,
157
158 /// Optional subject-level access list. When `Some(vec)`, only API
159 /// keys whose `subject` appears in `vec` are permitted to invoke
160 /// this resource. `None` means the resource is open to any key
161 /// allowed by `AgentDelegation.allowed_knowledge`.
162 #[serde(default)]
163 pub allowed_to_subjects: Option<Vec<String>>,
164
165 /// When `true`, this knowledge resource is backed by an
166 /// underlying tool/MCP entry (the operator co-registered both).
167 /// The `tool_id` references that tool. At `tenzro_useKnowledge`
168 /// time, the node delegates dispatch through the tool pathway.
169 /// When `false`, the endpoint is invoked directly via HTTP POST.
170 #[serde(default)]
171 pub backing_tool_id: Option<String>,
172}
173
174fn default_last_seen() -> u64 {
175 std::time::SystemTime::now()
176 .duration_since(std::time::UNIX_EPOCH)
177 .unwrap_or_default()
178 .as_secs()
179}
180
181impl KnowledgeRecord {
182 pub fn new(
183 name: String,
184 version: String,
185 kind: KnowledgeKind,
186 endpoint: String,
187 description: String,
188 category: String,
189 ) -> Self {
190 let knowledge_id = uuid::Uuid::new_v4().to_string();
191 let created_at = default_last_seen();
192 Self {
193 knowledge_id,
194 name,
195 version,
196 kind,
197 endpoint,
198 description,
199 capabilities: Vec::new(),
200 category,
201 creator_did: None,
202 creator_wallet: None,
203 price_per_call: 0,
204 status: KnowledgeStatus::Active,
205 created_at,
206 invocation_count: 0,
207 last_seen_at: created_at,
208 params_schema: None,
209 response_schema: None,
210 allowed_to_subjects: None,
211 backing_tool_id: None,
212 }
213 }
214
215 pub fn is_paid(&self) -> bool {
216 self.price_per_call > 0
217 }
218
219 pub fn validate_for_registration(&self) -> Result<(), &'static str> {
220 if self.is_paid() && self.creator_wallet.is_none() {
221 return Err("Paid knowledge resource requires a creator_wallet");
222 }
223 Ok(())
224 }
225
226 pub fn is_available(&self) -> bool {
227 self.status == KnowledgeStatus::Active
228 }
229
230 pub fn touch(&mut self) {
231 self.last_seen_at = default_last_seen();
232 }
233
234 pub fn is_subject_allowed(&self, subject: Option<&str>) -> bool {
235 match (&self.allowed_to_subjects, subject) {
236 (None, _) => true,
237 (Some(list), Some(s)) => list.iter().any(|x| x == s),
238 (Some(_), None) => false,
239 }
240 }
241}
242
243/// Filter shape for list / search RPCs.
244#[derive(Debug, Clone, Default, Serialize, Deserialize)]
245pub struct KnowledgeFilter {
246 pub kind: Option<String>,
247 pub category: Option<String>,
248 pub status: Option<String>,
249 pub creator_did: Option<String>,
250 /// Free-text query — matches name, description, and capabilities.
251 pub query: Option<String>,
252 pub limit: Option<usize>,
253 pub offset: Option<usize>,
254}
255
256/// Result of a knowledge invocation. Same shape as
257/// `ToolInvocationResult` so clients can dispatch on the
258/// invocation_id without knowing which registry served the call.
259#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct KnowledgeInvocationResult {
261 pub knowledge_id: String,
262 pub invocation_id: String,
263 pub output: serde_json::Value,
264 pub settlement_tx: Option<String>,
265 pub amount_paid: u128,
266 pub completed_at: u64,
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn knowledge_record_new() {
275 let r = KnowledgeRecord::new(
276 "test-feed".to_string(),
277 "1.0.0".to_string(),
278 KnowledgeKind::Feed,
279 "https://feed.example.com/api/v1/prices".to_string(),
280 "Live SOL/USD price feed".to_string(),
281 "finance".to_string(),
282 );
283 assert!(!r.knowledge_id.is_empty());
284 assert_eq!(r.kind, KnowledgeKind::Feed);
285 assert!(r.is_available());
286 assert_eq!(r.price_per_call, 0);
287 assert!(!r.is_paid());
288 }
289
290 #[test]
291 fn paid_without_wallet_fails() {
292 let mut r = KnowledgeRecord::new(
293 "p".to_string(),
294 "1".to_string(),
295 KnowledgeKind::VectorIndex,
296 "e".to_string(),
297 "d".to_string(),
298 "c".to_string(),
299 );
300 r.price_per_call = 100;
301 assert!(r.validate_for_registration().is_err());
302 }
303
304 #[test]
305 fn subject_allowlist_enforcement() {
306 let mut r = KnowledgeRecord::new(
307 "n".to_string(),
308 "1".to_string(),
309 KnowledgeKind::Feed,
310 "e".to_string(),
311 "d".to_string(),
312 "c".to_string(),
313 );
314 // No allow-list = open.
315 assert!(r.is_subject_allowed(Some("anyone")));
316 assert!(r.is_subject_allowed(None));
317 // With allow-list = closed to listed subjects only.
318 r.allowed_to_subjects = Some(vec!["alice".to_string()]);
319 assert!(r.is_subject_allowed(Some("alice")));
320 assert!(!r.is_subject_allowed(Some("bob")));
321 assert!(!r.is_subject_allowed(None));
322 }
323
324 #[test]
325 fn kind_str_roundtrip() {
326 for k in [
327 KnowledgeKind::VectorIndex,
328 KnowledgeKind::DocumentCorpus,
329 KnowledgeKind::IndexedDataset,
330 KnowledgeKind::Feed,
331 KnowledgeKind::EmbeddingStore,
332 KnowledgeKind::Other,
333 ] {
334 assert_eq!(KnowledgeKind::parse_str(k.as_str()), Some(k));
335 }
336 assert_eq!(KnowledgeKind::parse_str("nonsense"), None);
337 }
338}