Skip to main content

_tempus_ddb/
lib.rs

1//! Tempus DDB: a fail-closed authorization gate for autonomous agent actions.
2//!
3//! An agent signs an *intent*; the gate evaluates a signed tenant policy and issues a
4//! single-use, expiring *permit* (`ALLOWED` or `BLOCKED`); a credential-isolated
5//! *executor* consumes the permit exactly once and signs the *outcome*; the gate links
6//! everything into a dual-signed *receipt* that anyone holding the gate public key can
7//! verify offline. Unknown schemas, policies, identities and execution states are
8//! refused or denied, never allowed, and with no installed policy every action is denied.
9//!
10//! This crate is published mainly as the native extension behind the
11//! [`tempus-ddb` Python package](https://pypi.org/project/tempus-ddb/) (`tempus_ddb._tempus_ddb`,
12//! built with PyO3 and maturin). The Rust API that is useful on its own:
13//!
14//! - [`verify_receipt`], [`verify_bundle`] and [`verify_checkpoint`]: independent
15//!   verification of exported evidence against a pinned gate public key, with no database.
16//! - [`generate_keypair`]: Ed25519 key generation in the key-file format the CLI reads.
17//! - [`StorageLayer`] and [`SqliteStorage`]: the storage boundary used
18//!   by the gate and the legacy decision ledger.
19//!
20//! Signed JSON documents are parsed strictly: duplicate object keys are refused, and
21//! numbers must be exact (no floats, no integers beyond `i64`/`u64`).
22//!
23//! Documentation: <https://julio-patron.github.io/tempus-ddb/docs.html>. Threat model,
24//! compatibility rules and changelog live in the repository:
25//! <https://github.com/Julio-Patron/tempus-ddb>.
26#![allow(clippy::useless_conversion)] // PyO3 macro expansion on Rust 1.96.
27
28#[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
50// --- 1. PATRÓN STORAGE LAYER (TRAIT) ---
51pub 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// Experimental WASM support: in-memory stub storage only, not persistent ledger storage.
65#[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(); // Simulación de nuevo hash
94        }
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        // En una implementación real, serializaríamos a JSON usando serde_json
117        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// Implementación SQLite (Para Python / Nativo)
135#[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    /// Load an Ed25519 signing key from a file path.
219    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    /// Return the signing key, loading from disk on first use and caching.
242    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    /// Load the configured signing backend. Local key files remain compatible, while
252    /// production deployments may point at a non-secret Vault Transit signer config.
253    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    /// Compute the canonical SHA-256 hash for a decision (matches main.rs logic).
263    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        // Check for unregistered actors (warnings, not errors)
508        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    /// Register an agent through a signed, immutable delegation event.
552    ///
553    /// The first registration bootstraps the gate key itself. Every later
554    /// registration must be signed by an active agent with delegation rights.
555    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    /// Verify that an agent has a valid signed registration and is active.
565    pub fn verify_agent(&self, public_key: &str) -> Result<bool, String> {
566        b2a::verify_agent(self, public_key)
567    }
568
569    /// List registered agents and the validity of their registration receipts.
570    pub fn list_agents(&self) -> Result<String, String> {
571        b2a::list_agents(self)
572    }
573
574    /// Get a single registered agent.
575    pub fn get_agent(&self, public_key: &str) -> Result<String, String> {
576        b2a::get_agent(self, public_key)
577    }
578
579    /// Rotate an identity to a new Ed25519 verification key while retaining its stable ID.
580    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    /// Revoke an active identity key and invalidate its unconsumed permits.
589    pub fn revoke_agent(&self, public_key: &str, reason: &str) -> Result<String, String> {
590        b2a::revoke_agent(self, public_key, reason)
591    }
592
593    /// List signed rotation and revocation events.
594    pub fn list_identity_events(&self) -> Result<String, String> {
595        b2a::list_identity_events(self)
596    }
597
598    /// Ask the Tempus gate for a single-use authorization receipt.
599    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    /// Lower-level transport-neutral authorization API for signed requests.
609    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    /// Consume an allowed permit exactly once and append the executor outcome.
620    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    /// Consume an allowed permit exactly once with an already signed outcome.
630    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    /// Return the authorization and optional execution receipt for an action.
639    pub fn get_trace(&self, action_id: &str) -> Result<String, String> {
640        b2a::get_trace(self, action_id)
641    }
642
643    /// Cryptographically verify an action trace end to end.
644    pub fn verify_trace(&self, action_id: &str) -> Result<String, String> {
645        b2a::verify_trace(self, action_id)
646    }
647
648    /// Verify an action trace against a gate public key pinned by the verifier
649    /// (and optionally pinned executor keys). Does not trust the database's
650    /// own trusted root.
651    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    /// Export a self-contained evidence bundle for one action (see
661    /// `tempus.evidence-bundle.v1`). Contains no private key material.
662    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    /// Install and activate a deterministic policy bundle signed by the gate.
671    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    /// DEVELOPMENT ONLY. Explicitly install a signed policy that allows every action,
680    /// resource and executor for every tenant without its own policy. Nothing installs
681    /// this implicitly: without it (or a real policy) the gate answers BLOCKED with
682    /// `POLICY_NOT_INSTALLED`. Idempotent while a current dev policy is active. A stored
683    /// dev policy whose `allowed_currencies` is not a list of exact codes is left in
684    /// place and refused on load (`TEMPUS_POLICY_INVALID`); only this call replaces it.
685    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    /// List active and retired policy bundles.
692    pub fn list_policies(&self) -> Result<String, String> {
693        phase3::list_policies(&self.conn)
694    }
695
696    /// Exercise the configured local or remote signer with an offline-verifiable fixture.
697    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        // Validate payload and rules are valid JSON
746        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        // Load signing key from keyfile
752        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        // Resolve parent_id and causal_depth
762        let (parent_id, causal_depth) = match Self::get_last_decision_conn(&tx)? {
763            Some(last_d) => {
764                if genesis {
765                    // Even if there are existing decisions, --genesis forces a new root
766                    ("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        // Timestamp in microseconds
794        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        // Compute the deterministic SHA-256 hash
800        let hash_bytes =
801            Self::calculate_canonical_hash(&parent_id, &actor_id, timestamp, payload, rules)?;
802        let id = hex::encode(hash_bytes);
803
804        // Ed25519 signature of the hash
805        let signature_struct = signing_key.sign(&hash_bytes);
806        let signature = hex::encode(signature_struct.to_bytes());
807
808        // Insert into SQLite
809        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                &current_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                    &current_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// --- 3. BINDINGS PARA PYTHON (PYO3) ---
1033#[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    // --- Multi-Signer: Agent management ---
1122
1123    #[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    /// Verify an action trace. With `gate_public_key` the gate key is pinned by
1222    /// the caller and the database's own trusted root is not used; without it the
1223    /// result is marked `"independent": false`.
1224    #[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    /// Export one action as a self-contained evidence bundle (JSON string).
1246    #[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    /// DEVELOPMENT ONLY: explicitly install an allow-all policy (emits a UserWarning).
1261    #[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    /// Return the public key of the current keyfile ("who am I?").
1292    #[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        // Try to find the agent alias
1303        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/// Verify a standalone receipt (trace, permit or outcome JSON) with no database,
1355/// against a gate public key pinned by the verifier.
1356#[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/// Verify an evidence bundle with no database, against a pinned gate public key.
1366#[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/// Verify a checkpoint and its event stream with no database. The signer must
1376/// equal `expected_public_key`, which the verifier supplies out of band.
1377#[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/// Verify a standalone receipt (trace, permit or outcome JSON) without a database.
1439#[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/// Verify an evidence bundle without a database, against a pinned gate key.
1457#[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/// Verify a checkpoint and event stream without a database, against a pinned key.
1475#[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// --- 4. BINDINGS PARA JAVASCRIPT (WASM-BINDGEN) ---
1501#[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}