| 1 | #!/usr/bin/env bash |
| 2 | set -euo pipefail |
| 3 | |
| 4 | if [[ "${EUID}" -ne 0 ]]; then |
| 5 | echo "Run as root: sudo bash scripts/tencent-lighthouse/bootstrap-ubuntu.sh" >&2 |
| 6 | exit 1 |
| 7 | fi |
| 8 | |
| 9 | CODEWHALE_USER="${CODEWHALE_USER:-${DEEPSEEK_USER:-codewhale}}" |
| 10 | CODEWHALE_ROOT="${CODEWHALE_ROOT:-${DEEPSEEK_ROOT:-/opt/codewhale}}" |
| 11 | WHALEBRO_ROOT="${WHALEBRO_ROOT:-/opt/whalebro}" |
| 12 | REPO_URL="${CODEWHALE_REPO_URL:-${DEEPSEEK_REPO_URL:-https://github.com/codewhale-hq/CodeWhale.git}}" |
| 13 | WHALEBRO_EXTRA_REPOS="${WHALEBRO_EXTRA_REPOS:-}" |
| 14 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 15 | SOURCE_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" |
| 16 | SOURCE_BRANCH="$(git -C "${SOURCE_ROOT}" branch --show-current 2>/dev/null || true)" |
| 17 | REPO_BRANCH="${CODEWHALE_REPO_BRANCH:-${DEEPSEEK_REPO_BRANCH:-${SOURCE_BRANCH:-main}}}" |
| 18 | |
| 19 | # SSH_ALLOWED_CIDRS accepts comma/space separated IPv4 /8-/32 or IPv6 |
| 20 | # /16-/128 CIDRs. Validate the whole list before any host changes. For a |
| 21 | # deliberately public SSH endpoint, explicitly set SSH_ALLOW_ANY_SOURCE=1. |
| 22 | ssh_cidrs="${SSH_ALLOWED_CIDRS:-}" |
| 23 | ssh_cidrs="${ssh_cidrs//,/ }" |
| 24 | if [[ -z "${ssh_cidrs//[[:space:]]/}" ]]; then |
| 25 | if [[ "${SSH_ALLOW_ANY_SOURCE:-0}" != "1" ]]; then |
| 26 | echo "Set SSH_ALLOWED_CIDRS to trusted source CIDRs, or explicitly set SSH_ALLOW_ANY_SOURCE=1. No host changes were made." >&2 |
| 27 | exit 1 |
| 28 | fi |
| 29 | else |
| 30 | command -v python3 >/dev/null || { echo "Python 3 is required to validate SSH source CIDRs. No host changes were made." >&2; exit 1; } |
| 31 | for cidr in ${ssh_cidrs}; do |
| 32 | if ! python3 -c 'import ipaddress, sys |
| 33 | value = sys.argv[1] |
| 34 | if "/" not in value or "%" in value: |
| 35 | sys.exit(1) |
| 36 | try: |
| 37 | network = ipaddress.ip_network(value, strict=False) |
| 38 | except ValueError: |
| 39 | sys.exit(1) |
| 40 | sys.exit(0 if (8 if network.version == 4 else 16) <= network.prefixlen else 1)' "${cidr}"; then |
| 41 | echo "SSH_ALLOWED_CIDRS entry '${cidr}' is not an IPv4 /8-/32 or IPv6 /16-/128 CIDR. No host changes were made." >&2 |
| 42 | exit 1 |
| 43 | fi |
| 44 | done |
| 45 | fi |
| 46 | |
| 47 | apt-get update |
| 48 | apt-get install -y \ |
| 49 | ca-certificates \ |
| 50 | curl \ |
| 51 | git \ |
| 52 | iproute2 \ |
| 53 | openssh-client \ |
| 54 | build-essential \ |
| 55 | pkg-config \ |
| 56 | libdbus-1-dev \ |
| 57 | libssl-dev \ |
| 58 | nodejs \ |
| 59 | npm \ |
| 60 | rsync \ |
| 61 | tmux \ |
| 62 | fail2ban \ |
| 63 | ufw |
| 64 | |
| 65 | node_major="$(node -p "Number(process.versions.node.split('.')[0])")" |
| 66 | if (( node_major < 18 )); then |
| 67 | echo "Node.js 18+ is required for the phone bridges; install a newer Node.js before running install-services.sh." >&2 |
| 68 | fi |
| 69 | |
| 70 | if ! id -u "${CODEWHALE_USER}" >/dev/null 2>&1; then |
| 71 | useradd --create-home --shell /bin/bash "${CODEWHALE_USER}" |
| 72 | fi |
| 73 | |
| 74 | install -d -o "${CODEWHALE_USER}" -g "${CODEWHALE_USER}" "${CODEWHALE_ROOT}" |
| 75 | install -d -o "${CODEWHALE_USER}" -g "${CODEWHALE_USER}" "${CODEWHALE_ROOT}/bridge" |
| 76 | install -d -o "${CODEWHALE_USER}" -g "${CODEWHALE_USER}" "${CODEWHALE_ROOT}/telegram-bridge" |
| 77 | install -d -o "${CODEWHALE_USER}" -g "${CODEWHALE_USER}" "${WHALEBRO_ROOT}" |
| 78 | install -d -o "${CODEWHALE_USER}" -g "${CODEWHALE_USER}" "${WHALEBRO_ROOT}/worktrees" |
| 79 | install -d -m 0750 -o root -g "${CODEWHALE_USER}" /etc/codewhale |
| 80 | install -d -m 0700 -o "${CODEWHALE_USER}" -g "${CODEWHALE_USER}" /var/lib/codewhale-feishu-bridge |
| 81 | install -d -m 0700 -o "${CODEWHALE_USER}" -g "${CODEWHALE_USER}" /var/lib/codewhale-telegram-bridge |
| 82 | |
| 83 | if [[ ! -d "${WHALEBRO_ROOT}/codewhale/.git" ]]; then |
| 84 | sudo -u "${CODEWHALE_USER}" git clone --branch "${REPO_BRANCH}" "${REPO_URL}" "${WHALEBRO_ROOT}/codewhale" |
| 85 | fi |
| 86 | |
| 87 | for repo_spec in ${WHALEBRO_EXTRA_REPOS}; do |
| 88 | repo_name="${repo_spec%%=*}" |
| 89 | repo_url="${repo_spec#*=}" |
| 90 | if [[ -z "${repo_name}" || -z "${repo_url}" || "${repo_name}" == "${repo_url}" ]]; then |
| 91 | echo "Skipping malformed WHALEBRO_EXTRA_REPOS entry: ${repo_spec}" >&2 |
| 92 | continue |
| 93 | fi |
| 94 | if [[ ! -d "${WHALEBRO_ROOT}/${repo_name}/.git" ]]; then |
| 95 | sudo -u "${CODEWHALE_USER}" git clone "${repo_url}" "${WHALEBRO_ROOT}/${repo_name}" || { |
| 96 | echo "Warning: failed to clone optional repo ${repo_name} from ${repo_url}" >&2 |
| 97 | } |
| 98 | fi |
| 99 | done |
| 100 | |
| 101 | if [[ ! -f /etc/codewhale/runtime.env ]]; then |
| 102 | cat >/etc/codewhale/runtime.env <<'EOF' |
| 103 | CODEWHALE_RUNTIME_TOKEN=replace-with-long-random-token |
| 104 | CODEWHALE_RUNTIME_PORT=7878 |
| 105 | CODEWHALE_RUNTIME_WORKERS=2 |
| 106 | CODEWHALE_PROVIDER=deepseek |
| 107 | DEEPSEEK_API_KEY=replace-with-provider-key |
| 108 | RUST_LOG=info |
| 109 | EOF |
| 110 | chown root:"${CODEWHALE_USER}" /etc/codewhale/runtime.env |
| 111 | chmod 0640 /etc/codewhale/runtime.env |
| 112 | fi |
| 113 | |
| 114 | if [[ ! -f /etc/codewhale/feishu-bridge.env ]]; then |
| 115 | cat >/etc/codewhale/feishu-bridge.env <<'EOF' |
| 116 | FEISHU_APP_ID=cli_xxxxxxxxxxxxxxxx |
| 117 | FEISHU_APP_SECRET=replace-with-app-secret |
| 118 | FEISHU_DOMAIN=feishu |
| 119 | CODEWHALE_RUNTIME_URL=http://127.0.0.1:7878 |
| 120 | CODEWHALE_RUNTIME_TOKEN=replace-with-same-token-as-runtime-env |
| 121 | CODEWHALE_WORKSPACE=/opt/whalebro |
| 122 | CODEWHALE_MODEL=auto |
| 123 | CODEWHALE_MODE=agent |
| 124 | CODEWHALE_ALLOW_SHELL=true |
| 125 | CODEWHALE_TRUST_MODE=false |
| 126 | CODEWHALE_AUTO_APPROVE=false |
| 127 | CODEWHALE_CHAT_ALLOWLIST= |
| 128 | CODEWHALE_ALLOW_UNLISTED=false |
| 129 | FEISHU_THREAD_MAP_PATH=/var/lib/codewhale-feishu-bridge/thread-map.json |
| 130 | FEISHU_ALLOW_GROUPS=false |
| 131 | FEISHU_REQUIRE_PREFIX_IN_GROUP=true |
| 132 | FEISHU_GROUP_PREFIX=/cw |
| 133 | FEISHU_MAX_REPLY_CHARS=3500 |
| 134 | CODEWHALE_TURN_TIMEOUT_MS=900000 |
| 135 | EOF |
| 136 | chown root:"${CODEWHALE_USER}" /etc/codewhale/feishu-bridge.env |
| 137 | chmod 0640 /etc/codewhale/feishu-bridge.env |
| 138 | fi |
| 139 | |
| 140 | # Apply the already validated narrow rules before deleting the broad one. |
| 141 | # SSH_ALLOW_ANY_SOURCE=1 is an explicit operator choice, never the default. |
| 142 | if [[ -n "${ssh_cidrs//[[:space:]]/}" ]]; then |
| 143 | for cidr in ${ssh_cidrs}; do |
| 144 | ufw allow from "${cidr}" to any app OpenSSH |
| 145 | done |
| 146 | ufw delete allow OpenSSH >/dev/null 2>&1 || true |
| 147 | else |
| 148 | echo "WARNING: SSH is reachable from every source. SSH_ALLOW_ANY_SOURCE=1 explicitly allows this. Set SSH_ALLOWED_CIDRS (e.g. 203.0.113.4/32) and rerun to restrict it." >&2 |
| 149 | ufw allow OpenSSH |
| 150 | fi |
| 151 | ufw --force enable |
| 152 | |
| 153 | cat <<EOF |
| 154 | |
| 155 | Base server setup complete. |
| 156 | |
| 157 | Next: |
| 158 | 1. Install Rust 1.89+ for ${CODEWHALE_USER}; rustup is the usual path. |
| 159 | 2. Build/install the unified binary: |
| 160 | sudo -iu ${CODEWHALE_USER} |
| 161 | cd ${WHALEBRO_ROOT}/codewhale |
| 162 | cargo install --path crates/cli --locked --force |
| 163 | 3. Copy integrations/feishu-bridge or integrations/telegram-bridge to ${CODEWHALE_ROOT} and run npm install. |
| 164 | 4. Edit /etc/codewhale/runtime.env and the selected bridge env file. |
| 165 | 5. Install systemd units with scripts/tencent-lighthouse/install-services.sh. |
| 166 | 6. After the env files are edited and services are started, run: |
| 167 | sudo bash scripts/tencent-lighthouse/doctor.sh |
| 168 | |
| 169 | EOF |
| 170 |