lib.rs 155 KB

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