1#![allow(clippy::useless_conversion)] #[cfg(not(target_arch = "wasm32"))]
29use pyo3::exceptions::{PyPermissionError, PyRuntimeError};
30#[cfg(not(target_arch = "wasm32"))]
31use pyo3::prelude::*;
32
33#[cfg(target_arch = "wasm32")]
34use wasm_bindgen::prelude::*;
35
36#[cfg(not(target_arch = "wasm32"))]
37mod b2a;
38
39#[cfg(not(target_arch = "wasm32"))]
40mod strict_json;
41
42#[cfg(not(target_arch = "wasm32"))]
43mod phase3;
44#[cfg(not(target_arch = "wasm32"))]
45use phase3::SignerBackend;
46
47#[cfg(not(target_arch = "wasm32"))]
48mod events;
49
50pub trait StorageLayer {
52 fn insert_decision(&mut self, payload: &str, rules: &str, genesis: bool) -> Result<(), String>;
53 fn insert_decision_batch(
54 &mut self,
55 decisions: Vec<(&str, &str)>,
56 genesis: bool,
57 ) -> Result<(), String>;
58 fn get_latest_hash(&self) -> Result<String, String>;
59 fn export_ledger(&self) -> Result<String, String>;
60 fn list_decisions(&self, limit: u32, offset: u32) -> Result<String, String>;
61 fn count_decisions(&self) -> Result<u64, String>;
62}
63
64#[cfg(target_arch = "wasm32")]
66pub struct MemoryStorage {
67 records: Vec<String>,
68 latest_hash: String,
69}
70
71#[cfg(target_arch = "wasm32")]
72impl MemoryStorage {
73 pub fn new() -> Self {
74 Self {
75 records: Vec::new(),
76 latest_hash: "GENESIS_HASH_MEM".to_string(),
77 }
78 }
79}
80
81#[cfg(target_arch = "wasm32")]
82impl StorageLayer for MemoryStorage {
83 fn insert_decision(
84 &mut self,
85 payload: &str,
86 _rules: &str,
87 genesis: bool,
88 ) -> Result<(), String> {
89 self.records.push(payload.to_string());
90 if genesis {
91 self.latest_hash = "GENESIS_HASH_MEM".to_string();
92 } else {
93 self.latest_hash = "NEW_HASH_MEM".to_string(); }
95 Ok(())
96 }
97
98 fn insert_decision_batch(
99 &mut self,
100 decisions: Vec<(&str, &str)>,
101 genesis: bool,
102 ) -> Result<(), String> {
103 let mut is_first = true;
104 for (payload, rules) in decisions {
105 self.insert_decision(payload, rules, genesis && is_first)?;
106 is_first = false;
107 }
108 Ok(())
109 }
110
111 fn get_latest_hash(&self) -> Result<String, String> {
112 Ok(self.latest_hash.clone())
113 }
114
115 fn export_ledger(&self) -> Result<String, String> {
116 Ok(format!("[{}]", self.records.join(",")))
118 }
119
120 fn list_decisions(&self, limit: u32, offset: u32) -> Result<String, String> {
121 let start = offset as usize;
122 let end = std::cmp::min(start + limit as usize, self.records.len());
123 if start >= self.records.len() {
124 return Ok("[]".to_string());
125 }
126 Ok(format!("[{}]", self.records[start..end].join(",")))
127 }
128
129 fn count_decisions(&self) -> Result<u64, String> {
130 Ok(self.records.len() as u64)
131 }
132}
133
134#[cfg(not(target_arch = "wasm32"))]
136use rusqlite::Connection;
137
138#[cfg(not(target_arch = "wasm32"))]
139use serde::{Deserialize, Serialize};
140
141#[cfg(not(target_arch = "wasm32"))]
142use sha2::{Digest, Sha256};
143
144#[cfg(not(target_arch = "wasm32"))]
145use ed25519_dalek::Signer;
146
147#[cfg(not(target_arch = "wasm32"))]
148#[derive(Debug, Serialize, Deserialize, Clone)]
149pub struct Decision {
150 id: String,
151 parent_id: String,
152 causal_depth: u64,
153 actor_id: String,
154 timestamp: u64,
155 payload: String,
156 rules_evaluated: String,
157 signature: String,
158}
159
160#[cfg(not(target_arch = "wasm32"))]
161pub struct SqliteStorage {
162 conn: Connection,
163 keyfile: String,
164 cached_signing_key: std::cell::RefCell<Option<ed25519_dalek::SigningKey>>,
165 cached_signer: std::cell::RefCell<Option<phase3::ConfiguredSigner>>,
166}
167
168#[cfg(not(target_arch = "wasm32"))]
169impl SqliteStorage {
170 pub fn new(db_path: String, keyfile: String) -> Result<Self, String> {
171 let conn = Connection::open(&db_path)
172 .map_err(|e| format!("Failed to open SQLite database '{}': {}", db_path, e))?;
173
174 conn.execute_batch(
175 "PRAGMA journal_mode = WAL;
176 PRAGMA synchronous = FULL;
177 PRAGMA busy_timeout = 5000;",
178 )
179 .map_err(|e| e.to_string())?;
180
181 conn.execute(
182 "CREATE TABLE IF NOT EXISTS decisions (
183 id TEXT PRIMARY KEY,
184 parent_id TEXT NOT NULL,
185 causal_depth INTEGER NOT NULL,
186 actor_id TEXT NOT NULL,
187 timestamp INTEGER NOT NULL,
188 payload TEXT NOT NULL,
189 rules_evaluated TEXT NOT NULL,
190 signature TEXT NOT NULL
191 );",
192 [],
193 )
194 .map_err(|e| format!("Failed to create decisions table: {}", e))?;
195
196 conn.execute(
197 "CREATE INDEX IF NOT EXISTS idx_parent_id ON decisions (parent_id);",
198 [],
199 )
200 .map_err(|e| format!("Failed to create parent index: {}", e))?;
201
202 conn.execute(
203 "CREATE INDEX IF NOT EXISTS idx_causal_depth ON decisions (causal_depth);",
204 [],
205 )
206 .map_err(|e| format!("Failed to create causal depth index: {}", e))?;
207
208 b2a::initialize_schema(&conn)?;
209
210 Ok(Self {
211 conn,
212 keyfile,
213 cached_signing_key: std::cell::RefCell::new(None),
214 cached_signer: std::cell::RefCell::new(None),
215 })
216 }
217
218 fn load_signing_key_from_path(keyfile: &str) -> Result<ed25519_dalek::SigningKey, String> {
220 #[derive(Deserialize)]
221 struct KeyPairConfig {
222 #[allow(dead_code)]
223 public_key: String,
224 private_key: String,
225 }
226
227 let content = std::fs::read_to_string(keyfile)
228 .map_err(|e| format!("Failed to read key file '{}': {}", keyfile, e))?;
229 let config: KeyPairConfig = serde_json::from_str(&content)
230 .map_err(|e| format!("Failed to parse key file JSON: {}", e))?;
231
232 let private_key_bytes = hex::decode(&config.private_key)
233 .map_err(|e| format!("Invalid private key hex: {}", e))?;
234 let private_key_array: [u8; 32] = private_key_bytes
235 .try_into()
236 .map_err(|_| "Private key must be exactly 32 bytes".to_string())?;
237
238 Ok(ed25519_dalek::SigningKey::from_bytes(&private_key_array))
239 }
240
241 pub(crate) fn load_signing_key(&self) -> Result<ed25519_dalek::SigningKey, String> {
243 if let Some(ref key) = *self.cached_signing_key.borrow() {
244 return Ok(key.clone());
245 }
246 let key = Self::load_signing_key_from_path(&self.keyfile)?;
247 *self.cached_signing_key.borrow_mut() = Some(key.clone());
248 Ok(key)
249 }
250
251 pub(crate) fn load_signer(&self) -> Result<phase3::ConfiguredSigner, String> {
254 if let Some(ref signer) = *self.cached_signer.borrow() {
255 return Ok(signer.clone());
256 }
257 let signer = phase3::ConfiguredSigner::from_path(&self.keyfile)?;
258 *self.cached_signer.borrow_mut() = Some(signer.clone());
259 Ok(signer)
260 }
261
262 pub fn calculate_canonical_hash(
264 parent_id: &str,
265 actor_id: &str,
266 timestamp: u64,
267 payload: &str,
268 rules_evaluated: &str,
269 ) -> Result<[u8; 32], String> {
270 #[derive(Serialize)]
271 struct CanonicalPayload<'a> {
272 parent_id: &'a str,
273 actor_id: &'a str,
274 timestamp: u64,
275 payload: serde_json::Value,
276 rules_evaluated: serde_json::Value,
277 }
278
279 let payload_val: serde_json::Value =
280 serde_json::from_str(payload).map_err(|e| format!("Invalid payload JSON: {}", e))?;
281 let rules_val: serde_json::Value = serde_json::from_str(rules_evaluated)
282 .map_err(|e| format!("Invalid rules JSON: {}", e))?;
283
284 let canonical = CanonicalPayload {
285 parent_id,
286 actor_id,
287 timestamp,
288 payload: payload_val,
289 rules_evaluated: rules_val,
290 };
291
292 let canonical_bytes = serde_json::to_vec(&canonical).unwrap();
293
294 let mut hasher = Sha256::new();
295 hasher.update(&canonical_bytes);
296 Ok(hasher.finalize().into())
297 }
298
299 fn get_last_decision_conn(conn: &rusqlite::Connection) -> Result<Option<Decision>, String> {
300 let mut stmt = conn.prepare(
301 "SELECT id, parent_id, causal_depth, actor_id, timestamp, payload, rules_evaluated, signature
302 FROM decisions
303 ORDER BY causal_depth DESC, timestamp DESC
304 LIMIT 1"
305 ).map_err(|e| format!("Failed to prepare select statement: {}", e))?;
306
307 let mut rows = stmt
308 .query([])
309 .map_err(|e| format!("Failed to query database: {}", e))?;
310 if let Some(row) = rows
311 .next()
312 .map_err(|e| format!("Error advancing row: {}", e))?
313 {
314 Ok(Some(Decision {
315 id: row.get(0).map_err(|e| e.to_string())?,
316 parent_id: row.get(1).map_err(|e| e.to_string())?,
317 causal_depth: row.get(2).map_err(|e| e.to_string())?,
318 actor_id: row.get(3).map_err(|e| e.to_string())?,
319 timestamp: row.get(4).map_err(|e| e.to_string())?,
320 payload: row.get(5).map_err(|e| e.to_string())?,
321 rules_evaluated: row.get(6).map_err(|e| e.to_string())?,
322 signature: row.get(7).map_err(|e| e.to_string())?,
323 }))
324 } else {
325 Ok(None)
326 }
327 }
328
329 #[allow(dead_code)]
330 fn get_last_decision(&self) -> Result<Option<Decision>, String> {
331 Self::get_last_decision_conn(&self.conn)
332 }
333
334 pub fn validate_ledger(&self) -> Result<String, String> {
335 let mut stmt = self.conn.prepare(
336 "SELECT id, parent_id, causal_depth, actor_id, timestamp, payload, rules_evaluated, signature
337 FROM decisions
338 ORDER BY causal_depth ASC, timestamp ASC"
339 ).map_err(|e| format!("Failed to prepare select statement: {}", e))?;
340
341 let rows = stmt
342 .query_map([], |row| {
343 Ok(Decision {
344 id: row.get(0)?,
345 parent_id: row.get(1)?,
346 causal_depth: row.get(2)?,
347 actor_id: row.get(3)?,
348 timestamp: row.get(4)?,
349 payload: row.get(5)?,
350 rules_evaluated: row.get(6)?,
351 signature: row.get(7)?,
352 })
353 })
354 .map_err(|e| format!("Failed to execute query: {}", e))?;
355
356 let mut decisions = Vec::new();
357 for r in rows {
358 match r {
359 Ok(d) => decisions.push(d),
360 Err(e) => return Err(format!("Error reading record: {}", e)),
361 }
362 }
363
364 if decisions.is_empty() {
365 return Ok(serde_json::to_string(&serde_json::json!({
366 "status": "valid",
367 "message": "Database is empty.",
368 "total_records": 0
369 }))
370 .unwrap());
371 }
372
373 use std::collections::HashMap;
374 let mut decision_map = HashMap::new();
375 let mut genesis_count = 0;
376 for d in &decisions {
377 decision_map.insert(d.id.clone(), d.clone());
378 if d.parent_id == "genesis" {
379 genesis_count += 1;
380 }
381 }
382
383 let mut errors = Vec::new();
384
385 if !decisions.is_empty() && genesis_count == 0 {
386 errors.push("No genesis decision found in the ledger.".to_string());
387 } else if genesis_count > 1 {
388 errors.push(format!(
389 "Multiple genesis decisions found ({}). Only one is allowed.",
390 genesis_count
391 ));
392 }
393
394 for d in &decisions {
395 let computed_hash = match Self::calculate_canonical_hash(
396 &d.parent_id,
397 &d.actor_id,
398 d.timestamp,
399 &d.payload,
400 &d.rules_evaluated,
401 ) {
402 Ok(h) => h,
403 Err(e) => {
404 errors.push(format!("Decision '{}' hash calculation error: {}", d.id, e));
405 continue;
406 }
407 };
408 let computed_id = hex::encode(computed_hash);
409 if computed_id != d.id {
410 errors.push(format!(
411 "Decision '{}' has invalid hash. Computed: '{}', Recorded: '{}'",
412 d.id, computed_id, d.id
413 ));
414 continue;
415 }
416
417 use ed25519_dalek::{Signature, Verifier, VerifyingKey};
418 let pub_bytes = match hex::decode(&d.actor_id) {
419 Ok(b) => b,
420 Err(e) => {
421 errors.push(format!("Invalid actor_id: {}", e));
422 continue;
423 }
424 };
425 let pub_array: [u8; 32] = match pub_bytes.try_into() {
426 Ok(a) => a,
427 Err(_) => {
428 errors.push("Invalid public key size".to_string());
429 continue;
430 }
431 };
432 let verifying_key = match VerifyingKey::from_bytes(&pub_array) {
433 Ok(k) => k,
434 Err(e) => {
435 errors.push(format!("Invalid public key: {}", e));
436 continue;
437 }
438 };
439
440 let sig_bytes = match hex::decode(&d.signature) {
441 Ok(b) => b,
442 Err(e) => {
443 errors.push(format!("Invalid signature hex: {}", e));
444 continue;
445 }
446 };
447 let sig_array: [u8; 64] = match sig_bytes.try_into() {
448 Ok(a) => a,
449 Err(_) => {
450 errors.push("Invalid signature size".to_string());
451 continue;
452 }
453 };
454 let signature = Signature::from_bytes(&sig_array);
455
456 let id_bytes = match hex::decode(&d.id) {
457 Ok(b) => b,
458 Err(e) => {
459 errors.push(format!("Invalid id hex: {}", e));
460 continue;
461 }
462 };
463
464 if let Err(e) = verifying_key.verify(&id_bytes, &signature) {
465 errors.push(format!(
466 "Decision '{}' signature verification failed: {}",
467 d.id, e
468 ));
469 continue;
470 }
471
472 if d.parent_id != "genesis" {
473 match decision_map.get(&d.parent_id) {
474 Some(parent) => {
475 if d.causal_depth != parent.causal_depth + 1 {
476 errors.push(format!(
477 "Decision '{}' causal depth mismatch. Expected '{}', found '{}'",
478 d.id,
479 parent.causal_depth + 1,
480 d.causal_depth
481 ));
482 }
483 if d.timestamp < parent.timestamp {
484 errors.push(format!(
485 "Decision '{}' temporal anomaly. Precedes parent '{}'",
486 d.id, d.parent_id
487 ));
488 }
489 }
490 None => {
491 errors.push(format!(
492 "Decision '{}' orphan node. Parent '{}' missing",
493 d.id, d.parent_id
494 ));
495 }
496 }
497 } else {
498 if d.causal_depth != 0 {
499 errors.push(format!(
500 "Genesis decision '{}' must have causal_depth 0",
501 d.id
502 ));
503 }
504 }
505 }
506
507 let mut warnings = Vec::new();
509 let mut unique_actors: std::collections::HashSet<String> = std::collections::HashSet::new();
510 for d in &decisions {
511 unique_actors.insert(d.actor_id.clone());
512 }
513 for actor in &unique_actors {
514 let registered: bool = self
515 .conn
516 .prepare("SELECT 1 FROM agents WHERE public_key = ?1 LIMIT 1")
517 .and_then(|mut s| s.exists([actor]))
518 .unwrap_or(false);
519 if !registered {
520 warnings.push(format!(
521 "Actor '{}' is not registered in the agents table.",
522 actor
523 ));
524 }
525 }
526
527 if errors.is_empty() {
528 let mut result = serde_json::json!({
529 "status": "valid",
530 "message": "All decisions verified successfully.",
531 "total_records": decisions.len(),
532 "unique_actors": unique_actors.len()
533 });
534 if !warnings.is_empty() {
535 result["warnings"] = serde_json::json!(warnings);
536 }
537 Ok(serde_json::to_string(&result).unwrap())
538 } else {
539 let mut result = serde_json::json!({
540 "status": "invalid",
541 "errors": errors,
542 "total_records": decisions.len()
543 });
544 if !warnings.is_empty() {
545 result["warnings"] = serde_json::json!(warnings);
546 }
547 Err(serde_json::to_string(&result).unwrap())
548 }
549 }
550
551 pub fn register_agent(
556 &self,
557 public_key: &str,
558 alias: &str,
559 metadata: &str,
560 ) -> Result<String, String> {
561 b2a::register_agent(self, public_key, alias, metadata)
562 }
563
564 pub fn verify_agent(&self, public_key: &str) -> Result<bool, String> {
566 b2a::verify_agent(self, public_key)
567 }
568
569 pub fn list_agents(&self) -> Result<String, String> {
571 b2a::list_agents(self)
572 }
573
574 pub fn get_agent(&self, public_key: &str) -> Result<String, String> {
576 b2a::get_agent(self, public_key)
577 }
578
579 pub fn rotate_agent(
581 &self,
582 current_public_key: &str,
583 new_public_key: &str,
584 ) -> Result<String, String> {
585 b2a::rotate_agent(self, current_public_key, new_public_key)
586 }
587
588 pub fn revoke_agent(&self, public_key: &str, reason: &str) -> Result<String, String> {
590 b2a::revoke_agent(self, public_key, reason)
591 }
592
593 pub fn list_identity_events(&self) -> Result<String, String> {
595 b2a::list_identity_events(self)
596 }
597
598 pub fn request_action(
600 &self,
601 intent: &str,
602 agent_keyfile: &str,
603 ttl_seconds: u64,
604 ) -> Result<String, String> {
605 b2a::request_action(self, intent, agent_keyfile, ttl_seconds)
606 }
607
608 pub fn request_action_signed(
610 &self,
611 intent: &str,
612 agent_id: &str,
613 agent_signature: &str,
614 ttl_seconds: u64,
615 ) -> Result<String, String> {
616 b2a::request_action_signed(self, intent, agent_id, agent_signature, ttl_seconds)
617 }
618
619 pub fn commit_outcome(
621 &self,
622 authorization_id: &str,
623 outcome: &str,
624 executor_keyfile: &str,
625 ) -> Result<String, String> {
626 b2a::commit_outcome(self, authorization_id, outcome, executor_keyfile)
627 }
628
629 pub fn commit_outcome_signed(
631 &self,
632 authorization_id: &str,
633 outcome: &str,
634 ) -> Result<String, String> {
635 b2a::commit_outcome_signed(self, authorization_id, outcome)
636 }
637
638 pub fn get_trace(&self, action_id: &str) -> Result<String, String> {
640 b2a::get_trace(self, action_id)
641 }
642
643 pub fn verify_trace(&self, action_id: &str) -> Result<String, String> {
645 b2a::verify_trace(self, action_id)
646 }
647
648 pub fn verify_trace_pinned(
652 &self,
653 action_id: &str,
654 gate_public_key: &str,
655 executor_public_keys: &[String],
656 ) -> Result<String, String> {
657 b2a::verify_trace_pinned(self, action_id, gate_public_key, executor_public_keys)
658 }
659
660 pub fn export_bundle(
663 &self,
664 action_id: &str,
665 max_stream_events: Option<u64>,
666 ) -> Result<String, String> {
667 b2a::export_bundle(self, action_id, max_stream_events)
668 }
669
670 pub fn install_policy(&self, policy_spec: &str) -> Result<String, String> {
672 let spec = strict_json::parse_labeled(policy_spec, "policy")?;
673 strict_json::reject_inexact_numbers(&spec, "policy")?;
674 let signer = self.load_signer()?;
675 let bundle = phase3::install_policy(&self.conn, &signer, &spec, b2a::now_micros()?)?;
676 b2a::canonicalize(&bundle)
677 }
678
679 pub fn install_allow_all_dev_policy(&self) -> Result<String, String> {
686 let signer = self.load_signer()?;
687 let bundle = phase3::install_allow_all_dev_policy(&self.conn, &signer, b2a::now_micros()?)?;
688 b2a::canonicalize(&bundle)
689 }
690
691 pub fn list_policies(&self) -> Result<String, String> {
693 phase3::list_policies(&self.conn)
694 }
695
696 pub fn signer_conformance(&self) -> Result<String, String> {
698 phase3::signer_conformance(&self.load_signer()?)
699 }
700
701 pub fn create_checkpoint(&self, tenant_id: &str) -> Result<String, String> {
702 let signer = self.load_signer()?;
703 let created_at = b2a::now_micros()?;
704 events::create_checkpoint(&self.conn, &signer, tenant_id, created_at)
705 }
706
707 pub fn export_event_stream(
708 &self,
709 tenant_id: &str,
710 from_sequence: u64,
711 limit: u32,
712 ) -> Result<String, String> {
713 events::export_event_stream(&self.conn, tenant_id, from_sequence, limit)
714 }
715
716 pub fn verify_checkpoint_stream(
717 &self,
718 checkpoint_json: &str,
719 stream_json: &str,
720 expected_public_key: Option<&str>,
721 ) -> Result<String, String> {
722 let expected = match expected_public_key {
723 Some(pk) => Some(pk.to_string()),
724 None => self
725 .load_signer()
726 .ok()
727 .map(|s| s.identity().public_key.clone())
728 .or_else(|| {
729 self.conn
730 .query_row(
731 "SELECT public_key FROM trusted_roots WHERE id = 1",
732 [],
733 |r| r.get(0),
734 )
735 .ok()
736 }),
737 };
738 events::verify_checkpoint_stream(checkpoint_json, stream_json, expected.as_deref())
739 }
740}
741
742#[cfg(not(target_arch = "wasm32"))]
743impl StorageLayer for SqliteStorage {
744 fn insert_decision(&mut self, payload: &str, rules: &str, genesis: bool) -> Result<(), String> {
745 serde_json::from_str::<serde_json::Value>(payload)
747 .map_err(|e| format!("Payload is not valid JSON: {}", e))?;
748 serde_json::from_str::<serde_json::Value>(rules)
749 .map_err(|e| format!("Rules is not valid JSON: {}", e))?;
750
751 let signing_key = self.load_signing_key()?;
753 let verifying_key = signing_key.verifying_key();
754 let actor_id = hex::encode(verifying_key.to_bytes());
755
756 let tx = self
757 .conn
758 .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
759 .map_err(|e| format!("Failed to begin transaction: {}", e))?;
760
761 let (parent_id, causal_depth) = match Self::get_last_decision_conn(&tx)? {
763 Some(last_d) => {
764 if genesis {
765 ("genesis".to_string(), 0u64)
767 } else {
768 (last_d.id, last_d.causal_depth + 1)
769 }
770 }
771 None => {
772 if genesis {
773 ("genesis".to_string(), 0u64)
774 } else {
775 return Err(
776 "Database is empty. Use genesis=true to record the first decision."
777 .to_string(),
778 );
779 }
780 }
781 };
782
783 if parent_id == "genesis" {
784 let mut stmt = tx
785 .prepare("SELECT 1 FROM decisions WHERE parent_id = 'genesis' LIMIT 1")
786 .map_err(|e| format!("Prepare error: {}", e))?;
787 let exists = stmt.exists([]).map_err(|e| format!("Query error: {}", e))?;
788 if exists {
789 return Err("A genesis decision already exists in the database.".to_string());
790 }
791 }
792
793 let timestamp = std::time::SystemTime::now()
795 .duration_since(std::time::SystemTime::UNIX_EPOCH)
796 .map_err(|e| format!("System time error: {}", e))?
797 .as_micros() as u64;
798
799 let hash_bytes =
801 Self::calculate_canonical_hash(&parent_id, &actor_id, timestamp, payload, rules)?;
802 let id = hex::encode(hash_bytes);
803
804 let signature_struct = signing_key.sign(&hash_bytes);
806 let signature = hex::encode(signature_struct.to_bytes());
807
808 tx.execute(
810 "INSERT INTO decisions (id, parent_id, causal_depth, actor_id, timestamp, payload, rules_evaluated, signature)
811 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
812 (
813 &id,
814 &parent_id,
815 causal_depth,
816 &actor_id,
817 timestamp,
818 payload,
819 rules,
820 &signature,
821 ),
822 ).map_err(|e| format!("Failed to insert decision into SQLite: {}", e))?;
823
824 tx.commit()
825 .map_err(|e| format!("Failed to commit transaction: {}", e))?;
826
827 Ok(())
828 }
829
830 fn insert_decision_batch(
831 &mut self,
832 decisions: Vec<(&str, &str)>,
833 genesis: bool,
834 ) -> Result<(), String> {
835 if decisions.is_empty() {
836 return Ok(());
837 }
838
839 let signing_key = self.load_signing_key()?;
840 let verifying_key = signing_key.verifying_key();
841 let actor_id = hex::encode(verifying_key.to_bytes());
842
843 let tx = self
844 .conn
845 .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
846 .map_err(|e| format!("Failed to begin transaction: {}", e))?;
847
848 let mut current_parent_id;
849 let mut current_causal_depth;
850
851 match Self::get_last_decision_conn(&tx)? {
852 Some(last_d) => {
853 if genesis {
854 current_parent_id = "genesis".to_string();
855 current_causal_depth = 0u64;
856 } else {
857 current_parent_id = last_d.id;
858 current_causal_depth = last_d.causal_depth + 1;
859 }
860 }
861 None => {
862 if genesis {
863 current_parent_id = "genesis".to_string();
864 current_causal_depth = 0u64;
865 } else {
866 return Err(
867 "Database is empty. Use genesis=true to record the first decision."
868 .to_string(),
869 );
870 }
871 }
872 };
873
874 if current_parent_id == "genesis" {
875 let mut stmt = tx
876 .prepare("SELECT 1 FROM decisions WHERE parent_id = 'genesis' LIMIT 1")
877 .map_err(|e| format!("Prepare error: {}", e))?;
878 let exists = stmt.exists([]).map_err(|e| format!("Query error: {}", e))?;
879 if exists {
880 return Err("A genesis decision already exists in the database.".to_string());
881 }
882 }
883
884 for (payload, rules) in decisions {
885 serde_json::from_str::<serde_json::Value>(payload)
886 .map_err(|e| format!("Payload is not valid JSON: {}", e))?;
887 serde_json::from_str::<serde_json::Value>(rules)
888 .map_err(|e| format!("Rules is not valid JSON: {}", e))?;
889
890 let timestamp = std::time::SystemTime::now()
891 .duration_since(std::time::SystemTime::UNIX_EPOCH)
892 .map_err(|e| format!("System time error: {}", e))?
893 .as_micros() as u64;
894
895 let hash_bytes = Self::calculate_canonical_hash(
896 ¤t_parent_id,
897 &actor_id,
898 timestamp,
899 payload,
900 rules,
901 )?;
902 let id = hex::encode(hash_bytes);
903
904 let signature_struct = signing_key.sign(&hash_bytes);
905 let signature = hex::encode(signature_struct.to_bytes());
906
907 tx.execute(
908 "INSERT INTO decisions (id, parent_id, causal_depth, actor_id, timestamp, payload, rules_evaluated, signature)
909 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
910 (
911 &id,
912 ¤t_parent_id,
913 current_causal_depth,
914 &actor_id,
915 timestamp,
916 payload,
917 rules,
918 &signature,
919 ),
920 ).map_err(|e| format!("Failed to insert decision into SQLite: {}", e))?;
921
922 current_parent_id = id;
923 current_causal_depth += 1;
924 }
925
926 tx.commit()
927 .map_err(|e| format!("Failed to commit transaction: {}", e))?;
928
929 Ok(())
930 }
931
932 fn get_latest_hash(&self) -> Result<String, String> {
933 let mut stmt = self
934 .conn
935 .prepare("SELECT id FROM decisions ORDER BY causal_depth DESC, timestamp DESC LIMIT 1")
936 .map_err(|e| format!("Failed to prepare select statement: {}", e))?;
937
938 let mut rows = stmt
939 .query([])
940 .map_err(|e| format!("Failed to query database: {}", e))?;
941 if let Some(row) = rows
942 .next()
943 .map_err(|e| format!("Error advancing row: {}", e))?
944 {
945 let id: String = row.get(0).unwrap();
946 Ok(id)
947 } else {
948 Ok("GENESIS".to_string())
949 }
950 }
951
952 fn export_ledger(&self) -> Result<String, String> {
953 let mut stmt = self.conn.prepare(
954 "SELECT id, parent_id, causal_depth, actor_id, timestamp, payload, rules_evaluated, signature
955 FROM decisions
956 ORDER BY causal_depth ASC, timestamp ASC"
957 ).map_err(|e| format!("Failed to prepare select statement: {}", e))?;
958
959 let rows = stmt
960 .query_map([], |row| {
961 Ok(Decision {
962 id: row.get(0)?,
963 parent_id: row.get(1)?,
964 causal_depth: row.get(2)?,
965 actor_id: row.get(3)?,
966 timestamp: row.get(4)?,
967 payload: row.get(5)?,
968 rules_evaluated: row.get(6)?,
969 signature: row.get(7)?,
970 })
971 })
972 .map_err(|e| format!("Failed to execute query: {}", e))?;
973
974 let mut decisions = Vec::new();
975 for r in rows {
976 match r {
977 Ok(d) => decisions.push(d),
978 Err(e) => return Err(format!("Error reading record: {}", e)),
979 }
980 }
981
982 serde_json::to_string(&decisions)
983 .map_err(|e| format!("Failed to serialize ledger to JSON: {}", e))
984 }
985
986 fn list_decisions(&self, limit: u32, offset: u32) -> Result<String, String> {
987 let mut stmt = self.conn.prepare(
988 "SELECT id, parent_id, causal_depth, actor_id, timestamp, payload, rules_evaluated, signature
989 FROM decisions
990 ORDER BY causal_depth DESC, timestamp DESC
991 LIMIT ?1 OFFSET ?2"
992 ).map_err(|e| format!("Failed to prepare select statement: {}", e))?;
993
994 let rows = stmt
995 .query_map([limit, offset], |row| {
996 Ok(Decision {
997 id: row.get(0)?,
998 parent_id: row.get(1)?,
999 causal_depth: row.get(2)?,
1000 actor_id: row.get(3)?,
1001 timestamp: row.get(4)?,
1002 payload: row.get(5)?,
1003 rules_evaluated: row.get(6)?,
1004 signature: row.get(7)?,
1005 })
1006 })
1007 .map_err(|e| format!("Failed to execute query: {}", e))?;
1008
1009 let mut decisions = Vec::new();
1010 for r in rows {
1011 match r {
1012 Ok(d) => decisions.push(d),
1013 Err(e) => return Err(format!("Error reading record: {}", e)),
1014 }
1015 }
1016
1017 serde_json::to_string(&decisions)
1018 .map_err(|e| format!("Failed to serialize decisions to JSON: {}", e))
1019 }
1020
1021 fn count_decisions(&self) -> Result<u64, String> {
1022 let mut stmt = self
1023 .conn
1024 .prepare("SELECT COUNT(*) FROM decisions")
1025 .map_err(|e| format!("Failed to prepare count statement: {}", e))?;
1026 let count: u64 = stmt
1027 .query_row([], |row| row.get(0))
1028 .map_err(|e| format!("Failed to count decisions: {}", e))?;
1029 Ok(count)
1030 }
1031}
1032#[cfg(not(target_arch = "wasm32"))]
1034#[pyclass(unsendable)]
1035pub struct TempusDDB {
1036 storage: SqliteStorage,
1037 #[allow(dead_code)]
1038 keyfile: String,
1039}
1040
1041#[cfg(not(target_arch = "wasm32"))]
1042#[allow(clippy::useless_conversion)]
1043#[pymethods]
1044impl TempusDDB {
1045 #[new]
1046 fn new(db_path: String, keyfile: String) -> PyResult<Self> {
1047 let storage =
1048 SqliteStorage::new(db_path, keyfile.clone()).map_err(PyPermissionError::new_err)?;
1049
1050 if std::path::Path::new(&keyfile).exists() {
1051 storage.load_signer().map_err(PyPermissionError::new_err)?;
1052 }
1053
1054 Ok(TempusDDB { storage, keyfile })
1055 }
1056
1057 #[allow(unused_variables)]
1058 #[pyo3(signature = (payload, rules, genesis=false))]
1059 fn record(&mut self, payload: &str, rules: &str, genesis: bool) -> PyResult<String> {
1060 self.storage
1061 .insert_decision(payload, rules, genesis)
1062 .map_err(PyPermissionError::new_err)?;
1063
1064 let result_json = format!(
1065 r#"{{"status": "success", "action": "recorded", "latest_hash": "{}"}}"#,
1066 self.storage.get_latest_hash().unwrap_or_default()
1067 );
1068 Ok(result_json)
1069 }
1070
1071 #[allow(unused_variables)]
1072 #[pyo3(signature = (decisions, genesis=false))]
1073 fn record_batch(
1074 &mut self,
1075 decisions: Vec<(String, String)>,
1076 genesis: bool,
1077 ) -> PyResult<String> {
1078 let refs: Vec<(&str, &str)> = decisions
1079 .iter()
1080 .map(|(p, r)| (p.as_str(), r.as_str()))
1081 .collect();
1082 self.storage
1083 .insert_decision_batch(refs, genesis)
1084 .map_err(PyPermissionError::new_err)?;
1085
1086 let result_json = format!(
1087 r#"{{"status": "success", "action": "recorded_batch", "latest_hash": "{}"}}"#,
1088 self.storage.get_latest_hash().unwrap_or_default()
1089 );
1090 Ok(result_json)
1091 }
1092
1093 #[pyo3(signature = ())]
1094 fn validate(&self) -> PyResult<String> {
1095 self.storage
1096 .validate_ledger()
1097 .map_err(PyRuntimeError::new_err)
1098 }
1099
1100 #[pyo3(signature = ())]
1101 fn export(&self) -> PyResult<String> {
1102 self.storage
1103 .export_ledger()
1104 .map_err(PyRuntimeError::new_err)
1105 }
1106
1107 #[pyo3(signature = (limit=10, offset=0))]
1108 fn list(&self, limit: u32, offset: u32) -> PyResult<String> {
1109 self.storage
1110 .list_decisions(limit, offset)
1111 .map_err(PyRuntimeError::new_err)
1112 }
1113
1114 #[pyo3(signature = ())]
1115 fn count(&self) -> PyResult<u64> {
1116 self.storage
1117 .count_decisions()
1118 .map_err(PyRuntimeError::new_err)
1119 }
1120
1121 #[pyo3(signature = (public_key, alias, metadata="{}".to_string()))]
1124 fn register_agent(&self, public_key: &str, alias: &str, metadata: String) -> PyResult<String> {
1125 self.storage
1126 .register_agent(public_key, alias, &metadata)
1127 .map_err(PyRuntimeError::new_err)
1128 }
1129
1130 #[pyo3(signature = ())]
1131 fn list_agents(&self) -> PyResult<String> {
1132 self.storage.list_agents().map_err(PyRuntimeError::new_err)
1133 }
1134
1135 #[pyo3(signature = (public_key,))]
1136 fn get_agent(&self, public_key: &str) -> PyResult<String> {
1137 self.storage
1138 .get_agent(public_key)
1139 .map_err(PyRuntimeError::new_err)
1140 }
1141
1142 #[pyo3(signature = (current_public_key, new_public_key))]
1143 fn rotate_agent(&self, current_public_key: &str, new_public_key: &str) -> PyResult<String> {
1144 self.storage
1145 .rotate_agent(current_public_key, new_public_key)
1146 .map_err(PyRuntimeError::new_err)
1147 }
1148
1149 #[pyo3(signature = (public_key, reason))]
1150 fn revoke_agent(&self, public_key: &str, reason: &str) -> PyResult<String> {
1151 self.storage
1152 .revoke_agent(public_key, reason)
1153 .map_err(PyRuntimeError::new_err)
1154 }
1155
1156 #[pyo3(signature = ())]
1157 fn list_identity_events(&self) -> PyResult<String> {
1158 self.storage
1159 .list_identity_events()
1160 .map_err(PyRuntimeError::new_err)
1161 }
1162
1163 #[pyo3(signature = (public_key,))]
1164 fn verify_agent(&self, public_key: &str) -> PyResult<bool> {
1165 self.storage
1166 .verify_agent(public_key)
1167 .map_err(PyRuntimeError::new_err)
1168 }
1169
1170 #[pyo3(signature = (intent, agent_keyfile, ttl_seconds=60))]
1171 fn request_action(
1172 &self,
1173 intent: &str,
1174 agent_keyfile: &str,
1175 ttl_seconds: u64,
1176 ) -> PyResult<String> {
1177 self.storage
1178 .request_action(intent, agent_keyfile, ttl_seconds)
1179 .map_err(PyRuntimeError::new_err)
1180 }
1181
1182 #[pyo3(signature = (intent, agent_id, agent_signature, ttl_seconds=60))]
1183 fn request_action_signed(
1184 &self,
1185 intent: &str,
1186 agent_id: &str,
1187 agent_signature: &str,
1188 ttl_seconds: u64,
1189 ) -> PyResult<String> {
1190 self.storage
1191 .request_action_signed(intent, agent_id, agent_signature, ttl_seconds)
1192 .map_err(PyRuntimeError::new_err)
1193 }
1194
1195 #[pyo3(signature = (authorization_id, outcome, executor_keyfile))]
1196 fn commit_outcome(
1197 &self,
1198 authorization_id: &str,
1199 outcome: &str,
1200 executor_keyfile: &str,
1201 ) -> PyResult<String> {
1202 self.storage
1203 .commit_outcome(authorization_id, outcome, executor_keyfile)
1204 .map_err(PyRuntimeError::new_err)
1205 }
1206
1207 #[pyo3(signature = (authorization_id, outcome))]
1208 fn commit_outcome_signed(&self, authorization_id: &str, outcome: &str) -> PyResult<String> {
1209 self.storage
1210 .commit_outcome_signed(authorization_id, outcome)
1211 .map_err(PyRuntimeError::new_err)
1212 }
1213
1214 #[pyo3(signature = (action_id,))]
1215 fn get_trace(&self, action_id: &str) -> PyResult<String> {
1216 self.storage
1217 .get_trace(action_id)
1218 .map_err(PyRuntimeError::new_err)
1219 }
1220
1221 #[pyo3(signature = (action_id, gate_public_key=None, executor_public_keys=None))]
1225 fn verify_trace(
1226 &self,
1227 action_id: &str,
1228 gate_public_key: Option<&str>,
1229 executor_public_keys: Option<Vec<String>>,
1230 ) -> PyResult<String> {
1231 match gate_public_key {
1232 Some(gate) => self.storage.verify_trace_pinned(
1233 action_id,
1234 gate,
1235 &executor_public_keys.unwrap_or_default(),
1236 ),
1237 None if executor_public_keys.is_some() => {
1238 Err("executor_public_keys requires gate_public_key".to_string())
1239 }
1240 None => self.storage.verify_trace(action_id),
1241 }
1242 .map_err(PyRuntimeError::new_err)
1243 }
1244
1245 #[pyo3(signature = (action_id, max_stream_events=None))]
1247 fn export_bundle(&self, action_id: &str, max_stream_events: Option<u64>) -> PyResult<String> {
1248 self.storage
1249 .export_bundle(action_id, max_stream_events)
1250 .map_err(PyRuntimeError::new_err)
1251 }
1252
1253 #[pyo3(signature = (policy_spec,))]
1254 fn install_policy(&self, policy_spec: &str) -> PyResult<String> {
1255 self.storage
1256 .install_policy(policy_spec)
1257 .map_err(PyRuntimeError::new_err)
1258 }
1259
1260 #[pyo3(signature = ())]
1262 fn install_allow_all_dev_policy(&self, py: Python<'_>) -> PyResult<String> {
1263 let bundle = self
1264 .storage
1265 .install_allow_all_dev_policy()
1266 .map_err(PyRuntimeError::new_err)?;
1267 PyErr::warn(
1268 py,
1269 &py.get_type::<pyo3::exceptions::PyUserWarning>(),
1270 c"tempus: allow-all development policy installed; every action is ALLOWED. \
1271 Do not use in production: install a real policy with install_policy().",
1272 1,
1273 )?;
1274 Ok(bundle)
1275 }
1276
1277 #[pyo3(signature = ())]
1278 fn list_policies(&self) -> PyResult<String> {
1279 self.storage
1280 .list_policies()
1281 .map_err(PyRuntimeError::new_err)
1282 }
1283
1284 #[pyo3(signature = ())]
1285 fn signer_conformance(&self) -> PyResult<String> {
1286 self.storage
1287 .signer_conformance()
1288 .map_err(PyRuntimeError::new_err)
1289 }
1290
1291 #[pyo3(signature = ())]
1293 fn whoami(&self) -> PyResult<String> {
1294 let signer = self
1295 .storage
1296 .load_signer()
1297 .map_err(PyRuntimeError::new_err)?;
1298 use crate::phase3::SignerBackend;
1299 let identity = signer.identity();
1300 let public_key = identity.public_key.clone();
1301
1302 let alias = match self.storage.get_agent(&public_key) {
1304 Ok(json_str) => serde_json::from_str::<serde_json::Value>(&json_str)
1305 .ok()
1306 .and_then(|v| v["alias"].as_str().map(|s| s.to_string()))
1307 .unwrap_or_default(),
1308 Err(_) => String::new(),
1309 };
1310
1311 Ok(serde_json::to_string(&serde_json::json!({
1312 "public_key": public_key,
1313 "alias": alias,
1314 "keyfile": self.keyfile,
1315 "signer_uri": identity.signer_uri,
1316 "key_version": identity.key_version,
1317 "algorithm": identity.algorithm,
1318 }))
1319 .unwrap())
1320 }
1321
1322 #[pyo3(signature = (tenant_id="*".to_string()))]
1323 fn create_checkpoint(&self, tenant_id: String) -> PyResult<String> {
1324 self.storage
1325 .create_checkpoint(&tenant_id)
1326 .map_err(PyRuntimeError::new_err)
1327 }
1328
1329 #[pyo3(signature = (tenant_id="*".to_string(), from_sequence=1, limit=1000))]
1330 fn export_event_stream(
1331 &self,
1332 tenant_id: String,
1333 from_sequence: u64,
1334 limit: u32,
1335 ) -> PyResult<String> {
1336 self.storage
1337 .export_event_stream(&tenant_id, from_sequence, limit)
1338 .map_err(PyRuntimeError::new_err)
1339 }
1340
1341 #[pyo3(signature = (checkpoint_json, stream_json, expected_public_key=None))]
1342 fn verify_checkpoint_stream(
1343 &self,
1344 checkpoint_json: &str,
1345 stream_json: &str,
1346 expected_public_key: Option<&str>,
1347 ) -> PyResult<String> {
1348 self.storage
1349 .verify_checkpoint_stream(checkpoint_json, stream_json, expected_public_key)
1350 .map_err(PyRuntimeError::new_err)
1351 }
1352}
1353
1354#[cfg(not(target_arch = "wasm32"))]
1357pub fn verify_receipt(
1358 receipt_json: &str,
1359 gate_public_key: &str,
1360 executor_public_keys: &[String],
1361) -> Result<String, String> {
1362 b2a::verify_receipt_pinned(receipt_json, gate_public_key, executor_public_keys)
1363}
1364
1365#[cfg(not(target_arch = "wasm32"))]
1367pub fn verify_bundle(
1368 bundle_json: &str,
1369 gate_public_key: &str,
1370 executor_public_keys: &[String],
1371) -> Result<String, String> {
1372 b2a::verify_bundle_pinned(bundle_json, gate_public_key, executor_public_keys)
1373}
1374
1375#[cfg(not(target_arch = "wasm32"))]
1378pub fn verify_checkpoint(
1379 checkpoint_json: &str,
1380 stream_json: &str,
1381 expected_public_key: &str,
1382) -> Result<String, String> {
1383 let expected = expected_public_key.trim().to_ascii_lowercase();
1384 b2a::decode_public_key(&expected).map_err(|e| format!("gate public key: {e}"))?;
1385 events::verify_checkpoint_stream(checkpoint_json, stream_json, Some(&expected))
1386}
1387
1388#[cfg(not(target_arch = "wasm32"))]
1389pub fn generate_keypair(output: &str) -> Result<String, String> {
1390 use ed25519_dalek::SigningKey;
1391 use rand::rngs::OsRng;
1392 use std::fs::File;
1393 use std::io::Write;
1394
1395 #[derive(Serialize)]
1396 struct KeyPairConfig {
1397 public_key: String,
1398 private_key: String,
1399 }
1400
1401 let mut csprng = OsRng;
1402 let signing_key = SigningKey::generate(&mut csprng);
1403 let verifying_key = signing_key.verifying_key();
1404
1405 let config = KeyPairConfig {
1406 public_key: hex::encode(verifying_key.to_bytes()),
1407 private_key: hex::encode(signing_key.to_bytes()),
1408 };
1409
1410 let json_str =
1411 serde_json::to_string_pretty(&config).map_err(|e| format!("Serialization error: {}", e))?;
1412
1413 let mut file =
1414 File::create(output).map_err(|e| format!("Error creating key file {}: {}", output, e))?;
1415
1416 file.write_all(json_str.as_bytes())
1417 .map_err(|e| format!("Error writing key file {}: {}", output, e))?;
1418
1419 #[cfg(unix)]
1420 {
1421 use std::os::unix::fs::PermissionsExt;
1422 let perms = std::fs::Permissions::from_mode(0o600);
1423 if let Err(e) = std::fs::set_permissions(output, perms) {
1424 eprintln!("Warning: Could not set key file permissions: {}", e);
1425 }
1426 }
1427
1428 Ok(json_str)
1429}
1430
1431#[cfg(not(target_arch = "wasm32"))]
1432#[pyfunction]
1433#[allow(clippy::useless_conversion)]
1434pub fn gen_keys(output: String) -> PyResult<String> {
1435 generate_keypair(&output).map_err(PyRuntimeError::new_err)
1436}
1437
1438#[cfg(not(target_arch = "wasm32"))]
1440#[pyfunction]
1441#[pyo3(name = "verify_receipt", signature = (receipt_json, gate_public_key, executor_public_keys=None))]
1442#[allow(clippy::useless_conversion)]
1443pub fn py_verify_receipt(
1444 receipt_json: &str,
1445 gate_public_key: &str,
1446 executor_public_keys: Option<Vec<String>>,
1447) -> PyResult<String> {
1448 verify_receipt(
1449 receipt_json,
1450 gate_public_key,
1451 &executor_public_keys.unwrap_or_default(),
1452 )
1453 .map_err(PyRuntimeError::new_err)
1454}
1455
1456#[cfg(not(target_arch = "wasm32"))]
1458#[pyfunction]
1459#[pyo3(name = "verify_bundle", signature = (bundle_json, gate_public_key, executor_public_keys=None))]
1460#[allow(clippy::useless_conversion)]
1461pub fn py_verify_bundle(
1462 bundle_json: &str,
1463 gate_public_key: &str,
1464 executor_public_keys: Option<Vec<String>>,
1465) -> PyResult<String> {
1466 verify_bundle(
1467 bundle_json,
1468 gate_public_key,
1469 &executor_public_keys.unwrap_or_default(),
1470 )
1471 .map_err(PyRuntimeError::new_err)
1472}
1473
1474#[cfg(not(target_arch = "wasm32"))]
1476#[pyfunction]
1477#[pyo3(name = "verify_checkpoint", signature = (checkpoint_json, stream_json, expected_public_key))]
1478#[allow(clippy::useless_conversion)]
1479pub fn py_verify_checkpoint(
1480 checkpoint_json: &str,
1481 stream_json: &str,
1482 expected_public_key: &str,
1483) -> PyResult<String> {
1484 verify_checkpoint(checkpoint_json, stream_json, expected_public_key)
1485 .map_err(PyRuntimeError::new_err)
1486}
1487
1488#[cfg(not(target_arch = "wasm32"))]
1489#[pymodule]
1490fn _tempus_ddb(m: &Bound<'_, PyModule>) -> PyResult<()> {
1491 m.add_function(wrap_pyfunction!(py_verify_receipt, m)?)?;
1492 m.add_function(wrap_pyfunction!(py_verify_bundle, m)?)?;
1493 m.add_function(wrap_pyfunction!(py_verify_checkpoint, m)?)?;
1494 m.add_class::<TempusDDB>()?;
1495 m.add_class::<TempusExecutor>()?;
1496 m.add_function(wrap_pyfunction!(gen_keys, m)?)?;
1497 Ok(())
1498}
1499
1500#[cfg(target_arch = "wasm32")]
1502#[wasm_bindgen]
1503pub struct TempusDDBWasm {
1504 storage: MemoryStorage,
1505 keyfile: String,
1506}
1507
1508#[cfg(target_arch = "wasm32")]
1509#[wasm_bindgen]
1510impl TempusDDBWasm {
1511 #[wasm_bindgen(constructor)]
1512 pub fn new(keyfile: String) -> Result<TempusDDBWasm, JsValue> {
1513 let storage = MemoryStorage::new();
1514 Ok(TempusDDBWasm { storage, keyfile })
1515 }
1516
1517 #[wasm_bindgen]
1518 pub fn record(&mut self, payload: &str, rules: &str, genesis: bool) -> Result<String, JsValue> {
1519 self.storage
1520 .insert_decision(payload, rules, genesis)
1521 .map_err(|e| JsValue::from_str(&e))?;
1522
1523 let result_json = format!(
1524 r#"{{"status": "success", "action": "recorded", "latest_hash": "{}"}}"#,
1525 self.storage.get_latest_hash().unwrap_or_default()
1526 );
1527 Ok(result_json)
1528 }
1529
1530 #[wasm_bindgen]
1531 pub fn get_ledger(&self) -> Result<String, JsValue> {
1532 self.storage
1533 .export_ledger()
1534 .map_err(|e| JsValue::from_str(&e))
1535 }
1536}
1537
1538#[cfg(not(target_arch = "wasm32"))]
1539mod executor;
1540
1541#[cfg(not(target_arch = "wasm32"))]
1542use executor::{MediatedExecutor, SqliteExecutorStorage};
1543
1544#[cfg(not(target_arch = "wasm32"))]
1545#[pyclass(unsendable)]
1546pub struct TempusExecutor {
1547 inner: MediatedExecutor,
1548}
1549
1550#[cfg(not(target_arch = "wasm32"))]
1551#[pymethods]
1552impl TempusExecutor {
1553 #[new]
1554 #[pyo3(signature = (db_path, keyfile, trusted_gate_id, trusted_tenant_id, pool_size=8, gate_db=None))]
1555 pub fn new(
1556 db_path: String,
1557 keyfile: String,
1558 trusted_gate_id: String,
1559 trusted_tenant_id: String,
1560 pool_size: u32,
1561 gate_db: Option<String>,
1562 ) -> PyResult<Self> {
1563 let storage = SqliteExecutorStorage::with_pool_size(&db_path, pool_size)
1564 .map_err(PyRuntimeError::new_err)?;
1565 let gate_db_final = gate_db.or_else(|| std::env::var("TEMPUS_GATE_DB").ok());
1566 let mut inner = MediatedExecutor::new(
1567 Box::new(storage),
1568 &keyfile,
1569 &trusted_gate_id,
1570 &trusted_tenant_id,
1571 )
1572 .map_err(PyRuntimeError::new_err)?;
1573 inner = inner.with_gate_db(gate_db_final);
1574 Ok(Self { inner })
1575 }
1576
1577 pub fn verify_and_consume_permit(&self, permit_json: String) -> PyResult<String> {
1578 let auth = self
1579 .inner
1580 .verify_and_consume_permit(&permit_json)
1581 .map_err(PyPermissionError::new_err)?;
1582 Ok(serde_json::to_string(&auth).unwrap())
1583 }
1584
1585 pub fn complete_execution(
1586 &self,
1587 authorization_id: String,
1588 action_id: String,
1589 status: String,
1590 output_json: String,
1591 ) -> PyResult<String> {
1592 let output: serde_json::Value = serde_json::from_str(&output_json)
1593 .map_err(|e| PyRuntimeError::new_err(format!("Invalid output JSON: {}", e)))?;
1594
1595 let outcome = self
1596 .inner
1597 .complete_execution(&authorization_id, &action_id, &status, output)
1598 .map_err(PyRuntimeError::new_err)?;
1599
1600 Ok(outcome)
1601 }
1602
1603 pub fn mark_unknown(&self, authorization_id: String, reason: String) -> PyResult<String> {
1604 self.inner
1605 .mark_unknown(&authorization_id, &reason)
1606 .map_err(PyRuntimeError::new_err)
1607 }
1608
1609 #[pyo3(signature = (older_than_seconds=0))]
1610 pub fn recover_incomplete(&self, older_than_seconds: u64) -> PyResult<String> {
1611 self.inner
1612 .recover_incomplete(older_than_seconds)
1613 .map_err(PyRuntimeError::new_err)
1614 }
1615
1616 pub fn get_execution_state(&self, authorization_id: String) -> PyResult<String> {
1617 self.inner
1618 .get_execution_state(&authorization_id)
1619 .map_err(PyRuntimeError::new_err)
1620 }
1621}