grok-cli-acp 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/.env.example +42 -0
  2. package/.github/workflows/ci.yml +30 -0
  3. package/.github/workflows/rust.yml +22 -0
  4. package/.grok/.env.example +85 -0
  5. package/.grok/COMPLETE_FIX_SUMMARY.md +466 -0
  6. package/.grok/ENV_CONFIG_GUIDE.md +173 -0
  7. package/.grok/QUICK_REFERENCE.md +180 -0
  8. package/.grok/README.md +104 -0
  9. package/.grok/TESTING_GUIDE.md +393 -0
  10. package/CHANGELOG.md +465 -0
  11. package/CODE_REVIEW_SUMMARY.md +414 -0
  12. package/COMPLETE_FIX_SUMMARY.md +415 -0
  13. package/CONFIGURATION.md +489 -0
  14. package/CONTEXT_FILES_GUIDE.md +419 -0
  15. package/CONTRIBUTING.md +55 -0
  16. package/CURSOR_POSITION_FIX.md +206 -0
  17. package/Cargo.toml +88 -0
  18. package/ERROR_HANDLING_REPORT.md +361 -0
  19. package/FINAL_FIX_SUMMARY.md +462 -0
  20. package/FIXES.md +37 -0
  21. package/FIXES_SUMMARY.md +87 -0
  22. package/GROK_API_MIGRATION_SUMMARY.md +111 -0
  23. package/LICENSE +22 -0
  24. package/MIGRATION_TO_GROK_API.md +223 -0
  25. package/README.md +504 -0
  26. package/REVIEW_COMPLETE.md +416 -0
  27. package/REVIEW_QUICK_REFERENCE.md +173 -0
  28. package/SECURITY.md +463 -0
  29. package/SECURITY_AUDIT.md +661 -0
  30. package/SETUP.md +287 -0
  31. package/TESTING_TOOLS.md +88 -0
  32. package/TESTING_TOOL_EXECUTION.md +239 -0
  33. package/TOOL_EXECUTION_FIX.md +491 -0
  34. package/VERIFICATION_CHECKLIST.md +419 -0
  35. package/docs/API.md +74 -0
  36. package/docs/CHAT_LOGGING.md +39 -0
  37. package/docs/CURSOR_FIX_DEMO.md +306 -0
  38. package/docs/ERROR_HANDLING_GUIDE.md +547 -0
  39. package/docs/FILE_OPERATIONS.md +449 -0
  40. package/docs/INTERACTIVE.md +401 -0
  41. package/docs/PROJECT_CREATION_GUIDE.md +570 -0
  42. package/docs/QUICKSTART.md +378 -0
  43. package/docs/QUICK_REFERENCE.md +691 -0
  44. package/docs/RELEASE_NOTES_0.1.2.md +240 -0
  45. package/docs/TOOLS.md +459 -0
  46. package/docs/TOOLS_QUICK_REFERENCE.md +210 -0
  47. package/docs/ZED_INTEGRATION.md +371 -0
  48. package/docs/extensions.md +464 -0
  49. package/docs/settings.md +293 -0
  50. package/examples/extensions/logging-hook/README.md +91 -0
  51. package/examples/extensions/logging-hook/extension.json +22 -0
  52. package/package.json +30 -0
  53. package/scripts/test_acp.py +252 -0
  54. package/scripts/test_acp.sh +143 -0
  55. package/scripts/test_acp_simple.sh +72 -0
  56. package/src/acp/mod.rs +741 -0
  57. package/src/acp/protocol.rs +323 -0
  58. package/src/acp/security.rs +298 -0
  59. package/src/acp/tools.rs +697 -0
  60. package/src/bin/banner_demo.rs +216 -0
  61. package/src/bin/docgen.rs +18 -0
  62. package/src/bin/installer.rs +217 -0
  63. package/src/cli/app.rs +310 -0
  64. package/src/cli/commands/acp.rs +721 -0
  65. package/src/cli/commands/chat.rs +485 -0
  66. package/src/cli/commands/code.rs +513 -0
  67. package/src/cli/commands/config.rs +394 -0
  68. package/src/cli/commands/health.rs +442 -0
  69. package/src/cli/commands/history.rs +421 -0
  70. package/src/cli/commands/mod.rs +14 -0
  71. package/src/cli/commands/settings.rs +1384 -0
  72. package/src/cli/mod.rs +166 -0
  73. package/src/config/mod.rs +2212 -0
  74. package/src/display/ascii_art.rs +139 -0
  75. package/src/display/banner.rs +289 -0
  76. package/src/display/components/input.rs +323 -0
  77. package/src/display/components/mod.rs +2 -0
  78. package/src/display/components/settings_list.rs +306 -0
  79. package/src/display/interactive.rs +1255 -0
  80. package/src/display/mod.rs +62 -0
  81. package/src/display/terminal.rs +42 -0
  82. package/src/display/tips.rs +316 -0
  83. package/src/grok_client_ext.rs +177 -0
  84. package/src/hooks/loader.rs +407 -0
  85. package/src/hooks/mod.rs +158 -0
  86. package/src/lib.rs +174 -0
  87. package/src/main.rs +65 -0
  88. package/src/mcp/client.rs +195 -0
  89. package/src/mcp/config.rs +20 -0
  90. package/src/mcp/mod.rs +6 -0
  91. package/src/mcp/protocol.rs +67 -0
  92. package/src/utils/auth.rs +41 -0
  93. package/src/utils/chat_logger.rs +568 -0
  94. package/src/utils/context.rs +390 -0
  95. package/src/utils/mod.rs +16 -0
  96. package/src/utils/network.rs +320 -0
  97. package/src/utils/rate_limiter.rs +166 -0
  98. package/src/utils/session.rs +73 -0
  99. package/src/utils/shell_permissions.rs +389 -0
  100. package/src/utils/telemetry.rs +41 -0
