lib.rs 146 KB

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