diff --git a/.github/workflows/deploy-installers.yml b/.github/workflows/deploy-installers.yml deleted file mode 100644 index cfed8eb..0000000 --- a/.github/workflows/deploy-installers.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: Deploy Installer Worker - -on: - push: - branches: [main] - paths: - - ".github/workflows/deploy-installers.yml" - - "docs/install.sh" - - "docs/install-hermes.sh" - - "scripts/cloudflare-installer-worker.js" - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: secopsai-installer-worker-production - cancel-in-progress: false - -jobs: - deploy: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - - - name: Set up Node.js 22 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 - with: - node-version: "22" - - - name: Resolve existing installer Worker - id: worker - env: - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - run: | - set -euo pipefail - zone_response=$(curl -fsSL \ - -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \ - "https://api.cloudflare.com/client/v4/zones?name=secopsai.dev&status=active") - zone_id=$(printf '%s' "$zone_response" | jq -er '.result[0].id') - routes_response=$(curl -fsSL \ - -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \ - "https://api.cloudflare.com/client/v4/zones/${zone_id}/workers/routes") - worker_name=$(printf '%s' "$routes_response" | jq -r \ - '.result[] | select(.pattern == "secopsai.dev/install.sh*" or .pattern == "secopsai.dev/install.sh") | .script' \ - | head -n 1) - if [ -z "$worker_name" ] || [ "$worker_name" = "null" ]; then - worker_name="secopsai-installer-router" - fi - echo "name=$worker_name" >> "$GITHUB_OUTPUT" - - - name: Deploy installer Worker and routes - env: - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - run: | - npx --yes wrangler@4.114.0 deploy scripts/cloudflare-installer-worker.js \ - --name "${{ steps.worker.outputs.name }}" \ - --compatibility-date 2026-07-25 \ - --route "secopsai.dev/install.sh*" \ - --route "secopsai.dev/install-hermes.sh*" - - - name: Verify public installers - run: | - set -euo pipefail - for installer in install.sh install-hermes.sh; do - first_line=$(curl -fsSL --retry 5 --retry-delay 5 \ - "https://secopsai.dev/${installer}" | sed -n '1p') - if [ "$first_line" != "#!/bin/sh" ]; then - echo "${installer} did not return a POSIX shell installer" >&2 - exit 1 - fi - done diff --git a/.github/workflows/test-and-build.yml b/.github/workflows/test-and-build.yml index a8409a9..fdfcaa0 100644 --- a/.github/workflows/test-and-build.yml +++ b/.github/workflows/test-and-build.yml @@ -98,6 +98,10 @@ jobs: sh -n docs/install.sh sh -n docs/install-hermes.sh cmp website/index.html www/index.html + cmp docs/install.sh website/install.sh + cmp docs/install.sh www/install.sh + cmp docs/install-hermes.sh website/install-hermes.sh + cmp docs/install-hermes.sh www/install-hermes.sh grep -F 'data-tab="hermes"' website/index.html grep -F 'https://secopsai.dev/install-hermes.sh' website/index.html diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md index e24773b..ac1bbcc 100644 --- a/docs/deployment-guide.md +++ b/docs/deployment-guide.md @@ -55,18 +55,30 @@ You should see HTTP 200. ### Step 3: Point secopsai.dev to the deployment -You have two simple options. +The production site publishes installer files directly from both tracked static +site roots. CI verifies that these files are byte-identical to the canonical +copies under `docs/`: -Option A (recommended): make `secopsai.dev` itself serve this Pages project. +- `website/install.sh` and `www/install.sh` +- `website/install-hermes.sh` and `www/install-hermes.sh` + +This is the recommended path because the existing Cloudflare Pages Git +deployments require no separate Worker credential or manual route update. + +You have two fallback options if the apex site is moved away from these Pages +projects. + +Option A: make `secopsai.dev` itself serve this Pages project. 1. In Pages project settings, add custom domain `secopsai.dev`. 2. Let Cloudflare create/update DNS records automatically. 3. Wait for SSL status to become Active. -Now this should work directly: +Now both commands should work directly: ```bash curl -fsSLI https://secopsai.dev/install.sh +curl -fsSLI https://secopsai.dev/install-hermes.sh ``` Option B: keep docs on `docs.secopsai.dev` and route only installer paths from apex domain. @@ -91,22 +103,12 @@ If you prefer users to stay on `secopsai.dev` without redirect, use the Worker s - `scripts/cloudflare-installer-worker.js` -Production deployment is automated by `.github/workflows/deploy-installers.yml`. -It discovers the Worker already bound to the standard installer, publishes the -reviewed script with the repository's Cloudflare secrets, manages both routes, -and verifies that each public endpoint returns a shell script. - -Required GitHub repository secrets: - -- `CLOUDFLARE_ACCOUNT_ID` -- `CLOUDFLARE_API_TOKEN` with Workers Scripts edit and Zone Workers Routes edit/read access - -The managed routes are: +The Worker source supports both routes: - `secopsai.dev/install.sh*` - `secopsai.dev/install-hermes.sh*` -Manual recovery: +Worker setup or recovery: 1. Cloudflare Dashboard -> Workers & Pages -> Create Worker. 2. Paste script from `scripts/cloudflare-installer-worker.js`. diff --git a/tests/test_hermes_installers.py b/tests/test_hermes_installers.py index 84f16f5..dfb1c4c 100644 --- a/tests/test_hermes_installers.py +++ b/tests/test_hermes_installers.py @@ -33,16 +33,16 @@ def test_shell_installers_are_syntax_valid() -> None: def test_public_hermes_installer_and_worker_route_are_wired() -> None: installer = (ROOT / "docs" / "install-hermes.sh").read_text(encoding="utf-8") worker = (ROOT / "scripts" / "cloudflare-installer-worker.js").read_text(encoding="utf-8") - deployment = (ROOT / ".github" / "workflows" / "deploy-installers.yml").read_text(encoding="utf-8") assert 'MIN_VERSION="0.18.2"' in installer assert 'PLUGIN="Techris93/secopsai/integrations/hermes"' in installer assert "hermes plugins install" in installer assert "hermes service install" in installer assert '"/install-hermes.sh": "https://docs.secopsai.dev/install-hermes.sh"' in worker - assert 'wrangler@4.114.0 deploy scripts/cloudflare-installer-worker.js' in deployment - assert '--route "secopsai.dev/install.sh*"' in deployment - assert '--route "secopsai.dev/install-hermes.sh*"' in deployment - assert "CLOUDFLARE_API_TOKEN" in deployment + assert (ROOT / "website" / "install-hermes.sh").read_text(encoding="utf-8") == installer + assert (ROOT / "www" / "install-hermes.sh").read_text(encoding="utf-8") == installer + standard = (ROOT / "docs" / "install.sh").read_text(encoding="utf-8") + assert (ROOT / "website" / "install.sh").read_text(encoding="utf-8") == standard + assert (ROOT / "www" / "install.sh").read_text(encoding="utf-8") == standard def test_tracked_website_copies_are_identical_and_contain_hermes_tab() -> None: diff --git a/website/install-hermes.sh b/website/install-hermes.sh new file mode 100644 index 0000000..c77a48c --- /dev/null +++ b/website/install-hermes.sh @@ -0,0 +1,88 @@ +#!/bin/sh +set -eu + +MIN_VERSION="0.18.2" +CORE_HOME="${SECOPSAI_HOME:-"$HOME/secopsai"}" +HERMES_DATA_HOME="${HERMES_HOME:-"$HOME/.hermes"}" +INTERVAL="${SECOPSAI_HERMES_REFRESH_SECONDS:-300}" +PLUGIN="Techris93/secopsai/integrations/hermes" + +fail() { + echo "Error: $*" >&2 + exit 1 +} + +for command in hermes git bash python3 curl; do + command -v "$command" >/dev/null 2>&1 || fail "$command is required" +done + +python3 -m pip --version >/dev/null 2>&1 || fail "python3 -m pip is required" + +version_output=$(hermes --version 2>&1 | sed -n '1p') +version=$(printf '%s\n' "$version_output" | sed -n 's/.*Hermes Agent v\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/p') +[ -n "$version" ] || fail "could not determine the Hermes Agent version" + +if ! python3 - "$version" "$MIN_VERSION" <<'PY' +import sys + +def version(value): + return tuple(int(part) for part in value.split(".")) + +raise SystemExit(0 if version(sys.argv[1]) >= version(sys.argv[2]) else 1) +PY +then + fail "Hermes Agent $MIN_VERSION or later is required (found $version)" +fi + +[ -d "$HERMES_DATA_HOME" ] || fail "Hermes home not found: $HERMES_DATA_HOME" + +temporary=$(mktemp "${TMPDIR:-/tmp}/secopsai-hermes.XXXXXX") +trap 'rm -f "$temporary"' EXIT HUP INT TERM + +echo "Installing SecOpsAI Core for Hermes Agent..." +curl -fsSL https://secopsai.dev/install.sh -o "$temporary" +SECOPSAI_HOME="$CORE_HOME" SECOPSAI_SETUP_PROFILE=hermes bash "$temporary" + +PYTHON="$CORE_HOME/.venv/bin/python" +[ -x "$PYTHON" ] || fail "SecOpsAI virtual environment was not created: $PYTHON" + +echo "Installing the read-only SecOpsAI Hermes plugin..." +hermes plugins install "$PLUGIN" --force --enable + +echo "Running the first bounded Hermes telemetry refresh..." +HERMES_HOME="$HERMES_DATA_HOME" "$PYTHON" -m secopsai.cli --json hermes refresh --hermes-home "$HERMES_DATA_HOME" + +echo "Installing persistent monitoring..." +HERMES_HOME="$HERMES_DATA_HOME" "$PYTHON" -m secopsai.cli --json hermes service install \ + --hermes-home "$HERMES_DATA_HOME" \ + --interval "$INTERVAL" + +echo "Verifying the complete integration..." +HERMES_HOME="$HERMES_DATA_HOME" "$PYTHON" -m secopsai.cli --json hermes doctor --hermes-home "$HERMES_DATA_HOME" + +cat < (default: pinned immutable commit) +# SECOPSAI_HOME= (default: $HOME/secopsai) +# +# This script: +# 1. Clones or updates the secopsai repo at SECOPSAI_HOME +# 2. Checks out the requested ref +# 3. Runs setup.sh from the repo root (non-interactive-safe by default) + +INSTALL_REF="${SECOPSAI_INSTALL_REF:-v1.0.0}" +REPO_URL="https://github.com/Techris93/secopsai.git" +REPO_DIR="${SECOPSAI_HOME:-"$HOME/secopsai"}" +SETUP_PROFILE="${SECOPSAI_SETUP_PROFILE:-default}" + +if ! command -v git >/dev/null 2>&1; then + echo "Error: git is required to install secopsai." >&2 + exit 1 +fi + +if ! command -v bash >/dev/null 2>&1; then + echo "Error: bash is required to install secopsai." >&2 + exit 1 +fi + +# Clone or update repo +if [ ! -d "$REPO_DIR/.git" ]; then + echo "Cloning secopsai into $REPO_DIR..." + git clone "$REPO_URL" "$REPO_DIR" +else + echo "Using existing secopsai checkout at $REPO_DIR..." +fi + +cd "$REPO_DIR" + +if [ -n "$(git status --porcelain --untracked-files=no)" ]; then + echo "Error: the existing SecOpsAI checkout has local tracked changes." >&2 + echo "Commit or preserve those changes, or set SECOPSAI_HOME to a dedicated install directory." >&2 + exit 1 +fi + +echo "Checking out $INSTALL_REF..." +git fetch --tags origin >/dev/null 2>&1 +git checkout --detach "$INSTALL_REF" >/dev/null 2>&1 + +echo "Running setup.sh..." +exec bash setup.sh --non-interactive --profile "$SETUP_PROFILE" diff --git a/www/install-hermes.sh b/www/install-hermes.sh new file mode 100644 index 0000000..c77a48c --- /dev/null +++ b/www/install-hermes.sh @@ -0,0 +1,88 @@ +#!/bin/sh +set -eu + +MIN_VERSION="0.18.2" +CORE_HOME="${SECOPSAI_HOME:-"$HOME/secopsai"}" +HERMES_DATA_HOME="${HERMES_HOME:-"$HOME/.hermes"}" +INTERVAL="${SECOPSAI_HERMES_REFRESH_SECONDS:-300}" +PLUGIN="Techris93/secopsai/integrations/hermes" + +fail() { + echo "Error: $*" >&2 + exit 1 +} + +for command in hermes git bash python3 curl; do + command -v "$command" >/dev/null 2>&1 || fail "$command is required" +done + +python3 -m pip --version >/dev/null 2>&1 || fail "python3 -m pip is required" + +version_output=$(hermes --version 2>&1 | sed -n '1p') +version=$(printf '%s\n' "$version_output" | sed -n 's/.*Hermes Agent v\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/p') +[ -n "$version" ] || fail "could not determine the Hermes Agent version" + +if ! python3 - "$version" "$MIN_VERSION" <<'PY' +import sys + +def version(value): + return tuple(int(part) for part in value.split(".")) + +raise SystemExit(0 if version(sys.argv[1]) >= version(sys.argv[2]) else 1) +PY +then + fail "Hermes Agent $MIN_VERSION or later is required (found $version)" +fi + +[ -d "$HERMES_DATA_HOME" ] || fail "Hermes home not found: $HERMES_DATA_HOME" + +temporary=$(mktemp "${TMPDIR:-/tmp}/secopsai-hermes.XXXXXX") +trap 'rm -f "$temporary"' EXIT HUP INT TERM + +echo "Installing SecOpsAI Core for Hermes Agent..." +curl -fsSL https://secopsai.dev/install.sh -o "$temporary" +SECOPSAI_HOME="$CORE_HOME" SECOPSAI_SETUP_PROFILE=hermes bash "$temporary" + +PYTHON="$CORE_HOME/.venv/bin/python" +[ -x "$PYTHON" ] || fail "SecOpsAI virtual environment was not created: $PYTHON" + +echo "Installing the read-only SecOpsAI Hermes plugin..." +hermes plugins install "$PLUGIN" --force --enable + +echo "Running the first bounded Hermes telemetry refresh..." +HERMES_HOME="$HERMES_DATA_HOME" "$PYTHON" -m secopsai.cli --json hermes refresh --hermes-home "$HERMES_DATA_HOME" + +echo "Installing persistent monitoring..." +HERMES_HOME="$HERMES_DATA_HOME" "$PYTHON" -m secopsai.cli --json hermes service install \ + --hermes-home "$HERMES_DATA_HOME" \ + --interval "$INTERVAL" + +echo "Verifying the complete integration..." +HERMES_HOME="$HERMES_DATA_HOME" "$PYTHON" -m secopsai.cli --json hermes doctor --hermes-home "$HERMES_DATA_HOME" + +cat < (default: pinned immutable commit) +# SECOPSAI_HOME= (default: $HOME/secopsai) +# +# This script: +# 1. Clones or updates the secopsai repo at SECOPSAI_HOME +# 2. Checks out the requested ref +# 3. Runs setup.sh from the repo root (non-interactive-safe by default) + +INSTALL_REF="${SECOPSAI_INSTALL_REF:-v1.0.0}" +REPO_URL="https://github.com/Techris93/secopsai.git" +REPO_DIR="${SECOPSAI_HOME:-"$HOME/secopsai"}" +SETUP_PROFILE="${SECOPSAI_SETUP_PROFILE:-default}" + +if ! command -v git >/dev/null 2>&1; then + echo "Error: git is required to install secopsai." >&2 + exit 1 +fi + +if ! command -v bash >/dev/null 2>&1; then + echo "Error: bash is required to install secopsai." >&2 + exit 1 +fi + +# Clone or update repo +if [ ! -d "$REPO_DIR/.git" ]; then + echo "Cloning secopsai into $REPO_DIR..." + git clone "$REPO_URL" "$REPO_DIR" +else + echo "Using existing secopsai checkout at $REPO_DIR..." +fi + +cd "$REPO_DIR" + +if [ -n "$(git status --porcelain --untracked-files=no)" ]; then + echo "Error: the existing SecOpsAI checkout has local tracked changes." >&2 + echo "Commit or preserve those changes, or set SECOPSAI_HOME to a dedicated install directory." >&2 + exit 1 +fi + +echo "Checking out $INSTALL_REF..." +git fetch --tags origin >/dev/null 2>&1 +git checkout --detach "$INSTALL_REF" >/dev/null 2>&1 + +echo "Running setup.sh..." +exec bash setup.sh --non-interactive --profile "$SETUP_PROFILE"