Skip to main content

chio_store_sqlite/
authority.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3use std::sync::Mutex;
4
5use chio_core::capability::{CapabilityToken, CapabilityTokenBody, ChioScope};
6use chio_core::crypto::{Keypair, PublicKey};
7use chio_kernel::{
8    AuthoritySnapshot, AuthorityStatus, AuthorityStoreError, AuthorityTrustedKeySnapshot,
9    CapabilityAuthority, KernelError,
10};
11use rusqlite::{params, Connection};
12use uuid::Uuid;
13
14pub struct SqliteCapabilityAuthority {
15    path: PathBuf,
16    cached_public_key: Mutex<PublicKey>,
17    cached_trusted_public_keys: Mutex<Vec<PublicKey>>,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct AuthorityClusterFence {
22    pub leader_url: Option<String>,
23    pub election_term: u64,
24    pub updated_at: u64,
25    pub authority_generation: u64,
26    pub authority_rotated_at: u64,
27}
28
29impl SqliteCapabilityAuthority {
30    pub fn open(path: impl AsRef<Path>) -> Result<Self, AuthorityStoreError> {
31        let path = path.as_ref().to_path_buf();
32        if let Some(parent) = path.parent() {
33            fs::create_dir_all(parent)?;
34        }
35
36        let bootstrap = Keypair::generate();
37        let connection = Self::open_connection(&path)?;
38        connection.execute(
39            r#"
40            INSERT INTO authority_state (singleton_id, seed_hex, public_key_hex, generation, rotated_at)
41            VALUES (1, ?1, ?2, 1, ?3)
42            ON CONFLICT(singleton_id) DO NOTHING
43            "#,
44            params![
45                bootstrap.seed_hex(),
46                bootstrap.public_key().to_hex(),
47                unix_now() as i64
48            ],
49        )?;
50        let current_public_key = connection
51            .query_row(
52                r#"
53                SELECT seed_hex
54                FROM authority_state
55                WHERE singleton_id = 1
56                "#,
57                [],
58                |row| row.get::<_, String>(0),
59            )
60            .map(|seed_hex| Keypair::from_seed_hex(seed_hex.trim()))
61            .map_err(AuthorityStoreError::from)??;
62        connection.execute(
63            r#"
64            UPDATE authority_state
65            SET public_key_hex = COALESCE(NULLIF(public_key_hex, ''), ?1)
66            WHERE singleton_id = 1
67            "#,
68            params![current_public_key.public_key().to_hex()],
69        )?;
70        connection.execute(
71            r#"
72            INSERT INTO authority_trusted_keys (public_key_hex, generation, activated_at)
73            VALUES (?1, 1, ?2)
74            ON CONFLICT(public_key_hex) DO NOTHING
75            "#,
76            params![current_public_key.public_key().to_hex(), unix_now() as i64],
77        )?;
78        let status = Self::read_status_from_connection(&connection)?;
79        Ok(Self {
80            path,
81            cached_public_key: Mutex::new(status.public_key),
82            cached_trusted_public_keys: Mutex::new(status.trusted_public_keys),
83        })
84    }
85
86    pub fn status(&self) -> Result<AuthorityStatus, AuthorityStoreError> {
87        let connection = Self::open_connection(&self.path)?;
88        let status = Self::read_status_from_connection(&connection)?;
89        self.update_cached_public_key(status.public_key.clone());
90        self.update_cached_trusted_public_keys(status.trusted_public_keys.clone());
91        Ok(status)
92    }
93
94    pub fn rotate(&self) -> Result<AuthorityStatus, AuthorityStoreError> {
95        let connection = Self::open_connection(&self.path)?;
96        let status_before = Self::read_status_from_connection(&connection)?;
97        let keypair = Keypair::generate();
98        let rotated_at = unix_now();
99        let next_generation = status_before.generation.saturating_add(1);
100
101        connection.execute(
102            r#"
103            UPDATE authority_state
104            SET seed_hex = ?1, public_key_hex = ?2, generation = ?3, rotated_at = ?4
105            WHERE singleton_id = 1
106            "#,
107            params![
108                keypair.seed_hex(),
109                keypair.public_key().to_hex(),
110                next_generation as i64,
111                rotated_at as i64,
112            ],
113        )?;
114        connection.execute(
115            r#"
116            INSERT INTO authority_trusted_keys (public_key_hex, generation, activated_at)
117            VALUES (?1, ?2, ?3)
118            ON CONFLICT(public_key_hex) DO NOTHING
119            "#,
120            params![
121                keypair.public_key().to_hex(),
122                next_generation as i64,
123                rotated_at as i64
124            ],
125        )?;
126
127        let status = Self::read_status_from_connection(&connection)?;
128        self.update_cached_public_key(status.public_key.clone());
129        self.update_cached_trusted_public_keys(status.trusted_public_keys.clone());
130        Ok(status)
131    }
132
133    pub fn snapshot(&self) -> Result<AuthoritySnapshot, AuthorityStoreError> {
134        let connection = Self::open_connection(&self.path)?;
135        let status = Self::read_status_from_connection(&connection)?;
136        Ok(AuthoritySnapshot {
137            public_key_hex: status.public_key.to_hex(),
138            generation: status.generation,
139            rotated_at: status.rotated_at,
140            trusted_keys: Self::read_trusted_key_snapshots(&connection)?,
141        })
142    }
143
144    pub fn apply_snapshot(
145        &self,
146        snapshot: &AuthoritySnapshot,
147    ) -> Result<bool, AuthorityStoreError> {
148        let connection = Self::open_connection(&self.path)?;
149        let local_snapshot = self.snapshot()?;
150        let remote_public_key = PublicKey::from_hex(snapshot.public_key_hex.trim())?;
151
152        // Cluster snapshots replicate verification history, not signing custody.
153        connection.execute(
154            r#"
155            INSERT INTO authority_trusted_keys (public_key_hex, generation, activated_at)
156            VALUES (?1, ?2, ?3)
157            ON CONFLICT(public_key_hex) DO UPDATE SET
158                generation = MAX(generation, excluded.generation),
159                activated_at = MIN(activated_at, excluded.activated_at)
160            "#,
161            params![
162                remote_public_key.to_hex(),
163                snapshot.generation as i64,
164                snapshot.rotated_at as i64
165            ],
166        )?;
167        for trusted_key in &snapshot.trusted_keys {
168            connection.execute(
169                r#"
170                INSERT INTO authority_trusted_keys (public_key_hex, generation, activated_at)
171                VALUES (?1, ?2, ?3)
172                ON CONFLICT(public_key_hex) DO UPDATE SET
173                    generation = MAX(generation, excluded.generation),
174                    activated_at = MIN(activated_at, excluded.activated_at)
175                "#,
176                params![
177                    trusted_key.public_key_hex,
178                    trusted_key.generation as i64,
179                    trusted_key.activated_at as i64
180                ],
181            )?;
182        }
183
184        let should_replace = snapshot.generation > local_snapshot.generation
185            || (snapshot.generation == local_snapshot.generation
186                && (snapshot.rotated_at, snapshot.public_key_hex.as_str())
187                    > (
188                        local_snapshot.rotated_at,
189                        local_snapshot.public_key_hex.as_str(),
190                    ));
191        if should_replace {
192            connection.execute(
193                r#"
194                UPDATE authority_state
195                SET public_key_hex = ?1, generation = ?2, rotated_at = ?3
196                WHERE singleton_id = 1
197                "#,
198                params![
199                    remote_public_key.to_hex(),
200                    snapshot.generation as i64,
201                    snapshot.rotated_at as i64,
202                ],
203            )?;
204        }
205
206        let status = Self::read_status_from_connection(&connection)?;
207        self.update_cached_public_key(status.public_key);
208        self.update_cached_trusted_public_keys(status.trusted_public_keys);
209        Ok(should_replace)
210    }
211
212    pub fn current_keypair(&self) -> Result<Keypair, AuthorityStoreError> {
213        self.read_current_keypair()
214    }
215
216    pub fn local_keypair(&self) -> Result<Keypair, AuthorityStoreError> {
217        let connection = Self::open_connection(&self.path)?;
218        Self::read_keypair_from_connection(&connection)
219    }
220
221    pub fn cluster_fence(&self) -> Result<AuthorityClusterFence, AuthorityStoreError> {
222        let connection = Self::open_connection(&self.path)?;
223        Self::read_cluster_fence_from_connection(&connection)
224    }
225
226    pub fn seed_cluster_fence(
227        &self,
228        leader_url: Option<&str>,
229        election_term: u64,
230    ) -> Result<bool, AuthorityStoreError> {
231        let connection = Self::open_connection(&self.path)?;
232        let current = Self::read_cluster_fence_from_connection(&connection)?;
233        let (_, authority_generation, authority_rotated_at) =
234            Self::read_public_state_from_connection(&connection)?;
235        let next_leader = leader_url.map(ToOwned::to_owned);
236        let same_term_same_leader = election_term == current.election_term
237            && current.leader_url.as_deref() == next_leader.as_deref();
238        let fence_authority_state_is_stale = (current.election_term > 0
239            || current.leader_url.is_some())
240            && (current.authority_generation != authority_generation
241                || current.authority_rotated_at != authority_rotated_at);
242        let should_update = election_term > current.election_term
243            || (election_term == current.election_term
244                && current.leader_url.is_none()
245                && next_leader.is_some())
246            || (same_term_same_leader && fence_authority_state_is_stale);
247        if should_update {
248            Self::write_cluster_fence_to_connection(&connection, next_leader, election_term)?;
249        }
250        Ok(should_update)
251    }
252
253    pub fn enforce_cluster_fence(
254        &self,
255        leader_url: &str,
256        election_term: u64,
257    ) -> Result<(), AuthorityStoreError> {
258        let connection = Self::open_connection(&self.path)?;
259        let current = Self::read_cluster_fence_from_connection(&connection)?;
260        let (_, authority_generation, authority_rotated_at) =
261            Self::read_public_state_from_connection(&connection)?;
262        if (current.election_term > 0 || current.leader_url.is_some())
263            && (current.authority_generation != authority_generation
264                || current.authority_rotated_at != authority_rotated_at)
265        {
266            return Err(AuthorityStoreError::Fence(format!(
267                "persisted authority fence generation `{}` rotated_at `{}` does not match current authority generation `{authority_generation}` rotated_at `{authority_rotated_at}`",
268                current.authority_generation, current.authority_rotated_at
269            )));
270        }
271        if election_term < current.election_term {
272            return Err(AuthorityStoreError::Fence(format!(
273                "stale authority term `{election_term}` is below persisted term `{}`",
274                current.election_term
275            )));
276        }
277        if election_term == current.election_term
278            && current
279                .leader_url
280                .as_deref()
281                .is_some_and(|current_leader| current_leader != leader_url)
282        {
283            return Err(AuthorityStoreError::Fence(format!(
284                "authority term `{election_term}` is already fenced to leader `{}`",
285                current.leader_url.unwrap_or_default()
286            )));
287        }
288        Self::write_cluster_fence_to_connection(
289            &connection,
290            Some(leader_url.to_string()),
291            election_term,
292        )
293    }
294
295    fn open_connection(path: &Path) -> Result<Connection, AuthorityStoreError> {
296        let connection = Connection::open(path)?;
297        connection.execute_batch(
298            r#"
299            PRAGMA journal_mode = WAL;
300            PRAGMA synchronous = FULL;
301            PRAGMA busy_timeout = 5000;
302
303            CREATE TABLE IF NOT EXISTS authority_state (
304                singleton_id INTEGER PRIMARY KEY CHECK (singleton_id = 1),
305                seed_hex TEXT NOT NULL,
306                public_key_hex TEXT,
307                generation INTEGER NOT NULL,
308                rotated_at INTEGER NOT NULL
309            );
310
311            CREATE TABLE IF NOT EXISTS authority_trusted_keys (
312                public_key_hex TEXT PRIMARY KEY,
313                generation INTEGER NOT NULL,
314                activated_at INTEGER NOT NULL
315            );
316
317            CREATE TABLE IF NOT EXISTS authority_cluster_fence (
318                singleton_id INTEGER PRIMARY KEY CHECK (singleton_id = 1),
319                leader_url TEXT,
320                election_term INTEGER NOT NULL,
321                updated_at INTEGER NOT NULL,
322                authority_generation INTEGER NOT NULL DEFAULT 0,
323                authority_rotated_at INTEGER NOT NULL DEFAULT 0
324            );
325
326            INSERT INTO authority_cluster_fence (singleton_id, leader_url, election_term, updated_at)
327            VALUES (1, NULL, 0, 0)
328            ON CONFLICT(singleton_id) DO NOTHING;
329            "#,
330        )?;
331        if !Self::table_has_column(&connection, "authority_state", "public_key_hex")? {
332            connection.execute(
333                "ALTER TABLE authority_state ADD COLUMN public_key_hex TEXT",
334                [],
335            )?;
336        }
337        if !Self::table_has_column(
338            &connection,
339            "authority_cluster_fence",
340            "authority_generation",
341        )? {
342            connection.execute(
343                "ALTER TABLE authority_cluster_fence ADD COLUMN authority_generation INTEGER NOT NULL DEFAULT 0",
344                [],
345            )?;
346        }
347        if !Self::table_has_column(
348            &connection,
349            "authority_cluster_fence",
350            "authority_rotated_at",
351        )? {
352            connection.execute(
353                "ALTER TABLE authority_cluster_fence ADD COLUMN authority_rotated_at INTEGER NOT NULL DEFAULT 0",
354                [],
355            )?;
356        }
357        Ok(connection)
358    }
359
360    fn read_status_from_connection(
361        connection: &Connection,
362    ) -> Result<AuthorityStatus, AuthorityStoreError> {
363        let (public_key, generation, rotated_at) =
364            Self::read_public_state_from_connection(connection)?;
365        Ok(AuthorityStatus {
366            public_key,
367            generation,
368            rotated_at,
369            trusted_public_keys: Self::read_trusted_public_keys(connection)?,
370        })
371    }
372
373    fn read_keypair_from_connection(
374        connection: &Connection,
375    ) -> Result<Keypair, AuthorityStoreError> {
376        let seed_hex = connection.query_row(
377            r#"
378            SELECT seed_hex
379            FROM authority_state
380            WHERE singleton_id = 1
381            "#,
382            [],
383            |row| row.get::<_, String>(0),
384        )?;
385        Keypair::from_seed_hex(seed_hex.trim()).map_err(Into::into)
386    }
387
388    fn read_public_state_from_connection(
389        connection: &Connection,
390    ) -> Result<(PublicKey, u64, u64), AuthorityStoreError> {
391        let (seed_hex, public_key_hex, generation, rotated_at) = connection.query_row(
392            r#"
393            SELECT seed_hex, public_key_hex, generation, rotated_at
394            FROM authority_state
395            WHERE singleton_id = 1
396            "#,
397            [],
398            |row| {
399                Ok((
400                    row.get::<_, String>(0)?,
401                    row.get::<_, Option<String>>(1)?,
402                    row.get::<_, i64>(2)?,
403                    row.get::<_, i64>(3)?,
404                ))
405            },
406        )?;
407        let public_key = match public_key_hex
408            .as_deref()
409            .map(str::trim)
410            .filter(|value| !value.is_empty())
411        {
412            Some(public_key_hex) => PublicKey::from_hex(public_key_hex)?,
413            None => Keypair::from_seed_hex(seed_hex.trim())?.public_key(),
414        };
415        Ok((
416            public_key,
417            generation.max(0) as u64,
418            rotated_at.max(0) as u64,
419        ))
420    }
421
422    fn read_current_keypair(&self) -> Result<Keypair, AuthorityStoreError> {
423        let connection = Self::open_connection(&self.path)?;
424        let keypair = Self::read_keypair_from_connection(&connection)?;
425        let status = Self::read_status_from_connection(&connection)?;
426        self.update_cached_public_key(status.public_key.clone());
427        self.update_cached_trusted_public_keys(status.trusted_public_keys.clone());
428        if keypair.public_key() != status.public_key {
429            return Err(AuthorityStoreError::Fence(format!(
430                "local signing seed public key {} does not match replicated authority public key {}",
431                keypair.public_key().to_hex(),
432                status.public_key.to_hex(),
433            )));
434        }
435        Ok(keypair)
436    }
437
438    fn table_has_column(
439        connection: &Connection,
440        table: &str,
441        column: &str,
442    ) -> Result<bool, AuthorityStoreError> {
443        let pragma = format!("PRAGMA table_info({table})");
444        let mut statement = connection.prepare(&pragma)?;
445        let mut rows = statement.query([])?;
446        while let Some(row) = rows.next()? {
447            if row.get::<_, String>(1)? == column {
448                return Ok(true);
449            }
450        }
451        Ok(false)
452    }
453
454    fn update_cached_public_key(&self, public_key: PublicKey) {
455        match self.cached_public_key.lock() {
456            Ok(mut guard) => *guard = public_key,
457            Err(poisoned) => *poisoned.into_inner() = public_key,
458        }
459    }
460
461    fn update_cached_trusted_public_keys(&self, public_keys: Vec<PublicKey>) {
462        match self.cached_trusted_public_keys.lock() {
463            Ok(mut guard) => *guard = public_keys,
464            Err(poisoned) => *poisoned.into_inner() = public_keys,
465        }
466    }
467
468    fn cached_public_key(&self) -> PublicKey {
469        match self.cached_public_key.lock() {
470            Ok(guard) => guard.clone(),
471            Err(poisoned) => poisoned.into_inner().clone(),
472        }
473    }
474
475    fn cached_trusted_public_keys(&self) -> Vec<PublicKey> {
476        match self.cached_trusted_public_keys.lock() {
477            Ok(guard) => guard.clone(),
478            Err(poisoned) => poisoned.into_inner().clone(),
479        }
480    }
481
482    fn read_trusted_public_keys(
483        connection: &Connection,
484    ) -> Result<Vec<PublicKey>, AuthorityStoreError> {
485        let mut statement = connection.prepare(
486            r#"
487            SELECT public_key_hex
488            FROM authority_trusted_keys
489            ORDER BY generation ASC, activated_at ASC
490            "#,
491        )?;
492        let rows = statement.query_map([], |row| row.get::<_, String>(0))?;
493        rows.map(|row| {
494            let public_key_hex = row?;
495            PublicKey::from_hex(public_key_hex.trim()).map_err(AuthorityStoreError::from)
496        })
497        .collect()
498    }
499
500    fn read_trusted_key_snapshots(
501        connection: &Connection,
502    ) -> Result<Vec<AuthorityTrustedKeySnapshot>, AuthorityStoreError> {
503        let mut statement = connection.prepare(
504            r#"
505            SELECT public_key_hex, generation, activated_at
506            FROM authority_trusted_keys
507            ORDER BY generation ASC, activated_at ASC, public_key_hex ASC
508            "#,
509        )?;
510        let rows = statement.query_map([], |row| {
511            Ok(AuthorityTrustedKeySnapshot {
512                public_key_hex: row.get(0)?,
513                generation: row.get::<_, i64>(1)?.max(0) as u64,
514                activated_at: row.get::<_, i64>(2)?.max(0) as u64,
515            })
516        })?;
517        rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
518    }
519
520    fn read_cluster_fence_from_connection(
521        connection: &Connection,
522    ) -> Result<AuthorityClusterFence, AuthorityStoreError> {
523        let (leader_url, election_term, updated_at, authority_generation, authority_rotated_at) =
524            connection.query_row(
525                r#"
526            SELECT leader_url, election_term, updated_at, authority_generation, authority_rotated_at
527            FROM authority_cluster_fence
528            WHERE singleton_id = 1
529            "#,
530                [],
531                |row| {
532                    Ok((
533                        row.get::<_, Option<String>>(0)?,
534                        row.get::<_, i64>(1)?,
535                        row.get::<_, i64>(2)?,
536                        row.get::<_, i64>(3)?,
537                        row.get::<_, i64>(4)?,
538                    ))
539                },
540            )?;
541        Ok(AuthorityClusterFence {
542            leader_url,
543            election_term: election_term.max(0) as u64,
544            updated_at: updated_at.max(0) as u64,
545            authority_generation: authority_generation.max(0) as u64,
546            authority_rotated_at: authority_rotated_at.max(0) as u64,
547        })
548    }
549
550    fn write_cluster_fence_to_connection(
551        connection: &Connection,
552        leader_url: Option<String>,
553        election_term: u64,
554    ) -> Result<(), AuthorityStoreError> {
555        let (_, authority_generation, authority_rotated_at) =
556            Self::read_public_state_from_connection(connection)?;
557        connection.execute(
558            r#"
559            UPDATE authority_cluster_fence
560            SET
561                leader_url = ?1,
562                election_term = ?2,
563                updated_at = ?3,
564                authority_generation = ?4,
565                authority_rotated_at = ?5
566            WHERE singleton_id = 1
567            "#,
568            params![
569                leader_url,
570                election_term as i64,
571                unix_now() as i64,
572                authority_generation as i64,
573                authority_rotated_at as i64,
574            ],
575        )?;
576        Ok(())
577    }
578}
579
580impl CapabilityAuthority for SqliteCapabilityAuthority {
581    fn authority_public_key(&self) -> PublicKey {
582        self.status()
583            .map(|status| status.public_key)
584            .unwrap_or_else(|_| self.cached_public_key())
585    }
586
587    fn trusted_public_keys(&self) -> Vec<PublicKey> {
588        self.status()
589            .map(|status| status.trusted_public_keys)
590            .unwrap_or_else(|_| self.cached_trusted_public_keys())
591    }
592
593    fn issue_capability(
594        &self,
595        subject: &PublicKey,
596        scope: ChioScope,
597        ttl_seconds: u64,
598    ) -> Result<CapabilityToken, KernelError> {
599        let keypair = self
600            .read_current_keypair()
601            .map_err(|error| KernelError::CapabilityIssuanceFailed(error.to_string()))?;
602        let now = unix_now();
603        let body = CapabilityTokenBody {
604            id: format!("cap-{}", Uuid::now_v7()),
605            issuer: keypair.public_key(),
606            subject: subject.clone(),
607            scope,
608            issued_at: now,
609            expires_at: now.saturating_add(ttl_seconds),
610            delegation_chain: vec![],
611        };
612
613        CapabilityToken::sign(body, &keypair)
614            .map_err(|error| KernelError::CapabilityIssuanceFailed(error.to_string()))
615    }
616}
617
618fn unix_now() -> u64 {
619    std::time::SystemTime::now()
620        .duration_since(std::time::UNIX_EPOCH)
621        .map(|duration| duration.as_secs())
622        .unwrap_or(0)
623}
624
625#[cfg(test)]
626#[allow(clippy::expect_used, clippy::unwrap_used)]
627mod tests {
628    use std::fs;
629    use std::time::{SystemTime, UNIX_EPOCH};
630
631    use super::*;
632    use chio_core::capability::{Operation, ToolGrant};
633    use chio_kernel::LocalCapabilityAuthority;
634
635    fn unique_db_path(prefix: &str) -> std::path::PathBuf {
636        let nonce = SystemTime::now()
637            .duration_since(UNIX_EPOCH)
638            .expect("time before epoch")
639            .as_nanos();
640        std::env::temp_dir().join(format!("{prefix}-{nonce}.sqlite3"))
641    }
642
643    #[test]
644    fn local_capability_authority_signs_capabilities() {
645        let authority = LocalCapabilityAuthority::new(Keypair::generate());
646        let subject = Keypair::generate().public_key();
647        let capability = authority
648            .issue_capability(
649                &subject,
650                ChioScope {
651                    grants: vec![ToolGrant {
652                        server_id: "srv-a".to_string(),
653                        tool_name: "read_file".to_string(),
654                        operations: vec![Operation::Invoke],
655                        constraints: vec![],
656                        max_invocations: None,
657                        max_cost_per_invocation: None,
658                        max_total_cost: None,
659                        dpop_required: None,
660                    }],
661                    resource_grants: vec![],
662                    prompt_grants: vec![],
663                },
664                300,
665            )
666            .unwrap();
667
668        assert_eq!(capability.subject, subject);
669        assert_eq!(capability.issuer, authority.authority_public_key());
670        assert!(capability.id.starts_with("cap-"));
671        assert!(capability.verify_signature().unwrap());
672    }
673
674    #[test]
675    fn sqlite_capability_authority_persists_and_rotates_across_handles() {
676        let path = unique_db_path("chio-authority");
677        let authority_a = SqliteCapabilityAuthority::open(&path).unwrap();
678        let authority_b = SqliteCapabilityAuthority::open(&path).unwrap();
679
680        let before = authority_a.status().unwrap();
681        assert_eq!(before.generation, 1);
682        assert_eq!(authority_b.status().unwrap().public_key, before.public_key);
683
684        let rotated = authority_a.rotate().unwrap();
685        assert_eq!(rotated.generation, 2);
686        assert_ne!(rotated.public_key, before.public_key);
687
688        let observed = authority_b.status().unwrap();
689        assert_eq!(observed.public_key, rotated.public_key);
690        assert_eq!(observed.generation, rotated.generation);
691
692        let _ = fs::remove_file(path);
693    }
694
695    #[test]
696    fn sqlite_capability_authority_issues_with_current_rotated_key() {
697        let path = unique_db_path("chio-authority-issue");
698        let authority = SqliteCapabilityAuthority::open(&path).unwrap();
699        let subject = Keypair::generate().public_key();
700        let first = authority
701            .issue_capability(
702                &subject,
703                ChioScope {
704                    grants: vec![ToolGrant {
705                        server_id: "srv-a".to_string(),
706                        tool_name: "read_file".to_string(),
707                        operations: vec![Operation::Invoke],
708                        constraints: vec![],
709                        max_invocations: None,
710                        max_cost_per_invocation: None,
711                        max_total_cost: None,
712                        dpop_required: None,
713                    }],
714                    resource_grants: vec![],
715                    prompt_grants: vec![],
716                },
717                300,
718            )
719            .unwrap();
720        let rotated = authority.rotate().unwrap();
721        let second = authority
722            .issue_capability(
723                &subject,
724                ChioScope {
725                    grants: vec![ToolGrant {
726                        server_id: "srv-a".to_string(),
727                        tool_name: "read_file".to_string(),
728                        operations: vec![Operation::Invoke],
729                        constraints: vec![],
730                        max_invocations: None,
731                        max_cost_per_invocation: None,
732                        max_total_cost: None,
733                        dpop_required: None,
734                    }],
735                    resource_grants: vec![],
736                    prompt_grants: vec![],
737                },
738                300,
739            )
740            .unwrap();
741
742        assert_ne!(first.issuer, second.issuer);
743        assert_eq!(second.issuer, rotated.public_key);
744        assert!(second.verify_signature().unwrap());
745
746        let _ = fs::remove_file(path);
747    }
748
749    #[test]
750    fn sqlite_capability_authority_snapshot_updates_public_view_without_copying_seed() {
751        let source_path = unique_db_path("chio-authority-source");
752        let follower_path = unique_db_path("chio-authority-follower");
753        let source = SqliteCapabilityAuthority::open(&source_path).unwrap();
754        let follower = SqliteCapabilityAuthority::open(&follower_path).unwrap();
755
756        let follower_local_key = follower.current_keypair().unwrap().public_key();
757        let rotated = source.rotate().unwrap();
758        let snapshot = source.snapshot().unwrap();
759
760        assert!(follower.apply_snapshot(&snapshot).unwrap());
761
762        let follower_status = follower.status().unwrap();
763        assert_eq!(follower_status.public_key, rotated.public_key);
764        assert_eq!(follower_status.generation, rotated.generation);
765        let error = match follower.current_keypair() {
766            Ok(_) => panic!("mismatched follower keypair should be rejected"),
767            Err(error) => error.to_string(),
768        };
769        assert!(error.contains("does not match replicated authority public key"));
770        assert!(error.contains(&follower_local_key.to_hex()));
771
772        let _ = fs::remove_file(source_path);
773        let _ = fs::remove_file(follower_path);
774    }
775
776    #[test]
777    fn sqlite_capability_authority_same_term_snapshot_does_not_clear_fenced_leader() {
778        let path = unique_db_path("chio-authority-fence");
779        let authority = SqliteCapabilityAuthority::open(&path).unwrap();
780
781        assert!(authority
782            .seed_cluster_fence(Some("http://leader-a"), 7)
783            .unwrap());
784        assert!(!authority.seed_cluster_fence(None, 7).unwrap());
785
786        let fence = authority.cluster_fence().unwrap();
787        let status = authority.status().unwrap();
788        assert_eq!(fence.election_term, 7);
789        assert_eq!(fence.leader_url.as_deref(), Some("http://leader-a"));
790        assert_eq!(fence.authority_generation, status.generation);
791        assert_eq!(fence.authority_rotated_at, status.rotated_at);
792
793        let _ = fs::remove_file(path);
794    }
795
796    #[test]
797    fn sqlite_capability_authority_enforce_cluster_fence_rejects_stale_rotation() {
798        let path = unique_db_path("chio-authority-fence-stale-rotation");
799        let authority = SqliteCapabilityAuthority::open(&path).unwrap();
800
801        assert!(authority
802            .seed_cluster_fence(Some("http://leader-a"), 7)
803            .unwrap());
804        let fence = authority.cluster_fence().unwrap();
805        authority.rotate().unwrap();
806
807        let error = authority
808            .enforce_cluster_fence("http://leader-a", 7)
809            .expect_err("stale persisted fence should fail closed")
810            .to_string();
811        assert!(error.contains("persisted authority fence generation"));
812        assert!(error.contains(&fence.authority_generation.to_string()));
813
814        let _ = fs::remove_file(path);
815    }
816
817    #[test]
818    fn sqlite_capability_authority_seed_cluster_fence_refreshes_same_term_authority_state() {
819        let path = unique_db_path("chio-authority-fence-same-term-refresh");
820        let authority = SqliteCapabilityAuthority::open(&path).unwrap();
821
822        assert!(authority
823            .seed_cluster_fence(Some("http://leader-a"), 7)
824            .unwrap());
825        authority.rotate().unwrap();
826
827        assert!(authority
828            .seed_cluster_fence(Some("http://leader-a"), 7)
829            .unwrap());
830        authority
831            .enforce_cluster_fence("http://leader-a", 7)
832            .expect("same-term reseed should refresh authority generation");
833
834        let fence = authority.cluster_fence().unwrap();
835        let status = authority.status().unwrap();
836        assert_eq!(fence.election_term, 7);
837        assert_eq!(fence.leader_url.as_deref(), Some("http://leader-a"));
838        assert_eq!(fence.authority_generation, status.generation);
839        assert_eq!(fence.authority_rotated_at, status.rotated_at);
840
841        let _ = fs::remove_file(path);
842    }
843}