Skip to main content

ralph/config/
trust.rs

1//! Repo-local execution trust loading.
2//!
3//! Responsibilities:
4//! - Define the local trust file contract for execution-sensitive project settings.
5//! - Load `.ralph/trust.jsonc` files with JSONC support.
6//! - Provide helpers for source-aware trust checks during config resolution.
7//!
8//! Not handled here:
9//! - Main config layering or schema generation (see `crate::contracts::config`).
10//! - CI command validation or execution (see `crate::config::validation` and `crate::runutil`).
11//!
12//! Invariants/assumptions:
13//! - Trust is local-only and must not be committed to version control.
14//! - Missing trust files mean the repo is untrusted.
15
16use anyhow::{Context, Result};
17use chrono::{DateTime, Utc};
18use serde::{Deserialize, Serialize};
19use std::fs;
20use std::path::{Path, PathBuf};
21
22/// Local trust file for execution-sensitive project configuration.
23#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
24#[serde(default, deny_unknown_fields)]
25pub struct RepoTrust {
26    /// Allow repo-local executable configuration such as `agent.ci_gate`.
27    pub allow_project_commands: bool,
28
29    /// Timestamp for the explicit trust decision.
30    pub trusted_at: Option<DateTime<Utc>>,
31}
32
33impl RepoTrust {
34    pub fn is_trusted(&self) -> bool {
35        self.allow_project_commands
36    }
37}
38
39/// Preferred local trust path for a repository root.
40pub fn project_trust_path(repo_root: &Path) -> PathBuf {
41    repo_root.join(".ralph").join("trust.jsonc")
42}
43
44/// Load repo trust if present, otherwise return the default untrusted state.
45pub fn load_repo_trust(repo_root: &Path) -> Result<RepoTrust> {
46    let path = project_trust_path(repo_root);
47    if !path.exists() {
48        return Ok(RepoTrust::default());
49    }
50
51    let raw = fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?;
52    crate::jsonc::parse_jsonc::<RepoTrust>(&raw, &format!("trust {}", path.display()))
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use tempfile::TempDir;
59
60    #[test]
61    fn project_trust_path_is_jsonc_only() {
62        let repo_root = TempDir::new().expect("temp dir");
63        assert_eq!(
64            project_trust_path(repo_root.path()),
65            repo_root.path().join(".ralph/trust.jsonc")
66        );
67    }
68
69    #[test]
70    fn load_repo_trust_ignores_legacy_json_file() {
71        let repo_root = TempDir::new().expect("temp dir");
72        let ralph_dir = repo_root.path().join(".ralph");
73        fs::create_dir_all(&ralph_dir).expect("create .ralph");
74        fs::write(
75            ralph_dir.join("trust.json"),
76            r#"{"allow_project_commands":true}"#,
77        )
78        .expect("write legacy trust file");
79
80        assert_eq!(
81            load_repo_trust(repo_root.path()).expect("load trust"),
82            RepoTrust::default()
83        );
84    }
85}