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.
- package/.env.example +42 -0
- package/.github/workflows/ci.yml +30 -0
- package/.github/workflows/rust.yml +22 -0
- package/.grok/.env.example +85 -0
- package/.grok/COMPLETE_FIX_SUMMARY.md +466 -0
- package/.grok/ENV_CONFIG_GUIDE.md +173 -0
- package/.grok/QUICK_REFERENCE.md +180 -0
- package/.grok/README.md +104 -0
- package/.grok/TESTING_GUIDE.md +393 -0
- package/CHANGELOG.md +465 -0
- package/CODE_REVIEW_SUMMARY.md +414 -0
- package/COMPLETE_FIX_SUMMARY.md +415 -0
- package/CONFIGURATION.md +489 -0
- package/CONTEXT_FILES_GUIDE.md +419 -0
- package/CONTRIBUTING.md +55 -0
- package/CURSOR_POSITION_FIX.md +206 -0
- package/Cargo.toml +88 -0
- package/ERROR_HANDLING_REPORT.md +361 -0
- package/FINAL_FIX_SUMMARY.md +462 -0
- package/FIXES.md +37 -0
- package/FIXES_SUMMARY.md +87 -0
- package/GROK_API_MIGRATION_SUMMARY.md +111 -0
- package/LICENSE +22 -0
- package/MIGRATION_TO_GROK_API.md +223 -0
- package/README.md +504 -0
- package/REVIEW_COMPLETE.md +416 -0
- package/REVIEW_QUICK_REFERENCE.md +173 -0
- package/SECURITY.md +463 -0
- package/SECURITY_AUDIT.md +661 -0
- package/SETUP.md +287 -0
- package/TESTING_TOOLS.md +88 -0
- package/TESTING_TOOL_EXECUTION.md +239 -0
- package/TOOL_EXECUTION_FIX.md +491 -0
- package/VERIFICATION_CHECKLIST.md +419 -0
- package/docs/API.md +74 -0
- package/docs/CHAT_LOGGING.md +39 -0
- package/docs/CURSOR_FIX_DEMO.md +306 -0
- package/docs/ERROR_HANDLING_GUIDE.md +547 -0
- package/docs/FILE_OPERATIONS.md +449 -0
- package/docs/INTERACTIVE.md +401 -0
- package/docs/PROJECT_CREATION_GUIDE.md +570 -0
- package/docs/QUICKSTART.md +378 -0
- package/docs/QUICK_REFERENCE.md +691 -0
- package/docs/RELEASE_NOTES_0.1.2.md +240 -0
- package/docs/TOOLS.md +459 -0
- package/docs/TOOLS_QUICK_REFERENCE.md +210 -0
- package/docs/ZED_INTEGRATION.md +371 -0
- package/docs/extensions.md +464 -0
- package/docs/settings.md +293 -0
- package/examples/extensions/logging-hook/README.md +91 -0
- package/examples/extensions/logging-hook/extension.json +22 -0
- package/package.json +30 -0
- package/scripts/test_acp.py +252 -0
- package/scripts/test_acp.sh +143 -0
- package/scripts/test_acp_simple.sh +72 -0
- package/src/acp/mod.rs +741 -0
- package/src/acp/protocol.rs +323 -0
- package/src/acp/security.rs +298 -0
- package/src/acp/tools.rs +697 -0
- package/src/bin/banner_demo.rs +216 -0
- package/src/bin/docgen.rs +18 -0
- package/src/bin/installer.rs +217 -0
- package/src/cli/app.rs +310 -0
- package/src/cli/commands/acp.rs +721 -0
- package/src/cli/commands/chat.rs +485 -0
- package/src/cli/commands/code.rs +513 -0
- package/src/cli/commands/config.rs +394 -0
- package/src/cli/commands/health.rs +442 -0
- package/src/cli/commands/history.rs +421 -0
- package/src/cli/commands/mod.rs +14 -0
- package/src/cli/commands/settings.rs +1384 -0
- package/src/cli/mod.rs +166 -0
- package/src/config/mod.rs +2212 -0
- package/src/display/ascii_art.rs +139 -0
- package/src/display/banner.rs +289 -0
- package/src/display/components/input.rs +323 -0
- package/src/display/components/mod.rs +2 -0
- package/src/display/components/settings_list.rs +306 -0
- package/src/display/interactive.rs +1255 -0
- package/src/display/mod.rs +62 -0
- package/src/display/terminal.rs +42 -0
- package/src/display/tips.rs +316 -0
- package/src/grok_client_ext.rs +177 -0
- package/src/hooks/loader.rs +407 -0
- package/src/hooks/mod.rs +158 -0
- package/src/lib.rs +174 -0
- package/src/main.rs +65 -0
- package/src/mcp/client.rs +195 -0
- package/src/mcp/config.rs +20 -0
- package/src/mcp/mod.rs +6 -0
- package/src/mcp/protocol.rs +67 -0
- package/src/utils/auth.rs +41 -0
- package/src/utils/chat_logger.rs +568 -0
- package/src/utils/context.rs +390 -0
- package/src/utils/mod.rs +16 -0
- package/src/utils/network.rs +320 -0
- package/src/utils/rate_limiter.rs +166 -0
- package/src/utils/session.rs +73 -0
- package/src/utils/shell_permissions.rs +389 -0
- 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
|
+
}
|