Implement secure configuration management with encryption and validation (Phase 1 improvements)

This commit is contained in:
truetype committed 2026-03-19 20:29:30 +01:00
commit d4d6aafea9
12 files changed
+1656

No files matched your search

+489
View File
@@ -0,0 +1,489 @@
"""
Text User Interface for MaxClaw Agent configuration management
"""
import curses
import sys
from typing import Optional, List, Dict, Any
from config_manager import config_manager
def draw_menu(stdscr, selected_row_idx, options):
"""Draw a menu with options"""
stdscr.clear()
h, w = stdscr.getmaxyx()
# Title
title = "MaxClaw Agent Configuration TUI"
stdscr.attron(curses.A_BOLD)
stdscr.addstr(0, (w - len(title)) // 2, title)
stdscr.attroff(curses.A_BOLD)
# Instructions
instructions = "Use arrow keys to navigate, Enter to select, 'q' to quit"
stdscr.addstr(2, (w - len(instructions)) // 2, instructions)
# Menu options
for idx, option in enumerate(options):
x = w // 2 - len(option) // 2
y = h // 2 - len(options) // 2 + idx
if idx == selected_row_idx:
stdscr.attron(curses.A_REVERSE)
stdscr.addstr(y, x, option)
stdscr.attroff(curses.A_REVERSE)
else:
stdscr.addstr(y, x, option)
stdscr.refresh()
def get_string_input(stdscr, prompt, y, x, max_length=50):
"""Get string input from user"""
curses.echo()
stdscr.addstr(y, x, prompt)
stdscr.refresh()
input_win = curses.newwin(1, max_length, y, x + len(prompt))
input_win.box()
input_win.refresh()
curses.curs_set(1)
result = input_win.getstr(0, 0, max_length).decode('utf-8')
curses.curs_set(0)
curses.noecho()
return result
def get_password_input(stdscr, prompt, y, x, max_length=50):
"""Get password input from user (hidden)"""
curses.noecho()
stdscr.addstr(y, x, prompt)
stdscr.refresh()
input_win = curses.newwin(1, max_length, y, x + len(prompt))
input_win.box()
input_win.refresh()
curses.curs_set(1)
result = ""
while True:
ch = input_win.getch()
if ch in [curses.KEY_ENTER, 10, 13]: # Enter key
break
elif ch in [curses.KEY_BACKSPACE, 127, 8]: # Backspace
if len(result) > 0:
result = result[:-1]
input_win.addstr(0, len(result), " ")
input_win.move(0, len(result))
elif ch >= 32 and ch <= 126 and len(result) < max_length: # Printable characters
result += chr(ch)
input_win.addstr(0, len(result)-1, "*")
input_win.refresh()
curses.curs_set(0)
return result
def show_message(stdscr, message, wait_for_key=True):
"""Show a message to the user"""
h, w = stdscr.getmaxyx()
stdscr.addstr(h//2, (w - len(message)) // 2, message)
stdscr.refresh()
if wait_for_key:
stdscr.getch()
def main_tui(stdscr):
"""Main TUI function"""
# Initialize colors if available
if curses.has_colors():
curses.start_color()
curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE)
# Main menu options
main_options = [
"View Configurations",
"Add Configuration",
"Remove Configuration",
"Set Active Configuration",
"View Active Configuration",
"Exit"
]
current_selection = 0
while True:
draw_menu(stdscr, current_selection, main_options)
key = stdscr.getch()
if key == curses.KEY_UP and current_selection > 0:
current_selection -= 1
elif key == curses.KEY_DOWN and current_selection < len(main_options) - 1:
current_selection += 1
elif key == curses.KEY_ENTER or key in [10, 13]:
# Enter key pressed
if current_selection == 0: # View Configurations
view_configurations_tui(stdscr)
elif current_selection == 1: # Add Configuration
add_configuration_tui(stdscr)
elif current_selection == 2: # Remove Configuration
remove_configuration_tui(stdscr)
elif current_selection == 3: # Set Active Configuration
set_active_configuration_tui(stdscr)
elif current_selection == 4: # View Active Configuration
view_active_configuration_tui(stdscr)
elif current_selection == 5: # Exit
break
elif key == ord('q') or key == ord('Q'):
break
def view_configurations_tui(stdscr):
"""View all configurations in TUI"""
configs = config_manager.get_configurations()
active = config_manager.get_active_configuration()
stdscr.clear()
h, w = stdscr.getmaxyx()
# Title
title = "Configurations"
stdscr.attron(curses.A_BOLD)
stdscr.addstr(0, (w - len(title)) // 2, title)
stdscr.attroff(curses.A_BOLD)
if not configs:
stdscr.addstr(2, 0, "No configurations found.")
else:
# Header
header = f"{'Name':<20} {'Provider':<12} {'Model':<20} {'Active'}"
stdscr.addstr(2, 0, header)
stdscr.addstr(3, 0, "-" * len(header))
# Configurations
for idx, (name, config) in enumerate(configs.items()):
y = 4 + idx
if y >= h - 2: # Prevent overflow
break
marker = "*" if name == active else " "
provider = config.get("provider", "unknown")[:11]
model = config.get("model_name", "unknown")[:19]
line = f"{marker} {name:<19} {provider:<12} {model:<20}"
stdscr.addstr(y, 0, line)
# Instructions
instructions = "Press any key to return to main menu"
stdscr.addstr(h-2, (w - len(instructions)) // 2, instructions)
stdscr.refresh()
stdscr.getch()
def add_configuration_tui(stdscr):
"""Add a new configuration via TUI"""
stdscr.clear()
h, w = stdscr.getmaxyx()
# Title
title = "Add New Configuration"
stdscr.attron(curses.A_BOLD)
stdscr.addstr(0, (w - len(title)) // 2, title)
stdscr.attroff(curses.A_BOLD)
# Get configuration name
name = get_string_input(stdscr, "Configuration name: ", 2, 0)
if not name:
show_message(stdscr, "Error: Configuration name cannot be empty")
return
if config_manager.configuration_exists(name):
overwrite_prompt = f"Configuration '{name}' already exists. Overwrite? (y/n): "
overwrite = get_string_input(stdscr, overwrite_prompt, 3, 0, 1)
if overwrite.lower() != 'y':
show_message(stdscr, "Cancelled.")
return
# Select provider
providers = ["OpenAI", "Ollama", "OpenRouter"]
provider_selection = 0
while True:
stdscr.clear()
stdscr.attron(curses.A_BOLD)
stdscr.addstr(0, (w - len(title)) // 2, title)
stdscr.attroff(curses.A_BOLD)
stdscr.addstr(2, 0, f"Name: {name}")
stdscr.addstr(3, 0, "Select provider:")
for idx, provider in enumerate(providers):
y = 4 + idx
if idx == provider_selection:
stdscr.attron(curses.A_REVERSE)
stdscr.addstr(y, 2, f"> {provider}")
stdscr.attroff(curses.A_REVERSE)
else:
stdscr.addstr(y, 2, f" {provider}")
stdscr.addstr(h-2, 0, "Use arrow keys to select, Enter to confirm")
stdscr.refresh()
key = stdscr.getch()
if key == curses.KEY_UP and provider_selection > 0:
provider_selection -= 1
elif key == curses.KEY_DOWN and provider_selection < len(providers) - 1:
provider_selection += 1
elif key == curses.KEY_ENTER or key in [10, 13]:
break
provider_map = {"OpenAI": "openai", "Ollama": "ollama", "OpenRouter": "openrouter"}
provider = provider_map[providers[provider_selection]]
config = {"provider": provider}
# Get model name
model_name = get_string_input(stdscr, f"Model name for {provider} (e.g., gpt-4o, gemma2:9b): ", 5, 0)
if not model_name:
show_message(stdscr, "Error: Model name cannot be empty")
return
config["model_name"] = model_name
# Get provider-specific settings
y_pos = 6
if provider == "openai":
api_key = get_password_input(stdscr, "OpenAI API key: ", y_pos, 0)
config["api_key"] = api_key
y_pos += 1
elif provider == "ollama":
base_url = get_string_input(stdscr, f"Ollama base URL (default: http://localhost:11434): ", y_pos, 0)
if not base_url:
base_url = "http://localhost:11434"
config["base_url"] = base_url
y_pos += 1
elif provider == "openrouter":
api_key = get_password_input(stdscr, "OpenRouter API key: ", y_pos, 0)
config["api_key"] = api_key
y_pos += 1
base_url = get_string_input(stdscr, f"OpenRouter base URL (default: https://openrouter.ai/api/v1): ", y_pos, 0)
if not base_url:
base_url = "https://openrouter.ai/api/v1"
config["base_url"] = base_url
y_pos += 1
# Ask if this should be the active configuration
set_active_prompt = "Set as active configuration? (y/n): "
set_active = get_string_input(stdscr, set_active_prompt, y_pos, 0, 1)
# Save the configuration
try:
config_manager.add_configuration(name, config)
message = f"Configuration '{name}' saved successfully."
if set_active.lower() == 'y':
config_manager.set_active_configuration(name)
message += f" Configuration '{name}' set as active."
show_message(stdscr, message)
except Exception as e:
show_message(stdscr, f"Error saving configuration: {e}")
def remove_configuration_tui(stdscr):
"""Remove a configuration via TUI"""
configs = config_manager.get_configurations()
if not configs:
show_message(stdscr, "No configurations to remove.")
return
stdscr.clear()
h, w = stdscr.getmaxyx()
# Title
title = "Remove Configuration"
stdscr.attron(curses.A_BOLD)
stdscr.addstr(0, (w - len(title)) // 2, title)
stdscr.attroff(curses.A_BOLD)
# List configurations
config_names = list(configs.keys())
selection = 0
while True:
stdscr.clear()
stdscr.attron(curses.A_BOLD)
stdscr.addstr(0, (w - len(title)) // 2, title)
stdscr.attroff(curses.A_BOLD)
stdscr.addstr(2, 0, "Select configuration to remove:")
for idx, name in enumerate(config_names):
y = 3 + idx
if y >= h - 4: # Prevent overflow
break
active_marker = " *"
if name == config_manager.get_active_configuration():
active_marker = " (active)"
if idx == selection:
stdscr.attron(curses.A_REVERSE)
stdscr.addstr(y, 2, f"> {name}{active_marker}")
stdscr.attroff(curses.A_REVERSE)
else:
stdscr.addstr(y, 2, f" {name}{active_marker}")
stdscr.addstr(h-2, 0, "Use arrow keys to select, Enter to confirm, 'c' to cancel")
stdscr.refresh()
key = stdscr.getch()
if key == curses.KEY_UP and selection > 0:
selection -= 1
elif key == curses.KEY_DOWN and selection < len(config_names) - 1:
selection += 1
elif key == curses.KEY_ENTER or key in [10, 13]:
# Confirm removal
name_to_remove = config_names[selection]
active = config_manager.get_active_configuration()
if name_to_remove == active:
confirm_prompt = f"'{name_to_remove}' is the active configuration. Remove it anyway? (y/n): "
confirm = get_string_input(stdscr, confirm_prompt, h-3, 0, 1)
if confirm.lower() != 'y':
show_message(stdscr, "Cancelled.")
return
confirm_prompt = f"Are you sure you want to remove configuration '{name_to_remove}'? (y/n): "
confirm = get_string_input(stdscr, confirm_prompt, h-3, 0, 1)
if confirm.lower() == 'y':
if config_manager.remove_configuration(name_to_remove):
show_message(stdscr, f"Configuration '{name_to_remove}' removed successfully.")
if name_to_remove == active:
show_message(stdscr, "Active configuration has been unset.")
else:
show_message(stdscr, f"Error: Failed to remove configuration '{name_to_remove}'.")
else:
show_message(stdscr, "Cancelled.")
return
elif key == ord('c') or key == ord('C'):
show_message(stdscr, "Cancelled.")
return
def set_active_configuration_tui(stdscr):
"""Set active configuration via TUI"""
configs = config_manager.get_configurations()
if not configs:
show_message(stdscr, "No configurations available.")
return
stdscr.clear()
h, w = stdscr.getmaxyx()
# Title
title = "Set Active Configuration"
stdscr.attron(curses.A_BOLD)
stdscr.addstr(0, (w - len(title)) // 2, title)
stdscr.attroff(curses.A_BOLD)
# List configurations
config_names = list(configs.keys())
selection = 0
while True:
stdscr.clear()
stdscr.attron(curses.A_BOLD)
stdscr.addstr(0, (w - len(title)) // 2, title)
stdscr.attroff(curses.A_BOLD)
stdscr.addstr(2, 0, "Select configuration to set as active:")
for idx, name in enumerate(config_names):
y = 3 + idx
if y >= h - 4: # Prevent overflow
break
current_active = config_manager.get_active_configuration()
active_marker = " (current)" if name == current_active else ""
if idx == selection:
stdscr.attron(curses.A_REVERSE)
stdscr.addstr(y, 2, f"> {name}{active_marker}")
stdscr.attroff(curses.A_REVERSE)
else:
stdscr.addstr(y, 2, f" {name}{active_marker}")
stdscr.addstr(h-2, 0, "Use arrow keys to select, Enter to confirm, 'c' to cancel")
stdscr.refresh()
key = stdscr.getch()
if key == curses.KEY_UP and selection > 0:
selection -= 1
elif key == curses.KEY_DOWN and selection < len(config_names) - 1:
selection += 1
elif key == curses.KEY_ENTER or key in [10, 13]:
# Set as active
name_to_set = config_names[selection]
if config_manager.set_active_configuration(name_to_set):
show_message(stdscr, f"Configuration '{name_to_set}' set as active.")
else:
show_message(stdscr, f"Error: Failed to set configuration '{name_to_set}' as active.")
return
elif key == ord('c') or key == ord('C'):
show_message(stdscr, "Cancelled.")
return
def view_active_configuration_tui(stdscr):
"""View active configuration via TUI"""
active_name = config_manager.get_active_configuration()
if not active_name:
show_message(stdscr, "No active configuration set.")
return
config = config_manager.get_configuration(active_name)
if not config:
show_message(stdscr, f"Error: Active configuration '{active_name}' not found.")
return
stdscr.clear()
h, w = stdscr.getmaxyx()
# Title
title = "Active Configuration"
stdscr.attron(curses.A_BOLD)
stdscr.addstr(0, (w - len(title)) // 2, title)
stdscr.attroff(curses.A_BOLD)
# Display configuration details
details = [
f"Name: {active_name}",
f"Provider: {config.get('provider', 'unknown')}",
f"Model: {config.get('model_name', 'unknown')}"
]
# Add provider-specific details
provider = config.get('provider')
if provider == "openai":
details.append(f"API Key: {'Set' if config.get('api_key') else 'Not set'}")
elif provider == "ollama":
base_url = config.get('base_url', 'http://localhost:11434')
details.append(f"Base URL: {base_url}")
elif provider == "openrouter":
details.append(f"API Key: {'Set' if config.get('api_key') else 'Not set'}")
base_url = config.get('base_url', 'https://openrouter.ai/api/v1')
details.append(f"Base URL: {base_url}")
# Display details
for idx, detail in enumerate(details):
y = 2 + idx
if y >= h - 2: # Prevent overflow
break
stdscr.addstr(y, 0, detail)
# Instructions
instructions = "Press any key to return to main menu"
stdscr.addstr(h-2, (w - len(instructions)) // 2, instructions)
stdscr.refresh()
stdscr.getch()
def main():
"""Main entry point for the TUI"""
curses.wrapper(main_tui)
if __name__ == "__main__":
main()