460 lines
13 KiB
Markdown
460 lines
13 KiB
Markdown
# Didactyl Server Installation Guide — systemd + Dedicated User Model
|
|
|
|
## Overview
|
|
|
|
This document describes how to install Didactyl on a Linux server as a dedicated system user, managed by systemd, with scoped sudo privileges for server maintenance tasks.
|
|
|
|
The mental model: **Didactyl is a person on your server.** It gets its own home directory, its own login identity, and explicit permission to manage the services you delegate to it — nothing more.
|
|
|
|
## Architecture
|
|
|
|
```mermaid
|
|
graph TD
|
|
subgraph Internet
|
|
RELAYS[Nostr Relays<br/>wss://relay.damus.io<br/>wss://relay.primal.net]
|
|
LLM_API[LLM Provider API<br/>OpenAI / PPQ / Ollama]
|
|
end
|
|
|
|
subgraph Server
|
|
subgraph "systemd"
|
|
SVC[didactyl.service<br/>User=didactyl<br/>Group=didactyl]
|
|
end
|
|
|
|
subgraph "/home/didactyl/"
|
|
BIN[didactyl binary]
|
|
CFG[genesis.jsonc<br/>mode 600]
|
|
LOGS[context.log.md<br/>didactyl.log]
|
|
end
|
|
|
|
subgraph "Privilege Boundary"
|
|
SUDOERS[/etc/sudoers.d/didactyl<br/>Whitelisted commands only]
|
|
end
|
|
|
|
subgraph "Unprivileged Commands"
|
|
MON[free / df / uptime<br/>ps / cat /proc/*<br/>ss -tlnp / top]
|
|
end
|
|
|
|
subgraph "Privileged Commands via sudo"
|
|
SYSD[systemctl status/restart/start/stop<br/>journalctl<br/>daemon-reload]
|
|
end
|
|
end
|
|
|
|
ADMIN[Administrator<br/>via Nostr DM] -->|NIP-04 encrypted| RELAYS
|
|
RELAYS <-->|WebSocket| SVC
|
|
SVC --> BIN
|
|
BIN <-->|HTTPS| LLM_API
|
|
BIN -->|popen as didactyl| MON
|
|
BIN -->|sudo via popen| SUDOERS
|
|
SUDOERS --> SYSD
|
|
|
|
style CFG fill:#f66,stroke:#333,color:#fff
|
|
style SUDOERS fill:#ff9,stroke:#333
|
|
style SVC fill:#9f9,stroke:#333
|
|
```
|
|
|
|
## Security Model
|
|
|
|
### Privilege Tiers
|
|
|
|
Didactyl enforces three privilege tiers for inbound Nostr messages (see `src/config.h` security_config_t):
|
|
|
|
| Tier | Who | Can use tools? | Can chat? |
|
|
|------|-----|---------------|-----------|
|
|
| **ADMIN** | Your npub (config `admin.pubkey`) | ✅ Yes | ✅ Yes |
|
|
| **WoT** | Contacts in your follow list | ❌ No (configurable) | ✅ Yes |
|
|
| **Stranger** | Everyone else | ❌ No | Canned response or ignored |
|
|
|
|
Only the ADMIN tier can trigger `local_shell_exec` — the tool that runs commands on the server.
|
|
|
|
### OS-Level Privilege Separation
|
|
|
|
The `local_shell_exec` tool in `src/tools/tool_local.c` calls `popen()` directly. Whatever the process user can do, the agent can do. This is why we:
|
|
|
|
1. Run as a **dedicated unprivileged user** (`didactyl`)
|
|
2. Grant **specific sudo permissions** via `/etc/sudoers.d/didactyl`
|
|
3. Use **systemd sandboxing** directives to limit filesystem access
|
|
|
|
### What the Agent Cannot Do
|
|
|
|
With the configuration described in this guide:
|
|
|
|
- ❌ Write anywhere outside `/home/didactyl/`
|
|
- ❌ Run arbitrary root commands not in the sudoers whitelist
|
|
- ❌ Install or remove packages (unless explicitly whitelisted)
|
|
- ❌ Modify system configuration files in `/etc/`
|
|
- ❌ Access other users' home directories (with proper home directory permissions)
|
|
- ❌ Accept tool commands from anyone except the ADMIN pubkey
|
|
|
|
---
|
|
|
|
## Installation Steps
|
|
|
|
### Prerequisites
|
|
|
|
- A Linux server with systemd (Debian/Ubuntu, RHEL/Fedora, Arch, etc.)
|
|
- Network access to Nostr relays and your LLM provider
|
|
- The Didactyl static binary (download from releases or build with `./build_static.sh`)
|
|
- A Nostr keypair for the agent (nsec)
|
|
- Your admin Nostr keypair (npub)
|
|
- An LLM API key
|
|
|
|
### Step 1: Create the Didactyl User
|
|
|
|
```bash
|
|
# Create a regular user with a home directory and bash shell
|
|
sudo useradd -m -s /bin/bash -c "Didactyl Nostr Agent" didactyl
|
|
```
|
|
|
|
**Why bash and not nologin?** The `local_shell_exec` tool runs commands via `sh -lc` (login shell). A real shell ensures PATH, locale, and environment are properly initialized. The agent needs to "log in" to do its job.
|
|
|
|
**Why a real home directory?** The agent needs a workspace for:
|
|
- Its binary and config
|
|
- Log files (`context.log.md`, `didactyl.log`)
|
|
- Any files it creates during shell operations
|
|
- The `working_directory` for the shell tool
|
|
|
|
### Step 2: Install the Binary and Config
|
|
|
|
```bash
|
|
# Copy the static binary
|
|
sudo cp didactyl_static_x86_64 /home/didactyl/didactyl
|
|
sudo chmod 755 /home/didactyl/didactyl
|
|
|
|
# Copy and configure genesis.jsonc
|
|
sudo cp genesis.jsonc /home/didactyl/genesis.jsonc
|
|
|
|
# CRITICAL: Lock down permissions on the config file (contains nsec private key)
|
|
sudo chmod 600 /home/didactyl/genesis.jsonc
|
|
|
|
# Set ownership
|
|
sudo chown -R didactyl:didactyl /home/didactyl/
|
|
|
|
# Protect the home directory from other users
|
|
sudo chmod 750 /home/didactyl/
|
|
```
|
|
|
|
### Step 3: Configure genesis.jsonc
|
|
|
|
Edit `/home/didactyl/genesis.jsonc` with the agent's identity, your admin pubkey, LLM credentials, and the shell working directory:
|
|
|
|
```jsonc
|
|
{
|
|
"key": {
|
|
"nsec": "nsec1..." // Agent's private key
|
|
},
|
|
|
|
"admin": {
|
|
"pubkey": "npub1..." // YOUR public key
|
|
},
|
|
|
|
"dm_protocol": "nip04",
|
|
|
|
"llm": {
|
|
"provider": "openai",
|
|
"api_key": "sk-...",
|
|
"model": "gpt-4o-mini",
|
|
"base_url": "https://api.openai.com/v1",
|
|
"max_tokens": 512,
|
|
"temperature": 0.7
|
|
},
|
|
|
|
"tools": {
|
|
"enabled": true,
|
|
"max_turns": 8,
|
|
"shell": {
|
|
"enabled": true,
|
|
"timeout_seconds": 60, // systemctl can be slow
|
|
"max_output_bytes": 131072, // journalctl output can be large
|
|
"working_directory": "/home/didactyl"
|
|
}
|
|
},
|
|
|
|
"api": {
|
|
"enabled": true,
|
|
"port": 8484,
|
|
"bind_address": "127.0.0.1" // Localhost only — do NOT expose
|
|
},
|
|
|
|
"startup_events": [
|
|
{
|
|
"kind": 10002,
|
|
"content": "",
|
|
"tags": [
|
|
["r", "wss://relay.damus.io"],
|
|
["r", "wss://relay.primal.net"]
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
**Key settings for server maintenance:**
|
|
|
|
| Setting | Value | Why |
|
|
|---------|-------|-----|
|
|
| `shell.timeout_seconds` | `60` | `systemctl` and `journalctl` can take time |
|
|
| `shell.max_output_bytes` | `131072` (128KB) | Log output can be verbose |
|
|
| `shell.working_directory` | `/home/didactyl` | Agent's home — safe default CWD |
|
|
| `api.bind_address` | `127.0.0.1` | Never expose the admin API to the network |
|
|
|
|
### Step 4: Configure sudo Privileges
|
|
|
|
```bash
|
|
sudo visudo -f /etc/sudoers.d/didactyl
|
|
```
|
|
|
|
Add the following (adjust to your needs):
|
|
|
|
```sudoers
|
|
# /etc/sudoers.d/didactyl
|
|
# Didactyl agent — scoped system maintenance privileges
|
|
|
|
# Service management
|
|
didactyl ALL=(ALL) NOPASSWD: /usr/bin/systemctl status *
|
|
didactyl ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart *
|
|
didactyl ALL=(ALL) NOPASSWD: /usr/bin/systemctl start *
|
|
didactyl ALL=(ALL) NOPASSWD: /usr/bin/systemctl stop *
|
|
didactyl ALL=(ALL) NOPASSWD: /usr/bin/systemctl enable *
|
|
didactyl ALL=(ALL) NOPASSWD: /usr/bin/systemctl disable *
|
|
didactyl ALL=(ALL) NOPASSWD: /usr/bin/systemctl is-active *
|
|
didactyl ALL=(ALL) NOPASSWD: /usr/bin/systemctl is-enabled *
|
|
didactyl ALL=(ALL) NOPASSWD: /usr/bin/systemctl list-units *
|
|
didactyl ALL=(ALL) NOPASSWD: /usr/bin/systemctl daemon-reload
|
|
|
|
# Log inspection
|
|
didactyl ALL=(ALL) NOPASSWD: /usr/bin/journalctl *
|
|
|
|
# Network diagnostics (if needed)
|
|
didactyl ALL=(ALL) NOPASSWD: /usr/sbin/lsof *
|
|
```
|
|
|
|
**Commands that do NOT need sudo** (the didactyl user can run these directly):
|
|
|
|
- `free -h` — memory usage
|
|
- `df -h` — disk usage
|
|
- `uptime` — load averages
|
|
- `top -bn1` — process snapshot
|
|
- `ps aux` — process list
|
|
- `cat /proc/cpuinfo` — CPU info
|
|
- `cat /proc/meminfo` — memory info
|
|
- `ss -tlnp` — listening ports (as unprivileged user, shows own processes)
|
|
- `uname -a` — kernel info
|
|
- `w` — who is logged in
|
|
|
|
### Step 5: Create the systemd Unit File
|
|
|
|
```bash
|
|
sudo tee /etc/systemd/system/didactyl.service << 'EOF'
|
|
[Unit]
|
|
Description=Didactyl Sovereign Nostr Agent
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=didactyl
|
|
Group=didactyl
|
|
WorkingDirectory=/home/didactyl
|
|
|
|
ExecStart=/home/didactyl/didactyl --config /home/didactyl/genesis.jsonc --debug 3
|
|
|
|
# --- Privilege & Sandbox ---
|
|
# Must be false — sudo needs to escalate privileges
|
|
NoNewPrivileges=false
|
|
|
|
# Protect filesystem: read-only except for agent home
|
|
ProtectSystem=strict
|
|
ReadWritePaths=/home/didactyl
|
|
|
|
# Must be false — the agent's home IS its workspace
|
|
ProtectHome=false
|
|
|
|
# Isolate /tmp
|
|
PrivateTmp=yes
|
|
|
|
# --- Environment ---
|
|
Environment=SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
|
|
|
|
# --- Restart Policy ---
|
|
Restart=on-failure
|
|
RestartSec=10
|
|
|
|
# --- Logging ---
|
|
StandardOutput=journal
|
|
StandardError=journal
|
|
SyslogIdentifier=didactyl
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
```
|
|
|
|
**Important systemd notes:**
|
|
|
|
| Directive | Value | Reason |
|
|
|-----------|-------|--------|
|
|
| `NoNewPrivileges` | `false` | Required for `sudo` to work from `popen()` |
|
|
| `ProtectSystem` | `strict` | Makes `/usr`, `/boot`, `/etc` read-only |
|
|
| `ReadWritePaths` | `/home/didactyl` | Whitelist the agent's home for writes |
|
|
| `ProtectHome` | `false` | The agent lives in `/home/` — can't protect it from itself |
|
|
| `PrivateTmp` | `yes` | Isolates `/tmp` so other processes can't snoop |
|
|
| `SSL_CERT_FILE` | path to CA bundle | Required for TLS connections to relays and LLM API |
|
|
|
|
### Step 6: Enable and Start
|
|
|
|
```bash
|
|
# Reload systemd to pick up the new unit
|
|
sudo systemctl daemon-reload
|
|
|
|
# Enable auto-start on boot
|
|
sudo systemctl enable didactyl
|
|
|
|
# Start the agent
|
|
sudo systemctl start didactyl
|
|
|
|
# Verify it's running
|
|
sudo systemctl status didactyl
|
|
|
|
# Watch logs in real-time
|
|
sudo journalctl -u didactyl -f
|
|
```
|
|
|
|
### Step 7: Verify the Agent is Working
|
|
|
|
1. **Check systemd status:**
|
|
```bash
|
|
sudo systemctl status didactyl
|
|
```
|
|
Should show `active (running)`.
|
|
|
|
2. **Check logs for relay connections:**
|
|
```bash
|
|
sudo journalctl -u didactyl --no-pager -n 50
|
|
```
|
|
Look for relay connection messages and "EOSE" (End of Stored Events).
|
|
|
|
3. **Send a test DM via Nostr:**
|
|
From your admin Nostr client, send an encrypted DM to the agent's npub:
|
|
```
|
|
What is the server uptime?
|
|
```
|
|
The agent should call `local_shell_exec` with `uptime` and reply with the result.
|
|
|
|
4. **Test sudo access:**
|
|
```
|
|
What services are running? Use systemctl list-units --type=service --state=running
|
|
```
|
|
|
|
5. **Check the local API (from the server itself):**
|
|
```bash
|
|
curl http://127.0.0.1:8484/api/context
|
|
```
|
|
|
|
---
|
|
|
|
## Updating the Agent
|
|
|
|
To update Didactyl to a new version:
|
|
|
|
```bash
|
|
# Stop the service
|
|
sudo systemctl stop didactyl
|
|
|
|
# Replace the binary
|
|
sudo cp didactyl_static_x86_64_new /home/didactyl/didactyl
|
|
sudo chown didactyl:didactyl /home/didactyl/didactyl
|
|
sudo chmod 755 /home/didactyl/didactyl
|
|
|
|
# Start the service
|
|
sudo systemctl start didactyl
|
|
```
|
|
|
|
The agent's identity, skills, and memory live on Nostr — replacing the binary doesn't lose any state.
|
|
|
|
---
|
|
|
|
## Expanding Privileges
|
|
|
|
The sudoers file is the single control point for what the agent can do with elevated privileges. To grant additional capabilities:
|
|
|
|
```bash
|
|
sudo visudo -f /etc/sudoers.d/didactyl
|
|
```
|
|
|
|
**Examples of additional privileges you might add:**
|
|
|
|
```sudoers
|
|
# Package management (careful!)
|
|
didactyl ALL=(ALL) NOPASSWD: /usr/bin/apt update
|
|
didactyl ALL=(ALL) NOPASSWD: /usr/bin/apt install *
|
|
didactyl ALL=(ALL) NOPASSWD: /usr/bin/apt upgrade -y
|
|
|
|
# Docker management
|
|
didactyl ALL=(ALL) NOPASSWD: /usr/bin/docker ps *
|
|
didactyl ALL=(ALL) NOPASSWD: /usr/bin/docker restart *
|
|
didactyl ALL=(ALL) NOPASSWD: /usr/bin/docker logs *
|
|
|
|
# Firewall inspection
|
|
didactyl ALL=(ALL) NOPASSWD: /usr/sbin/ufw status *
|
|
didactyl ALL=(ALL) NOPASSWD: /usr/sbin/iptables -L *
|
|
|
|
# Nginx/Apache config test
|
|
didactyl ALL=(ALL) NOPASSWD: /usr/sbin/nginx -t
|
|
didactyl ALL=(ALL) NOPASSWD: /usr/sbin/apachectl configtest
|
|
```
|
|
|
|
---
|
|
|
|
## Customizing the Agent's Personality for Server Maintenance
|
|
|
|
The agent's behavior is defined by its default skill content in `genesis.jsonc` (the `default_skill.content` field). For a server maintenance role, you should customize the soul/skill to include instructions like:
|
|
|
|
- What services it's responsible for monitoring
|
|
- How frequently to check (via triggered skills)
|
|
- What constitutes an alert-worthy condition
|
|
- How to format status reports
|
|
- Which `sudo` commands are available to it
|
|
- Escalation procedures (when to alert you vs. auto-fix)
|
|
|
|
This is a skill/prompt engineering task that can be done after the base installation is working.
|
|
|
|
---
|
|
|
|
## Quick Reference
|
|
|
|
### File Locations
|
|
|
|
| File | Path | Permissions |
|
|
|------|------|-------------|
|
|
| Binary | `/home/didactyl/didactyl` | `755 didactyl:didactyl` |
|
|
| Config | `/home/didactyl/genesis.jsonc` | `600 didactyl:didactyl` |
|
|
| Home directory | `/home/didactyl/` | `750 didactyl:didactyl` |
|
|
| systemd unit | `/etc/systemd/system/didactyl.service` | `644 root:root` |
|
|
| sudoers | `/etc/sudoers.d/didactyl` | `440 root:root` |
|
|
|
|
### Common Commands
|
|
|
|
```bash
|
|
# Service management
|
|
sudo systemctl start didactyl
|
|
sudo systemctl stop didactyl
|
|
sudo systemctl restart didactyl
|
|
sudo systemctl status didactyl
|
|
|
|
# Logs
|
|
sudo journalctl -u didactyl -f # follow live
|
|
sudo journalctl -u didactyl --since today
|
|
sudo journalctl -u didactyl -n 100 # last 100 lines
|
|
|
|
# Check agent's local API
|
|
curl http://127.0.0.1:8484/api/context
|
|
|
|
# Edit sudo permissions
|
|
sudo visudo -f /etc/sudoers.d/didactyl
|
|
|
|
# Edit config (stop service first)
|
|
sudo systemctl stop didactyl
|
|
sudo -u didactyl nano /home/didactyl/genesis.jsonc
|
|
sudo systemctl start didactyl
|
|
```
|