From 900666a377bcadbd0e906e32f9993f7ff18e18ee Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Wed, 16 Sep 2026 11:09:53 +0200 Subject: [PATCH 1/2] [Cloud env] init cloud env --- .cursor/Dockerfile | 28 ++++ .cursor/README.md | 41 ++++++ .cursor/cloud-install.sh | 55 ++++++++ .cursor/cloud-shell-lib.sh | 95 ++++++++++++++ .cursor/cloud-validate.sh | 157 +++++++++++++++++++++++ .cursor/environment.json | 7 + .cursor/reinstall-tentacles.sh | 11 ++ .github/workflows/cloud-env-validate.yml | 54 ++++++++ .gitignore | 3 + 9 files changed, 451 insertions(+) create mode 100644 .cursor/Dockerfile create mode 100644 .cursor/README.md create mode 100644 .cursor/cloud-install.sh create mode 100644 .cursor/cloud-shell-lib.sh create mode 100644 .cursor/cloud-validate.sh create mode 100644 .cursor/environment.json create mode 100644 .cursor/reinstall-tentacles.sh create mode 100644 .github/workflows/cloud-env-validate.yml diff --git a/.cursor/Dockerfile b/.cursor/Dockerfile new file mode 100644 index 0000000000..a42c723c6b --- /dev/null +++ b/.cursor/Dockerfile @@ -0,0 +1,28 @@ +FROM python:3.13-slim-trixie + +ENV DEBIAN_FRONTEND=noninteractive +ENV CRYPTOGRAPHY_DONT_BUILD_RUST=1 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + git \ + sudo \ + curl \ + ca-certificates \ + build-essential \ + gcc \ + binutils \ + libffi-dev \ + libssl-dev \ + libxml2-dev \ + libxslt1-dev \ + libjpeg62-turbo-dev \ + libopenblas-dev \ + zlib1g-dev \ + && rm -rf /var/lib/apt/lists/* + +RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \ + && apt-get install -y --no-install-recommends nodejs \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /workspace diff --git a/.cursor/README.md b/.cursor/README.md new file mode 100644 index 0000000000..50a466342e --- /dev/null +++ b/.cursor/README.md @@ -0,0 +1,41 @@ +# OctoBot Cursor Cloud environment + +CI-parity install and validation for [Cursor Cloud Agents](https://cursor.com/docs/cloud-agent/setup). + +## Profiles (`OCTOBOT_CLOUD_PROFILE`) + +| Profile | Layers | +|---------|--------| +| `package-only` | L1–L3 (no tentacles install) | +| `ci-tentacles` | L1–L5 (**default** in `environment.json`) | +| `ui-node-web` | L1–L6 (includes `node_web_interface` build) | + +Install: `bash .cursor/cloud-install.sh` (profile via env var). + +## Agent / shell usage + +```bash +source .cursor/env.sh +# Python, OctoBot CLI, pytest +``` + +After editing `packages/tentacles/`: `bash .cursor/reinstall-tentacles.sh` (do not edit repo-root `tentacles/`). + +## Validate environment (on demand) + +```bash +source .cursor/env.sh +bash .cursor/cloud-validate.sh --profile ci-tentacles --json +``` + +Not run from install; use after Build changes or when debugging a broken env. + +## Debugging failures + +| Surface | What to search | +|---------|----------------| +| Cursor Cloud Build log | `OCTOBOT_CLOUD_FAILED` or `==> [L` step markers | +| GitHub `cloud-env-validate` | Same markers; `cloud-validate.json` artifact on failure | +| Success install | `OCTOBOT_CLOUD_INSTALL_OK` | + +If a dashboard environment snapshot shadows this repo config, remove the saved snapshot so committed `environment.json` wins. diff --git a/.cursor/cloud-install.sh b/.cursor/cloud-install.sh new file mode 100644 index 0000000000..35bab8e91c --- /dev/null +++ b/.cursor/cloud-install.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/cloud-shell-lib.sh" +cloud_lib_init "cloud-install.sh" + +echo "OCTOBOT_CLOUD_PROFILE=${CLOUD_PROFILE}" +log_step "INIT" "repo=${REPO_ROOT}" + +if cloud_profile_includes_layer "L1"; then + log_step "L1" "Toolchain (Pants, optional Rust)" + if ! command -v pants >/dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -fsSL https://static.pantsbuild.org/setup/get-pants.sh | bash + fi + export PATH="${HOME}/.local/bin:${PATH}" + if compgen -G "packages/*/crates/*/Cargo.toml" >/dev/null 2>&1; then + if ! command -v cargo >/dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable + # shellcheck disable=SC1091 + source "${HOME}/.cargo/env" + fi + fi +fi + +if cloud_profile_includes_layer "L2"; then + log_step "L2" "Pants package OctoBot wheel" + export PATH="${HOME}/.local/bin:${PATH}" + pants package :OctoBot $(pants list --filter-target-type=package_shell_command ::) +fi + +if cloud_profile_includes_layer "L3"; then + log_step "L3" "Python venv and OctoBot wheel" + python3 -m venv "${REPO_ROOT}/.cursor/venv" + cloud_write_env_files + # shellcheck disable=SC1091 + source "${REPO_ROOT}/.cursor/env.sh" + pip install --upgrade pip setuptools wheel + pip install -r dev_requirements.txt + pip install dist/*.whl + if compgen -G "packages/*/crates/*/Cargo.toml" >/dev/null 2>&1; then + pip install maturin + fi +fi + +if cloud_profile_includes_layer "L4"; then + cloud_npm_client_build +fi + +if cloud_profile_includes_layer "L5"; then + cloud_tentacles_pack_and_install +fi + +if [[ "${CLOUD_PROFILE}" == "ui-node-web" ]]; then + cloud_node_web_interface_build +fi + +echo "OCTOBOT_CLOUD_INSTALL_OK profile=${CLOUD_PROFILE}" diff --git a/.cursor/cloud-shell-lib.sh b/.cursor/cloud-shell-lib.sh new file mode 100644 index 0000000000..6ae483afc5 --- /dev/null +++ b/.cursor/cloud-shell-lib.sh @@ -0,0 +1,95 @@ +# Shared helpers for OctoBot Cursor Cloud shell scripts. +# shellcheck shell=bash + +cloud_lib_init() { + local script_name="$1" + CLOUD_SCRIPT_NAME="$script_name" + set -euo pipefail + CLOUD_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + REPO_ROOT="$(cd "${CLOUD_LIB_DIR}/.." && pwd)" + cd "${REPO_ROOT}" + CLOUD_PROFILE="${OCTOBOT_CLOUD_PROFILE:-ci-tentacles}" + CLOUD_LAST_LOG_TAIL="" + trap 'cloud_err_trap' ERR +} + +log_step() { + local step_id="$1" + local message="$2" + echo "==> [${step_id}] ${message}" +} + +die() { + local step_id="$1" + local message="$2" + echo "OCTOBOT_CLOUD_FAILED script=${CLOUD_SCRIPT_NAME} step=${step_id} profile=${CLOUD_PROFILE} message=${message}" >&2 + if [[ -n "${CLOUD_LAST_LOG_TAIL}" && -f "${CLOUD_LAST_LOG_TAIL}" ]]; then + echo "--- last command output (tail) ---" >&2 + tail -n 30 "${CLOUD_LAST_LOG_TAIL}" >&2 || true + fi + exit 1 +} + +cloud_err_trap() { + local step_id="${CLOUD_TRAP_STEP:-ERR}" + die "${step_id}" "command failed: ${BASH_COMMAND}" +} + +cloud_profile_includes_layer() { + local layer="$1" + case "${CLOUD_PROFILE}" in + package-only) + [[ "${layer}" == "L1" || "${layer}" == "L2" || "${layer}" == "L3" ]] && return 0 + return 1 + ;; + ci-tentacles) + [[ "${layer}" == "L6" ]] && return 1 + return 0 + ;; + ui-node-web) + return 0 + ;; + *) + die "INIT" "unknown OCTOBOT_CLOUD_PROFILE=${CLOUD_PROFILE}" + ;; + esac +} + +cloud_write_env_files() { + log_step "ENV_FILES" "Writing .cursor/env.sh and .cursor/pythonpath.sh" + cat >"${REPO_ROOT}/.cursor/env.sh" <"${REPO_ROOT}/.cursor/pythonpath.sh" <<'EOF' +# For packages/node, packages/flow, packages/copy tests from repo root (matches CI) +export PYTHONPATH=".:${PYTHONPATH:-}" +EOF +} + +cloud_tentacles_pack_and_install() { + log_step "TENTACLES_PACK" "OctoBot tentacles pack from packages/tentacles" + mkdir -p "${REPO_ROOT}/output" + export ALLOW_UNSIGNED_TENTACLES=true + # shellcheck disable=SC1091 + source "${REPO_ROOT}/.cursor/env.sh" + # -p is package filename only; default output_dir is output/ (see main.yml tentacles install) + OctoBot tentacles -d packages/tentacles -p any_platform.zip + log_step "TENTACLES_INSTALL" "OctoBot tentacles install to repo root" + OctoBot tentacles --install --location output/any_platform.zip --all +} + +cloud_npm_client_build() { + log_step "L4" "npm ci and build octobot_client_ts" + npm ci + npm run build -w packages/client/octobot_client_ts +} + +cloud_node_web_interface_build() { + local ui_dir="${REPO_ROOT}/packages/tentacles/Services/Interfaces/node_web_interface" + log_step "L6" "npm ci and build node_web_interface" + (cd "${ui_dir}" && npm ci && npm run build) +} diff --git a/.cursor/cloud-validate.sh b/.cursor/cloud-validate.sh new file mode 100644 index 0000000000..10f6ae9c67 --- /dev/null +++ b/.cursor/cloud-validate.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/cloud-shell-lib.sh" +cloud_lib_init "cloud-validate.sh" + +EMIT_JSON=0 +while [[ $# -gt 0 ]]; do + case "$1" in + --profile) CLOUD_PROFILE="$2"; shift 2 ;; + --json) EMIT_JSON=1; shift ;; + *) die "ARGS" "unknown argument $1" ;; + esac +done + +VALIDATE_STEPS=() +VALIDATE_OK=1 +VALIDATE_FAILED_STEP="" +VALIDATE_MESSAGE="" + +validate_record() { + local name="$1" + local ok="$2" + local detail="$3" + VALIDATE_STEPS+=("${name}|${ok}|${detail}") + if [[ "${ok}" != "1" ]]; then + VALIDATE_OK=0 + VALIDATE_FAILED_STEP="${name}" + VALIDATE_MESSAGE="${detail}" + return 1 + fi + return 0 +} + +validate_emit_json() { + export VALIDATE_OK="${VALIDATE_OK}" + export VALIDATE_FAILED_STEP="${VALIDATE_FAILED_STEP}" + export VALIDATE_MESSAGE="${VALIDATE_MESSAGE}" + export CLOUD_PROFILE="${CLOUD_PROFILE}" + export VALIDATE_STEP_LINES + VALIDATE_STEP_LINES="$(printf '%s\n' "${VALIDATE_STEPS[@]}")" + python3 - <<'PY' +import json +import os + +steps = [] +for entry in os.environ.get("VALIDATE_STEP_LINES", "").split("\n"): + if not entry.strip(): + continue + name, ok, detail = entry.split("|", 2) + steps.append({"name": name, "ok": ok == "1", "detail": detail}) + +failed = os.environ.get("VALIDATE_FAILED_STEP") or None +message = os.environ.get("VALIDATE_MESSAGE") or None +if failed == "": + failed = None +if message == "": + message = None + +payload = { + "ok": os.environ.get("VALIDATE_OK") == "1", + "profile": os.environ.get("CLOUD_PROFILE"), + "failed_step": failed, + "message": message, + "steps": steps, +} +print(json.dumps(payload, indent=2)) +PY +} + +validate_finish() { + if [[ "${EMIT_JSON}" == "1" ]]; then + validate_emit_json | tee "${REPO_ROOT}/cloud-validate.json" + fi + if [[ "${VALIDATE_OK}" != "1" ]]; then + die "${VALIDATE_FAILED_STEP}" "${VALIDATE_MESSAGE}" + fi + exit 0 +} + +[[ -f "${REPO_ROOT}/.cursor/env.sh" ]] || die "VALIDATE_ENV" "missing .cursor/env.sh — run cloud-install.sh" +# shellcheck disable=SC1091 +source "${REPO_ROOT}/.cursor/env.sh" + +log_step "VALIDATE_PYTHON_VERSION" "python 3.13.x" +pyver="$(python --version 2>&1)" || die "VALIDATE_PYTHON_VERSION" "python not runnable" +if [[ "${pyver}" != *"3.13"* ]]; then + validate_record "VALIDATE_PYTHON_VERSION" 0 "${pyver}" || validate_finish +fi +validate_record "VALIDATE_PYTHON_VERSION" 1 "ok" || validate_finish + +log_step "VALIDATE_OCTOBOT_CLI" "OctoBot on PATH" +if ! OctoBot --version >/dev/null; then + validate_record "VALIDATE_OCTOBOT_CLI" 0 "OctoBot CLI failed" || validate_finish +fi +validate_record "VALIDATE_OCTOBOT_CLI" 1 "ok" || validate_finish + +log_step "VALIDATE_IMPORT_OCTOBOT" "import octobot" +if ! python -c "import octobot"; then + validate_record "VALIDATE_IMPORT_OCTOBOT" 0 "import failed" || validate_finish +fi +validate_record "VALIDATE_IMPORT_OCTOBOT" 1 "ok" || validate_finish + +needs_tentacles=false +[[ "${CLOUD_PROFILE}" != "package-only" ]] && needs_tentacles=true + +if [[ "${needs_tentacles}" == "true" ]]; then + log_step "VALIDATE_TENTACLES_DIR" "repo-root tentacles/" + if [[ ! -d tentacles ]] || [[ -z "$(ls -A tentacles 2>/dev/null)" ]]; then + validate_record "VALIDATE_TENTACLES_DIR" 0 "missing or empty" || validate_finish + fi + validate_record "VALIDATE_TENTACLES_DIR" 1 "ok" || validate_finish + + log_step "VALIDATE_IMPORT_TENTACLES" "import tentacles" + if ! python -c "import tentacles"; then + validate_record "VALIDATE_IMPORT_TENTACLES" 0 "import failed" || validate_finish + fi + validate_record "VALIDATE_IMPORT_TENTACLES" 1 "ok" || validate_finish + + client_dist="packages/client/octobot_client_ts/dist/index.js" + log_step "VALIDATE_CLIENT_BUILD" "${client_dist}" + if [[ ! -f "${client_dist}" ]]; then + validate_record "VALIDATE_CLIENT_BUILD" 0 "missing" || validate_finish + fi + validate_record "VALIDATE_CLIENT_BUILD" 1 "ok" || validate_finish +fi + +log_step "VALIDATE_SENTINEL_OCTOBOT" "pytest collect-only" +if ! pytest --collect-only tests/unit_tests/test_octobot_version.py >/dev/null; then + validate_record "VALIDATE_SENTINEL_OCTOBOT" 0 "collect failed" || validate_finish +fi +validate_record "VALIDATE_SENTINEL_OCTOBOT" 1 "ok" || validate_finish + +log_step "VALIDATE_SENTINEL_COMMONS" "pytest collect-only" +if ! (cd packages/commons && pytest --collect-only tests/test_copy_util.py >/dev/null); then + validate_record "VALIDATE_SENTINEL_COMMONS" 0 "collect failed" || validate_finish +fi +validate_record "VALIDATE_SENTINEL_COMMONS" 1 "ok" || validate_finish + +if [[ "${needs_tentacles}" == "true" ]]; then + log_step "VALIDATE_SENTINEL_FLOW_COLLECT" "pytest collect-only with PYTHONPATH" + # shellcheck disable=SC1091 + source "${REPO_ROOT}/.cursor/pythonpath.sh" + if ! pytest --collect-only packages/flow/tests/protocol/test_wire_compat.py >/dev/null; then + validate_record "VALIDATE_SENTINEL_FLOW_COLLECT" 0 "collect failed" || validate_finish + fi + validate_record "VALIDATE_SENTINEL_FLOW_COLLECT" 1 "ok" || validate_finish +fi + +if [[ "${CLOUD_PROFILE}" == "ui-node-web" ]]; then + ui_dist="packages/tentacles/Services/Interfaces/node_web_interface/dist" + log_step "VALIDATE_UI_BUILD" "${ui_dist}" + if [[ ! -d "${ui_dist}" ]]; then + validate_record "VALIDATE_UI_BUILD" 0 "missing" || validate_finish + fi + validate_record "VALIDATE_UI_BUILD" 1 "ok" || validate_finish +fi + +validate_finish diff --git a/.cursor/environment.json b/.cursor/environment.json new file mode 100644 index 0000000000..47596a1693 --- /dev/null +++ b/.cursor/environment.json @@ -0,0 +1,7 @@ +{ + "build": { + "dockerfile": "Dockerfile", + "context": ".." + }, + "install": "OCTOBOT_CLOUD_PROFILE=ci-tentacles bash .cursor/cloud-install.sh" +} diff --git a/.cursor/reinstall-tentacles.sh b/.cursor/reinstall-tentacles.sh new file mode 100644 index 0000000000..17a91c729d --- /dev/null +++ b/.cursor/reinstall-tentacles.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/cloud-shell-lib.sh" +cloud_lib_init "reinstall-tentacles.sh" +# shellcheck disable=SC1091 +[[ -f "${REPO_ROOT}/.cursor/env.sh" ]] || die "INIT" "run cloud-install.sh first" +source "${REPO_ROOT}/.cursor/env.sh" +cloud_tentacles_pack_and_install +if [[ "${CLOUD_PROFILE}" == "ui-node-web" ]]; then + cloud_node_web_interface_build +fi +echo "OCTOBOT_CLOUD_INSTALL_OK profile=${CLOUD_PROFILE} script=reinstall-tentacles" diff --git a/.github/workflows/cloud-env-validate.yml b/.github/workflows/cloud-env-validate.yml new file mode 100644 index 0000000000..775c531e06 --- /dev/null +++ b/.github/workflows/cloud-env-validate.yml @@ -0,0 +1,54 @@ +name: cloud-env-validate + +on: + pull_request: + paths: + - ".cursor/**" + - ".github/workflows/cloud-env-validate.yml" + +permissions: read-all + +jobs: + validate: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-python@v6 + with: + python-version: "3.13.x" + + - uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + + - name: Cloud install (ci-tentacles) + run: OCTOBOT_CLOUD_PROFILE=ci-tentacles bash .cursor/cloud-install.sh + + - name: Cloud validate + id: validate + run: | + source .cursor/env.sh + bash .cursor/cloud-validate.sh --profile ci-tentacles --json | tee cloud-validate.json + + - name: Upload validate report on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: cloud-validate.json + path: cloud-validate.json + + - name: Step summary on failure + if: failure() + run: | + echo "## cloud-env-validate failed" >> "$GITHUB_STEP_SUMMARY" + grep OCTOBOT_CLOUD_FAILED cloud-validate.json 2>/dev/null || cat cloud-validate.json >> "$GITHUB_STEP_SUMMARY" + + notify: + if: ${{ failure() }} + needs: + - validate + uses: Drakkar-Software/.github/.github/workflows/failure_notify_workflow.yml@master + secrets: + DISCORD_GITHUB_WEBHOOK: ${{ secrets.DISCORD_GITHUB_WEBHOOK }} diff --git a/.gitignore b/.gitignore index 3889479639..fc7c350850 100644 --- a/.gitignore +++ b/.gitignore @@ -113,6 +113,9 @@ celerybeat-schedule # virtualenv .venv venv/ +.cursor/venv/ +.cursor/env.sh +.cursor/pythonpath.sh ENV/ # Spyder project settings From e9046a121163934c467b67c6f4f94e55318ae750 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 16 Sep 2026 11:05:54 +0000 Subject: [PATCH 2/2] [Cloud env] prefer repo-pinned Node (.nvmrc) in generated env.sh Interactive Cloud Agent shells resolve `node` to the platform-provided Node (v22 via /exec-daemon/node) which precedes /usr/bin/node in PATH, even though the repo pins Node 24 (.nvmrc, .cursor/Dockerfile). Make the generated .cursor/env.sh prepend the directory of a system node whose major matches .nvmrc, ahead of the platform node, while keeping the venv bin first so python/OctoBot still resolve to the OctoBot venv. Co-authored-by: Guillaume De Saint Martin --- .cursor/cloud-shell-lib.sh | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.cursor/cloud-shell-lib.sh b/.cursor/cloud-shell-lib.sh index 6ae483afc5..eba8d03ec8 100644 --- a/.cursor/cloud-shell-lib.sh +++ b/.cursor/cloud-shell-lib.sh @@ -61,8 +61,22 @@ cloud_write_env_files() { # Generated by cloud-install.sh — source before Python, OctoBot, pytest export OCTOBOT_REPO_ROOT="${REPO_ROOT}" export OCTOBOT_CLOUD_VENV="${REPO_ROOT}/.cursor/venv" -export PATH="\${OCTOBOT_CLOUD_VENV}/bin:\${PATH}" export DISABLE_SENTRY=True +# Prefer the repo-pinned Node (.nvmrc) over any platform-provided node earlier in PATH. +# Prepended before the venv so the venv still wins for python/OctoBot below. +_octobot_nvmrc_major="\$(tr -dc '0-9.' < "${REPO_ROOT}/.nvmrc" 2>/dev/null | cut -d. -f1)" +for _octobot_node in /usr/bin/node /usr/local/bin/node; do + if [ -x "\$_octobot_node" ]; then + _octobot_node_major="\$("\$_octobot_node" --version 2>/dev/null | tr -dc '0-9.' | cut -d. -f1)" + if [ -n "\$_octobot_nvmrc_major" ] && [ "\$_octobot_node_major" = "\$_octobot_nvmrc_major" ]; then + export PATH="\$(dirname "\$_octobot_node"):\${PATH}" + break + fi + fi +done +unset _octobot_nvmrc_major _octobot_node _octobot_node_major +# venv bin must remain first so python/OctoBot resolve to the OctoBot venv. +export PATH="\${OCTOBOT_CLOUD_VENV}/bin:\${PATH}" EOF cat >"${REPO_ROOT}/.cursor/pythonpath.sh" <<'EOF' # For packages/node, packages/flow, packages/copy tests from repo root (matches CI)