Files
Dirigent/scripts/install-whispergate-openclaw.sh

349 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
set -Eeuo pipefail
# WhisperGate installer/uninstaller for OpenClaw
# Requirements:
# - all writes via `openclaw config set ... --json`
# - install supports rollback on failure
# - uninstall reverts ALL recorded changes
# - every install writes a change record
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:-whisper-gateway}"
NO_REPLY_MODEL_ID="${NO_REPLY_MODEL_ID:-no-reply}"
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:-[\"🔚\"]}"
STATE_DIR="${STATE_DIR:-$HOME/.openclaw/whispergate-install-records}"
LATEST_RECORD_LINK="${LATEST_RECORD_LINK:-$HOME/.openclaw/whispergate-install-record-latest.json}"
MODE=""
if [[ "${1:-}" == "--install" ]]; then
MODE="install"
elif [[ "${1:-}" == "--uninstall" ]]; then
MODE="uninstall"
else
echo "Usage: $0 --install | --uninstall"
exit 2
fi
TIMESTAMP="$(date +%Y%m%d%H%M%S)"
BACKUP_PATH="${OPENCLAW_CONFIG_PATH}.bak-whispergate-${MODE}-${TIMESTAMP}"
RECORD_PATH="${STATE_DIR}/whispergate-${TIMESTAMP}.json"
PROVIDER_PATH="models.providers[\"${NO_REPLY_PROVIDER_ID}\"]"
PATH_PLUGINS_LOAD="plugins.load.paths"
PATH_PLUGIN_ENTRY="plugins.entries.whispergate"
INSTALLED_OK=0
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "[whispergate] missing command: $1" >&2
exit 1
}
}
oc_get_json_or_missing() {
local path="$1"
if openclaw config get "$path" --json >/tmp/wg_get.json 2>/dev/null; then
printf '{"exists":true,"value":%s}\n' "$(cat /tmp/wg_get.json)"
else
printf '{"exists":false}'
fi
}
oc_set_json() {
local path="$1"
local json="$2"
openclaw config set "$path" "$json" --json >/dev/null
}
oc_unset() {
local path="$1"
openclaw config unset "$path" >/dev/null
}
write_record() {
local mode="$1"
local prev_paths_json="$2"
local next_paths_json="$3"
mkdir -p "$STATE_DIR"
python3 - <<'PY'
import json, os
record={
'mode': os.environ['REC_MODE'],
'timestamp': os.environ['TIMESTAMP'],
'openclawConfigPath': os.environ['OPENCLAW_CONFIG_PATH'],
'backupPath': os.environ['BACKUP_PATH'],
'paths': json.loads(os.environ['PREV_PATHS_JSON']),
'applied': json.loads(os.environ['NEXT_PATHS_JSON']),
}
with open(os.environ['RECORD_PATH'],'w',encoding='utf-8') as f:
json.dump(record,f,ensure_ascii=False,indent=2)
PY
cp -f "$RECORD_PATH" "$LATEST_RECORD_LINK"
}
rollback_install() {
local ec=$?
if [[ $INSTALLED_OK -eq 1 ]]; then
return
fi
echo "[whispergate] install failed (exit=$ec), rolling back..."
if [[ -f "$BACKUP_PATH" ]]; then
cp -f "$BACKUP_PATH" "$OPENCLAW_CONFIG_PATH"
echo "[whispergate] rollback complete"
else
echo "[whispergate] WARNING: backup missing; rollback skipped"
fi
exit "$ec"
}
run_install() {
trap rollback_install ERR
cp -f "$OPENCLAW_CONFIG_PATH" "$BACKUP_PATH"
echo "[whispergate] backup: $BACKUP_PATH"
local prev_paths_json
prev_paths_json="$(PATH_PLUGINS_LOAD="$PATH_PLUGINS_LOAD" PATH_PLUGIN_ENTRY="$PATH_PLUGIN_ENTRY" PROVIDER_PATH="$PROVIDER_PATH" python3 - <<'PY'
import json, os, subprocess
def get(path):
p=subprocess.run(['openclaw','config','get',path,'--json'],capture_output=True,text=True)
if p.returncode==0:
return {'exists':True,'value':json.loads(p.stdout)}
return {'exists':False}
payload={
os.environ['PATH_PLUGINS_LOAD']: get(os.environ['PATH_PLUGINS_LOAD']),
os.environ['PATH_PLUGIN_ENTRY']: get(os.environ['PATH_PLUGIN_ENTRY']),
os.environ['PROVIDER_PATH']: get(os.environ['PROVIDER_PATH']),
}
print(json.dumps(payload,ensure_ascii=False))
PY
)"
# 1+2) set plugins object in one write (avoid transient schema failure)
local current_plugins_json new_plugins_json
if openclaw config get plugins --json >/tmp/wg_plugins.json 2>/dev/null; then
current_plugins_json="$(cat /tmp/wg_plugins.json)"
else
current_plugins_json='{}'
fi
new_plugins_json="$(CURRENT_PLUGINS_JSON="$current_plugins_json" PLUGIN_PATH="$PLUGIN_PATH" BYPASS_USER_IDS_JSON="$BYPASS_USER_IDS_JSON" END_SYMBOLS_JSON="$END_SYMBOLS_JSON" NO_REPLY_PROVIDER_ID="$NO_REPLY_PROVIDER_ID" NO_REPLY_MODEL_ID="$NO_REPLY_MODEL_ID" python3 - <<'PY'
import json, os
plugins=json.loads(os.environ['CURRENT_PLUGINS_JSON'])
if not isinstance(plugins,dict):
plugins={}
load=plugins.setdefault('load',{})
paths=load.get('paths')
if not isinstance(paths,list):
paths=[]
pp=os.environ['PLUGIN_PATH']
if pp not in paths:
paths.append(pp)
load['paths']=paths
entries=plugins.setdefault('entries',{})
entries['whispergate']={
'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(plugins,ensure_ascii=False))
PY
)"
oc_set_json "plugins" "$new_plugins_json"
# 3) provider
local provider_json
provider_json="$(NO_REPLY_BASE_URL="$NO_REPLY_BASE_URL" NO_REPLY_API_KEY="$NO_REPLY_API_KEY" NO_REPLY_MODEL_ID="$NO_REPLY_MODEL_ID" 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': f"{os.environ['NO_REPLY_MODEL_ID']} (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
)"
oc_set_json "$PROVIDER_PATH" "$provider_json"
# validate writes
openclaw config get "$PATH_PLUGINS_LOAD" --json >/dev/null
openclaw config get "$PATH_PLUGIN_ENTRY" --json >/dev/null
openclaw config get "$PROVIDER_PATH" --json >/dev/null
local next_paths_json
next_paths_json="$(PATH_PLUGINS_LOAD="$PATH_PLUGINS_LOAD" PATH_PLUGIN_ENTRY="$PATH_PLUGIN_ENTRY" PROVIDER_PATH="$PROVIDER_PATH" python3 - <<'PY'
import json, os, subprocess
def get(path):
p=subprocess.run(['openclaw','config','get',path,'--json'],capture_output=True,text=True)
if p.returncode==0:
return {'exists':True,'value':json.loads(p.stdout)}
return {'exists':False}
payload={
os.environ['PATH_PLUGINS_LOAD']: get(os.environ['PATH_PLUGINS_LOAD']),
os.environ['PATH_PLUGIN_ENTRY']: get(os.environ['PATH_PLUGIN_ENTRY']),
os.environ['PROVIDER_PATH']: get(os.environ['PROVIDER_PATH']),
}
print(json.dumps(payload,ensure_ascii=False))
PY
)"
REC_MODE="install" PREV_PATHS_JSON="$prev_paths_json" NEXT_PATHS_JSON="$next_paths_json" TIMESTAMP="$TIMESTAMP" OPENCLAW_CONFIG_PATH="$OPENCLAW_CONFIG_PATH" BACKUP_PATH="$BACKUP_PATH" RECORD_PATH="$RECORD_PATH" write_record install "$prev_paths_json" "$next_paths_json"
INSTALLED_OK=1
trap - ERR
echo "[whispergate] install ok"
echo "[whispergate] record: $RECORD_PATH"
}
run_uninstall() {
local rec_file=""
if [[ -n "${RECORD_FILE:-}" ]]; then
rec_file="$RECORD_FILE"
elif [[ -f "$LATEST_RECORD_LINK" ]]; then
rec_file="$LATEST_RECORD_LINK"
else
echo "[whispergate] no record found. set RECORD_FILE=<path> or install first." >&2
exit 1
fi
if [[ ! -f "$rec_file" ]]; then
echo "[whispergate] record file not found: $rec_file" >&2
exit 1
fi
cp -f "$OPENCLAW_CONFIG_PATH" "$BACKUP_PATH"
echo "[whispergate] backup before uninstall: $BACKUP_PATH"
python3 - <<'PY'
import json, os, subprocess, sys
rec=json.load(open(os.environ['REC_FILE'],encoding='utf-8'))
paths=rec.get('paths',{})
PLUGINS_LOAD='plugins.load.paths'
PLUGINS_ENTRY='plugins.entries.whispergate'
PROVIDER_PATHS=[k for k in paths.keys() if k.startswith('models.providers[')]
# 1) restore plugins atomically to avoid transient schema failures
pcur=subprocess.run(['openclaw','config','get','plugins','--json'],capture_output=True,text=True)
plugins={}
if pcur.returncode==0:
plugins=json.loads(pcur.stdout)
if not isinstance(plugins,dict):
plugins={}
load=plugins.get('load') if isinstance(plugins.get('load'),dict) else {}
entries=plugins.get('entries') if isinstance(plugins.get('entries'),dict) else {}
# restore plugins.load.paths from record
info=paths.get(PLUGINS_LOAD,{'exists':False})
if info.get('exists'):
load['paths']=info.get('value')
else:
load.pop('paths',None)
# restore plugins.entries.whispergate from record
info=paths.get(PLUGINS_ENTRY,{'exists':False})
if info.get('exists'):
entries['whispergate']=info.get('value')
else:
entries.pop('whispergate',None)
plugins['load']=load
plugins['entries']=entries
pset=subprocess.run(['openclaw','config','set','plugins',json.dumps(plugins,ensure_ascii=False),'--json'],capture_output=True,text=True)
if pset.returncode!=0:
sys.stderr.write(pset.stderr or pset.stdout)
raise SystemExit(1)
# 2) restore provider paths (usually custom no-reply provider)
for path in PROVIDER_PATHS:
info=paths.get(path,{'exists':False})
if info.get('exists'):
val=json.dumps(info.get('value'),ensure_ascii=False)
p=subprocess.run(['openclaw','config','set',path,val,'--json'],capture_output=True,text=True)
if p.returncode!=0:
sys.stderr.write(p.stderr or p.stdout)
raise SystemExit(1)
else:
p=subprocess.run(['openclaw','config','unset',path],capture_output=True,text=True)
if p.returncode!=0:
txt=(p.stderr or p.stdout or '').lower()
if 'not found' not in txt and 'missing' not in txt and 'does not exist' not in txt:
sys.stderr.write(p.stderr or p.stdout)
raise SystemExit(1)
print('ok')
PY
local next_paths_json
next_paths_json="$(python3 - <<'PY'
import json, os, subprocess
paths=json.load(open(os.environ['REC_FILE'],encoding='utf-8')).get('paths',{}).keys()
def get(path):
p=subprocess.run(['openclaw','config','get',path,'--json'],capture_output=True,text=True)
if p.returncode==0:
return {'exists':True,'value':json.loads(p.stdout)}
return {'exists':False}
payload={k:get(k) for k in paths}
print(json.dumps(payload,ensure_ascii=False))
PY
)"
local prev_paths_json
prev_paths_json="$(python3 - <<'PY'
import json, os
print(json.dumps(json.load(open(os.environ['REC_FILE'],encoding='utf-8')).get('applied',{}),ensure_ascii=False))
PY
)"
REC_MODE="uninstall" PREV_PATHS_JSON="$prev_paths_json" NEXT_PATHS_JSON="$next_paths_json" TIMESTAMP="$TIMESTAMP" OPENCLAW_CONFIG_PATH="$OPENCLAW_CONFIG_PATH" BACKUP_PATH="$BACKUP_PATH" RECORD_PATH="$RECORD_PATH" write_record uninstall "$prev_paths_json" "$next_paths_json"
echo "[whispergate] uninstall ok"
echo "[whispergate] record: $RECORD_PATH"
}
main() {
require_cmd openclaw
require_cmd python3
[[ -f "$OPENCLAW_CONFIG_PATH" ]] || { echo "[whispergate] config not found: $OPENCLAW_CONFIG_PATH"; exit 1; }
if [[ "$MODE" == "install" ]]; then
run_install
else
REC_FILE="${RECORD_FILE:-$LATEST_RECORD_LINK}" run_uninstall
fi
}
main