From 10af489ba4552e21d260bd4bfea9b5e5783ce139 Mon Sep 17 00:00:00 2001 From: Ben Tossell Date: Mon, 16 Feb 2026 21:30:17 -0500 Subject: [PATCH] =?UTF-8?q?config:=20support=20Ubuntu=20=E2=80=94=20replac?= =?UTF-8?q?e=20grep=20-P=20with=20grep=20-E,=20add=20distro-agnostic=20gui?= =?UTF-8?q?deline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pi/todos/20e26efc.md | 60 +++++++++++++++++++++++++++++++++++++++ .pi/todos/cb931656.md | 37 ++++++++++++++++++++++++ AGENTS.md | 1 + bin/harden-permissions.sh | 6 ++-- bin/hornet-safe-bash | 36 +++++++++++++---------- setup.sh | 19 +++++++++++-- 6 files changed, 139 insertions(+), 20 deletions(-) create mode 100644 .pi/todos/20e26efc.md create mode 100644 .pi/todos/cb931656.md diff --git a/.pi/todos/20e26efc.md b/.pi/todos/20e26efc.md new file mode 100644 index 0000000..91d42e6 --- /dev/null +++ b/.pi/todos/20e26efc.md @@ -0,0 +1,60 @@ +{ + "id": "20e26efc", + "title": "CI job: run setup + tests on Ubuntu droplet per PR", + "tags": [ + "infra", + "ci", + "ubuntu" + ], + "status": "todo", + "created_at": "2026-02-17T02:30:52.375Z" +} + +## Goal +Add a GitHub Actions workflow that SSHes into the DigitalOcean droplet and runs the full hornet setup + test suite on every PR. + +## Depends on +- TODO-cb931656 (manual verification must pass first) + +## Design decisions needed +- **Fresh state per run**: `uninstall.sh` at start of each run? Or snapshot/restore? Or ephemeral droplet via DO API? +- **Secrets**: droplet IP + SSH key stored as GitHub Actions secrets (`DROPLET_IP`, `DROPLET_SSH_KEY`) +- **Concurrency**: only one CI run at a time on the droplet (use GitHub concurrency group) +- **Scope**: full setup + test, or just test.sh (setup is slow, ~2-3 min)? + +## Proposed workflow +```yaml +name: Integration (Ubuntu) +on: + pull_request: + branches: [main] + +concurrency: + group: droplet-integration + cancel-in-progress: true + +jobs: + integration: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run on droplet + env: + DROPLET_IP: ${{ secrets.DROPLET_IP }} + SSH_KEY: ${{ secrets.DROPLET_SSH_KEY }} + run: | + # SSH into droplet, rsync repo, run uninstall (clean slate), + # run setup.sh, deploy.sh, test.sh, security-audit.sh +``` + +## Steps +1. Create SSH key pair for CI, add public key to droplet +2. Add `DROPLET_IP` and `DROPLET_SSH_KEY` as GitHub repo secrets +3. Write the workflow file (`.github/workflows/integration.yml`) +4. Handle cleanup: uninstall.sh at start of run for clean state +5. Fail the PR if any step exits non-zero +6. Consider: also run security-audit.sh (some checks need live system) + +## Open questions +- Do we want to spin up/destroy droplets per run (more isolated, costs more) or reuse one? +- Should we test `start.sh` actually booting an agent, or just setup + unit tests? diff --git a/.pi/todos/cb931656.md b/.pi/todos/cb931656.md new file mode 100644 index 0000000..c34efbb --- /dev/null +++ b/.pi/todos/cb931656.md @@ -0,0 +1,37 @@ +{ + "id": "cb931656", + "title": "Verify hornet setup on Ubuntu droplet (manual)", + "tags": [ + "infra", + "ubuntu", + "ci" + ], + "status": "todo", + "created_at": "2026-02-17T02:30:39.055Z", + "assigned_to_session": "381813d9-c69a-4472-9a00-e232ffb746d1" +} + +## Goal +SSH into the DigitalOcean Ubuntu droplet and manually verify the full hornet setup works end-to-end. + +## Steps +1. SSH into the box as root +2. Install prerequisites: `git`, `curl`, `docker`, `iptables`, `tmux` +3. Clone the hornet repo +4. Run `setup.sh ` — creates `hornet_agent` user, installs Node, pi, firewall, etc. +5. Create a minimal `.env` with dummy/test values (enough to pass varlock validation) +6. Run `bin/deploy.sh` — deploy extensions, skills, bridge to runtime +7. Run `bin/test.sh` — all 207 tests must pass +8. Run `bin/security-audit.sh` — verify firewall, perms, proc isolation +9. Boot the agent: `sudo -u hornet_agent ~/runtime/start.sh` — verify it starts without errors +10. Tear down: run `bin/uninstall.sh` to verify clean removal + +## Success criteria +- `setup.sh` completes without errors on Ubuntu +- All tests pass +- Security audit is clean +- Agent boots and varlock validates the env + +## Notes +- Need droplet IP, root credentials (store securely, don't commit) +- This is a one-time manual run; the CI todo automates it afterward diff --git a/AGENTS.md b/AGENTS.md index 37be779..4419597 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -110,6 +110,7 @@ Add new test files to `bin/test.sh` — don't scatter test invocations across CI - Skills are deployed from `pi/skills/` → agent's `~/.pi/agent/skills/`. - Agent commits operational learnings to its own skills dir (not back to source). - **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. ## Security Notes diff --git a/bin/harden-permissions.sh b/bin/harden-permissions.sh index 26ddf4c..6a492db 100755 --- a/bin/harden-permissions.sh +++ b/bin/harden-permissions.sh @@ -51,9 +51,11 @@ if [ -d "$HOME/.pi/agent/sessions" ]; then fi # Session logs (full conversation history) -find "$HOME/.pi/agent/sessions" -name '*.jsonl' -not -perm 600 -exec chmod 600 {} + 2>/dev/null && \ - count=$(find "$HOME/.pi/agent/sessions" -name '*.jsonl' 2>/dev/null | wc -l) && \ +if [ -d "$HOME/.pi/agent/sessions" ]; then + find "$HOME/.pi/agent/sessions" -name '*.jsonl' -not -perm 600 -exec chmod 600 {} + 2>/dev/null || true + count=$(find "$HOME/.pi/agent/sessions" -name '*.jsonl' 2>/dev/null | wc -l) [ "$count" -gt 0 ] && echo " ✓ $count session log(s) → 600" +fi # Pi settings fix_file "$HOME/.pi/agent/settings.json" "600" diff --git a/bin/hornet-safe-bash b/bin/hornet-safe-bash index 5ede7f6..e255e1a 100755 --- a/bin/hornet-safe-bash +++ b/bin/hornet-safe-bash @@ -5,6 +5,9 @@ # # This is defense-in-depth — the agent's instructions also prohibit these, # but a successful injection might override soft instructions. +# +# NOTE: Avoid grep -P (Perl regex) — not available on all distros. +# Use grep -E (extended regex) or awk instead. # Patterns that should NEVER be executed by the agent COMMAND="$*" @@ -16,64 +19,67 @@ block() { } # Fork bomb -if echo "$COMMAND" | grep -qP ':\(\)\s*\{.*\|.*&.*\}'; then +if echo "$COMMAND" | grep -qE ':\(\)[[:space:]]*\{.*\|.*&.*\}'; then block "fork bomb" fi # rm -rf / or rm -rf /* (root filesystem deletion) -if echo "$COMMAND" | grep -qP 'rm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+)?(-[a-zA-Z]*r[a-zA-Z]*\s+)?(\/\s*$|\/\*|\/\s+)'; then +if echo "$COMMAND" | grep -qE 'rm[[:space:]]+(-[a-zA-Z]*f[a-zA-Z]*[[:space:]]+)?(-[a-zA-Z]*r[a-zA-Z]*[[:space:]]+)?(/[[:space:]]*$|/\*|/[[:space:]]+)'; then block "recursive delete of root filesystem" fi -if echo "$COMMAND" | grep -qP 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+)?(-[a-zA-Z]*f[a-zA-Z]*\s+)?(\/\s*$|\/\*|\/\s+)'; then +if echo "$COMMAND" | grep -qE 'rm[[:space:]]+(-[a-zA-Z]*r[a-zA-Z]*[[:space:]]+)?(-[a-zA-Z]*f[a-zA-Z]*[[:space:]]+)?(/[[:space:]]*$|/\*|/[[:space:]]+)'; then block "recursive delete of root filesystem" fi # dd writing to block devices -if echo "$COMMAND" | grep -qP 'dd\s+.*of=/dev/(sd|vd|nvme|xvd)'; then +if echo "$COMMAND" | grep -qE 'dd[[:space:]]+.*of=/dev/(sd|vd|nvme|xvd)'; then block "dd write to block device" fi # mkfs on block devices -if echo "$COMMAND" | grep -qP 'mkfs\b.*\/dev\/'; then +if echo "$COMMAND" | grep -qE 'mkfs[^a-zA-Z].*/dev/'; then block "mkfs on block device" fi # chmod 777 on sensitive paths -if echo "$COMMAND" | grep -qP 'chmod\s+(-[a-zA-Z]*\s+)?777\s+(\/|\/etc|\/home|\/root|\/var)'; then +if echo "$COMMAND" | grep -qE 'chmod[[:space:]]+(-[a-zA-Z]*[[:space:]]+)?777[[:space:]]+(/|/etc|/home|/root|/var)'; then block "chmod 777 on sensitive path" fi # Curl/wget piped to shell -if echo "$COMMAND" | grep -qP '(curl|wget)\s+.*\|\s*(ba)?sh'; then +if echo "$COMMAND" | grep -qE '(curl|wget)[[:space:]]+.*\|[[:space:]]*(ba)?sh'; then block "piping download to shell" fi # Reverse shell patterns -if echo "$COMMAND" | grep -qP 'bash\s+-i\s+>(&|\|)\s*/dev/tcp/'; then +if echo "$COMMAND" | grep -qE 'bash[[:space:]]+-i[[:space:]]+>[&|][[:space:]]*/dev/tcp/'; then block "reverse shell (bash /dev/tcp)" fi -if echo "$COMMAND" | grep -qP 'nc\s+(-[a-zA-Z]*\s+)*[0-9]+.*-e\s*(\/bin\/)?(ba)?sh'; then +if echo "$COMMAND" | grep -qE 'nc[[:space:]]+(-[a-zA-Z]*[[:space:]]+)*[0-9]+.*-e[[:space:]]*(\/bin\/)?(ba)?sh'; then block "reverse shell (netcat)" fi -if echo "$COMMAND" | grep -qP 'python[23]?\s+-c.*socket.*connect.*subprocess'; then +if echo "$COMMAND" | grep -qE 'python[23]?[[:space:]]+-c.*socket.*connect.*subprocess'; then block "reverse shell (python)" fi # crontab modification (persistence) -if echo "$COMMAND" | grep -qP '(crontab\s+-[erl]|echo.*>\s*/etc/cron)'; then +if echo "$COMMAND" | grep -qE '(crontab[[:space:]]+-[erl]|echo.*>[[:space:]]*/etc/cron)'; then block "crontab modification" fi # Modifying /etc/passwd or /etc/shadow -if echo "$COMMAND" | grep -qP '>\s*/etc/(passwd|shadow|sudoers)'; then +if echo "$COMMAND" | grep -qE '>[[:space:]]*/etc/(passwd|shadow|sudoers)'; then block "write to system auth files" fi # SSH key injection to other users -if echo "$COMMAND" | grep -qP '>\s*/home/(?!hornet_agent).*/\.ssh/authorized_keys'; then - block "SSH key injection to another user" +# Can't use negative lookahead without grep -P, so match broadly then exclude our user +if echo "$COMMAND" | grep -qE '>[[:space:]]*/home/.*/.ssh/authorized_keys'; then + if ! echo "$COMMAND" | grep -qE '>[[:space:]]*/home/hornet_agent/.ssh/authorized_keys'; then + block "SSH key injection to another user" + fi fi -if echo "$COMMAND" | grep -qP '>\s*/root/\.ssh/authorized_keys'; then +if echo "$COMMAND" | grep -qE '>[[:space:]]*/root/.ssh/authorized_keys'; then block "SSH key injection to root" fi diff --git a/setup.sh b/setup.sh index 6e0dd51..2db7e29 100755 --- a/setup.sh +++ b/setup.sh @@ -3,7 +3,7 @@ # Run as root or with sudo from the admin user account # # Prerequisites: -# - Arch Linux (or similar) +# - Linux (tested on Arch and Ubuntu) # - Docker installed # # This script: @@ -35,6 +35,10 @@ HORNET_HOME="/home/hornet_agent" REPO_DIR="$(cd "$(dirname "$0")" && pwd)" NODE_VERSION="22.14.0" +# Work from a neutral directory — sudo -u hornet_agent inherits CWD, and +# git/find fail if CWD is a directory the agent can't access (e.g. /root). +cd /tmp + echo "=== Creating hornet_agent user ===" if id hornet_agent &>/dev/null; then echo "User already exists, skipping" @@ -103,7 +107,16 @@ echo "=== Configuring shared repo permissions ===" # Set core.sharedRepository=group on all repos so git creates objects # with group-write perms. Without this, umask 077 in start.sh causes # new .git/objects to be owner-only, breaking group access (admin user). -for repo in "$REPO_DIR" "$HORNET_HOME/workspace/modem" "$HORNET_HOME/workspace/website"; do + +# Source repo — set as admin user (agent can't access admin home, and root +# needs safe.directory due to different ownership) +if [ -d "$REPO_DIR/.git" ]; then + sudo -u "$ADMIN_USER" git -C "$REPO_DIR" config core.sharedRepository group + echo " ✓ $REPO_DIR" +fi + +# Agent workspace repos — set as agent +for repo in "$HORNET_HOME/workspace/modem" "$HORNET_HOME/workspace/website"; do if [ -d "$repo/.git" ]; then sudo -u hornet_agent git -C "$repo" config core.sharedRepository group echo " ✓ $repo" @@ -232,7 +245,7 @@ fi echo "Process isolation: hornet_agent can only see its own processes" echo "=== Hardening permissions ===" -sudo -u hornet_agent bash -c "'$REPO_DIR/bin/harden-permissions.sh'" +sudo -u hornet_agent bash -c "cd ~ && '$HORNET_HOME/runtime/bin/harden-permissions.sh'" echo "" echo "=== Setup complete ==="