zeph_config/integrity.rs
1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Pure-data configuration for vault-anchor downgrade-resistance (`[integrity]`, issue #6449).
5//!
6//! Layers on top of the transcript/session hash-chain feature (issue #6360): the chain alone
7//! detects in-place edits and a partial strip of chain metadata, but not a fully consistent
8//! whole-file strip. `[integrity]` controls whether a per-file vault anchor is written on
9//! finalize/close to close that gap. See `zeph_common::anchor` for the mechanism.
10
11use serde::{Deserialize, Serialize};
12
13/// Vault-anchor downgrade-resistance posture.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum AnchorMode {
17 /// Write and check a per-file vault anchor (the default, whenever the age vault + a
18 /// history-integrity key are available). Degrades gracefully — never a hard failure — to
19 /// chain-only (#6453-level) protection if the vault isn't reachable at bootstrap; see
20 /// `zeph_core::anchor_store`'s startup warning.
21 #[default]
22 Vault,
23 /// Explicit opt-out: transcripts/sessions stay chain-verified (#6453) but not
24 /// downgrade-resistant against a whole-file strip.
25 None,
26}
27
28/// Configuration for vault-anchor downgrade-resistance (`[integrity]`, issue #6449).
29///
30/// # Examples
31///
32/// ```
33/// use zeph_config::{AnchorMode, IntegrityConfig};
34///
35/// let cfg: IntegrityConfig = toml::from_str("").unwrap();
36/// assert_eq!(cfg.anchor, AnchorMode::Vault);
37/// assert_eq!(cfg.max_session_anchors, 512);
38/// ```
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(default)]
41pub struct IntegrityConfig {
42 /// Vault-anchor downgrade-resistance posture.
43 pub anchor: AnchorMode,
44 /// Upper bound on the number of session anchors retained in the vault at once. Once
45 /// exceeded, the reconcile-and-cap sweep evicts the oldest anchors (ordered by the
46 /// vault-embedded `written_at` field, never filesystem mtime) down to this cap — those
47 /// sessions degrade to chain-only (#6453-level) protection, never a brick (an evicted
48 /// session still opens normally). Transcript anchors are independently bounded by
49 /// `subagent.transcript_max_files` and need no separate cap. Default `512`: typical
50 /// single-user/small-team deployments never evict.
51 pub max_session_anchors: usize,
52}
53
54impl Default for IntegrityConfig {
55 fn default() -> Self {
56 Self {
57 anchor: AnchorMode::Vault,
58 max_session_anchors: 512,
59 }
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66
67 #[test]
68 fn empty_table_yields_spec_defaults() {
69 let cfg: IntegrityConfig = toml::from_str("").unwrap();
70 assert_eq!(cfg, IntegrityConfig::default());
71 }
72
73 #[test]
74 fn anchor_none_deserializes() {
75 let cfg: IntegrityConfig = toml::from_str("anchor = \"none\"").unwrap();
76 assert_eq!(cfg.anchor, AnchorMode::None);
77 }
78
79 #[test]
80 fn max_session_anchors_is_overridable() {
81 let cfg: IntegrityConfig = toml::from_str("max_session_anchors = 10").unwrap();
82 assert_eq!(cfg.max_session_anchors, 10);
83 assert_eq!(
84 cfg.anchor,
85 AnchorMode::Vault,
86 "unspecified fields keep their default"
87 );
88 }
89}