Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .pi/todos/20e26efc.md
Original file line number Diff line number Diff line change
@@ -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?
37 changes: 37 additions & 0 deletions .pi/todos/cb931656.md
Original file line number Diff line number Diff line change
@@ -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 <admin_user>` — 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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 4 additions & 2 deletions bin/harden-permissions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +54 to +58

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The use of || true in harden-permissions.sh silences errors from the chmod command, potentially leaving sensitive session logs with incorrect permissions without any failure indication.
Severity: HIGH

Suggested Fix

Remove the || true to allow the script to fail correctly if the chmod command returns a non-zero exit code. The script should exit with an error if it cannot successfully harden the permissions on the session log files, ensuring that failures are not silent.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: bin/harden-permissions.sh#L54-L58

Potential issue: The `harden-permissions.sh` script uses `|| true` after a `find ...
-exec chmod 600 {} +` command. This masks any failure from the `chmod` operation. If
`chmod` fails to set the correct permissions on sensitive session logs (e.g., due to
ownership or permission errors), the script will not report an error and will continue
execution. This can lead to a silent security failure where session logs containing full
conversation histories remain group-readable, despite the script appearing to have
succeeded. The subsequent log message "✓ $count session log(s) → 600" can give a false
sense of security.


# Pi settings
fix_file "$HOME/.pi/agent/settings.json" "600"
Expand Down
36 changes: 21 additions & 15 deletions bin/hornet-safe-bash
Original file line number Diff line number Diff line change
Expand Up @@ -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="$*"
Expand All @@ -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
Comment thread
sentry[bot] marked this conversation as resolved.
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

Expand Down
19 changes: 16 additions & 3 deletions setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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 ==="
Expand Down