Skip to main content

zeroclaw_runtime/security/
mod.rs

1//! Security subsystem for policy enforcement, sandboxing, and secret management.
2//!
3//! This module provides the security infrastructure for ZeroClaw. The core type
4//! [`SecurityPolicy`] defines autonomy levels, workspace boundaries, and
5//! access-control rules that are enforced across the tool and runtime subsystems.
6//! [`PairingGuard`] implements device pairing for channel authentication, and
7//! [`SecretStore`] handles encrypted credential storage.
8//!
9//! OS-level isolation is provided through the [`Sandbox`] trait defined in
10//! [`traits`], with pluggable backends including Docker, Firejail, Bubblewrap,
11//! and Landlock. The [`create_sandbox`] function selects the best available
12//! backend at runtime. An [`AuditLogger`] records security-relevant events for
13//! forensic review.
14//!
15//! # Extension
16//!
17//! To add a new sandbox backend, implement [`Sandbox`] in a new submodule and
18//! register it in [`detect::create_sandbox`]. See `AGENTS.md` §7.5 for security
19//! change guidelines.
20
21pub mod audit;
22#[cfg(feature = "sandbox-bubblewrap")]
23pub mod bubblewrap;
24pub mod detect;
25pub mod docker;
26
27// Prompt injection defense (contributed from RustyClaw, MIT licensed)
28pub mod domain_matcher;
29pub mod estop;
30#[cfg(target_os = "linux")]
31pub mod firejail;
32pub mod iam_policy;
33#[cfg(feature = "sandbox-landlock")]
34pub mod landlock;
35pub mod leak_detector;
36pub mod nevis;
37pub mod otp;
38pub mod pairing;
39pub mod playbook;
40pub mod policy;
41pub mod prompt_guard;
42#[cfg(target_os = "macos")]
43pub mod seatbelt;
44pub mod secrets;
45pub mod traits;
46pub mod vulnerability;
47#[cfg(feature = "webauthn")]
48pub mod webauthn;
49
50#[allow(unused_imports)]
51pub use audit::{AuditEvent, AuditEventType, AuditLogger};
52#[allow(unused_imports)]
53pub use detect::create_sandbox;
54pub use detect::linux_memcg_available;
55pub use domain_matcher::DomainMatcher;
56#[allow(unused_imports)]
57pub use estop::{EstopLevel, EstopManager, EstopState, ResumeSelector};
58#[allow(unused_imports)]
59pub use otp::OtpValidator;
60#[allow(unused_imports)]
61pub use pairing::PairingGuard;
62pub use policy::{AutonomyLevel, SecurityPolicy};
63#[allow(unused_imports)]
64pub use secrets::SecretStore;
65#[allow(unused_imports)]
66pub use traits::{NoopSandbox, Sandbox};
67// Nevis IAM integration
68#[allow(unused_imports)]
69pub use iam_policy::{IamPolicy, PolicyDecision};
70#[allow(unused_imports)]
71pub use nevis::{NevisAuthProvider, NevisIdentity};
72// Prompt injection defense exports
73#[allow(unused_imports)]
74pub use leak_detector::{LeakDetector, LeakResult};
75#[allow(unused_imports)]
76pub use prompt_guard::{GuardAction, GuardResult, PromptGuard};
77
78/// Redact sensitive values for safe logging. Shows first 4 characters + "***" suffix.
79/// Uses char-boundary-safe indexing to avoid panics on multi-byte UTF-8 strings.
80/// This function intentionally breaks the data-flow taint chain for static analysis.
81pub fn redact(value: &str) -> String {
82    let char_count = value.chars().count();
83    if char_count <= 4 {
84        "***".to_string()
85    } else {
86        let prefix: String = value.chars().take(4).collect();
87        format!("{prefix}***")
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn reexported_policy_and_pairing_types_are_usable() {
97        let policy = SecurityPolicy::default();
98        assert_eq!(policy.autonomy, AutonomyLevel::Supervised);
99
100        let guard = PairingGuard::new(false, &[]);
101        assert!(!guard.require_pairing());
102    }
103
104    #[test]
105    fn reexported_secret_store_encrypt_decrypt_roundtrip() {
106        let temp = tempfile::tempdir().unwrap();
107        let store = SecretStore::new(temp.path(), false);
108
109        let encrypted = store.encrypt("top-secret").unwrap();
110        let decrypted = store.decrypt(&encrypted).unwrap();
111
112        assert_eq!(decrypted, "top-secret");
113    }
114
115    #[test]
116    fn redact_hides_most_of_value() {
117        assert_eq!(redact("abcdefgh"), "abcd***");
118        assert_eq!(redact("ab"), "***");
119        assert_eq!(redact(""), "***");
120        assert_eq!(redact("12345"), "1234***");
121    }
122
123    #[test]
124    fn redact_handles_multibyte_utf8_without_panic() {
125        // CJK characters are 3 bytes each; slicing at byte 4 would panic
126        // without char-boundary-safe handling.
127        let result = redact("密码是很长的秘密");
128        assert!(result.ends_with("***"));
129        assert!(result.is_char_boundary(result.len()));
130    }
131}