diff --git a/CHANGELOG.md b/CHANGELOG.md index 16e9224..92aa2d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Added containerization (`Dockerfile`, `docker-compose.yml`) - Added helper scripts for smoke/dev lifecycle and rule validation - Added no-touch config rendering and integration docs +- Added installer script with rollback (`scripts/install-whispergate-openclaw.sh`) - Added discord-control-api with: - `channel-private-create` (create private channel for allowlist) - `channel-private-update` (update allowlist/overwrites for existing channel) diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md index 8bc5a18..3043de3 100644 --- a/docs/INTEGRATION.md +++ b/docs/INTEGRATION.md @@ -27,8 +27,30 @@ The script prints JSON for: You can merge this snippet manually into your `openclaw.json`. +## Installer script (with rollback) + +For production-like install with automatic rollback on error: + +```bash +./scripts/install-whispergate-openclaw.sh +``` + +Environment overrides: + +- `PLUGIN_PATH` +- `NO_REPLY_PROVIDER_ID` +- `NO_REPLY_MODEL_ID` +- `NO_REPLY_BASE_URL` +- `NO_REPLY_API_KEY` +- `BYPASS_USER_IDS_JSON` +- `END_SYMBOLS_JSON` + +The script: +- writes via `openclaw config set ... --json` +- creates config backup first +- restores backup automatically if any step fails + ## Notes -- This repo does not run config mutation commands. - Keep no-reply API bound to loopback/private network. - If you use API auth, set `AUTH_TOKEN` and align provider apiKey usage. diff --git a/scripts/install-whispergate-openclaw.sh b/scripts/install-whispergate-openclaw.sh new file mode 100755 index 0000000..9cb4946 --- /dev/null +++ b/scripts/install-whispergate-openclaw.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# Install WhisperGate config into OpenClaw safely. +# - uses `openclaw config set ... --json` for writes +# - creates full config backup +# - rolls back automatically on failure + +OPENCLAW_CONFIG_PATH="${OPENCLAW_CONFIG_PATH:-$HOME/.openclaw/openclaw.json}" +PLUGIN_PATH="${PLUGIN_PATH:-/root/.openclaw/workspace-operator/WhisperGate/dist/plugin}" +NO_REPLY_PROVIDER_ID="${NO_REPLY_PROVIDER_ID:-custom-127-0-0-1-8787}" +NO_REPLY_MODEL_ID="${NO_REPLY_MODEL_ID:-whispergate-no-reply-v1}" +NO_REPLY_BASE_URL="${NO_REPLY_BASE_URL:-http://127.0.0.1:8787/v1}" +NO_REPLY_API_KEY="${NO_REPLY_API_KEY:-wg-local-test-token}" +BYPASS_USER_IDS_JSON="${BYPASS_USER_IDS_JSON:-["561921120408698910","1474088632750047324"]}" +END_SYMBOLS_JSON="${END_SYMBOLS_JSON:-["🔚"]}" + +BACKUP_PATH="${OPENCLAW_CONFIG_PATH}.bak-whispergate-install-$(date +%Y%m%d%H%M%S)" +INSTALLED_OK=0 + +rollback() { + local ec=$? + if [[ $INSTALLED_OK -eq 1 ]]; then + return + fi + echo "[whispergate-install] ERROR (exit=$ec). Rolling back config from backup..." + if [[ -f "$BACKUP_PATH" ]]; then + cp -f "$BACKUP_PATH" "$OPENCLAW_CONFIG_PATH" + echo "[whispergate-install] rollback complete: $OPENCLAW_CONFIG_PATH restored" + else + echo "[whispergate-install] WARNING: backup file not found, cannot auto-rollback" + fi + exit "$ec" +} + +trap rollback ERR + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || { + echo "[whispergate-install] missing command: $1" >&2 + exit 1 + } +} + +oc_set_json() { + local path="$1" + local json="$2" + openclaw config set "$path" "$json" --json >/dev/null +} + +echo "[whispergate-install] checking prerequisites..." +require_cmd openclaw +require_cmd python3 + +if [[ ! -f "$OPENCLAW_CONFIG_PATH" ]]; then + echo "[whispergate-install] config not found: $OPENCLAW_CONFIG_PATH" >&2 + exit 1 +fi + +cp -f "$OPENCLAW_CONFIG_PATH" "$BACKUP_PATH" +echo "[whispergate-install] backup created: $BACKUP_PATH" + +# 1) plugins.load.paths append plugin path (dedupe) +CURRENT_PATHS_JSON="[]" +if openclaw config get plugins.load.paths --json >/tmp/wg_paths.json 2>/dev/null; then + CURRENT_PATHS_JSON="$(cat /tmp/wg_paths.json)" +fi + +NEW_PATHS_JSON="$(python3 - <<'PY' +import json, os +cur=json.loads(os.environ['CURRENT_PATHS_JSON']) +pp=os.environ['PLUGIN_PATH'] +if not isinstance(cur,list): + cur=[] +if pp not in cur: + cur.append(pp) +print(json.dumps(cur, ensure_ascii=False)) +PY +)" + +oc_set_json "plugins.load.paths" "$NEW_PATHS_JSON" +echo "[whispergate-install] set plugins.load.paths" + +# 2) plugin entry +PLUGIN_ENTRY_JSON="$(python3 - <<'PY' +import json, os +entry={ + 'enabled': True, + 'config': { + 'enabled': True, + 'discordOnly': True, + 'bypassUserIds': json.loads(os.environ['BYPASS_USER_IDS_JSON']), + 'endSymbols': json.loads(os.environ['END_SYMBOLS_JSON']), + 'noReplyProvider': os.environ['NO_REPLY_PROVIDER_ID'], + 'noReplyModel': os.environ['NO_REPLY_MODEL_ID'], + } +} +print(json.dumps(entry, ensure_ascii=False)) +PY +)" + +oc_set_json "plugins.entries.whispergate" "$PLUGIN_ENTRY_JSON" +echo "[whispergate-install] set plugins.entries.whispergate" + +# 3) no-reply model provider (under models.providers) +PROVIDER_JSON="$(python3 - <<'PY' +import json, os +provider={ + 'baseUrl': os.environ['NO_REPLY_BASE_URL'], + 'apiKey': os.environ['NO_REPLY_API_KEY'], + 'api': 'openai-completions', + 'models': [{ + 'id': os.environ['NO_REPLY_MODEL_ID'], + 'name': 'whispergate-no-reply-v1 (Custom Provider)', + 'reasoning': False, + 'input': ['text'], + 'cost': {'input': 0, 'output': 0, 'cacheRead': 0, 'cacheWrite': 0}, + 'contextWindow': 4096, + 'maxTokens': 4096, + }] +} +print(json.dumps(provider, ensure_ascii=False)) +PY +)" + +PROVIDER_PATH="models.providers[\"${NO_REPLY_PROVIDER_ID}\"]" +oc_set_json "$PROVIDER_PATH" "$PROVIDER_JSON" +echo "[whispergate-install] set ${PROVIDER_PATH}" + +# 4) quick validation reads +openclaw config get plugins.entries.whispergate --json >/dev/null +openclaw config get "$PROVIDER_PATH" --json >/dev/null + +INSTALLED_OK=1 +trap - ERR + +echo "[whispergate-install] install completed successfully" +echo "[whispergate-install] next steps:" +echo " 1) start no-reply api (port 8787)" +echo " 2) restart gateway: openclaw gateway restart"