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:
- Run as a dedicated unprivileged user (
didactyl) - Grant specific sudo permissions via
/etc/sudoers.d/didactyl - 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_directoryfor 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 usagedf -h— disk usageuptime— load averagestop -bn1— process snapshotps aux— process listcat /proc/cpuinfo— CPU infocat /proc/meminfo— memory infoss -tlnp— listening ports (as unprivileged user, shows own processes)uname -a— kernel infow— 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
-
Check systemd status:
sudo systemctl status didactylShould show
active (running). -
Check logs for relay connections:
sudo journalctl -u didactyl --no-pager -n 50Look for relay connection messages and "EOSE" (End of Stored Events).
-
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_execwithuptimeand reply with the result. -
Test sudo access:
What services are running? Use systemctl list-units --type=service --state=running -
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
sudocommands 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