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