# 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
wss://relay.damus.io
wss://relay.primal.net] LLM_API[LLM Provider API
OpenAI / PPQ / Ollama] end subgraph Server subgraph "systemd" SVC[didactyl.service
User=didactyl
Group=didactyl] end subgraph "/home/didactyl/" BIN[didactyl binary] CFG[genesis.jsonc
mode 600] LOGS[context.log.md
didactyl.log] end subgraph "Privilege Boundary" SUDOERS[/etc/sudoers.d/didactyl
Whitelisted commands only] end subgraph "Unprivileged Commands" MON[free / df / uptime
ps / cat /proc/*
ss -tlnp / top] end subgraph "Privileged Commands via sudo" SYSD[systemctl status/restart/start/stop
journalctl
daemon-reload] end end ADMIN[Administrator
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 ```