1use crate::error::{Error, Result};
7
8#[derive(Debug, Clone)]
10pub struct SpawnOpts {
11 pub session_id: String,
12 pub permission_mode: Option<String>,
13 pub dangerous: bool,
15}
16
17pub trait Backend {
19 fn name(&self) -> &'static str;
21
22 fn spawn_command(&self, opts: &SpawnOpts) -> String;
24
25 fn plan_markers(&self) -> &'static [&'static str];
27
28 fn permission_markers(&self) -> &'static [&'static str];
30
31 fn trust_markers(&self) -> &'static [&'static str];
34
35 fn verified_versions(&self) -> &'static [&'static str];
38
39 fn installed_version(&self) -> Option<String>;
44}
45
46pub fn marker_warning(backend: &dyn Backend, installed: Option<&str>) -> Option<String> {
51 let extra = std::env::var("CSD_VERIFIED_VERSIONS").unwrap_or_default();
52 marker_warning_with(backend.name(), backend.verified_versions(), installed, &extra)
53}
54
55fn marker_warning_with(backend_name: &str, verified: &[&str], installed: Option<&str>, extra: &str) -> Option<String> {
56 let Some(installed) = installed else {
57 return Some(format!(
58 "could not determine the installed {backend_name} version; gate markers were \
59 verified on {verified:?} and may not match — gate detection \
60 (plan/permission/trust) can silently miss prompts"
61 ));
62 };
63 let extra_match = extra.split(',').map(str::trim).any(|v| !v.is_empty() && v == installed);
64 if verified.contains(&installed) || extra_match {
65 return None;
66 }
67 Some(format!(
68 "{backend_name} {installed} is not marker-verified (verified: {verified:?}); gate \
69 detection (plan/permission/trust) may silently miss prompts. If gates still work on \
70 {installed}, set CSD_VERIFIED_VERSIONS={installed} to silence this warning"
71 ))
72}
73
74fn parse_version_output(stdout: &str) -> Option<String> {
77 let token = stdout.split_whitespace().next()?;
78 token
79 .starts_with(|c: char| c.is_ascii_digit())
80 .then(|| token.to_string())
81}
82
83pub const CLAUDE_PERMISSION_MODES: &[&str] =
85 &["plan", "acceptEdits", "auto", "bypassPermissions", "default", "dontAsk"];
86
87pub struct Claude;
88
89impl Backend for Claude {
90 fn name(&self) -> &'static str {
91 "claude"
92 }
93
94 fn spawn_command(&self, opts: &SpawnOpts) -> String {
95 let mut cmd = format!(
98 "env -u CLAUDECODE -u CLAUDE_CODE_ENTRYPOINT claude --session-id {}",
99 opts.session_id
100 );
101 if let Some(mode) = &opts.permission_mode {
102 cmd.push_str(" --permission-mode ");
103 cmd.push_str(mode);
104 }
105 if opts.dangerous {
106 cmd.push_str(" --dangerously-skip-permissions");
107 }
108 cmd
109 }
110
111 fn plan_markers(&self) -> &'static [&'static str] {
112 &["Here is Claude's plan", "Would you like to proceed?"]
113 }
114
115 fn permission_markers(&self) -> &'static [&'static str] {
116 &["Do you want to proceed?", "Do you want to make this edit?"]
120 }
121
122 fn trust_markers(&self) -> &'static [&'static str] {
123 &[
124 "Is this a project you created or one you trust?",
125 "Yes, I trust this folder",
126 ]
127 }
128
129 fn verified_versions(&self) -> &'static [&'static str] {
130 &["2.1.158", "2.1.173"]
132 }
133
134 fn installed_version(&self) -> Option<String> {
135 let output = std::process::Command::new("claude").arg("--version").output().ok()?;
136 if !output.status.success() {
137 return None;
138 }
139 parse_version_output(&String::from_utf8_lossy(&output.stdout))
140 }
141}
142
143pub fn resolve(name: &str) -> Result<Box<dyn Backend>> {
145 match name {
146 "claude" => Ok(Box::new(Claude)),
147 other => Err(Error::UnknownBackend(other.to_string())),
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn parses_claude_version_output() {
157 assert_eq!(
158 parse_version_output("2.1.173 (Claude Code)\n"),
159 Some("2.1.173".to_string())
160 );
161 assert_eq!(parse_version_output("2.1.158"), Some("2.1.158".to_string()));
162 assert_eq!(parse_version_output("Claude Code 2.1.173"), None);
163 assert_eq!(parse_version_output(""), None);
164 assert_eq!(parse_version_output(" \n"), None);
165 }
166
167 #[test]
168 fn no_warning_on_verified_version() {
169 assert_eq!(marker_warning_with("claude", &["2.1.158"], Some("2.1.158"), ""), None);
170 }
171
172 #[test]
173 fn warns_on_unverified_version() {
174 let warning = marker_warning_with("claude", &["2.1.158"], Some("2.1.173"), "").unwrap();
175 assert!(warning.contains("2.1.173"));
176 assert!(warning.contains("2.1.158"));
177 assert!(warning.contains("CSD_VERIFIED_VERSIONS=2.1.173"));
178 }
179
180 #[test]
181 fn warns_on_unknown_version() {
182 let warning = marker_warning_with("claude", &["2.1.158"], None, "").unwrap();
183 assert!(warning.contains("could not determine"));
184 }
185
186 #[test]
187 fn env_override_extends_verified_set() {
188 let extra = "2.1.170, 2.1.173";
190 assert_eq!(
191 marker_warning_with("claude", &["2.1.158"], Some("2.1.173"), extra),
192 None
193 );
194 assert_eq!(
195 marker_warning_with("claude", &["2.1.158"], Some("2.1.170"), extra),
196 None
197 );
198 assert!(marker_warning_with("claude", &["2.1.158"], Some("2.1.171"), extra).is_some());
199 assert!(marker_warning_with("claude", &["2.1.158"], Some(""), ",").is_some());
201 }
202}