1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 protocol_trait::Protocol,
6 ActionDefinition, Parameter, ParameterDefinition,
7};
8use crate::protocol::log_template::LogTemplate;
9use crate::protocol::EventType;
10use crate::state::app_state::AppState;
11use anyhow::{Context, Result};
12use serde_json::json;
13use std::sync::LazyLock;
14
15pub static WEBDAV_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
17 EventType::new(
18 "webdav_connected",
19 "WebDAV client initialized and ready to send requests",
20 json!({
21 "type": "propfind",
22 "path": "/dav/",
23 "depth": "1"
24 }),
25 )
26 .with_parameters(vec![Parameter {
27 name: "base_url".to_string(),
28 type_hint: "string".to_string(),
29 description: "Base URL for WebDAV requests".to_string(),
30 required: true,
31 }])
32});
33
34pub static WEBDAV_CLIENT_RESPONSE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
36 EventType::new(
37 "webdav_response_received",
38 "WebDAV response received from server",
39 json!({
40 "type": "propfind",
41 "path": "/dav/documents/",
42 "depth": "1"
43 }),
44 )
45 .with_parameters(vec![
46 Parameter {
47 name: "status_code".to_string(),
48 type_hint: "number".to_string(),
49 description: "HTTP status code".to_string(),
50 required: true,
51 },
52 Parameter {
53 name: "headers".to_string(),
54 type_hint: "object".to_string(),
55 description: "Response headers".to_string(),
56 required: true,
57 },
58 Parameter {
59 name: "body".to_string(),
60 type_hint: "string".to_string(),
61 description: "Response body (typically XML)".to_string(),
62 required: true,
63 },
64 Parameter {
65 name: "method".to_string(),
66 type_hint: "string".to_string(),
67 description: "WebDAV method used in request".to_string(),
68 required: true,
69 },
70 ])
71});
72
73pub struct WebdavClientProtocol;
75
76impl WebdavClientProtocol {
77 pub fn new() -> Self {
78 Self
79 }
80}
81
82impl Protocol for WebdavClientProtocol {
84 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
85 vec![
86 ParameterDefinition {
87 name: "default_headers".to_string(),
88 description: "Default headers to include in all requests".to_string(),
89 type_hint: "object".to_string(),
90 required: false,
91 example: json!({
92 "User-Agent": "NetGet/1.0",
93 "Accept": "application/xml"
94 }),
95 default: None,
96 },
97 ParameterDefinition {
98 name: "auth".to_string(),
99 description: "Authentication credentials (username:password)".to_string(),
100 type_hint: "string".to_string(),
101 required: false,
102 example: json!("username:password"),
103 default: None,
104 },
105 ]
106 }
107 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
108 vec![
109 ActionDefinition {
110 name: "propfind".to_string(),
111 description: "Retrieve properties of a resource (WebDAV PROPFIND)".to_string(),
112 parameters: vec![
113 Parameter {
114 name: "path".to_string(),
115 type_hint: "string".to_string(),
116 description: "Resource path (e.g., /dav/folder/), resolved against the client's base URL. An absolute URL is accepted only on that same origin; another host needs its own client".to_string(),
117 required: true,
118 },
119 Parameter {
120 name: "depth".to_string(),
121 type_hint: "string".to_string(),
122 description: "Depth header: 0 (resource only), 1 (resource + children), infinity (all)".to_string(),
123 required: false,
124 },
125 Parameter {
126 name: "properties".to_string(),
127 type_hint: "array".to_string(),
128 description: "Specific properties to request (default: allprop)".to_string(),
129 required: false,
130 },
131 ],
132 example: json!({
133 "type": "propfind",
134 "path": "/dav/documents/",
135 "depth": "1"
136 }),
137 log_template: None,
138 },
139 ActionDefinition {
140 name: "mkcol".to_string(),
141 description: "Create a new collection (directory)".to_string(),
142 parameters: vec![
143 Parameter {
144 name: "path".to_string(),
145 type_hint: "string".to_string(),
146 description: "Path for new collection".to_string(),
147 required: true,
148 },
149 ],
150 example: json!({
151 "type": "mkcol",
152 "path": "/dav/newfolder/"
153 }),
154 log_template: None,
155 },
156 ActionDefinition {
157 name: "copy".to_string(),
158 description: "Copy a resource to a new location".to_string(),
159 parameters: vec![
160 Parameter {
161 name: "source".to_string(),
162 type_hint: "string".to_string(),
163 description: "Source resource path".to_string(),
164 required: true,
165 },
166 Parameter {
167 name: "destination".to_string(),
168 type_hint: "string".to_string(),
169 description: "Destination path".to_string(),
170 required: true,
171 },
172 Parameter {
173 name: "overwrite".to_string(),
174 type_hint: "boolean".to_string(),
175 description: "Whether to overwrite existing resource (default: true)".to_string(),
176 required: false,
177 },
178 Parameter {
179 name: "depth".to_string(),
180 type_hint: "string".to_string(),
181 description: "Depth for copying collections (0 or infinity)".to_string(),
182 required: false,
183 },
184 ],
185 example: json!({
186 "type": "copy",
187 "source": "/dav/file.txt",
188 "destination": "/dav/backup/file.txt",
189 "overwrite": true
190 }),
191 log_template: None,
192 },
193 ActionDefinition {
194 name: "move".to_string(),
195 description: "Move a resource to a new location".to_string(),
196 parameters: vec![
197 Parameter {
198 name: "source".to_string(),
199 type_hint: "string".to_string(),
200 description: "Source resource path".to_string(),
201 required: true,
202 },
203 Parameter {
204 name: "destination".to_string(),
205 type_hint: "string".to_string(),
206 description: "Destination path".to_string(),
207 required: true,
208 },
209 Parameter {
210 name: "overwrite".to_string(),
211 type_hint: "boolean".to_string(),
212 description: "Whether to overwrite existing resource (default: true)".to_string(),
213 required: false,
214 },
215 ],
216 example: json!({
217 "type": "move",
218 "source": "/dav/old/file.txt",
219 "destination": "/dav/new/file.txt"
220 }),
221 log_template: None,
222 },
223 ActionDefinition {
224 name: "delete".to_string(),
225 description: "Delete a resource".to_string(),
226 parameters: vec![
227 Parameter {
228 name: "path".to_string(),
229 type_hint: "string".to_string(),
230 description: "Path to resource to delete".to_string(),
231 required: true,
232 },
233 ],
234 example: json!({
235 "type": "delete",
236 "path": "/dav/file.txt"
237 }),
238 log_template: None,
239 },
240 ActionDefinition {
241 name: "put".to_string(),
242 description: "Upload a file (HTTP PUT)".to_string(),
243 parameters: vec![
244 Parameter {
245 name: "path".to_string(),
246 type_hint: "string".to_string(),
247 description: "Path for the file".to_string(),
248 required: true,
249 },
250 Parameter {
251 name: "content".to_string(),
252 type_hint: "string".to_string(),
253 description: "File content".to_string(),
254 required: true,
255 },
256 Parameter {
257 name: "content_type".to_string(),
258 type_hint: "string".to_string(),
259 description: "Content-Type header (default: application/octet-stream)".to_string(),
260 required: false,
261 },
262 ],
263 example: json!({
264 "type": "put",
265 "path": "/dav/file.txt",
266 "content": "Hello, WebDAV!",
267 "content_type": "text/plain"
268 }),
269 log_template: None,
270 },
271 ActionDefinition {
272 name: "get".to_string(),
273 description: "Download a file (HTTP GET)".to_string(),
274 parameters: vec![
275 Parameter {
276 name: "path".to_string(),
277 type_hint: "string".to_string(),
278 description: "Path to file to download".to_string(),
279 required: true,
280 },
281 ],
282 example: json!({
283 "type": "get",
284 "path": "/dav/file.txt"
285 }),
286 log_template: None,
287 },
288 ActionDefinition {
289 name: "disconnect".to_string(),
290 description: "Disconnect from the WebDAV server".to_string(),
291 parameters: vec![],
292 example: json!({
293 "type": "disconnect"
294 }),
295 log_template: None,
296 },
297 ]
298 }
299 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
300 vec![
302 ActionDefinition {
303 name: "wait_for_more".to_string(),
304 description: "Do nothing and wait for the next WebDAV response. The \
305 correct answer when what arrived needs no follow-up -- without it the \
306 model has to invent an action it does not want."
307 .to_string(),
308 parameters: vec![],
309 example: json!({ "type": "wait_for_more" }),
310 log_template: None,
311 },
312 ActionDefinition {
313 name: "propfind".to_string(),
314 description: "Send another PROPFIND in response to received data".to_string(),
315 parameters: vec![
316 Parameter {
317 name: "path".to_string(),
318 type_hint: "string".to_string(),
319 description: "Resource path, resolved against the client's base URL. An absolute URL is accepted only on that same origin; another host needs its own client".to_string(),
320 required: true,
321 },
322 Parameter {
323 name: "depth".to_string(),
324 type_hint: "string".to_string(),
325 description: "Depth header".to_string(),
326 required: false,
327 },
328 ],
329 example: json!({
330 "type": "propfind",
331 "path": "/dav/folder/",
332 "depth": "1"
333 }),
334 log_template: Some(
335 LogTemplate::new()
336 .with_info("-> WebDAV propfind {path}")
337 .with_debug("WebDAV propfind: path={path} depth={depth}"),
338 ),
339 },
340 ]
341 }
342 fn protocol_name(&self) -> &'static str {
343 "WebDAV"
344 }
345 fn get_event_types(&self) -> Vec<EventType> {
346 vec![
347 EventType::new(
348 "webdav_connected",
349 "Triggered when WebDAV client is initialized",
350 json!({"type": "placeholder", "event_id": "webdav_connected"}),
351 ),
352 EventType::new(
353 "webdav_response_received",
354 "Triggered when WebDAV client receives a response",
355 json!({"type": "placeholder", "event_id": "webdav_response_received"}),
356 ),
357 ]
358 }
359 fn stack_name(&self) -> &'static str {
360 "ETH>IP>TCP>HTTP>WebDAV"
361 }
362 fn keywords(&self) -> Vec<&'static str> {
363 vec!["webdav", "webdav client", "connect to webdav", "dav"]
364 }
365 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
366 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
367
368 ProtocolMetadataV2::builder()
369 .state(DevelopmentState::Experimental)
370 .implementation("reqwest HTTP client with WebDAV methods")
371 .llm_control("Full control over WebDAV operations (PROPFIND, MKCOL, COPY, MOVE, etc.)")
372 .e2e_testing("Local WebDAV server or public WebDAV endpoint")
373 .build()
374 }
375 fn description(&self) -> &'static str {
376 "WebDAV client for remote file management"
377 }
378 fn example_prompt(&self) -> &'static str {
379 "Connect to http://webdav.example.com/dav and list the contents of the root directory"
380 }
381 fn group_name(&self) -> &'static str {
382 "File & Print"
383 }
384 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
385 use crate::llm::actions::StartupExamples;
386 use serde_json::json;
387
388 StartupExamples::new(
389 json!({
391 "type": "open_client",
392 "remote_addr": "webdav.example.com:80",
393 "base_stack": "webdav",
394 "instruction": "List the contents of /dav/ and upload a test file"
395 }),
396 json!({
398 "type": "open_client",
399 "remote_addr": "webdav.example.com:80",
400 "base_stack": "webdav",
401 "event_handlers": [{
402 "event_pattern": "webdav_response_received",
403 "handler": {
404 "type": "script",
405 "language": "python",
406 "code": "<webdav_client_handler>"
407 }
408 }]
409 }),
410 json!({
412 "type": "open_client",
413 "remote_addr": "webdav.example.com:80",
414 "base_stack": "webdav",
415 "event_handlers": [
416 {
417 "event_pattern": "webdav_connected",
418 "handler": {
419 "type": "static",
420 "actions": [{
421 "type": "propfind",
422 "path": "/dav/",
423 "depth": "1"
424 }]
425 }
426 },
427 {
428 "event_pattern": "webdav_response_received",
429 "handler": {
430 "type": "static",
431 "actions": [{
432 "type": "disconnect"
433 }]
434 }
435 }
436 ]
437 }),
438 )
439 }
440}
441
442impl Client for WebdavClientProtocol {
444 fn connect(
445 &self,
446 ctx: crate::protocol::ConnectContext,
447 ) -> std::pin::Pin<
448 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
449 > {
450 Box::pin(async move {
451 use crate::client::webdav::WebdavClient;
452 WebdavClient::connect_with_llm_actions(
453 ctx.remote_addr,
454 ctx.llm_client,
455 ctx.state,
456 ctx.status_tx,
457 ctx.client_id,
458 ctx.startup_params,
459 )
460 .await
461 })
462 }
463 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
464 let action_type = action
465 .get("type")
466 .and_then(|v| v.as_str())
467 .context("Missing 'type' field in action")?;
468
469 match action_type {
470 "propfind" => {
471 let path = action
472 .get("path")
473 .and_then(|v| v.as_str())
474 .context("Missing 'path' field")?
475 .to_string();
476
477 let depth = action
478 .get("depth")
479 .and_then(|v| v.as_str())
480 .unwrap_or("1")
481 .to_string();
482
483 let properties = action.get("properties").and_then(|v| v.as_array()).cloned();
484
485 Ok(ClientActionResult::Custom {
486 name: "webdav_request".to_string(),
487 data: json!({
488 "method": "PROPFIND",
489 "path": path,
490 "depth": depth,
491 "properties": properties,
492 }),
493 })
494 }
495 "mkcol" => {
496 let path = action
497 .get("path")
498 .and_then(|v| v.as_str())
499 .context("Missing 'path' field")?
500 .to_string();
501
502 Ok(ClientActionResult::Custom {
503 name: "webdav_request".to_string(),
504 data: json!({
505 "method": "MKCOL",
506 "path": path,
507 }),
508 })
509 }
510 "copy" => {
511 let source = action
512 .get("source")
513 .and_then(|v| v.as_str())
514 .context("Missing 'source' field")?
515 .to_string();
516
517 let destination = action
518 .get("destination")
519 .and_then(|v| v.as_str())
520 .context("Missing 'destination' field")?
521 .to_string();
522
523 let overwrite = action
524 .get("overwrite")
525 .and_then(|v| v.as_bool())
526 .unwrap_or(true);
527
528 let depth = action
529 .get("depth")
530 .and_then(|v| v.as_str())
531 .map(|s| s.to_string());
532
533 Ok(ClientActionResult::Custom {
534 name: "webdav_request".to_string(),
535 data: json!({
536 "method": "COPY",
537 "path": source,
538 "destination": destination,
539 "overwrite": overwrite,
540 "depth": depth,
541 }),
542 })
543 }
544 "move" => {
545 let source = action
546 .get("source")
547 .and_then(|v| v.as_str())
548 .context("Missing 'source' field")?
549 .to_string();
550
551 let destination = action
552 .get("destination")
553 .and_then(|v| v.as_str())
554 .context("Missing 'destination' field")?
555 .to_string();
556
557 let overwrite = action
558 .get("overwrite")
559 .and_then(|v| v.as_bool())
560 .unwrap_or(true);
561
562 Ok(ClientActionResult::Custom {
563 name: "webdav_request".to_string(),
564 data: json!({
565 "method": "MOVE",
566 "path": source,
567 "destination": destination,
568 "overwrite": overwrite,
569 }),
570 })
571 }
572 "delete" => {
573 let path = action
574 .get("path")
575 .and_then(|v| v.as_str())
576 .context("Missing 'path' field")?
577 .to_string();
578
579 Ok(ClientActionResult::Custom {
580 name: "webdav_request".to_string(),
581 data: json!({
582 "method": "DELETE",
583 "path": path,
584 }),
585 })
586 }
587 "put" => {
588 let path = action
589 .get("path")
590 .and_then(|v| v.as_str())
591 .context("Missing 'path' field")?
592 .to_string();
593
594 let content = action
595 .get("content")
596 .and_then(|v| v.as_str())
597 .context("Missing 'content' field")?
598 .to_string();
599
600 let content_type = action
601 .get("content_type")
602 .and_then(|v| v.as_str())
603 .unwrap_or("application/octet-stream")
604 .to_string();
605
606 Ok(ClientActionResult::Custom {
607 name: "webdav_request".to_string(),
608 data: json!({
609 "method": "PUT",
610 "path": path,
611 "content": content,
612 "content_type": content_type,
613 }),
614 })
615 }
616 "get" => {
617 let path = action
618 .get("path")
619 .and_then(|v| v.as_str())
620 .context("Missing 'path' field")?
621 .to_string();
622
623 Ok(ClientActionResult::Custom {
624 name: "webdav_request".to_string(),
625 data: json!({
626 "method": "GET",
627 "path": path,
628 }),
629 })
630 }
631 "disconnect" => Ok(ClientActionResult::Disconnect),
632 "wait_for_more" => Ok(ClientActionResult::WaitForMore),
636 _ => Err(anyhow::anyhow!(
637 "Unknown WebDAV client action: {}",
638 action_type
639 )),
640 }
641 }
642}