styrene-tui 0.5.0
Terminal UI for Styrene mesh network management
License: MIT
Requires Python: <3.13,>=3.11
Keywords: fleet, provisioning, reticulum, terminal, tui

# 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](https://github.com/styrene-lab/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

```bash
# 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):
```bash
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

```python
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

```python
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:
- **Kubernetes**: See [reticulum/k8s](../reticulum/k8s) for manifests
- **Docker**: Use `reticulum-hub` container image
- **Configuration**: See [Hub Config Guide](../reticulum/docs/HUB-CONFIG.md)

Quick start:
```bash
# Run as hub (relay mode, no command execution)
styrene --headless --mode hub
```

## RPC Server Setup

### 1. Installation (NixOS Devices)

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

```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:**

```bash
# Using Reticulum CLI
rnstatus

# Look for: Identity: <abc123def456...>
```

Or programmatically:
```python
from styrene.services.lxmf_service import LXMFService
lxmf = LXMFService()
print(f"My hash: {lxmf.identity.hexhash}")
```

### 3. Deploy Systemd Service

```bash
# 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

```python
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

```python
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

```python
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

```python
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:

```yaml
# Give minimal access
identities:
  - hash: "operator_hash"
    permissions:
      - status
      - exec
      # NO reboot or update_config
```

**2. Command Whitelisting**

Review allowed commands in `handlers.py`:

```python
allowed_commands = {
    "systemctl", "journalctl", "cat", "ls",
    # NEVER: "rm", "dd", "mkfs", "curl"
}
```

**3. Identity Rotation**

```bash
# Generate new identity periodically
rnid -g -n my-device-v2

# Update auth.yaml with new hash
# Remove old identity
```

**4. Audit Logs**

```bash
# 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:**
```bash
rnprobe <device_hash>
```

**Verify RPC server is running:**
```bash
ssh device-ip
sudo systemctl status styrene-bond-rpc
```

### "Authorization denied"

**Get your identity hash:**
```bash
rnstatus  # Look for: Identity: <abc123...>
```

**Add to server's auth.yaml:**
```yaml
identities:
  - hash: "abc123..."
    permissions: ["status", "exec"]
```

**Reload server:**
```bash
sudo systemctl restart styrene-bond-rpc
```

### Debug Logging

```python
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

```bash
# 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

```bash
# 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](LICENSE) file for details.

## Acknowledgments

- [Reticulum](https://github.com/markqvist/Reticulum)
- [LXMF](https://github.com/markqvist/LXMF)
- [Textual](https://github.com/Textualize/textual)
- [SQLAlchemy](https://github.com/sqlalchemy/sqlalchemy)