From 340e7682d00303c09ef0d5c611cd0db3d059b643 Mon Sep 17 00:00:00 2001 From: Ben Tossell Date: Mon, 16 Feb 2026 22:36:19 -0500 Subject: [PATCH 1/4] docs: add CI badges, requirements, interactive installer - CI and Integration status badges in README header - Requirements table (OS, RAM, CPU, disk) - Linux platform note (tested on Ubuntu 24.04 + Arch Linux) - install.sh: interactive one-command installer - Detects distro (Ubuntu/Arch), installs prereqs - Clones repo or uses existing clone - Runs setup.sh automatically - Walks through secrets with validation + links - Writes .env with correct permissions - Offers to launch agent in tmux - README Quick Start updated to feature installer - Tested on fresh Ubuntu 24.04 and Arch Linux droplets --- AGENTS.md | 3 + README.md | 16 ++- install.sh | 413 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 429 insertions(+), 3 deletions(-) create mode 100755 install.sh diff --git a/AGENTS.md b/AGENTS.md index 9c41d82..b20418c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -76,6 +76,9 @@ Agent runtime layout: ## Development Workflow ```bash +# First-time install (interactive — handles everything) +sudo ~/hornet/install.sh + # Edit source files directly in ~/hornet/ # Deploy to agent runtime diff --git a/README.md b/README.md index 3a1bc31..cd26637 100644 --- a/README.md +++ b/README.md @@ -33,14 +33,21 @@ Agents work on real files in real repos — no sandbox friction. They make real ## Quick Start ```bash -# Clone (as admin — source lives in admin's home, not agent's) -git clone ~/hornet +git clone https://github.com/modem-dev/hornet.git ~/hornet +sudo ~/hornet/install.sh +``` + +The installer detects your distro, installs dependencies, creates the agent user, sets up the firewall, and walks you through API keys interactively. Takes ~2 minutes. + +
+Manual setup (without installer) +```bash # Setup (creates user, firewall, permissions — run as root) sudo bash ~/hornet/setup.sh # Add secrets -sudo su - hornet_agent -c 'vim ~/.config/.env' +sudo -u hornet_agent vim ~/.config/.env # Deploy source → agent runtime ~/hornet/bin/deploy.sh @@ -49,6 +56,9 @@ sudo su - hornet_agent -c 'vim ~/.config/.env' sudo -u hornet_agent ~/runtime/start.sh ``` +See [CONFIGURATION.md](CONFIGURATION.md) for the full list of secrets and how to obtain them. +
+ ## Configuration Secrets and configuration live in `~hornet_agent/.config/.env` (not in repo, 600 perms). diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..1c63b67 --- /dev/null +++ b/install.sh @@ -0,0 +1,413 @@ +#!/bin/bash +# Hornet Interactive Installer +# +# One-command setup: +# curl -sSf https://raw.githubusercontent.com/modem-dev/hornet/main/install.sh | sudo bash +# +# Or from a clone: +# sudo ./install.sh +# +# What this does: +# 1. Detects distro, installs system prerequisites +# 2. Clones the repo (or uses existing clone) +# 3. Runs setup.sh (user, Node.js, firewall, etc.) +# 4. Walks you through secrets interactively +# 5. Deploys and launches the agent +# +# Must run as root. Tested on Ubuntu 24.04 and Arch Linux. + +set -euo pipefail + +# ── Formatting ─────────────────────────────────────────────────────────────── + +BOLD='\033[1m' +DIM='\033[2m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +RED='\033[0;31m' +CYAN='\033[0;36m' +RESET='\033[0m' + +info() { echo -e "${BOLD}${GREEN}▸${RESET} $1"; } +warn() { echo -e "${BOLD}${YELLOW}▸${RESET} $1"; } +err() { echo -e "${BOLD}${RED}✗${RESET} $1" >&2; } +ask() { echo -en "${BOLD}${CYAN}?${RESET} $1"; } +dim() { echo -e "${DIM}$1${RESET}"; } +header() { echo -e "\n${BOLD}── $1 ──${RESET}\n"; } + +die() { err "$1"; exit 1; } + +# ── Preflight ──────────────────────────────────────────────────────────────── + +if [ "$(id -u)" -ne 0 ]; then + die "Must run as root. Try: sudo $0" +fi + +if [[ ! "$(uname -s)" =~ Linux ]]; then + die "Hornet requires Linux (kernel-level isolation). macOS/Windows are not supported." +fi + +echo "" +echo -e "${BOLD}🐝 Hornet Installer${RESET}" +echo "" + +# ── Detect distro ──────────────────────────────────────────────────────────── + +header "System" + +DISTRO="unknown" +if [ -f /etc/os-release ]; then + # shellcheck disable=SC1091 + . /etc/os-release + case "$ID" in + ubuntu|debian) DISTRO="ubuntu" ;; + arch|archarm) DISTRO="arch" ;; + *) + if [ -n "${ID_LIKE:-}" ]; then + case "$ID_LIKE" in + *debian*|*ubuntu*) DISTRO="ubuntu" ;; + *arch*) DISTRO="arch" ;; + esac + fi + ;; + esac +fi + +if [ "$DISTRO" = "unknown" ]; then + die "Unsupported distro. Hornet is tested on Ubuntu 24.04 and Arch Linux." +fi + +info "Detected: ${BOLD}$PRETTY_NAME${RESET} ($DISTRO)" + +# ── Detect admin user ──────────────────────────────────────────────────────── + +# If run via sudo, SUDO_USER is the real user. Otherwise ask. +ADMIN_USER="${SUDO_USER:-}" +if [ -z "$ADMIN_USER" ] || [ "$ADMIN_USER" = "root" ]; then + # Try to find a non-root user with a home directory + ADMIN_USER="" + while IFS=: read -r username _ uid _ _ home _; do + if [ "$uid" -ge 1000 ] && [ "$uid" -lt 60000 ] && [ -d "$home" ] && [ "$username" != "hornet_agent" ]; then + ADMIN_USER="$username" + break + fi + done < /etc/passwd + + if [ -z "$ADMIN_USER" ]; then + ask "Admin username (your account, not root): " + read -r ADMIN_USER + else + ask "Admin username [${ADMIN_USER}]: " + read -r input + if [ -n "$input" ]; then + ADMIN_USER="$input" + fi + fi +fi + +if ! id "$ADMIN_USER" &>/dev/null; then + die "User '$ADMIN_USER' does not exist." +fi + +ADMIN_HOME=$(eval echo "~$ADMIN_USER") +info "Admin user: ${BOLD}$ADMIN_USER${RESET} ($ADMIN_HOME)" + +# ── Install prerequisites ──────────────────────────────────────────────────── + +header "Prerequisites" + +install_prereqs_ubuntu() { + # Wait for unattended-upgrades (common on fresh VMs) + if pgrep -x 'apt|apt-get|dpkg|unattended-upgrade' >/dev/null 2>&1; then + info "Waiting for background apt to finish..." + for _ in $(seq 1 60); do + if ! pgrep -x 'apt|apt-get|dpkg|unattended-upgrade' >/dev/null 2>&1; then + break + fi + sleep 2 + done + fi + apt-get update -qq + apt-get install -y -qq git curl tmux iptables docker.io sudo 2>&1 | tail -3 +} + +install_prereqs_arch() { + pacman -Syu --noconfirm --needed git curl tmux iptables docker sudo 2>&1 | tail -5 +} + +info "Installing: git, curl, tmux, iptables, docker, sudo" +"install_prereqs_$DISTRO" +info "Prerequisites installed" + +# ── Clone or locate repo ──────────────────────────────────────────────────── + +header "Source" + +REPO_DIR="$ADMIN_HOME/hornet" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd || echo "")" + +if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/setup.sh" ] && [ -f "$SCRIPT_DIR/bin/deploy.sh" ]; then + # Running from an existing clone + REPO_DIR="$SCRIPT_DIR" + info "Using existing clone: $REPO_DIR" +else + # Need to clone + if [ -d "$REPO_DIR/.git" ]; then + info "Repo already exists at $REPO_DIR, pulling latest..." + sudo -u "$ADMIN_USER" git -C "$REPO_DIR" pull --ff-only 2>&1 | tail -1 + else + REPO_URL="https://github.com/modem-dev/hornet.git" + info "Cloning $REPO_URL → $REPO_DIR" + sudo -u "$ADMIN_USER" git clone "$REPO_URL" "$REPO_DIR" 2>&1 | tail -1 + fi +fi + +if [ ! -f "$REPO_DIR/setup.sh" ]; then + die "setup.sh not found in $REPO_DIR — bad clone?" +fi + +info "Source ready: $REPO_DIR" + +# ── Run setup.sh ───────────────────────────────────────────────────────────── + +header "Setup" + +info "Running setup.sh (user, Node.js, firewall, permissions)..." +info "This takes 1–2 minutes." +echo "" +bash "$REPO_DIR/setup.sh" "$ADMIN_USER" +echo "" +info "Core setup complete" + +# ── Interactive secrets ────────────────────────────────────────────────────── + +header "Secrets" + +HORNET_HOME="/home/hornet_agent" +ENV_FILE="$HORNET_HOME/.config/.env" + +echo -e "Hornet needs API keys to talk to services. You'll be prompted for each one." +echo -e "Press ${BOLD}Enter${RESET} to skip optional values. You can always edit later:" +echo -e " ${DIM}$ENV_FILE${RESET}" +echo "" + +# Accumulate env vars +declare -A ENV_VARS + +# prompt_secret KEY "description" "url" [required] [prefix] +prompt_secret() { + local key="$1" desc="$2" url="${3:-}" required="${4:-}" prefix="${5:-}" + local label="" + if [ "$required" = "required" ]; then + label="${RED}*${RESET} " + fi + + if [ -n "$url" ]; then + dim " $url" + fi + ask "${label}${desc}: " + read -r value + + # Validate prefix if provided (prefix can be "a|b" for alternatives) + if [ -n "$value" ] && [ -n "$prefix" ]; then + local match=false + IFS='|' read -ra prefixes <<< "$prefix" + for p in "${prefixes[@]}"; do + if [[ "$value" == "$p"* ]]; then + match=true + break + fi + done + if [ "$match" = false ]; then + warn "Expected prefix '${prefix}' — saved anyway" + fi + fi + + # Warn if required and empty + if [ -z "$value" ] && [ "$required" = "required" ]; then + warn "Skipped (required — agent won't fully work without this)" + fi + + if [ -n "$value" ]; then + ENV_VARS[$key]="$value" + fi +} + +# -- Required -- +echo -e "${BOLD}Required${RESET} ${DIM}(agent won't start without these)${RESET}" +echo "" + +prompt_secret "OPENCODE_ZEN_API_KEY" \ + "LLM API key (Anthropic/OpenAI)" \ + "https://docs.anthropic.com/en/api/getting-started" \ + "required" + +prompt_secret "GITHUB_TOKEN" \ + "GitHub personal access token" \ + "https://github.com/settings/tokens" \ + "required" \ + "ghp_|github_pat_" + +prompt_secret "SLACK_BOT_TOKEN" \ + "Slack bot token" \ + "https://api.slack.com/apps → OAuth & Permissions" \ + "required" \ + "xoxb-" + +prompt_secret "SLACK_APP_TOKEN" \ + "Slack app-level token (Socket Mode)" \ + "https://api.slack.com/apps → Basic Information → App-Level Tokens" \ + "required" \ + "xapp-" + +prompt_secret "SLACK_ALLOWED_USERS" \ + "Slack user IDs (comma-separated)" \ + "Click your Slack profile → ··· → Copy member ID" \ + "required" \ + "U" + +echo "" + +# -- Optional -- +echo -e "${BOLD}Optional${RESET} ${DIM}(press Enter to skip)${RESET}" +echo "" + +prompt_secret "AGENTMAIL_API_KEY" \ + "AgentMail API key" \ + "https://app.agentmail.to" + +prompt_secret "HORNET_EMAIL" \ + "Agent email address (e.g. agent@agentmail.to)" + +if [ -n "${ENV_VARS[AGENTMAIL_API_KEY]:-}" ]; then + prompt_secret "HORNET_SECRET" \ + "Email auth secret (or press Enter to auto-generate)" + if [ -z "${ENV_VARS[HORNET_SECRET]:-}" ]; then + ENV_VARS[HORNET_SECRET]="$(openssl rand -hex 32)" + dim " Auto-generated: ${ENV_VARS[HORNET_SECRET]}" + fi + + prompt_secret "HORNET_ALLOWED_EMAILS" \ + "Allowed sender emails (comma-separated)" +fi + +prompt_secret "SENTRY_AUTH_TOKEN" \ + "Sentry API token" \ + "https://sentry.io/settings/account/api/auth-tokens/" + +if [ -n "${ENV_VARS[SENTRY_AUTH_TOKEN]:-}" ]; then + prompt_secret "SENTRY_ORG" "Sentry org slug" + prompt_secret "SENTRY_CHANNEL_ID" "Slack channel ID for Sentry alerts" "" "" "C" +fi + +prompt_secret "KERNEL_API_KEY" \ + "Kernel cloud browser API key" \ + "https://kernel.computer" + +# Always set these +ENV_VARS[HORNET_AGENT_USER]="hornet_agent" +ENV_VARS[HORNET_AGENT_HOME]="$HORNET_HOME" +ENV_VARS[HORNET_SOURCE_DIR]="$REPO_DIR" + +# ── Write .env ─────────────────────────────────────────────────────────────── + +header "Configuration" + +# Build .env content +ENV_CONTENT="# Hornet agent configuration +# Generated by install.sh on $(date -Iseconds) +# Edit with: sudo -u hornet_agent vim $ENV_FILE +" + +# Write in a sensible order +ordered_keys=( + OPENCODE_ZEN_API_KEY + GITHUB_TOKEN + SLACK_BOT_TOKEN + SLACK_APP_TOKEN + SLACK_ALLOWED_USERS + AGENTMAIL_API_KEY + HORNET_EMAIL + HORNET_SECRET + HORNET_ALLOWED_EMAILS + SENTRY_AUTH_TOKEN + SENTRY_ORG + SENTRY_CHANNEL_ID + KERNEL_API_KEY + HORNET_AGENT_USER + HORNET_AGENT_HOME + HORNET_SOURCE_DIR +) + +for key in "${ordered_keys[@]}"; do + if [ -n "${ENV_VARS[$key]:-}" ]; then + ENV_CONTENT+="${key}=${ENV_VARS[$key]}"$'\n' + fi +done + +# Write as agent user with correct permissions +echo "$ENV_CONTENT" > "$ENV_FILE" +chown hornet_agent:hornet_agent "$ENV_FILE" +chmod 600 "$ENV_FILE" + +info "Wrote $(grep -c '=' "$ENV_FILE") variables to $ENV_FILE" + +# ── Launch ─────────────────────────────────────────────────────────────────── + +header "Launch" + +# Check if we have the minimum required secrets +MISSING="" +for key in OPENCODE_ZEN_API_KEY GITHUB_TOKEN SLACK_BOT_TOKEN SLACK_APP_TOKEN SLACK_ALLOWED_USERS; do + if [ -z "${ENV_VARS[$key]:-}" ]; then + MISSING+=" - $key\n" + fi +done + +if [ -n "$MISSING" ]; then + warn "Missing required secrets — skipping launch:" + echo -e "$MISSING" + echo -e "Add them to ${BOLD}$ENV_FILE${RESET} and start manually:" + echo -e " ${DIM}sudo -u hornet_agent ~/runtime/start.sh${RESET}" + echo "" +else + ask "Start the agent now? [Y/n]: " + read -r launch + if [ -z "$launch" ] || [[ "$launch" =~ ^[Yy] ]]; then + info "Launching in tmux session 'hornet'..." + sudo -u hornet_agent tmux new-session -d -s hornet "$HORNET_HOME/runtime/start.sh" 2>/dev/null || true + sleep 2 + if sudo -u hornet_agent tmux has-session -t hornet 2>/dev/null; then + info "Agent is running ✓" + else + warn "tmux session didn't start — try manually:" + echo -e " ${DIM}sudo -u hornet_agent ~/runtime/start.sh${RESET}" + fi + else + info "Skipped. Start later with:" + echo -e " ${DIM}sudo -u hornet_agent ~/runtime/start.sh${RESET}" + fi +fi + +# ── Done ───────────────────────────────────────────────────────────────────── + +header "Done" + +SSH_PUB="$HORNET_HOME/.ssh/id_ed25519.pub" + +echo -e "🐝 ${BOLD}Hornet is installed.${RESET}" +echo "" +echo -e " ${BOLD}Edit secrets:${RESET} sudo -u hornet_agent vim $ENV_FILE" +echo -e " ${BOLD}Start agent:${RESET} sudo -u hornet_agent ~/runtime/start.sh" +echo -e " ${BOLD}Attach to tmux:${RESET} sudo -u hornet_agent tmux attach -t hornet" +echo -e " ${BOLD}Update runtime:${RESET} $REPO_DIR/bin/deploy.sh" +echo -e " ${BOLD}Security audit:${RESET} $REPO_DIR/bin/security-audit.sh" +echo "" +if [ -f "$SSH_PUB" ]; then + echo -e " ${YELLOW}⚠${RESET} Add the agent's SSH key to GitHub:" + echo -e " $(cat "$SSH_PUB")" + echo -e " ${DIM}https://github.com/settings/keys${RESET}" + echo "" +fi +echo -e " ${DIM}Full configuration reference: $REPO_DIR/CONFIGURATION.md${RESET}" +echo "" From 01c15af902c56f4f8db2252f44f0a0e2d4ea9fba Mon Sep 17 00:00:00 2001 From: Ben Tossell Date: Mon, 16 Feb 2026 23:09:24 -0500 Subject: [PATCH 2/4] install: fix eval injection, update usage comments - Replace eval echo ~user with getent passwd for home dir lookup - Recommend clone+run as primary method over curl|bash --- install.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/install.sh b/install.sh index 1c63b67..04428cb 100755 --- a/install.sh +++ b/install.sh @@ -2,9 +2,9 @@ # Hornet Interactive Installer # # One-command setup: -# curl -sSf https://raw.githubusercontent.com/modem-dev/hornet/main/install.sh | sudo bash +# git clone https://github.com/modem-dev/hornet.git ~/hornet && sudo ~/hornet/install.sh # -# Or from a clone: +# Or if already cloned: # sudo ./install.sh # # What this does: @@ -109,7 +109,7 @@ if ! id "$ADMIN_USER" &>/dev/null; then die "User '$ADMIN_USER' does not exist." fi -ADMIN_HOME=$(eval echo "~$ADMIN_USER") +ADMIN_HOME=$(getent passwd "$ADMIN_USER" | cut -d: -f6) info "Admin user: ${BOLD}$ADMIN_USER${RESET} ($ADMIN_HOME)" # ── Install prerequisites ──────────────────────────────────────────────────── From b63038ca8b1d28c61f4543c1240b0af5664e44d8 Mon Sep 17 00:00:00 2001 From: Ben Tossell Date: Mon, 16 Feb 2026 23:10:39 -0500 Subject: [PATCH 3/4] ci: test install.sh instead of manual setup in integration tests CI scripts now run install.sh with simulated input, then verify: - .env written with correct perms (600) and owner (hornet_agent) - Runtime deployed (start.sh, extensions) - Required secrets present in .env - All 5 test suites pass --- bin/ci/setup-arch.sh | 29 ++++++++++++++++++++++------- bin/ci/setup-ubuntu.sh | 29 ++++++++++++++++++++++------- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/bin/ci/setup-arch.sh b/bin/ci/setup-arch.sh index a21fd77..5b2aff2 100755 --- a/bin/ci/setup-arch.sh +++ b/bin/ci/setup-arch.sh @@ -1,14 +1,14 @@ #!/bin/bash # CI setup script for Arch Linux droplets. -# Runs as root on a fresh droplet. Installs prereqs, uploads source, -# runs setup.sh, and executes the test suite. +# Runs as root on a fresh droplet. Tests the interactive installer, +# then runs the test suite. # # Expects: /tmp/hornet-src.tar.gz already uploaded via scp. set -euo pipefail -echo "=== [Arch] Installing prerequisites ===" -pacman -Syu --noconfirm --needed git curl tmux iptables docker sudo 2>&1 | tail -10 +echo "=== [Arch] Installing git (needed to init test repo) ===" +pacman -Sy --noconfirm --needed git sudo 2>&1 | tail -3 echo "=== Preparing source ===" useradd -m -s /bin/bash hornet_admin @@ -18,9 +18,24 @@ tar xzf /tmp/hornet-src.tar.gz chown -R hornet_admin:hornet_admin /home/hornet_admin/ sudo -u hornet_admin bash -c 'cd ~/hornet && git init -q && git config user.email "ci@test" && git config user.name "CI" && git add -A && git commit -q -m "init"' -echo "=== Running setup.sh ===" -cd / -bash /home/hornet_admin/hornet/setup.sh hornet_admin +echo "=== Running install.sh ===" +# Simulate interactive input: admin user, required secrets, skip optionals, decline launch +printf 'hornet_admin\nsk-test-key\nghp_testtoken\nxoxb-test\nxapp-test\nU01TEST\n\n\n\n\n\nn\n' \ + | bash /home/hornet_admin/hornet/install.sh + +echo "=== Verifying install ===" +# .env exists with correct permissions +test -f /home/hornet_agent/.config/.env +test "$(stat -c '%a' /home/hornet_agent/.config/.env)" = "600" +test "$(stat -c '%U' /home/hornet_agent/.config/.env)" = "hornet_agent" +# Runtime deployed +test -f /home/hornet_agent/runtime/start.sh +test -d /home/hornet_agent/.pi/agent/extensions +# Required secrets written +grep -q "OPENCODE_ZEN_API_KEY=sk-test-key" /home/hornet_agent/.config/.env +grep -q "SLACK_BOT_TOKEN=xoxb-test" /home/hornet_agent/.config/.env +grep -q "HORNET_SOURCE_DIR=" /home/hornet_agent/.config/.env +echo " ✓ install.sh verification passed" echo "=== Installing test dependencies ===" export PATH="/home/hornet_agent/opt/node-v22.14.0-linux-x64/bin:$PATH" diff --git a/bin/ci/setup-ubuntu.sh b/bin/ci/setup-ubuntu.sh index a02f44c..cf66366 100755 --- a/bin/ci/setup-ubuntu.sh +++ b/bin/ci/setup-ubuntu.sh @@ -1,7 +1,7 @@ #!/bin/bash # CI setup script for Ubuntu droplets. -# Runs as root on a fresh droplet. Installs prereqs, uploads source, -# runs setup.sh, and executes the test suite. +# Runs as root on a fresh droplet. Tests the interactive installer, +# then runs the test suite. # # Expects: /tmp/hornet-src.tar.gz already uploaded via scp. @@ -17,9 +17,9 @@ for _ in $(seq 1 60); do sleep 2 done -echo "=== [Ubuntu] Installing prerequisites ===" +echo "=== [Ubuntu] Installing git (needed to init test repo) ===" apt-get update -qq -apt-get install -y -qq git curl tmux iptables docker.io 2>&1 | tail -3 +apt-get install -y -qq git 2>&1 | tail -1 echo "=== Preparing source ===" useradd -m -s /bin/bash hornet_admin @@ -29,9 +29,24 @@ tar xzf /tmp/hornet-src.tar.gz chown -R hornet_admin:hornet_admin /home/hornet_admin/ sudo -u hornet_admin bash -c 'cd ~/hornet && git init -q && git config user.email "ci@test" && git config user.name "CI" && git add -A && git commit -q -m "init"' -echo "=== Running setup.sh ===" -cd / -bash /home/hornet_admin/hornet/setup.sh hornet_admin +echo "=== Running install.sh ===" +# Simulate interactive input: admin user, required secrets, skip optionals, decline launch +printf 'hornet_admin\nsk-test-key\nghp_testtoken\nxoxb-test\nxapp-test\nU01TEST\n\n\n\n\n\nn\n' \ + | bash /home/hornet_admin/hornet/install.sh + +echo "=== Verifying install ===" +# .env exists with correct permissions +test -f /home/hornet_agent/.config/.env +test "$(stat -c '%a' /home/hornet_agent/.config/.env)" = "600" +test "$(stat -c '%U' /home/hornet_agent/.config/.env)" = "hornet_agent" +# Runtime deployed +test -f /home/hornet_agent/runtime/start.sh +test -d /home/hornet_agent/.pi/agent/extensions +# Required secrets written +grep -q "OPENCODE_ZEN_API_KEY=sk-test-key" /home/hornet_agent/.config/.env +grep -q "SLACK_BOT_TOKEN=xoxb-test" /home/hornet_agent/.config/.env +grep -q "HORNET_SOURCE_DIR=" /home/hornet_agent/.config/.env +echo " ✓ install.sh verification passed" echo "=== Installing test dependencies ===" export PATH="/home/hornet_agent/opt/node-v22.14.0-linux-x64/bin:$PATH" From 3232c550a490ef8e665f430fd6a016df97457aca Mon Sep 17 00:00:00 2001 From: Ben Tossell Date: Mon, 16 Feb 2026 23:14:14 -0500 Subject: [PATCH 4/4] docs: add droplet testing guide to AGENTS.md Practical instructions for using bin/ci/droplet.sh to spin up ephemeral DO droplets for manual or scripted testing. Covers create, SSH, upload, run, and cleanup. --- AGENTS.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index b20418c..1fe8c94 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -119,6 +119,38 @@ Add new test files to `bin/test.sh` — don't scatter test invocations across CI - **When changing behavior, update all docs.** Check and update: `README.md`, `CONFIGURATION.md`, skill files (`pi/skills/*/SKILL.md`), and `AGENTS.md`. Inline code examples in docs must match the actual implementation. - **No distro-specific commands.** Scripts must work on both Arch and Ubuntu (and any standard Linux). Use `grep -E` (not `grep -P`), POSIX-compatible tools, and avoid package manager calls (`pacman`, `apt`, etc.). If a package is needed, document it as a prerequisite rather than auto-installing it. +## Testing on Droplets + +Use `bin/ci/droplet.sh` to spin up ephemeral DigitalOcean droplets for testing setup, install, or shell changes on real Linux. Requires `DO_API_TOKEN` env var. + +```bash +# Generate a throwaway SSH key +ssh-keygen -t ed25519 -f /tmp/ci_key -N "" -q + +# Create a droplet (Ubuntu or Arch) +eval "$(bin/ci/droplet.sh create my-test ubuntu-24-04-x64 /tmp/ci_key.pub)" +# → sets DROPLET_ID, DROPLET_IP, SSH_KEY_ID + +# Or Arch (custom image): +eval "$(bin/ci/droplet.sh create my-test 217410218 /tmp/ci_key.pub)" + +# Wait for SSH, upload source, run a CI script +bin/ci/droplet.sh wait-ssh "$DROPLET_IP" /tmp/ci_key +tar czf /tmp/hornet-src.tar.gz --exclude=node_modules --exclude=.git . +scp -i /tmp/ci_key /tmp/hornet-src.tar.gz "root@$DROPLET_IP:/tmp/" +bin/ci/droplet.sh run "$DROPLET_IP" /tmp/ci_key bin/ci/setup-ubuntu.sh + +# Or SSH in for manual poking +ssh -i /tmp/ci_key "root@$DROPLET_IP" + +# Clean up when done (~$0.003/run) +bin/ci/droplet.sh destroy "$DROPLET_ID" "$SSH_KEY_ID" +``` + +Droplets take ~15s to create, ~10s for SSH, ~90s for full setup+tests. Always destroy after — they cost ~$0.003 per run but add up if forgotten. + +The CI scripts (`bin/ci/setup-ubuntu.sh`, `bin/ci/setup-arch.sh`) run `install.sh` with simulated input, verify the result, then run the full test suite. Use them as-is or SSH in and test manually. + ## Security Notes - `tool-guard.ts` blocks: writes outside `/home/hornet_agent/`, writes to source repo, writes to protected runtime files, dangerous bash patterns (reverse shells, fork bombs, rm -rf /, etc.), credential exfiltration.