//! 提交冻结机械闸(39 治理) — AI 引擎派发命令的 git commit 拦截。 //! //! 冻结期间(用户 🔒 令)禁止引擎/挂机会话执行 git commit:命令在 spawn 前被拒, //! 改动留在工作区。用户本人终端不受影响(本闸只作用于引擎 execute 入口)。 //! //! 冻结标志解析(优先级:环境变量 DEVFLOW_COMMIT_FROZEN > env 内 DevFlow 标志): //! - env::var("DEVFLOW_COMMIT_FROZEN")="1"/"true" → 冻结 //! - ShellRequest.env 同名键同样生效(测试/调用方定向注入) //! 默认不冻结(用户本人正常提交流程零影响)。 //! //! 拦截范围:命令子串含 `git commit`(含 `git -C x commit`)即拒;`git commit --amend` //! 同拒。`git log/diff/status/add/push` 不拦(只冻结提交动作)。 //! //! 提示文案给 LLM 指明出路:改留工作区,等「攒批提交通知」。 use std::sync::atomic::{AtomicBool, Ordering}; use crate::shell::ShellRequest; /// 进程级冻结标志(引擎侧写入:unattended 轮询每轮按看板 🔒 行 + KV 覆盖同步)。 /// 用静态位而非 std::env::set_var(Rust 2024 edition set_var unsafe 且多线程 UB)。 static FROZEN: AtomicBool = AtomicBool::new(false); /// 引擎侧设置/解除冻结(unattended 轮询每轮调用)。 pub fn set_frozen(v: bool) { FROZEN.store(v, Ordering::SeqCst); } /// 环境变量真值判定:"1"/"true"(含大小写变体)为冻结,其余("0"/"false"/空)为不冻结。 fn env_val_is_truthy(v: &str) -> bool { matches!(v, "1" | "true" | "TRUE" | "True") } /// 是否启用冻结:静态标志(引擎同步) 或 环境变量 DEVFLOW_COMMIT_FROZEN(调用方/测试定向注入)。 pub fn is_frozen(extra_env: Option<&std::collections::HashMap>) -> bool { if FROZEN.load(Ordering::SeqCst) { return true; } if let Some(v) = extra_env.and_then(|m| m.get("DEVFLOW_COMMIT_FROZEN")) { return env_val_is_truthy(v); } std::env::var("DEVFLOW_COMMIT_FROZEN") .map(|v| env_val_is_truthy(&v)) .unwrap_or(false) } /// 命令是否为 git commit 提交动作(token 判据,含 -C 前缀形态与复合命令)。 pub fn is_commit_command(command: &str) -> bool { // 按常见命令分隔符切段( ; && || | 换行),每段独立判 git commit(防 "x; git commit" 绕过) command .split(|c: char| matches!(c, ';' | '|' | '&' | '\n')) .any(is_commit_segment) } /// 单段判据:token 找 "git",跳过全局选项(-C path 等)后是否接 "commit"。 fn is_commit_segment(segment: &str) -> bool { let tokens: Vec = segment .split_whitespace() .map(|t| t.trim_matches(|c: char| c == '"' || c == '\'').to_lowercase()) .collect(); for (i, t) in tokens.iter().enumerate() { if t == "git" { let mut j = i + 1; while j < tokens.len() { match tokens[j].as_str() { // `-c key=val` 两 token 形态:值在下一 token,跳 2 "-c" => j += 2, // `--git-dir=path`/`--work-tree=path` 单 token 合并形态:自含值,跳 1 t if t.starts_with("--git-dir=") || t.starts_with("--work-tree=") => j += 1, // `--git-dir path`/`--work-tree path` 两 token 形态:跳 2 "--git-dir" | "--work-tree" => j += 2, "commit" => return true, _ => break, } } } } false } /// 冻结闸检查入口:冻结在场且命令是 git commit → 返回拒绝 Err(不 spawn)。 pub fn check(request: &ShellRequest) -> Result<(), String> { if !is_frozen(Some(&request.env)) { return Ok(()); } if is_commit_command(&request.command) { return Err(format!( "[提交冻结令] 检测到 git commit 命令,当前处于提交冻结期(用户 🔒 令),已拒绝执行: \ 改动保留在工作区,等用户「攒批提交通知」后才可提交。命令: {}", truncate_cmd(&request.command) )); } Ok(()) } fn truncate_cmd(s: &str) -> String { if s.chars().count() <= 120 { s.to_string() } else { let cut: String = s.chars().take(120).collect(); format!("{}…", cut) } } #[cfg(test)] mod tests { use super::*; use std::collections::HashMap; fn req(cmd: &str) -> ShellRequest { ShellRequest { command: cmd.to_string(), working_dir: None, env: HashMap::new(), timeout_secs: None, shell_type: None, } } fn req_env(cmd: &str, env: &[(&str, &str)]) -> ShellRequest { let mut r = req(cmd); r.env = env .iter() .map(|(k, v)| (k.to_string(), v.to_string())) .collect(); r } #[test] fn commit_detection_covers_variants() { assert!(is_commit_command("git commit -m 'x'")); assert!(is_commit_command("git -C E:/repo commit -m x")); assert!(is_commit_command("git commit --amend")); assert!(is_commit_command("git commit -m y")); assert!(is_commit_command("git commit; git push")); // PowerShell / sh 包裹形态 assert!(is_commit_command("powershell -Command git commit -m x")); assert!(is_commit_command("cd repo && git commit -m x")); assert!(is_commit_command("git add . && git commit -m x")); } #[test] fn non_commit_git_not_blocked() { assert!(!is_commit_command("git log -1")); assert!(!is_commit_command("git status")); assert!(!is_commit_command("git diff --stat")); assert!(!is_commit_command("git add .")); assert!(!is_commit_command("git push origin main")); assert!(!is_commit_command("cargo test")); // commitment 等误命中防护:commit 后无后续 token 才算 assert!(!is_commit_command("git log --grep commit")); } #[test] fn freeze_gate_blocks_commit_when_frozen() { let r = req_env("git commit -m '细项批'", &[("DEVFLOW_COMMIT_FROZEN", "1")]); assert!(check(&r).is_err(), "冻结在场 commit 必须被拒"); let r2 = req_env("git log -1", &[("DEVFLOW_COMMIT_FROZEN", "1")]); assert!(check(&r2).is_ok(), "冻结只拦 commit 不拦只读命令"); } /// 解冻态放行用例:持与 static_flag_toggle 同一把串行闸——静态位是进程级单源, /// 置位瞬间并行跑到本用例会误判冻结(实测互踩),凡依赖「静态位=false」断言的用例一律持闸。 #[test] fn no_freeze_no_block() { let _gate = crate::shell::PWSH_CACHE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()); let r = req("git commit -m '用户本人提交'"); assert!(check(&r).is_ok(), "无冻结标志零影响(用户正常提交不受影响)"); } #[test] fn frozen_false_value_no_block() { let _gate = crate::shell::PWSH_CACHE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()); let r = req_env("git commit -m x", &[("DEVFLOW_COMMIT_FROZEN", "0")]); assert!(check(&r).is_ok(), "显式关闭标志不拦(解冻态)"); } /// 静态标志(引擎同步路径)切换:置位拦 commit,复位恢复。 /// 串行闸:静态位是进程级单源,置位期间并行的 env 用例(check 走 is_frozen 静态位短路)会互踩, /// 持锁串行 + 进出恢复默认(false)防污染。 #[test] fn static_flag_toggle() { // 复用 PWSH_CACHE_TEST_LOCK 串行闸(进程级互斥,专用闸后续有需要再单立) let _gate = crate::shell::PWSH_CACHE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()); set_frozen(true); assert!(check(&req("git commit -m x")).is_err(), "静态冻结位拦 commit"); assert!(check(&req("git diff")).is_ok(), "静态冻结位不拦只读命令"); set_frozen(false); assert!(check(&req("git commit -m x")).is_ok(), "复位后恢复放行"); } }