lib.rs 182 KB

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