Skip to main content

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}