lean_ctx/core/
workspace_args.rs1pub struct Parsed {
7 pub verb: String,
8 pub positional: Option<String>,
9 pub file: Option<String>,
10 pub id: Option<String>,
11 pub expect_revision: Option<String>,
12 pub read_only: bool,
13 pub json: bool,
14}
15
16pub fn parse(args: &[String]) -> Result<Parsed, String> {
18 let mut parsed = Parsed {
19 verb: String::new(),
20 positional: None,
21 file: None,
22 id: None,
23 expect_revision: None,
24 read_only: false,
25 json: false,
26 };
27 let mut words = parse_into(args, &mut parsed)?.into_iter();
28 parsed.verb = words.next().ok_or("a command is required")?;
29 parsed.positional = words.next();
30 if let Some(extra) = words.next() {
31 return Err(format!("unexpected argument: {extra}"));
32 }
33 Ok(parsed)
34}
35
36fn parse_into(args: &[String], parsed: &mut Parsed) -> Result<Vec<String>, String> {
39 let mut words = Vec::new();
40 let mut rest = args.iter();
41 while let Some(arg) = rest.next() {
42 match arg.as_str() {
43 "--json" => parsed.json = true,
44 "--read-only" => parsed.read_only = true,
45 "--file" | "--id" | "--expect-revision" => {
46 let value = rest
47 .next()
48 .ok_or_else(|| format!("{arg} needs a value"))?
49 .clone();
50 let slot = match arg.as_str() {
51 "--file" => &mut parsed.file,
52 "--id" => &mut parsed.id,
53 _ => &mut parsed.expect_revision,
54 };
55 if slot.replace(value).is_some() {
56 return Err(format!("{arg} is given twice"));
57 }
58 }
59 flag if flag.starts_with("--") => match flag.split_once('=') {
60 Some((name, _)) if matches!(name, "--json" | "--read-only") => {
61 return Err(format!("{name} takes no value"));
62 }
63 Some((name, value)) => {
64 let pair = [name.to_string(), value.to_string()];
65 words.extend(parse_into(&pair, parsed)?);
66 }
67 None => return Err(format!("unknown argument: {flag}")),
68 },
69 _ => words.push(arg.clone()),
70 }
71 }
72 Ok(words)
73}
74
75#[doc(hidden)]
79pub fn agent_shell_may_run(args: &[String]) -> bool {
80 if args
81 .first()
82 .is_none_or(|first| matches!(first.as_str(), "-h" | "--help" | "help"))
83 {
84 return true;
85 }
86 parse(args)
87 .is_ok_and(|parsed| matches!(parsed.verb.as_str(), "show" | "diagnose" | "remove" | "rm"))
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93
94 fn words(text: &str) -> Vec<String> {
95 text.split_whitespace().map(str::to_string).collect()
96 }
97
98 #[test]
99 fn agent_shells_may_only_read_or_narrow() {
100 for allowed in [
101 "",
102 "--help",
103 "show",
104 "diagnose --json",
105 "remove --file /w /x",
106 ] {
107 assert!(agent_shell_may_run(&words(allowed)), "{allowed}");
108 }
109 for refused in [
110 "add /etc",
111 "--file /w.json --expect-revision 3 add /etc",
112 "--json add /etc",
113 "--read-only add /etc",
114 "--json=x add /etc",
115 "create --id x /code",
116 "--id x create /code",
117 "frobnicate",
118 ] {
119 assert!(!agent_shell_may_run(&words(refused)), "{refused}");
120 }
121 }
122}