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,323 @@
|
|
|
1
|
+
use serde::{Deserialize, Serialize, Serializer};
|
|
2
|
+
use serde_json::Value;
|
|
3
|
+
|
|
4
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
|
5
|
+
pub struct SessionId(pub String);
|
|
6
|
+
|
|
7
|
+
impl SessionId {
|
|
8
|
+
pub fn new(id: impl Into<String>) -> Self {
|
|
9
|
+
Self(id.into())
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
14
|
+
pub struct AgentCapabilities {
|
|
15
|
+
#[serde(default, rename = "sessionCapabilities")]
|
|
16
|
+
pub session_capabilities: SessionCapabilities,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
impl Default for AgentCapabilities {
|
|
20
|
+
fn default() -> Self {
|
|
21
|
+
Self::new()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
impl AgentCapabilities {
|
|
26
|
+
pub fn new() -> Self {
|
|
27
|
+
Self {
|
|
28
|
+
session_capabilities: SessionCapabilities::new(),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
34
|
+
pub struct SessionCapabilities {}
|
|
35
|
+
|
|
36
|
+
impl SessionCapabilities {
|
|
37
|
+
pub fn new() -> Self {
|
|
38
|
+
Self {}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
43
|
+
pub struct InitializeRequest {
|
|
44
|
+
#[serde(
|
|
45
|
+
default = "default_protocol_version",
|
|
46
|
+
deserialize_with = "deserialize_protocol_version",
|
|
47
|
+
alias = "protocolVersion"
|
|
48
|
+
)]
|
|
49
|
+
pub protocol_version: String,
|
|
50
|
+
#[serde(default, alias = "clientCapabilities")]
|
|
51
|
+
pub capabilities: Value,
|
|
52
|
+
#[serde(default, alias = "clientInfo")]
|
|
53
|
+
pub client_info: Value,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fn default_protocol_version() -> String {
|
|
57
|
+
"1".to_string()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fn deserialize_protocol_version<'de, D>(deserializer: D) -> Result<String, D::Error>
|
|
61
|
+
where
|
|
62
|
+
D: serde::Deserializer<'de>,
|
|
63
|
+
{
|
|
64
|
+
use serde::de::Error;
|
|
65
|
+
let value = Value::deserialize(deserializer)?;
|
|
66
|
+
|
|
67
|
+
match value {
|
|
68
|
+
Value::Number(n) => {
|
|
69
|
+
if let Some(i) = n.as_i64() {
|
|
70
|
+
Ok(i.to_string())
|
|
71
|
+
} else if let Some(f) = n.as_f64() {
|
|
72
|
+
Ok(f.to_string())
|
|
73
|
+
} else {
|
|
74
|
+
Ok("1".to_string())
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
Value::String(s) => Ok(s),
|
|
78
|
+
_ => Err(D::Error::custom(
|
|
79
|
+
"protocol_version must be a number or string",
|
|
80
|
+
)),
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fn serialize_protocol_version<S>(version: &str, serializer: S) -> Result<S::Ok, S::Error>
|
|
85
|
+
where
|
|
86
|
+
S: Serializer,
|
|
87
|
+
{
|
|
88
|
+
// Try to parse as integer, if successful serialize as number, otherwise as string
|
|
89
|
+
if let Ok(num) = version.parse::<i64>() {
|
|
90
|
+
serializer.serialize_i64(num)
|
|
91
|
+
} else {
|
|
92
|
+
serializer.serialize_str(version)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
97
|
+
pub struct InitializeResponse {
|
|
98
|
+
#[serde(
|
|
99
|
+
rename = "protocolVersion",
|
|
100
|
+
serialize_with = "serialize_protocol_version"
|
|
101
|
+
)]
|
|
102
|
+
pub protocol_version: String,
|
|
103
|
+
#[serde(rename = "agentCapabilities")]
|
|
104
|
+
pub agent_capabilities: AgentCapabilities,
|
|
105
|
+
#[serde(rename = "agentInfo")]
|
|
106
|
+
pub agent_info: Implementation,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
impl InitializeResponse {
|
|
110
|
+
pub fn new(version: impl Into<String>) -> Self {
|
|
111
|
+
Self {
|
|
112
|
+
protocol_version: version.into(),
|
|
113
|
+
agent_capabilities: AgentCapabilities::new(),
|
|
114
|
+
agent_info: Implementation {
|
|
115
|
+
name: "grok-cli".to_string(),
|
|
116
|
+
version: env!("CARGO_PKG_VERSION").to_string(),
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
pub fn agent_capabilities(mut self, caps: AgentCapabilities) -> Self {
|
|
122
|
+
self.agent_capabilities = caps;
|
|
123
|
+
self
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
pub fn agent_info(mut self, info: Implementation) -> Self {
|
|
127
|
+
self.agent_info = info;
|
|
128
|
+
self
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
133
|
+
pub struct Implementation {
|
|
134
|
+
pub name: String,
|
|
135
|
+
pub version: String,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
impl Implementation {
|
|
139
|
+
pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
|
|
140
|
+
Self {
|
|
141
|
+
name: name.into(),
|
|
142
|
+
version: version.into(),
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#[derive(Debug, Serialize, Deserialize)]
|
|
148
|
+
pub struct NewSessionRequest {
|
|
149
|
+
#[serde(default, alias = "sessionCapabilities")]
|
|
150
|
+
pub capabilities: Value,
|
|
151
|
+
#[serde(default, alias = "workspaceRoot")]
|
|
152
|
+
pub workspace_root: Option<String>,
|
|
153
|
+
#[serde(default, alias = "workingDirectory")]
|
|
154
|
+
pub working_directory: Option<String>,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
158
|
+
pub struct NewSessionResponse {
|
|
159
|
+
#[serde(rename = "sessionId")]
|
|
160
|
+
pub session_id: SessionId,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
impl NewSessionResponse {
|
|
164
|
+
pub fn new(session_id: SessionId) -> Self {
|
|
165
|
+
Self { session_id }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
170
|
+
pub struct PromptRequest {
|
|
171
|
+
#[serde(alias = "sessionId")]
|
|
172
|
+
pub session_id: SessionId,
|
|
173
|
+
pub prompt: Vec<ContentBlock>,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
177
|
+
#[serde(tag = "type")]
|
|
178
|
+
pub enum ContentBlock {
|
|
179
|
+
#[serde(rename = "text")]
|
|
180
|
+
Text(TextContent),
|
|
181
|
+
#[serde(rename = "resource")]
|
|
182
|
+
Resource(ResourceContent),
|
|
183
|
+
#[serde(rename = "resource_link")]
|
|
184
|
+
ResourceLink(ResourceLinkContent),
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
188
|
+
pub struct TextContent {
|
|
189
|
+
pub text: String,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
impl TextContent {
|
|
193
|
+
pub fn new(text: impl Into<String>) -> Self {
|
|
194
|
+
Self { text: text.into() }
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
199
|
+
pub struct ResourceContent {
|
|
200
|
+
pub resource: EmbeddedResourceResource,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
204
|
+
#[serde(untagged)]
|
|
205
|
+
pub enum EmbeddedResourceResource {
|
|
206
|
+
TextResourceContents(TextResourceContents),
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
210
|
+
pub struct TextResourceContents {
|
|
211
|
+
pub uri: String,
|
|
212
|
+
pub text: String,
|
|
213
|
+
#[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
|
|
214
|
+
pub mime_type: Option<String>,
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
218
|
+
pub struct ResourceLinkContent {
|
|
219
|
+
pub uri: String,
|
|
220
|
+
pub name: String,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
224
|
+
pub struct PromptResponse {
|
|
225
|
+
#[serde(rename = "stopReason")]
|
|
226
|
+
pub stop_reason: StopReason,
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
impl PromptResponse {
|
|
230
|
+
pub fn new(stop_reason: StopReason) -> Self {
|
|
231
|
+
Self { stop_reason }
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
236
|
+
#[serde(rename_all = "snake_case")]
|
|
237
|
+
pub enum StopReason {
|
|
238
|
+
EndTurn,
|
|
239
|
+
MaxTokens,
|
|
240
|
+
StopSequence,
|
|
241
|
+
ToolUse,
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
245
|
+
pub struct SessionNotification {
|
|
246
|
+
#[serde(rename = "sessionId")]
|
|
247
|
+
pub session_id: SessionId,
|
|
248
|
+
pub update: SessionUpdate,
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
impl SessionNotification {
|
|
252
|
+
pub fn new(session_id: SessionId, update: SessionUpdate) -> Self {
|
|
253
|
+
Self { session_id, update }
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
258
|
+
#[serde(tag = "sessionUpdate")]
|
|
259
|
+
pub enum SessionUpdate {
|
|
260
|
+
#[serde(rename = "agent_message_chunk")]
|
|
261
|
+
AgentMessageChunk(ContentChunk),
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
265
|
+
pub struct ContentChunk {
|
|
266
|
+
pub content: ContentBlock,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
impl ContentChunk {
|
|
270
|
+
pub fn new(content: ContentBlock) -> Self {
|
|
271
|
+
Self { content }
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
pub struct ProtocolVersion;
|
|
276
|
+
|
|
277
|
+
impl ProtocolVersion {
|
|
278
|
+
pub const LATEST: &'static str = "1";
|
|
279
|
+
pub const V1: &'static str = "1";
|
|
280
|
+
pub const DATE_FORMAT: &'static str = "2024-04-15";
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
pub struct MethodNames {
|
|
284
|
+
pub initialize: &'static str,
|
|
285
|
+
pub session_new: &'static str,
|
|
286
|
+
pub session_prompt: &'static str,
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
pub const AGENT_METHOD_NAMES: MethodNames = MethodNames {
|
|
290
|
+
initialize: "initialize",
|
|
291
|
+
session_new: "session/new",
|
|
292
|
+
session_prompt: "session/prompt",
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
#[cfg(test)]
|
|
296
|
+
mod serialization_tests {
|
|
297
|
+
use super::*;
|
|
298
|
+
use serde_json::json;
|
|
299
|
+
|
|
300
|
+
#[test]
|
|
301
|
+
fn test_session_notification_serialization() {
|
|
302
|
+
let text = "Hello world";
|
|
303
|
+
let content = ContentBlock::Text(TextContent::new(text));
|
|
304
|
+
let update = SessionUpdate::AgentMessageChunk(ContentChunk::new(content));
|
|
305
|
+
let notification = SessionNotification::new(SessionId::new("session-123"), update);
|
|
306
|
+
|
|
307
|
+
let json = serde_json::to_string_pretty(¬ification).unwrap();
|
|
308
|
+
println!("Serialized Notification: {}", json);
|
|
309
|
+
|
|
310
|
+
let expected = json!({
|
|
311
|
+
"sessionId": "session-123",
|
|
312
|
+
"update": {
|
|
313
|
+
"sessionUpdate": "agent_message_chunk",
|
|
314
|
+
"content": {
|
|
315
|
+
"type": "text",
|
|
316
|
+
"text": "Hello world"
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
assert_eq!(serde_json::to_value(¬ification).unwrap(), expected);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
use anyhow::{Result, anyhow};
|
|
2
|
+
use std::path::{Path, PathBuf};
|
|
3
|
+
use std::sync::{Arc, Mutex};
|
|
4
|
+
|
|
5
|
+
#[derive(Debug, Clone)]
|
|
6
|
+
pub struct SecurityPolicy {
|
|
7
|
+
trusted_directories: Vec<PathBuf>,
|
|
8
|
+
working_directory: PathBuf,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
impl SecurityPolicy {
|
|
12
|
+
pub fn new() -> Self {
|
|
13
|
+
let working_directory = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
|
14
|
+
Self {
|
|
15
|
+
trusted_directories: Vec::new(),
|
|
16
|
+
working_directory,
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
pub fn with_working_directory(working_directory: PathBuf) -> Self {
|
|
21
|
+
Self {
|
|
22
|
+
trusted_directories: Vec::new(),
|
|
23
|
+
working_directory,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
pub fn add_trusted_directory<P: AsRef<Path>>(&mut self, path: P) {
|
|
28
|
+
let path = path.as_ref();
|
|
29
|
+
// Canonicalize the path to resolve symlinks and make it absolute
|
|
30
|
+
let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
|
|
31
|
+
self.trusted_directories.push(canonical);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Get the working directory
|
|
35
|
+
pub fn working_directory(&self) -> &Path {
|
|
36
|
+
&self.working_directory
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Resolve a path to its canonical absolute form
|
|
40
|
+
pub fn resolve_path<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf> {
|
|
41
|
+
let path = path.as_ref();
|
|
42
|
+
|
|
43
|
+
// Convert to absolute path relative to working directory
|
|
44
|
+
let absolute = if path.is_absolute() {
|
|
45
|
+
path.to_path_buf()
|
|
46
|
+
} else {
|
|
47
|
+
self.working_directory.join(path)
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Canonicalize to resolve symlinks and .. components
|
|
51
|
+
// If the file doesn't exist yet, try to canonicalize the parent
|
|
52
|
+
absolute.canonicalize().or_else(|_| {
|
|
53
|
+
if let Some(parent) = absolute.parent() {
|
|
54
|
+
let canonical_parent = parent.canonicalize()?;
|
|
55
|
+
if let Some(file_name) = absolute.file_name() {
|
|
56
|
+
Ok(canonical_parent.join(file_name))
|
|
57
|
+
} else {
|
|
58
|
+
Ok(canonical_parent)
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
Ok(absolute)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
pub fn is_path_trusted<P: AsRef<Path>>(&self, path: P) -> bool {
|
|
67
|
+
// Resolve the path first
|
|
68
|
+
let resolved = match self.resolve_path(path) {
|
|
69
|
+
Ok(p) => p,
|
|
70
|
+
Err(_) => return false,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// If no trusted directories are set, everything is untrusted (deny by default)
|
|
74
|
+
if self.trusted_directories.is_empty() {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
self.trusted_directories
|
|
79
|
+
.iter()
|
|
80
|
+
.any(|trusted| resolved.starts_with(trusted))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
pub fn validate_shell_command(&self, command: &str) -> Result<()> {
|
|
84
|
+
// Basic blacklist for really dangerous things if needed,
|
|
85
|
+
// but mostly we rely on user confirmation + trusted scope.
|
|
86
|
+
// For now, allow all if confirmed.
|
|
87
|
+
if command.trim().is_empty() {
|
|
88
|
+
return Err(anyhow!("Command cannot be empty"));
|
|
89
|
+
}
|
|
90
|
+
Ok(())
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
pub struct SecurityManager {
|
|
95
|
+
policy: Arc<Mutex<SecurityPolicy>>,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
impl SecurityManager {
|
|
99
|
+
pub fn new() -> Self {
|
|
100
|
+
Self {
|
|
101
|
+
policy: Arc::new(Mutex::new(SecurityPolicy::new())),
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
pub fn get_policy(&self) -> SecurityPolicy {
|
|
106
|
+
self.policy
|
|
107
|
+
.lock()
|
|
108
|
+
.expect("SecurityManager mutex poisoned - this is a bug")
|
|
109
|
+
.clone()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
pub fn add_trusted_directory<P: AsRef<Path>>(&self, path: P) {
|
|
113
|
+
self.policy
|
|
114
|
+
.lock()
|
|
115
|
+
.expect("SecurityManager mutex poisoned - this is a bug")
|
|
116
|
+
.add_trusted_directory(path);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
pub fn check_path_access<P: AsRef<Path>>(&self, path: P) -> Result<()> {
|
|
120
|
+
if self.get_policy().is_path_trusted(path) {
|
|
121
|
+
Ok(())
|
|
122
|
+
} else {
|
|
123
|
+
Err(anyhow!("Access denied: Path is not in a trusted directory"))
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#[cfg(test)]
|
|
129
|
+
mod tests {
|
|
130
|
+
use super::*;
|
|
131
|
+
use std::fs;
|
|
132
|
+
use tempfile::TempDir;
|
|
133
|
+
|
|
134
|
+
#[test]
|
|
135
|
+
fn test_absolute_path_trusted() {
|
|
136
|
+
let temp_dir = TempDir::new().unwrap();
|
|
137
|
+
let temp_path = temp_dir.path().canonicalize().unwrap();
|
|
138
|
+
|
|
139
|
+
let mut policy = SecurityPolicy::with_working_directory(temp_path.clone());
|
|
140
|
+
policy.add_trusted_directory(&temp_path);
|
|
141
|
+
|
|
142
|
+
// Create a test file
|
|
143
|
+
let file_path = temp_path.join("test.txt");
|
|
144
|
+
fs::write(&file_path, "test").unwrap();
|
|
145
|
+
|
|
146
|
+
// Absolute path should be trusted
|
|
147
|
+
assert!(policy.is_path_trusted(&file_path));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#[test]
|
|
151
|
+
fn test_relative_path_resolution() {
|
|
152
|
+
let temp_dir = TempDir::new().unwrap();
|
|
153
|
+
let temp_path = temp_dir.path().canonicalize().unwrap();
|
|
154
|
+
|
|
155
|
+
let mut policy = SecurityPolicy::with_working_directory(temp_path.clone());
|
|
156
|
+
policy.add_trusted_directory(&temp_path);
|
|
157
|
+
|
|
158
|
+
// Create a test file
|
|
159
|
+
let file_path = temp_path.join("test.txt");
|
|
160
|
+
fs::write(&file_path, "test").unwrap();
|
|
161
|
+
|
|
162
|
+
// Relative path should be resolved and trusted
|
|
163
|
+
assert!(policy.is_path_trusted("test.txt"));
|
|
164
|
+
assert!(policy.is_path_trusted("./test.txt"));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#[test]
|
|
168
|
+
fn test_parent_directory_access() {
|
|
169
|
+
let temp_dir = TempDir::new().unwrap();
|
|
170
|
+
let temp_path = temp_dir.path().canonicalize().unwrap();
|
|
171
|
+
|
|
172
|
+
// Create subdirectory
|
|
173
|
+
let sub_dir = temp_path.join("subdir");
|
|
174
|
+
fs::create_dir(&sub_dir).unwrap();
|
|
175
|
+
|
|
176
|
+
// Create file in parent
|
|
177
|
+
let file_path = temp_path.join("parent.txt");
|
|
178
|
+
fs::write(&file_path, "test").unwrap();
|
|
179
|
+
|
|
180
|
+
// Set working directory to subdirectory, but trust parent
|
|
181
|
+
let mut policy = SecurityPolicy::with_working_directory(sub_dir.clone());
|
|
182
|
+
policy.add_trusted_directory(&temp_path);
|
|
183
|
+
|
|
184
|
+
// Access file in parent using relative path
|
|
185
|
+
assert!(policy.is_path_trusted("../parent.txt"));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#[test]
|
|
189
|
+
fn test_path_outside_trusted_denied() {
|
|
190
|
+
let temp_dir = TempDir::new().unwrap();
|
|
191
|
+
let temp_path = temp_dir.path().canonicalize().unwrap();
|
|
192
|
+
|
|
193
|
+
let mut policy = SecurityPolicy::with_working_directory(temp_path.clone());
|
|
194
|
+
policy.add_trusted_directory(&temp_path);
|
|
195
|
+
|
|
196
|
+
// Path outside trusted directory should be denied
|
|
197
|
+
#[cfg(target_os = "windows")]
|
|
198
|
+
let outside_path = "C:\\Windows\\System32\\cmd.exe";
|
|
199
|
+
#[cfg(not(target_os = "windows"))]
|
|
200
|
+
let outside_path = "/etc/passwd";
|
|
201
|
+
|
|
202
|
+
assert!(!policy.is_path_trusted(outside_path));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
#[test]
|
|
206
|
+
fn test_resolve_path_nonexistent() {
|
|
207
|
+
let temp_dir = TempDir::new().unwrap();
|
|
208
|
+
let temp_path = temp_dir.path().canonicalize().unwrap();
|
|
209
|
+
|
|
210
|
+
let policy = SecurityPolicy::with_working_directory(temp_path.clone());
|
|
211
|
+
|
|
212
|
+
// Should resolve path even if file doesn't exist yet
|
|
213
|
+
let result = policy.resolve_path("newfile.txt");
|
|
214
|
+
assert!(result.is_ok());
|
|
215
|
+
let resolved = result.unwrap();
|
|
216
|
+
assert_eq!(resolved, temp_path.join("newfile.txt"));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#[test]
|
|
220
|
+
fn test_symlink_resolution() {
|
|
221
|
+
let temp_dir = TempDir::new().unwrap();
|
|
222
|
+
let temp_path = temp_dir.path().canonicalize().unwrap();
|
|
223
|
+
|
|
224
|
+
// Create a file
|
|
225
|
+
let real_file = temp_path.join("real.txt");
|
|
226
|
+
fs::write(&real_file, "test").unwrap();
|
|
227
|
+
|
|
228
|
+
// Create a symlink (skip on Windows if not admin)
|
|
229
|
+
#[cfg(unix)]
|
|
230
|
+
{
|
|
231
|
+
let link_path = temp_path.join("link.txt");
|
|
232
|
+
std::os::unix::fs::symlink(&real_file, &link_path).unwrap();
|
|
233
|
+
|
|
234
|
+
let mut policy = SecurityPolicy::with_working_directory(temp_path.clone());
|
|
235
|
+
policy.add_trusted_directory(&temp_path);
|
|
236
|
+
|
|
237
|
+
// Symlink should resolve to real path and be trusted
|
|
238
|
+
assert!(policy.is_path_trusted("link.txt"));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
#[test]
|
|
243
|
+
fn test_multiple_trusted_directories() {
|
|
244
|
+
let temp_dir1 = TempDir::new().unwrap();
|
|
245
|
+
let temp_dir2 = TempDir::new().unwrap();
|
|
246
|
+
let temp_path1 = temp_dir1.path().canonicalize().unwrap();
|
|
247
|
+
let temp_path2 = temp_dir2.path().canonicalize().unwrap();
|
|
248
|
+
|
|
249
|
+
let mut policy = SecurityPolicy::with_working_directory(temp_path1.clone());
|
|
250
|
+
policy.add_trusted_directory(&temp_path1);
|
|
251
|
+
policy.add_trusted_directory(&temp_path2);
|
|
252
|
+
|
|
253
|
+
// Create files in both directories
|
|
254
|
+
let file1 = temp_path1.join("file1.txt");
|
|
255
|
+
let file2 = temp_path2.join("file2.txt");
|
|
256
|
+
fs::write(&file1, "test1").unwrap();
|
|
257
|
+
fs::write(&file2, "test2").unwrap();
|
|
258
|
+
|
|
259
|
+
// Both should be trusted
|
|
260
|
+
assert!(policy.is_path_trusted(&file1));
|
|
261
|
+
assert!(policy.is_path_trusted(&file2));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
#[test]
|
|
265
|
+
fn test_empty_trusted_directories() {
|
|
266
|
+
let temp_dir = TempDir::new().unwrap();
|
|
267
|
+
let temp_path = temp_dir.path().canonicalize().unwrap();
|
|
268
|
+
|
|
269
|
+
let policy = SecurityPolicy::with_working_directory(temp_path.clone());
|
|
270
|
+
|
|
271
|
+
// Without any trusted directories, nothing should be trusted
|
|
272
|
+
assert!(!policy.is_path_trusted("test.txt"));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#[test]
|
|
276
|
+
fn test_security_manager() {
|
|
277
|
+
let temp_dir = TempDir::new().unwrap();
|
|
278
|
+
let temp_path = temp_dir.path().canonicalize().unwrap();
|
|
279
|
+
|
|
280
|
+
let manager = SecurityManager::new();
|
|
281
|
+
manager.add_trusted_directory(&temp_path);
|
|
282
|
+
|
|
283
|
+
// Create a test file
|
|
284
|
+
let file_path = temp_path.join("test.txt");
|
|
285
|
+
fs::write(&file_path, "test").unwrap();
|
|
286
|
+
|
|
287
|
+
// Should be able to check access
|
|
288
|
+
assert!(manager.check_path_access(&file_path).is_ok());
|
|
289
|
+
|
|
290
|
+
// Path outside should be denied
|
|
291
|
+
#[cfg(target_os = "windows")]
|
|
292
|
+
let outside_path = "C:\\Windows\\System32\\cmd.exe";
|
|
293
|
+
#[cfg(not(target_os = "windows"))]
|
|
294
|
+
let outside_path = "/etc/passwd";
|
|
295
|
+
|
|
296
|
+
assert!(manager.check_path_access(outside_path).is_err());
|
|
297
|
+
}
|
|
298
|
+
}
|