lib.rs 146 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244
  1. use std::collections::{BTreeMap, BTreeSet};
  2. use std::path::{Path, PathBuf};
  3. use std::process::Command;
  4. use std::time::{Duration, Instant};
  5. use api::{
  6. detect_provider_kind, max_tokens_for_model, resolve_model_alias, ContentBlockDelta,
  7. InputContentBlock, InputMessage, MessageRequest, MessageResponse, OutputContentBlock,
  8. ProviderClient, ProviderKind, StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition,
  9. ToolResultContentBlock,
  10. };
  11. use reqwest::blocking::Client;
  12. use runtime::{
  13. edit_file, execute_bash, glob_search, grep_search, load_system_prompt, read_file, write_file,
  14. ApiClient, ApiRequest, AssistantEvent, BashCommandInput, ContentBlock, ConversationMessage,
  15. ConversationRuntime, GrepSearchInput, MessageRole, PermissionMode, PermissionPolicy,
  16. RuntimeError, Session, TokenUsage, ToolError, ToolExecutor,
  17. };
  18. use serde::{Deserialize, Serialize};
  19. use serde_json::{json, Value};
  20. #[derive(Debug, Clone, PartialEq, Eq)]
  21. pub struct ToolManifestEntry {
  22. pub name: String,
  23. pub source: ToolSource,
  24. }
  25. #[derive(Debug, Clone, Copy, PartialEq, Eq)]
  26. pub enum ToolSource {
  27. Base,
  28. Conditional,
  29. }
  30. #[derive(Debug, Clone, Default, PartialEq, Eq)]
  31. pub struct ToolRegistry {
  32. entries: Vec<ToolManifestEntry>,
  33. }
  34. impl ToolRegistry {
  35. #[must_use]
  36. pub fn new(entries: Vec<ToolManifestEntry>) -> Self {
  37. Self { entries }
  38. }
  39. #[must_use]
  40. pub fn entries(&self) -> &[ToolManifestEntry] {
  41. &self.entries
  42. }
  43. }
  44. #[derive(Debug, Clone, PartialEq, Eq)]
  45. pub struct ToolSpec {
  46. pub name: &'static str,
  47. pub description: &'static str,
  48. pub input_schema: Value,
  49. pub required_permission: PermissionMode,
  50. }
  51. #[must_use]
  52. #[allow(clippy::too_many_lines)]
  53. pub fn mvp_tool_specs() -> Vec<ToolSpec> {
  54. vec![
  55. ToolSpec {
  56. name: "bash",
  57. description: "Execute a shell command in the current workspace.",
  58. input_schema: json!({
  59. "type": "object",
  60. "properties": {
  61. "command": { "type": "string" },
  62. "timeout": { "type": "integer", "minimum": 1 },
  63. "description": { "type": "string" },
  64. "run_in_background": { "type": "boolean" },
  65. "dangerouslyDisableSandbox": { "type": "boolean" }
  66. },
  67. "required": ["command"],
  68. "additionalProperties": false
  69. }),
  70. required_permission: PermissionMode::DangerFullAccess,
  71. },
  72. ToolSpec {
  73. name: "read_file",
  74. description: "Read a text file from the workspace.",
  75. input_schema: json!({
  76. "type": "object",
  77. "properties": {
  78. "path": { "type": "string" },
  79. "offset": { "type": "integer", "minimum": 0 },
  80. "limit": { "type": "integer", "minimum": 1 }
  81. },
  82. "required": ["path"],
  83. "additionalProperties": false
  84. }),
  85. required_permission: PermissionMode::ReadOnly,
  86. },
  87. ToolSpec {
  88. name: "write_file",
  89. description: "Write a text file in the workspace.",
  90. input_schema: json!({
  91. "type": "object",
  92. "properties": {
  93. "path": { "type": "string" },
  94. "content": { "type": "string" }
  95. },
  96. "required": ["path", "content"],
  97. "additionalProperties": false
  98. }),
  99. required_permission: PermissionMode::WorkspaceWrite,
  100. },
  101. ToolSpec {
  102. name: "edit_file",
  103. description: "Replace text in a workspace file.",
  104. input_schema: json!({
  105. "type": "object",
  106. "properties": {
  107. "path": { "type": "string" },
  108. "old_string": { "type": "string" },
  109. "new_string": { "type": "string" },
  110. "replace_all": { "type": "boolean" }
  111. },
  112. "required": ["path", "old_string", "new_string"],
  113. "additionalProperties": false
  114. }),
  115. required_permission: PermissionMode::WorkspaceWrite,
  116. },
  117. ToolSpec {
  118. name: "glob_search",
  119. description: "Find files by glob pattern.",
  120. input_schema: json!({
  121. "type": "object",
  122. "properties": {
  123. "pattern": { "type": "string" },
  124. "path": { "type": "string" }
  125. },
  126. "required": ["pattern"],
  127. "additionalProperties": false
  128. }),
  129. required_permission: PermissionMode::ReadOnly,
  130. },
  131. ToolSpec {
  132. name: "grep_search",
  133. description: "Search file contents with a regex pattern.",
  134. input_schema: json!({
  135. "type": "object",
  136. "properties": {
  137. "pattern": { "type": "string" },
  138. "path": { "type": "string" },
  139. "glob": { "type": "string" },
  140. "output_mode": { "type": "string" },
  141. "-B": { "type": "integer", "minimum": 0 },
  142. "-A": { "type": "integer", "minimum": 0 },
  143. "-C": { "type": "integer", "minimum": 0 },
  144. "context": { "type": "integer", "minimum": 0 },
  145. "-n": { "type": "boolean" },
  146. "-i": { "type": "boolean" },
  147. "type": { "type": "string" },
  148. "head_limit": { "type": "integer", "minimum": 1 },
  149. "offset": { "type": "integer", "minimum": 0 },
  150. "multiline": { "type": "boolean" }
  151. },
  152. "required": ["pattern"],
  153. "additionalProperties": false
  154. }),
  155. required_permission: PermissionMode::ReadOnly,
  156. },
  157. ToolSpec {
  158. name: "WebFetch",
  159. description:
  160. "Fetch a URL, convert it into readable text, and answer a prompt about it.",
  161. input_schema: json!({
  162. "type": "object",
  163. "properties": {
  164. "url": { "type": "string", "format": "uri" },
  165. "prompt": { "type": "string" }
  166. },
  167. "required": ["url", "prompt"],
  168. "additionalProperties": false
  169. }),
  170. required_permission: PermissionMode::ReadOnly,
  171. },
  172. ToolSpec {
  173. name: "WebSearch",
  174. description: "Search the web for current information and return cited results.",
  175. input_schema: json!({
  176. "type": "object",
  177. "properties": {
  178. "query": { "type": "string", "minLength": 2 },
  179. "allowed_domains": {
  180. "type": "array",
  181. "items": { "type": "string" }
  182. },
  183. "blocked_domains": {
  184. "type": "array",
  185. "items": { "type": "string" }
  186. }
  187. },
  188. "required": ["query"],
  189. "additionalProperties": false
  190. }),
  191. required_permission: PermissionMode::ReadOnly,
  192. },
  193. ToolSpec {
  194. name: "TodoWrite",
  195. description: "Update the structured task list for the current session.",
  196. input_schema: json!({
  197. "type": "object",
  198. "properties": {
  199. "todos": {
  200. "type": "array",
  201. "items": {
  202. "type": "object",
  203. "properties": {
  204. "content": { "type": "string" },
  205. "activeForm": { "type": "string" },
  206. "status": {
  207. "type": "string",
  208. "enum": ["pending", "in_progress", "completed"]
  209. }
  210. },
  211. "required": ["content", "activeForm", "status"],
  212. "additionalProperties": false
  213. }
  214. }
  215. },
  216. "required": ["todos"],
  217. "additionalProperties": false
  218. }),
  219. required_permission: PermissionMode::WorkspaceWrite,
  220. },
  221. ToolSpec {
  222. name: "Skill",
  223. description: "Load a local skill definition and its instructions.",
  224. input_schema: json!({
  225. "type": "object",
  226. "properties": {
  227. "skill": { "type": "string" },
  228. "args": { "type": "string" }
  229. },
  230. "required": ["skill"],
  231. "additionalProperties": false
  232. }),
  233. required_permission: PermissionMode::ReadOnly,
  234. },
  235. ToolSpec {
  236. name: "Agent",
  237. description: "Launch a specialized agent task and persist its handoff metadata.",
  238. input_schema: json!({
  239. "type": "object",
  240. "properties": {
  241. "description": { "type": "string" },
  242. "prompt": { "type": "string" },
  243. "subagent_type": { "type": "string" },
  244. "name": { "type": "string" },
  245. "model": { "type": "string" }
  246. },
  247. "required": ["description", "prompt"],
  248. "additionalProperties": false
  249. }),
  250. required_permission: PermissionMode::DangerFullAccess,
  251. },
  252. ToolSpec {
  253. name: "ToolSearch",
  254. description: "Search for deferred or specialized tools by exact name or keywords.",
  255. input_schema: json!({
  256. "type": "object",
  257. "properties": {
  258. "query": { "type": "string" },
  259. "max_results": { "type": "integer", "minimum": 1 }
  260. },
  261. "required": ["query"],
  262. "additionalProperties": false
  263. }),
  264. required_permission: PermissionMode::ReadOnly,
  265. },
  266. ToolSpec {
  267. name: "NotebookEdit",
  268. description: "Replace, insert, or delete a cell in a Jupyter notebook.",
  269. input_schema: json!({
  270. "type": "object",
  271. "properties": {
  272. "notebook_path": { "type": "string" },
  273. "cell_id": { "type": "string" },
  274. "new_source": { "type": "string" },
  275. "cell_type": { "type": "string", "enum": ["code", "markdown"] },
  276. "edit_mode": { "type": "string", "enum": ["replace", "insert", "delete"] }
  277. },
  278. "required": ["notebook_path"],
  279. "additionalProperties": false
  280. }),
  281. required_permission: PermissionMode::WorkspaceWrite,
  282. },
  283. ToolSpec {
  284. name: "Sleep",
  285. description: "Wait for a specified duration without holding a shell process.",
  286. input_schema: json!({
  287. "type": "object",
  288. "properties": {
  289. "duration_ms": { "type": "integer", "minimum": 0 }
  290. },
  291. "required": ["duration_ms"],
  292. "additionalProperties": false
  293. }),
  294. required_permission: PermissionMode::ReadOnly,
  295. },
  296. ToolSpec {
  297. name: "SendUserMessage",
  298. description: "Send a message to the user.",
  299. input_schema: json!({
  300. "type": "object",
  301. "properties": {
  302. "message": { "type": "string" },
  303. "attachments": {
  304. "type": "array",
  305. "items": { "type": "string" }
  306. },
  307. "status": {
  308. "type": "string",
  309. "enum": ["normal", "proactive"]
  310. }
  311. },
  312. "required": ["message", "status"],
  313. "additionalProperties": false
  314. }),
  315. required_permission: PermissionMode::ReadOnly,
  316. },
  317. ToolSpec {
  318. name: "Config",
  319. description: "Get or set Claude Code settings.",
  320. input_schema: json!({
  321. "type": "object",
  322. "properties": {
  323. "setting": { "type": "string" },
  324. "value": {
  325. "type": ["string", "boolean", "number"]
  326. }
  327. },
  328. "required": ["setting"],
  329. "additionalProperties": false
  330. }),
  331. required_permission: PermissionMode::WorkspaceWrite,
  332. },
  333. ToolSpec {
  334. name: "StructuredOutput",
  335. description: "Return structured output in the requested format.",
  336. input_schema: json!({
  337. "type": "object",
  338. "additionalProperties": true
  339. }),
  340. required_permission: PermissionMode::ReadOnly,
  341. },
  342. ToolSpec {
  343. name: "REPL",
  344. description: "Execute code in a REPL-like subprocess.",
  345. input_schema: json!({
  346. "type": "object",
  347. "properties": {
  348. "code": { "type": "string" },
  349. "language": { "type": "string" },
  350. "timeout_ms": { "type": "integer", "minimum": 1 }
  351. },
  352. "required": ["code", "language"],
  353. "additionalProperties": false
  354. }),
  355. required_permission: PermissionMode::DangerFullAccess,
  356. },
  357. ToolSpec {
  358. name: "PowerShell",
  359. description: "Execute a PowerShell command with optional timeout.",
  360. input_schema: json!({
  361. "type": "object",
  362. "properties": {
  363. "command": { "type": "string" },
  364. "timeout": { "type": "integer", "minimum": 1 },
  365. "description": { "type": "string" },
  366. "run_in_background": { "type": "boolean" }
  367. },
  368. "required": ["command"],
  369. "additionalProperties": false
  370. }),
  371. required_permission: PermissionMode::DangerFullAccess,
  372. },
  373. ]
  374. }
  375. pub fn execute_tool(name: &str, input: &Value) -> Result<String, String> {
  376. match name {
  377. "bash" => from_value::<BashCommandInput>(input).and_then(run_bash),
  378. "read_file" => from_value::<ReadFileInput>(input).and_then(run_read_file),
  379. "write_file" => from_value::<WriteFileInput>(input).and_then(run_write_file),
  380. "edit_file" => from_value::<EditFileInput>(input).and_then(run_edit_file),
  381. "glob_search" => from_value::<GlobSearchInputValue>(input).and_then(run_glob_search),
  382. "grep_search" => from_value::<GrepSearchInput>(input).and_then(run_grep_search),
  383. "WebFetch" => from_value::<WebFetchInput>(input).and_then(run_web_fetch),
  384. "WebSearch" => from_value::<WebSearchInput>(input).and_then(run_web_search),
  385. "TodoWrite" => from_value::<TodoWriteInput>(input).and_then(run_todo_write),
  386. "Skill" => from_value::<SkillInput>(input).and_then(run_skill),
  387. "Agent" => from_value::<AgentInput>(input).and_then(run_agent),
  388. "ToolSearch" => from_value::<ToolSearchInput>(input).and_then(run_tool_search),
  389. "NotebookEdit" => from_value::<NotebookEditInput>(input).and_then(run_notebook_edit),
  390. "Sleep" => from_value::<SleepInput>(input).and_then(run_sleep),
  391. "SendUserMessage" | "Brief" => from_value::<BriefInput>(input).and_then(run_brief),
  392. "Config" => from_value::<ConfigInput>(input).and_then(run_config),
  393. "StructuredOutput" => {
  394. from_value::<StructuredOutputInput>(input).and_then(run_structured_output)
  395. }
  396. "REPL" => from_value::<ReplInput>(input).and_then(run_repl),
  397. "PowerShell" => from_value::<PowerShellInput>(input).and_then(run_powershell),
  398. _ => Err(format!("unsupported tool: {name}")),
  399. }
  400. }
  401. fn from_value<T: for<'de> Deserialize<'de>>(input: &Value) -> Result<T, String> {
  402. serde_json::from_value(input.clone()).map_err(|error| error.to_string())
  403. }
  404. fn run_bash(input: BashCommandInput) -> Result<String, String> {
  405. serde_json::to_string_pretty(&execute_bash(input).map_err(|error| error.to_string())?)
  406. .map_err(|error| error.to_string())
  407. }
  408. #[allow(clippy::needless_pass_by_value)]
  409. fn run_read_file(input: ReadFileInput) -> Result<String, String> {
  410. to_pretty_json(read_file(&input.path, input.offset, input.limit).map_err(io_to_string)?)
  411. }
  412. #[allow(clippy::needless_pass_by_value)]
  413. fn run_write_file(input: WriteFileInput) -> Result<String, String> {
  414. to_pretty_json(write_file(&input.path, &input.content).map_err(io_to_string)?)
  415. }
  416. #[allow(clippy::needless_pass_by_value)]
  417. fn run_edit_file(input: EditFileInput) -> Result<String, String> {
  418. to_pretty_json(
  419. edit_file(
  420. &input.path,
  421. &input.old_string,
  422. &input.new_string,
  423. input.replace_all.unwrap_or(false),
  424. )
  425. .map_err(io_to_string)?,
  426. )
  427. }
  428. #[allow(clippy::needless_pass_by_value)]
  429. fn run_glob_search(input: GlobSearchInputValue) -> Result<String, String> {
  430. to_pretty_json(glob_search(&input.pattern, input.path.as_deref()).map_err(io_to_string)?)
  431. }
  432. #[allow(clippy::needless_pass_by_value)]
  433. fn run_grep_search(input: GrepSearchInput) -> Result<String, String> {
  434. to_pretty_json(grep_search(&input).map_err(io_to_string)?)
  435. }
  436. #[allow(clippy::needless_pass_by_value)]
  437. fn run_web_fetch(input: WebFetchInput) -> Result<String, String> {
  438. to_pretty_json(execute_web_fetch(&input)?)
  439. }
  440. #[allow(clippy::needless_pass_by_value)]
  441. fn run_web_search(input: WebSearchInput) -> Result<String, String> {
  442. to_pretty_json(execute_web_search(&input)?)
  443. }
  444. fn run_todo_write(input: TodoWriteInput) -> Result<String, String> {
  445. to_pretty_json(execute_todo_write(input)?)
  446. }
  447. fn run_skill(input: SkillInput) -> Result<String, String> {
  448. to_pretty_json(execute_skill(input)?)
  449. }
  450. fn run_agent(input: AgentInput) -> Result<String, String> {
  451. to_pretty_json(execute_agent(input)?)
  452. }
  453. fn run_tool_search(input: ToolSearchInput) -> Result<String, String> {
  454. to_pretty_json(execute_tool_search(input))
  455. }
  456. fn run_notebook_edit(input: NotebookEditInput) -> Result<String, String> {
  457. to_pretty_json(execute_notebook_edit(input)?)
  458. }
  459. fn run_sleep(input: SleepInput) -> Result<String, String> {
  460. to_pretty_json(execute_sleep(input))
  461. }
  462. fn run_brief(input: BriefInput) -> Result<String, String> {
  463. to_pretty_json(execute_brief(input)?)
  464. }
  465. fn run_config(input: ConfigInput) -> Result<String, String> {
  466. to_pretty_json(execute_config(input)?)
  467. }
  468. fn run_structured_output(input: StructuredOutputInput) -> Result<String, String> {
  469. to_pretty_json(execute_structured_output(input))
  470. }
  471. fn run_repl(input: ReplInput) -> Result<String, String> {
  472. to_pretty_json(execute_repl(input)?)
  473. }
  474. fn run_powershell(input: PowerShellInput) -> Result<String, String> {
  475. to_pretty_json(execute_powershell(input).map_err(|error| error.to_string())?)
  476. }
  477. fn to_pretty_json<T: serde::Serialize>(value: T) -> Result<String, String> {
  478. serde_json::to_string_pretty(&value).map_err(|error| error.to_string())
  479. }
  480. #[allow(clippy::needless_pass_by_value)]
  481. fn io_to_string(error: std::io::Error) -> String {
  482. error.to_string()
  483. }
  484. #[derive(Debug, Deserialize)]
  485. struct ReadFileInput {
  486. path: String,
  487. offset: Option<usize>,
  488. limit: Option<usize>,
  489. }
  490. #[derive(Debug, Deserialize)]
  491. struct WriteFileInput {
  492. path: String,
  493. content: String,
  494. }
  495. #[derive(Debug, Deserialize)]
  496. struct EditFileInput {
  497. path: String,
  498. old_string: String,
  499. new_string: String,
  500. replace_all: Option<bool>,
  501. }
  502. #[derive(Debug, Deserialize)]
  503. struct GlobSearchInputValue {
  504. pattern: String,
  505. path: Option<String>,
  506. }
  507. #[derive(Debug, Deserialize)]
  508. struct WebFetchInput {
  509. url: String,
  510. prompt: String,
  511. }
  512. #[derive(Debug, Deserialize)]
  513. struct WebSearchInput {
  514. query: String,
  515. allowed_domains: Option<Vec<String>>,
  516. blocked_domains: Option<Vec<String>>,
  517. }
  518. #[derive(Debug, Deserialize)]
  519. struct TodoWriteInput {
  520. todos: Vec<TodoItem>,
  521. }
  522. #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
  523. struct TodoItem {
  524. content: String,
  525. #[serde(rename = "activeForm")]
  526. active_form: String,
  527. status: TodoStatus,
  528. }
  529. #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
  530. #[serde(rename_all = "snake_case")]
  531. enum TodoStatus {
  532. Pending,
  533. InProgress,
  534. Completed,
  535. }
  536. #[derive(Debug, Deserialize)]
  537. struct SkillInput {
  538. skill: String,
  539. args: Option<String>,
  540. }
  541. #[derive(Debug, Deserialize)]
  542. struct AgentInput {
  543. description: String,
  544. prompt: String,
  545. subagent_type: Option<String>,
  546. name: Option<String>,
  547. model: Option<String>,
  548. }
  549. #[derive(Debug, Deserialize)]
  550. struct ToolSearchInput {
  551. query: String,
  552. max_results: Option<usize>,
  553. }
  554. #[derive(Debug, Deserialize)]
  555. struct NotebookEditInput {
  556. notebook_path: String,
  557. cell_id: Option<String>,
  558. new_source: Option<String>,
  559. cell_type: Option<NotebookCellType>,
  560. edit_mode: Option<NotebookEditMode>,
  561. }
  562. #[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
  563. #[serde(rename_all = "lowercase")]
  564. enum NotebookCellType {
  565. Code,
  566. Markdown,
  567. }
  568. #[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
  569. #[serde(rename_all = "lowercase")]
  570. enum NotebookEditMode {
  571. Replace,
  572. Insert,
  573. Delete,
  574. }
  575. #[derive(Debug, Deserialize)]
  576. struct SleepInput {
  577. duration_ms: u64,
  578. }
  579. #[derive(Debug, Deserialize)]
  580. struct BriefInput {
  581. message: String,
  582. attachments: Option<Vec<String>>,
  583. status: BriefStatus,
  584. }
  585. #[derive(Debug, Deserialize)]
  586. #[serde(rename_all = "lowercase")]
  587. enum BriefStatus {
  588. Normal,
  589. Proactive,
  590. }
  591. #[derive(Debug, Deserialize)]
  592. struct ConfigInput {
  593. setting: String,
  594. value: Option<ConfigValue>,
  595. }
  596. #[derive(Debug, Deserialize)]
  597. #[serde(untagged)]
  598. enum ConfigValue {
  599. String(String),
  600. Bool(bool),
  601. Number(f64),
  602. }
  603. #[derive(Debug, Deserialize)]
  604. #[serde(transparent)]
  605. struct StructuredOutputInput(BTreeMap<String, Value>);
  606. #[derive(Debug, Deserialize)]
  607. struct ReplInput {
  608. code: String,
  609. language: String,
  610. timeout_ms: Option<u64>,
  611. }
  612. #[derive(Debug, Deserialize)]
  613. struct PowerShellInput {
  614. command: String,
  615. timeout: Option<u64>,
  616. description: Option<String>,
  617. run_in_background: Option<bool>,
  618. }
  619. #[derive(Debug, Serialize)]
  620. struct WebFetchOutput {
  621. bytes: usize,
  622. code: u16,
  623. #[serde(rename = "codeText")]
  624. code_text: String,
  625. result: String,
  626. #[serde(rename = "durationMs")]
  627. duration_ms: u128,
  628. url: String,
  629. }
  630. #[derive(Debug, Serialize)]
  631. struct WebSearchOutput {
  632. query: String,
  633. results: Vec<WebSearchResultItem>,
  634. #[serde(rename = "durationSeconds")]
  635. duration_seconds: f64,
  636. }
  637. #[derive(Debug, Serialize)]
  638. struct TodoWriteOutput {
  639. #[serde(rename = "oldTodos")]
  640. old_todos: Vec<TodoItem>,
  641. #[serde(rename = "newTodos")]
  642. new_todos: Vec<TodoItem>,
  643. #[serde(rename = "verificationNudgeNeeded")]
  644. verification_nudge_needed: Option<bool>,
  645. }
  646. #[derive(Debug, Serialize)]
  647. struct SkillOutput {
  648. skill: String,
  649. path: String,
  650. args: Option<String>,
  651. description: Option<String>,
  652. prompt: String,
  653. }
  654. #[derive(Debug, Clone, Serialize, Deserialize)]
  655. struct AgentOutput {
  656. #[serde(rename = "agentId")]
  657. agent_id: String,
  658. name: String,
  659. description: String,
  660. #[serde(rename = "subagentType")]
  661. subagent_type: Option<String>,
  662. model: Option<String>,
  663. status: String,
  664. #[serde(rename = "outputFile")]
  665. output_file: String,
  666. #[serde(rename = "manifestFile")]
  667. manifest_file: String,
  668. #[serde(rename = "createdAt")]
  669. created_at: String,
  670. #[serde(rename = "startedAt", skip_serializing_if = "Option::is_none")]
  671. started_at: Option<String>,
  672. #[serde(rename = "completedAt", skip_serializing_if = "Option::is_none")]
  673. completed_at: Option<String>,
  674. #[serde(skip_serializing_if = "Option::is_none")]
  675. error: Option<String>,
  676. }
  677. #[derive(Debug, Clone)]
  678. struct AgentJob {
  679. manifest: AgentOutput,
  680. prompt: String,
  681. system_prompt: Vec<String>,
  682. allowed_tools: BTreeSet<String>,
  683. }
  684. #[derive(Debug, Serialize)]
  685. struct ToolSearchOutput {
  686. matches: Vec<String>,
  687. query: String,
  688. normalized_query: String,
  689. #[serde(rename = "total_deferred_tools")]
  690. total_deferred_tools: usize,
  691. #[serde(rename = "pending_mcp_servers")]
  692. pending_mcp_servers: Option<Vec<String>>,
  693. }
  694. #[derive(Debug, Serialize)]
  695. struct NotebookEditOutput {
  696. new_source: String,
  697. cell_id: Option<String>,
  698. cell_type: Option<NotebookCellType>,
  699. language: String,
  700. edit_mode: String,
  701. error: Option<String>,
  702. notebook_path: String,
  703. original_file: String,
  704. updated_file: String,
  705. }
  706. #[derive(Debug, Serialize)]
  707. struct SleepOutput {
  708. duration_ms: u64,
  709. message: String,
  710. }
  711. #[derive(Debug, Serialize)]
  712. struct BriefOutput {
  713. message: String,
  714. attachments: Option<Vec<ResolvedAttachment>>,
  715. #[serde(rename = "sentAt")]
  716. sent_at: String,
  717. }
  718. #[derive(Debug, Serialize)]
  719. struct ResolvedAttachment {
  720. path: String,
  721. size: u64,
  722. #[serde(rename = "isImage")]
  723. is_image: bool,
  724. }
  725. #[derive(Debug, Serialize)]
  726. struct ConfigOutput {
  727. success: bool,
  728. operation: Option<String>,
  729. setting: Option<String>,
  730. value: Option<Value>,
  731. #[serde(rename = "previousValue")]
  732. previous_value: Option<Value>,
  733. #[serde(rename = "newValue")]
  734. new_value: Option<Value>,
  735. error: Option<String>,
  736. }
  737. #[derive(Debug, Serialize)]
  738. struct StructuredOutputResult {
  739. data: String,
  740. structured_output: BTreeMap<String, Value>,
  741. }
  742. #[derive(Debug, Serialize)]
  743. struct ReplOutput {
  744. language: String,
  745. stdout: String,
  746. stderr: String,
  747. #[serde(rename = "exitCode")]
  748. exit_code: i32,
  749. #[serde(rename = "durationMs")]
  750. duration_ms: u128,
  751. }
  752. #[derive(Debug, Serialize)]
  753. #[serde(untagged)]
  754. enum WebSearchResultItem {
  755. SearchResult {
  756. tool_use_id: String,
  757. content: Vec<SearchHit>,
  758. },
  759. Commentary(String),
  760. }
  761. #[derive(Debug, Serialize)]
  762. struct SearchHit {
  763. title: String,
  764. url: String,
  765. }
  766. fn execute_web_fetch(input: &WebFetchInput) -> Result<WebFetchOutput, String> {
  767. let started = Instant::now();
  768. let client = build_http_client()?;
  769. let request_url = normalize_fetch_url(&input.url)?;
  770. let response = client
  771. .get(request_url.clone())
  772. .send()
  773. .map_err(|error| error.to_string())?;
  774. let status = response.status();
  775. let final_url = response.url().to_string();
  776. let code = status.as_u16();
  777. let code_text = status.canonical_reason().unwrap_or("Unknown").to_string();
  778. let content_type = response
  779. .headers()
  780. .get(reqwest::header::CONTENT_TYPE)
  781. .and_then(|value| value.to_str().ok())
  782. .unwrap_or_default()
  783. .to_string();
  784. let body = response.text().map_err(|error| error.to_string())?;
  785. let bytes = body.len();
  786. let normalized = normalize_fetched_content(&body, &content_type);
  787. let result = summarize_web_fetch(&final_url, &input.prompt, &normalized, &body, &content_type);
  788. Ok(WebFetchOutput {
  789. bytes,
  790. code,
  791. code_text,
  792. result,
  793. duration_ms: started.elapsed().as_millis(),
  794. url: final_url,
  795. })
  796. }
  797. fn execute_web_search(input: &WebSearchInput) -> Result<WebSearchOutput, String> {
  798. let started = Instant::now();
  799. let client = build_http_client()?;
  800. let search_url = build_search_url(&input.query)?;
  801. let response = client
  802. .get(search_url)
  803. .send()
  804. .map_err(|error| error.to_string())?;
  805. let final_url = response.url().clone();
  806. let html = response.text().map_err(|error| error.to_string())?;
  807. let mut hits = extract_search_hits(&html);
  808. if hits.is_empty() && final_url.host_str().is_some() {
  809. hits = extract_search_hits_from_generic_links(&html);
  810. }
  811. if let Some(allowed) = input.allowed_domains.as_ref() {
  812. hits.retain(|hit| host_matches_list(&hit.url, allowed));
  813. }
  814. if let Some(blocked) = input.blocked_domains.as_ref() {
  815. hits.retain(|hit| !host_matches_list(&hit.url, blocked));
  816. }
  817. dedupe_hits(&mut hits);
  818. hits.truncate(8);
  819. let summary = if hits.is_empty() {
  820. format!("No web search results matched the query {:?}.", input.query)
  821. } else {
  822. let rendered_hits = hits
  823. .iter()
  824. .map(|hit| format!("- [{}]({})", hit.title, hit.url))
  825. .collect::<Vec<_>>()
  826. .join("\n");
  827. format!(
  828. "Search results for {:?}. Include a Sources section in the final answer.\n{}",
  829. input.query, rendered_hits
  830. )
  831. };
  832. Ok(WebSearchOutput {
  833. query: input.query.clone(),
  834. results: vec![
  835. WebSearchResultItem::Commentary(summary),
  836. WebSearchResultItem::SearchResult {
  837. tool_use_id: String::from("web_search_1"),
  838. content: hits,
  839. },
  840. ],
  841. duration_seconds: started.elapsed().as_secs_f64(),
  842. })
  843. }
  844. fn build_http_client() -> Result<Client, String> {
  845. Client::builder()
  846. .timeout(Duration::from_secs(20))
  847. .redirect(reqwest::redirect::Policy::limited(10))
  848. .user_agent("clawd-rust-tools/0.1")
  849. .build()
  850. .map_err(|error| error.to_string())
  851. }
  852. fn normalize_fetch_url(url: &str) -> Result<String, String> {
  853. let parsed = reqwest::Url::parse(url).map_err(|error| error.to_string())?;
  854. if parsed.scheme() == "http" {
  855. let host = parsed.host_str().unwrap_or_default();
  856. if host != "localhost" && host != "127.0.0.1" && host != "::1" {
  857. let mut upgraded = parsed;
  858. upgraded
  859. .set_scheme("https")
  860. .map_err(|()| String::from("failed to upgrade URL to https"))?;
  861. return Ok(upgraded.to_string());
  862. }
  863. }
  864. Ok(parsed.to_string())
  865. }
  866. fn build_search_url(query: &str) -> Result<reqwest::Url, String> {
  867. if let Ok(base) = std::env::var("CLAWD_WEB_SEARCH_BASE_URL") {
  868. let mut url = reqwest::Url::parse(&base).map_err(|error| error.to_string())?;
  869. url.query_pairs_mut().append_pair("q", query);
  870. return Ok(url);
  871. }
  872. let mut url = reqwest::Url::parse("https://html.duckduckgo.com/html/")
  873. .map_err(|error| error.to_string())?;
  874. url.query_pairs_mut().append_pair("q", query);
  875. Ok(url)
  876. }
  877. fn normalize_fetched_content(body: &str, content_type: &str) -> String {
  878. if content_type.contains("html") {
  879. html_to_text(body)
  880. } else {
  881. body.trim().to_string()
  882. }
  883. }
  884. fn summarize_web_fetch(
  885. url: &str,
  886. prompt: &str,
  887. content: &str,
  888. raw_body: &str,
  889. content_type: &str,
  890. ) -> String {
  891. let lower_prompt = prompt.to_lowercase();
  892. let compact = collapse_whitespace(content);
  893. let detail = if lower_prompt.contains("title") {
  894. extract_title(content, raw_body, content_type).map_or_else(
  895. || preview_text(&compact, 600),
  896. |title| format!("Title: {title}"),
  897. )
  898. } else if lower_prompt.contains("summary") || lower_prompt.contains("summarize") {
  899. preview_text(&compact, 900)
  900. } else {
  901. let preview = preview_text(&compact, 900);
  902. format!("Prompt: {prompt}\nContent preview:\n{preview}")
  903. };
  904. format!("Fetched {url}\n{detail}")
  905. }
  906. fn extract_title(content: &str, raw_body: &str, content_type: &str) -> Option<String> {
  907. if content_type.contains("html") {
  908. let lowered = raw_body.to_lowercase();
  909. if let Some(start) = lowered.find("<title>") {
  910. let after = start + "<title>".len();
  911. if let Some(end_rel) = lowered[after..].find("</title>") {
  912. let title =
  913. collapse_whitespace(&decode_html_entities(&raw_body[after..after + end_rel]));
  914. if !title.is_empty() {
  915. return Some(title);
  916. }
  917. }
  918. }
  919. }
  920. for line in content.lines() {
  921. let trimmed = line.trim();
  922. if !trimmed.is_empty() {
  923. return Some(trimmed.to_string());
  924. }
  925. }
  926. None
  927. }
  928. fn html_to_text(html: &str) -> String {
  929. let mut text = String::with_capacity(html.len());
  930. let mut in_tag = false;
  931. let mut previous_was_space = false;
  932. for ch in html.chars() {
  933. match ch {
  934. '<' => in_tag = true,
  935. '>' => in_tag = false,
  936. _ if in_tag => {}
  937. '&' => {
  938. text.push('&');
  939. previous_was_space = false;
  940. }
  941. ch if ch.is_whitespace() => {
  942. if !previous_was_space {
  943. text.push(' ');
  944. previous_was_space = true;
  945. }
  946. }
  947. _ => {
  948. text.push(ch);
  949. previous_was_space = false;
  950. }
  951. }
  952. }
  953. collapse_whitespace(&decode_html_entities(&text))
  954. }
  955. fn decode_html_entities(input: &str) -> String {
  956. input
  957. .replace("&amp;", "&")
  958. .replace("&lt;", "<")
  959. .replace("&gt;", ">")
  960. .replace("&quot;", "\"")
  961. .replace("&#39;", "'")
  962. .replace("&nbsp;", " ")
  963. }
  964. fn collapse_whitespace(input: &str) -> String {
  965. input.split_whitespace().collect::<Vec<_>>().join(" ")
  966. }
  967. fn preview_text(input: &str, max_chars: usize) -> String {
  968. if input.chars().count() <= max_chars {
  969. return input.to_string();
  970. }
  971. let shortened = input.chars().take(max_chars).collect::<String>();
  972. format!("{}…", shortened.trim_end())
  973. }
  974. fn extract_search_hits(html: &str) -> Vec<SearchHit> {
  975. let mut hits = Vec::new();
  976. let mut remaining = html;
  977. while let Some(anchor_start) = remaining.find("result__a") {
  978. let after_class = &remaining[anchor_start..];
  979. let Some(href_idx) = after_class.find("href=") else {
  980. remaining = &after_class[1..];
  981. continue;
  982. };
  983. let href_slice = &after_class[href_idx + 5..];
  984. let Some((url, rest)) = extract_quoted_value(href_slice) else {
  985. remaining = &after_class[1..];
  986. continue;
  987. };
  988. let Some(close_tag_idx) = rest.find('>') else {
  989. remaining = &after_class[1..];
  990. continue;
  991. };
  992. let after_tag = &rest[close_tag_idx + 1..];
  993. let Some(end_anchor_idx) = after_tag.find("</a>") else {
  994. remaining = &after_tag[1..];
  995. continue;
  996. };
  997. let title = html_to_text(&after_tag[..end_anchor_idx]);
  998. if let Some(decoded_url) = decode_duckduckgo_redirect(&url) {
  999. hits.push(SearchHit {
  1000. title: title.trim().to_string(),
  1001. url: decoded_url,
  1002. });
  1003. }
  1004. remaining = &after_tag[end_anchor_idx + 4..];
  1005. }
  1006. hits
  1007. }
  1008. fn extract_search_hits_from_generic_links(html: &str) -> Vec<SearchHit> {
  1009. let mut hits = Vec::new();
  1010. let mut remaining = html;
  1011. while let Some(anchor_start) = remaining.find("<a") {
  1012. let after_anchor = &remaining[anchor_start..];
  1013. let Some(href_idx) = after_anchor.find("href=") else {
  1014. remaining = &after_anchor[2..];
  1015. continue;
  1016. };
  1017. let href_slice = &after_anchor[href_idx + 5..];
  1018. let Some((url, rest)) = extract_quoted_value(href_slice) else {
  1019. remaining = &after_anchor[2..];
  1020. continue;
  1021. };
  1022. let Some(close_tag_idx) = rest.find('>') else {
  1023. remaining = &after_anchor[2..];
  1024. continue;
  1025. };
  1026. let after_tag = &rest[close_tag_idx + 1..];
  1027. let Some(end_anchor_idx) = after_tag.find("</a>") else {
  1028. remaining = &after_anchor[2..];
  1029. continue;
  1030. };
  1031. let title = html_to_text(&after_tag[..end_anchor_idx]);
  1032. if title.trim().is_empty() {
  1033. remaining = &after_tag[end_anchor_idx + 4..];
  1034. continue;
  1035. }
  1036. let decoded_url = decode_duckduckgo_redirect(&url).unwrap_or(url);
  1037. if decoded_url.starts_with("http://") || decoded_url.starts_with("https://") {
  1038. hits.push(SearchHit {
  1039. title: title.trim().to_string(),
  1040. url: decoded_url,
  1041. });
  1042. }
  1043. remaining = &after_tag[end_anchor_idx + 4..];
  1044. }
  1045. hits
  1046. }
  1047. fn extract_quoted_value(input: &str) -> Option<(String, &str)> {
  1048. let quote = input.chars().next()?;
  1049. if quote != '"' && quote != '\'' {
  1050. return None;
  1051. }
  1052. let rest = &input[quote.len_utf8()..];
  1053. let end = rest.find(quote)?;
  1054. Some((rest[..end].to_string(), &rest[end + quote.len_utf8()..]))
  1055. }
  1056. fn decode_duckduckgo_redirect(url: &str) -> Option<String> {
  1057. if url.starts_with("http://") || url.starts_with("https://") {
  1058. return Some(html_entity_decode_url(url));
  1059. }
  1060. let joined = if url.starts_with("//") {
  1061. format!("https:{url}")
  1062. } else if url.starts_with('/') {
  1063. format!("https://duckduckgo.com{url}")
  1064. } else {
  1065. return None;
  1066. };
  1067. let parsed = reqwest::Url::parse(&joined).ok()?;
  1068. if parsed.path() == "/l/" || parsed.path() == "/l" {
  1069. for (key, value) in parsed.query_pairs() {
  1070. if key == "uddg" {
  1071. return Some(html_entity_decode_url(value.as_ref()));
  1072. }
  1073. }
  1074. }
  1075. Some(joined)
  1076. }
  1077. fn html_entity_decode_url(url: &str) -> String {
  1078. decode_html_entities(url)
  1079. }
  1080. fn host_matches_list(url: &str, domains: &[String]) -> bool {
  1081. let Ok(parsed) = reqwest::Url::parse(url) else {
  1082. return false;
  1083. };
  1084. let Some(host) = parsed.host_str() else {
  1085. return false;
  1086. };
  1087. let host = host.to_ascii_lowercase();
  1088. domains.iter().any(|domain| {
  1089. let normalized = normalize_domain_filter(domain);
  1090. !normalized.is_empty() && (host == normalized || host.ends_with(&format!(".{normalized}")))
  1091. })
  1092. }
  1093. fn normalize_domain_filter(domain: &str) -> String {
  1094. let trimmed = domain.trim();
  1095. let candidate = reqwest::Url::parse(trimmed)
  1096. .ok()
  1097. .and_then(|url| url.host_str().map(str::to_string))
  1098. .unwrap_or_else(|| trimmed.to_string());
  1099. candidate
  1100. .trim()
  1101. .trim_start_matches('.')
  1102. .trim_end_matches('/')
  1103. .to_ascii_lowercase()
  1104. }
  1105. fn dedupe_hits(hits: &mut Vec<SearchHit>) {
  1106. let mut seen = BTreeSet::new();
  1107. hits.retain(|hit| seen.insert(hit.url.clone()));
  1108. }
  1109. fn execute_todo_write(input: TodoWriteInput) -> Result<TodoWriteOutput, String> {
  1110. validate_todos(&input.todos)?;
  1111. let store_path = todo_store_path()?;
  1112. let old_todos = if store_path.exists() {
  1113. serde_json::from_str::<Vec<TodoItem>>(
  1114. &std::fs::read_to_string(&store_path).map_err(|error| error.to_string())?,
  1115. )
  1116. .map_err(|error| error.to_string())?
  1117. } else {
  1118. Vec::new()
  1119. };
  1120. let all_done = input
  1121. .todos
  1122. .iter()
  1123. .all(|todo| matches!(todo.status, TodoStatus::Completed));
  1124. let persisted = if all_done {
  1125. Vec::new()
  1126. } else {
  1127. input.todos.clone()
  1128. };
  1129. if let Some(parent) = store_path.parent() {
  1130. std::fs::create_dir_all(parent).map_err(|error| error.to_string())?;
  1131. }
  1132. std::fs::write(
  1133. &store_path,
  1134. serde_json::to_string_pretty(&persisted).map_err(|error| error.to_string())?,
  1135. )
  1136. .map_err(|error| error.to_string())?;
  1137. let verification_nudge_needed = (all_done
  1138. && input.todos.len() >= 3
  1139. && !input
  1140. .todos
  1141. .iter()
  1142. .any(|todo| todo.content.to_lowercase().contains("verif")))
  1143. .then_some(true);
  1144. Ok(TodoWriteOutput {
  1145. old_todos,
  1146. new_todos: input.todos,
  1147. verification_nudge_needed,
  1148. })
  1149. }
  1150. fn execute_skill(input: SkillInput) -> Result<SkillOutput, String> {
  1151. let skill_path = resolve_skill_path(&input.skill)?;
  1152. let prompt = std::fs::read_to_string(&skill_path).map_err(|error| error.to_string())?;
  1153. let description = parse_skill_description(&prompt);
  1154. Ok(SkillOutput {
  1155. skill: input.skill,
  1156. path: skill_path.display().to_string(),
  1157. args: input.args,
  1158. description,
  1159. prompt,
  1160. })
  1161. }
  1162. fn validate_todos(todos: &[TodoItem]) -> Result<(), String> {
  1163. if todos.is_empty() {
  1164. return Err(String::from("todos must not be empty"));
  1165. }
  1166. // Allow multiple in_progress items for parallel workflows
  1167. if todos.iter().any(|todo| todo.content.trim().is_empty()) {
  1168. return Err(String::from("todo content must not be empty"));
  1169. }
  1170. if todos.iter().any(|todo| todo.active_form.trim().is_empty()) {
  1171. return Err(String::from("todo activeForm must not be empty"));
  1172. }
  1173. Ok(())
  1174. }
  1175. fn todo_store_path() -> Result<std::path::PathBuf, String> {
  1176. if let Ok(path) = std::env::var("CLAWD_TODO_STORE") {
  1177. return Ok(std::path::PathBuf::from(path));
  1178. }
  1179. let cwd = std::env::current_dir().map_err(|error| error.to_string())?;
  1180. Ok(cwd.join(".clawd-todos.json"))
  1181. }
  1182. fn resolve_skill_path(skill: &str) -> Result<std::path::PathBuf, String> {
  1183. let requested = skill.trim().trim_start_matches('/').trim_start_matches('$');
  1184. if requested.is_empty() {
  1185. return Err(String::from("skill must not be empty"));
  1186. }
  1187. let mut candidates = Vec::new();
  1188. if let Ok(codex_home) = std::env::var("CODEX_HOME") {
  1189. candidates.push(std::path::PathBuf::from(codex_home).join("skills"));
  1190. }
  1191. candidates.push(std::path::PathBuf::from("/home/bellman/.codex/skills"));
  1192. for root in candidates {
  1193. let direct = root.join(requested).join("SKILL.md");
  1194. if direct.exists() {
  1195. return Ok(direct);
  1196. }
  1197. if let Ok(entries) = std::fs::read_dir(&root) {
  1198. for entry in entries.flatten() {
  1199. let path = entry.path().join("SKILL.md");
  1200. if !path.exists() {
  1201. continue;
  1202. }
  1203. if entry
  1204. .file_name()
  1205. .to_string_lossy()
  1206. .eq_ignore_ascii_case(requested)
  1207. {
  1208. return Ok(path);
  1209. }
  1210. }
  1211. }
  1212. }
  1213. Err(format!("unknown skill: {requested}"))
  1214. }
  1215. const DEFAULT_AGENT_MODEL: &str = "claude-opus-4-6";
  1216. const DEFAULT_AGENT_SYSTEM_DATE: &str = "2026-03-31";
  1217. const DEFAULT_AGENT_MAX_ITERATIONS: usize = 32;
  1218. fn execute_agent(input: AgentInput) -> Result<AgentOutput, String> {
  1219. execute_agent_with_spawn(input, spawn_agent_job)
  1220. }
  1221. fn execute_agent_with_spawn<F>(input: AgentInput, spawn_fn: F) -> Result<AgentOutput, String>
  1222. where
  1223. F: FnOnce(AgentJob) -> Result<(), String>,
  1224. {
  1225. if input.description.trim().is_empty() {
  1226. return Err(String::from("description must not be empty"));
  1227. }
  1228. if input.prompt.trim().is_empty() {
  1229. return Err(String::from("prompt must not be empty"));
  1230. }
  1231. let agent_id = make_agent_id();
  1232. let output_dir = agent_store_dir()?;
  1233. std::fs::create_dir_all(&output_dir).map_err(|error| error.to_string())?;
  1234. let output_file = output_dir.join(format!("{agent_id}.md"));
  1235. let manifest_file = output_dir.join(format!("{agent_id}.json"));
  1236. let normalized_subagent_type = normalize_subagent_type(input.subagent_type.as_deref());
  1237. let model = resolve_agent_model(input.model.as_deref());
  1238. let agent_name = input
  1239. .name
  1240. .as_deref()
  1241. .map(slugify_agent_name)
  1242. .filter(|name| !name.is_empty())
  1243. .unwrap_or_else(|| slugify_agent_name(&input.description));
  1244. let created_at = iso8601_now();
  1245. let system_prompt = build_agent_system_prompt(&normalized_subagent_type)?;
  1246. let allowed_tools = allowed_tools_for_subagent(&normalized_subagent_type);
  1247. let output_contents = format!(
  1248. "# Agent Task
  1249. - id: {}
  1250. - name: {}
  1251. - description: {}
  1252. - subagent_type: {}
  1253. - created_at: {}
  1254. ## Prompt
  1255. {}
  1256. ",
  1257. agent_id, agent_name, input.description, normalized_subagent_type, created_at, input.prompt
  1258. );
  1259. std::fs::write(&output_file, output_contents).map_err(|error| error.to_string())?;
  1260. let manifest = AgentOutput {
  1261. agent_id,
  1262. name: agent_name,
  1263. description: input.description,
  1264. subagent_type: Some(normalized_subagent_type),
  1265. model: Some(model),
  1266. status: String::from("running"),
  1267. output_file: output_file.display().to_string(),
  1268. manifest_file: manifest_file.display().to_string(),
  1269. created_at: created_at.clone(),
  1270. started_at: Some(created_at),
  1271. completed_at: None,
  1272. error: None,
  1273. };
  1274. write_agent_manifest(&manifest)?;
  1275. let manifest_for_spawn = manifest.clone();
  1276. let job = AgentJob {
  1277. manifest: manifest_for_spawn,
  1278. prompt: input.prompt,
  1279. system_prompt,
  1280. allowed_tools,
  1281. };
  1282. if let Err(error) = spawn_fn(job) {
  1283. let error = format!("failed to spawn sub-agent: {error}");
  1284. persist_agent_terminal_state(&manifest, "failed", None, Some(error.clone()))?;
  1285. return Err(error);
  1286. }
  1287. Ok(manifest)
  1288. }
  1289. fn spawn_agent_job(job: AgentJob) -> Result<(), String> {
  1290. let thread_name = format!("clawd-agent-{}", job.manifest.agent_id);
  1291. std::thread::Builder::new()
  1292. .name(thread_name)
  1293. .spawn(move || {
  1294. let result =
  1295. std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| run_agent_job(&job)));
  1296. match result {
  1297. Ok(Ok(())) => {}
  1298. Ok(Err(error)) => {
  1299. let _ =
  1300. persist_agent_terminal_state(&job.manifest, "failed", None, Some(error));
  1301. }
  1302. Err(_) => {
  1303. let _ = persist_agent_terminal_state(
  1304. &job.manifest,
  1305. "failed",
  1306. None,
  1307. Some(String::from("sub-agent thread panicked")),
  1308. );
  1309. }
  1310. }
  1311. })
  1312. .map(|_| ())
  1313. .map_err(|error| error.to_string())
  1314. }
  1315. fn run_agent_job(job: &AgentJob) -> Result<(), String> {
  1316. let mut runtime = build_agent_runtime(job)?.with_max_iterations(DEFAULT_AGENT_MAX_ITERATIONS);
  1317. let summary = runtime
  1318. .run_turn(job.prompt.clone(), None)
  1319. .map_err(|error| error.to_string())?;
  1320. let final_text = final_assistant_text(&summary);
  1321. persist_agent_terminal_state(&job.manifest, "completed", Some(final_text.as_str()), None)
  1322. }
  1323. fn build_agent_runtime(
  1324. job: &AgentJob,
  1325. ) -> Result<ConversationRuntime<ProviderRuntimeClient, SubagentToolExecutor>, String> {
  1326. let model = job
  1327. .manifest
  1328. .model
  1329. .clone()
  1330. .unwrap_or_else(|| DEFAULT_AGENT_MODEL.to_string());
  1331. let allowed_tools = job.allowed_tools.clone();
  1332. let api_client = ProviderRuntimeClient::new(model, allowed_tools.clone())?;
  1333. let tool_executor = SubagentToolExecutor::new(allowed_tools);
  1334. Ok(ConversationRuntime::new(
  1335. Session::new(),
  1336. api_client,
  1337. tool_executor,
  1338. agent_permission_policy(),
  1339. job.system_prompt.clone(),
  1340. ))
  1341. }
  1342. fn build_agent_system_prompt(subagent_type: &str) -> Result<Vec<String>, String> {
  1343. let cwd = std::env::current_dir().map_err(|error| error.to_string())?;
  1344. let mut prompt = load_system_prompt(
  1345. cwd,
  1346. DEFAULT_AGENT_SYSTEM_DATE.to_string(),
  1347. std::env::consts::OS,
  1348. "unknown",
  1349. )
  1350. .map_err(|error| error.to_string())?;
  1351. prompt.push(format!(
  1352. "You are a background sub-agent of type `{subagent_type}`. Work only on the delegated task, use only the tools available to you, do not ask the user questions, and finish with a concise result."
  1353. ));
  1354. Ok(prompt)
  1355. }
  1356. fn resolve_agent_model(model: Option<&str>) -> String {
  1357. model
  1358. .map(str::trim)
  1359. .filter(|model| !model.is_empty())
  1360. .unwrap_or(DEFAULT_AGENT_MODEL)
  1361. .to_string()
  1362. }
  1363. fn allowed_tools_for_subagent(subagent_type: &str) -> BTreeSet<String> {
  1364. let tools = match subagent_type {
  1365. "Explore" => vec![
  1366. "read_file",
  1367. "glob_search",
  1368. "grep_search",
  1369. "WebFetch",
  1370. "WebSearch",
  1371. "ToolSearch",
  1372. "Skill",
  1373. "StructuredOutput",
  1374. ],
  1375. "Plan" => vec![
  1376. "read_file",
  1377. "glob_search",
  1378. "grep_search",
  1379. "WebFetch",
  1380. "WebSearch",
  1381. "ToolSearch",
  1382. "Skill",
  1383. "TodoWrite",
  1384. "StructuredOutput",
  1385. "SendUserMessage",
  1386. ],
  1387. "Verification" => vec![
  1388. "bash",
  1389. "read_file",
  1390. "glob_search",
  1391. "grep_search",
  1392. "WebFetch",
  1393. "WebSearch",
  1394. "ToolSearch",
  1395. "TodoWrite",
  1396. "StructuredOutput",
  1397. "SendUserMessage",
  1398. "PowerShell",
  1399. ],
  1400. "claude-code-guide" => vec![
  1401. "read_file",
  1402. "glob_search",
  1403. "grep_search",
  1404. "WebFetch",
  1405. "WebSearch",
  1406. "ToolSearch",
  1407. "Skill",
  1408. "StructuredOutput",
  1409. "SendUserMessage",
  1410. ],
  1411. "statusline-setup" => vec![
  1412. "bash",
  1413. "read_file",
  1414. "write_file",
  1415. "edit_file",
  1416. "glob_search",
  1417. "grep_search",
  1418. "ToolSearch",
  1419. ],
  1420. _ => vec![
  1421. "bash",
  1422. "read_file",
  1423. "write_file",
  1424. "edit_file",
  1425. "glob_search",
  1426. "grep_search",
  1427. "WebFetch",
  1428. "WebSearch",
  1429. "TodoWrite",
  1430. "Skill",
  1431. "ToolSearch",
  1432. "NotebookEdit",
  1433. "Sleep",
  1434. "SendUserMessage",
  1435. "Config",
  1436. "StructuredOutput",
  1437. "REPL",
  1438. "PowerShell",
  1439. ],
  1440. };
  1441. tools.into_iter().map(str::to_string).collect()
  1442. }
  1443. fn agent_permission_policy() -> PermissionPolicy {
  1444. mvp_tool_specs().into_iter().fold(
  1445. PermissionPolicy::new(PermissionMode::DangerFullAccess),
  1446. |policy, spec| policy.with_tool_requirement(spec.name, spec.required_permission),
  1447. )
  1448. }
  1449. fn write_agent_manifest(manifest: &AgentOutput) -> Result<(), String> {
  1450. std::fs::write(
  1451. &manifest.manifest_file,
  1452. serde_json::to_string_pretty(manifest).map_err(|error| error.to_string())?,
  1453. )
  1454. .map_err(|error| error.to_string())
  1455. }
  1456. fn persist_agent_terminal_state(
  1457. manifest: &AgentOutput,
  1458. status: &str,
  1459. result: Option<&str>,
  1460. error: Option<String>,
  1461. ) -> Result<(), String> {
  1462. append_agent_output(
  1463. &manifest.output_file,
  1464. &format_agent_terminal_output(status, result, error.as_deref()),
  1465. )?;
  1466. let mut next_manifest = manifest.clone();
  1467. next_manifest.status = status.to_string();
  1468. next_manifest.completed_at = Some(iso8601_now());
  1469. next_manifest.error = error;
  1470. write_agent_manifest(&next_manifest)
  1471. }
  1472. fn append_agent_output(path: &str, suffix: &str) -> Result<(), String> {
  1473. use std::io::Write as _;
  1474. let mut file = std::fs::OpenOptions::new()
  1475. .append(true)
  1476. .open(path)
  1477. .map_err(|error| error.to_string())?;
  1478. file.write_all(suffix.as_bytes())
  1479. .map_err(|error| error.to_string())
  1480. }
  1481. fn format_agent_terminal_output(status: &str, result: Option<&str>, error: Option<&str>) -> String {
  1482. let mut sections = vec![format!("\n## Result\n\n- status: {status}\n")];
  1483. if let Some(result) = result.filter(|value| !value.trim().is_empty()) {
  1484. sections.push(format!("\n### Final response\n\n{}\n", result.trim()));
  1485. }
  1486. if let Some(error) = error.filter(|value| !value.trim().is_empty()) {
  1487. sections.push(format!("\n### Error\n\n{}\n", error.trim()));
  1488. }
  1489. sections.join("")
  1490. }
  1491. struct ProviderRuntimeClient {
  1492. runtime: tokio::runtime::Runtime,
  1493. client: ProviderClient,
  1494. model: String,
  1495. allowed_tools: BTreeSet<String>,
  1496. }
  1497. impl ProviderRuntimeClient {
  1498. fn new(model: String, allowed_tools: BTreeSet<String>) -> Result<Self, String> {
  1499. let model = resolve_model_alias(&model).to_string();
  1500. let client = match detect_provider_kind(&model) {
  1501. ProviderKind::Anthropic | ProviderKind::Xai | ProviderKind::OpenAi => {
  1502. ProviderClient::from_model(&model).map_err(|error| error.to_string())?
  1503. }
  1504. };
  1505. Ok(Self {
  1506. runtime: tokio::runtime::Runtime::new().map_err(|error| error.to_string())?,
  1507. client,
  1508. model,
  1509. allowed_tools,
  1510. })
  1511. }
  1512. }
  1513. impl ApiClient for ProviderRuntimeClient {
  1514. fn stream(&mut self, request: ApiRequest) -> Result<Vec<AssistantEvent>, RuntimeError> {
  1515. let tools = tool_specs_for_allowed_tools(Some(&self.allowed_tools))
  1516. .into_iter()
  1517. .map(|spec| ToolDefinition {
  1518. name: spec.name.to_string(),
  1519. description: Some(spec.description.to_string()),
  1520. input_schema: spec.input_schema,
  1521. })
  1522. .collect::<Vec<_>>();
  1523. let message_request = MessageRequest {
  1524. model: self.model.clone(),
  1525. max_tokens: max_tokens_for_model(&self.model),
  1526. messages: convert_messages(&request.messages),
  1527. system: (!request.system_prompt.is_empty()).then(|| request.system_prompt.join("\n\n")),
  1528. tools: (!tools.is_empty()).then_some(tools),
  1529. tool_choice: (!self.allowed_tools.is_empty()).then_some(ToolChoice::Auto),
  1530. stream: true,
  1531. };
  1532. self.runtime.block_on(async {
  1533. let mut stream = self
  1534. .client
  1535. .stream_message(&message_request)
  1536. .await
  1537. .map_err(|error| RuntimeError::new(error.to_string()))?;
  1538. let mut events = Vec::new();
  1539. let mut pending_tool: Option<(String, String, String)> = None;
  1540. let mut saw_stop = false;
  1541. while let Some(event) = stream
  1542. .next_event()
  1543. .await
  1544. .map_err(|error| RuntimeError::new(error.to_string()))?
  1545. {
  1546. match event {
  1547. ApiStreamEvent::MessageStart(start) => {
  1548. for block in start.message.content {
  1549. push_output_block(block, &mut events, &mut pending_tool, true);
  1550. }
  1551. }
  1552. ApiStreamEvent::ContentBlockStart(start) => {
  1553. push_output_block(
  1554. start.content_block,
  1555. &mut events,
  1556. &mut pending_tool,
  1557. true,
  1558. );
  1559. }
  1560. ApiStreamEvent::ContentBlockDelta(delta) => match delta.delta {
  1561. ContentBlockDelta::TextDelta { text } => {
  1562. if !text.is_empty() {
  1563. events.push(AssistantEvent::TextDelta(text));
  1564. }
  1565. }
  1566. ContentBlockDelta::InputJsonDelta { partial_json } => {
  1567. if let Some((_, _, input)) = &mut pending_tool {
  1568. input.push_str(&partial_json);
  1569. }
  1570. }
  1571. },
  1572. ApiStreamEvent::ContentBlockStop(_) => {
  1573. if let Some((id, name, input)) = pending_tool.take() {
  1574. events.push(AssistantEvent::ToolUse { id, name, input });
  1575. }
  1576. }
  1577. ApiStreamEvent::MessageDelta(delta) => {
  1578. events.push(AssistantEvent::Usage(TokenUsage {
  1579. input_tokens: delta.usage.input_tokens,
  1580. output_tokens: delta.usage.output_tokens,
  1581. cache_creation_input_tokens: 0,
  1582. cache_read_input_tokens: 0,
  1583. }));
  1584. }
  1585. ApiStreamEvent::MessageStop(_) => {
  1586. saw_stop = true;
  1587. events.push(AssistantEvent::MessageStop);
  1588. }
  1589. }
  1590. }
  1591. if !saw_stop
  1592. && events.iter().any(|event| {
  1593. matches!(event, AssistantEvent::TextDelta(text) if !text.is_empty())
  1594. || matches!(event, AssistantEvent::ToolUse { .. })
  1595. })
  1596. {
  1597. events.push(AssistantEvent::MessageStop);
  1598. }
  1599. if events
  1600. .iter()
  1601. .any(|event| matches!(event, AssistantEvent::MessageStop))
  1602. {
  1603. return Ok(events);
  1604. }
  1605. let response = self
  1606. .client
  1607. .send_message(&MessageRequest {
  1608. stream: false,
  1609. ..message_request.clone()
  1610. })
  1611. .await
  1612. .map_err(|error| RuntimeError::new(error.to_string()))?;
  1613. Ok(response_to_events(response))
  1614. })
  1615. }
  1616. }
  1617. struct SubagentToolExecutor {
  1618. allowed_tools: BTreeSet<String>,
  1619. }
  1620. impl SubagentToolExecutor {
  1621. fn new(allowed_tools: BTreeSet<String>) -> Self {
  1622. Self { allowed_tools }
  1623. }
  1624. }
  1625. impl ToolExecutor for SubagentToolExecutor {
  1626. fn execute(&mut self, tool_name: &str, input: &str) -> Result<String, ToolError> {
  1627. if !self.allowed_tools.contains(tool_name) {
  1628. return Err(ToolError::new(format!(
  1629. "tool `{tool_name}` is not enabled for this sub-agent"
  1630. )));
  1631. }
  1632. let value = serde_json::from_str(input)
  1633. .map_err(|error| ToolError::new(format!("invalid tool input JSON: {error}")))?;
  1634. execute_tool(tool_name, &value).map_err(ToolError::new)
  1635. }
  1636. }
  1637. fn tool_specs_for_allowed_tools(allowed_tools: Option<&BTreeSet<String>>) -> Vec<ToolSpec> {
  1638. mvp_tool_specs()
  1639. .into_iter()
  1640. .filter(|spec| allowed_tools.is_none_or(|allowed| allowed.contains(spec.name)))
  1641. .collect()
  1642. }
  1643. fn convert_messages(messages: &[ConversationMessage]) -> Vec<InputMessage> {
  1644. messages
  1645. .iter()
  1646. .filter_map(|message| {
  1647. let role = match message.role {
  1648. MessageRole::System | MessageRole::User | MessageRole::Tool => "user",
  1649. MessageRole::Assistant => "assistant",
  1650. };
  1651. let content = message
  1652. .blocks
  1653. .iter()
  1654. .map(|block| match block {
  1655. ContentBlock::Text { text } => InputContentBlock::Text { text: text.clone() },
  1656. ContentBlock::ToolUse { id, name, input } => InputContentBlock::ToolUse {
  1657. id: id.clone(),
  1658. name: name.clone(),
  1659. input: serde_json::from_str(input)
  1660. .unwrap_or_else(|_| serde_json::json!({ "raw": input })),
  1661. },
  1662. ContentBlock::ToolResult {
  1663. tool_use_id,
  1664. output,
  1665. is_error,
  1666. ..
  1667. } => InputContentBlock::ToolResult {
  1668. tool_use_id: tool_use_id.clone(),
  1669. content: vec![ToolResultContentBlock::Text {
  1670. text: output.clone(),
  1671. }],
  1672. is_error: *is_error,
  1673. },
  1674. })
  1675. .collect::<Vec<_>>();
  1676. (!content.is_empty()).then(|| InputMessage {
  1677. role: role.to_string(),
  1678. content,
  1679. })
  1680. })
  1681. .collect()
  1682. }
  1683. fn push_output_block(
  1684. block: OutputContentBlock,
  1685. events: &mut Vec<AssistantEvent>,
  1686. pending_tool: &mut Option<(String, String, String)>,
  1687. streaming_tool_input: bool,
  1688. ) {
  1689. match block {
  1690. OutputContentBlock::Text { text } => {
  1691. if !text.is_empty() {
  1692. events.push(AssistantEvent::TextDelta(text));
  1693. }
  1694. }
  1695. OutputContentBlock::ToolUse { id, name, input } => {
  1696. let initial_input = if streaming_tool_input
  1697. && input.is_object()
  1698. && input.as_object().is_some_and(serde_json::Map::is_empty)
  1699. {
  1700. String::new()
  1701. } else {
  1702. input.to_string()
  1703. };
  1704. *pending_tool = Some((id, name, initial_input));
  1705. }
  1706. }
  1707. }
  1708. fn response_to_events(response: MessageResponse) -> Vec<AssistantEvent> {
  1709. let mut events = Vec::new();
  1710. let mut pending_tool = None;
  1711. for block in response.content {
  1712. push_output_block(block, &mut events, &mut pending_tool, false);
  1713. if let Some((id, name, input)) = pending_tool.take() {
  1714. events.push(AssistantEvent::ToolUse { id, name, input });
  1715. }
  1716. }
  1717. events.push(AssistantEvent::Usage(TokenUsage {
  1718. input_tokens: response.usage.input_tokens,
  1719. output_tokens: response.usage.output_tokens,
  1720. cache_creation_input_tokens: response.usage.cache_creation_input_tokens,
  1721. cache_read_input_tokens: response.usage.cache_read_input_tokens,
  1722. }));
  1723. events.push(AssistantEvent::MessageStop);
  1724. events
  1725. }
  1726. fn final_assistant_text(summary: &runtime::TurnSummary) -> String {
  1727. summary
  1728. .assistant_messages
  1729. .last()
  1730. .map(|message| {
  1731. message
  1732. .blocks
  1733. .iter()
  1734. .filter_map(|block| match block {
  1735. ContentBlock::Text { text } => Some(text.as_str()),
  1736. _ => None,
  1737. })
  1738. .collect::<Vec<_>>()
  1739. .join("")
  1740. })
  1741. .unwrap_or_default()
  1742. }
  1743. #[allow(clippy::needless_pass_by_value)]
  1744. fn execute_tool_search(input: ToolSearchInput) -> ToolSearchOutput {
  1745. let deferred = deferred_tool_specs();
  1746. let max_results = input.max_results.unwrap_or(5).max(1);
  1747. let query = input.query.trim().to_string();
  1748. let normalized_query = normalize_tool_search_query(&query);
  1749. let matches = search_tool_specs(&query, max_results, &deferred);
  1750. ToolSearchOutput {
  1751. matches,
  1752. query,
  1753. normalized_query,
  1754. total_deferred_tools: deferred.len(),
  1755. pending_mcp_servers: None,
  1756. }
  1757. }
  1758. fn deferred_tool_specs() -> Vec<ToolSpec> {
  1759. mvp_tool_specs()
  1760. .into_iter()
  1761. .filter(|spec| {
  1762. !matches!(
  1763. spec.name,
  1764. "bash" | "read_file" | "write_file" | "edit_file" | "glob_search" | "grep_search"
  1765. )
  1766. })
  1767. .collect()
  1768. }
  1769. fn search_tool_specs(query: &str, max_results: usize, specs: &[ToolSpec]) -> Vec<String> {
  1770. let lowered = query.to_lowercase();
  1771. if let Some(selection) = lowered.strip_prefix("select:") {
  1772. return selection
  1773. .split(',')
  1774. .map(str::trim)
  1775. .filter(|part| !part.is_empty())
  1776. .filter_map(|wanted| {
  1777. let wanted = canonical_tool_token(wanted);
  1778. specs
  1779. .iter()
  1780. .find(|spec| canonical_tool_token(spec.name) == wanted)
  1781. .map(|spec| spec.name.to_string())
  1782. })
  1783. .take(max_results)
  1784. .collect();
  1785. }
  1786. let mut required = Vec::new();
  1787. let mut optional = Vec::new();
  1788. for term in lowered.split_whitespace() {
  1789. if let Some(rest) = term.strip_prefix('+') {
  1790. if !rest.is_empty() {
  1791. required.push(rest);
  1792. }
  1793. } else {
  1794. optional.push(term);
  1795. }
  1796. }
  1797. let terms = if required.is_empty() {
  1798. optional.clone()
  1799. } else {
  1800. required.iter().chain(optional.iter()).copied().collect()
  1801. };
  1802. let mut scored = specs
  1803. .iter()
  1804. .filter_map(|spec| {
  1805. let name = spec.name.to_lowercase();
  1806. let canonical_name = canonical_tool_token(spec.name);
  1807. let normalized_description = normalize_tool_search_query(spec.description);
  1808. let haystack = format!(
  1809. "{name} {} {canonical_name}",
  1810. spec.description.to_lowercase()
  1811. );
  1812. let normalized_haystack = format!("{canonical_name} {normalized_description}");
  1813. if required.iter().any(|term| !haystack.contains(term)) {
  1814. return None;
  1815. }
  1816. let mut score = 0_i32;
  1817. for term in &terms {
  1818. let canonical_term = canonical_tool_token(term);
  1819. if haystack.contains(term) {
  1820. score += 2;
  1821. }
  1822. if name == *term {
  1823. score += 8;
  1824. }
  1825. if name.contains(term) {
  1826. score += 4;
  1827. }
  1828. if canonical_name == canonical_term {
  1829. score += 12;
  1830. }
  1831. if normalized_haystack.contains(&canonical_term) {
  1832. score += 3;
  1833. }
  1834. }
  1835. if score == 0 && !lowered.is_empty() {
  1836. return None;
  1837. }
  1838. Some((score, spec.name.to_string()))
  1839. })
  1840. .collect::<Vec<_>>();
  1841. scored.sort_by(|left, right| right.0.cmp(&left.0).then_with(|| left.1.cmp(&right.1)));
  1842. scored
  1843. .into_iter()
  1844. .map(|(_, name)| name)
  1845. .take(max_results)
  1846. .collect()
  1847. }
  1848. fn normalize_tool_search_query(query: &str) -> String {
  1849. query
  1850. .trim()
  1851. .split(|ch: char| ch.is_whitespace() || ch == ',')
  1852. .filter(|term| !term.is_empty())
  1853. .map(canonical_tool_token)
  1854. .collect::<Vec<_>>()
  1855. .join(" ")
  1856. }
  1857. fn canonical_tool_token(value: &str) -> String {
  1858. let mut canonical = value
  1859. .chars()
  1860. .filter(char::is_ascii_alphanumeric)
  1861. .flat_map(char::to_lowercase)
  1862. .collect::<String>();
  1863. if let Some(stripped) = canonical.strip_suffix("tool") {
  1864. canonical = stripped.to_string();
  1865. }
  1866. canonical
  1867. }
  1868. fn agent_store_dir() -> Result<std::path::PathBuf, String> {
  1869. if let Ok(path) = std::env::var("CLAWD_AGENT_STORE") {
  1870. return Ok(std::path::PathBuf::from(path));
  1871. }
  1872. let cwd = std::env::current_dir().map_err(|error| error.to_string())?;
  1873. if let Some(workspace_root) = cwd.ancestors().nth(2) {
  1874. return Ok(workspace_root.join(".clawd-agents"));
  1875. }
  1876. Ok(cwd.join(".clawd-agents"))
  1877. }
  1878. fn make_agent_id() -> String {
  1879. let nanos = std::time::SystemTime::now()
  1880. .duration_since(std::time::UNIX_EPOCH)
  1881. .unwrap_or_default()
  1882. .as_nanos();
  1883. format!("agent-{nanos}")
  1884. }
  1885. fn slugify_agent_name(description: &str) -> String {
  1886. let mut out = description
  1887. .chars()
  1888. .map(|ch| {
  1889. if ch.is_ascii_alphanumeric() {
  1890. ch.to_ascii_lowercase()
  1891. } else {
  1892. '-'
  1893. }
  1894. })
  1895. .collect::<String>();
  1896. while out.contains("--") {
  1897. out = out.replace("--", "-");
  1898. }
  1899. out.trim_matches('-').chars().take(32).collect()
  1900. }
  1901. fn normalize_subagent_type(subagent_type: Option<&str>) -> String {
  1902. let trimmed = subagent_type.map(str::trim).unwrap_or_default();
  1903. if trimmed.is_empty() {
  1904. return String::from("general-purpose");
  1905. }
  1906. match canonical_tool_token(trimmed).as_str() {
  1907. "general" | "generalpurpose" | "generalpurposeagent" => String::from("general-purpose"),
  1908. "explore" | "explorer" | "exploreagent" => String::from("Explore"),
  1909. "plan" | "planagent" => String::from("Plan"),
  1910. "verification" | "verificationagent" | "verify" | "verifier" => {
  1911. String::from("Verification")
  1912. }
  1913. "claudecodeguide" | "claudecodeguideagent" | "guide" => String::from("claude-code-guide"),
  1914. "statusline" | "statuslinesetup" => String::from("statusline-setup"),
  1915. _ => trimmed.to_string(),
  1916. }
  1917. }
  1918. fn iso8601_now() -> String {
  1919. std::time::SystemTime::now()
  1920. .duration_since(std::time::UNIX_EPOCH)
  1921. .unwrap_or_default()
  1922. .as_secs()
  1923. .to_string()
  1924. }
  1925. #[allow(clippy::too_many_lines)]
  1926. fn execute_notebook_edit(input: NotebookEditInput) -> Result<NotebookEditOutput, String> {
  1927. let path = std::path::PathBuf::from(&input.notebook_path);
  1928. if path.extension().and_then(|ext| ext.to_str()) != Some("ipynb") {
  1929. return Err(String::from(
  1930. "File must be a Jupyter notebook (.ipynb file).",
  1931. ));
  1932. }
  1933. let original_file = std::fs::read_to_string(&path).map_err(|error| error.to_string())?;
  1934. let mut notebook: serde_json::Value =
  1935. serde_json::from_str(&original_file).map_err(|error| error.to_string())?;
  1936. let language = notebook
  1937. .get("metadata")
  1938. .and_then(|metadata| metadata.get("kernelspec"))
  1939. .and_then(|kernelspec| kernelspec.get("language"))
  1940. .and_then(serde_json::Value::as_str)
  1941. .unwrap_or("python")
  1942. .to_string();
  1943. let cells = notebook
  1944. .get_mut("cells")
  1945. .and_then(serde_json::Value::as_array_mut)
  1946. .ok_or_else(|| String::from("Notebook cells array not found"))?;
  1947. let edit_mode = input.edit_mode.unwrap_or(NotebookEditMode::Replace);
  1948. let target_index = match input.cell_id.as_deref() {
  1949. Some(cell_id) => Some(resolve_cell_index(cells, Some(cell_id), edit_mode)?),
  1950. None if matches!(
  1951. edit_mode,
  1952. NotebookEditMode::Replace | NotebookEditMode::Delete
  1953. ) =>
  1954. {
  1955. Some(resolve_cell_index(cells, None, edit_mode)?)
  1956. }
  1957. None => None,
  1958. };
  1959. let resolved_cell_type = match edit_mode {
  1960. NotebookEditMode::Delete => None,
  1961. NotebookEditMode::Insert => Some(input.cell_type.unwrap_or(NotebookCellType::Code)),
  1962. NotebookEditMode::Replace => Some(input.cell_type.unwrap_or_else(|| {
  1963. target_index
  1964. .and_then(|index| cells.get(index))
  1965. .and_then(cell_kind)
  1966. .unwrap_or(NotebookCellType::Code)
  1967. })),
  1968. };
  1969. let new_source = require_notebook_source(input.new_source, edit_mode)?;
  1970. let cell_id = match edit_mode {
  1971. NotebookEditMode::Insert => {
  1972. let resolved_cell_type = resolved_cell_type.expect("insert cell type");
  1973. let new_id = make_cell_id(cells.len());
  1974. let new_cell = build_notebook_cell(&new_id, resolved_cell_type, &new_source);
  1975. let insert_at = target_index.map_or(cells.len(), |index| index + 1);
  1976. cells.insert(insert_at, new_cell);
  1977. cells
  1978. .get(insert_at)
  1979. .and_then(|cell| cell.get("id"))
  1980. .and_then(serde_json::Value::as_str)
  1981. .map(ToString::to_string)
  1982. }
  1983. NotebookEditMode::Delete => {
  1984. let removed = cells.remove(target_index.expect("delete target index"));
  1985. removed
  1986. .get("id")
  1987. .and_then(serde_json::Value::as_str)
  1988. .map(ToString::to_string)
  1989. }
  1990. NotebookEditMode::Replace => {
  1991. let resolved_cell_type = resolved_cell_type.expect("replace cell type");
  1992. let cell = cells
  1993. .get_mut(target_index.expect("replace target index"))
  1994. .ok_or_else(|| String::from("Cell index out of range"))?;
  1995. cell["source"] = serde_json::Value::Array(source_lines(&new_source));
  1996. cell["cell_type"] = serde_json::Value::String(match resolved_cell_type {
  1997. NotebookCellType::Code => String::from("code"),
  1998. NotebookCellType::Markdown => String::from("markdown"),
  1999. });
  2000. match resolved_cell_type {
  2001. NotebookCellType::Code => {
  2002. if !cell.get("outputs").is_some_and(serde_json::Value::is_array) {
  2003. cell["outputs"] = json!([]);
  2004. }
  2005. if cell.get("execution_count").is_none() {
  2006. cell["execution_count"] = serde_json::Value::Null;
  2007. }
  2008. }
  2009. NotebookCellType::Markdown => {
  2010. if let Some(object) = cell.as_object_mut() {
  2011. object.remove("outputs");
  2012. object.remove("execution_count");
  2013. }
  2014. }
  2015. }
  2016. cell.get("id")
  2017. .and_then(serde_json::Value::as_str)
  2018. .map(ToString::to_string)
  2019. }
  2020. };
  2021. let updated_file =
  2022. serde_json::to_string_pretty(&notebook).map_err(|error| error.to_string())?;
  2023. std::fs::write(&path, &updated_file).map_err(|error| error.to_string())?;
  2024. Ok(NotebookEditOutput {
  2025. new_source,
  2026. cell_id,
  2027. cell_type: resolved_cell_type,
  2028. language,
  2029. edit_mode: format_notebook_edit_mode(edit_mode),
  2030. error: None,
  2031. notebook_path: path.display().to_string(),
  2032. original_file,
  2033. updated_file,
  2034. })
  2035. }
  2036. fn require_notebook_source(
  2037. source: Option<String>,
  2038. edit_mode: NotebookEditMode,
  2039. ) -> Result<String, String> {
  2040. match edit_mode {
  2041. NotebookEditMode::Delete => Ok(source.unwrap_or_default()),
  2042. NotebookEditMode::Insert | NotebookEditMode::Replace => source
  2043. .ok_or_else(|| String::from("new_source is required for insert and replace edits")),
  2044. }
  2045. }
  2046. fn build_notebook_cell(cell_id: &str, cell_type: NotebookCellType, source: &str) -> Value {
  2047. let mut cell = json!({
  2048. "cell_type": match cell_type {
  2049. NotebookCellType::Code => "code",
  2050. NotebookCellType::Markdown => "markdown",
  2051. },
  2052. "id": cell_id,
  2053. "metadata": {},
  2054. "source": source_lines(source),
  2055. });
  2056. if let Some(object) = cell.as_object_mut() {
  2057. match cell_type {
  2058. NotebookCellType::Code => {
  2059. object.insert(String::from("outputs"), json!([]));
  2060. object.insert(String::from("execution_count"), Value::Null);
  2061. }
  2062. NotebookCellType::Markdown => {}
  2063. }
  2064. }
  2065. cell
  2066. }
  2067. fn cell_kind(cell: &serde_json::Value) -> Option<NotebookCellType> {
  2068. cell.get("cell_type")
  2069. .and_then(serde_json::Value::as_str)
  2070. .map(|kind| {
  2071. if kind == "markdown" {
  2072. NotebookCellType::Markdown
  2073. } else {
  2074. NotebookCellType::Code
  2075. }
  2076. })
  2077. }
  2078. #[allow(clippy::needless_pass_by_value)]
  2079. fn execute_sleep(input: SleepInput) -> SleepOutput {
  2080. std::thread::sleep(Duration::from_millis(input.duration_ms));
  2081. SleepOutput {
  2082. duration_ms: input.duration_ms,
  2083. message: format!("Slept for {}ms", input.duration_ms),
  2084. }
  2085. }
  2086. fn execute_brief(input: BriefInput) -> Result<BriefOutput, String> {
  2087. if input.message.trim().is_empty() {
  2088. return Err(String::from("message must not be empty"));
  2089. }
  2090. let attachments = input
  2091. .attachments
  2092. .as_ref()
  2093. .map(|paths| {
  2094. paths
  2095. .iter()
  2096. .map(|path| resolve_attachment(path))
  2097. .collect::<Result<Vec<_>, String>>()
  2098. })
  2099. .transpose()?;
  2100. let message = match input.status {
  2101. BriefStatus::Normal | BriefStatus::Proactive => input.message,
  2102. };
  2103. Ok(BriefOutput {
  2104. message,
  2105. attachments,
  2106. sent_at: iso8601_timestamp(),
  2107. })
  2108. }
  2109. fn resolve_attachment(path: &str) -> Result<ResolvedAttachment, String> {
  2110. let resolved = std::fs::canonicalize(path).map_err(|error| error.to_string())?;
  2111. let metadata = std::fs::metadata(&resolved).map_err(|error| error.to_string())?;
  2112. Ok(ResolvedAttachment {
  2113. path: resolved.display().to_string(),
  2114. size: metadata.len(),
  2115. is_image: is_image_path(&resolved),
  2116. })
  2117. }
  2118. fn is_image_path(path: &Path) -> bool {
  2119. matches!(
  2120. path.extension()
  2121. .and_then(|ext| ext.to_str())
  2122. .map(str::to_ascii_lowercase)
  2123. .as_deref(),
  2124. Some("png" | "jpg" | "jpeg" | "gif" | "webp" | "bmp" | "svg")
  2125. )
  2126. }
  2127. fn execute_config(input: ConfigInput) -> Result<ConfigOutput, String> {
  2128. let setting = input.setting.trim();
  2129. if setting.is_empty() {
  2130. return Err(String::from("setting must not be empty"));
  2131. }
  2132. let Some(spec) = supported_config_setting(setting) else {
  2133. return Ok(ConfigOutput {
  2134. success: false,
  2135. operation: None,
  2136. setting: None,
  2137. value: None,
  2138. previous_value: None,
  2139. new_value: None,
  2140. error: Some(format!("Unknown setting: \"{setting}\"")),
  2141. });
  2142. };
  2143. let path = config_file_for_scope(spec.scope)?;
  2144. let mut document = read_json_object(&path)?;
  2145. if let Some(value) = input.value {
  2146. let normalized = normalize_config_value(spec, value)?;
  2147. let previous_value = get_nested_value(&document, spec.path).cloned();
  2148. set_nested_value(&mut document, spec.path, normalized.clone());
  2149. write_json_object(&path, &document)?;
  2150. Ok(ConfigOutput {
  2151. success: true,
  2152. operation: Some(String::from("set")),
  2153. setting: Some(setting.to_string()),
  2154. value: Some(normalized.clone()),
  2155. previous_value,
  2156. new_value: Some(normalized),
  2157. error: None,
  2158. })
  2159. } else {
  2160. Ok(ConfigOutput {
  2161. success: true,
  2162. operation: Some(String::from("get")),
  2163. setting: Some(setting.to_string()),
  2164. value: get_nested_value(&document, spec.path).cloned(),
  2165. previous_value: None,
  2166. new_value: None,
  2167. error: None,
  2168. })
  2169. }
  2170. }
  2171. fn execute_structured_output(input: StructuredOutputInput) -> StructuredOutputResult {
  2172. StructuredOutputResult {
  2173. data: String::from("Structured output provided successfully"),
  2174. structured_output: input.0,
  2175. }
  2176. }
  2177. fn execute_repl(input: ReplInput) -> Result<ReplOutput, String> {
  2178. if input.code.trim().is_empty() {
  2179. return Err(String::from("code must not be empty"));
  2180. }
  2181. let _ = input.timeout_ms;
  2182. let runtime = resolve_repl_runtime(&input.language)?;
  2183. let started = Instant::now();
  2184. let output = Command::new(runtime.program)
  2185. .args(runtime.args)
  2186. .arg(&input.code)
  2187. .output()
  2188. .map_err(|error| error.to_string())?;
  2189. Ok(ReplOutput {
  2190. language: input.language,
  2191. stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
  2192. stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
  2193. exit_code: output.status.code().unwrap_or(1),
  2194. duration_ms: started.elapsed().as_millis(),
  2195. })
  2196. }
  2197. struct ReplRuntime {
  2198. program: &'static str,
  2199. args: &'static [&'static str],
  2200. }
  2201. fn resolve_repl_runtime(language: &str) -> Result<ReplRuntime, String> {
  2202. match language.trim().to_ascii_lowercase().as_str() {
  2203. "python" | "py" => Ok(ReplRuntime {
  2204. program: detect_first_command(&["python3", "python"])
  2205. .ok_or_else(|| String::from("python runtime not found"))?,
  2206. args: &["-c"],
  2207. }),
  2208. "javascript" | "js" | "node" => Ok(ReplRuntime {
  2209. program: detect_first_command(&["node"])
  2210. .ok_or_else(|| String::from("node runtime not found"))?,
  2211. args: &["-e"],
  2212. }),
  2213. "sh" | "shell" | "bash" => Ok(ReplRuntime {
  2214. program: detect_first_command(&["bash", "sh"])
  2215. .ok_or_else(|| String::from("shell runtime not found"))?,
  2216. args: &["-lc"],
  2217. }),
  2218. other => Err(format!("unsupported REPL language: {other}")),
  2219. }
  2220. }
  2221. fn detect_first_command(commands: &[&'static str]) -> Option<&'static str> {
  2222. commands
  2223. .iter()
  2224. .copied()
  2225. .find(|command| command_exists(command))
  2226. }
  2227. #[derive(Clone, Copy)]
  2228. enum ConfigScope {
  2229. Global,
  2230. Settings,
  2231. }
  2232. #[derive(Clone, Copy)]
  2233. struct ConfigSettingSpec {
  2234. scope: ConfigScope,
  2235. kind: ConfigKind,
  2236. path: &'static [&'static str],
  2237. options: Option<&'static [&'static str]>,
  2238. }
  2239. #[derive(Clone, Copy)]
  2240. enum ConfigKind {
  2241. Boolean,
  2242. String,
  2243. }
  2244. fn supported_config_setting(setting: &str) -> Option<ConfigSettingSpec> {
  2245. Some(match setting {
  2246. "theme" => ConfigSettingSpec {
  2247. scope: ConfigScope::Global,
  2248. kind: ConfigKind::String,
  2249. path: &["theme"],
  2250. options: None,
  2251. },
  2252. "editorMode" => ConfigSettingSpec {
  2253. scope: ConfigScope::Global,
  2254. kind: ConfigKind::String,
  2255. path: &["editorMode"],
  2256. options: Some(&["default", "vim", "emacs"]),
  2257. },
  2258. "verbose" => ConfigSettingSpec {
  2259. scope: ConfigScope::Global,
  2260. kind: ConfigKind::Boolean,
  2261. path: &["verbose"],
  2262. options: None,
  2263. },
  2264. "preferredNotifChannel" => ConfigSettingSpec {
  2265. scope: ConfigScope::Global,
  2266. kind: ConfigKind::String,
  2267. path: &["preferredNotifChannel"],
  2268. options: None,
  2269. },
  2270. "autoCompactEnabled" => ConfigSettingSpec {
  2271. scope: ConfigScope::Global,
  2272. kind: ConfigKind::Boolean,
  2273. path: &["autoCompactEnabled"],
  2274. options: None,
  2275. },
  2276. "autoMemoryEnabled" => ConfigSettingSpec {
  2277. scope: ConfigScope::Settings,
  2278. kind: ConfigKind::Boolean,
  2279. path: &["autoMemoryEnabled"],
  2280. options: None,
  2281. },
  2282. "autoDreamEnabled" => ConfigSettingSpec {
  2283. scope: ConfigScope::Settings,
  2284. kind: ConfigKind::Boolean,
  2285. path: &["autoDreamEnabled"],
  2286. options: None,
  2287. },
  2288. "fileCheckpointingEnabled" => ConfigSettingSpec {
  2289. scope: ConfigScope::Global,
  2290. kind: ConfigKind::Boolean,
  2291. path: &["fileCheckpointingEnabled"],
  2292. options: None,
  2293. },
  2294. "showTurnDuration" => ConfigSettingSpec {
  2295. scope: ConfigScope::Global,
  2296. kind: ConfigKind::Boolean,
  2297. path: &["showTurnDuration"],
  2298. options: None,
  2299. },
  2300. "terminalProgressBarEnabled" => ConfigSettingSpec {
  2301. scope: ConfigScope::Global,
  2302. kind: ConfigKind::Boolean,
  2303. path: &["terminalProgressBarEnabled"],
  2304. options: None,
  2305. },
  2306. "todoFeatureEnabled" => ConfigSettingSpec {
  2307. scope: ConfigScope::Global,
  2308. kind: ConfigKind::Boolean,
  2309. path: &["todoFeatureEnabled"],
  2310. options: None,
  2311. },
  2312. "model" => ConfigSettingSpec {
  2313. scope: ConfigScope::Settings,
  2314. kind: ConfigKind::String,
  2315. path: &["model"],
  2316. options: None,
  2317. },
  2318. "alwaysThinkingEnabled" => ConfigSettingSpec {
  2319. scope: ConfigScope::Settings,
  2320. kind: ConfigKind::Boolean,
  2321. path: &["alwaysThinkingEnabled"],
  2322. options: None,
  2323. },
  2324. "permissions.defaultMode" => ConfigSettingSpec {
  2325. scope: ConfigScope::Settings,
  2326. kind: ConfigKind::String,
  2327. path: &["permissions", "defaultMode"],
  2328. options: Some(&["default", "plan", "acceptEdits", "dontAsk", "auto"]),
  2329. },
  2330. "language" => ConfigSettingSpec {
  2331. scope: ConfigScope::Settings,
  2332. kind: ConfigKind::String,
  2333. path: &["language"],
  2334. options: None,
  2335. },
  2336. "teammateMode" => ConfigSettingSpec {
  2337. scope: ConfigScope::Global,
  2338. kind: ConfigKind::String,
  2339. path: &["teammateMode"],
  2340. options: Some(&["tmux", "in-process", "auto"]),
  2341. },
  2342. _ => return None,
  2343. })
  2344. }
  2345. fn normalize_config_value(spec: ConfigSettingSpec, value: ConfigValue) -> Result<Value, String> {
  2346. let normalized = match (spec.kind, value) {
  2347. (ConfigKind::Boolean, ConfigValue::Bool(value)) => Value::Bool(value),
  2348. (ConfigKind::Boolean, ConfigValue::String(value)) => {
  2349. match value.trim().to_ascii_lowercase().as_str() {
  2350. "true" => Value::Bool(true),
  2351. "false" => Value::Bool(false),
  2352. _ => return Err(String::from("setting requires true or false")),
  2353. }
  2354. }
  2355. (ConfigKind::Boolean, ConfigValue::Number(_)) => {
  2356. return Err(String::from("setting requires true or false"))
  2357. }
  2358. (ConfigKind::String, ConfigValue::String(value)) => Value::String(value),
  2359. (ConfigKind::String, ConfigValue::Bool(value)) => Value::String(value.to_string()),
  2360. (ConfigKind::String, ConfigValue::Number(value)) => json!(value),
  2361. };
  2362. if let Some(options) = spec.options {
  2363. let Some(as_str) = normalized.as_str() else {
  2364. return Err(String::from("setting requires a string value"));
  2365. };
  2366. if !options.iter().any(|option| option == &as_str) {
  2367. return Err(format!(
  2368. "Invalid value \"{as_str}\". Options: {}",
  2369. options.join(", ")
  2370. ));
  2371. }
  2372. }
  2373. Ok(normalized)
  2374. }
  2375. fn config_file_for_scope(scope: ConfigScope) -> Result<PathBuf, String> {
  2376. let cwd = std::env::current_dir().map_err(|error| error.to_string())?;
  2377. Ok(match scope {
  2378. ConfigScope::Global => config_home_dir()?.join("settings.json"),
  2379. ConfigScope::Settings => cwd.join(".claude").join("settings.local.json"),
  2380. })
  2381. }
  2382. fn config_home_dir() -> Result<PathBuf, String> {
  2383. if let Ok(path) = std::env::var("CLAUDE_CONFIG_HOME") {
  2384. return Ok(PathBuf::from(path));
  2385. }
  2386. let home = std::env::var("HOME").map_err(|_| String::from("HOME is not set"))?;
  2387. Ok(PathBuf::from(home).join(".claude"))
  2388. }
  2389. fn read_json_object(path: &Path) -> Result<serde_json::Map<String, Value>, String> {
  2390. match std::fs::read_to_string(path) {
  2391. Ok(contents) => {
  2392. if contents.trim().is_empty() {
  2393. return Ok(serde_json::Map::new());
  2394. }
  2395. serde_json::from_str::<Value>(&contents)
  2396. .map_err(|error| error.to_string())?
  2397. .as_object()
  2398. .cloned()
  2399. .ok_or_else(|| String::from("config file must contain a JSON object"))
  2400. }
  2401. Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(serde_json::Map::new()),
  2402. Err(error) => Err(error.to_string()),
  2403. }
  2404. }
  2405. fn write_json_object(path: &Path, value: &serde_json::Map<String, Value>) -> Result<(), String> {
  2406. if let Some(parent) = path.parent() {
  2407. std::fs::create_dir_all(parent).map_err(|error| error.to_string())?;
  2408. }
  2409. std::fs::write(
  2410. path,
  2411. serde_json::to_string_pretty(value).map_err(|error| error.to_string())?,
  2412. )
  2413. .map_err(|error| error.to_string())
  2414. }
  2415. fn get_nested_value<'a>(
  2416. value: &'a serde_json::Map<String, Value>,
  2417. path: &[&str],
  2418. ) -> Option<&'a Value> {
  2419. let (first, rest) = path.split_first()?;
  2420. let mut current = value.get(*first)?;
  2421. for key in rest {
  2422. current = current.as_object()?.get(*key)?;
  2423. }
  2424. Some(current)
  2425. }
  2426. fn set_nested_value(root: &mut serde_json::Map<String, Value>, path: &[&str], new_value: Value) {
  2427. let (first, rest) = path.split_first().expect("config path must not be empty");
  2428. if rest.is_empty() {
  2429. root.insert((*first).to_string(), new_value);
  2430. return;
  2431. }
  2432. let entry = root
  2433. .entry((*first).to_string())
  2434. .or_insert_with(|| Value::Object(serde_json::Map::new()));
  2435. if !entry.is_object() {
  2436. *entry = Value::Object(serde_json::Map::new());
  2437. }
  2438. let map = entry.as_object_mut().expect("object inserted");
  2439. set_nested_value(map, rest, new_value);
  2440. }
  2441. fn iso8601_timestamp() -> String {
  2442. if let Ok(output) = Command::new("date")
  2443. .args(["-u", "+%Y-%m-%dT%H:%M:%SZ"])
  2444. .output()
  2445. {
  2446. if output.status.success() {
  2447. return String::from_utf8_lossy(&output.stdout).trim().to_string();
  2448. }
  2449. }
  2450. iso8601_now()
  2451. }
  2452. #[allow(clippy::needless_pass_by_value)]
  2453. fn execute_powershell(input: PowerShellInput) -> std::io::Result<runtime::BashCommandOutput> {
  2454. let _ = &input.description;
  2455. let shell = detect_powershell_shell()?;
  2456. execute_shell_command(
  2457. shell,
  2458. &input.command,
  2459. input.timeout,
  2460. input.run_in_background,
  2461. )
  2462. }
  2463. fn detect_powershell_shell() -> std::io::Result<&'static str> {
  2464. if command_exists("pwsh") {
  2465. Ok("pwsh")
  2466. } else if command_exists("powershell") {
  2467. Ok("powershell")
  2468. } else {
  2469. Err(std::io::Error::new(
  2470. std::io::ErrorKind::NotFound,
  2471. "PowerShell executable not found (expected `pwsh` or `powershell` in PATH)",
  2472. ))
  2473. }
  2474. }
  2475. fn command_exists(command: &str) -> bool {
  2476. std::process::Command::new("sh")
  2477. .arg("-lc")
  2478. .arg(format!("command -v {command} >/dev/null 2>&1"))
  2479. .status()
  2480. .map(|status| status.success())
  2481. .unwrap_or(false)
  2482. }
  2483. #[allow(clippy::too_many_lines)]
  2484. fn execute_shell_command(
  2485. shell: &str,
  2486. command: &str,
  2487. timeout: Option<u64>,
  2488. run_in_background: Option<bool>,
  2489. ) -> std::io::Result<runtime::BashCommandOutput> {
  2490. if run_in_background.unwrap_or(false) {
  2491. let child = std::process::Command::new(shell)
  2492. .arg("-NoProfile")
  2493. .arg("-NonInteractive")
  2494. .arg("-Command")
  2495. .arg(command)
  2496. .stdin(std::process::Stdio::null())
  2497. .stdout(std::process::Stdio::null())
  2498. .stderr(std::process::Stdio::null())
  2499. .spawn()?;
  2500. return Ok(runtime::BashCommandOutput {
  2501. stdout: String::new(),
  2502. stderr: String::new(),
  2503. raw_output_path: None,
  2504. interrupted: false,
  2505. is_image: None,
  2506. background_task_id: Some(child.id().to_string()),
  2507. backgrounded_by_user: Some(true),
  2508. assistant_auto_backgrounded: Some(false),
  2509. dangerously_disable_sandbox: None,
  2510. return_code_interpretation: None,
  2511. no_output_expected: Some(true),
  2512. structured_content: None,
  2513. persisted_output_path: None,
  2514. persisted_output_size: None,
  2515. sandbox_status: None,
  2516. });
  2517. }
  2518. let mut process = std::process::Command::new(shell);
  2519. process
  2520. .arg("-NoProfile")
  2521. .arg("-NonInteractive")
  2522. .arg("-Command")
  2523. .arg(command);
  2524. process
  2525. .stdout(std::process::Stdio::piped())
  2526. .stderr(std::process::Stdio::piped());
  2527. if let Some(timeout_ms) = timeout {
  2528. let mut child = process.spawn()?;
  2529. let started = Instant::now();
  2530. loop {
  2531. if let Some(status) = child.try_wait()? {
  2532. let output = child.wait_with_output()?;
  2533. return Ok(runtime::BashCommandOutput {
  2534. stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
  2535. stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
  2536. raw_output_path: None,
  2537. interrupted: false,
  2538. is_image: None,
  2539. background_task_id: None,
  2540. backgrounded_by_user: None,
  2541. assistant_auto_backgrounded: None,
  2542. dangerously_disable_sandbox: None,
  2543. return_code_interpretation: status
  2544. .code()
  2545. .filter(|code| *code != 0)
  2546. .map(|code| format!("exit_code:{code}")),
  2547. no_output_expected: Some(output.stdout.is_empty() && output.stderr.is_empty()),
  2548. structured_content: None,
  2549. persisted_output_path: None,
  2550. persisted_output_size: None,
  2551. sandbox_status: None,
  2552. });
  2553. }
  2554. if started.elapsed() >= Duration::from_millis(timeout_ms) {
  2555. let _ = child.kill();
  2556. let output = child.wait_with_output()?;
  2557. let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
  2558. let stderr = if stderr.trim().is_empty() {
  2559. format!("Command exceeded timeout of {timeout_ms} ms")
  2560. } else {
  2561. format!(
  2562. "{}
  2563. Command exceeded timeout of {timeout_ms} ms",
  2564. stderr.trim_end()
  2565. )
  2566. };
  2567. return Ok(runtime::BashCommandOutput {
  2568. stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
  2569. stderr,
  2570. raw_output_path: None,
  2571. interrupted: true,
  2572. is_image: None,
  2573. background_task_id: None,
  2574. backgrounded_by_user: None,
  2575. assistant_auto_backgrounded: None,
  2576. dangerously_disable_sandbox: None,
  2577. return_code_interpretation: Some(String::from("timeout")),
  2578. no_output_expected: Some(false),
  2579. structured_content: None,
  2580. persisted_output_path: None,
  2581. persisted_output_size: None,
  2582. sandbox_status: None,
  2583. });
  2584. }
  2585. std::thread::sleep(Duration::from_millis(10));
  2586. }
  2587. }
  2588. let output = process.output()?;
  2589. Ok(runtime::BashCommandOutput {
  2590. stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
  2591. stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
  2592. raw_output_path: None,
  2593. interrupted: false,
  2594. is_image: None,
  2595. background_task_id: None,
  2596. backgrounded_by_user: None,
  2597. assistant_auto_backgrounded: None,
  2598. dangerously_disable_sandbox: None,
  2599. return_code_interpretation: output
  2600. .status
  2601. .code()
  2602. .filter(|code| *code != 0)
  2603. .map(|code| format!("exit_code:{code}")),
  2604. no_output_expected: Some(output.stdout.is_empty() && output.stderr.is_empty()),
  2605. structured_content: None,
  2606. persisted_output_path: None,
  2607. persisted_output_size: None,
  2608. sandbox_status: None,
  2609. })
  2610. }
  2611. fn resolve_cell_index(
  2612. cells: &[serde_json::Value],
  2613. cell_id: Option<&str>,
  2614. edit_mode: NotebookEditMode,
  2615. ) -> Result<usize, String> {
  2616. if cells.is_empty()
  2617. && matches!(
  2618. edit_mode,
  2619. NotebookEditMode::Replace | NotebookEditMode::Delete
  2620. )
  2621. {
  2622. return Err(String::from("Notebook has no cells to edit"));
  2623. }
  2624. if let Some(cell_id) = cell_id {
  2625. cells
  2626. .iter()
  2627. .position(|cell| cell.get("id").and_then(serde_json::Value::as_str) == Some(cell_id))
  2628. .ok_or_else(|| format!("Cell id not found: {cell_id}"))
  2629. } else {
  2630. Ok(cells.len().saturating_sub(1))
  2631. }
  2632. }
  2633. fn source_lines(source: &str) -> Vec<serde_json::Value> {
  2634. if source.is_empty() {
  2635. return vec![serde_json::Value::String(String::new())];
  2636. }
  2637. source
  2638. .split_inclusive('\n')
  2639. .map(|line| serde_json::Value::String(line.to_string()))
  2640. .collect()
  2641. }
  2642. fn format_notebook_edit_mode(mode: NotebookEditMode) -> String {
  2643. match mode {
  2644. NotebookEditMode::Replace => String::from("replace"),
  2645. NotebookEditMode::Insert => String::from("insert"),
  2646. NotebookEditMode::Delete => String::from("delete"),
  2647. }
  2648. }
  2649. fn make_cell_id(index: usize) -> String {
  2650. format!("cell-{}", index + 1)
  2651. }
  2652. fn parse_skill_description(contents: &str) -> Option<String> {
  2653. for line in contents.lines() {
  2654. if let Some(value) = line.strip_prefix("description:") {
  2655. let trimmed = value.trim();
  2656. if !trimmed.is_empty() {
  2657. return Some(trimmed.to_string());
  2658. }
  2659. }
  2660. }
  2661. None
  2662. }
  2663. #[cfg(test)]
  2664. mod tests {
  2665. use std::collections::BTreeSet;
  2666. use std::fs;
  2667. use std::io::{Read, Write};
  2668. use std::net::{SocketAddr, TcpListener};
  2669. use std::path::PathBuf;
  2670. use std::sync::{Arc, Mutex, OnceLock};
  2671. use std::thread;
  2672. use std::time::Duration;
  2673. use super::{
  2674. agent_permission_policy, allowed_tools_for_subagent, execute_agent_with_spawn,
  2675. execute_tool, final_assistant_text, mvp_tool_specs, persist_agent_terminal_state,
  2676. AgentInput, AgentJob, SubagentToolExecutor,
  2677. };
  2678. use runtime::{ApiRequest, AssistantEvent, ConversationRuntime, RuntimeError, Session};
  2679. use serde_json::json;
  2680. fn env_lock() -> &'static Mutex<()> {
  2681. static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
  2682. LOCK.get_or_init(|| Mutex::new(()))
  2683. }
  2684. fn temp_path(name: &str) -> PathBuf {
  2685. let unique = std::time::SystemTime::now()
  2686. .duration_since(std::time::UNIX_EPOCH)
  2687. .expect("time")
  2688. .as_nanos();
  2689. std::env::temp_dir().join(format!("clawd-tools-{unique}-{name}"))
  2690. }
  2691. #[test]
  2692. fn exposes_mvp_tools() {
  2693. let names = mvp_tool_specs()
  2694. .into_iter()
  2695. .map(|spec| spec.name)
  2696. .collect::<Vec<_>>();
  2697. assert!(names.contains(&"bash"));
  2698. assert!(names.contains(&"read_file"));
  2699. assert!(names.contains(&"WebFetch"));
  2700. assert!(names.contains(&"WebSearch"));
  2701. assert!(names.contains(&"TodoWrite"));
  2702. assert!(names.contains(&"Skill"));
  2703. assert!(names.contains(&"Agent"));
  2704. assert!(names.contains(&"ToolSearch"));
  2705. assert!(names.contains(&"NotebookEdit"));
  2706. assert!(names.contains(&"Sleep"));
  2707. assert!(names.contains(&"SendUserMessage"));
  2708. assert!(names.contains(&"Config"));
  2709. assert!(names.contains(&"StructuredOutput"));
  2710. assert!(names.contains(&"REPL"));
  2711. assert!(names.contains(&"PowerShell"));
  2712. }
  2713. #[test]
  2714. fn rejects_unknown_tool_names() {
  2715. let error = execute_tool("nope", &json!({})).expect_err("tool should be rejected");
  2716. assert!(error.contains("unsupported tool"));
  2717. }
  2718. #[test]
  2719. fn web_fetch_returns_prompt_aware_summary() {
  2720. let server = TestServer::spawn(Arc::new(|request_line: &str| {
  2721. assert!(request_line.starts_with("GET /page "));
  2722. HttpResponse::html(
  2723. 200,
  2724. "OK",
  2725. "<html><head><title>Ignored</title></head><body><h1>Test Page</h1><p>Hello <b>world</b> from local server.</p></body></html>",
  2726. )
  2727. }));
  2728. let result = execute_tool(
  2729. "WebFetch",
  2730. &json!({
  2731. "url": format!("http://{}/page", server.addr()),
  2732. "prompt": "Summarize this page"
  2733. }),
  2734. )
  2735. .expect("WebFetch should succeed");
  2736. let output: serde_json::Value = serde_json::from_str(&result).expect("valid json");
  2737. assert_eq!(output["code"], 200);
  2738. let summary = output["result"].as_str().expect("result string");
  2739. assert!(summary.contains("Fetched"));
  2740. assert!(summary.contains("Test Page"));
  2741. assert!(summary.contains("Hello world from local server"));
  2742. let titled = execute_tool(
  2743. "WebFetch",
  2744. &json!({
  2745. "url": format!("http://{}/page", server.addr()),
  2746. "prompt": "What is the page title?"
  2747. }),
  2748. )
  2749. .expect("WebFetch title query should succeed");
  2750. let titled_output: serde_json::Value = serde_json::from_str(&titled).expect("valid json");
  2751. let titled_summary = titled_output["result"].as_str().expect("result string");
  2752. assert!(titled_summary.contains("Title: Ignored"));
  2753. }
  2754. #[test]
  2755. fn web_fetch_supports_plain_text_and_rejects_invalid_url() {
  2756. let server = TestServer::spawn(Arc::new(|request_line: &str| {
  2757. assert!(request_line.starts_with("GET /plain "));
  2758. HttpResponse::text(200, "OK", "plain text response")
  2759. }));
  2760. let result = execute_tool(
  2761. "WebFetch",
  2762. &json!({
  2763. "url": format!("http://{}/plain", server.addr()),
  2764. "prompt": "Show me the content"
  2765. }),
  2766. )
  2767. .expect("WebFetch should succeed for text content");
  2768. let output: serde_json::Value = serde_json::from_str(&result).expect("valid json");
  2769. assert_eq!(output["url"], format!("http://{}/plain", server.addr()));
  2770. assert!(output["result"]
  2771. .as_str()
  2772. .expect("result")
  2773. .contains("plain text response"));
  2774. let error = execute_tool(
  2775. "WebFetch",
  2776. &json!({
  2777. "url": "not a url",
  2778. "prompt": "Summarize"
  2779. }),
  2780. )
  2781. .expect_err("invalid URL should fail");
  2782. assert!(error.contains("relative URL without a base") || error.contains("invalid"));
  2783. }
  2784. #[test]
  2785. fn web_search_extracts_and_filters_results() {
  2786. let server = TestServer::spawn(Arc::new(|request_line: &str| {
  2787. assert!(request_line.contains("GET /search?q=rust+web+search "));
  2788. HttpResponse::html(
  2789. 200,
  2790. "OK",
  2791. r#"
  2792. <html><body>
  2793. <a class="result__a" href="https://docs.rs/reqwest">Reqwest docs</a>
  2794. <a class="result__a" href="https://example.com/blocked">Blocked result</a>
  2795. </body></html>
  2796. "#,
  2797. )
  2798. }));
  2799. std::env::set_var(
  2800. "CLAWD_WEB_SEARCH_BASE_URL",
  2801. format!("http://{}/search", server.addr()),
  2802. );
  2803. let result = execute_tool(
  2804. "WebSearch",
  2805. &json!({
  2806. "query": "rust web search",
  2807. "allowed_domains": ["https://DOCS.rs/"],
  2808. "blocked_domains": ["HTTPS://EXAMPLE.COM"]
  2809. }),
  2810. )
  2811. .expect("WebSearch should succeed");
  2812. std::env::remove_var("CLAWD_WEB_SEARCH_BASE_URL");
  2813. let output: serde_json::Value = serde_json::from_str(&result).expect("valid json");
  2814. assert_eq!(output["query"], "rust web search");
  2815. let results = output["results"].as_array().expect("results array");
  2816. let search_result = results
  2817. .iter()
  2818. .find(|item| item.get("content").is_some())
  2819. .expect("search result block present");
  2820. let content = search_result["content"].as_array().expect("content array");
  2821. assert_eq!(content.len(), 1);
  2822. assert_eq!(content[0]["title"], "Reqwest docs");
  2823. assert_eq!(content[0]["url"], "https://docs.rs/reqwest");
  2824. }
  2825. #[test]
  2826. fn web_search_handles_generic_links_and_invalid_base_url() {
  2827. let _guard = env_lock()
  2828. .lock()
  2829. .unwrap_or_else(std::sync::PoisonError::into_inner);
  2830. let server = TestServer::spawn(Arc::new(|request_line: &str| {
  2831. assert!(request_line.contains("GET /fallback?q=generic+links "));
  2832. HttpResponse::html(
  2833. 200,
  2834. "OK",
  2835. r#"
  2836. <html><body>
  2837. <a href="https://example.com/one">Example One</a>
  2838. <a href="https://example.com/one">Duplicate Example One</a>
  2839. <a href="https://docs.rs/tokio">Tokio Docs</a>
  2840. </body></html>
  2841. "#,
  2842. )
  2843. }));
  2844. std::env::set_var(
  2845. "CLAWD_WEB_SEARCH_BASE_URL",
  2846. format!("http://{}/fallback", server.addr()),
  2847. );
  2848. let result = execute_tool(
  2849. "WebSearch",
  2850. &json!({
  2851. "query": "generic links"
  2852. }),
  2853. )
  2854. .expect("WebSearch fallback parsing should succeed");
  2855. std::env::remove_var("CLAWD_WEB_SEARCH_BASE_URL");
  2856. let output: serde_json::Value = serde_json::from_str(&result).expect("valid json");
  2857. let results = output["results"].as_array().expect("results array");
  2858. let search_result = results
  2859. .iter()
  2860. .find(|item| item.get("content").is_some())
  2861. .expect("search result block present");
  2862. let content = search_result["content"].as_array().expect("content array");
  2863. assert_eq!(content.len(), 2);
  2864. assert_eq!(content[0]["url"], "https://example.com/one");
  2865. assert_eq!(content[1]["url"], "https://docs.rs/tokio");
  2866. std::env::set_var("CLAWD_WEB_SEARCH_BASE_URL", "://bad-base-url");
  2867. let error = execute_tool("WebSearch", &json!({ "query": "generic links" }))
  2868. .expect_err("invalid base URL should fail");
  2869. std::env::remove_var("CLAWD_WEB_SEARCH_BASE_URL");
  2870. assert!(error.contains("relative URL without a base") || error.contains("empty host"));
  2871. }
  2872. #[test]
  2873. fn todo_write_persists_and_returns_previous_state() {
  2874. let _guard = env_lock()
  2875. .lock()
  2876. .unwrap_or_else(std::sync::PoisonError::into_inner);
  2877. let path = temp_path("todos.json");
  2878. std::env::set_var("CLAWD_TODO_STORE", &path);
  2879. let first = execute_tool(
  2880. "TodoWrite",
  2881. &json!({
  2882. "todos": [
  2883. {"content": "Add tool", "activeForm": "Adding tool", "status": "in_progress"},
  2884. {"content": "Run tests", "activeForm": "Running tests", "status": "pending"}
  2885. ]
  2886. }),
  2887. )
  2888. .expect("TodoWrite should succeed");
  2889. let first_output: serde_json::Value = serde_json::from_str(&first).expect("valid json");
  2890. assert_eq!(first_output["oldTodos"].as_array().expect("array").len(), 0);
  2891. let second = execute_tool(
  2892. "TodoWrite",
  2893. &json!({
  2894. "todos": [
  2895. {"content": "Add tool", "activeForm": "Adding tool", "status": "completed"},
  2896. {"content": "Run tests", "activeForm": "Running tests", "status": "completed"},
  2897. {"content": "Verify", "activeForm": "Verifying", "status": "completed"}
  2898. ]
  2899. }),
  2900. )
  2901. .expect("TodoWrite should succeed");
  2902. std::env::remove_var("CLAWD_TODO_STORE");
  2903. let _ = std::fs::remove_file(path);
  2904. let second_output: serde_json::Value = serde_json::from_str(&second).expect("valid json");
  2905. assert_eq!(
  2906. second_output["oldTodos"].as_array().expect("array").len(),
  2907. 2
  2908. );
  2909. assert_eq!(
  2910. second_output["newTodos"].as_array().expect("array").len(),
  2911. 3
  2912. );
  2913. assert!(second_output["verificationNudgeNeeded"].is_null());
  2914. }
  2915. #[test]
  2916. fn todo_write_rejects_invalid_payloads_and_sets_verification_nudge() {
  2917. let _guard = env_lock()
  2918. .lock()
  2919. .unwrap_or_else(std::sync::PoisonError::into_inner);
  2920. let path = temp_path("todos-errors.json");
  2921. std::env::set_var("CLAWD_TODO_STORE", &path);
  2922. let empty = execute_tool("TodoWrite", &json!({ "todos": [] }))
  2923. .expect_err("empty todos should fail");
  2924. assert!(empty.contains("todos must not be empty"));
  2925. // Multiple in_progress items are now allowed for parallel workflows
  2926. let _multi_active = execute_tool(
  2927. "TodoWrite",
  2928. &json!({
  2929. "todos": [
  2930. {"content": "One", "activeForm": "Doing one", "status": "in_progress"},
  2931. {"content": "Two", "activeForm": "Doing two", "status": "in_progress"}
  2932. ]
  2933. }),
  2934. )
  2935. .expect("multiple in-progress todos should succeed");
  2936. let blank_content = execute_tool(
  2937. "TodoWrite",
  2938. &json!({
  2939. "todos": [
  2940. {"content": " ", "activeForm": "Doing it", "status": "pending"}
  2941. ]
  2942. }),
  2943. )
  2944. .expect_err("blank content should fail");
  2945. assert!(blank_content.contains("todo content must not be empty"));
  2946. let nudge = execute_tool(
  2947. "TodoWrite",
  2948. &json!({
  2949. "todos": [
  2950. {"content": "Write tests", "activeForm": "Writing tests", "status": "completed"},
  2951. {"content": "Fix errors", "activeForm": "Fixing errors", "status": "completed"},
  2952. {"content": "Ship branch", "activeForm": "Shipping branch", "status": "completed"}
  2953. ]
  2954. }),
  2955. )
  2956. .expect("completed todos should succeed");
  2957. std::env::remove_var("CLAWD_TODO_STORE");
  2958. let _ = fs::remove_file(path);
  2959. let output: serde_json::Value = serde_json::from_str(&nudge).expect("valid json");
  2960. assert_eq!(output["verificationNudgeNeeded"], true);
  2961. }
  2962. #[test]
  2963. fn skill_loads_local_skill_prompt() {
  2964. let result = execute_tool(
  2965. "Skill",
  2966. &json!({
  2967. "skill": "help",
  2968. "args": "overview"
  2969. }),
  2970. )
  2971. .expect("Skill should succeed");
  2972. let output: serde_json::Value = serde_json::from_str(&result).expect("valid json");
  2973. assert_eq!(output["skill"], "help");
  2974. assert!(output["path"]
  2975. .as_str()
  2976. .expect("path")
  2977. .ends_with("/help/SKILL.md"));
  2978. assert!(output["prompt"]
  2979. .as_str()
  2980. .expect("prompt")
  2981. .contains("Guide on using oh-my-codex plugin"));
  2982. let dollar_result = execute_tool(
  2983. "Skill",
  2984. &json!({
  2985. "skill": "$help"
  2986. }),
  2987. )
  2988. .expect("Skill should accept $skill invocation form");
  2989. let dollar_output: serde_json::Value =
  2990. serde_json::from_str(&dollar_result).expect("valid json");
  2991. assert_eq!(dollar_output["skill"], "$help");
  2992. assert!(dollar_output["path"]
  2993. .as_str()
  2994. .expect("path")
  2995. .ends_with("/help/SKILL.md"));
  2996. }
  2997. #[test]
  2998. fn tool_search_supports_keyword_and_select_queries() {
  2999. let keyword = execute_tool(
  3000. "ToolSearch",
  3001. &json!({"query": "web current", "max_results": 3}),
  3002. )
  3003. .expect("ToolSearch should succeed");
  3004. let keyword_output: serde_json::Value = serde_json::from_str(&keyword).expect("valid json");
  3005. let matches = keyword_output["matches"].as_array().expect("matches");
  3006. assert!(matches.iter().any(|value| value == "WebSearch"));
  3007. let selected = execute_tool("ToolSearch", &json!({"query": "select:Agent,Skill"}))
  3008. .expect("ToolSearch should succeed");
  3009. let selected_output: serde_json::Value =
  3010. serde_json::from_str(&selected).expect("valid json");
  3011. assert_eq!(selected_output["matches"][0], "Agent");
  3012. assert_eq!(selected_output["matches"][1], "Skill");
  3013. let aliased = execute_tool("ToolSearch", &json!({"query": "AgentTool"}))
  3014. .expect("ToolSearch should support tool aliases");
  3015. let aliased_output: serde_json::Value = serde_json::from_str(&aliased).expect("valid json");
  3016. assert_eq!(aliased_output["matches"][0], "Agent");
  3017. assert_eq!(aliased_output["normalized_query"], "agent");
  3018. let selected_with_alias =
  3019. execute_tool("ToolSearch", &json!({"query": "select:AgentTool,Skill"}))
  3020. .expect("ToolSearch alias select should succeed");
  3021. let selected_with_alias_output: serde_json::Value =
  3022. serde_json::from_str(&selected_with_alias).expect("valid json");
  3023. assert_eq!(selected_with_alias_output["matches"][0], "Agent");
  3024. assert_eq!(selected_with_alias_output["matches"][1], "Skill");
  3025. }
  3026. #[test]
  3027. fn agent_persists_handoff_metadata() {
  3028. let _guard = env_lock()
  3029. .lock()
  3030. .unwrap_or_else(std::sync::PoisonError::into_inner);
  3031. let dir = temp_path("agent-store");
  3032. std::env::set_var("CLAWD_AGENT_STORE", &dir);
  3033. let captured = Arc::new(Mutex::new(None::<AgentJob>));
  3034. let captured_for_spawn = Arc::clone(&captured);
  3035. let manifest = execute_agent_with_spawn(
  3036. AgentInput {
  3037. description: "Audit the branch".to_string(),
  3038. prompt: "Check tests and outstanding work.".to_string(),
  3039. subagent_type: Some("Explore".to_string()),
  3040. name: Some("ship-audit".to_string()),
  3041. model: None,
  3042. },
  3043. move |job| {
  3044. *captured_for_spawn
  3045. .lock()
  3046. .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(job);
  3047. Ok(())
  3048. },
  3049. )
  3050. .expect("Agent should succeed");
  3051. std::env::remove_var("CLAWD_AGENT_STORE");
  3052. assert_eq!(manifest.name, "ship-audit");
  3053. assert_eq!(manifest.subagent_type.as_deref(), Some("Explore"));
  3054. assert_eq!(manifest.status, "running");
  3055. assert!(!manifest.created_at.is_empty());
  3056. assert!(manifest.started_at.is_some());
  3057. assert!(manifest.completed_at.is_none());
  3058. let contents = std::fs::read_to_string(&manifest.output_file).expect("agent file exists");
  3059. let manifest_contents =
  3060. std::fs::read_to_string(&manifest.manifest_file).expect("manifest file exists");
  3061. assert!(contents.contains("Audit the branch"));
  3062. assert!(contents.contains("Check tests and outstanding work."));
  3063. assert!(manifest_contents.contains("\"subagentType\": \"Explore\""));
  3064. assert!(manifest_contents.contains("\"status\": \"running\""));
  3065. let captured_job = captured
  3066. .lock()
  3067. .unwrap_or_else(std::sync::PoisonError::into_inner)
  3068. .clone()
  3069. .expect("spawn job should be captured");
  3070. assert_eq!(captured_job.prompt, "Check tests and outstanding work.");
  3071. assert!(captured_job.allowed_tools.contains("read_file"));
  3072. assert!(!captured_job.allowed_tools.contains("Agent"));
  3073. let normalized = execute_tool(
  3074. "Agent",
  3075. &json!({
  3076. "description": "Verify the branch",
  3077. "prompt": "Check tests.",
  3078. "subagent_type": "explorer"
  3079. }),
  3080. )
  3081. .expect("Agent should normalize built-in aliases");
  3082. let normalized_output: serde_json::Value =
  3083. serde_json::from_str(&normalized).expect("valid json");
  3084. assert_eq!(normalized_output["subagentType"], "Explore");
  3085. let named = execute_tool(
  3086. "Agent",
  3087. &json!({
  3088. "description": "Review the branch",
  3089. "prompt": "Inspect diff.",
  3090. "name": "Ship Audit!!!"
  3091. }),
  3092. )
  3093. .expect("Agent should normalize explicit names");
  3094. let named_output: serde_json::Value = serde_json::from_str(&named).expect("valid json");
  3095. assert_eq!(named_output["name"], "ship-audit");
  3096. let _ = std::fs::remove_dir_all(dir);
  3097. }
  3098. #[test]
  3099. fn agent_fake_runner_can_persist_completion_and_failure() {
  3100. let _guard = env_lock()
  3101. .lock()
  3102. .unwrap_or_else(std::sync::PoisonError::into_inner);
  3103. let dir = temp_path("agent-runner");
  3104. std::env::set_var("CLAWD_AGENT_STORE", &dir);
  3105. let completed = execute_agent_with_spawn(
  3106. AgentInput {
  3107. description: "Complete the task".to_string(),
  3108. prompt: "Do the work".to_string(),
  3109. subagent_type: Some("Explore".to_string()),
  3110. name: Some("complete-task".to_string()),
  3111. model: Some("claude-sonnet-4-6".to_string()),
  3112. },
  3113. |job| {
  3114. persist_agent_terminal_state(
  3115. &job.manifest,
  3116. "completed",
  3117. Some("Finished successfully"),
  3118. None,
  3119. )
  3120. },
  3121. )
  3122. .expect("completed agent should succeed");
  3123. let completed_manifest = std::fs::read_to_string(&completed.manifest_file)
  3124. .expect("completed manifest should exist");
  3125. let completed_output =
  3126. std::fs::read_to_string(&completed.output_file).expect("completed output should exist");
  3127. assert!(completed_manifest.contains("\"status\": \"completed\""));
  3128. assert!(completed_output.contains("Finished successfully"));
  3129. let failed = execute_agent_with_spawn(
  3130. AgentInput {
  3131. description: "Fail the task".to_string(),
  3132. prompt: "Do the failing work".to_string(),
  3133. subagent_type: Some("Verification".to_string()),
  3134. name: Some("fail-task".to_string()),
  3135. model: None,
  3136. },
  3137. |job| {
  3138. persist_agent_terminal_state(
  3139. &job.manifest,
  3140. "failed",
  3141. None,
  3142. Some(String::from("simulated failure")),
  3143. )
  3144. },
  3145. )
  3146. .expect("failed agent should still spawn");
  3147. let failed_manifest =
  3148. std::fs::read_to_string(&failed.manifest_file).expect("failed manifest should exist");
  3149. let failed_output =
  3150. std::fs::read_to_string(&failed.output_file).expect("failed output should exist");
  3151. assert!(failed_manifest.contains("\"status\": \"failed\""));
  3152. assert!(failed_manifest.contains("simulated failure"));
  3153. assert!(failed_output.contains("simulated failure"));
  3154. let spawn_error = execute_agent_with_spawn(
  3155. AgentInput {
  3156. description: "Spawn error task".to_string(),
  3157. prompt: "Never starts".to_string(),
  3158. subagent_type: None,
  3159. name: Some("spawn-error".to_string()),
  3160. model: None,
  3161. },
  3162. |_| Err(String::from("thread creation failed")),
  3163. )
  3164. .expect_err("spawn errors should surface");
  3165. assert!(spawn_error.contains("failed to spawn sub-agent"));
  3166. let spawn_error_manifest = std::fs::read_dir(&dir)
  3167. .expect("agent dir should exist")
  3168. .filter_map(Result::ok)
  3169. .map(|entry| entry.path())
  3170. .filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("json"))
  3171. .find_map(|path| {
  3172. let contents = std::fs::read_to_string(&path).ok()?;
  3173. contents
  3174. .contains("\"name\": \"spawn-error\"")
  3175. .then_some(contents)
  3176. })
  3177. .expect("failed manifest should still be written");
  3178. assert!(spawn_error_manifest.contains("\"status\": \"failed\""));
  3179. assert!(spawn_error_manifest.contains("thread creation failed"));
  3180. std::env::remove_var("CLAWD_AGENT_STORE");
  3181. let _ = std::fs::remove_dir_all(dir);
  3182. }
  3183. #[test]
  3184. fn agent_tool_subset_mapping_is_expected() {
  3185. let general = allowed_tools_for_subagent("general-purpose");
  3186. assert!(general.contains("bash"));
  3187. assert!(general.contains("write_file"));
  3188. assert!(!general.contains("Agent"));
  3189. let explore = allowed_tools_for_subagent("Explore");
  3190. assert!(explore.contains("read_file"));
  3191. assert!(explore.contains("grep_search"));
  3192. assert!(!explore.contains("bash"));
  3193. let plan = allowed_tools_for_subagent("Plan");
  3194. assert!(plan.contains("TodoWrite"));
  3195. assert!(plan.contains("StructuredOutput"));
  3196. assert!(!plan.contains("Agent"));
  3197. let verification = allowed_tools_for_subagent("Verification");
  3198. assert!(verification.contains("bash"));
  3199. assert!(verification.contains("PowerShell"));
  3200. assert!(!verification.contains("write_file"));
  3201. }
  3202. #[derive(Debug)]
  3203. struct MockSubagentApiClient {
  3204. calls: usize,
  3205. input_path: String,
  3206. }
  3207. impl runtime::ApiClient for MockSubagentApiClient {
  3208. fn stream(&mut self, request: ApiRequest) -> Result<Vec<AssistantEvent>, RuntimeError> {
  3209. self.calls += 1;
  3210. match self.calls {
  3211. 1 => {
  3212. assert_eq!(request.messages.len(), 1);
  3213. Ok(vec![
  3214. AssistantEvent::ToolUse {
  3215. id: "tool-1".to_string(),
  3216. name: "read_file".to_string(),
  3217. input: json!({ "path": self.input_path }).to_string(),
  3218. },
  3219. AssistantEvent::MessageStop,
  3220. ])
  3221. }
  3222. 2 => {
  3223. assert!(request.messages.len() >= 3);
  3224. Ok(vec![
  3225. AssistantEvent::TextDelta("Scope: completed mock review".to_string()),
  3226. AssistantEvent::MessageStop,
  3227. ])
  3228. }
  3229. _ => panic!("unexpected mock stream call"),
  3230. }
  3231. }
  3232. }
  3233. #[test]
  3234. fn subagent_runtime_executes_tool_loop_with_isolated_session() {
  3235. let _guard = env_lock()
  3236. .lock()
  3237. .unwrap_or_else(std::sync::PoisonError::into_inner);
  3238. let path = temp_path("subagent-input.txt");
  3239. std::fs::write(&path, "hello from child").expect("write input file");
  3240. let mut runtime = ConversationRuntime::new(
  3241. Session::new(),
  3242. MockSubagentApiClient {
  3243. calls: 0,
  3244. input_path: path.display().to_string(),
  3245. },
  3246. SubagentToolExecutor::new(BTreeSet::from([String::from("read_file")])),
  3247. agent_permission_policy(),
  3248. vec![String::from("system prompt")],
  3249. );
  3250. let summary = runtime
  3251. .run_turn("Inspect the delegated file", None)
  3252. .expect("subagent loop should succeed");
  3253. assert_eq!(
  3254. final_assistant_text(&summary),
  3255. "Scope: completed mock review"
  3256. );
  3257. assert!(runtime
  3258. .session()
  3259. .messages
  3260. .iter()
  3261. .flat_map(|message| message.blocks.iter())
  3262. .any(|block| matches!(
  3263. block,
  3264. runtime::ContentBlock::ToolResult { output, .. }
  3265. if output.contains("hello from child")
  3266. )));
  3267. let _ = std::fs::remove_file(path);
  3268. }
  3269. #[test]
  3270. fn agent_rejects_blank_required_fields() {
  3271. let missing_description = execute_tool(
  3272. "Agent",
  3273. &json!({
  3274. "description": " ",
  3275. "prompt": "Inspect"
  3276. }),
  3277. )
  3278. .expect_err("blank description should fail");
  3279. assert!(missing_description.contains("description must not be empty"));
  3280. let missing_prompt = execute_tool(
  3281. "Agent",
  3282. &json!({
  3283. "description": "Inspect branch",
  3284. "prompt": " "
  3285. }),
  3286. )
  3287. .expect_err("blank prompt should fail");
  3288. assert!(missing_prompt.contains("prompt must not be empty"));
  3289. }
  3290. #[test]
  3291. fn notebook_edit_replaces_inserts_and_deletes_cells() {
  3292. let path = temp_path("notebook.ipynb");
  3293. std::fs::write(
  3294. &path,
  3295. r#"{
  3296. "cells": [
  3297. {"cell_type": "code", "id": "cell-a", "metadata": {}, "source": ["print(1)\n"], "outputs": [], "execution_count": null}
  3298. ],
  3299. "metadata": {"kernelspec": {"language": "python"}},
  3300. "nbformat": 4,
  3301. "nbformat_minor": 5
  3302. }"#,
  3303. )
  3304. .expect("write notebook");
  3305. let replaced = execute_tool(
  3306. "NotebookEdit",
  3307. &json!({
  3308. "notebook_path": path.display().to_string(),
  3309. "cell_id": "cell-a",
  3310. "new_source": "print(2)\n",
  3311. "edit_mode": "replace"
  3312. }),
  3313. )
  3314. .expect("NotebookEdit replace should succeed");
  3315. let replaced_output: serde_json::Value = serde_json::from_str(&replaced).expect("json");
  3316. assert_eq!(replaced_output["cell_id"], "cell-a");
  3317. assert_eq!(replaced_output["cell_type"], "code");
  3318. let inserted = execute_tool(
  3319. "NotebookEdit",
  3320. &json!({
  3321. "notebook_path": path.display().to_string(),
  3322. "cell_id": "cell-a",
  3323. "new_source": "# heading\n",
  3324. "cell_type": "markdown",
  3325. "edit_mode": "insert"
  3326. }),
  3327. )
  3328. .expect("NotebookEdit insert should succeed");
  3329. let inserted_output: serde_json::Value = serde_json::from_str(&inserted).expect("json");
  3330. assert_eq!(inserted_output["cell_type"], "markdown");
  3331. let appended = execute_tool(
  3332. "NotebookEdit",
  3333. &json!({
  3334. "notebook_path": path.display().to_string(),
  3335. "new_source": "print(3)\n",
  3336. "edit_mode": "insert"
  3337. }),
  3338. )
  3339. .expect("NotebookEdit append should succeed");
  3340. let appended_output: serde_json::Value = serde_json::from_str(&appended).expect("json");
  3341. assert_eq!(appended_output["cell_type"], "code");
  3342. let deleted = execute_tool(
  3343. "NotebookEdit",
  3344. &json!({
  3345. "notebook_path": path.display().to_string(),
  3346. "cell_id": "cell-a",
  3347. "edit_mode": "delete"
  3348. }),
  3349. )
  3350. .expect("NotebookEdit delete should succeed without new_source");
  3351. let deleted_output: serde_json::Value = serde_json::from_str(&deleted).expect("json");
  3352. assert!(deleted_output["cell_type"].is_null());
  3353. assert_eq!(deleted_output["new_source"], "");
  3354. let final_notebook: serde_json::Value =
  3355. serde_json::from_str(&std::fs::read_to_string(&path).expect("read notebook"))
  3356. .expect("valid notebook json");
  3357. let cells = final_notebook["cells"].as_array().expect("cells array");
  3358. assert_eq!(cells.len(), 2);
  3359. assert_eq!(cells[0]["cell_type"], "markdown");
  3360. assert!(cells[0].get("outputs").is_none());
  3361. assert_eq!(cells[1]["cell_type"], "code");
  3362. assert_eq!(cells[1]["source"][0], "print(3)\n");
  3363. let _ = std::fs::remove_file(path);
  3364. }
  3365. #[test]
  3366. fn notebook_edit_rejects_invalid_inputs() {
  3367. let text_path = temp_path("notebook.txt");
  3368. fs::write(&text_path, "not a notebook").expect("write text file");
  3369. let wrong_extension = execute_tool(
  3370. "NotebookEdit",
  3371. &json!({
  3372. "notebook_path": text_path.display().to_string(),
  3373. "new_source": "print(1)\n"
  3374. }),
  3375. )
  3376. .expect_err("non-ipynb file should fail");
  3377. assert!(wrong_extension.contains("Jupyter notebook"));
  3378. let _ = fs::remove_file(&text_path);
  3379. let empty_notebook = temp_path("empty.ipynb");
  3380. fs::write(
  3381. &empty_notebook,
  3382. r#"{"cells":[],"metadata":{"kernelspec":{"language":"python"}},"nbformat":4,"nbformat_minor":5}"#,
  3383. )
  3384. .expect("write empty notebook");
  3385. let missing_source = execute_tool(
  3386. "NotebookEdit",
  3387. &json!({
  3388. "notebook_path": empty_notebook.display().to_string(),
  3389. "edit_mode": "insert"
  3390. }),
  3391. )
  3392. .expect_err("insert without source should fail");
  3393. assert!(missing_source.contains("new_source is required"));
  3394. let missing_cell = execute_tool(
  3395. "NotebookEdit",
  3396. &json!({
  3397. "notebook_path": empty_notebook.display().to_string(),
  3398. "edit_mode": "delete"
  3399. }),
  3400. )
  3401. .expect_err("delete on empty notebook should fail");
  3402. assert!(missing_cell.contains("Notebook has no cells to edit"));
  3403. let _ = fs::remove_file(empty_notebook);
  3404. }
  3405. #[test]
  3406. fn bash_tool_reports_success_exit_failure_timeout_and_background() {
  3407. let success = execute_tool("bash", &json!({ "command": "printf 'hello'" }))
  3408. .expect("bash should succeed");
  3409. let success_output: serde_json::Value = serde_json::from_str(&success).expect("json");
  3410. assert_eq!(success_output["stdout"], "hello");
  3411. assert_eq!(success_output["interrupted"], false);
  3412. let failure = execute_tool("bash", &json!({ "command": "printf 'oops' >&2; exit 7" }))
  3413. .expect("bash failure should still return structured output");
  3414. let failure_output: serde_json::Value = serde_json::from_str(&failure).expect("json");
  3415. assert_eq!(failure_output["returnCodeInterpretation"], "exit_code:7");
  3416. assert!(failure_output["stderr"]
  3417. .as_str()
  3418. .expect("stderr")
  3419. .contains("oops"));
  3420. let timeout = execute_tool("bash", &json!({ "command": "sleep 1", "timeout": 10 }))
  3421. .expect("bash timeout should return output");
  3422. let timeout_output: serde_json::Value = serde_json::from_str(&timeout).expect("json");
  3423. assert_eq!(timeout_output["interrupted"], true);
  3424. assert_eq!(timeout_output["returnCodeInterpretation"], "timeout");
  3425. assert!(timeout_output["stderr"]
  3426. .as_str()
  3427. .expect("stderr")
  3428. .contains("Command exceeded timeout"));
  3429. let background = execute_tool(
  3430. "bash",
  3431. &json!({ "command": "sleep 1", "run_in_background": true }),
  3432. )
  3433. .expect("bash background should succeed");
  3434. let background_output: serde_json::Value = serde_json::from_str(&background).expect("json");
  3435. assert!(background_output["backgroundTaskId"].as_str().is_some());
  3436. assert_eq!(background_output["noOutputExpected"], true);
  3437. }
  3438. #[test]
  3439. fn file_tools_cover_read_write_and_edit_behaviors() {
  3440. let _guard = env_lock()
  3441. .lock()
  3442. .unwrap_or_else(std::sync::PoisonError::into_inner);
  3443. let root = temp_path("fs-suite");
  3444. fs::create_dir_all(&root).expect("create root");
  3445. let original_dir = std::env::current_dir().expect("cwd");
  3446. std::env::set_current_dir(&root).expect("set cwd");
  3447. let write_create = execute_tool(
  3448. "write_file",
  3449. &json!({ "path": "nested/demo.txt", "content": "alpha\nbeta\nalpha\n" }),
  3450. )
  3451. .expect("write create should succeed");
  3452. let write_create_output: serde_json::Value =
  3453. serde_json::from_str(&write_create).expect("json");
  3454. assert_eq!(write_create_output["type"], "create");
  3455. assert!(root.join("nested/demo.txt").exists());
  3456. let write_update = execute_tool(
  3457. "write_file",
  3458. &json!({ "path": "nested/demo.txt", "content": "alpha\nbeta\ngamma\n" }),
  3459. )
  3460. .expect("write update should succeed");
  3461. let write_update_output: serde_json::Value =
  3462. serde_json::from_str(&write_update).expect("json");
  3463. assert_eq!(write_update_output["type"], "update");
  3464. assert_eq!(write_update_output["originalFile"], "alpha\nbeta\nalpha\n");
  3465. let read_full = execute_tool("read_file", &json!({ "path": "nested/demo.txt" }))
  3466. .expect("read full should succeed");
  3467. let read_full_output: serde_json::Value = serde_json::from_str(&read_full).expect("json");
  3468. assert_eq!(read_full_output["file"]["content"], "alpha\nbeta\ngamma");
  3469. assert_eq!(read_full_output["file"]["startLine"], 1);
  3470. let read_slice = execute_tool(
  3471. "read_file",
  3472. &json!({ "path": "nested/demo.txt", "offset": 1, "limit": 1 }),
  3473. )
  3474. .expect("read slice should succeed");
  3475. let read_slice_output: serde_json::Value = serde_json::from_str(&read_slice).expect("json");
  3476. assert_eq!(read_slice_output["file"]["content"], "beta");
  3477. assert_eq!(read_slice_output["file"]["startLine"], 2);
  3478. let read_past_end = execute_tool(
  3479. "read_file",
  3480. &json!({ "path": "nested/demo.txt", "offset": 50 }),
  3481. )
  3482. .expect("read past EOF should succeed");
  3483. let read_past_end_output: serde_json::Value =
  3484. serde_json::from_str(&read_past_end).expect("json");
  3485. assert_eq!(read_past_end_output["file"]["content"], "");
  3486. assert_eq!(read_past_end_output["file"]["startLine"], 4);
  3487. let read_error = execute_tool("read_file", &json!({ "path": "missing.txt" }))
  3488. .expect_err("missing file should fail");
  3489. assert!(!read_error.is_empty());
  3490. let edit_once = execute_tool(
  3491. "edit_file",
  3492. &json!({ "path": "nested/demo.txt", "old_string": "alpha", "new_string": "omega" }),
  3493. )
  3494. .expect("single edit should succeed");
  3495. let edit_once_output: serde_json::Value = serde_json::from_str(&edit_once).expect("json");
  3496. assert_eq!(edit_once_output["replaceAll"], false);
  3497. assert_eq!(
  3498. fs::read_to_string(root.join("nested/demo.txt")).expect("read file"),
  3499. "omega\nbeta\ngamma\n"
  3500. );
  3501. execute_tool(
  3502. "write_file",
  3503. &json!({ "path": "nested/demo.txt", "content": "alpha\nbeta\nalpha\n" }),
  3504. )
  3505. .expect("reset file");
  3506. let edit_all = execute_tool(
  3507. "edit_file",
  3508. &json!({
  3509. "path": "nested/demo.txt",
  3510. "old_string": "alpha",
  3511. "new_string": "omega",
  3512. "replace_all": true
  3513. }),
  3514. )
  3515. .expect("replace all should succeed");
  3516. let edit_all_output: serde_json::Value = serde_json::from_str(&edit_all).expect("json");
  3517. assert_eq!(edit_all_output["replaceAll"], true);
  3518. assert_eq!(
  3519. fs::read_to_string(root.join("nested/demo.txt")).expect("read file"),
  3520. "omega\nbeta\nomega\n"
  3521. );
  3522. let edit_same = execute_tool(
  3523. "edit_file",
  3524. &json!({ "path": "nested/demo.txt", "old_string": "omega", "new_string": "omega" }),
  3525. )
  3526. .expect_err("identical old/new should fail");
  3527. assert!(edit_same.contains("must differ"));
  3528. let edit_missing = execute_tool(
  3529. "edit_file",
  3530. &json!({ "path": "nested/demo.txt", "old_string": "missing", "new_string": "omega" }),
  3531. )
  3532. .expect_err("missing substring should fail");
  3533. assert!(edit_missing.contains("old_string not found"));
  3534. std::env::set_current_dir(&original_dir).expect("restore cwd");
  3535. let _ = fs::remove_dir_all(root);
  3536. }
  3537. #[test]
  3538. fn glob_and_grep_tools_cover_success_and_errors() {
  3539. let _guard = env_lock()
  3540. .lock()
  3541. .unwrap_or_else(std::sync::PoisonError::into_inner);
  3542. let root = temp_path("search-suite");
  3543. fs::create_dir_all(root.join("nested")).expect("create root");
  3544. let original_dir = std::env::current_dir().expect("cwd");
  3545. std::env::set_current_dir(&root).expect("set cwd");
  3546. fs::write(
  3547. root.join("nested/lib.rs"),
  3548. "fn main() {}\nlet alpha = 1;\nlet alpha = 2;\n",
  3549. )
  3550. .expect("write rust file");
  3551. fs::write(root.join("nested/notes.txt"), "alpha\nbeta\n").expect("write txt file");
  3552. let globbed = execute_tool("glob_search", &json!({ "pattern": "nested/*.rs" }))
  3553. .expect("glob should succeed");
  3554. let globbed_output: serde_json::Value = serde_json::from_str(&globbed).expect("json");
  3555. assert_eq!(globbed_output["numFiles"], 1);
  3556. assert!(globbed_output["filenames"][0]
  3557. .as_str()
  3558. .expect("filename")
  3559. .ends_with("nested/lib.rs"));
  3560. let glob_error = execute_tool("glob_search", &json!({ "pattern": "[" }))
  3561. .expect_err("invalid glob should fail");
  3562. assert!(!glob_error.is_empty());
  3563. let grep_content = execute_tool(
  3564. "grep_search",
  3565. &json!({
  3566. "pattern": "alpha",
  3567. "path": "nested",
  3568. "glob": "*.rs",
  3569. "output_mode": "content",
  3570. "-n": true,
  3571. "head_limit": 1,
  3572. "offset": 1
  3573. }),
  3574. )
  3575. .expect("grep content should succeed");
  3576. let grep_content_output: serde_json::Value =
  3577. serde_json::from_str(&grep_content).expect("json");
  3578. assert_eq!(grep_content_output["numFiles"], 0);
  3579. assert!(grep_content_output["appliedLimit"].is_null());
  3580. assert_eq!(grep_content_output["appliedOffset"], 1);
  3581. assert!(grep_content_output["content"]
  3582. .as_str()
  3583. .expect("content")
  3584. .contains("let alpha = 2;"));
  3585. let grep_count = execute_tool(
  3586. "grep_search",
  3587. &json!({ "pattern": "alpha", "path": "nested", "output_mode": "count" }),
  3588. )
  3589. .expect("grep count should succeed");
  3590. let grep_count_output: serde_json::Value = serde_json::from_str(&grep_count).expect("json");
  3591. assert_eq!(grep_count_output["numMatches"], 3);
  3592. let grep_error = execute_tool(
  3593. "grep_search",
  3594. &json!({ "pattern": "(alpha", "path": "nested" }),
  3595. )
  3596. .expect_err("invalid regex should fail");
  3597. assert!(!grep_error.is_empty());
  3598. std::env::set_current_dir(&original_dir).expect("restore cwd");
  3599. let _ = fs::remove_dir_all(root);
  3600. }
  3601. #[test]
  3602. fn sleep_waits_and_reports_duration() {
  3603. let started = std::time::Instant::now();
  3604. let result =
  3605. execute_tool("Sleep", &json!({"duration_ms": 20})).expect("Sleep should succeed");
  3606. let elapsed = started.elapsed();
  3607. let output: serde_json::Value = serde_json::from_str(&result).expect("json");
  3608. assert_eq!(output["duration_ms"], 20);
  3609. assert!(output["message"]
  3610. .as_str()
  3611. .expect("message")
  3612. .contains("Slept for 20ms"));
  3613. assert!(elapsed >= Duration::from_millis(15));
  3614. }
  3615. #[test]
  3616. fn brief_returns_sent_message_and_attachment_metadata() {
  3617. let attachment = std::env::temp_dir().join(format!(
  3618. "clawd-brief-{}.png",
  3619. std::time::SystemTime::now()
  3620. .duration_since(std::time::UNIX_EPOCH)
  3621. .expect("time")
  3622. .as_nanos()
  3623. ));
  3624. std::fs::write(&attachment, b"png-data").expect("write attachment");
  3625. let result = execute_tool(
  3626. "SendUserMessage",
  3627. &json!({
  3628. "message": "hello user",
  3629. "attachments": [attachment.display().to_string()],
  3630. "status": "normal"
  3631. }),
  3632. )
  3633. .expect("SendUserMessage should succeed");
  3634. let output: serde_json::Value = serde_json::from_str(&result).expect("json");
  3635. assert_eq!(output["message"], "hello user");
  3636. assert!(output["sentAt"].as_str().is_some());
  3637. assert_eq!(output["attachments"][0]["isImage"], true);
  3638. let _ = std::fs::remove_file(attachment);
  3639. }
  3640. #[test]
  3641. fn config_reads_and_writes_supported_values() {
  3642. let _guard = env_lock()
  3643. .lock()
  3644. .unwrap_or_else(std::sync::PoisonError::into_inner);
  3645. let root = std::env::temp_dir().join(format!(
  3646. "clawd-config-{}",
  3647. std::time::SystemTime::now()
  3648. .duration_since(std::time::UNIX_EPOCH)
  3649. .expect("time")
  3650. .as_nanos()
  3651. ));
  3652. let home = root.join("home");
  3653. let cwd = root.join("cwd");
  3654. std::fs::create_dir_all(home.join(".claude")).expect("home dir");
  3655. std::fs::create_dir_all(cwd.join(".claude")).expect("cwd dir");
  3656. std::fs::write(
  3657. home.join(".claude").join("settings.json"),
  3658. r#"{"verbose":false}"#,
  3659. )
  3660. .expect("write global settings");
  3661. let original_home = std::env::var("HOME").ok();
  3662. let original_claude_home = std::env::var("CLAUDE_CONFIG_HOME").ok();
  3663. let original_dir = std::env::current_dir().expect("cwd");
  3664. std::env::set_var("HOME", &home);
  3665. std::env::remove_var("CLAUDE_CONFIG_HOME");
  3666. std::env::set_current_dir(&cwd).expect("set cwd");
  3667. let get = execute_tool("Config", &json!({"setting": "verbose"})).expect("get config");
  3668. let get_output: serde_json::Value = serde_json::from_str(&get).expect("json");
  3669. assert_eq!(get_output["value"], false);
  3670. let set = execute_tool(
  3671. "Config",
  3672. &json!({"setting": "permissions.defaultMode", "value": "plan"}),
  3673. )
  3674. .expect("set config");
  3675. let set_output: serde_json::Value = serde_json::from_str(&set).expect("json");
  3676. assert_eq!(set_output["operation"], "set");
  3677. assert_eq!(set_output["newValue"], "plan");
  3678. let invalid = execute_tool(
  3679. "Config",
  3680. &json!({"setting": "permissions.defaultMode", "value": "bogus"}),
  3681. )
  3682. .expect_err("invalid config value should error");
  3683. assert!(invalid.contains("Invalid value"));
  3684. let unknown =
  3685. execute_tool("Config", &json!({"setting": "nope"})).expect("unknown setting result");
  3686. let unknown_output: serde_json::Value = serde_json::from_str(&unknown).expect("json");
  3687. assert_eq!(unknown_output["success"], false);
  3688. std::env::set_current_dir(&original_dir).expect("restore cwd");
  3689. match original_home {
  3690. Some(value) => std::env::set_var("HOME", value),
  3691. None => std::env::remove_var("HOME"),
  3692. }
  3693. match original_claude_home {
  3694. Some(value) => std::env::set_var("CLAUDE_CONFIG_HOME", value),
  3695. None => std::env::remove_var("CLAUDE_CONFIG_HOME"),
  3696. }
  3697. let _ = std::fs::remove_dir_all(root);
  3698. }
  3699. #[test]
  3700. fn structured_output_echoes_input_payload() {
  3701. let result = execute_tool("StructuredOutput", &json!({"ok": true, "items": [1, 2, 3]}))
  3702. .expect("StructuredOutput should succeed");
  3703. let output: serde_json::Value = serde_json::from_str(&result).expect("json");
  3704. assert_eq!(output["data"], "Structured output provided successfully");
  3705. assert_eq!(output["structured_output"]["ok"], true);
  3706. assert_eq!(output["structured_output"]["items"][1], 2);
  3707. }
  3708. #[test]
  3709. fn repl_executes_python_code() {
  3710. let result = execute_tool(
  3711. "REPL",
  3712. &json!({"language": "python", "code": "print(1 + 1)", "timeout_ms": 500}),
  3713. )
  3714. .expect("REPL should succeed");
  3715. let output: serde_json::Value = serde_json::from_str(&result).expect("json");
  3716. assert_eq!(output["language"], "python");
  3717. assert_eq!(output["exitCode"], 0);
  3718. assert!(output["stdout"].as_str().expect("stdout").contains('2'));
  3719. }
  3720. #[test]
  3721. fn powershell_runs_via_stub_shell() {
  3722. let _guard = env_lock()
  3723. .lock()
  3724. .unwrap_or_else(std::sync::PoisonError::into_inner);
  3725. let dir = std::env::temp_dir().join(format!(
  3726. "clawd-pwsh-bin-{}",
  3727. std::time::SystemTime::now()
  3728. .duration_since(std::time::UNIX_EPOCH)
  3729. .expect("time")
  3730. .as_nanos()
  3731. ));
  3732. std::fs::create_dir_all(&dir).expect("create dir");
  3733. let script = dir.join("pwsh");
  3734. std::fs::write(
  3735. &script,
  3736. r#"#!/bin/sh
  3737. while [ "$1" != "-Command" ] && [ $# -gt 0 ]; do shift; done
  3738. shift
  3739. printf 'pwsh:%s' "$1"
  3740. "#,
  3741. )
  3742. .expect("write script");
  3743. std::process::Command::new("/bin/chmod")
  3744. .arg("+x")
  3745. .arg(&script)
  3746. .status()
  3747. .expect("chmod");
  3748. let original_path = std::env::var("PATH").unwrap_or_default();
  3749. std::env::set_var("PATH", format!("{}:{}", dir.display(), original_path));
  3750. let result = execute_tool(
  3751. "PowerShell",
  3752. &json!({"command": "Write-Output hello", "timeout": 1000}),
  3753. )
  3754. .expect("PowerShell should succeed");
  3755. let background = execute_tool(
  3756. "PowerShell",
  3757. &json!({"command": "Write-Output hello", "run_in_background": true}),
  3758. )
  3759. .expect("PowerShell background should succeed");
  3760. std::env::set_var("PATH", original_path);
  3761. let _ = std::fs::remove_dir_all(dir);
  3762. let output: serde_json::Value = serde_json::from_str(&result).expect("json");
  3763. assert_eq!(output["stdout"], "pwsh:Write-Output hello");
  3764. assert!(output["stderr"].as_str().expect("stderr").is_empty());
  3765. let background_output: serde_json::Value = serde_json::from_str(&background).expect("json");
  3766. assert!(background_output["backgroundTaskId"].as_str().is_some());
  3767. assert_eq!(background_output["backgroundedByUser"], true);
  3768. assert_eq!(background_output["assistantAutoBackgrounded"], false);
  3769. }
  3770. #[test]
  3771. fn powershell_errors_when_shell_is_missing() {
  3772. let _guard = env_lock()
  3773. .lock()
  3774. .unwrap_or_else(std::sync::PoisonError::into_inner);
  3775. let original_path = std::env::var("PATH").unwrap_or_default();
  3776. let empty_dir = std::env::temp_dir().join(format!(
  3777. "clawd-empty-bin-{}",
  3778. std::time::SystemTime::now()
  3779. .duration_since(std::time::UNIX_EPOCH)
  3780. .expect("time")
  3781. .as_nanos()
  3782. ));
  3783. std::fs::create_dir_all(&empty_dir).expect("create empty dir");
  3784. std::env::set_var("PATH", empty_dir.display().to_string());
  3785. let err = execute_tool("PowerShell", &json!({"command": "Write-Output hello"}))
  3786. .expect_err("PowerShell should fail when shell is missing");
  3787. std::env::set_var("PATH", original_path);
  3788. let _ = std::fs::remove_dir_all(empty_dir);
  3789. assert!(err.contains("PowerShell executable not found"));
  3790. }
  3791. struct TestServer {
  3792. addr: SocketAddr,
  3793. shutdown: Option<std::sync::mpsc::Sender<()>>,
  3794. handle: Option<thread::JoinHandle<()>>,
  3795. }
  3796. impl TestServer {
  3797. fn spawn(handler: Arc<dyn Fn(&str) -> HttpResponse + Send + Sync + 'static>) -> Self {
  3798. let listener = TcpListener::bind("127.0.0.1:0").expect("bind test server");
  3799. listener
  3800. .set_nonblocking(true)
  3801. .expect("set nonblocking listener");
  3802. let addr = listener.local_addr().expect("local addr");
  3803. let (tx, rx) = std::sync::mpsc::channel::<()>();
  3804. let handle = thread::spawn(move || loop {
  3805. if rx.try_recv().is_ok() {
  3806. break;
  3807. }
  3808. match listener.accept() {
  3809. Ok((mut stream, _)) => {
  3810. let mut buffer = [0_u8; 4096];
  3811. let size = stream.read(&mut buffer).expect("read request");
  3812. let request = String::from_utf8_lossy(&buffer[..size]).into_owned();
  3813. let request_line = request.lines().next().unwrap_or_default().to_string();
  3814. let response = handler(&request_line);
  3815. stream
  3816. .write_all(response.to_bytes().as_slice())
  3817. .expect("write response");
  3818. }
  3819. Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
  3820. thread::sleep(Duration::from_millis(10));
  3821. }
  3822. Err(error) => panic!("server accept failed: {error}"),
  3823. }
  3824. });
  3825. Self {
  3826. addr,
  3827. shutdown: Some(tx),
  3828. handle: Some(handle),
  3829. }
  3830. }
  3831. fn addr(&self) -> SocketAddr {
  3832. self.addr
  3833. }
  3834. }
  3835. impl Drop for TestServer {
  3836. fn drop(&mut self) {
  3837. if let Some(tx) = self.shutdown.take() {
  3838. let _ = tx.send(());
  3839. }
  3840. if let Some(handle) = self.handle.take() {
  3841. handle.join().expect("join test server");
  3842. }
  3843. }
  3844. }
  3845. struct HttpResponse {
  3846. status: u16,
  3847. reason: &'static str,
  3848. content_type: &'static str,
  3849. body: String,
  3850. }
  3851. impl HttpResponse {
  3852. fn html(status: u16, reason: &'static str, body: &str) -> Self {
  3853. Self {
  3854. status,
  3855. reason,
  3856. content_type: "text/html; charset=utf-8",
  3857. body: body.to_string(),
  3858. }
  3859. }
  3860. fn text(status: u16, reason: &'static str, body: &str) -> Self {
  3861. Self {
  3862. status,
  3863. reason,
  3864. content_type: "text/plain; charset=utf-8",
  3865. body: body.to_string(),
  3866. }
  3867. }
  3868. fn to_bytes(&self) -> Vec<u8> {
  3869. format!(
  3870. "HTTP/1.1 {} {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
  3871. self.status,
  3872. self.reason,
  3873. self.content_type,
  3874. self.body.len(),
  3875. self.body
  3876. )
  3877. .into_bytes()
  3878. }
  3879. }
  3880. }
备用站点 当前处于降级运行的备用站点,仅供应急访问,数据和功能可能不是最新。