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,407 @@
|
|
|
1
|
+
//! Extension loading and management
|
|
2
|
+
//!
|
|
3
|
+
//! This module provides functionality to discover, load, and initialize
|
|
4
|
+
//! extensions from configuration or extension directories.
|
|
5
|
+
|
|
6
|
+
use super::{Extension, ExtensionManager, Hook, HookManager, ToolContext};
|
|
7
|
+
use crate::config::ExtensionsConfig;
|
|
8
|
+
use anyhow::{anyhow, Result};
|
|
9
|
+
use serde::{Deserialize, Serialize};
|
|
10
|
+
use std::fs;
|
|
11
|
+
use std::path::{Path, PathBuf};
|
|
12
|
+
use tracing::{debug, info, warn};
|
|
13
|
+
|
|
14
|
+
/// Extension metadata from manifest file
|
|
15
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
16
|
+
pub struct ExtensionManifest {
|
|
17
|
+
/// Extension name
|
|
18
|
+
pub name: String,
|
|
19
|
+
|
|
20
|
+
/// Extension version
|
|
21
|
+
pub version: String,
|
|
22
|
+
|
|
23
|
+
/// Extension description
|
|
24
|
+
pub description: Option<String>,
|
|
25
|
+
|
|
26
|
+
/// Author information
|
|
27
|
+
pub author: Option<String>,
|
|
28
|
+
|
|
29
|
+
/// Extension type (hook, tool, etc.)
|
|
30
|
+
pub extension_type: ExtensionType,
|
|
31
|
+
|
|
32
|
+
/// Hooks configuration
|
|
33
|
+
#[serde(default)]
|
|
34
|
+
pub hooks: Vec<HookConfig>,
|
|
35
|
+
|
|
36
|
+
/// Extension dependencies
|
|
37
|
+
#[serde(default)]
|
|
38
|
+
pub dependencies: Vec<String>,
|
|
39
|
+
|
|
40
|
+
/// Enabled by default
|
|
41
|
+
#[serde(default = "default_true")]
|
|
42
|
+
pub enabled: bool,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn default_true() -> bool {
|
|
46
|
+
true
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Type of extension
|
|
50
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
51
|
+
#[serde(rename_all = "snake_case")]
|
|
52
|
+
pub enum ExtensionType {
|
|
53
|
+
/// Hook-based extension
|
|
54
|
+
Hook,
|
|
55
|
+
/// Tool provider extension
|
|
56
|
+
Tool,
|
|
57
|
+
/// Combined hook and tool extension
|
|
58
|
+
Combined,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Hook configuration from manifest
|
|
62
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
63
|
+
pub struct HookConfig {
|
|
64
|
+
/// Hook name/identifier
|
|
65
|
+
pub name: String,
|
|
66
|
+
|
|
67
|
+
/// Hook type
|
|
68
|
+
pub hook_type: HookType,
|
|
69
|
+
|
|
70
|
+
/// Optional script or command to execute
|
|
71
|
+
pub script: Option<String>,
|
|
72
|
+
|
|
73
|
+
/// Optional configuration
|
|
74
|
+
pub config: Option<serde_json::Value>,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Type of hook
|
|
78
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
79
|
+
#[serde(rename_all = "snake_case")]
|
|
80
|
+
pub enum HookType {
|
|
81
|
+
/// Executes before tool invocation
|
|
82
|
+
BeforeTool,
|
|
83
|
+
/// Executes after tool invocation
|
|
84
|
+
AfterTool,
|
|
85
|
+
/// Both before and after
|
|
86
|
+
Both,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Loaded extension with its manifest
|
|
90
|
+
pub struct LoadedExtension {
|
|
91
|
+
pub manifest: ExtensionManifest,
|
|
92
|
+
pub path: PathBuf,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Extension loader for discovering and loading extensions
|
|
96
|
+
pub struct ExtensionLoader {
|
|
97
|
+
config: ExtensionsConfig,
|
|
98
|
+
loaded_extensions: Vec<LoadedExtension>,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
impl ExtensionLoader {
|
|
102
|
+
/// Create a new extension loader with the given configuration
|
|
103
|
+
pub fn new(config: ExtensionsConfig) -> Self {
|
|
104
|
+
Self {
|
|
105
|
+
config,
|
|
106
|
+
loaded_extensions: Vec::new(),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// Discover all available extensions from the extension directory
|
|
111
|
+
pub fn discover_extensions(&mut self) -> Result<Vec<ExtensionManifest>> {
|
|
112
|
+
if !self.config.enabled {
|
|
113
|
+
debug!("Extension system is disabled");
|
|
114
|
+
return Ok(Vec::new());
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let extension_dir = match &self.config.extension_dir {
|
|
118
|
+
Some(dir) => dir.clone(),
|
|
119
|
+
None => self.get_default_extension_dir()?,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
if !extension_dir.exists() {
|
|
123
|
+
info!(
|
|
124
|
+
"Extension directory does not exist: {}",
|
|
125
|
+
extension_dir.display()
|
|
126
|
+
);
|
|
127
|
+
return Ok(Vec::new());
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let mut manifests = Vec::new();
|
|
131
|
+
|
|
132
|
+
for entry in fs::read_dir(&extension_dir)? {
|
|
133
|
+
let entry = entry?;
|
|
134
|
+
let path = entry.path();
|
|
135
|
+
|
|
136
|
+
if path.is_dir() {
|
|
137
|
+
match self.load_extension_manifest(&path) {
|
|
138
|
+
Ok(manifest) => {
|
|
139
|
+
info!("Discovered extension: {}", manifest.name);
|
|
140
|
+
self.loaded_extensions.push(LoadedExtension {
|
|
141
|
+
manifest: manifest.clone(),
|
|
142
|
+
path: path.clone(),
|
|
143
|
+
});
|
|
144
|
+
manifests.push(manifest);
|
|
145
|
+
}
|
|
146
|
+
Err(e) => {
|
|
147
|
+
warn!("Failed to load extension from {}: {}", path.display(), e);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
Ok(manifests)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// Load extension manifest from a directory
|
|
157
|
+
fn load_extension_manifest(&self, extension_path: &Path) -> Result<ExtensionManifest> {
|
|
158
|
+
let manifest_path = extension_path.join("extension.json");
|
|
159
|
+
|
|
160
|
+
if !manifest_path.exists() {
|
|
161
|
+
return Err(anyhow!(
|
|
162
|
+
"Extension manifest not found at {}",
|
|
163
|
+
manifest_path.display()
|
|
164
|
+
));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let content = fs::read_to_string(&manifest_path)?;
|
|
168
|
+
let manifest: ExtensionManifest = serde_json::from_str(&content)?;
|
|
169
|
+
|
|
170
|
+
Ok(manifest)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// Get the default extension directory
|
|
174
|
+
fn get_default_extension_dir(&self) -> Result<PathBuf> {
|
|
175
|
+
let home_dir =
|
|
176
|
+
dirs::home_dir().ok_or_else(|| anyhow!("Could not determine home directory"))?;
|
|
177
|
+
Ok(home_dir.join(".grok").join("extensions"))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/// Load and register all enabled extensions
|
|
181
|
+
pub fn load_extensions(&mut self, extension_manager: &mut ExtensionManager) -> Result<()> {
|
|
182
|
+
if !self.config.enabled {
|
|
183
|
+
debug!("Extension system is disabled, skipping extension loading");
|
|
184
|
+
return Ok(());
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Discover extensions if not already done
|
|
188
|
+
if self.loaded_extensions.is_empty() {
|
|
189
|
+
self.discover_extensions()?;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Filter to only enabled extensions
|
|
193
|
+
let enabled_extensions: Vec<_> = self
|
|
194
|
+
.loaded_extensions
|
|
195
|
+
.iter()
|
|
196
|
+
.filter(|ext| {
|
|
197
|
+
ext.manifest.enabled
|
|
198
|
+
&& (self.config.enabled_extensions.is_empty()
|
|
199
|
+
|| self.config.enabled_extensions.contains(&ext.manifest.name))
|
|
200
|
+
})
|
|
201
|
+
.collect();
|
|
202
|
+
|
|
203
|
+
for loaded_ext in enabled_extensions {
|
|
204
|
+
match self.instantiate_extension(loaded_ext) {
|
|
205
|
+
Ok(extension) => {
|
|
206
|
+
info!("Loading extension: {}", loaded_ext.manifest.name);
|
|
207
|
+
extension_manager.register(extension);
|
|
208
|
+
}
|
|
209
|
+
Err(e) => {
|
|
210
|
+
warn!(
|
|
211
|
+
"Failed to instantiate extension {}: {}",
|
|
212
|
+
loaded_ext.manifest.name, e
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
Ok(())
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// Instantiate an extension from its loaded metadata
|
|
222
|
+
fn instantiate_extension(&self, loaded_ext: &LoadedExtension) -> Result<Box<dyn Extension>> {
|
|
223
|
+
// For now, we create a simple hook-based extension
|
|
224
|
+
// In a full implementation, this could load dynamic libraries or scripts
|
|
225
|
+
Ok(Box::new(ConfigBasedExtension {
|
|
226
|
+
manifest: loaded_ext.manifest.clone(),
|
|
227
|
+
}))
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// Get list of loaded extension manifests
|
|
231
|
+
pub fn get_loaded_extensions(&self) -> Vec<&ExtensionManifest> {
|
|
232
|
+
self.loaded_extensions.iter().map(|e| &e.manifest).collect()
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/// A simple extension implementation based on configuration
|
|
237
|
+
struct ConfigBasedExtension {
|
|
238
|
+
manifest: ExtensionManifest,
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
impl Extension for ConfigBasedExtension {
|
|
242
|
+
fn name(&self) -> &str {
|
|
243
|
+
&self.manifest.name
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
fn register_hooks(&self, hook_manager: &mut HookManager) -> Result<()> {
|
|
247
|
+
for hook_config in &self.manifest.hooks {
|
|
248
|
+
let hook = create_hook_from_config(&self.manifest.name, hook_config)?;
|
|
249
|
+
hook_manager.register(hook);
|
|
250
|
+
}
|
|
251
|
+
Ok(())
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/// Create a hook instance from configuration
|
|
256
|
+
fn create_hook_from_config(
|
|
257
|
+
extension_name: &str,
|
|
258
|
+
hook_config: &HookConfig,
|
|
259
|
+
) -> Result<Box<dyn Hook>> {
|
|
260
|
+
Ok(Box::new(ConfigBasedHook {
|
|
261
|
+
extension_name: extension_name.to_string(),
|
|
262
|
+
config: hook_config.clone(),
|
|
263
|
+
}))
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/// A hook implementation based on configuration
|
|
267
|
+
struct ConfigBasedHook {
|
|
268
|
+
extension_name: String,
|
|
269
|
+
config: HookConfig,
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
impl Hook for ConfigBasedHook {
|
|
273
|
+
fn name(&self) -> &str {
|
|
274
|
+
&self.config.name
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
fn before_tool(&self, context: &ToolContext) -> Result<bool> {
|
|
278
|
+
if self.config.hook_type == HookType::BeforeTool || self.config.hook_type == HookType::Both
|
|
279
|
+
{
|
|
280
|
+
debug!(
|
|
281
|
+
"Extension '{}' hook '{}' executing before tool '{}'",
|
|
282
|
+
self.extension_name, self.config.name, context.tool_name
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// In a full implementation, this could execute a script or custom logic
|
|
286
|
+
// For now, we just log and continue
|
|
287
|
+
}
|
|
288
|
+
Ok(true)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
fn after_tool(&self, context: &ToolContext, result: &str) -> Result<()> {
|
|
292
|
+
if self.config.hook_type == HookType::AfterTool || self.config.hook_type == HookType::Both {
|
|
293
|
+
debug!(
|
|
294
|
+
"Extension '{}' hook '{}' executing after tool '{}' (result length: {})",
|
|
295
|
+
self.extension_name,
|
|
296
|
+
self.config.name,
|
|
297
|
+
context.tool_name,
|
|
298
|
+
result.len()
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
// In a full implementation, this could execute a script or custom logic
|
|
302
|
+
// For now, we just log
|
|
303
|
+
}
|
|
304
|
+
Ok(())
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
#[cfg(test)]
|
|
309
|
+
mod tests {
|
|
310
|
+
use super::*;
|
|
311
|
+
use std::fs;
|
|
312
|
+
use tempfile::tempdir;
|
|
313
|
+
|
|
314
|
+
#[test]
|
|
315
|
+
fn test_load_extension_manifest() {
|
|
316
|
+
let temp_dir = tempdir().unwrap();
|
|
317
|
+
let ext_dir = temp_dir.path().join("test-extension");
|
|
318
|
+
fs::create_dir(&ext_dir).unwrap();
|
|
319
|
+
|
|
320
|
+
let manifest = ExtensionManifest {
|
|
321
|
+
name: "test-extension".to_string(),
|
|
322
|
+
version: "1.0.0".to_string(),
|
|
323
|
+
description: Some("Test extension".to_string()),
|
|
324
|
+
author: Some("Test Author".to_string()),
|
|
325
|
+
extension_type: ExtensionType::Hook,
|
|
326
|
+
hooks: vec![HookConfig {
|
|
327
|
+
name: "test-hook".to_string(),
|
|
328
|
+
hook_type: HookType::BeforeTool,
|
|
329
|
+
script: None,
|
|
330
|
+
config: None,
|
|
331
|
+
}],
|
|
332
|
+
dependencies: vec![],
|
|
333
|
+
enabled: true,
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
let manifest_path = ext_dir.join("extension.json");
|
|
337
|
+
let json = serde_json::to_string_pretty(&manifest).unwrap();
|
|
338
|
+
fs::write(&manifest_path, json).unwrap();
|
|
339
|
+
|
|
340
|
+
let config = ExtensionsConfig {
|
|
341
|
+
enabled: true,
|
|
342
|
+
extension_dir: Some(temp_dir.path().to_path_buf()),
|
|
343
|
+
enabled_extensions: vec![],
|
|
344
|
+
allow_config_extensions: true,
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
let loader = ExtensionLoader::new(config);
|
|
348
|
+
let loaded_manifest = loader.load_extension_manifest(&ext_dir).unwrap();
|
|
349
|
+
|
|
350
|
+
assert_eq!(loaded_manifest.name, "test-extension");
|
|
351
|
+
assert_eq!(loaded_manifest.version, "1.0.0");
|
|
352
|
+
assert_eq!(loaded_manifest.hooks.len(), 1);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
#[test]
|
|
356
|
+
fn test_discover_extensions() {
|
|
357
|
+
let temp_dir = tempdir().unwrap();
|
|
358
|
+
|
|
359
|
+
// Create two test extensions
|
|
360
|
+
for i in 1..=2 {
|
|
361
|
+
let ext_dir = temp_dir.path().join(format!("extension-{}", i));
|
|
362
|
+
fs::create_dir(&ext_dir).unwrap();
|
|
363
|
+
|
|
364
|
+
let manifest = ExtensionManifest {
|
|
365
|
+
name: format!("extension-{}", i),
|
|
366
|
+
version: "1.0.0".to_string(),
|
|
367
|
+
description: None,
|
|
368
|
+
author: None,
|
|
369
|
+
extension_type: ExtensionType::Hook,
|
|
370
|
+
hooks: vec![],
|
|
371
|
+
dependencies: vec![],
|
|
372
|
+
enabled: true,
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
let manifest_path = ext_dir.join("extension.json");
|
|
376
|
+
let json = serde_json::to_string_pretty(&manifest).unwrap();
|
|
377
|
+
fs::write(&manifest_path, json).unwrap();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let config = ExtensionsConfig {
|
|
381
|
+
enabled: true,
|
|
382
|
+
extension_dir: Some(temp_dir.path().to_path_buf()),
|
|
383
|
+
enabled_extensions: vec![],
|
|
384
|
+
allow_config_extensions: true,
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
let mut loader = ExtensionLoader::new(config);
|
|
388
|
+
let manifests = loader.discover_extensions().unwrap();
|
|
389
|
+
|
|
390
|
+
assert_eq!(manifests.len(), 2);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
#[test]
|
|
394
|
+
fn test_extension_disabled() {
|
|
395
|
+
let config = ExtensionsConfig {
|
|
396
|
+
enabled: false,
|
|
397
|
+
extension_dir: None,
|
|
398
|
+
enabled_extensions: vec![],
|
|
399
|
+
allow_config_extensions: false,
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
let mut loader = ExtensionLoader::new(config);
|
|
403
|
+
let manifests = loader.discover_extensions().unwrap();
|
|
404
|
+
|
|
405
|
+
assert_eq!(manifests.len(), 0);
|
|
406
|
+
}
|
|
407
|
+
}
|
package/src/hooks/mod.rs
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
pub mod loader;
|
|
2
|
+
|
|
3
|
+
use anyhow::Result;
|
|
4
|
+
use serde_json::Value;
|
|
5
|
+
|
|
6
|
+
#[derive(Debug, Clone)]
|
|
7
|
+
pub struct ToolContext {
|
|
8
|
+
pub tool_name: String,
|
|
9
|
+
pub args: Value,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
pub trait Hook: Send + Sync {
|
|
13
|
+
fn name(&self) -> &str;
|
|
14
|
+
|
|
15
|
+
// Return Ok(true) to continue, Ok(false) to abort (silently or with log), Err to fail hard
|
|
16
|
+
fn before_tool(&self, _context: &ToolContext) -> Result<bool> {
|
|
17
|
+
Ok(true)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
fn after_tool(&self, _context: &ToolContext, _result: &str) -> Result<()> {
|
|
21
|
+
Ok(())
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
pub struct HookManager {
|
|
26
|
+
hooks: Vec<Box<dyn Hook>>,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
impl Default for HookManager {
|
|
30
|
+
fn default() -> Self {
|
|
31
|
+
Self::new()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
impl HookManager {
|
|
36
|
+
pub fn new() -> Self {
|
|
37
|
+
Self { hooks: Vec::new() }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
pub fn register(&mut self, hook: Box<dyn Hook>) {
|
|
41
|
+
self.hooks.push(hook);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
pub fn execute_before_tool(&self, tool_name: &str, args: &Value) -> Result<bool> {
|
|
45
|
+
let context = ToolContext {
|
|
46
|
+
tool_name: tool_name.to_string(),
|
|
47
|
+
args: args.clone(),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
for hook in &self.hooks {
|
|
51
|
+
if !hook.before_tool(&context)? {
|
|
52
|
+
return Ok(false);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
Ok(true)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
pub fn execute_after_tool(&self, tool_name: &str, args: &Value, result: &str) -> Result<()> {
|
|
59
|
+
let context = ToolContext {
|
|
60
|
+
tool_name: tool_name.to_string(),
|
|
61
|
+
args: args.clone(),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
for hook in &self.hooks {
|
|
65
|
+
hook.after_tool(&context, result)?;
|
|
66
|
+
}
|
|
67
|
+
Ok(())
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
pub trait Extension: Send + Sync {
|
|
72
|
+
fn name(&self) -> &str;
|
|
73
|
+
fn register_hooks(&self, hook_manager: &mut HookManager) -> Result<()>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
pub struct ExtensionManager {
|
|
77
|
+
extensions: Vec<Box<dyn Extension>>,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
impl Default for ExtensionManager {
|
|
81
|
+
fn default() -> Self {
|
|
82
|
+
Self::new()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
impl ExtensionManager {
|
|
87
|
+
pub fn new() -> Self {
|
|
88
|
+
Self {
|
|
89
|
+
extensions: Vec::new(),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
pub fn register(&mut self, extension: Box<dyn Extension>) {
|
|
94
|
+
self.extensions.push(extension);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
pub fn register_all_hooks(&self, hook_manager: &mut HookManager) -> Result<()> {
|
|
98
|
+
for ext in &self.extensions {
|
|
99
|
+
ext.register_hooks(hook_manager)?;
|
|
100
|
+
}
|
|
101
|
+
Ok(())
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#[cfg(test)]
|
|
106
|
+
mod tests {
|
|
107
|
+
use super::*;
|
|
108
|
+
use std::sync::{Arc, Mutex};
|
|
109
|
+
|
|
110
|
+
struct TestHook {
|
|
111
|
+
name: String,
|
|
112
|
+
before_called: Arc<Mutex<bool>>,
|
|
113
|
+
after_called: Arc<Mutex<bool>>,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
impl Hook for TestHook {
|
|
117
|
+
fn name(&self) -> &str {
|
|
118
|
+
&self.name
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fn before_tool(&self, _context: &ToolContext) -> Result<bool> {
|
|
122
|
+
let mut called = self.before_called.lock().unwrap();
|
|
123
|
+
*called = true;
|
|
124
|
+
Ok(true)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
fn after_tool(&self, _context: &ToolContext, _result: &str) -> Result<()> {
|
|
128
|
+
let mut called = self.after_called.lock().unwrap();
|
|
129
|
+
*called = true;
|
|
130
|
+
Ok(())
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#[test]
|
|
135
|
+
fn test_hooks_execution() {
|
|
136
|
+
let before = Arc::new(Mutex::new(false));
|
|
137
|
+
let after = Arc::new(Mutex::new(false));
|
|
138
|
+
|
|
139
|
+
let hook = TestHook {
|
|
140
|
+
name: "test".to_string(),
|
|
141
|
+
before_called: before.clone(),
|
|
142
|
+
after_called: after.clone(),
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
let mut manager = HookManager::new();
|
|
146
|
+
manager.register(Box::new(hook));
|
|
147
|
+
|
|
148
|
+
let args = serde_json::json!({});
|
|
149
|
+
|
|
150
|
+
assert!(manager.execute_before_tool("test_tool", &args).unwrap());
|
|
151
|
+
assert!(*before.lock().unwrap());
|
|
152
|
+
|
|
153
|
+
manager
|
|
154
|
+
.execute_after_tool("test_tool", &args, "result")
|
|
155
|
+
.unwrap();
|
|
156
|
+
assert!(*after.lock().unwrap());
|
|
157
|
+
}
|
|
158
|
+
}
|