Files
didactyl/plans/server_installation_guide.md
T

13 KiB

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

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

# 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

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

{
  "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

sudo visudo -f /etc/sudoers.d/didactyl

Add the following (adjust to your needs):

# /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

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

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

    sudo systemctl status didactyl
    

    Should show active (running).

  2. Check logs for relay connections:

    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):

    curl http://127.0.0.1:8484/api/context
    

Updating the Agent

To update Didactyl to a new version:

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

sudo visudo -f /etc/sudoers.d/didactyl

Examples of additional privileges you might add:

# 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

# 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