lib.rs 210 KB

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