Skip to main content

forge_engine/
invariants.rs

1use std::path::Path;
2
3use crate::error::{ForgeError, ForgeResult};
4use crate::store::schema;
5
6/// Refuses to open a database that is not a verified Forge DB.
7///
8/// Checks (all must pass):
9/// 1. File must be a valid SQLite database (magic bytes check).
10/// 2. `PRAGMA user_version` must be within valid range.
11/// 3. Table `forge_meta` must exist.
12/// 4. `forge_meta` row `key = 'schema_hash'` must exist.
13/// 5. `forge_meta.value` for `schema_hash` must equal the compiled-in `FORGE_SCHEMA_HASH`.
14pub fn refuse_to_open_db(path: &Path) -> ForgeResult<()> {
15    let spec = forge_policy::DbIdentitySpec {
16        min_user_version: schema::FORGE_MIN_USER_VERSION,
17        max_user_version: schema::FORGE_MAX_USER_VERSION,
18        required_tables: schema::REQUIRED_TABLES,
19        schema_table: "forge_meta",
20        schema_key: "schema_hash",
21        expected_schema_hash: schema::forge_schema_hash(),
22    };
23
24    forge_policy::verify_sqlite_db_identity(path, &spec).map_err(|error| match error {
25        forge_policy::PolicyError::RefuseToOpenDb { reason } => {
26            ForgeError::RefuseToOpenDb { reason }
27        }
28        other => ForgeError::RefuseToOpenDb {
29            reason: other.to_string(),
30        },
31    })
32}
33
34/// Validates that patches do not touch forbidden paths.
35pub fn validate_forbidden_paths(
36    paths: &[&str],
37    forbidden_patterns: &[String],
38    allow_test_modifications: bool,
39) -> Vec<crate::error::Violation> {
40    let owned_paths: Vec<String> = paths.iter().map(|path| (*path).to_string()).collect();
41    forge_policy::validate_forbidden_paths(
42        &owned_paths,
43        forbidden_patterns,
44        allow_test_modifications,
45    )
46}
47
48/// Validates patch caps (max files, max total lines, max lines per file).
49pub fn validate_patch_caps(
50    files_changed: usize,
51    total_lines_changed: usize,
52    per_file_lines: &[usize],
53    max_files: usize,
54    max_total_lines: usize,
55    max_per_file: usize,
56) -> Vec<crate::error::Violation> {
57    forge_policy::validate_patch_caps(
58        files_changed,
59        total_lines_changed,
60        per_file_lines,
61        max_files,
62        max_total_lines,
63        max_per_file,
64    )
65}
66
67/// Validates that a CEA node contains no raw source code.
68/// Checks recursively through all nested objects and arrays.
69pub fn validate_cea_no_raw_source(sig_json: &str) -> ForgeResult<()> {
70    let value: serde_json::Value =
71        serde_json::from_str(sig_json).map_err(|_| ForgeError::CeaRawSourceDetected)?;
72    check_value_no_raw_source(&value)
73}
74
75/// Recursively check a JSON value for forbidden raw-source fields.
76fn check_value_no_raw_source(value: &serde_json::Value) -> ForgeResult<()> {
77    match value {
78        serde_json::Value::Object(obj) => {
79            for key in &["context_before", "context_after", "raw_source", "raw_lines"] {
80                if obj.contains_key(*key) {
81                    return Err(ForgeError::CeaRawSourceDetected);
82                }
83            }
84
85            // Validate context_hash is a hex string (blake3 = 64 hex chars)
86            if let Some(hash) = obj.get("context_hash") {
87                if let Some(s) = hash.as_str() {
88                    if s.len() != 64 || !s.chars().all(|c| c.is_ascii_hexdigit()) {
89                        return Err(ForgeError::CeaRawSourceDetected);
90                    }
91                }
92            }
93
94            // Check file_extension is just an extension, not a full path
95            if let Some(ext) = obj.get("file_extension") {
96                if let Some(s) = ext.as_str() {
97                    if s.contains('/') || s.contains('\\') || s.len() > 10 {
98                        return Err(ForgeError::CeaRawSourceDetected);
99                    }
100                }
101            }
102
103            // Recurse into all values
104            for v in obj.values() {
105                check_value_no_raw_source(v)?;
106            }
107        }
108        serde_json::Value::Array(arr) => {
109            for v in arr {
110                check_value_no_raw_source(v)?;
111            }
112        }
113        _ => {}
114    }
115    Ok(())
116}