omnimancer-cli 0.1.0__py3-none-any.whl
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.
- omnimancer/__init__.py +42 -0
- omnimancer/__main__.py +9 -0
- omnimancer/cli/__init__.py +8 -0
- omnimancer/cli/agent_manager.py +826 -0
- omnimancer/cli/agent_wizard.py +944 -0
- omnimancer/cli/approval_formatter.py +734 -0
- omnimancer/cli/approval_integration.py +734 -0
- omnimancer/cli/approval_prompt.py +1286 -0
- omnimancer/cli/batch_approval_display.py +617 -0
- omnimancer/cli/batch_approval_filters.py +729 -0
- omnimancer/cli/batch_operation_monitor.py +775 -0
- omnimancer/cli/commands/performance_cmd.py +624 -0
- omnimancer/cli/commands.py +757 -0
- omnimancer/cli/completion_handler.py +281 -0
- omnimancer/cli/config_setup_wizard.py +783 -0
- omnimancer/cli/dynamic_commands.py +371 -0
- omnimancer/cli/handlers/__init__.py +18 -0
- omnimancer/cli/handlers/agent_handler.py +865 -0
- omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
- omnimancer/cli/handlers/custom_agent_handler.py +352 -0
- omnimancer/cli/handlers/permissions_handler.py +1609 -0
- omnimancer/cli/interface.py +6695 -0
- omnimancer/cli/performance_dashboard.py +612 -0
- omnimancer/core/__init__.py +34 -0
- omnimancer/core/agent/__init__.py +256 -0
- omnimancer/core/agent/agent_config.py +715 -0
- omnimancer/core/agent/agent_switcher.py +668 -0
- omnimancer/core/agent/approval_context.py +460 -0
- omnimancer/core/agent/approval_dialog.py +902 -0
- omnimancer/core/agent/approval_interface.py +628 -0
- omnimancer/core/agent/approval_manager.py +760 -0
- omnimancer/core/agent/cli_integration.py +470 -0
- omnimancer/core/agent/config.py +845 -0
- omnimancer/core/agent/custom_agent_integration.py +654 -0
- omnimancer/core/agent/diff_renderer.py +706 -0
- omnimancer/core/agent/file_content_display.py +503 -0
- omnimancer/core/agent/file_modification_workflow.py +658 -0
- omnimancer/core/agent/file_system_manager.py +1790 -0
- omnimancer/core/agent/input_handler.py +614 -0
- omnimancer/core/agent/metrics_collector.py +771 -0
- omnimancer/core/agent/optimization_engine.py +729 -0
- omnimancer/core/agent/performance_monitor.py +717 -0
- omnimancer/core/agent/persona.py +870 -0
- omnimancer/core/agent/persona_validator.py +672 -0
- omnimancer/core/agent/program_executor.py +748 -0
- omnimancer/core/agent/proposed_changes_integration.py +663 -0
- omnimancer/core/agent/read_before_write_errors.py +492 -0
- omnimancer/core/agent/read_before_write_ui.py +647 -0
- omnimancer/core/agent/rich_renderer.py +763 -0
- omnimancer/core/agent/status_config.py +238 -0
- omnimancer/core/agent/status_core.py +446 -0
- omnimancer/core/agent/status_integrated_engine.py +540 -0
- omnimancer/core/agent/status_manager.py +878 -0
- omnimancer/core/agent/status_ui_components.py +535 -0
- omnimancer/core/agent/template_applicator.py +548 -0
- omnimancer/core/agent/terminal_status_listener.py +394 -0
- omnimancer/core/agent/token_tracker.py +679 -0
- omnimancer/core/agent/types.py +60 -0
- omnimancer/core/agent/web_client.py +745 -0
- omnimancer/core/agent/workflow_orchestrator.py +826 -0
- omnimancer/core/agent_engine.py +1625 -0
- omnimancer/core/agent_mode_manager.py +706 -0
- omnimancer/core/agent_progress_ui.py +545 -0
- omnimancer/core/chat_manager.py +141 -0
- omnimancer/core/config_manager.py +2199 -0
- omnimancer/core/config_migration.py +772 -0
- omnimancer/core/config_provider.py +1974 -0
- omnimancer/core/config_security_validator.py +393 -0
- omnimancer/core/config_validator.py +768 -0
- omnimancer/core/conversation_manager.py +352 -0
- omnimancer/core/engine.py +787 -0
- omnimancer/core/enhanced_input.py +523 -0
- omnimancer/core/env_loader.py +83 -0
- omnimancer/core/fallback_manager.py +568 -0
- omnimancer/core/health_monitor.py +462 -0
- omnimancer/core/history_manager.py +462 -0
- omnimancer/core/mcp_integration_layer.py +878 -0
- omnimancer/core/models.py +1889 -0
- omnimancer/core/provider_capabilities.py +403 -0
- omnimancer/core/provider_initializer.py +450 -0
- omnimancer/core/provider_registry.py +432 -0
- omnimancer/core/security/__init__.py +16 -0
- omnimancer/core/security/approval_workflow.py +406 -0
- omnimancer/core/security/audit_logger.py +511 -0
- omnimancer/core/security/permission_controller.py +511 -0
- omnimancer/core/security/sandbox_manager.py +379 -0
- omnimancer/core/security/security_manager.py +530 -0
- omnimancer/core/setup_wizard.py +345 -0
- omnimancer/core/setup_wizard_core.py +449 -0
- omnimancer/core/setup_wizard_guide_generator.py +376 -0
- omnimancer/core/setup_wizard_provider_setup.py +454 -0
- omnimancer/core/setup_wizard_ui.py +317 -0
- omnimancer/core/setup_wizard_validation.py +371 -0
- omnimancer/core/signal_handler.py +174 -0
- omnimancer/mcp/__init__.py +11 -0
- omnimancer/mcp/client.py +725 -0
- omnimancer/mcp/manager.py +649 -0
- omnimancer/providers/__init__.py +24 -0
- omnimancer/providers/azure.py +608 -0
- omnimancer/providers/base.py +262 -0
- omnimancer/providers/bedrock.py +711 -0
- omnimancer/providers/claude.py +426 -0
- omnimancer/providers/claude_code.py +406 -0
- omnimancer/providers/cohere.py +412 -0
- omnimancer/providers/factory.py +988 -0
- omnimancer/providers/gemini.py +708 -0
- omnimancer/providers/mistral.py +554 -0
- omnimancer/providers/ollama.py +510 -0
- omnimancer/providers/openai.py +362 -0
- omnimancer/providers/openrouter.py +709 -0
- omnimancer/providers/perplexity.py +476 -0
- omnimancer/providers/vertex.py +617 -0
- omnimancer/providers/xai.py +484 -0
- omnimancer/ui/__init__.py +6 -0
- omnimancer/ui/cancellation_handler.py +235 -0
- omnimancer/ui/progress_indicator.py +318 -0
- omnimancer/utils/__init__.py +22 -0
- omnimancer/utils/error_handler.py +589 -0
- omnimancer/utils/errors.py +667 -0
- omnimancer/utils/retry.py +238 -0
- omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
- omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
- omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
- omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
- omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration migration system for Omnimancer.
|
|
3
|
+
|
|
4
|
+
This module handles automatic migration from old configuration formats
|
|
5
|
+
to new ones, including backup and rollback functionality.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import shutil
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List, Tuple
|
|
13
|
+
|
|
14
|
+
from ..utils.errors import ConfigurationError
|
|
15
|
+
from .models import (
|
|
16
|
+
Config,
|
|
17
|
+
ProviderConfig,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ConfigMigration:
|
|
22
|
+
"""Handles configuration migration between versions."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, config_path):
|
|
25
|
+
"""
|
|
26
|
+
Initialize the migration system.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
config_path: Path to the configuration file (str or Path)
|
|
30
|
+
"""
|
|
31
|
+
if isinstance(config_path, str):
|
|
32
|
+
self.config_path = Path(config_path)
|
|
33
|
+
elif hasattr(config_path, "__fspath__") or isinstance(config_path, Path):
|
|
34
|
+
self.config_path = Path(config_path)
|
|
35
|
+
else:
|
|
36
|
+
# Handle other path-like objects
|
|
37
|
+
self.config_path = Path(str(config_path))
|
|
38
|
+
|
|
39
|
+
self.backup_dir = self.config_path.parent / "backups"
|
|
40
|
+
self.backup_dir.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
|
|
42
|
+
def needs_migration(self) -> bool:
|
|
43
|
+
"""
|
|
44
|
+
Check if the configuration needs migration.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
True if migration is needed
|
|
48
|
+
"""
|
|
49
|
+
if not self.config_path.exists():
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
with open(self.config_path, "r") as f:
|
|
54
|
+
config_data = json.load(f)
|
|
55
|
+
|
|
56
|
+
# Check version
|
|
57
|
+
current_version = config_data.get("config_version", "1.0")
|
|
58
|
+
return current_version != "2.0"
|
|
59
|
+
|
|
60
|
+
except (json.JSONDecodeError, KeyError):
|
|
61
|
+
# If we can't read the config or it's malformed, assume migration needed
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
def migrate_config(self) -> Tuple[bool, List[str]]:
|
|
65
|
+
"""
|
|
66
|
+
Migrate configuration to the latest version.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Tuple of (success, list of migration messages)
|
|
70
|
+
"""
|
|
71
|
+
messages = []
|
|
72
|
+
|
|
73
|
+
if not self.config_path.exists():
|
|
74
|
+
messages.append("No configuration file found, nothing to migrate")
|
|
75
|
+
return True, messages
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
# Create backup first
|
|
79
|
+
backup_path = self.create_backup()
|
|
80
|
+
messages.append(f"Created backup at: {backup_path}")
|
|
81
|
+
|
|
82
|
+
# Load old configuration
|
|
83
|
+
with open(self.config_path, "r") as f:
|
|
84
|
+
old_config = json.load(f)
|
|
85
|
+
|
|
86
|
+
# Determine migration path
|
|
87
|
+
old_version = old_config.get("config_version", "1.0")
|
|
88
|
+
messages.append(f"Migrating from version {old_version} to 2.0")
|
|
89
|
+
|
|
90
|
+
# Perform migration based on version
|
|
91
|
+
if old_version == "1.0" or "config_version" not in old_config:
|
|
92
|
+
new_config = self._migrate_from_v1(old_config)
|
|
93
|
+
messages.extend(self._get_v1_migration_messages())
|
|
94
|
+
else:
|
|
95
|
+
# Future migrations would go here
|
|
96
|
+
new_config = old_config
|
|
97
|
+
messages.append(f"No migration needed for version {old_version}")
|
|
98
|
+
|
|
99
|
+
# Validate migrated configuration
|
|
100
|
+
try:
|
|
101
|
+
config_obj = Config(**new_config)
|
|
102
|
+
validation_errors = config_obj.validate_configuration()
|
|
103
|
+
if validation_errors:
|
|
104
|
+
messages.append(
|
|
105
|
+
"Warning: Migrated configuration has validation errors:"
|
|
106
|
+
)
|
|
107
|
+
messages.extend([f" - {error}" for error in validation_errors])
|
|
108
|
+
except Exception as e:
|
|
109
|
+
messages.append(
|
|
110
|
+
f"Warning: Could not validate migrated configuration: {e}"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Save migrated configuration
|
|
114
|
+
with open(self.config_path, "w") as f:
|
|
115
|
+
json.dump(new_config, f, indent=2)
|
|
116
|
+
|
|
117
|
+
messages.append("Configuration migration completed successfully")
|
|
118
|
+
return True, messages
|
|
119
|
+
|
|
120
|
+
except Exception as e:
|
|
121
|
+
messages.append(f"Migration failed: {e}")
|
|
122
|
+
# Try to restore backup
|
|
123
|
+
try:
|
|
124
|
+
self.restore_backup(backup_path)
|
|
125
|
+
messages.append("Restored backup due to migration failure")
|
|
126
|
+
except Exception as restore_error:
|
|
127
|
+
messages.append(f"Failed to restore backup: {restore_error}")
|
|
128
|
+
|
|
129
|
+
return False, messages
|
|
130
|
+
|
|
131
|
+
def create_backup(self) -> Path:
|
|
132
|
+
"""
|
|
133
|
+
Create a backup of the current configuration.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Path to the backup file
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
ConfigurationError: If backup creation fails
|
|
140
|
+
"""
|
|
141
|
+
if not self.config_path.exists():
|
|
142
|
+
raise ConfigurationError("No configuration file to backup")
|
|
143
|
+
|
|
144
|
+
# Create unique timestamp with microseconds to avoid collisions
|
|
145
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
|
146
|
+
backup_filename = f"config_backup_{timestamp}.json"
|
|
147
|
+
backup_path = self.backup_dir / backup_filename
|
|
148
|
+
|
|
149
|
+
# Ensure backup directory exists
|
|
150
|
+
self.backup_dir.mkdir(parents=True, exist_ok=True)
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
shutil.copy2(self.config_path, backup_path)
|
|
154
|
+
return backup_path
|
|
155
|
+
except Exception as e:
|
|
156
|
+
raise ConfigurationError(f"Failed to create backup: {e}")
|
|
157
|
+
|
|
158
|
+
def restore_backup(self, backup_path: Path) -> None:
|
|
159
|
+
"""
|
|
160
|
+
Restore configuration from a backup.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
backup_path: Path to the backup file
|
|
164
|
+
|
|
165
|
+
Raises:
|
|
166
|
+
ConfigurationError: If restore fails
|
|
167
|
+
"""
|
|
168
|
+
if not backup_path.exists():
|
|
169
|
+
raise ConfigurationError(f"Backup file not found: {backup_path}")
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
shutil.copy2(backup_path, self.config_path)
|
|
173
|
+
except Exception as e:
|
|
174
|
+
raise ConfigurationError(f"Failed to restore backup: {e}")
|
|
175
|
+
|
|
176
|
+
def list_backups(self) -> List[Dict[str, Any]]:
|
|
177
|
+
"""
|
|
178
|
+
List available configuration backups.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
List of backup information dictionaries
|
|
182
|
+
"""
|
|
183
|
+
backups = []
|
|
184
|
+
|
|
185
|
+
if not self.backup_dir.exists():
|
|
186
|
+
return backups
|
|
187
|
+
|
|
188
|
+
for backup_file in self.backup_dir.glob("config_backup_*.json"):
|
|
189
|
+
try:
|
|
190
|
+
stat = backup_file.stat()
|
|
191
|
+
backups.append(
|
|
192
|
+
{
|
|
193
|
+
"path": backup_file,
|
|
194
|
+
"filename": backup_file.name,
|
|
195
|
+
"created": datetime.fromtimestamp(stat.st_ctime),
|
|
196
|
+
"size": stat.st_size,
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
except Exception:
|
|
200
|
+
# Skip files we can't read
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
# Sort by creation time, newest first
|
|
204
|
+
backups.sort(key=lambda x: x["created"], reverse=True)
|
|
205
|
+
return backups
|
|
206
|
+
|
|
207
|
+
def cleanup_old_backups(self, keep_count: int = 10) -> int:
|
|
208
|
+
"""
|
|
209
|
+
Clean up old backup files, keeping only the most recent ones.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
keep_count: Number of backups to keep
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Number of backups deleted
|
|
216
|
+
"""
|
|
217
|
+
backups = self.list_backups()
|
|
218
|
+
|
|
219
|
+
if len(backups) <= keep_count:
|
|
220
|
+
return 0
|
|
221
|
+
|
|
222
|
+
deleted_count = 0
|
|
223
|
+
# Keep the first keep_count backups (newest), delete the rest (oldest)
|
|
224
|
+
for backup in backups[keep_count:]:
|
|
225
|
+
try:
|
|
226
|
+
backup["path"].unlink()
|
|
227
|
+
deleted_count += 1
|
|
228
|
+
except Exception:
|
|
229
|
+
# Skip files we can't delete
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
return deleted_count
|
|
233
|
+
|
|
234
|
+
def _migrate_from_v1(self, old_config: Dict[str, Any]) -> Dict[str, Any]:
|
|
235
|
+
"""
|
|
236
|
+
Migrate from version 1.0 configuration format.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
old_config: Old configuration dictionary
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
New configuration dictionary
|
|
243
|
+
"""
|
|
244
|
+
new_config = {
|
|
245
|
+
"config_version": "2.0",
|
|
246
|
+
"created_at": datetime.now().isoformat(),
|
|
247
|
+
"updated_at": datetime.now().isoformat(),
|
|
248
|
+
"default_provider": old_config.get("default_provider", "claude"),
|
|
249
|
+
"storage_path": old_config.get(
|
|
250
|
+
"storage_path", str(Path.home() / ".omnimancer")
|
|
251
|
+
),
|
|
252
|
+
"providers": {},
|
|
253
|
+
"chat_settings": {
|
|
254
|
+
"max_tokens": old_config.get("max_tokens"),
|
|
255
|
+
"temperature": old_config.get("temperature"),
|
|
256
|
+
"context_length": old_config.get("context_length", 4000),
|
|
257
|
+
"save_history": old_config.get("save_history", True),
|
|
258
|
+
},
|
|
259
|
+
"mcp": {
|
|
260
|
+
"enabled": True,
|
|
261
|
+
"servers": {},
|
|
262
|
+
"auto_approve_timeout": 30,
|
|
263
|
+
"max_concurrent_servers": 10,
|
|
264
|
+
"default_timeout": 30,
|
|
265
|
+
"retry_attempts": 3,
|
|
266
|
+
"retry_delay": 1.0,
|
|
267
|
+
"allow_dangerous_tools": False,
|
|
268
|
+
"sandbox_mode": True,
|
|
269
|
+
"max_tool_execution_time": 300,
|
|
270
|
+
"log_tool_calls": True,
|
|
271
|
+
"log_level": "INFO",
|
|
272
|
+
"metrics_enabled": False,
|
|
273
|
+
"auto_discover_tools": True,
|
|
274
|
+
"tool_cache_ttl": 3600,
|
|
275
|
+
"refresh_tools_on_startup": True,
|
|
276
|
+
},
|
|
277
|
+
"profiles": {},
|
|
278
|
+
"active_profile": None,
|
|
279
|
+
"config_sources": {},
|
|
280
|
+
"debug_mode": old_config.get("debug_mode", False),
|
|
281
|
+
"log_level": old_config.get("log_level", "INFO"),
|
|
282
|
+
"auto_update_check": True,
|
|
283
|
+
"telemetry_enabled": False,
|
|
284
|
+
"provider_fallback_enabled": True,
|
|
285
|
+
"provider_health_check_enabled": True,
|
|
286
|
+
"provider_timeout_default": 30.0,
|
|
287
|
+
"api_key_encryption_enabled": True,
|
|
288
|
+
"secure_storage_enabled": True,
|
|
289
|
+
"concurrent_requests_limit": 5,
|
|
290
|
+
"request_cache_enabled": True,
|
|
291
|
+
"request_cache_ttl": 300,
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
# Migrate provider configurations
|
|
295
|
+
old_providers = old_config.get("providers", {})
|
|
296
|
+
for provider_name, provider_config in old_providers.items():
|
|
297
|
+
new_provider_config = self._migrate_provider_config_v1(
|
|
298
|
+
provider_name, provider_config
|
|
299
|
+
)
|
|
300
|
+
new_config["providers"][provider_name] = new_provider_config
|
|
301
|
+
|
|
302
|
+
# Migrate MCP configuration if it exists
|
|
303
|
+
if "mcp" in old_config:
|
|
304
|
+
old_mcp = old_config["mcp"]
|
|
305
|
+
new_config["mcp"].update(
|
|
306
|
+
{
|
|
307
|
+
"enabled": old_mcp.get("enabled", True),
|
|
308
|
+
"servers": self._migrate_mcp_servers_v1(old_mcp.get("servers", {})),
|
|
309
|
+
"auto_approve_timeout": old_mcp.get("auto_approve_timeout", 30),
|
|
310
|
+
"max_concurrent_servers": old_mcp.get("max_concurrent_servers", 10),
|
|
311
|
+
}
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
return new_config
|
|
315
|
+
|
|
316
|
+
def _migrate_provider_config_v1(
|
|
317
|
+
self, provider_name: str, old_config: Dict[str, Any]
|
|
318
|
+
) -> Dict[str, Any]:
|
|
319
|
+
"""
|
|
320
|
+
Migrate a single provider configuration from v1 format.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
provider_name: Name of the provider
|
|
324
|
+
old_config: Old provider configuration
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
New provider configuration dictionary
|
|
328
|
+
"""
|
|
329
|
+
# Get provider defaults
|
|
330
|
+
provider_defaults = self._get_provider_defaults(provider_name)
|
|
331
|
+
|
|
332
|
+
new_config = {
|
|
333
|
+
"api_key": old_config.get("api_key"),
|
|
334
|
+
"model": old_config.get("model", provider_defaults.get("model", "default")),
|
|
335
|
+
"max_tokens": old_config.get("max_tokens"),
|
|
336
|
+
"temperature": old_config.get("temperature"),
|
|
337
|
+
"base_url": old_config.get("base_url"),
|
|
338
|
+
"organization": old_config.get("organization"),
|
|
339
|
+
"project_id": old_config.get("project_id"),
|
|
340
|
+
"timeout": old_config.get("timeout"),
|
|
341
|
+
"top_p": old_config.get("top_p"),
|
|
342
|
+
"frequency_penalty": old_config.get("frequency_penalty"),
|
|
343
|
+
"presence_penalty": old_config.get("presence_penalty"),
|
|
344
|
+
"config_version": "2.0",
|
|
345
|
+
"enabled": old_config.get("enabled", True),
|
|
346
|
+
"priority": old_config.get(
|
|
347
|
+
"priority", provider_defaults.get("priority", 0)
|
|
348
|
+
),
|
|
349
|
+
"max_retries": old_config.get("max_retries", 3),
|
|
350
|
+
"retry_delay": old_config.get("retry_delay", 1.0),
|
|
351
|
+
"exponential_backoff": old_config.get("exponential_backoff", True),
|
|
352
|
+
"health_check_enabled": old_config.get("health_check_enabled", True),
|
|
353
|
+
"health_check_interval": old_config.get("health_check_interval", 300),
|
|
354
|
+
"health_check_timeout": old_config.get("health_check_timeout", 10.0),
|
|
355
|
+
"auth_type": old_config.get("auth_type", "api_key"),
|
|
356
|
+
"custom_headers": old_config.get("custom_headers"),
|
|
357
|
+
"oauth_config": old_config.get("oauth_config"),
|
|
358
|
+
"extra_settings": old_config.get("extra_settings"),
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
# Add provider-specific defaults
|
|
362
|
+
new_config.update(provider_defaults)
|
|
363
|
+
|
|
364
|
+
# Remove None values
|
|
365
|
+
return {k: v for k, v in new_config.items() if v is not None}
|
|
366
|
+
|
|
367
|
+
def _migrate_mcp_servers_v1(self, old_servers: Dict[str, Any]) -> Dict[str, Any]:
|
|
368
|
+
"""
|
|
369
|
+
Migrate MCP server configurations from v1 format.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
old_servers: Old MCP server configurations
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
New MCP server configurations dictionary
|
|
376
|
+
"""
|
|
377
|
+
new_servers = {}
|
|
378
|
+
|
|
379
|
+
for server_name, server_config in old_servers.items():
|
|
380
|
+
new_servers[server_name] = {
|
|
381
|
+
"name": server_config.get("name", server_name),
|
|
382
|
+
"command": server_config.get("command", ""),
|
|
383
|
+
"args": server_config.get("args", []),
|
|
384
|
+
"env": server_config.get("env", {}),
|
|
385
|
+
"enabled": server_config.get("enabled", True),
|
|
386
|
+
"auto_approve": server_config.get("auto_approve", []),
|
|
387
|
+
"timeout": server_config.get("timeout", 30),
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return new_servers
|
|
391
|
+
|
|
392
|
+
def _get_provider_defaults(self, provider_name: str) -> Dict[str, Any]:
|
|
393
|
+
"""
|
|
394
|
+
Get default configuration values for a provider.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
provider_name: Name of the provider
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
Dictionary of default configuration values
|
|
401
|
+
"""
|
|
402
|
+
defaults = {
|
|
403
|
+
"claude": {
|
|
404
|
+
"model": "claude-3-sonnet-20240229",
|
|
405
|
+
"provider_type": "claude",
|
|
406
|
+
"supports_tools": True,
|
|
407
|
+
"supports_multimodal": True,
|
|
408
|
+
"supports_streaming": True,
|
|
409
|
+
"supports_function_calling": True,
|
|
410
|
+
"supports_system_messages": True,
|
|
411
|
+
"priority": 10,
|
|
412
|
+
"auth_type": "api_key",
|
|
413
|
+
},
|
|
414
|
+
"openai": {
|
|
415
|
+
"model": "gpt-4",
|
|
416
|
+
"provider_type": "openai",
|
|
417
|
+
"supports_tools": True,
|
|
418
|
+
"supports_multimodal": True,
|
|
419
|
+
"supports_streaming": True,
|
|
420
|
+
"supports_function_calling": True,
|
|
421
|
+
"supports_system_messages": True,
|
|
422
|
+
"priority": 9,
|
|
423
|
+
"auth_type": "api_key",
|
|
424
|
+
},
|
|
425
|
+
"gemini": {
|
|
426
|
+
"model": "gemini-1.5-pro",
|
|
427
|
+
"provider_type": "gemini",
|
|
428
|
+
"supports_tools": True,
|
|
429
|
+
"supports_multimodal": True,
|
|
430
|
+
"supports_streaming": True,
|
|
431
|
+
"supports_function_calling": True,
|
|
432
|
+
"supports_system_messages": True,
|
|
433
|
+
"priority": 8,
|
|
434
|
+
"auth_type": "api_key",
|
|
435
|
+
"safety_settings": {
|
|
436
|
+
"HARM_CATEGORY_HARASSMENT": "BLOCK_MEDIUM_AND_ABOVE",
|
|
437
|
+
"HARM_CATEGORY_HATE_SPEECH": "BLOCK_MEDIUM_AND_ABOVE",
|
|
438
|
+
"HARM_CATEGORY_SEXUALLY_EXPLICIT": "BLOCK_MEDIUM_AND_ABOVE",
|
|
439
|
+
"HARM_CATEGORY_DANGEROUS_CONTENT": "BLOCK_MEDIUM_AND_ABOVE",
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
"cohere": {
|
|
443
|
+
"model": "command-r",
|
|
444
|
+
"provider_type": "cohere",
|
|
445
|
+
"supports_tools": False,
|
|
446
|
+
"supports_multimodal": False,
|
|
447
|
+
"supports_streaming": True,
|
|
448
|
+
"supports_function_calling": False,
|
|
449
|
+
"supports_system_messages": True,
|
|
450
|
+
"priority": 7,
|
|
451
|
+
"auth_type": "api_key",
|
|
452
|
+
},
|
|
453
|
+
"ollama": {
|
|
454
|
+
"model": "llama2",
|
|
455
|
+
"provider_type": "ollama",
|
|
456
|
+
"supports_tools": False,
|
|
457
|
+
"supports_multimodal": False,
|
|
458
|
+
"supports_streaming": True,
|
|
459
|
+
"supports_function_calling": False,
|
|
460
|
+
"supports_system_messages": True,
|
|
461
|
+
"priority": 6,
|
|
462
|
+
"auth_type": "none",
|
|
463
|
+
"base_url": "http://localhost:11434",
|
|
464
|
+
},
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return defaults.get(
|
|
468
|
+
provider_name,
|
|
469
|
+
{
|
|
470
|
+
"provider_type": provider_name,
|
|
471
|
+
"supports_tools": False,
|
|
472
|
+
"supports_multimodal": False,
|
|
473
|
+
"supports_streaming": True,
|
|
474
|
+
"supports_function_calling": False,
|
|
475
|
+
"supports_system_messages": True,
|
|
476
|
+
"priority": 5,
|
|
477
|
+
"auth_type": "api_key",
|
|
478
|
+
},
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
def _get_v1_migration_messages(self) -> List[str]:
|
|
482
|
+
"""
|
|
483
|
+
Get informational messages about v1 migration.
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
List of migration messages
|
|
487
|
+
"""
|
|
488
|
+
return [
|
|
489
|
+
"Added new provider capability flags (supports_tools, supports_multimodal, etc.)",
|
|
490
|
+
"Added provider priority and health check settings",
|
|
491
|
+
"Added MCP (Model Context Protocol) configuration section",
|
|
492
|
+
"Added configuration profiles support",
|
|
493
|
+
"Added enhanced security and performance settings",
|
|
494
|
+
"Added configuration source tracking",
|
|
495
|
+
"Updated configuration schema to version 2.0",
|
|
496
|
+
]
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
class ConfigValidator:
|
|
500
|
+
"""Validates configuration and provides detailed error reporting."""
|
|
501
|
+
|
|
502
|
+
def __init__(self, config: Config):
|
|
503
|
+
"""
|
|
504
|
+
Initialize the validator.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
config: Configuration to validate
|
|
508
|
+
"""
|
|
509
|
+
self.config = config
|
|
510
|
+
|
|
511
|
+
def validate_config(self, config: Config) -> List[str]:
|
|
512
|
+
"""
|
|
513
|
+
Validate configuration and return list of errors.
|
|
514
|
+
|
|
515
|
+
Args:
|
|
516
|
+
config: Configuration to validate
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
List of validation errors
|
|
520
|
+
"""
|
|
521
|
+
self.config = config
|
|
522
|
+
errors = []
|
|
523
|
+
|
|
524
|
+
# General validation
|
|
525
|
+
general_errors = self._validate_general()
|
|
526
|
+
errors.extend(general_errors)
|
|
527
|
+
|
|
528
|
+
# Provider validation
|
|
529
|
+
for provider_name, provider_config in config.providers.items():
|
|
530
|
+
provider_errors = self._validate_provider(provider_name, provider_config)
|
|
531
|
+
errors.extend(provider_errors)
|
|
532
|
+
|
|
533
|
+
# MCP validation
|
|
534
|
+
mcp_errors = self._validate_mcp()
|
|
535
|
+
errors.extend(mcp_errors)
|
|
536
|
+
|
|
537
|
+
return errors
|
|
538
|
+
|
|
539
|
+
def validate_full_config(self) -> Dict[str, List[str]]:
|
|
540
|
+
"""
|
|
541
|
+
Perform comprehensive configuration validation.
|
|
542
|
+
|
|
543
|
+
Returns:
|
|
544
|
+
Dictionary mapping validation categories to error lists
|
|
545
|
+
"""
|
|
546
|
+
validation_results = {
|
|
547
|
+
"general": [],
|
|
548
|
+
"providers": {},
|
|
549
|
+
"mcp": [],
|
|
550
|
+
"profiles": {},
|
|
551
|
+
"security": [],
|
|
552
|
+
"performance": [],
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
# General validation
|
|
556
|
+
validation_results["general"] = self._validate_general()
|
|
557
|
+
|
|
558
|
+
# Provider validation
|
|
559
|
+
for provider_name, provider_config in self.config.providers.items():
|
|
560
|
+
provider_errors = self._validate_provider(provider_name, provider_config)
|
|
561
|
+
if provider_errors:
|
|
562
|
+
validation_results["providers"][provider_name] = provider_errors
|
|
563
|
+
|
|
564
|
+
# MCP validation
|
|
565
|
+
validation_results["mcp"] = self._validate_mcp()
|
|
566
|
+
|
|
567
|
+
# Profile validation
|
|
568
|
+
for profile_name, profile in self.config.profiles.items():
|
|
569
|
+
profile_errors = self._validate_profile(profile_name, profile)
|
|
570
|
+
if profile_errors:
|
|
571
|
+
validation_results["profiles"][profile_name] = profile_errors
|
|
572
|
+
|
|
573
|
+
# Security validation
|
|
574
|
+
validation_results["security"] = self._validate_security()
|
|
575
|
+
|
|
576
|
+
# Performance validation
|
|
577
|
+
validation_results["performance"] = self._validate_performance()
|
|
578
|
+
|
|
579
|
+
# Remove empty categories
|
|
580
|
+
return {k: v for k, v in validation_results.items() if v}
|
|
581
|
+
|
|
582
|
+
def _validate_general(self) -> List[str]:
|
|
583
|
+
"""Validate general configuration settings."""
|
|
584
|
+
errors = []
|
|
585
|
+
|
|
586
|
+
if not self.config.default_provider:
|
|
587
|
+
errors.append("No default provider specified")
|
|
588
|
+
elif self.config.default_provider not in self.config.providers:
|
|
589
|
+
errors.append(
|
|
590
|
+
f"Default provider '{self.config.default_provider}' not configured"
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
if not self.config.providers:
|
|
594
|
+
errors.append("No providers configured")
|
|
595
|
+
|
|
596
|
+
if not self.config.storage_path:
|
|
597
|
+
errors.append("No storage path specified")
|
|
598
|
+
else:
|
|
599
|
+
try:
|
|
600
|
+
storage_path = Path(self.config.storage_path).expanduser()
|
|
601
|
+
if not storage_path.parent.exists():
|
|
602
|
+
errors.append(
|
|
603
|
+
f"Storage path parent directory does not exist: {storage_path.parent}"
|
|
604
|
+
)
|
|
605
|
+
except Exception as e:
|
|
606
|
+
errors.append(f"Invalid storage path: {e}")
|
|
607
|
+
|
|
608
|
+
return errors
|
|
609
|
+
|
|
610
|
+
def _validate_provider(
|
|
611
|
+
self, provider_name: str, config: ProviderConfig
|
|
612
|
+
) -> List[str]:
|
|
613
|
+
"""Validate a single provider configuration."""
|
|
614
|
+
errors = []
|
|
615
|
+
|
|
616
|
+
if not config.model:
|
|
617
|
+
errors.append("No model specified")
|
|
618
|
+
|
|
619
|
+
if config.auth_type != "none" and not config.api_key:
|
|
620
|
+
errors.append("API key required but not provided")
|
|
621
|
+
|
|
622
|
+
# Provider-specific validation
|
|
623
|
+
if config.provider_type == "claude" or provider_name == "claude":
|
|
624
|
+
errors.extend(self._validate_claude_provider(config))
|
|
625
|
+
elif config.provider_type == "openai" or provider_name == "openai":
|
|
626
|
+
errors.extend(self._validate_openai_provider(config))
|
|
627
|
+
elif config.provider_type == "gemini" or provider_name == "gemini":
|
|
628
|
+
errors.extend(self._validate_gemini_provider(config))
|
|
629
|
+
elif config.provider_type == "cohere" or provider_name == "cohere":
|
|
630
|
+
errors.extend(self._validate_cohere_provider(config))
|
|
631
|
+
elif config.provider_type == "ollama" or provider_name == "ollama":
|
|
632
|
+
errors.extend(self._validate_ollama_provider(config))
|
|
633
|
+
|
|
634
|
+
return errors
|
|
635
|
+
|
|
636
|
+
def _validate_claude_provider(self, config: ProviderConfig) -> List[str]:
|
|
637
|
+
"""Validate Claude provider configuration."""
|
|
638
|
+
errors = []
|
|
639
|
+
|
|
640
|
+
valid_models = [
|
|
641
|
+
"claude-3-sonnet-20240229",
|
|
642
|
+
"claude-3-haiku-20240307",
|
|
643
|
+
"claude-3-opus-20240229",
|
|
644
|
+
"claude-3-5-sonnet-20241022",
|
|
645
|
+
]
|
|
646
|
+
if config.model not in valid_models:
|
|
647
|
+
errors.append(
|
|
648
|
+
f"Unknown Claude model '{config.model}'. Valid models: {', '.join(valid_models)}"
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
return errors
|
|
652
|
+
|
|
653
|
+
def _validate_openai_provider(self, config: ProviderConfig) -> List[str]:
|
|
654
|
+
"""Validate OpenAI provider configuration."""
|
|
655
|
+
errors = []
|
|
656
|
+
|
|
657
|
+
valid_models = [
|
|
658
|
+
"gpt-4",
|
|
659
|
+
"gpt-4-turbo",
|
|
660
|
+
"gpt-4o",
|
|
661
|
+
"gpt-4o-mini",
|
|
662
|
+
"gpt-3.5-turbo",
|
|
663
|
+
"gpt-3.5-turbo-16k",
|
|
664
|
+
]
|
|
665
|
+
if config.model not in valid_models:
|
|
666
|
+
errors.append(
|
|
667
|
+
f"Unknown OpenAI model '{config.model}'. Valid models: {', '.join(valid_models)}"
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
return errors
|
|
671
|
+
|
|
672
|
+
def _validate_gemini_provider(self, config: ProviderConfig) -> List[str]:
|
|
673
|
+
"""Validate Gemini provider configuration."""
|
|
674
|
+
errors = []
|
|
675
|
+
|
|
676
|
+
valid_models = ["gemini-1.5-pro", "gemini-1.5-flash", "gemini-1.0-pro"]
|
|
677
|
+
if config.model not in valid_models:
|
|
678
|
+
errors.append(
|
|
679
|
+
f"Unknown Gemini model '{config.model}'. Valid models: {', '.join(valid_models)}"
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
return errors
|
|
683
|
+
|
|
684
|
+
def _validate_cohere_provider(self, config: ProviderConfig) -> List[str]:
|
|
685
|
+
"""Validate Cohere provider configuration."""
|
|
686
|
+
errors = []
|
|
687
|
+
|
|
688
|
+
valid_models = [
|
|
689
|
+
"command-r",
|
|
690
|
+
"command-r-plus",
|
|
691
|
+
"command-light",
|
|
692
|
+
"command",
|
|
693
|
+
]
|
|
694
|
+
if config.model not in valid_models:
|
|
695
|
+
errors.append(
|
|
696
|
+
f"Unknown Cohere model '{config.model}'. Valid models: {', '.join(valid_models)}"
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
return errors
|
|
700
|
+
|
|
701
|
+
def _validate_ollama_provider(self, config: ProviderConfig) -> List[str]:
|
|
702
|
+
"""Validate Ollama provider configuration."""
|
|
703
|
+
errors = []
|
|
704
|
+
|
|
705
|
+
if config.base_url and not config.base_url.startswith(("http://", "https://")):
|
|
706
|
+
errors.append("Ollama base_url must start with 'http://' or 'https://'")
|
|
707
|
+
|
|
708
|
+
return errors
|
|
709
|
+
|
|
710
|
+
def _validate_mcp(self) -> List[str]:
|
|
711
|
+
"""Validate MCP configuration."""
|
|
712
|
+
errors = []
|
|
713
|
+
|
|
714
|
+
for server_name, server_config in self.config.mcp.servers.items():
|
|
715
|
+
if not server_config.name:
|
|
716
|
+
errors.append(f"MCP server '{server_name}' has no name")
|
|
717
|
+
if not server_config.command:
|
|
718
|
+
errors.append(f"MCP server '{server_name}' has no command")
|
|
719
|
+
|
|
720
|
+
return errors
|
|
721
|
+
|
|
722
|
+
def _validate_profile(
|
|
723
|
+
self, profile_name: str, profile: "ConfigProfile"
|
|
724
|
+
) -> List[str]:
|
|
725
|
+
"""Validate a configuration profile."""
|
|
726
|
+
errors = []
|
|
727
|
+
|
|
728
|
+
if not profile.default_provider:
|
|
729
|
+
errors.append("No default provider specified")
|
|
730
|
+
elif profile.default_provider not in profile.providers:
|
|
731
|
+
errors.append(
|
|
732
|
+
f"Default provider '{profile.default_provider}' not configured"
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
if not profile.providers:
|
|
736
|
+
errors.append("No providers configured")
|
|
737
|
+
|
|
738
|
+
return errors
|
|
739
|
+
|
|
740
|
+
def _validate_security(self) -> List[str]:
|
|
741
|
+
"""Validate security-related settings."""
|
|
742
|
+
errors = []
|
|
743
|
+
|
|
744
|
+
# Check for insecure configurations
|
|
745
|
+
if not self.config.api_key_encryption_enabled:
|
|
746
|
+
errors.append("API key encryption is disabled - this is a security risk")
|
|
747
|
+
|
|
748
|
+
if not self.config.secure_storage_enabled:
|
|
749
|
+
errors.append("Secure storage is disabled - this is a security risk")
|
|
750
|
+
|
|
751
|
+
# Check MCP security settings
|
|
752
|
+
if self.config.mcp.allow_dangerous_tools:
|
|
753
|
+
errors.append("MCP dangerous tools are allowed - this is a security risk")
|
|
754
|
+
|
|
755
|
+
if not self.config.mcp.sandbox_mode:
|
|
756
|
+
errors.append("MCP sandbox mode is disabled - this is a security risk")
|
|
757
|
+
|
|
758
|
+
return errors
|
|
759
|
+
|
|
760
|
+
def _validate_performance(self) -> List[str]:
|
|
761
|
+
"""Validate performance-related settings."""
|
|
762
|
+
errors = []
|
|
763
|
+
|
|
764
|
+
if self.config.concurrent_requests_limit > 20:
|
|
765
|
+
errors.append(
|
|
766
|
+
"High concurrent request limit may cause rate limiting issues"
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
if self.config.request_cache_ttl > 3600:
|
|
770
|
+
errors.append("Long cache TTL may cause stale data issues")
|
|
771
|
+
|
|
772
|
+
return errors
|