1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 protocol_trait::Protocol,
6 ActionDefinition, Parameter, ParameterDefinition,
7};
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14pub static VNC_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16 EventType::new(
17 "vnc_connected",
18 "VNC client successfully connected and authenticated",
19 json!({
20 "type": "request_framebuffer_update",
21 "incremental": true
22 }),
23 )
24 .with_parameters(vec![
25 Parameter {
26 name: "remote_addr".to_string(),
27 type_hint: "string".to_string(),
28 description: "Remote VNC server address".to_string(),
29 required: true,
30 },
31 Parameter {
32 name: "width".to_string(),
33 type_hint: "number".to_string(),
34 description: "Framebuffer width in pixels".to_string(),
35 required: true,
36 },
37 Parameter {
38 name: "height".to_string(),
39 type_hint: "number".to_string(),
40 description: "Framebuffer height in pixels".to_string(),
41 required: true,
42 },
43 Parameter {
44 name: "server_name".to_string(),
45 type_hint: "string".to_string(),
46 description: "VNC server name/description".to_string(),
47 required: true,
48 },
49 ])
50});
51
52pub static VNC_CLIENT_FRAMEBUFFER_UPDATE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
54 EventType::new(
55 "vnc_framebuffer_update",
56 "Framebuffer update received from VNC server",
57 json!({
58 "type": "send_pointer_event",
59 "x": 100,
60 "y": 200,
61 "button_mask": 1
62 }),
63 )
64 .with_parameters(vec![
65 Parameter {
66 name: "rectangles".to_string(),
67 type_hint: "number".to_string(),
68 description: "Number of rectangles in this update".to_string(),
69 required: true,
70 },
71 Parameter {
72 name: "update_summary".to_string(),
73 type_hint: "string".to_string(),
74 description: "Human-readable summary of the update".to_string(),
75 required: true,
76 },
77 ])
78});
79
80pub static VNC_CLIENT_SERVER_CUT_TEXT_EVENT: LazyLock<EventType> = LazyLock::new(|| {
82 EventType::new(
83 "vnc_server_cut_text",
84 "Server sent clipboard text",
85 json!({"type": "placeholder", "event_id": "vnc_server_cut_text"}),
86 )
87 .with_parameters(vec![Parameter {
88 name: "text".to_string(),
89 type_hint: "string".to_string(),
90 description: "Clipboard text from server".to_string(),
91 required: true,
92 }])
93});
94
95pub struct VncClientProtocol;
97
98impl VncClientProtocol {
99 pub fn new() -> Self {
100 Self
101 }
102}
103
104impl Protocol for VncClientProtocol {
106 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
107 vec![
108 ActionDefinition {
109 name: "request_framebuffer_update".to_string(),
110 description: "Request a framebuffer update from the VNC server".to_string(),
111 parameters: vec![
112 Parameter {
113 name: "incremental".to_string(),
114 type_hint: "boolean".to_string(),
115 description: "If true, only send changes since last update".to_string(),
116 required: false,
117 },
118 Parameter {
119 name: "x".to_string(),
120 type_hint: "number".to_string(),
121 description: "X coordinate of update region (default: 0)".to_string(),
122 required: false,
123 },
124 Parameter {
125 name: "y".to_string(),
126 type_hint: "number".to_string(),
127 description: "Y coordinate of update region (default: 0)".to_string(),
128 required: false,
129 },
130 Parameter {
131 name: "width".to_string(),
132 type_hint: "number".to_string(),
133 description: "Width of update region (default: full width)".to_string(),
134 required: false,
135 },
136 Parameter {
137 name: "height".to_string(),
138 type_hint: "number".to_string(),
139 description: "Height of update region (default: full height)".to_string(),
140 required: false,
141 },
142 ],
143 example: json!({
144 "type": "request_framebuffer_update",
145 "incremental": true
146 }),
147 log_template: None,
148 },
149 ActionDefinition {
150 name: "send_pointer_event".to_string(),
151 description: "Send a mouse pointer event (move, click, release)".to_string(),
152 parameters: vec![
153 Parameter {
154 name: "x".to_string(),
155 type_hint: "number".to_string(),
156 description: "X coordinate of pointer".to_string(),
157 required: true,
158 },
159 Parameter {
160 name: "y".to_string(),
161 type_hint: "number".to_string(),
162 description: "Y coordinate of pointer".to_string(),
163 required: true,
164 },
165 Parameter {
166 name: "button_mask".to_string(),
167 type_hint: "number".to_string(),
168 description: "Button mask (0=no buttons, 1=left, 2=middle, 4=right)"
169 .to_string(),
170 required: true,
171 },
172 ],
173 example: json!({
174 "type": "send_pointer_event",
175 "x": 100,
176 "y": 200,
177 "button_mask": 1
178 }),
179 log_template: None,
180 },
181 ActionDefinition {
182 name: "send_key_event".to_string(),
183 description: "Send a keyboard event (key press or release)".to_string(),
184 parameters: vec![
185 Parameter {
186 name: "key".to_string(),
187 type_hint: "string".to_string(),
188 description: "The key to send: a single character such as \"a\" or \"A\", a named key such as \"Enter\", \"Escape\", \"Tab\", \"Backspace\", an arrow (\"Left\"/\"Up\"/\"Right\"/\"Down\"), a modifier (\"Shift\", \"Control\", \"Alt\", \"Meta\") or a function key (\"F1\"..\"F35\"). An X11 keysym number is also accepted. An unrecognised value is an error rather than a silently-sent keysym 0"
189 .to_string(),
190 required: true,
191 },
192 Parameter {
193 name: "down".to_string(),
194 type_hint: "boolean".to_string(),
195 description: "True for key press, false for key release".to_string(),
196 required: true,
197 },
198 ],
199 example: json!({
200 "type": "send_key_event",
201 "key": "a",
202 "down": true
203 }),
204 log_template: None,
205 },
206 ActionDefinition {
207 name: "send_client_cut_text".to_string(),
208 description: "Send clipboard text to the server".to_string(),
209 parameters: vec![Parameter {
210 name: "text".to_string(),
211 type_hint: "string".to_string(),
212 description: "Text to send to clipboard".to_string(),
213 required: true,
214 }],
215 example: json!({
216 "type": "send_client_cut_text",
217 "text": "Hello, VNC!"
218 }),
219 log_template: None,
220 },
221 ActionDefinition {
222 name: "disconnect".to_string(),
223 description: "Disconnect from the VNC server".to_string(),
224 parameters: vec![],
225 example: json!({
226 "type": "disconnect"
227 }),
228 log_template: None,
229 },
230 ]
231 }
232 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
233 vec![
234 ActionDefinition {
235 name: "request_framebuffer_update".to_string(),
236 description: "Request a framebuffer update in response to server event".to_string(),
237 parameters: vec![Parameter {
238 name: "incremental".to_string(),
239 type_hint: "boolean".to_string(),
240 description: "If true, only send changes since last update".to_string(),
241 required: false,
242 }],
243 example: json!({
244 "type": "request_framebuffer_update",
245 "incremental": true
246 }),
247 log_template: None,
248 },
249 ActionDefinition {
250 name: "send_pointer_event".to_string(),
251 description: "Send pointer event in response to framebuffer update".to_string(),
252 parameters: vec![
253 Parameter {
254 name: "x".to_string(),
255 type_hint: "number".to_string(),
256 description: "X coordinate".to_string(),
257 required: true,
258 },
259 Parameter {
260 name: "y".to_string(),
261 type_hint: "number".to_string(),
262 description: "Y coordinate".to_string(),
263 required: true,
264 },
265 Parameter {
266 name: "button_mask".to_string(),
267 type_hint: "number".to_string(),
268 description: "Button mask".to_string(),
269 required: true,
270 },
271 ],
272 example: json!({
273 "type": "send_pointer_event",
274 "x": 100,
275 "y": 200,
276 "button_mask": 1
277 }),
278 log_template: None,
279 },
280 ActionDefinition {
286 name: "send_key_event".to_string(),
287 description: "Send a key press or release to the VNC server".to_string(),
288 parameters: vec![
289 Parameter {
290 name: "key".to_string(),
291 type_hint: "string".to_string(),
292 description: "The key: a single character such as \"a\", a named key such as \"Enter\"/\"Escape\"/\"Tab\", an arrow, a modifier, or \"F1\"..\"F35\". An X11 keysym number is also accepted"
293 .to_string(),
294 required: true,
295 },
296 Parameter {
297 name: "down".to_string(),
298 type_hint: "boolean".to_string(),
299 description: "True for key press, false for key release".to_string(),
300 required: true,
301 },
302 ],
303 example: json!({"type": "send_key_event", "key": "a", "down": true}),
304 log_template: None,
305 },
306 ActionDefinition {
307 name: "send_client_cut_text".to_string(),
308 description: "Set the VNC server's clipboard to this text".to_string(),
309 parameters: vec![Parameter {
310 name: "text".to_string(),
311 type_hint: "string".to_string(),
312 description: "Clipboard contents to send".to_string(),
313 required: true,
314 }],
315 example: json!({"type": "send_client_cut_text", "text": "hello"}),
316 log_template: None,
317 },
318 ActionDefinition {
319 name: "wait_for_more".to_string(),
320 description: "Wait for more updates before taking action".to_string(),
321 parameters: vec![],
322 example: json!({
323 "type": "wait_for_more"
324 }),
325 log_template: None,
326 },
327 ]
328 }
329 fn protocol_name(&self) -> &'static str {
330 "VNC"
331 }
332 fn get_event_types(&self) -> Vec<EventType> {
333 vec![
334 EventType::new(
335 "vnc_connected",
336 "Triggered when VNC client connects and authenticates",
337 json!({"type": "placeholder", "event_id": "vnc_connected"}),
338 ),
339 EventType::new(
340 "vnc_framebuffer_update",
341 "Triggered when framebuffer update is received",
342 json!({"type": "placeholder", "event_id": "vnc_framebuffer_update"}),
343 ),
344 EventType::new(
345 "vnc_server_cut_text",
346 "Triggered when server sends clipboard text",
347 json!({"type": "placeholder", "event_id": "vnc_server_cut_text"}),
348 ),
349 ]
350 }
351 fn stack_name(&self) -> &'static str {
352 "ETH>IP>TCP>RFB"
353 }
354 fn keywords(&self) -> Vec<&'static str> {
355 vec!["vnc", "vnc client", "remote desktop", "rfb"]
356 }
357 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
358 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
359
360 ProtocolMetadataV2::builder()
361 .state(DevelopmentState::Experimental)
362 .implementation("Custom RFB protocol implementation")
363 .llm_control("Control mouse, keyboard, and screen updates")
364 .e2e_testing("x11vnc or TigerVNC server")
365 .build()
366 }
367 fn description(&self) -> &'static str {
368 "VNC (Remote Framebuffer) client for remote desktop control"
369 }
370 fn example_prompt(&self) -> &'static str {
371 "Connect to VNC at localhost:5900 and click at position (100, 200)"
372 }
373 fn group_name(&self) -> &'static str {
374 "Specialized"
375 }
376 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
377 vec![ParameterDefinition {
378 name: "password".to_string(),
379 type_hint: "string".to_string(),
380 description: "VNC password (optional, for VNC authentication)".to_string(),
381 required: false,
382 example: json!("mypassword"),
383 default: None,
384 }]
385 }
386 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
387 use crate::llm::actions::StartupExamples;
388 use serde_json::json;
389
390 StartupExamples::new(
391 json!({
393 "type": "open_client",
394 "remote_addr": "localhost:5900",
395 "base_stack": "vnc",
396 "instruction": "Click at position (100, 200) and type 'Hello'"
397 }),
398 json!({
400 "type": "open_client",
401 "remote_addr": "localhost:5900",
402 "base_stack": "vnc",
403 "event_handlers": [{
404 "event_pattern": "vnc_framebuffer_update",
405 "handler": {
406 "type": "script",
407 "language": "python",
408 "code": "<vnc_client_handler>"
409 }
410 }]
411 }),
412 json!({
414 "type": "open_client",
415 "remote_addr": "localhost:5900",
416 "base_stack": "vnc",
417 "event_handlers": [
418 {
419 "event_pattern": "vnc_connected",
420 "handler": {
421 "type": "static",
422 "actions": [{
423 "type": "request_framebuffer_update",
424 "incremental": true
425 }]
426 }
427 },
428 {
429 "event_pattern": "vnc_framebuffer_update",
430 "handler": {
431 "type": "static",
432 "actions": [{
433 "type": "send_pointer_event",
434 "x": 100,
435 "y": 200,
436 "button_mask": 1
437 }]
438 }
439 }
440 ]
441 }),
442 )
443 }
444}
445
446impl Client for VncClientProtocol {
448 fn connect(
449 &self,
450 ctx: crate::protocol::ConnectContext,
451 ) -> std::pin::Pin<
452 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
453 > {
454 Box::pin(async move {
455 use crate::client::vnc::VncClient;
456 VncClient::connect_with_llm_actions(
457 ctx.remote_addr,
458 ctx.llm_client,
459 ctx.state,
460 ctx.status_tx,
461 ctx.client_id,
462 ctx.startup_params,
463 )
464 .await
465 })
466 }
467 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
468 let action_type = action
469 .get("type")
470 .and_then(|v| v.as_str())
471 .context("Missing 'type' field in action")?;
472
473 match action_type {
474 "request_framebuffer_update" => {
475 let incremental = action
476 .get("incremental")
477 .and_then(|v| v.as_bool())
478 .unwrap_or(true);
479 let x = super::rfb_u16(&action, "x", 0)?;
480 let y = super::rfb_u16(&action, "y", 0)?;
481 let width = super::rfb_u16(&action, "width", u16::MAX)?;
482 let height = super::rfb_u16(&action, "height", u16::MAX)?;
483
484 Ok(ClientActionResult::Custom {
485 name: "request_framebuffer_update".to_string(),
486 data: json!({
487 "incremental": incremental,
488 "x": x,
489 "y": y,
490 "width": width,
491 "height": height,
492 }),
493 })
494 }
495 "send_pointer_event" => {
496 for key in ["x", "y", "button_mask"] {
497 if action.get(key).is_none_or(serde_json::Value::is_null) {
498 anyhow::bail!("Missing '{key}' field");
499 }
500 }
501 let x = super::rfb_u16(&action, "x", 0)?;
502 let y = super::rfb_u16(&action, "y", 0)?;
503 let button_mask = super::rfb_u8(&action, "button_mask", 0)?;
504
505 Ok(ClientActionResult::Custom {
506 name: "send_pointer_event".to_string(),
507 data: json!({
508 "x": x,
509 "y": y,
510 "button_mask": button_mask,
511 }),
512 })
513 }
514 "send_key_event" => {
515 let key = crate::client::vnc::resolve_keysym(
519 action.get("key").unwrap_or(&serde_json::Value::Null),
520 )
521 .context(
522 "Missing or invalid 'key' field: use a single character like \"a\", a name like \"Enter\"/\"Escape\"/\"F1\", or an X11 keysym number",
523 )?;
524 let down = action
525 .get("down")
526 .and_then(|v| v.as_bool())
527 .context("Missing or invalid 'down' field")?;
528
529 Ok(ClientActionResult::Custom {
530 name: "send_key_event".to_string(),
531 data: json!({
532 "key": key,
533 "down": down,
534 }),
535 })
536 }
537 "send_client_cut_text" => {
538 let text = action
539 .get("text")
540 .and_then(|v| v.as_str())
541 .context("Missing or invalid 'text' field")?;
542
543 Ok(ClientActionResult::Custom {
544 name: "send_client_cut_text".to_string(),
545 data: json!({
546 "text": text,
547 }),
548 })
549 }
550 "disconnect" => Ok(ClientActionResult::Disconnect),
551 "wait_for_more" => Ok(ClientActionResult::WaitForMore),
552 _ => Err(anyhow::anyhow!(
553 "Unknown VNC client action: {}",
554 action_type
555 )),
556 }
557 }
558}