Terminal UI for Styrene mesh network management
Project description
Styrene
Terminal UI for Reticulum mesh network management.
A production-ready terminal interface for LXMF messaging, device discovery, and remote management over Reticulum mesh networks. Built on styrene-core for headless library functionality with an Imperial CRT terminal interface.
For headless deployments, see styrened - a lightweight daemon optimized for edge devices and NixOS.
Architecture
Styrene is part of a three-package ecosystem:
┌──────────────────┐ ┌──────────────────┐
│ styrene (TUI) │ │ styrened │
│ (this package) │ │ (daemon) │
├──────────────────┤ ├──────────────────┤
│ styrene-core │
│ (headless library) │
├────────────────────────────────────────┤
│ Reticulum Network Stack │
└────────────────────────────────────────┘
Package Roles:
- styrene-core - Headless library for RNS/LXMF applications
- styrene (this) - Interactive terminal UI
- styrened - Lightweight daemon for edge devices (no UI deps)
This separation enables:
- Interactive management with full TUI (this package)
- Headless services with minimal footprint (styrened)
- Custom applications building on styrene-core
Quick Start
# Install styrene (includes styrene-core)
pip install styrene
# Run the TUI
styrene
# Or for development:
git clone https://github.com/styrene-lab/styrene.git
cd styrene
pip install -e ".[dev]"
make run
For headless deployments (edge devices, servers):
pip install styrened
styrened
# See https://github.com/styrene-lab/styrened
Features
Text Messaging
- Send/receive messages over LXMF mesh networks
- Conversation history with persistent SQLite storage
- Unread message tracking and notifications
- Imperial CRT theme - Classic green phosphor terminal aesthetics
Remote Device Management
- Device status monitoring - CPU, memory, disk, network, services
- Remote command execution - Execute shell commands securely
- Device rebooting - Immediate or scheduled with delay
- Configuration management - Update YAML configs remotely
- SSH-like console - Familiar command-line interface over mesh
Security and Authorization
- Identity-based access control - Per-user, per-command permissions
- Command whitelisting - Only safe commands executable
- Audit logging - All operations logged with timestamps
- Systemd hardening - Resource limits, sandboxing, privilege restrictions
Usage Guide
Text Messaging
from styrene_core.protocols.chat import ChatProtocol
from styrene_core.services.lxmf_service import get_lxmf_service
# Initialize
lxmf = get_lxmf_service()
if lxmf.is_initialized:
chat = ChatProtocol(
router=lxmf.router,
identity=lxmf._identity,
db_engine=None # Or provide SQLAlchemy engine
)
# Send message
chat.send_message(
destination_hash="abc123def456...",
content="Hello over LXMF!"
)
Device Management
RPC Client Usage
from styrene.services.rpc_client import RPCClient
# Initialize
lxmf = LXMFService()
rpc = RPCClient(lxmf)
device_hash = "remote_device_identity_hash"
# Query device status
status = await rpc.call_status(device_hash)
print(f"IP: {status.ip}, Uptime: {status.uptime}s")
# Execute command
result = await rpc.call_exec(device_hash, "systemctl", ["status", "reticulum"])
print(f"Exit code: {result.exit_code}\n{result.stdout}")
# Reboot device (with 5-minute delay)
reboot = await rpc.call_reboot(device_hash, delay=300)
print(f"Reboot scheduled: {reboot.message}")
# Update configuration
config = await rpc.call_update_config(
device_hash,
{"log_level": "DEBUG", "max_retries": "5"}
)
print(f"Updated: {config.updated_keys}")
Device Console UI
SSH-like interface accessible via TUI:
$ status
IP Address: 192.168.1.100
Uptime: 1d 5h 23m (106380s)
Services: reticulum, lxmf, sshd
Disk Usage: 65% (32GB/50GB)
$ exec systemctl status reticulum
● reticulum.service - Reticulum Network
Active: active (running) since...
$ reboot 300
Success: Reboot scheduled in 5 minutes
Available commands:
status- Display device informationexec <command> [args...]- Execute shell commandreboot [delay]- Reboot device (delay in seconds)update-config <key> <value>- Update configuration
Keyboard shortcuts:
Enter- Execute commandEscape- Return to previous screenCtrl+L- Clear history
Hub Deployment
Styrene can be deployed as a public mesh hub for community infrastructure. When run in hub mode:
- RPC relay mode: Routes RPC messages between devices without executing commands
- Device discovery: Tracks mesh topology and announces hub presence
- Message propagation: LXMF store-and-forward via lxmd
- Security: Command execution is disabled on public hubs
For detailed hub deployment:
- Kubernetes: See reticulum/k8s for manifests
- Docker: Use
reticulum-hubcontainer image - Configuration: See Hub Config Guide
Quick start:
# Run as hub (relay mode, no command execution)
styrene --headless --mode hub
RPC Server Setup
1. Installation (NixOS Devices)
# Install server package
pip install -e packages/styrene-bond-rpc/
# Create config directory
mkdir -p ~/.config/styrene-bond-rpc
2. Configure Authorization
Create ~/.config/styrene-bond-rpc/auth.yaml:
identities:
# Admin - full access
- hash: "abc123def456..." # Client identity hash
name: "Admin User"
permissions:
- status
- exec
- reboot
- update_config
# Monitor - read-only
- hash: "789ghi012jkl..."
name: "Monitor User"
permissions:
- status
# Operator - limited control
- hash: "345mno678pqr..."
name: "Operator"
permissions:
- status
- exec
- reboot
Getting your identity hash:
# Using Reticulum CLI
rnstatus
# Look for: Identity: <abc123def456...>
Or programmatically:
from styrene.services.lxmf_service import LXMFService
lxmf = LXMFService()
print(f"My hash: {lxmf.identity.hexhash}")
3. Deploy Systemd Service
# Install service file
sudo cp packages/styrene-bond-rpc/systemd/styrene-bond-rpc.service /etc/systemd/system/
# Start service
sudo systemctl daemon-reload
sudo systemctl enable --now styrene-bond-rpc
# Verify
sudo systemctl status styrene-bond-rpc
journalctl -u styrene-bond-rpc -f
4. Verify from Client
from styrene.services.rpc_client import RPCClient
rpc = RPCClient(lxmf)
status = await rpc.call_status("server_device_hash")
print(f"Server alive! IP: {status.ip}")
API Reference
Chat Protocol
from styrene.protocols.chat import ChatProtocol
chat = ChatProtocol(router=lxmf, identity=identity)
# Send message
await chat.send_message(destination="...", content="Hello!")
# Protocol ID
assert chat.protocol_id == "chat"
RPC Client
from styrene.services.rpc_client import RPCClient
rpc = RPCClient(lxmf_service)
# Set default timeout
rpc.default_timeout = 60.0
# Query status
status: StatusResponse = await rpc.call_status(dest, timeout=30.0)
# Execute command
result: ExecResult = await rpc.call_exec(dest, "cmd", ["args"])
# Reboot device
reboot: RebootResult = await rpc.call_reboot(dest, delay=300)
# Update config
config: UpdateConfigResult = await rpc.call_update_config(dest, {...})
Response Types
from styrene.models.rpc_messages import (
StatusResponse,
ExecResult,
RebootResult,
UpdateConfigResult
)
# StatusResponse
status.uptime: int # Uptime in seconds
status.ip: str # IP address
status.services: list[str] # Running services
status.disk_used: int # Disk used (bytes)
status.disk_total: int # Total disk (bytes)
# ExecResult
result.exit_code: int # Command exit code
result.stdout: str # Standard output
result.stderr: str # Standard error
# RebootResult
reboot.success: bool # Reboot scheduled?
reboot.message: str # Human-readable message
reboot.scheduled_time: float? # Unix timestamp (if delayed)
# UpdateConfigResult
config.success: bool # Update succeeded?
config.message: str # Human-readable message
config.updated_keys: list[str] # Updated keys
Architecture
┌─────────────── Styrene TUI Application ───────────────┐
│ │
│ UI Layer (Textual) │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ Inbox │ │Conversation│ │Device Console│ │
│ └────┬─────┘ └─────┬──────┘ └──────┬───────┘ │
│ │ │ │ │
│ ┌────▼──────────────▼───────────────▼──────┐ │
│ │ ProtocolRegistry (Router) │ │
│ │ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Chat │ │ RPC │ │ │
│ │ │ Protocol │ │ Client │ │ │
│ │ └─────┬────┘ └─────┬────┘ │ │
│ └────────┼────────────────────┼─────────────┘ │
│ │ │ │
│ ┌────────▼────────────────────▼─────────┐ │
│ │ LXMFService (Transport) │ │
│ └────────┬──────────────────────────┬───┘ │
└───────────┼──────────────────────────┼────────────────┘
│ │
┌───────▼────────┐ ┌────────▼────────┐
│ SQLite DB │ │ Reticulum │
│ (Messages) │ │ Mesh Network │
└────────────────┘ └─────────────────┘
│
┌───────▼────────┐
│ Remote Device │
│ RPC Server │
└────────────────┘
Key Components:
- ProtocolRegistry - Routes messages to correct protocol handler
- ChatProtocol - Text messaging with persistence
- RPCClient - Device management commands
- LXMFService - LXMF/Reticulum integration
- Message Model - SQLAlchemy ORM for persistence
Security
Threat Mitigation
- Unauthorized commands - Identity-based authorization
- Arbitrary code execution - Command whitelisting
- Privilege escalation - Systemd hardening
- Resource exhaustion - CPU/memory limits
Best Practices
1. Principle of Least Privilege
Only grant necessary permissions:
# Give minimal access
identities:
- hash: "operator_hash"
permissions:
- status
- exec
# NO reboot or update_config
2. Command Whitelisting
Review allowed commands in handlers.py:
allowed_commands = {
"systemctl", "journalctl", "cat", "ls",
# NEVER: "rm", "dd", "mkfs", "curl"
}
3. Identity Rotation
# Generate new identity periodically
rnid -g -n my-device-v2
# Update auth.yaml with new hash
# Remove old identity
4. Audit Logs
# Check for denied access
journalctl -u styrene-bond-rpc | grep "Authorization denied"
# Monitor failed commands
journalctl -u styrene-bond-rpc | grep "exit_code.*[^0]"
Troubleshooting
"RPC timeout - no response"
Check device is reachable:
rnprobe <device_hash>
Verify RPC server is running:
ssh device-ip
sudo systemctl status styrene-bond-rpc
"Authorization denied"
Get your identity hash:
rnstatus # Look for: Identity: <abc123...>
Add to server's auth.yaml:
identities:
- hash: "abc123..."
permissions: ["status", "exec"]
Reload server:
sudo systemctl restart styrene-bond-rpc
Debug Logging
import logging
logging.basicConfig(level=logging.DEBUG)
# Or specific modules
logging.getLogger('styrene.protocols').setLevel(logging.DEBUG)
logging.getLogger('styrene.services.rpc_client').setLevel(logging.DEBUG)
Development
Running Tests
# All tests
python -m pytest tests/ packages/styrene-bond-rpc/tests/ -v
# Specific test file
python -m pytest tests/protocols/test_chat.py -v
# With coverage
python -m pytest --cov=src --cov-report=html
Code Quality
# Linter
ruff check src/ tests/
ruff check --fix src/ tests/
# Type checker
mypy src/
# Full validation
make validate
Project Structure
src/styrene/
├── protocols/ # Protocol implementations
│ ├── base.py # Protocol ABC
│ ├── registry.py # Protocol router
│ └── chat.py # Chat protocol
├── services/ # Business logic
│ ├── lxmf_service.py # LXMF transport
│ └── rpc_client.py # RPC client
├── screens/ # TUI screens
│ ├── inbox.py
│ ├── conversation.py
│ └── device_console.py
├── models/ # Data models
│ ├── messages.py
│ └── rpc_messages.py
└── widgets/ # Custom widgets
packages/styrene-bond-rpc/ # RPC server package
├── src/
│ ├── server.py
│ ├── handlers.py
│ └── auth.py
├── systemd/
└── tests/
tests/ # Client tests
├── protocols/
├── services/
├── screens/
└── integration/
License
MIT License - see LICENSE file for details.
Acknowledgments
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file styrene_tui-0.5.0.tar.gz.
File metadata
- Download URL: styrene_tui-0.5.0.tar.gz
- Upload date:
- Size: 1.0 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.4 {"installer":{"name":"uv","version":"0.10.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9e7dcc17caa79e888d9f05fb5f38d890e33dd7ead1d672832548758df46515c3
|
|
| MD5 |
0cf088ebc10c1337f88ab849e9ff4ff0
|
|
| BLAKE2b-256 |
31bd7524e1525b597afc98ec15cc8514d3fd931d182571167a40d9a8da5c3bf7
|
File details
Details for the file styrene_tui-0.5.0-py3-none-any.whl.
File metadata
- Download URL: styrene_tui-0.5.0-py3-none-any.whl
- Upload date:
- Size: 149.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.4 {"installer":{"name":"uv","version":"0.10.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e981fcdf2506957880cc02a86ab8c915cb318eeabea77628c77b1edb520fc4ae
|
|
| MD5 |
e289ad7cde26f67f66b5ff7afa457171
|
|
| BLAKE2b-256 |
ba82c833ecbce15ccde775ea32b32f2aefc73282270d9ccd66b8fffc80d651ef
|