Skip to main content

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 information
  • exec <command> [args...] - Execute shell command
  • reboot [delay] - Reboot device (delay in seconds)
  • update-config <key> <value> - Update configuration

Keyboard shortcuts:

  • Enter - Execute command
  • Escape - Return to previous screen
  • Ctrl+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:

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

styrene_tui-0.5.0.tar.gz (1.0 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

styrene_tui-0.5.0-py3-none-any.whl (149.4 kB view details)

Uploaded Python 3

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

Hashes for styrene_tui-0.5.0.tar.gz
Algorithm Hash digest
SHA256 9e7dcc17caa79e888d9f05fb5f38d890e33dd7ead1d672832548758df46515c3
MD5 0cf088ebc10c1337f88ab849e9ff4ff0
BLAKE2b-256 31bd7524e1525b597afc98ec15cc8514d3fd931d182571167a40d9a8da5c3bf7

See more details on using hashes here.

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

Hashes for styrene_tui-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e981fcdf2506957880cc02a86ab8c915cb318eeabea77628c77b1edb520fc4ae
MD5 e289ad7cde26f67f66b5ff7afa457171
BLAKE2b-256 ba82c833ecbce15ccde775ea32b32f2aefc73282270d9ccd66b8fffc80d651ef

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page