From c119697f7fbe265dc8ec2775daa2bd1c258f8677 Mon Sep 17 00:00:00 2001 From: orion Date: Wed, 25 Feb 2026 23:19:58 +0000 Subject: [PATCH] fix(installer): make uninstall atomic for plugins and pass rollback test --- scripts/install-whispergate-openclaw.sh | 51 ++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/scripts/install-whispergate-openclaw.sh b/scripts/install-whispergate-openclaw.sh index 573b325..5bf0c3d 100755 --- a/scripts/install-whispergate-openclaw.sh +++ b/scripts/install-whispergate-openclaw.sh @@ -245,25 +245,64 @@ run_uninstall() { python3 - <<'PY' import json, os, subprocess, sys + rec=json.load(open(os.environ['REC_FILE'],encoding='utf-8')) paths=rec.get('paths',{}) -for path, info in paths.items(): - exists=bool(info.get('exists')) - if exists: +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) + 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: - # path may already be absent; tolerate common "not found" style failures 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) + sys.stderr.write(p.stderr or p.stdout) raise SystemExit(1) + print('ok') PY