Table of Contents
- Loxide
- Software Design Document
- Document Control
- Table of Contents
- 1. Introduction
- 2. System Overview
- 3. Architecture Design
- 4. Component Design
- 4.1 Main Application (Loxide.py)
- 4.2 API Wrapper (API.py)
- 4.3 Multi-Agent Selector (multiagentselector.py)
- 4.4 Policy Selector (policyselector.py)
- 4.5 Policy Tree Widget (policytreewidget.py)
- 4.6 Configuration Manager (configmanager.py)
- 5. Data Design
- 6. Security Design
- 6.1 Credential Storage Architecture
- 6.2 Key Derivation Implementation
- 6.3 Credential Encryption
- 6.4 Credential Retrieval
- 6.5 Platform Keyring Configuration
- 6.6 Password Complexity Validation
- 7. User Interface Design
- 8. External Interface Design
- 9. Distribution and Deployment
- 9.1 Windows Executable (Nuitka)
- 9.1.1 Build Configuration
- 9.1.2 Build Options Explained
- 9.1.3 Build Requirements
- 9.1.4 Build Artifacts
- 9.2 Linux Distribution
- 9.3 Platform Dependencies
- 10. Error Handling
- 11. Phase 2: LEMON Integration
- 12. Appendices
- Appendix A: Textual Framework Patterns
- Appendix B: API Response Schemas
- Appendix C: Configuration Schema
- Document Approval
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
- Introduction
- System Overview
- Architecture Design
- Component Design
- Data Design
- Security Design
- User Interface Design
- External Interface Design
- Distribution and Deployment
- Error Handling
- Phase 2: LEMON Integration
- 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:
- Security - Protect credentials and sensitive operations
- Reliability - Handle errors gracefully without data loss
- Usability - Provide intuitive keyboard-driven interface
- Performance - Respond quickly to user actions
- 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:
- Presentation Layer - TUI screens and widgets
- Business Logic Layer - Operations and workflows
- Data Access Layer - API communication and data models
- 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