diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index edc91a4..5f2834e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,11 @@ jobs: - name: Typecheck run: npm run typecheck + - name: ShellCheck + run: | + find bin/ setup.sh start.sh -type f \( -name '*.sh' -o -name 'hornet-safe-bash' -o -name 'hornet-docker' \) \ + | xargs shellcheck -s bash -S warning + test: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 0000000..5e15ea7 --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,83 @@ +name: Integration + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +concurrency: + group: integration-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +jobs: + integration: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - distro: ubuntu + image: ubuntu-24-04-x64 + setup_script: bin/ci/setup-ubuntu.sh + # To add Arch (or other distros), add entries here: + # - distro: arch + # image: arch-linux-x64 # or a custom snapshot ID + # setup_script: bin/ci/setup-arch.sh + + name: ${{ matrix.distro }} + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v4 + + - name: Generate ephemeral SSH key + run: | + mkdir -p ~/.ssh + ssh-keygen -t ed25519 -f ~/.ssh/ci_key -N "" -q + + - name: Create droplet + id: droplet + env: + DO_API_TOKEN: ${{ secrets.DO_API_TOKEN }} + run: | + output=$(bash bin/ci/droplet.sh create \ + "ci-${{ matrix.distro }}-${{ github.run_id }}" \ + "${{ matrix.image }}" \ + ~/.ssh/ci_key.pub) + echo "$output" >> "$GITHUB_OUTPUT" + echo "$output" + + - name: Wait for SSH + env: + DO_API_TOKEN: ${{ secrets.DO_API_TOKEN }} + run: | + bash bin/ci/droplet.sh wait-ssh \ + "${{ steps.droplet.outputs.DROPLET_IP }}" \ + ~/.ssh/ci_key + + - name: Upload source + run: | + tar czf /tmp/hornet-src.tar.gz \ + --exclude=node_modules --exclude=.git . + scp -o StrictHostKeyChecking=no -o BatchMode=yes \ + -i ~/.ssh/ci_key \ + /tmp/hornet-src.tar.gz \ + "root@${{ steps.droplet.outputs.DROPLET_IP }}:/tmp/hornet-src.tar.gz" + + - name: Setup and test + run: | + bash bin/ci/droplet.sh run \ + "${{ steps.droplet.outputs.DROPLET_IP }}" \ + ~/.ssh/ci_key \ + "${{ matrix.setup_script }}" + + - name: Cleanup + if: always() + env: + DO_API_TOKEN: ${{ secrets.DO_API_TOKEN }} + run: | + bash bin/ci/droplet.sh destroy \ + "${{ steps.droplet.outputs.DROPLET_ID }}" \ + "${{ steps.droplet.outputs.SSH_KEY_ID }}" diff --git a/.pi/todos/20e26efc.md b/.pi/todos/20e26efc.md new file mode 100644 index 0000000..189db36 --- /dev/null +++ b/.pi/todos/20e26efc.md @@ -0,0 +1,38 @@ +{ + "id": "20e26efc", + "title": "CI job: run setup + tests on fresh Ubuntu droplet per PR", + "tags": [ + "infra", + "ci", + "ubuntu" + ], + "status": "done", + "created_at": "2026-02-17T02:30:52.375Z" +} + +## Result +Implemented and verified end-to-end. Workflow will activate once PR merges to main (GH Actions limitation: new workflow files in PRs don't trigger until they exist on the base branch). + +### What was built +- **`bin/ci/droplet.sh`** — reusable DO droplet lifecycle: `create`, `destroy`, `wait-ssh`, `run` +- **`bin/ci/setup-ubuntu.sh`** — Ubuntu-specific: prereqs, setup.sh, test suite +- **`.github/workflows/integration.yml`** — matrix-based workflow (Ubuntu now, extensible) + +### Design +- **Ephemeral everything**: fresh droplet + SSH key generated per run, destroyed on cleanup +- **Single secret**: `DO_API_TOKEN` (set in GitHub repo) +- **Matrix**: `include` array with `distro`, `image`, `setup_script` — add Arch/other by adding entries +- **Concurrency group**: one run at a time per PR +- **Always cleanup**: `if: always()` destroys droplet + SSH key even on failure + +### Local test results (3 full cycles) +- Create droplet: ~15s +- SSH ready: ~10s +- Prereqs + setup + deploy: ~90s +- Tests (5 suites): ~10s +- Destroy: instant +- **Total: ~2 minutes per run** +- All 5 test suites pass on fresh Ubuntu 24.04 + +### PR +https://github.com/modem-dev/hornet/pull/10 diff --git a/.pi/todos/cb931656.md b/.pi/todos/cb931656.md new file mode 100644 index 0000000..368293f --- /dev/null +++ b/.pi/todos/cb931656.md @@ -0,0 +1,32 @@ +{ + "id": "cb931656", + "title": "Verify hornet setup on Ubuntu droplet (manual)", + "tags": [ + "infra", + "ubuntu", + "ci" + ], + "status": "done", + "created_at": "2026-02-17T02:30:39.055Z" +} + +## Result +Verified on DigitalOcean Ubuntu 24.04 droplet (4GB RAM, 2 vCPU). + +### Bugs found and fixed +1. **`setup.sh` — shared repo permissions**: ran `git config` as `hornet_agent` on admin's repo → agent can't access admin home. Fix: run as `$ADMIN_USER` instead. +2. **`setup.sh` — harden-permissions path**: called source copy (`$REPO_DIR/bin/harden-permissions.sh`) which agent can't access. Fix: use deployed copy (`$HORNET_HOME/runtime/bin/`). +3. **`setup.sh` — CWD inheritance**: `sudo -u hornet_agent` inherits root's CWD, causing `find`/`git` failures. Fix: `cd /tmp` at top of script. +4. **`harden-permissions.sh` — sessions dir**: `find` on non-existent sessions dir fails under `set -e`. Fix: wrap in `if [ -d ... ]` guard. +5. **`hornet-safe-bash` — `grep -P`**: Perl regex not reliably available on Ubuntu. Fix: all patterns converted to `grep -E`. + +### Verification results +- ✅ `setup.sh` completes cleanly (user, Node, pi, firewall, hidepid, deploy, harden) +- ✅ All 5 test suites pass (tool-guard, bridge security, extension scanner, safe-bash, log redaction) +- ✅ Varlock validates env schema +- ✅ `start.sh` boots pi agent (fails at API auth with dummy keys — expected) +- ✅ Firewall active with all rules +- ✅ Process isolation working + +### PR +https://github.com/modem-dev/hornet/pull/10 diff --git a/AGENTS.md b/AGENTS.md index 37be779..6d3a06b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,6 +14,9 @@ bin/ security & operations scripts harden-permissions.sh filesystem hardening (runs on boot) scan-extensions.mjs extension static analysis redact-logs.sh secret scrubber for session logs + ci/ CI integration scripts + droplet.sh ephemeral DigitalOcean droplet lifecycle (create/destroy/ssh) + setup-ubuntu.sh Ubuntu droplet: prereqs + setup + tests hooks/ pre-commit blocks agent from modifying security files in git pi/ @@ -110,6 +113,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/ci/droplet.sh b/bin/ci/droplet.sh new file mode 100755 index 0000000..0b0305d --- /dev/null +++ b/bin/ci/droplet.sh @@ -0,0 +1,175 @@ +#!/bin/bash +# Manage ephemeral DigitalOcean droplets for CI. +# +# Usage: +# bin/ci/droplet.sh create +# bin/ci/droplet.sh destroy [ssh_key_id] +# bin/ci/droplet.sh wait-ssh +# bin/ci/droplet.sh run