-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathsetup.py
More file actions
1494 lines (1234 loc) · 69.6 KB
/
setup.py
File metadata and controls
1494 lines (1234 loc) · 69.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
"""
Aether Installer & Configurator
Interactive setup script for first-time installation and configuration.
"""
import os
import sys
from pathlib import Path
from typing import Dict, Any, Optional, List
# Add project root to path for imports
PROJECT_ROOT = Path(__file__).parent.absolute()
sys.path.insert(0, str(PROJECT_ROOT))
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.prompt import Prompt, Confirm, IntPrompt
from rich.progress import Progress, SpinnerColumn, TextColumn
import questionary
from questionary import Style
from utils.setup_helpers import (
DependencyDetector,
APIKeyValidator,
FoundryInstaller,
VirtualEnvHelper,
check_directory_writable,
test_import
)
def _check_halmos_available() -> bool:
"""Check if halmos binary is available (optional dependency)."""
import shutil
return shutil.which("halmos") is not None
# Custom style for questionary (matches rich theme)
custom_style = Style([
('qmark', 'fg:#00d7ff bold'), # Cyan question mark
('question', 'bold'), # Bold question
('answer', 'fg:#00d7ff bold'), # Cyan answer
('pointer', 'fg:#00d7ff bold'), # Cyan pointer
('highlighted', 'fg:#00d7ff bold'), # Cyan highlight
('selected', 'fg:#00d7ff'), # Cyan selected
('separator', 'fg:#666666'), # Gray separator
('instruction', ''), # Default instruction
('text', ''), # Default text
])
def select_with_arrows(prompt_text: str, choices: List[str], default: Optional[str] = None) -> str:
"""Interactive selector with arrow keys, space to select, enter to confirm.
Args:
prompt_text: Prompt text to display
choices: List of items to choose from
default: Default selection (item from choices list)
Returns:
The selected item
"""
try:
result = questionary.select(
prompt_text,
choices=choices,
default=default if default in choices else (choices[0] if choices else None),
style=custom_style,
use_shortcuts=True,
use_arrow_keys=True,
use_jk_keys=False
).ask()
return result if result else (default if default else choices[0])
except (KeyboardInterrupt, EOFError):
# User cancelled - return default
return default if default else choices[0]
def fetch_available_models(api_key: str) -> Dict[str, List[str]]:
"""Fetch available models from OpenAI API.
Returns a dict with categorized models:
- gpt5_models: List of GPT-5 models
- gpt4_models: List of GPT-4 models
- all_models: All available GPT models
"""
try:
from openai import OpenAI
client = OpenAI(api_key=api_key)
# Fetch all models
models = client.models.list()
model_ids = [m.id for m in models.data if 'gpt' in m.id.lower()]
# Categorize models
gpt5_models = sorted([m for m in model_ids if m.startswith('gpt-5')], reverse=True)
gpt4_models = sorted([m for m in model_ids if m.startswith('gpt-4')], reverse=True)
return {
'gpt5_models': gpt5_models,
'gpt4_models': gpt4_models,
'all_models': sorted(model_ids, reverse=True)
}
except Exception as e:
print(f"⚠️ Could not fetch models from API: {e}")
# Fallback to known models
return {
'gpt5_models': ['gpt-5.3-chat-latest', 'gpt-5.3-mini', 'gpt-5-chat-latest', 'gpt-5-pro', 'gpt-5-mini', 'gpt-5-nano'],
'gpt4_models': ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo'],
'all_models': ['gpt-5.3-chat-latest', 'gpt-5.3-mini', 'gpt-5-chat-latest', 'gpt-5-pro', 'gpt-5-mini', 'gpt-5-nano', 'gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo']
}
def fetch_available_gemini_models(api_key: str) -> Dict[str, List[str]]:
"""Fetch available Gemini models.
Note: Gemini API doesn't have a models.list() endpoint like OpenAI,
so we query available models via API call or use known models.
Returns a dict with categorized models:
- gemini_2_5_models: List of Gemini 2.5 models
- gemini_1_5_models: List of Gemini 1.5 models
- all_models: All available Gemini models
"""
try:
import requests
# Try to list models via Gemini API
url = f"https://generativelanguage.googleapis.com/v1beta/models?key={api_key}"
response = requests.get(url, timeout=5)
if response.status_code == 200:
data = response.json()
models = data.get('models', [])
# Extract model names that support generateContent
model_names = []
for model in models:
name = model.get('name', '').replace('models/', '')
# Only include models that support generateContent
if 'generateContent' in model.get('supportedGenerationMethods', []):
model_names.append(name)
# Categorize models
gemini_2_5_models = sorted([m for m in model_names if m.startswith('gemini-2.5')], reverse=True)
gemini_1_5_models = sorted([m for m in model_names if m.startswith('gemini-1.5')], reverse=True)
return {
'gemini_2_5_models': gemini_2_5_models,
'gemini_1_5_models': gemini_1_5_models,
'all_models': sorted(model_names, reverse=True)
}
except Exception as e:
print(f"⚠️ Could not fetch Gemini models from API: {e}")
# Fallback to known models
return {
'gemini_2_5_models': ['gemini-3.0-flash', 'gemini-3.0-pro', 'gemini-2.5-flash', 'gemini-2.5-pro'],
'gemini_1_5_models': ['gemini-1.5-flash', 'gemini-1.5-pro'],
'all_models': ['gemini-3.0-flash', 'gemini-3.0-pro', 'gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-1.5-flash', 'gemini-1.5-pro']
}
def fetch_available_anthropic_models(api_key: str) -> Dict[str, List[str]]:
"""Fetch available Anthropic Claude models.
Returns a dict with categorized models:
- claude_4_models: List of Claude 4.x models
- all_models: All available Claude models
"""
try:
import anthropic
client = anthropic.Anthropic(api_key=api_key)
# Anthropic doesn't have a models.list() endpoint, use known models
# Try a minimal call to verify the key works
client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=5,
messages=[{"role": "user", "content": "test"}],
)
# Key is valid, return known models
return {
'claude_4_models': ['claude-opus-4-6', 'claude-sonnet-4-5-20250929', 'claude-haiku-4-5-20251001'],
'all_models': ['claude-opus-4-6', 'claude-sonnet-4-5-20250929', 'claude-haiku-4-5-20251001']
}
except ImportError:
print("⚠️ anthropic package not installed")
return {
'claude_4_models': ['claude-opus-4-6', 'claude-sonnet-4-5-20250929', 'claude-haiku-4-5-20251001'],
'all_models': ['claude-opus-4-6', 'claude-sonnet-4-5-20250929', 'claude-haiku-4-5-20251001']
}
except Exception as e:
print(f"⚠️ Could not validate Anthropic API key: {e}")
# Fallback to known models
return {
'claude_4_models': ['claude-opus-4-6', 'claude-sonnet-4-5-20250929', 'claude-haiku-4-5-20251001'],
'all_models': ['claude-opus-4-6', 'claude-sonnet-4-5-20250929', 'claude-haiku-4-5-20251001']
}
class AetherSetup:
"""Main setup class for Aether installation and configuration."""
def __init__(self, interactive: bool = True, reconfigure_all: bool = False, reconfigure_keys: bool = False, reconfigure_models: bool = False):
self.console = Console()
self.interactive = interactive
self.reconfigure_all = reconfigure_all
self.reconfigure_keys = reconfigure_keys
self.reconfigure_models = reconfigure_models
self.project_root = PROJECT_ROOT
self.config_dir = Path.home() / '.aether'
self.config_file = self.config_dir / 'config.yaml'
self.setup_status = {
'python_version': False,
'foundry': False,
'venv': False,
'dependencies': False,
'api_keys': False,
'config': False,
'verification': False
}
self.api_keys = {}
self.existing_config = None
# Load existing configuration if available
self._load_existing_config()
def _load_existing_config(self):
"""Load existing configuration if available."""
try:
if self.config_file.exists():
from core.config_manager import ConfigManager
config_manager = ConfigManager()
self.existing_config = config_manager.config
# Debug output
print(f"DEBUG: Loaded existing config from {self.config_file}")
print(f"DEBUG: Config has OpenAI key: {bool(getattr(self.existing_config, 'openai_api_key', ''))}")
print(f"DEBUG: Config has Gemini key: {bool(getattr(self.existing_config, 'gemini_api_key', ''))}")
# Mark what's already configured
if getattr(self.existing_config, 'openai_api_key', ''):
self.setup_status['api_keys'] = True
if self.config_file.exists():
self.setup_status['config'] = True
else:
print(f"DEBUG: Config file does not exist: {self.config_file}")
self.existing_config = None
except Exception as e:
# If config is corrupted, we'll reconfigure
print(f"DEBUG: Error loading config: {e}")
self.existing_config = None
def _show_existing_config(self):
"""Show existing configuration summary."""
if not self.existing_config:
return
self.console.print("\n[bold cyan]Existing Configuration Detected[/bold cyan]")
# Show API keys (masked)
table = Table(title="Current Settings")
table.add_column("Setting", style="cyan")
table.add_column("Status", style="green")
table.add_column("Value")
openai_key = getattr(self.existing_config, 'openai_api_key', '')
if openai_key:
table.add_row("OpenAI API Key", "✓ Configured", f"{openai_key[:10]}...")
else:
table.add_row("OpenAI API Key", "✗ Not set", "-")
gemini_key = getattr(self.existing_config, 'gemini_api_key', '')
if gemini_key:
table.add_row("Gemini API Key", "✓ Configured", f"{gemini_key[:10]}...")
else:
table.add_row("Gemini API Key", "✗ Not set", "-")
anthropic_key = getattr(self.existing_config, 'anthropic_api_key', '')
if anthropic_key:
table.add_row("Anthropic API Key", "✓ Configured", f"{anthropic_key[:10]}...")
else:
table.add_row("Anthropic API Key", "✗ Not set", "-")
etherscan_key = getattr(self.existing_config, 'etherscan_api_key', '')
if etherscan_key:
table.add_row("Etherscan API Key", "✓ Configured", f"{etherscan_key[:10]}...")
else:
table.add_row("Etherscan API Key", "✗ Not set", "-")
# Show active model selections (based on provider)
validation_provider = getattr(self.existing_config, 'validation_provider', 'openai')
analysis_provider = getattr(self.existing_config, 'analysis_provider', 'openai')
generation_provider = getattr(self.existing_config, 'generation_provider', 'openai')
# Get the active model for each task
if validation_provider == 'anthropic':
validation_model = getattr(self.existing_config, 'anthropic_validation_model', 'claude-sonnet-4-5-20250929')
elif validation_provider == 'openai':
validation_model = getattr(self.existing_config, 'openai_validation_model', 'gpt-5-chat-latest')
else:
validation_model = getattr(self.existing_config, 'gemini_validation_model', 'gemini-2.5-flash')
if analysis_provider == 'anthropic':
analysis_model = getattr(self.existing_config, 'anthropic_analysis_model', 'claude-opus-4-6')
elif analysis_provider == 'openai':
analysis_model = getattr(self.existing_config, 'openai_analysis_model', 'gpt-5-chat-latest')
else:
analysis_model = getattr(self.existing_config, 'gemini_analysis_model', 'gemini-2.5-flash')
if generation_provider == 'anthropic':
generation_model = getattr(self.existing_config, 'anthropic_generation_model', 'claude-sonnet-4-5-20250929')
elif generation_provider == 'openai':
generation_model = getattr(self.existing_config, 'openai_generation_model', 'gpt-5-mini')
else:
generation_model = getattr(self.existing_config, 'gemini_generation_model', 'gemini-2.5-flash')
# Display active models
table.add_row("Active Validation Model", "✓ Set", f"{validation_provider} / {validation_model}")
table.add_row("Active Analysis Model", "✓ Set", f"{analysis_provider} / {analysis_model}")
table.add_row("Active Generation Model", "✓ Set", f"{generation_provider} / {generation_model}")
self.console.print(table)
def _show_reconfiguration_menu(self) -> bool:
"""Show interactive menu for reconfiguration options."""
while True:
self.console.print("\n[bold]What would you like to do?[/bold]")
self.console.print(" [cyan]1[/cyan] - Reconfigure API Keys")
self.console.print(" [cyan]2[/cyan] - Reconfigure Model Selections")
self.console.print(" [cyan]3[/cyan] - Full Reconfiguration (everything)")
self.console.print(" [cyan]4[/cyan] - Verify Installation")
self.console.print(" [cyan]5[/cyan] - View Configuration Again")
self.console.print(" [cyan]0[/cyan] - Exit (configuration is already complete)")
choice = Prompt.ask(
"\nSelect option",
choices=["0", "1", "2", "3", "4", "5"],
default="0"
)
if choice == "0":
self.console.print("\n[green]✓ Setup complete. Your configuration is ready![/green]")
return True
elif choice == "1":
# Reconfigure API keys only
self.console.print("\n[bold]Reconfiguring API Keys...[/bold]")
self.reconfigure_keys = True
if not self.configure_api_keys():
return False
if not self.create_configuration():
return False
self.console.print("[green]✓ API keys updated successfully![/green]")
# Reload config to show updated values
self._load_existing_config()
self._show_existing_config()
elif choice == "2":
# Show model selection submenu
if not self._show_model_selection_menu():
return False
# Reload config to show updated values
self._load_existing_config()
self._show_existing_config()
elif choice == "3":
# Full reconfiguration
self.console.print("\n[bold yellow]Full Reconfiguration[/bold yellow]")
if Confirm.ask("This will reconfigure everything. Continue?", default=False):
self.reconfigure_all = True
# Start fresh setup process
return self.run()
else:
self.console.print("[yellow]Cancelled full reconfiguration.[/yellow]")
elif choice == "4":
# Verify installation
self.console.print("\n[bold]Verifying Installation...[/bold]")
if self.verify_installation():
self.console.print("[green]✓ All checks passed![/green]")
else:
self.console.print("[yellow]Some verification checks failed. See above.[/yellow]")
elif choice == "5":
# View configuration again
self._show_existing_config()
# Loop back to menu
def _show_model_selection_menu(self) -> bool:
"""Show submenu for model selection with current assignments."""
# Load existing keys
if not self.api_keys:
existing_openai = getattr(self.existing_config, 'openai_api_key', '')
existing_gemini = getattr(self.existing_config, 'gemini_api_key', '')
existing_anthropic = getattr(self.existing_config, 'anthropic_api_key', '')
if existing_openai:
self.api_keys['OPENAI_API_KEY'] = existing_openai
if existing_gemini:
self.api_keys['GEMINI_API_KEY'] = existing_gemini
if existing_anthropic:
self.api_keys['ANTHROPIC_API_KEY'] = existing_anthropic
while True:
self.console.print("\n[bold]Model Selection Manager[/bold]")
self.console.print("\n[bold cyan]Current Model Assignments:[/bold cyan]")
# Show current assignments
tasks = ['validation', 'analysis', 'generation']
for task in tasks:
provider = getattr(self.existing_config, f'{task}_provider', 'openai')
if provider == 'anthropic':
model = getattr(self.existing_config, f'anthropic_{task}_model', 'N/A')
elif provider == 'openai':
model = getattr(self.existing_config, f'openai_{task}_model', 'N/A')
else:
model = getattr(self.existing_config, f'gemini_{task}_model', 'N/A')
self.console.print(f" {task.title()}: [cyan]{provider}[/cyan] / [yellow]{model}[/yellow]")
self.console.print("\n[bold]Select task to reconfigure:[/bold]")
self.console.print(" [cyan]1[/cyan] - Validation Model")
self.console.print(" [cyan]2[/cyan] - Analysis Model")
self.console.print(" [cyan]3[/cyan] - Generation Model")
self.console.print(" [cyan]4[/cyan] - AI Ensemble Agents (4 specialist agents)")
self.console.print(" [cyan]5[/cyan] - Reconfigure All Models")
self.console.print(" [cyan]0[/cyan] - Back to Main Menu")
choice = Prompt.ask(
"\nSelect option",
choices=["0", "1", "2", "3", "4", "5"],
default="0"
)
if choice == "0":
return True
elif choice == "1" or choice == "2" or choice == "3":
# Reconfigure specific task
task_map = {"1": "validation", "2": "analysis", "3": "generation"}
task_name = task_map[choice]
if not self._configure_single_task_model(task_name):
return False
if not self.create_configuration():
return False
self.console.print(f"[green]✓ {task_name.title()} model updated![/green]")
self._load_existing_config()
elif choice == "4":
# Configure AI Ensemble agents
if not self._configure_ensemble_agents():
return False
if not self.create_configuration():
return False
self.console.print("[green]✓ Ensemble agent models updated![/green]")
self._load_existing_config()
elif choice == "5":
# Reconfigure all models
self.reconfigure_models = True
if not self._configure_model_selection():
return False
if not self.create_configuration():
return False
self.console.print("[green]✓ All models updated![/green]")
self._load_existing_config()
def _configure_single_task_model(self, task_name: str) -> bool:
"""Configure model for a single task type.
Args:
task_name: One of 'validation', 'analysis', or 'generation'
"""
has_openai = self.api_keys.get('OPENAI_API_KEY')
has_gemini = self.api_keys.get('GEMINI_API_KEY')
has_anthropic = self.api_keys.get('ANTHROPIC_API_KEY')
if not has_openai and not has_gemini and not has_anthropic:
self.console.print("[red]No API keys configured![/red]")
return False
# Fetch available models
available_openai = None
available_gemini = None
available_anthropic = None
if has_openai:
with self.console.status("[bold green]Fetching OpenAI models..."):
available_openai = fetch_available_models(self.api_keys['OPENAI_API_KEY'])
if has_gemini:
with self.console.status("[bold green]Fetching Gemini models..."):
available_gemini = fetch_available_gemini_models(self.api_keys['GEMINI_API_KEY'])
if has_anthropic:
with self.console.status("[bold green]Fetching Anthropic models..."):
available_anthropic = fetch_available_anthropic_models(self.api_keys['ANTHROPIC_API_KEY'])
task_desc = {
'validation': 'false positive filtering - needs critical accuracy',
'analysis': 'vulnerability detection - balanced quality',
'generation': 'PoC/test generation - can use faster model'
}
self.console.print(f"\n[bold]Configure {task_name.title()} Model[/bold] (for {task_desc[task_name]})")
# Choose provider
provider = None
provider_choices = []
if has_openai:
provider_choices.append("openai")
if has_gemini:
provider_choices.append("gemini")
if has_anthropic:
provider_choices.append("anthropic")
if len(provider_choices) > 1:
current_provider = getattr(self.existing_config, f'{task_name}_provider', 'openai')
self.console.print(f"\n Current: [cyan]{current_provider}[/cyan]")
provider = select_with_arrows(
"Select provider (use arrow keys, Enter to confirm)",
provider_choices,
default=current_provider if current_provider in provider_choices else provider_choices[0]
)
self.api_keys[f'{task_name.upper()}_PROVIDER'] = provider
elif len(provider_choices) == 1:
provider = provider_choices[0]
self.api_keys[f'{task_name.upper()}_PROVIDER'] = provider
# Select model from chosen provider
if provider == "openai" and available_openai:
choice_list = available_openai['gpt5_models'][:10] + available_openai['gpt4_models'][:5]
current_model = getattr(self.existing_config, f'openai_{task_name}_model', choice_list[0])
self.console.print(f"\n Current: [yellow]{current_model}[/yellow]")
model = select_with_arrows(
"Select OpenAI model (use arrow keys, Enter to confirm)",
choice_list,
default=current_model if current_model in choice_list else choice_list[0]
)
self.api_keys[f'{task_name.upper()}_MODEL'] = model
elif provider == "gemini" and available_gemini:
choice_list = available_gemini['gemini_2_5_models'] + available_gemini['gemini_1_5_models']
current_model = getattr(self.existing_config, f'gemini_{task_name}_model', choice_list[0])
self.console.print(f"\n Current: [yellow]{current_model}[/yellow]")
model = select_with_arrows(
"Select Gemini model (use arrow keys, Enter to confirm)",
choice_list,
default=current_model if current_model in choice_list else choice_list[0]
)
self.api_keys[f'GEMINI_{task_name.upper()}_MODEL'] = model
elif provider == "anthropic" and available_anthropic:
choice_list = available_anthropic['claude_4_models']
current_model = getattr(self.existing_config, f'anthropic_{task_name}_model', choice_list[0])
self.console.print(f"\n Current: [yellow]{current_model}[/yellow]")
model = select_with_arrows(
"Select Anthropic model (use arrow keys, Enter to confirm)",
choice_list,
default=current_model if current_model in choice_list else choice_list[0]
)
self.api_keys[f'ANTHROPIC_{task_name.upper()}_MODEL'] = model
return True
def _configure_ensemble_agents(self) -> bool:
"""Configure models for each AI ensemble agent."""
self.console.print("\n[bold]AI Ensemble Agent Configuration[/bold]")
self.console.print("Configure models for each specialist agent\n")
# Show current agent assignments
agents = [
('gpt5_security', 'GPT-5 Security Auditor', 'Security vulnerabilities (access control, reentrancy, etc.)'),
('gpt5_defi', 'GPT-5 DeFi Specialist', 'DeFi protocols (AMM, lending, oracle manipulation)'),
('gemini_security', 'Gemini Security Hunter', 'Security patterns (external calls, delegatecall, etc.)'),
('gemini_verification', 'Gemini Formal Verifier', 'Formal verification (arithmetic, overflow, precision)'),
('anthropic_security', 'Anthropic Security Auditor', 'Deep security analysis (Claude Opus 4.6)'),
('anthropic_reasoning', 'Anthropic Reasoning Specialist', 'Extended reasoning for complex vulnerabilities'),
]
self.console.print("[bold cyan]Current Agent Models:[/bold cyan]")
for agent_key, agent_name, agent_focus in agents:
current_model = getattr(self.existing_config, f'agent_{agent_key}_model', 'N/A')
self.console.print(f" {agent_name}: [yellow]{current_model}[/yellow]")
self.console.print(f" Focus: [dim]{agent_focus}[/dim]")
# Fetch available models
has_openai = self.api_keys.get('OPENAI_API_KEY')
has_gemini = self.api_keys.get('GEMINI_API_KEY')
has_anthropic = self.api_keys.get('ANTHROPIC_API_KEY')
available_openai = None
available_gemini = None
available_anthropic = None
if has_openai:
with self.console.status("[bold green]Fetching OpenAI models..."):
available_openai = fetch_available_models(self.api_keys['OPENAI_API_KEY'])
if has_gemini:
with self.console.status("[bold green]Fetching Gemini models..."):
available_gemini = fetch_available_gemini_models(self.api_keys['GEMINI_API_KEY'])
if has_anthropic:
with self.console.status("[bold green]Fetching Anthropic models..."):
available_anthropic = fetch_available_anthropic_models(self.api_keys['ANTHROPIC_API_KEY'])
# Configure each agent
for agent_key, agent_name, agent_focus in agents:
self.console.print(f"\n[bold]{agent_name}[/bold]")
self.console.print(f"[dim]Focus: {agent_focus}[/dim]")
current_model = getattr(self.existing_config, f'agent_{agent_key}_model', 'N/A')
self.console.print(f"Current: [yellow]{current_model}[/yellow]")
# Determine which provider this agent uses
is_gemini_agent = 'gemini' in agent_key
is_anthropic_agent = 'anthropic' in agent_key
if is_anthropic_agent and available_anthropic:
# Anthropic agent - select from Claude models
choice_list = available_anthropic['claude_4_models']
model = select_with_arrows(
f"Select Anthropic model for {agent_name} (use arrow keys, Enter to confirm)",
choice_list,
default=current_model if current_model in choice_list else choice_list[0]
)
elif is_gemini_agent and available_gemini:
# Gemini agent - select from Gemini models
choice_list = available_gemini['gemini_2_5_models'] + available_gemini['gemini_1_5_models']
model = select_with_arrows(
f"Select Gemini model for {agent_name} (use arrow keys, Enter to confirm)",
choice_list,
default=current_model if current_model in choice_list else choice_list[0]
)
elif not is_gemini_agent and not is_anthropic_agent and available_openai:
# GPT-5 agent - select from OpenAI models
choice_list = available_openai['gpt5_models'][:10] + available_openai['gpt4_models'][:5]
model = select_with_arrows(
f"Select OpenAI model for {agent_name} (use arrow keys, Enter to confirm)",
choice_list,
default=current_model if current_model in choice_list else choice_list[0]
)
else:
self.console.print(f"[yellow]Skipping - no API key for this agent[/yellow]")
continue
# Store the agent model selection
self.api_keys[f'AGENT_{agent_key.upper()}_MODEL'] = model
return True
def run(self):
"""Run the complete setup process."""
print(f"DEBUG: existing_config = {self.existing_config is not None}")
print(f"DEBUG: reconfigure_all = {self.reconfigure_all}")
# Check if this is a fresh install or reconfiguration
is_fresh_install = not self.existing_config or self.reconfigure_all
if is_fresh_install:
# Fresh install - show full welcome and proceed
self.print_welcome()
else:
# Existing config - show menu-driven reconfiguration options
self.console.print("\n[bold cyan]Aether Setup - Configuration Manager[/bold cyan]")
self.console.print("Configuration already exists. Checking current settings...\n")
print("DEBUG: Showing existing config...")
self._show_existing_config()
if self.interactive:
# Show menu
return self._show_reconfiguration_menu()
else:
# Non-interactive with existing config - just exit
self.console.print("[green]✓ Configuration already exists[/green]")
return True
# Step 1: Check Python version
if not self.check_python_version():
return False
# Step 2: Detect and install dependencies
if not self.setup_dependencies():
return False
# Step 3: Setup virtual environment and install Python packages
if not self.setup_python_environment():
return False
# Step 4: Configure API keys
if not self.configure_api_keys():
return False
# Step 5: Create configuration file
if not self.create_configuration():
return False
# Step 6: Verify installation
if not self.verify_installation():
return False
# Step 7: Print next steps
self.print_next_steps()
return True
def print_welcome(self):
"""Print welcome message."""
welcome_text = """
[bold cyan]Aether - Smart Contract Security Analysis Framework[/bold cyan]
Welcome to the Aether installer! This script will guide you through
the installation and configuration process.
What this installer will do:
✓ Check system requirements (Python 3.11+)
✓ Install Foundry (forge/anvil) if needed
✓ Set up Python virtual environment
✓ Install Python dependencies
✓ Configure API keys (OpenAI, Gemini, Anthropic, Etherscan)
✓ Create configuration files
✓ Verify everything works
Let's get started!
"""
self.console.print(Panel(welcome_text, border_style="cyan"))
if self.interactive:
if not Confirm.ask("\nContinue with installation?", default=True):
self.console.print("[yellow]Setup cancelled.[/yellow]")
sys.exit(0)
def check_python_version(self) -> bool:
"""Check Python version meets requirements."""
self.console.print("\n[bold]Step 1: Checking Python version...[/bold]")
detector = DependencyDetector()
is_valid, version = detector.check_python_version()
if is_valid:
self.console.print(f" ✓ Python {version} [green](OK)[/green]")
self.setup_status['python_version'] = True
return True
else:
self.console.print(f" ✗ Python {version} [red](Python 3.11+ required)[/red]")
self.console.print("\n[red]Please upgrade Python to version 3.11 or higher.[/red]")
self.console.print("Visit: https://www.python.org/downloads/")
return False
def setup_dependencies(self) -> bool:
"""Detect and install system dependencies."""
self.console.print("\n[bold]Step 2: Checking system dependencies...[/bold]")
detector = DependencyDetector()
tools = detector.detect_all_tools()
# Display detection results
table = Table(title="Dependency Status")
table.add_column("Tool", style="cyan")
table.add_column("Status", style="green")
table.add_column("Version")
table.add_column("Required")
for tool_name, tool_info in tools.items():
status = "✓ Installed" if tool_info['installed'] else "✗ Missing"
status_style = "green" if tool_info['installed'] else "red"
version = tool_info.get('version', 'N/A')
required = "Yes" if tool_info.get('required') else "No"
# Check for version warnings
if tool_info.get('installed') and not tool_info.get('version_ok', True):
status = "⚠ Old Version"
status_style = "yellow"
table.add_row(
tool_name,
f"[{status_style}]{status}[/{status_style}]",
version,
required
)
self.console.print(table)
# Check if Foundry is installed
if not tools['forge']['installed']:
self.console.print("\n[yellow]Foundry (forge/anvil) is required for PoC generation and testing.[/yellow]")
# Check for Node.js version warnings
if tools.get('node', {}).get('installed') and not tools.get('node', {}).get('version_ok', True):
warning = tools['node'].get('warning', 'Node.js version too old')
self.console.print(f"\n[yellow]⚠️ {warning}[/yellow]")
if tools['node'].get('install_instructions'):
self.console.print(f" {tools['node']['install_instructions']}")
if self.interactive:
install_foundry = Confirm.ask("Install Foundry now?", default=True)
if install_foundry:
with self.console.status("[bold green]Installing Foundry..."):
success, message = FoundryInstaller.install_foundry()
if success:
self.console.print(f" ✓ {message}")
self.console.print("\n[yellow]Important:[/yellow] You may need to add Foundry to your PATH:")
self.console.print(FoundryInstaller.add_to_path_instructions())
if Confirm.ask("\nHave you added Foundry to your PATH?", default=False):
# Re-check
tools = detector.detect_all_tools()
if tools['forge']['installed']:
self.console.print(" ✓ Foundry is now available")
self.setup_status['foundry'] = True
else:
self.console.print(" [yellow]Foundry still not found. You may need to restart your terminal.[/yellow]")
else:
self.console.print(" [yellow]Please add Foundry to PATH and re-run setup.[/yellow]")
return False
else:
self.console.print(f" ✗ {message}")
self.console.print("\n[yellow]Manual installation required.[/yellow]")
self.console.print("Visit: https://book.getfoundry.sh/getting-started/installation")
if not Confirm.ask("Continue without Foundry?", default=False):
return False
else:
self.console.print("[yellow]Note: Some features will be unavailable without Foundry.[/yellow]")
else:
self.console.print("[yellow]Non-interactive mode: Please install Foundry manually.[/yellow]")
return False
else:
self.setup_status['foundry'] = True
return True
def setup_python_environment(self) -> bool:
"""Setup Python virtual environment and install dependencies."""
self.console.print("\n[bold]Step 3: Setting up Python environment...[/bold]")
venv_helper = VirtualEnvHelper()
# Check if already in a venv
if venv_helper.is_in_virtualenv():
self.console.print(" ✓ Already in a virtual environment")
self.setup_status['venv'] = True
else:
# Look for existing venv
existing_venv = venv_helper.find_venv_in_project(self.project_root)
if existing_venv:
self.console.print(f" ✓ Found existing virtual environment: {existing_venv}")
self.console.print(f"\n[yellow]Please activate it with:[/yellow]")
self.console.print(f" {venv_helper.get_activation_command(existing_venv)}")
self.console.print("\nThen re-run this setup script.")
return False
else:
if self.interactive and Confirm.ask("Create virtual environment?", default=True):
success, message = venv_helper.create_virtualenv(self.project_root)
if success:
venv_path = Path(message)
self.console.print(f" ✓ Virtual environment created: {venv_path}")
self.console.print(f"\n[yellow]Please activate it with:[/yellow]")
self.console.print(f" {venv_helper.get_activation_command(venv_path)}")
self.console.print("\nThen re-run this setup script.")
return False
else:
self.console.print(f" ✗ {message}")
return False
else:
self.console.print("[yellow]Virtual environment recommended but skipped.[/yellow]")
# Install Python dependencies
requirements_file = self.project_root / 'requirements.txt'
if requirements_file.exists():
self.console.print("\nInstalling Python dependencies...")
if self.interactive and not Confirm.ask("Install from requirements.txt?", default=True):
self.console.print("[yellow]Skipping dependency installation.[/yellow]")
return True
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=self.console
) as progress:
task = progress.add_task("Installing packages...", total=None)
success, message = venv_helper.install_requirements(requirements_file)
if success:
self.console.print(f" ✓ {message}")
self.setup_status['dependencies'] = True
else:
self.console.print(f" ✗ {message}")
if not self.interactive or not Confirm.ask("Continue anyway?", default=False):
return False
else:
self.console.print(f" [yellow]requirements.txt not found[/yellow]")
return True
def configure_api_keys(self) -> bool:
"""Configure API keys interactively."""
self.console.print("\n[bold]Step 4: Configuring API keys...[/bold]")
# Skip if already configured and not reconfiguring
if self.existing_config and not self.reconfigure_all and not self.reconfigure_keys:
existing_openai = getattr(self.existing_config, 'openai_api_key', '')
existing_gemini = getattr(self.existing_config, 'gemini_api_key', '')
existing_anthropic = getattr(self.existing_config, 'anthropic_api_key', '')
if existing_openai or existing_gemini or existing_anthropic:
self.console.print(" [green]✓ API keys already configured[/green]")
if self.interactive and not Confirm.ask(" Reconfigure API keys?", default=False):
# Use existing keys
if existing_openai:
self.api_keys['OPENAI_API_KEY'] = existing_openai
if existing_gemini:
self.api_keys['GEMINI_API_KEY'] = existing_gemini
if existing_anthropic:
self.api_keys['ANTHROPIC_API_KEY'] = existing_anthropic
if getattr(self.existing_config, 'etherscan_api_key', ''):
self.api_keys['ETHERSCAN_API_KEY'] = self.existing_config.etherscan_api_key
self.setup_status['api_keys'] = True
# Skip to model selection if not reconfiguring models
if not self.reconfigure_models:
return self._configure_model_selection()
return True
self.console.print("\n[cyan]API keys enable LLM-powered vulnerability analysis.[/cyan]")
self.console.print("You can configure them now or later via environment variables.\n")
validator = APIKeyValidator()
# OpenAI API Key
self.console.print("[bold]OpenAI API Key[/bold] (for GPT models)")
# Check existing config first, then env var
existing_openai = getattr(self.existing_config, 'openai_api_key', '') if self.existing_config else ''
openai_key = existing_openai or os.getenv('OPENAI_API_KEY', '')
if openai_key:
self.console.print(f" Found existing key: {openai_key[:10]}...")
if self.interactive and not Confirm.ask("Use this key?", default=True):
openai_key = ''
if not openai_key and self.interactive:
openai_key = Prompt.ask(" Enter OpenAI API key (or press Enter to skip)", default="")
if openai_key:
with self.console.status("[bold green]Validating OpenAI key..."):
is_valid, message = validator.validate_openai_key(openai_key)
if is_valid:
self.console.print(f" ✓ OpenAI key validated: {message}")
self.api_keys['OPENAI_API_KEY'] = openai_key
else:
self.console.print(f" ✗ OpenAI key validation failed: {message}")
if self.interactive and Confirm.ask(" Use anyway?", default=False):
self.api_keys['OPENAI_API_KEY'] = openai_key
else:
self.console.print(" [yellow]Skipped OpenAI key configuration[/yellow]")
# Gemini API Key
self.console.print("\n[bold]Gemini API Key[/bold] (for Gemini models)")
existing_gemini = getattr(self.existing_config, 'gemini_api_key', '') if self.existing_config else ''
gemini_key = existing_gemini or os.getenv('GEMINI_API_KEY', '')
if gemini_key:
self.console.print(f" Found existing key: {gemini_key[:10]}...")
if self.interactive and not Confirm.ask("Use this key?", default=True):
gemini_key = ''
if not gemini_key and self.interactive:
gemini_key = Prompt.ask(" Enter Gemini API key (or press Enter to skip)", default="")
if gemini_key:
with self.console.status("[bold green]Validating Gemini key..."):
is_valid, message = validator.validate_gemini_key(gemini_key)
if is_valid:
self.console.print(f" ✓ Gemini key validated: {message}")