Files
didactyl/plans/server_installation_guide.md

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