Skip to main content

netget/client/webdav/
actions.rs

1//! WebDAV client protocol actions implementation
2
3use 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
15/// WebDAV client connected event
16pub 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
34/// WebDAV client response received event
35pub 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
73/// WebDAV client protocol action handler
74pub struct WebdavClientProtocol;
75
76impl WebdavClientProtocol {
77    pub fn new() -> Self {
78        Self
79    }
80}
81
82// Implement Protocol trait (common functionality)
83impl 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        // Sync actions (response to events) - similar to async actions
301        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            // LLM mode: LLM controls WebDAV operations
390            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            // Script mode: Code-based file operations
397            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            // Static mode: Fixed directory listing
411            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
442// Implement Client trait (client-specific functionality)
443impl 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            // Declared in get_sync_actions, so it has to be executable here too --
633            // advertising a name the executor rejects shows the model a tool it is
634            // then punished for using.
635            "wait_for_more" => Ok(ClientActionResult::WaitForMore),
636            _ => Err(anyhow::anyhow!(
637                "Unknown WebDAV client action: {}",
638                action_type
639            )),
640        }
641    }
642}