Files
AirlockTools/docs/02_Software_Design_Document.md
Zarithas 423e9e8208 feat(release): Loxide 1.0 RC
- Added comprehensive documentation:
  - System Design Requirements (SDR)
  - System Design Specification (SDS)
  - API Reference
  - User Stories & Use Cases
- Fixed minor UI issues related to double encoding
2025-12-22 10:45:36 -05:00

38 KiB

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

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

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:

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

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

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

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

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

# 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

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

@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

@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

@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)

{
    "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)

{
    "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

# 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

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

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

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

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

# 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:

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

# 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

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

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

# 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=<Your Organization>',
  "--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

# From Gitea PyPI registry
pip install loxide \
    --extra-index-url https://<gitea-instance>/api/packages/<user>/pypi/simple/

9.2.2 Standalone Binary

# 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

# 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

# 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

# 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

# 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

{
    "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