@@ -0,0 +1,389 @@
1
+ //! Shell command permission and security system
2
+ //!
3
+ //! This module provides a permission system for shell commands executed in interactive mode,
4
+ //! similar to Gemini CLI's approach. It includes:
5
+ //! - Dangerous command detection (blocklist)
6
+ //! - User prompts for confirmation
7
+ //! - Session-level allowlist ("Always allow")
8
+ //! - Persistent policy storage
9
+
10
+ use anyhow::{anyhow, Result};
11
+ use colored::*;
12
+ use serde::{Deserialize, Serialize};
13
+ use std::collections::HashSet;
14
+ use std::fs;
15
+ use std::io::{self, Write};
16
+ use std::path::PathBuf;
17
+
18
+ /// Approval mode for shell commands
19
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20
+ #[derive(Default)]
21
+ pub enum ApprovalMode {
22
+ /// Ask for confirmation (default)
23
+ #[default]
24
+ Default,
25
+ /// Always allow without asking (DANGEROUS!)
26
+ Yolo,
27
+ }
28
+
29
+
30
+ /// Permission decision for a command
31
+ #[derive(Debug, Clone, PartialEq, Eq)]
32
+ pub enum PermissionDecision {
33
+ /// Allow execution
34
+ Allow,
35
+ /// Deny execution
36
+ Deny,
37
+ /// Allow and add to session allowlist
38
+ AllowAlways,
39
+ /// Blocked by policy (dangerous command)
40
+ Blocked(String),
41
+ }
42
+
43
+ /// Shell command permissions manager
44
+ #[derive(Debug, Clone)]
45
+ pub struct ShellPermissions {
46
+ /// Approval mode
47
+ approval_mode: ApprovalMode,
48
+ /// Session-level allowlist (commands allowed for this session)
49
+ session_allowlist: HashSet<String>,
50
+ /// Persistent allowlist (saved to disk)
51
+ persistent_allowlist: HashSet<String>,
52
+ /// Blocklist of dangerous commands
53
+ blocklist: HashSet<String>,
54
+ /// Path to persistent policy file
55
+ policy_path: Option<PathBuf>,
56
+ }
57
+
58
+ impl Default for ShellPermissions {
59
+ fn default() -> Self {
60
+ Self::new(ApprovalMode::Default)
61
+ }
62
+ }
63
+
64
+ impl ShellPermissions {
65
+ /// Create a new permissions manager
66
+ pub fn new(approval_mode: ApprovalMode) -> Self {
67
+ let mut permissions = Self {
68
+ approval_mode,
69
+ session_allowlist: HashSet::new(),
70
+ persistent_allowlist: HashSet::new(),
71
+ blocklist: Self::default_blocklist(),
72
+ policy_path: Self::get_policy_path().ok(),
73
+ };
74
+
75
+ // Load persistent allowlist from disk
76
+ if let Some(path) = &permissions.policy_path
77
+ && let Ok(policy) = Self::load_policy(path) {
78
+ permissions.persistent_allowlist = policy.allowed_commands;
79
+ }
80
+
81
+ permissions
82
+ }
83
+
84
+ /// Get the default blocklist of dangerous commands
85
+ fn default_blocklist() -> HashSet<String> {
86
+ let mut blocklist = HashSet::new();
87
+
88
+ // Destructive file operations
89
+ blocklist.insert("rm".to_string());
90
+ blocklist.insert("del".to_string());
91
+ blocklist.insert("rmdir".to_string());
92
+ blocklist.insert("rd".to_string());
93
+ blocklist.insert("format".to_string());
94
+ blocklist.insert("fdisk".to_string());
95
+ blocklist.insert("mkfs".to_string());
96
+
97
+ // System operations
98
+ blocklist.insert("shutdown".to_string());
99
+ blocklist.insert("reboot".to_string());
100
+ blocklist.insert("halt".to_string());
101
+ blocklist.insert("poweroff".to_string());
102
+ blocklist.insert("init".to_string());
103
+
104
+ // Dangerous shell operations
105
+ blocklist.insert(":(){ :|:& };:".to_string()); // Fork bomb
106
+ blocklist.insert("dd".to_string()); // Can overwrite disks
107
+
108
+ // Package management (can modify system)
109
+ blocklist.insert("apt-get".to_string());
110
+ blocklist.insert("yum".to_string());
111
+ blocklist.insert("dnf".to_string());
112
+ blocklist.insert("pacman".to_string());
113
+
114
+ blocklist
115
+ }
116
+
117
+ /// Get the path to the persistent policy file
118
+ fn get_policy_path() -> Result<PathBuf> {
119
+ let home_dir =
120
+ dirs::home_dir().ok_or_else(|| anyhow!("Could not determine home directory"))?;
121
+ Ok(home_dir.join(".grok").join("shell_policy.json"))
122
+ }
123
+
124
+ /// Load policy from disk
125
+ fn load_policy(path: &PathBuf) -> Result<ShellPolicy> {
126
+ let contents = fs::read_to_string(path)?;
127
+ let policy: ShellPolicy = serde_json::from_str(&contents)?;
128
+ Ok(policy)
129
+ }
130
+
131
+ /// Save policy to disk
132
+ fn save_policy(&self) -> Result<()> {
133
+ if let Some(path) = &self.policy_path {
134
+ // Create parent directory if it doesn't exist
135
+ if let Some(parent) = path.parent() {
136
+ fs::create_dir_all(parent)?;
137
+ }
138
+
139
+ let policy = ShellPolicy {
140
+ allowed_commands: self.persistent_allowlist.clone(),
141
+ };
142
+
143
+ let contents = serde_json::to_string_pretty(&policy)?;
144
+ fs::write(path, contents)?;
145
+ }
146
+ Ok(())
147
+ }
148
+
149
+ /// Extract the root command from a command string
150
+ fn extract_root_command(command: &str) -> String {
151
+ let trimmed = command.trim();
152
+
153
+ // Handle shell operators
154
+ let command_part = trimmed
155
+ .split(&['|', '&', ';', '>', '<'][..])
156
+ .next()
157
+ .unwrap_or(trimmed)
158
+ .trim();
159
+
160
+ // Extract just the command name (first word)
161
+ command_part
162
+ .split_whitespace()
163
+ .next()
164
+ .unwrap_or("")
165
+ .to_string()
166
+ }
167
+
168
+ /// Check if a command is blocked by policy
169
+ pub fn is_blocked(&self, command: &str) -> Option<String> {
170
+ let root_command = Self::extract_root_command(command);
171
+
172
+ if self.blocklist.contains(&root_command) {
173
+ return Some(format!(
174
+ "Command '{}' is blocked for security reasons",
175
+ root_command
176
+ ));
177
+ }
178
+
179
+ // Check for dangerous patterns
180
+ if command.contains("rm -rf /") || command.contains("del /s /q") {
181
+ return Some("Dangerous recursive delete pattern detected".to_string());
182
+ }
183
+
184
+ if command.contains(":(){ :|:& };:") {
185
+ return Some("Fork bomb pattern detected".to_string());
186
+ }
187
+
188
+ None
189
+ }
190
+
191
+ /// Check if a command is in the allowlist (session or persistent)
192
+ pub fn is_allowed(&self, command: &str) -> bool {
193
+ let root_command = Self::extract_root_command(command);
194
+
195
+ self.session_allowlist.contains(&root_command)
196
+ || self.persistent_allowlist.contains(&root_command)
197
+ }
198
+
199
+ /// Add a command to the session allowlist
200
+ pub fn add_to_session_allowlist(&mut self, command: &str) {
201
+ let root_command = Self::extract_root_command(command);
202
+ self.session_allowlist.insert(root_command);
203
+ }
204
+
205
+ /// Add a command to the persistent allowlist and save to disk
206
+ pub fn add_to_persistent_allowlist(&mut self, command: &str) -> Result<()> {
207
+ let root_command = Self::extract_root_command(command);
208
+ self.persistent_allowlist.insert(root_command);
209
+ self.save_policy()
210
+ }
211
+
212
+ /// Prompt the user for permission to execute a command
213
+ pub fn prompt_for_permission(&mut self, command: &str) -> Result<PermissionDecision> {
214
+ // In YOLO mode, always allow
215
+ if self.approval_mode == ApprovalMode::Yolo {
216
+ return Ok(PermissionDecision::Allow);
217
+ }
218
+
219
+ // Check if blocked
220
+ if let Some(reason) = self.is_blocked(command) {
221
+ return Ok(PermissionDecision::Blocked(reason));
222
+ }
223
+
224
+ // Check if already allowed
225
+ if self.is_allowed(command) {
226
+ return Ok(PermissionDecision::Allow);
227
+ }
228
+
229
+ // Prompt user
230
+ println!();
231
+ println!(
232
+ "{} {}",
233
+ "⚠️ Shell command requires permission:".yellow().bold(),
234
+ command.bright_yellow()
235
+ );
236
+ println!();
237
+ println!(" {} Allow once", "a)".bright_cyan());
238
+ println!(" {} Allow always (this session)", "s)".bright_cyan());
239
+ println!(" {} Allow always (save permanently)", "p)".bright_cyan());
240
+ println!(" {} Deny", "d)".bright_cyan());
241
+ println!();
242
+ print!("{} ", "Choose [a/s/p/d]:".bright_white().bold());
243
+ io::stdout().flush()?;
244
+
245
+ let mut input = String::new();
246
+ io::stdin().read_line(&mut input)?;
247
+ let choice = input.trim().to_lowercase();
248
+
249
+ match choice.as_str() {
250
+ "a" | "allow" => Ok(PermissionDecision::Allow),
251
+ "s" | "session" => {
252
+ self.add_to_session_allowlist(command);
253
+ println!("{}", "✓ Added to session allowlist".bright_green());
254
+ Ok(PermissionDecision::AllowAlways)
255
+ }
256
+ "p" | "permanent" => {
257
+ self.add_to_persistent_allowlist(command)?;
258
+ println!("{}", "✓ Added to permanent allowlist".bright_green());
259
+ Ok(PermissionDecision::AllowAlways)
260
+ }
261
+ "d" | "deny" | "n" | "no" => {
262
+ println!("{}", "✗ Command denied".bright_red());
263
+ Ok(PermissionDecision::Deny)
264
+ }
265
+ "" => {
266
+ // Default to deny on empty input
267
+ println!("{}", "✗ Command denied (no input)".bright_red());
268
+ Ok(PermissionDecision::Deny)
269
+ }
270
+ _ => {
271
+ println!("{}", "Invalid choice, defaulting to deny".bright_red());
272
+ Ok(PermissionDecision::Deny)
273
+ }
274
+ }
275
+ }
276
+
277
+ /// Check if a command should be executed (main entry point)
278
+ pub fn should_execute(&mut self, command: &str) -> Result<bool> {
279
+ let decision = self.prompt_for_permission(command)?;
280
+
281
+ match decision {
282
+ PermissionDecision::Allow | PermissionDecision::AllowAlways => Ok(true),
283
+ PermissionDecision::Deny => Ok(false),
284
+ PermissionDecision::Blocked(reason) => {
285
+ eprintln!("{} {}", "✗ Blocked:".bright_red().bold(), reason.red());
286
+ Ok(false)
287
+ }
288
+ }
289
+ }
290
+
291
+ /// Get approval mode
292
+ pub fn approval_mode(&self) -> ApprovalMode {
293
+ self.approval_mode
294
+ }
295
+
296
+ /// Set approval mode
297
+ pub fn set_approval_mode(&mut self, mode: ApprovalMode) {
298
+ self.approval_mode = mode;
299
+ }
300
+
301
+ /// Clear session allowlist
302
+ pub fn clear_session_allowlist(&mut self) {
303
+ self.session_allowlist.clear();
304
+ }
305
+
306
+ /// Get session allowlist
307
+ pub fn get_session_allowlist(&self) -> &HashSet<String> {
308
+ &self.session_allowlist
309
+ }
310
+
311
+ /// Get persistent allowlist
312
+ pub fn get_persistent_allowlist(&self) -> &HashSet<String> {
313
+ &self.persistent_allowlist
314
+ }
315
+
316
+ /// Remove a command from persistent allowlist
317
+ pub fn remove_from_persistent_allowlist(&mut self, command: &str) -> Result<()> {
318
+ self.persistent_allowlist.remove(command);
319
+ self.save_policy()
320
+ }
321
+
322
+ /// Reset persistent allowlist (clear all saved permissions)
323
+ pub fn reset_persistent_allowlist(&mut self) -> Result<()> {
324
+ self.persistent_allowlist.clear();
325
+ self.save_policy()
326
+ }
327
+ }
328
+
329
+ /// Persistent policy stored on disk
330
+ #[derive(Debug, Clone, Serialize, Deserialize)]
331
+ struct ShellPolicy {
332
+ allowed_commands: HashSet<String>,
333
+ }
334
+
335
+ #[cfg(test)]
336
+ mod tests {
337
+ use super::*;
338
+
339
+ #[test]
340
+ fn test_extract_root_command() {
341
+ assert_eq!(
342
+ ShellPermissions::extract_root_command("ls -la"),
343
+ "ls".to_string()
344
+ );
345
+ assert_eq!(
346
+ ShellPermissions::extract_root_command("git status | grep modified"),
347
+ "git".to_string()
348
+ );
349
+ assert_eq!(
350
+ ShellPermissions::extract_root_command("echo hello && echo world"),
351
+ "echo".to_string()
352
+ );
353
+ assert_eq!(
354
+ ShellPermissions::extract_root_command(" pwd "),
355
+ "pwd".to_string()
356
+ );
357
+ }
358
+
359
+ #[test]
360
+ fn test_blocked_commands() {
361
+ let perms = ShellPermissions::new(ApprovalMode::Default);
362
+
363
+ assert!(perms.is_blocked("rm -rf /").is_some());
364
+ assert!(perms.is_blocked("shutdown now").is_some());
365
+ assert!(perms.is_blocked("format c:").is_some());
366
+ assert!(perms.is_blocked("ls -la").is_none());
367
+ assert!(perms.is_blocked("git status").is_none());
368
+ }
369
+
370
+ #[test]
371
+ fn test_allowlist() {
372
+ let mut perms = ShellPermissions::new(ApprovalMode::Default);
373
+
374
+ assert!(!perms.is_allowed("git status"));
375
+
376
+ perms.add_to_session_allowlist("git status");
377
+ assert!(perms.is_allowed("git status"));
378
+ assert!(perms.is_allowed("git commit")); // Same root command
379
+ }
380
+
381
+ #[test]
382
+ fn test_yolo_mode() {
383
+ let mut perms = ShellPermissions::new(ApprovalMode::Yolo);
384
+
385
+ // In YOLO mode, non-blocked commands are always allowed
386
+ let decision = perms.prompt_for_permission("ls -la").unwrap();
387
+ assert_eq!(decision, PermissionDecision::Allow);
388
+ }
389
+ }
@@ -0,0 +1,41 @@
1
+ use chrono::Utc;
2
+ use serde_json::{json, Value};
3
+ use std::fs::OpenOptions;
4
+ use std::io::Write;
5
+ use std::path::PathBuf;
6
+ use std::sync::Mutex;
7
+
8
+ struct TelemetryState {
9
+ enabled: bool,
10
+ log_file: Option<PathBuf>,
11
+ }
12
+
13
+ static TELEMETRY_STATE: Mutex<TelemetryState> = Mutex::new(TelemetryState {
14
+ enabled: false,
15
+ log_file: None,
16
+ });
17
+
18
+ pub fn init(enabled: bool, log_path: Option<PathBuf>) {
19
+ let mut state = TELEMETRY_STATE.lock().unwrap();
20
+ state.enabled = enabled;
21
+ state.log_file = log_path;
22
+ }
23
+
24
+ pub fn track_event(event: &str, properties: Value) {
25
+ let state = TELEMETRY_STATE.lock().unwrap();
26
+ if !state.enabled {
27
+ return;
28
+ }
29
+
30
+ let timestamp = Utc::now().to_rfc3339();
31
+ let log_entry = json!({
32
+ "timestamp": timestamp,
33
+ "event": event,
34
+ "properties": properties
35
+ });
36
+
37
+ if let Some(path) = &state.log_file
38
+ && let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
39
+ let _ = writeln!(file, "{}", log_entry);
40
+ }
41
+ }