ÿØÿà JFIF    ÿÛ „ !.%+&8&+/1555$;@;4?.451 4,$,44444444444414444444444444444444444444444444444444ÿÀ  á á" ÿÄ     ÿÄ ?    !1AQaq"2‘¡±ÁðBRbrÑá#‚’¢²3S CñÿÄ   ÿÄ !    !1QAa‘2ÿÚ   ? 5˜Z¯V¦cø)›t/? z¨±>Õ5€¶‹Á¤·¼z¼Ü¬+ñ®v¤¨_ˆR­BFn©—˜ý®ç̝P8gýt·ÉSTŦˆìät?þé¼íìN/Þa)ì–í6ô… Ï¿øÃj´¿KÇü]ÿ ªô¹-eKànëÕHTx}ýSÜ›ÿ ”7Ø×&µ<¦  ¥ÑO¶[Ù¯ä¨ÞÃÿ PZ-¬;#õ|•oaÿ ©CìÞz3˜öː/¤­ñTûIØ}š^ mÓ%ªxˆ¥ÉŸu=Z+ISe¿45™¼u;ú&WØ÷€æßQ™®{|íx*TC“#ZŠìZ§²‹ 6pv…³¿¡äª*áZÐ%ÒOáˆo"x«OHk w±æ+¬V(kMúŸ5Vö«$ ÁrÏbàb57/luR ¸ÑÛj Òµì`Мq­û žICÀÊ•©4€Âcà¨Ï€O´<èÐ:›ù(Ë^L8þ‘ÍÌ#¸Ð_Ì©ÙK(Öz 4¬û+¸;ü’V’84‘¬ÃŽ:[â‡ÔÌáõp¢~§ªlæ£ö{®G>J¼"°‡7¯ÆÉèßû ‹É‹§ÁòÃýâßî ^ƾÙõ‹×óH#«LP½ïX=xÑÍ$|W?•~• îëÔ©ª‹ {ÝT…Kÿ ”hûâá)J*ö˜–ÔU;iÇ€/ ÆþjóZ\ýwØ=Ìm ºèËL9 ýèÆð/¨’¥öo=nË.%Îì ŽÕ¯È|{Oj²ƒE6e/ßdÄõ²Ìâ1O®ò×TsəԸhOMýíMˆ¿¼H˜l²,7Â¥#MF/Úf°Ö½± ¸–dr‹NýÊ íjqx{œÉ ä-È ¦ øÄër¨q°ð †nцýÑÄÆ’mä…n<0È™;ÁÝá¯ÁZƒ7FÀmì­ É&9ˆîéi¶ùN§Y• ÃZãAâ?•‡©‰ , ó¾IŸŠc1 4â&y­&pŠ­6;M À 0¹qç»p.á …ŸÅáK@%6·y6ƒ‰3?”úºŽ‰éX5ªPT §µ!=Mž«Ú½‹ÅgÂSâÉaþÓoö–¯ÁÔìR>5éÿ üs¶ÆUcÌ kÇR ]ÿ ù¬¼«VŽ;Â|‡~¢¦”ÏŰæ {L™Õ°Óv¹ò¸írޡעCÃ!íVÕ {¶»sŒNPg/ "uÕbkm²“$ďå¿é¹§°½æz¯6 †s¿!s–wÚÝ“™Œ °.ûj>·+™Òa…©Œ&rÝÎtÛë긪Ît’LAVp%c Úý[ÄzJ¾ÇàXXç@˜ó<êL]·T˜¾¥1Ó©V‡g´æ½¦Ý@¹óø!_@´ÞâSÁ —S3™•& ]@JHÚý©ZŽ €×æÔr»Áf!‡yÞ4Mv*èÓã_{‘åóUuљØ«Oïé*®EvÑ Œ÷‡U \"㪒ÍK+À 4“M¡ï:0¥5í!'<@î´”>Ç»&Z–ïCCV˜Ì5Šo&îhè.žû |ÓK©h$s6KìŒëã)¹hI¦GïOåóI;ììü#É$Š0…Ææ¥TØ.5­¾gn´ “ÂÖ\:hœ89G)J@„}œ:’Ò{/Š"¦_Æ×7Æ3VÇŠÊa]ÚŒÙ€Ä–=®uÁßâACZƒ§§£ Qnâ:«,×{tyø¬iÛcœÜÄ€H½ÄÍCk´÷šß .W'b¤Íåh]÷€=,Žv×cÚEÚHXJX¶îo¨FÒtèöŸ>ªª6[J®Fµ£sGÁeqõfe\íjÒÐïÄÐGˆe1Ø‹.Ø”‘Ëuø Y­ˆÜ ŽG|zùªüMpDnQWÄ”%JŠ™)â*p@Örš«ÕT2Ð%ˆG#ª„ ·¤!°ŸOTÂT¸aÚ%4&h™LµšØüÐ.F¿²ÐÞ_Ç‚¾ÅÃaÜ÷09Æ q€öy˜v‡85õN÷]¬äѼóS{°_MެúÔ#°Ç¸0åÞè2ëôPcvÆw9®ií1Ä8F™˜à‰´+‰Ik1òÝ7“Ñ×ÒsÝ\x‚h`ÞÑ`ó"|µEcý£n˜h`}GÞ !±ù²Ápü²ß6 0ïi󜵩SÈÇ7˜-ÕURO˜¦´f$ªž-Í6(œ}<„ éc øs]ŽŽ„*—¾ ìdŽ„)méª\¿êÎIg¾ØÞ~I#C/¼¼´EÁÈŽi8“©õådô·>euä ƒ'Ê×लR1ÉJE1ÐAát`t;ÇР%Ý<‡¥„ÍÆ`×Oyó)õiI€ñQaŸ4Ûù\áàaÃÔ¹HÃu¹*k€¦<„e S‡&õÏ B!ŽhüÞ`yj}mªf×\¿ Ç~æ­9‡û\՞Ǖg²1Žû5V7 !àöšm° c`ܬøÇìµÒ'P"?…´Ö,"§^•õލsÔ)6˜sæéÍR¼ ò|Sl”‹7 nPW Gòú÷½§O¯‡„l¡kSÞŒr½PÊ@æ¢pŽ-mÿ #Ÿ˜Àº¶Áä¦;ïÔæ$1££`“Õ>„—·ž)ßð³ñ#Ï Ô$¶œ‰ÊE‹À;÷º ¯«P:Ñ”8–IÊtpÞ3ª“>ê“þës4ò2OÏÕ­±zô†Õ§‰.÷ä¸;¿˜“'œ›žª}«Œ{ª±Ì 9ÔóÞÕ‡0 $íWV3Üì¬ —@kÝ4@¿r¼±½¬™›?øØæ´'Áé®CË3-g$˜ö‡×auÚi´Žp/êÛ æF›Ú2v‹ã¿¿,nB1̨ƃqÞa5͝@&Æû“él÷ \C²½UÍc ¯k×¢U ÖéQå™—-r wô ÞÏ<Ò=&=ÿ Ôê Òêˈt,i—;LîÜ á¸*ÚÃ1$êL•LÍ <É)ýÐà’ ;F™{ƒ™˜€&'}‚ãÄK`¡ÞT@I;®žZóè‚s’7®°›+§O­Åq©é»²9<Ô J ¼9O’HL»Ùïì¸rk¼Ž_ý‘TŸu[²ßÚŒ·ü÷B%¯E ŸÔX5êO´ Ç•€’I0 ÉJX` ñ¹õ%;µŸD‘«´€àwÒ™U ûئžÖö\×®×´8 ½‡ºÐÆÓ§?Àkmœ=;d5*@-ì0F Rªýš[Ü6âö̃ڸr*KA9· u*µæ£?U¸Âêí†8@¦X4 e-ò„0s{ HâUpU?¼mñRa°®a%Ð'tÉ×’\¾ÊÉ]t›h>·(Ë@R¼¡Ãt h}’O÷au<+nT…Ö…MӐ??Óe95 q>í/;&JSû °¯ÊéÞ øƒ*Ã2½Ài&:nôUl=¾¿5eˆ3”ñc|Ú2V”>„»&eE;«ÚäC p¢Û úy 9š[ŒÌx¼擼A&DåÒ¯ˆ¤ÀÌ;"˜ ÏQä¸åhÊ}Ûq«Û0WžÒ|»€ø®öCm5•\ÇÀ§Pe3£]0ÃàLDÉ‰1øªxjgwT‚÷¿LΨK‹›ùs—xˆÜ±µ kæ¸f‰‰ÜGk/LÛØ6d9ò¶ùA{ƒA3š/¬D¬khÓk‰`˜"㯒r¿±Óã jx‡°e}<Ñø\3y:'À•/h½Í€Ç4~g ?Û(¼]v‘ªlKÎâ~?O‚W%{Ì:“'©úNq¾›úo(X’¥¯ˆ nFê{Ç€ü?º'ë ø‹ì Þ09ŒÌç9Æ —ËC`j@ÓÄ(+a‹un¸#ÂꟋ{K`‘ÑÍÍ'à´»/Û,KW;Þ4²þð ï Nm|~fGÏ(…³Ã)«1ö­Õ ¥‡¨©ƒÃ™ü-s=à=U66Ï«Ýc蓦W¹íž®›nÔ%êÇìŒ<#Ü×84ån®Ð ÒåOC` ñânÑs‡¢ç 1õ%Îhì½Ã½® e:ݼUZo™`  ÅZŸŒÊ«ê1ÏÄo$q¹Þ€©ˆhÐÉä¯ñ[!…Ú˜àJ:x2$Íß&PåT£6ç— ‡Í*4Ýšçjÿ ‰É nófÐ ó(L5C•åÆ\rMÒ@ò }y-W}™üýVù—ú¢=Ù”c®‘< M ž ´Phr ¦©TD ‘ù.$´÷O‡‘V2Æò.=IUŒ=ž‡â¬i™aþÓåÙ?òUø'ØÖ•.~* šTŒ!•-×áºTâ®ä#õü'´ eýlYÅÓeÕKÂrT"CÚ@u!Óxƒ{š3€}1¿(r}%«nËamjÑ%ÑNEò v ˜à  σöK³,*º.àzù¨™Ó ÚçâU¦*¿ 9{%Ö¹ njûdaXöb) kÛÆ±ûÓ\°M7ˆÂ=û›ç¿Ã‚­V»Cg–8ÙêE- j)k$º`Ã-ùEýeBÆÇ]c¡°ñty&Òd0nõ'¡W+ƒ*|–øµFa\GQªEAÔp5\Ǽ·¼Ç8·õ -â§Ú[ ‡ uZeÖ 3}×d'+¹:ð+K†Û®s!Ï$úe€<Û”x)1»a­¡LC]¸µík…ÚàA»AYº{†ªS[¦5HÒ7ù --,ísòDØ€èk ÞÀîÜ ò@â( ËNˆë›4ô½•/¦o‡€Û7 ê•ÆêòðÜy'Án½µ á˜ݦ ndeo…[ì¶Ê,¥R³Ä=À±—–ß;£™´ñSâ*g§”ïaið‘Jå~™ÓÞ ß³Õ¢»8x埒²52>AÊb&-÷\7´éÄù€T˜,w;3{ï˜k…à¹ÄqÀ«œ{€\ ˆ¾[´¨јr &Úé„Ívˆ±8†¿]|¬ņ4I×pÞS1ÈÖz‰#Ìv‡G!YNògñ:màTz¢Ý1ô©^O=~ë|5Bã™ç•¼µõ•bÆ@úÕS¬ÈŒ#¬zünrŸ û” Z²•èðV"ÁHÚý©wÝ €7¼Ìu1hÑa3Éä û f$o¿É ™Ú›ÝçnpÒ3äÌ3†Í§,Äï]$‰/pê †«À¼¸e9­Æê_C]žƒ·ý·frÁN«, E=›Çq -‰öŒ:aÏ¿±í&£Í:-} 84‘ÿ eƒQÑeëSsuiA ³g㟥ú£?ÿ ʼn*”“÷aühe:ÊWa@ÒÞk±eØ] F Ô—r.åä˜ @ö¥ªZoÐýYL·¥S²G/‡ñ <~*ZÆ´è>JlòàÛÆ½ÿ 窘ìGN¢:I®KšJp/`íIÁÀõ#Ä-€ö­šµŒoF4|ÆQØÆ@Ì|£Ô…¢À{9˜è½Üó›€ôYÒÎYsið;ís¤€à²ˆ‚4qÉVŒI$ ‰"° æµ8cXGjœˏ¡Aâý•ËÜ¢ûï e·çLx']á"oÅÎê3¯Ç—¹”ó0nå‚âg{Œñ> S´˜îè°g238‚ãköÝfÚd´6Ò€;ò÷±¢™¼›º ¢Æ'¥Ðx'e¬ç ]bÈÆV¢ó‹kýBO ðÊâ$Ÿ!×T 3Mýמ žìٍàÌü‘8÷€àæØ8æ©6‰©L´«…oãpð„~Çk‰!ñ;‹”ÛžÍ àž±z Ÿôû øŸÝužÏ;ÿ #|u6™Þ¬ÚˆÐõA4¶â|ôl|Ê2ŽÇ¤ÝÅÇY.<#Aí.k§hóF‚”Y; M½Ö4hŸ4&›­¿tès´%FìL¥£Ãk‰ÇT¤haÁ¤ÚxfÉ`ÑìË›>i 3t‚:,–+^÷´–{Û–Nxi"x‘Ûg î¨>¥Õ܁ùZH,2Û“:8xÊ¢Çí9.É-Ìâã-=çjwµS˜dütžçwýGòú®®ûº_ˆýx$–¡ãøO EÚÛÏ÷R„×w+3£Á£öUMyR²¹âŒ°š›¸Ñãò9§Ó_Dl+Ùßc›úšGÅÌc†Ž!Ko=¶.‘Îÿ c²(2®V mª.ÿ ¹B›¹å ù„öŸSV>™ü¯$y:G¢Z×àøúdî¹û­·ýÇ´:•c LÍõi_‹ö+ÎæGÊè>OŠ•äž´§Þ{X}¨1ÚTc›»Qþ•êô°t¿OP?eæ~É{5]•ÙR£r5†nZ\ã@ &îJõ ¾àC°þV>fé¥/ü5ñÊIº_é5 ;e­h<@ Ä&æÃëE%;X,ÒãÆÞ`Oò¦kŸm#˜!ÀyÄ¢| óLšò¥Ä` ¶R=|ÈCâh5ò3DˆïF†ðÒ#ÅìÛœ?¸yhBãœí ZxßÎÄhºRK„`Þödvײ™ÀÈÑÒgŒuY w³%†ƒÓzõ ÖÏp‚dH®¦A´ù§»ÓÇMæ~)ˆð‡û:ù&Ä •vGD´À n ݇¼Ö8Fö óáà£~Ë¥x`oK|Ä?fxiØü%pìR>éò+Û±éÎ>núlFŤ'tq8LZÏvÃ?„¡ß±È⽆¯³íü@x|PöUäèØã¡ð‚ŒAìÏ"vÍwóŸÍ{ ý0.z È•Ö{,N¡£¡ŸKÕÙž>Ýœþ ÍÀ°<×EA!Å‚D™IúOÍ¡>ôG}Â` ÍßkÜL™Ž Þð™ {IøF²¹òQ3&!ÃÂÞz.d&Ï-sH¸,Ôõ˜ŽP€ 77ˆÝ¼ÊëÜw =cÕ Ú,ØÐ5ÎYÐ)ì´öœgŒ[¤ßv㙑8心>h]§µháYš£²ºÑ.{Ï7Sð•?´~×SÃKýJÛ˜ ™Íäiúu<µX¶1õ^kâçIÑ£sZ4h>j*ÔšD:4­¿_ ÷¸ Õxæÿ ¸?Mù _•­ÊÐ ä ÷ý ÑwL œ­ïnTkÛUÍN©ë:¦fV ¶ÜÔÜMªÅâA½–¿R×TXš-%iTÊT•‡Ù‚JôϐZxWÑè‰f‰òG º ×Õû2aZ7OU3[“×AT–ÞŒ…-‘¤”Ì ì&(ˆ¿­•ƒkï’:ðY¦W‘ Å)“†‘˜³Åtcø˜ñTÂwÚÇ4|üLÇªí–v- qˆèU qPE.†â‘˜µ Æ,ÐÅs]8¾„oúÑ i>ÜxxÈó)ƒ ´æÁâØ$À‰vžŸf$Ž |ãw;ÀÁIJ»b` {¦Ó¤Ú$©YÀ‘n@Óïž«9J¼êG m¤ ܯ¹ÌW4€ÐÒÅÛ‡#褕Ÿn-?í|с¥÷Ú¹¬'´ÞÜ9ÓK `hê£SÄSà?7—Wí_´…óB›»:=Ãïq`<8ñÓŒÑlú2d¬ê³£hÖ[l|$vÝro~'R®‰§°ñmY ͧäP |PUª¹·:3Œ[Û{Xÿ ºâ@‚W–Äé u‚ ¯´*=íή.pûÒdt @G‰¬ s¸ ëÉücr ÞæÑ¨Ê@>¤¢Ö±. Þ'¯°ÌME[YéïĵÂCå½ Ué©Áû'Ê9%eÔðNU”ë‘ÌsD3/®+UI˜9h.WC”빓$#:pz:YÓ ¿xž* ³$Í +$kñAŠ‹†¢ Uê>¸)_š¬÷©ßAÂÔb9ÇU ¯¾á•9¯ÏÏ÷O÷¼¼Fähal1‰3Ì[Ïr•´UCksNÐ] R‘¸¥H+§Šé†c©vÖÞ0iÓ76s†î!§=ß ¼~Ô'°Ãmäoäš³ªøi1úÉ)³yV8 CLÄØÁ‘WYïi€H6ÖÑiámø^ÈY´°Ñ7¥Û*—Ñ©L«Qƒï—Ùrÿ ›£Ð*š¸ˆL©ˆ$ˆ ÷¾D§9È®«qbqC)–ˆïv´çñsÑVT­Ø, <àïºÀO«Jý·õ àfPìð .wFšir´þ’2_Y *Æ€x\« ì€9š@ Ž|F⇥ˆkZ@hÖÄ0t¿-<“‹qµ¾*ZL¤Ú)&BJpÓF5=$„at*Zš$’ÑtdûÝRI1 2މ$€$I$#‰SÞ’Hë¬ï;Á$¡t$’`<(ñÇt)$‡Ð.Êf¢X’Kt=Éé$‚ˆªè¢oÝëòI%Rgcª÷ŠyI%¡‰ÿ !ñ)´õ $¤ Ô’IIGÿÙ"""Nydus ops for installing and managing OpenClaw on customer VMs.""" import json import logging import os import re from typing import Any, Dict, List, Optional, Tuple from urllib.parse import urlparse from customer_local_ops import Ops, NydusResult from customer_local_ops.util.execute import runCommand LOG = logging.getLogger(__name__) DEFAULT_INSTALLER_BASE = "https://hfs-public.secureserver.net" INSTALLER_PATH_SUFFIX = "/-/ubuntu/amd64/24.04/executable/openclaw-installer-latest.signed" IMAGE_PATH_SUFFIX = "/-/ubuntu/amd64/24.04/container/docker/openclaw-latest.tar.gz" INSTALLER_PATH = "/tmp/openclaw-installer" SIGNED_DOWNLOAD_PATH = "/tmp/openclaw-installer.signed" SIG_TEMP_PATH = "/tmp/openclaw-installer.sig.asc" SIGNED_FILE_MARKER = b"\n---OPENCLAW-SIG---\n" ENV_FILE_PATH = "/opt/openclaw/.env" CONFIG_JSON_PATH = "/opt/openclaw/config/openclaw.json" PUB_KEY_PATH_SUFFIX = "/-/gpg/nydus-signing.pub.asc" PUB_KEY_DOWNLOAD_PATH = "/tmp/nydus-signing.pub.asc" _CONTAINER_NAME_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_.-]*$") _INSTALLER_ARG_RE = re.compile(r"^[^\s;&|`$<>\"']*$") _REDACTED = "<>" def _minimal_log_entry(entry): """Return only step name and success for completion callback (keeps payload small).""" return {"step": entry.get("step", ""), "success": entry.get("success", False)} def _redact(text, secrets): """Replace each non-empty secret in *text* with a redaction marker.""" for s in secrets: if s: text = text.replace(s, _REDACTED) return text def _run_step(cmd, tag, step_name, omit_strings=None): """Run a shell command and return a structured log entry.""" primary_omit = omit_strings[0] if omit_strings else None exit_code, outs, errs = runCommand(cmd, tag, useShell=True, omitString=primary_omit) log_entry = { "step": step_name, "success": exit_code == 0, "stdout": outs, "stderr": errs, } if omit_strings: log_entry["stdout"] = _redact(log_entry["stdout"], omit_strings) log_entry["stderr"] = _redact(log_entry["stderr"], omit_strings) return exit_code, outs, errs, log_entry def _run_step_argv(cmd_list, tag, step_name, omit_strings=None): """Run a command as an argv list (no shell) and return a structured log entry.""" primary_omit = omit_strings[0] if omit_strings else None exit_code, outs, errs = runCommand(cmd_list, tag, useShell=False, omitString=primary_omit) log_entry = { "step": step_name, "success": exit_code == 0, "stdout": outs, "stderr": errs, } if omit_strings: log_entry["stdout"] = _redact(log_entry["stdout"], omit_strings) log_entry["stderr"] = _redact(log_entry["stderr"], omit_strings) return exit_code, outs, errs, log_entry def _validate_installer_arg(value, field_name): """Validate an installer argument; reject shell metacharacters.""" if value is None: return "" value_str = str(value) if not _INSTALLER_ARG_RE.match(value_str): raise ValueError(f"Invalid characters in {field_name}") return value_str def _validate_base_url(raw_url): """Validate and normalize an installer base URL (must have https/http scheme and a host).""" url = raw_url.strip().rstrip("/") parsed = urlparse(url) if parsed.scheme not in ("https", "http"): raise ValueError( f"installer_base_url must use https or http scheme, got: {parsed.scheme!r}") if not parsed.netloc: raise ValueError("installer_base_url must include a hostname") return f"{parsed.scheme}://{parsed.netloc}{parsed.path}" def _install_fail(logs: List[dict], step_failed: str) -> NydusResult: return False, json.dumps({ "logs": logs, "step_failed": step_failed, "setup_complete": False, }) def _resolve_urls(payload_dict: Dict[str, Any]) -> Tuple[str, str, str]: """Return (installer_url, pub_key_url, image_url) from the validated base URL.""" raw_base = ( payload_dict.get("installer_base_url") or payload_dict.get("pypi_url") or DEFAULT_INSTALLER_BASE ) base = _validate_base_url(raw_base) installer_url = base + INSTALLER_PATH_SUFFIX pub_key_url = base + PUB_KEY_PATH_SUFFIX image_url = base + IMAGE_PATH_SUFFIX return installer_url, pub_key_url, image_url def _download_signed_installer(logs: List[dict], installer_url: str) -> Optional[NydusResult]: exit_code, _, _, log_entry = _run_step_argv( ["curl", "-fSL", "-o", SIGNED_DOWNLOAD_PATH, installer_url], "openclaw_download", "download_signed_installer") logs.append(log_entry) if exit_code != 0: return _install_fail(logs, "download_signed_installer") return None def _download_public_key(logs: List[dict], pub_key_url: str) -> Optional[NydusResult]: exit_code, _, _, log_entry = _run_step_argv( ["curl", "-fSL", "-o", PUB_KEY_DOWNLOAD_PATH, pub_key_url], "openclaw_download_pubkey", "download_pubkey") logs.append(log_entry) if exit_code != 0: return _install_fail(logs, "download_pubkey") return None def _extract_installer_from_signed_file(logs: List[dict]) -> Optional[NydusResult]: try: with open(SIGNED_DOWNLOAD_PATH, "rb") as handle: data_bytes = handle.read() idx = data_bytes.find(SIGNED_FILE_MARKER) if idx < 0: logs.append({ "step": "extract_signed", "success": False, "stderr": "Marker not found in .signed file", }) return _install_fail(logs, "extract_signed") with open(INSTALLER_PATH, "wb") as handle: handle.write(data_bytes[:idx]) with open(SIG_TEMP_PATH, "wb") as handle: handle.write(data_bytes[idx + len(SIGNED_FILE_MARKER):]) except OSError as exc: logs.append({ "step": "extract_signed", "success": False, "stderr": str(exc), }) return _install_fail(logs, "extract_signed") logs.append({ "step": "extract_signed", "success": True, "stdout": "", "stderr": "", }) return None def _verify_signed_installer_with_gpg(logs: List[dict]) -> Optional[NydusResult]: steps = ( ( "command -v gpg >/dev/null 2>&1 || " "(apt-get update -qq && apt-get install -y -qq gnupg)", "openclaw_ensure_gpg", "ensure_gpg", "ensure_gpg", ), ( f"gpg --batch --import {PUB_KEY_DOWNLOAD_PATH}", "openclaw_gpg_import", "gpg_import", "gpg_import", ), ( f"gpg --batch --verify {SIG_TEMP_PATH} {INSTALLER_PATH}", "openclaw_gpg_verify", "gpg_verify", "gpg_verify", ), ( f"chmod +x {INSTALLER_PATH}", "openclaw_chmod", "chmod_installer", "chmod_installer", ), ) for cmd, tag, step_name, step_key in steps: exit_code, _, _, log_entry = _run_step(cmd, tag, step_name) logs.append(log_entry) if exit_code != 0: return _install_fail(logs, step_key) return None def _cleanup_installer_artifacts(): """Remove signed bundle, signature, and public key left over from installer verification.""" for path in (SIGNED_DOWNLOAD_PATH, SIG_TEMP_PATH, PUB_KEY_DOWNLOAD_PATH): try: os.remove(path) except OSError: pass def _run_installer_binary_and_check_env(logs: List[dict], api_key: str, telegram_token: str, email: str, image_url: str = "") -> NydusResult: sensitive = [v for v in (api_key, telegram_token) if v] installer_cmd = [ "sudo", INSTALLER_PATH, "install", f"--api-key={api_key}", "--https", f"--telegram-token={telegram_token}", f"--email={email}", ] if image_url: installer_cmd.append(f"--image-url={image_url}") exit_code, _, _, log_entry = _run_step_argv( installer_cmd, "openclaw_install", "run_installer", omit_strings=sensitive) logs.append(log_entry) install_ok = exit_code == 0 _, v_outs, _, verify_entry = _run_step( f"test -f {ENV_FILE_PATH} && echo 'VERIFIED' || echo 'NOT_FOUND'", "openclaw_verify", "verify_installation", ) verify_entry["success"] = "VERIFIED" in v_outs logs.append(verify_entry) setup_complete = install_ok and "VERIFIED" in v_outs data = { "logs": [_minimal_log_entry(e) for e in logs], "setup_complete": setup_complete, "status": "complete" if setup_complete else "failed", } if not setup_complete: data["step_failed"] = ( "run_installer" if not install_ok else "verify_installation") return setup_complete, json.dumps(data) class OpenClaw(Ops): # pylint: disable=abstract-method """Nydus ops for installing and managing OpenClaw on customer VMs.""" def install_openclaw(self, payload) -> NydusResult: """Download signed installer, verify with GPG, install, and verify OpenClaw.""" logs: List[dict] = [] pl = payload or {} try: api_key = _validate_installer_arg(pl.get("api_key", ""), "api_key") telegram_token = _validate_installer_arg( pl.get("telegram_token", ""), "telegram_token") email = _validate_installer_arg(pl.get("email", ""), "email") installer_url, pub_key_url, image_url = _resolve_urls(pl) except ValueError as exc: data = {"logs": [], "step_failed": "validation", "setup_complete": False, "error": str(exc)} return False, json.dumps(data) try: for step_fn in ( lambda: _download_signed_installer(logs, installer_url), lambda: _download_public_key(logs, pub_key_url), lambda: _extract_installer_from_signed_file(logs), lambda: _verify_signed_installer_with_gpg(logs), ): err = step_fn() if err is not None: return err finally: _cleanup_installer_artifacts() return _run_installer_binary_and_check_env( logs, api_key, telegram_token, email, image_url=image_url) def update_openclaw(self, payload) -> NydusResult: """Re-download the installer binary, verify with GPG, then run ``openclaw-installer update`` to replace the Docker image.""" logs: List[dict] = [] pl = payload or {} try: installer_url, pub_key_url, image_url = _resolve_urls(pl) except ValueError as exc: data = {"logs": [], "step_failed": "validation", "setup_complete": False, "error": str(exc)} return False, json.dumps(data) try: for step_fn in ( lambda: _download_signed_installer(logs, installer_url), lambda: _download_public_key(logs, pub_key_url), lambda: _extract_installer_from_signed_file(logs), lambda: _verify_signed_installer_with_gpg(logs), ): err = step_fn() if err is not None: return err finally: _cleanup_installer_artifacts() cmd = ["sudo", INSTALLER_PATH, "update"] if image_url: cmd.append(f"--image-url={image_url}") exit_code, _, _, log_entry = _run_step_argv( cmd, "openclaw_update", "run_update") logs.append(log_entry) data = { "logs": [_minimal_log_entry(e) for e in logs], "setup_complete": exit_code == 0, "status": "complete" if exit_code == 0 else "failed", } if exit_code != 0: data["step_failed"] = "run_update" return exit_code == 0, json.dumps(data) def get_openclaw_setup_status(self) -> NydusResult: """Check if OpenClaw env file and docker containers are present.""" logs = [] env_cmd = f"test -f {ENV_FILE_PATH} && echo 'exists' || echo 'missing'" _, outs, _, log_entry = _run_step( env_cmd, "openclaw_env_check", "env_file_check") env_exists = "exists" in outs log_entry["success"] = env_exists logs.append(log_entry) docker_cmd = ( "sudo docker ps --filter 'name=openclaw'" " --format '{{.Names}}:{{.Status}}'") exit_code, outs, errs, log_entry = _run_step( docker_cmd, "openclaw_docker_check", "docker_containers_check") containers_running = exit_code == 0 and len(outs.strip()) > 0 log_entry["success"] = containers_running logs.append(log_entry) if exit_code != 0: data = { "setup_complete": False, "env_file_exists": env_exists, "containers": outs.strip(), "error": "docker ps failed: " + errs.strip(), "logs": [_minimal_log_entry(e) for e in logs], } return False, json.dumps(data) data = { "setup_complete": env_exists and containers_running, "env_file_exists": env_exists, "containers": outs.strip(), "logs": [_minimal_log_entry(e) for e in logs], } return True, json.dumps(data) def get_openclaw_gateway_token(self) -> NydusResult: """Extract gateway token from openclaw.json (single source of truth).""" try: with open(CONFIG_JSON_PATH, "r", encoding="utf-8") as fh: cfg = json.load(fh) token_value = (cfg.get("gateway", {}) .get("auth", {}) .get("token", "")) except (OSError, json.JSONDecodeError) as exc: data = {"token": None, "error": str(exc)} return False, json.dumps(data) if not token_value: data = {"token": None, "error": "gateway.auth.token missing in openclaw.json"} return False, json.dumps(data) data = {"token": token_value} return True, json.dumps(data) def manage_openclaw_containers(self, payload) -> NydusResult: """List, start, or stop OpenClaw docker containers.""" action = payload.get("action", "list") container_name = payload.get("container_name", "") if action == "list": cmd = ("sudo docker ps -a --filter 'name=openclaw'" " --format '{{json .}}'") exit_code, outs, errs = runCommand( cmd, "openclaw_docker_list", useShell=True) containers = [] for line in outs.strip().splitlines(): if line.strip(): try: containers.append(json.loads(line)) except json.JSONDecodeError: containers.append({"raw": line}) data = {"action": action, "containers": containers, "error": errs.strip()} return exit_code == 0, json.dumps(data) if action in ("start", "stop"): if not container_name: data = {"action": action, "error": "container_name is required for start/stop"} return False, json.dumps(data) if not _CONTAINER_NAME_RE.match(container_name): data = {"action": action, "container_name": container_name, "error": "Invalid container_name; only letters, " "digits, '.', '-', and '_' are allowed"} return False, json.dumps(data) cmd = ["sudo", "docker", action, container_name] exit_code, outs, errs = runCommand( cmd, f"openclaw_docker_{action}", useShell=False) data = {"action": action, "container_name": container_name, "result": outs.strip(), "error": errs.strip()} return exit_code == 0, json.dumps(data) data = {"action": action, "error": f"Unknown action: {action}"} return False, json.dumps(data)