Files
AirlockTools/docs/02_Software_Design_Document.md
T
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

1300 lines
38 KiB
Markdown

# 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=<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
```bash
# From Gitea PyPI registry
pip install loxide \
--extra-index-url https://<gitea-instance>/api/packages/<user>/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*