# Loxide ## Software Design Document **Document Version:** 1.0 **Date:** December 2025 **Authors:** Brandon Wickline (Lead Python Developer), James Brotosky (Lead Rust Developer) **Status:** Approved --- ## Document Control ### Revision History | Version | Date | Author | Description | |---------|------|--------|-------------| | 0.1 | Jul 2025 | B. Wickline | Initial architecture draft | | 0.2 | Oct 2025 | J. Brotosky | Added Rust integration design | | 0.3 | Oct 2025 | B. Wickline | Added security and configuration design | | 1.0 | Dec 2025 | B. Wickline | First release candidate | ### Document Approval | Role | Name | Signature | Date | |------|------|-----------|------| | Lead Developer | Brandon Wickline | | | | Lead Developer | James Brotosky | | | | Technical Reviewer | | | | --- ## Table of Contents 1. Introduction 2. System Overview 3. Architecture Design 4. Component Design 5. Data Design 6. Security Design 7. User Interface Design 8. External Interface Design 9. Distribution and Deployment 10. Error Handling 11. Phase 2: LEMON Integration 12. Appendices --- ## 1. Introduction ### 1.1 Purpose This Software Design Document (SDD) provides a comprehensive description of the design and architecture of Loxide, a terminal-based user interface application for Airlock endpoint security management. This document translates the requirements specified in the Software Requirements Specification (SRS) into a detailed design that can be implemented by the development team. ### 1.2 Scope This document covers: - System architecture and component design - Data models and storage design - Security implementation details - User interface design patterns - External interface specifications - Distribution and deployment design - Error handling strategies ### 1.3 Intended Audience This document is intended for: - Software developers implementing system components - System architects reviewing design decisions - Quality assurance engineers developing test plans - Operations personnel preparing deployment procedures - Technical stakeholders requiring design visibility ### 1.4 Design Goals The design prioritizes the following goals in order of importance: 1. **Security** - Protect credentials and sensitive operations 2. **Reliability** - Handle errors gracefully without data loss 3. **Usability** - Provide intuitive keyboard-driven interface 4. **Performance** - Respond quickly to user actions 5. **Maintainability** - Enable easy modification and extension ### 1.5 References | Reference | Description | |-----------|-------------| | Loxide SRS | Software Requirements Specification | | Textual Documentation | TUI framework reference | | Airlock API Specification | External API documentation | | Python Style Guide (PEP 8) | Code style reference | --- ## 2. System Overview ### 2.1 System Context Loxide operates as a client application connecting to an Airlock server for endpoint security management. The system provides a terminal-based interface enabling bulk operations that are cumbersome in the standard web console. ### 2.2 Design Constraints The following constraints influenced the design: | Constraint | Impact on Design | |------------|------------------| | Terminal-based UI | All interaction via keyboard; no mouse-dependent features | | Cross-platform (Windows/Linux) | Platform-specific code isolated; keyring abstraction | | Standalone executable | Nuitka compilation; bundled dependencies | | Private dependency | airlock_libs requires Gitea access | | Secure credential storage | Platform keyring with encryption layer | ### 2.3 System Decomposition The system is decomposed into the following major subsystems: ``` +------------------------------------------------------------------+ | LOXIDE | | | | +------------------------+ +---------------------------+ | | | User Interface | | Business Logic | | | | | | | | | | +------------------+ | | +---------------------+ | | | | | Main App | | | | Policy Operations | | | | | | (Loxide.py) | | | +---------------------+ | | | | +------------------+ | | +---------------------+ | | | | +------------------+ | | | Agent Operations | | | | | | Screens | | | +---------------------+ | | | | +------------------+ | | +---------------------+ | | | | +------------------+ | | | OTP Operations | | | | | | Widgets | | | +---------------------+ | | | | +------------------+ | | +---------------------+ | | | +------------------------+ | | Analysis Engine | | | | | +---------------------+ | | | +------------------------+ +---------------------------+ | | | Data Access | | | | | +---------------------------+ | | | +------------------+ | | Infrastructure | | | | | API Wrapper | | | | | | | | (API.py) | | | +---------------------+ | | | | +------------------+ | | | Configuration | | | | | +------------------+ | | +---------------------+ | | | | | Data Models | | | +---------------------+ | | | | +------------------+ | | | Security | | | | +------------------------+ | +---------------------+ | | | | +---------------------+ | | | +------------------------+ | | Logging | | | | | External Libraries | | +---------------------+ | | | | | +---------------------------+ | | | +------------------+ | | | | | airlock_libs | | | | | | (Rust) | | | | | +------------------+ | | | +------------------------+ | +------------------------------------------------------------------+ ``` --- ## 3. Architecture Design ### 3.1 Architectural Style Loxide employs a layered architecture with clear separation between: 1. **Presentation Layer** - TUI screens and widgets 2. **Business Logic Layer** - Operations and workflows 3. **Data Access Layer** - API communication and data models 4. **Infrastructure Layer** - Configuration, security, logging ### 3.2 Module Organization ``` loxide/ ├── Loxide.py # Main application entry point ├── models/ │ ├── agent.py # Agent data model │ ├── policy.py # Policy data model │ └── execution.py # Execution event model ├── services/ │ ├── API.py # Airlock API wrapper │ └── security.py # Credential management ├── TUI/ │ ├── Screens/ │ │ ├── policyprepworkflowscreen.py │ │ ├── quietagentworkflowscreen.py │ │ ├── executionhistoryscreen.py │ │ ├── otpactivityscreen.py │ │ ├── otprevokescreen.py │ │ ├── otpworkflowscreen.py │ │ └── moveagentworkflowscreen.py │ ├── Widgets/ │ │ ├── multiagentselector.py │ │ ├── policyselector.py │ │ ├── policytreewidget.py │ │ ├── agentmoveoperations.py │ │ ├── serverlogwidget.py │ │ ├── resultsdisplay.py │ │ └── themeselector.py │ └── Themes/ │ ├── theme_amber_terminal.py │ └── theme_retro_terminal.py ├── utils/ │ ├── configmanager.py # Configuration management │ ├── setup.py # Initialization and logging │ └── utils.py # Utility functions └── system_config.json # System configuration (bundled) ``` ### 3.3 Component Interactions #### 3.3.1 Startup Sequence ``` 1. main() called │ ├─> setup() initializes logging and configuration │ ├─> load_system_config() reads system_config.json │ ├─> load_user_config() reads/creates user_config.json │ └─> configure logging based on LOG_LEVEL │ ├─> getAPI() retrieves credentials │ ├─> check keyring for stored credential │ ├─> if found: prompt for password, decrypt │ └─> if not found: prompt for API key, encrypt, store │ ├─> AirlockAPIWrapper() created with base_url and api_key │ └─> LoxideApp().run() starts the TUI ├─> compose() builds initial UI └─> on_mount() loads initial data ``` #### 3.3.2 Screen Navigation Flow ``` MainScreen (Tabs) │ ├── Dashboard Tab │ └── Summary statistics and charts │ ├── Agent Operations Tab │ ├── MultiAgentSelector (widget) │ └── AgentMoveOperations (widget) │ ├── Move to Policy → PolicySelector (screen) │ ├── Toggle Enforcement │ ├── Generate OTP → OTPWorkflowScreen │ └── View History → ExecutionHistoryScreen │ ├── Policy Prep Tab │ └── PolicyPrepWorkflowScreen │ ├── Step 1: Select Policies │ ├── Step 2: Configure │ ├── Step 3: Fetch Data │ ├── Step 4: Review Results │ └── Step 5: Approve Hashes │ ├── Quiet Agents Tab │ └── QuietAgentWorkflowScreen │ ├── Configure thresholds │ ├── Run analysis (Rust) │ └── Review results │ ├── OTP Management Tab │ ├── Active OTPs list │ ├── OTPActivitiesScreen │ └── OTPRevokeScreen │ └── Server Logs Tab └── ServerLogWidget ``` ### 3.4 Data Flow #### 3.4.1 Agent Selection and Operation ``` User Input (hostnames) │ ▼ MultiAgentSelector │ ├── Parse input (split by newline/comma) │ ├── For each hostname: │ └── API.agent_find_by_hostname() │ ├── Compile matches/unmatches │ ▼ User Selection (checkboxes) │ ▼ AgentMoveOperations │ ├── Move: API.agent_move() ├── Toggle: API.policy_set_auditmode() ├── OTP: API.otp_generate() └── History: API.history_execution() │ ▼ ResultsDisplay (success/failure counts) ``` #### 3.4.2 Policy Preparation Workflow ``` Policy Selection │ ▼ Configuration (days, allowlists) │ ▼ Data Fetch │ ├── For each policy: │ └── API.policy_list_agents() │ ├── For each agent: │ └── API.history_execution() │ ▼ Categorization │ ├── Check against allowlists ├── Check against baselines ├── Apply VT_THREAT_TOLERANCE (using Airlock VT data) ├── Apply BAD_PUBLISHERS filter │ ▼ Results Display │ ├── Approved hashes ├── Unapproved hashes (by category) └── Blocked hashes │ ▼ Hash Approval │ └── API.hash_add_to_allowlist() ``` --- ## 4. Component Design ### 4.1 Main Application (Loxide.py) #### 4.1.1 Class: LoxideApp ```python class LoxideApp(App): """Main Textual application class.""" # Reactive properties api: reactive[AirlockAPIWrapper] current_tab: reactive[str] # Key bindings BINDINGS = [ ("q", "quit", "Quit"), ("t", "toggle_theme", "Theme"), ("r", "refresh", "Refresh"), ("?", "help", "Help"), ] def compose(self) -> ComposeResult: """Build the main UI layout.""" yield Header() yield Tabs(...) yield ContentSwitcher(...) yield Footer() def on_mount(self) -> None: """Initialize on application start.""" self.load_initial_data() async def action_quit(self) -> None: """Handle quit action with confirmation.""" ... ``` #### 4.1.2 Responsibilities - Application lifecycle management - Global keyboard shortcut handling - Tab navigation coordination - Theme management - Error handling and notification display ### 4.2 API Wrapper (API.py) #### 4.2.1 Class: AirlockAPIWrapper ```python class AirlockAPIWrapper: """Wrapper for Airlock REST API communication.""" def __init__(self, base_url: str, api_key: str): self.base_url = base_url.rstrip("/") self.api_key = api_key self.headers = {"X-APIKey": self.api_key} def _post(self, endpoint: str, payload: dict = None) -> dict: """Send POST request and return JSON response.""" ... # Agent operations def agent_find_all(self) -> pd.DataFrame: ... def agent_find_by_hostname(self, hostname: str) -> pd.DataFrame: ... def agent_move(self, agentid: str, groupid: str) -> dict: ... # Policy operations def policy_find_all(self) -> pd.DataFrame: ... def policy_set_auditmode(self, groupid: str, auditmode: str) -> dict: ... # OTP operations def otp_generate(self, agentid: str, duration: int, purpose: str) -> str: ... def otp_find_active(self) -> pd.DataFrame: ... def otp_revoke(self, otpid: str) -> dict: ... # Hash operations (includes VT data from Airlock) def hash_query(self, hashes: List[str]) -> pd.DataFrame: ... def hash_add_to_allowlist(self, applicationid: str, hashes: List[str]) -> dict: ... # History operations def history_execution(self, today: str, date_selected: str, agent_name: str) -> List[Dict]: ... ``` #### 4.2.2 Error Handling All API methods implement consistent error handling: ```python def _post(self, endpoint: str, payload: dict = None) -> dict: url = f"{self.base_url}{endpoint}" try: response = requests.post( url, headers=self.headers, data=json.dumps(payload or {}), verify=False, # Configurable for self-signed certs timeout=30 ) response.raise_for_status() return response.json() except requests.exceptions.Timeout: logger.error(f"Request timeout: {endpoint}") raise APITimeoutError(f"Request to {endpoint} timed out") except requests.exceptions.ConnectionError: logger.error(f"Connection error: {endpoint}") raise APIConnectionError(f"Cannot connect to {self.base_url}") except requests.exceptions.HTTPError as e: logger.error(f"HTTP error {e.response.status_code}: {endpoint}") raise APIError(f"API error: {e.response.status_code}") ``` ### 4.3 Multi-Agent Selector (multiagentselector.py) #### 4.3.1 Class: MultiAgentSelector ```python class MultiAgentSelector(Widget): """Widget for selecting multiple agents via various input methods.""" # Signals class AgentsSelected(Message): def __init__(self, agents: List[Agent]): ... def compose(self) -> ComposeResult: yield Input(placeholder="Paste hostnames or wildcards...") yield Button("Load from File") yield DataTable(id="agent-table") yield Static(id="status") async def on_input_submitted(self, event: Input.Submitted) -> None: """Process pasted input.""" lines = event.value.strip().split("\n") await self.process_hostnames(lines) async def process_hostnames(self, hostnames: List[str]) -> None: """Query API for each hostname and populate table.""" matched = [] unmatched = [] for hostname in hostnames: if "*" in hostname or "?" in hostname: # Wildcard handling agents = await self.expand_wildcard(hostname) else: agents = self.api.agent_find_by_hostname(hostname) ... def get_selected_agents(self) -> List[Agent]: """Return list of currently selected agents.""" ... ``` #### 4.3.2 Wildcard Expansion ```python async def expand_wildcard(self, pattern: str) -> List[Agent]: """Expand wildcard pattern to matching agents.""" import fnmatch all_agents = self.api.agent_find_all() matches = [] for _, agent in all_agents.iterrows(): if fnmatch.fnmatch(agent["hostname"].lower(), pattern.lower()): matches.append(agent) return matches ``` ### 4.4 Policy Selector (policyselector.py) #### 4.4.1 Class: PolicySelector ```python class PolicySelector(Widget): """Reusable widget for policy/group selection.""" # Configuration multi_select: bool = True show_agent_count: bool = True def compose(self) -> ComposeResult: yield Input(placeholder="Filter policies...") yield PolicyTreeWidget(id="policy-tree") def on_mount(self) -> None: self.load_policies() def load_policies(self) -> None: """Load policy tree from API.""" policies = self.api.policy_find_all() self.query_one(PolicyTreeWidget).build_tree(policies) def get_selected_policies(self) -> List[Policy]: """Return selected policy objects.""" ... ``` ### 4.5 Policy Tree Widget (policytreewidget.py) #### 4.5.1 Class: PolicyTreeWidget ```python class PolicyTreeWidget(Tree): """Tree view of policy hierarchy with checkboxes.""" def build_tree(self, policies: pd.DataFrame) -> None: """Build tree structure from flat policy list.""" # Build parent-child relationships root_policies = policies[policies["parentid"].isna()] for _, policy in root_policies.iterrows(): node = self.root.add(policy["groupname"], data=policy) self._add_children(node, policy["groupid"], policies) def _add_children(self, parent_node, parent_id: str, policies: pd.DataFrame) -> None: """Recursively add child policies.""" children = policies[policies["parentid"] == parent_id] for _, child in children.iterrows(): node = parent_node.add(child["groupname"], data=child) self._add_children(node, child["groupid"], policies) ``` ### 4.6 Configuration Manager (configmanager.py) #### 4.6.1 Architecture ```python # Two-tier configuration architecture # System config (immutable) - bundled in executable SYSTEM_CONFIG_KEYS = [ "URL", # Airlock server URL "APPNAME", # Application name "LOG_LEVEL", # Logging level "BAD_PATH_PARTS", # Paths to flag "BAD_PUBLISHERS", # Publishers to flag "PUPS", # Potentially unwanted programs "PATH_EXCLUSION_CONST", # Path exclusion threshold "MIN_FILES_FOR_PATH", # Minimum files for path suggestion "VT_THREAT_TOLERANCE", # VT score threshold (Airlock VT data) "POLICY_MAP_ENF_AUD", # Policy mappings ] # User config (mutable) - in user's config directory USER_CONFIG_KEYS = [ "TELEMETRY", # Telemetry opt-in "TELEM_URL", # Telemetry endpoint "TEXTUAL_THEME", # UI theme "EXTRAS", # Feature flags ] ``` #### 4.6.2 Configuration Loading ```python def load_system_config() -> dict: """Load system configuration from bundled file.""" # Check for bundled location (Nuitka) bundled_path = Path(getattr(sys, "_MEIPASS", "")) / "system_config.json" if bundled_path.exists(): return json.load(open(bundled_path)) # Fallback to development location return json.load(open(Path(__file__).parent.parent / "system_config.json")) def load_user_config(config_dir: Path) -> dict: """Load or create user configuration.""" user_config_path = config_dir / "user_config.json" if not user_config_path.exists(): # Create with defaults default_config = { "TELEMETRY": False, "TELEM_URL": "", "TEXTUAL_THEME": "gruvbox", "EXTRAS": "NOTTODAY", } user_config_path.parent.mkdir(parents=True, exist_ok=True) json.dump(default_config, open(user_config_path, "w"), indent=4) return default_config return json.load(open(user_config_path)) ``` --- ## 5. Data Design ### 5.1 Data Models #### 5.1.1 Agent Model ```python @dataclass class Agent: """Represents an Airlock agent/endpoint.""" agentid: str hostname: str groupid: str groupname: str status: int # 0=Offline, 1=Online, 3=Safemode username: str osversion: str agentversion: str lastcheckin: datetime @property def is_online(self) -> bool: return self.status == 1 @property def status_display(self) -> str: return {0: "Offline", 1: "Online", 3: "Safemode"}.get(self.status, "Unknown") ``` #### 5.1.2 Policy Model ```python @dataclass class Policy: """Represents an Airlock policy/group.""" groupid: str groupname: str parentid: Optional[str] auditmode: int # 0=Enforcement, 1=Audit agentcount: int @property def is_audit(self) -> bool: return self.auditmode == 1 @property def mode_display(self) -> str: return "Audit" if self.is_audit else "Enforcement" ``` #### 5.1.3 Execution Model ```python @dataclass class Execution: """Represents an application execution event.""" id: str hostname: str filename: str filepath: str sha256: str publisher: str category: int timestamp: datetime username: str commandline: str policyname: str vtscore: Optional[int] = None # From Airlock VT integration @property def category_display(self) -> str: categories = { 0: "Approved", 1: "Unapproved", 2: "Blocked", 3: "OTP Bypass", } return categories.get(self.category, f"Unknown ({self.category})") ``` ### 5.2 Data Storage #### 5.2.1 Configuration Files **System Configuration (system_config.json)** ```json { "URL": "https://airlock.example.com/api", "APPNAME": "Loxide", "LOG_LEVEL": "INFO", "BAD_PATH_PARTS": ["temp", "tmp", "cache"], "BAD_PUBLISHERS": ["Unknown Publisher", "Self-signed"], "PUPS": ["toolbars", "adware"], "PATH_EXCLUSION_CONST": 4, "MIN_FILES_FOR_PATH": 4, "VT_THREAT_TOLERANCE": 4, "POLICY_MAP_ENF_AUD": {} } ``` **User Configuration (user_config.json)** ```json { "TELEMETRY": false, "TELEM_URL": "", "TEXTUAL_THEME": "gruvbox", "EXTRAS": "NOTTODAY" } ``` #### 5.2.2 File Locations | Platform | Base Directory | Subdirectories | |----------|----------------|----------------| | Windows | %APPDATA%\Loxide | config/, logs/, cache/, data/ | | Linux | ~/.local/share/Loxide | config/, logs/, cache/, data/ | --- ## 6. Security Design ### 6.1 Credential Storage Architecture ``` +-------------------+ +-------------------+ +-------------------+ | User Password | --> | Key Derivation | --> | Encryption Key | +-------------------+ | (PBKDF2) | | (256-bit) | +-------------------+ +-------------------+ | v +-------------------+ +-------------------+ +-------------------+ | API Key | --> | AES-256-GCM | --> | Encrypted Blob | | (plaintext) | | Encryption | | (salt+nonce+ct) | +-------------------+ +-------------------+ +-------------------+ | v +-------------------+ | Platform Keyring | | (Base64 encoded) | +-------------------+ ``` ### 6.2 Key Derivation Implementation ```python # security.py KDF_ITERATIONS = 200_000 SALT_SIZE = 16 # 128-bit NONCE_SIZE = 12 # AES-GCM standard KEY_SIZE = 32 # AES-256 def _derive_key(password: bytes, salt: bytes) -> bytes: """Derive encryption key from password using PBKDF2.""" kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), length=KEY_SIZE, salt=salt, iterations=KDF_ITERATIONS, ) return kdf.derive(password) ``` ### 6.3 Credential Encryption ```python def store_api_key(service: str, username: str, api_key: str, password: str): """Encrypt and store API key in platform keyring.""" configure_keyring_backend() # Generate random salt salt = os.urandom(SALT_SIZE) # Derive encryption key key = _derive_key(password.encode(), salt) # Encrypt API key aesgcm = AESGCM(key) nonce = os.urandom(NONCE_SIZE) ciphertext = aesgcm.encrypt(nonce, api_key.encode(), associated_data=None) # Combine: salt || nonce || ciphertext blob = salt + nonce + ciphertext # Store base64-encoded in keyring keyring.set_password(service, username, base64.b64encode(blob).decode()) ``` ### 6.4 Credential Retrieval ```python def retrieve_api_key(service: str, username: str, password: str) -> str: """Retrieve and decrypt API key from platform keyring.""" configure_keyring_backend() # Get from keyring b64_blob = keyring.get_password(service, username) if b64_blob is None: raise ValueError("No stored credential") # Decode blob = base64.b64decode(b64_blob) # Extract components salt = blob[:SALT_SIZE] nonce = blob[SALT_SIZE:SALT_SIZE + NONCE_SIZE] ciphertext = blob[SALT_SIZE + NONCE_SIZE:] # Derive key and decrypt key = _derive_key(password.encode(), salt) aesgcm = AESGCM(key) plaintext = aesgcm.decrypt(nonce, ciphertext, associated_data=None) return plaintext.decode() ``` ### 6.5 Platform Keyring Configuration ```python def configure_keyring_backend(): """Configure appropriate keyring backend for current platform.""" system = platform.system() if system == "Windows": import keyring.backends.Windows keyring.set_keyring(keyring.backends.Windows.WinVaultKeyring()) elif system == "Linux": import keyring.backends.kwallet keyring.set_keyring(keyring.backends.kwallet.DBusKeyring()) else: raise EnvironmentError(f"Unsupported platform: {system}") ``` ### 6.6 Password Complexity Validation ```python def check_password_complexity(password: str) -> bool: """Validate password meets complexity requirements.""" if len(password) < 12: return False if not re.search(r"[A-Z]", password): # Uppercase return False if not re.search(r"[a-z]", password): # Lowercase return False if not re.search(r"[0-9]", password): # Digit return False if not re.search(r"[^A-Za-z0-9]", password): # Special return False return True ``` --- ## 7. User Interface Design ### 7.1 Screen Layout Standards #### 7.1.1 Screen Structure All screens follow a consistent layout: ``` +------------------------------------------------------------------+ | Header: Application Name | Current Screen | Connection Status | +------------------------------------------------------------------+ | | | Content Area | | | | (Varies by screen - tables, forms, wizards, etc.) | | | +------------------------------------------------------------------+ | Footer: [Q]uit [R]efresh [Esc]Back [?]Help [Tab]Navigate | +------------------------------------------------------------------+ ``` #### 7.1.2 Widget vs Screen Pattern | Type | Has Header/Footer | Navigation | Usage | |------|-------------------|------------|-------| | Screen | Yes | push_screen() | Full-page workflows | | Widget | No | Embedded | Reusable components | ### 7.2 Keyboard Navigation #### 7.2.1 Global Bindings | Key | Action | Context | |-----|--------|---------| | q | Quit application | Global | | Esc | Back/Cancel | Screens | | Tab | Next widget | Navigation | | Shift+Tab | Previous widget | Navigation | | r | Refresh data | Data views | | e | Export to CSV | Data tables | | ? | Show help | Global | #### 7.2.2 Data Table Navigation | Key | Action | |-----|--------| | Up/Down | Move row cursor | | Page Up/Down | Scroll page | | Home/End | First/last row | | Space | Toggle selection | | Enter | Select/activate | | a | Select all | | n | Deselect all | ### 7.3 Theme System #### 7.3.1 Theme Architecture ```python # Themes are Python functions returning CSS strings def get_amber_terminal_theme() -> str: return """ Screen { background: #1a1a1a; } Header { background: #ff8c00; color: #000000; } DataTable > .datatable--cursor { background: #ff8c00 30%; } Button { background: #ff8c00; color: #000000; } ... """ ``` #### 7.3.2 Available Themes | Theme | Description | |-------|-------------| | gruvbox | Default warm retro theme | | amber_terminal | Classic amber CRT look | | retro_terminal | Green phosphor terminal | --- ## 8. External Interface Design ### 8.1 Airlock API Integration #### 8.1.1 Request Format All API requests use POST with JSON payload: ```python def _post(self, endpoint: str, payload: dict = None) -> dict: url = f"{self.base_url}{endpoint}" headers = { "X-APIKey": self.api_key, "Content-Type": "application/json" } response = requests.post( url, headers=headers, data=json.dumps(payload or {}), verify=False, timeout=30 ) return response.json() ``` #### 8.1.2 Response Handling ```python # Standard response structure { "status": "success" | "error", "response": { # Endpoint-specific data }, "message": "Optional error message" } # Extraction pattern def agent_find_all(self) -> pd.DataFrame: result = self._post("/v1/agent/find", {}) return pd.DataFrame(result["response"]["agents"]) ``` ### 8.2 Data Export #### 8.2.1 CSV Export Implementation ```python def export_to_csv(data: pd.DataFrame, filepath: str) -> None: """Export DataFrame to CSV with Excel-compatible encoding.""" data.to_csv( filepath, index=False, encoding="utf-8-sig", # BOM for Excel quoting=csv.QUOTE_NONNUMERIC ) ``` #### 8.2.2 Timestamp Formatting ```python def format_timestamp(dt: datetime) -> str: """Format timestamp for display and export.""" return dt.strftime("%Y-%m-%d %H:%M:%S") ``` --- ## 9. Distribution and Deployment ### 9.1 Windows Executable (Nuitka) #### 9.1.1 Build Configuration ```powershell # Set inputs $icon = '.\Loxide_Icon.ico' $config = '.\system_config.json' # Clean prior outputs Remove-Item -Recurse -Force ` '.\Loxide.build', '.\Loxide.dist', '.\Loxide.onefile-build' ` -ErrorAction SilentlyContinue # Build arguments $args = @( '--onefile', '--follow-imports', '--msvc=latest', # Keyring + pywin32 modules (ensure lazy imports are included) '--include-module=keyring.backends.Windows', '--include-module=win32cred', '--include-module=pywintypes', '--include-module=pythoncom', '--include-module=win32api', '--include-module=win32security', '--include-module=win32con', # Product metadata and resources '--windows-product-name=Loxide', '--windows-file-version=0.9.0.0', '--windows-product-version=0.9.0', '--windows-company-name=', "--windows-icon-from-ico=$icon", "--include-data-file=$config=system_config.json", # Deployment mode (disable helper checks for production) '--deployment', '.\Loxide.py' ) & nuitka @args ``` #### 9.1.2 Build Options Explained | Option | Purpose | |--------|---------| | --onefile | Single executable output | | --follow-imports | Follow all import statements | | --msvc=latest | Use latest MSVC compiler | | --include-module=keyring.backends.Windows | Windows Credential Manager support | | --include-module=win32cred | Windows credential API | | --include-module=pywintypes | PyWin32 types | | --include-module=pythoncom | COM support | | --include-module=win32api | Windows API bindings | | --include-module=win32security | Security API bindings | | --include-module=win32con | Windows constants | | --windows-product-name | Executable metadata | | --windows-file-version | Version in file properties | | --windows-company-name | Company in file properties | | --windows-icon-from-ico | Application icon | | --include-data-file | Bundle system_config.json | | --deployment | Disable debug checks for production | #### 9.1.3 Build Requirements - Windows 10/11 build machine - Python 3.10+ - Nuitka package (`pip install nuitka`) - MSVC compiler (Visual Studio Build Tools) - PyWin32 package (`pip install pywin32`) - All runtime dependencies installed - Access to Gitea for airlock_libs #### 9.1.4 Build Artifacts | Directory | Contents | |-----------|----------| | Loxide.build | Intermediate build files | | Loxide.dist | Distribution files (standalone mode) | | Loxide.onefile-build | Onefile build cache | | Loxide.exe | Final executable (onefile mode) | ### 9.2 Linux Distribution #### 9.2.1 pip Installation ```bash # From Gitea PyPI registry pip install loxide \ --extra-index-url https:///api/packages//pypi/simple/ ``` #### 9.2.2 Standalone Binary ```bash # Nuitka on Linux python -m nuitka \ --standalone \ --onefile \ --include-package-data=textual \ Loxide.py ``` ### 9.3 Platform Dependencies | Platform | Keyring Backend | Additional Requirements | |----------|-----------------|------------------------| | Windows | Windows Credential Manager | None | | Linux | Secret Service | libsecret, KWallet or GNOME Keyring | --- ## 10. Error Handling ### 10.1 Error Categories | Category | Examples | Handling | |----------|----------|----------| | API Errors | Timeout, 4xx, 5xx | Toast notification, retry option | | Input Errors | Invalid hostname, bad date | Inline validation message | | UI Errors | Widget crash | Graceful degradation, error screen | | Configuration Errors | Missing config, bad JSON | Startup warning, defaults | | Credential Errors | Wrong password, no credential | Re-prompt with retry limit | ### 10.2 Logging Strategy ```python # setup.py class TextualNotificationHandler(logging.Handler): """Log handler that displays toasts for important messages.""" def emit(self, record): if record.levelno >= logging.WARNING: # Show as toast notification app = App.get_running_app() if app: app.notify(record.getMessage(), severity="warning") def setup_logging(log_level: str, log_dir: Path): """Configure logging with file and notification handlers.""" logger = logging.getLogger() logger.setLevel(getattr(logging, log_level)) # File handler for all messages file_handler = logging.FileHandler(log_dir / "Loxide.log") file_handler.setFormatter(logging.Formatter( "%(asctime)s - %(name)s - %(levelname)s - %(message)s" )) logger.addHandler(file_handler) # Notification handler for warnings and above notification_handler = TextualNotificationHandler() notification_handler.setLevel(logging.WARNING) logger.addHandler(notification_handler) ``` ### 10.3 Common Error Patterns #### 10.3.1 DataFrame Boolean Ambiguity ```python # Problem: "The truth value of a Series is ambiguous" # Wrong: agents[not agents["enforce_ready"]] # Correct: agents[~agents["enforce_ready"]] ``` #### 10.3.2 Empty DataFrame Access ```python # Problem: KeyError on empty DataFrame # Wrong: value = df["column"].iloc[0] # Correct: if not df.empty and "column" in df.columns: value = df["column"].iloc[0] ``` #### 10.3.3 Duplicate Widget IDs ```python # Problem: "DuplicateIds" error on widget mount # Solution: Remove existing widgets first async def update_content(self): await self.query("#my-widget").remove() await self.mount(MyWidget(id="my-widget")) ``` --- ## 11. Phase 2: LEMON Integration ### 11.1 Overview LEMON (Loxide Execution MONitoring) is a planned backend service for automated local approval sessions. Full specifications are in separate LEMON documentation. ### 11.2 Loxide Integration Points | Component | Integration | |-----------|-------------| | LEMONClient | New service class for LEMON API | | LEMONSessionsScreen | View/create sessions | | LEMONHashReviewScreen | Review pending hashes | | Certificate Configuration | mTLS setup | ### 11.3 Configuration Additions ```json { "LEMON_ENABLED": true, "LEMON_URL": "https://lemon.example.com/api/v1", "LEMON_USER_CERT": "/path/to/user.crt", "LEMON_CA_CERT": "/path/to/internal_ca.crt" } ``` --- ## 12. Appendices ### Appendix A: Textual Framework Patterns Key patterns used throughout the application. ### Appendix B: API Response Schemas Complete API response schema documentation. ### Appendix C: Configuration Schema JSON schema for configuration files. --- ## Document Approval This Software Design Document has been reviewed and approved for implementation. | Role | Name | Date | |------|------|------| | Author | Brandon Wickline | | | Technical Reviewer | James Brotosky | | | Approver | | | --- *End of Document*