Skip to main content

netget/client/webtransport/
actions.rs

1use crate::llm::actions::{
2    client_trait::{Client, ClientActionResult},
3    protocol_trait::Protocol,
4    ActionDefinition, ParameterDefinition,
5};
6use crate::protocol::{ConnectContext, EventType};
7use crate::server::webtransport::actions::{
8    parameter, reply, session_actions, validate, DATAGRAM_EVENT, STREAM_EVENT, STREAM_REPLY_EVENT,
9};
10use anyhow::{bail, Result};
11use serde_json::{json, Value};
12use std::sync::LazyLock;
13
14#[derive(Default)]
15pub struct WebTransportClientProtocol;
16impl WebTransportClientProtocol {
17    pub fn new() -> Self {
18        Self
19    }
20}
21
22pub static CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
23    EventType::new(
24        "webtransport_connected",
25        "The server accepted the session; open streams, send datagrams or wait for the server",
26        json!({"type": "webtransport_open_bi", "data": "hello"}),
27    )
28    .with_parameters(vec![parameter(
29        "url",
30        "string",
31        "The session's URL, e.g. https://127.0.0.1:4433/chat",
32        true,
33    )])
34    .with_actions(session_actions())
35});
36
37impl Protocol for WebTransportClientProtocol {
38    fn protocol_name(&self) -> &'static str {
39        "WebTransport"
40    }
41    fn stack_name(&self) -> &'static str {
42        "ETH>IP>UDP>QUIC>HTTP3>WebTransport"
43    }
44    fn keywords(&self) -> Vec<&'static str> {
45        vec!["webtransport", "webtransport client", "web transport"]
46    }
47    fn description(&self) -> &'static str {
48        "WebTransport over HTTP/3 client: opens a session on a URL, then exchanges streams and datagrams with the server"
49    }
50    fn get_async_actions(&self, _: &crate::state::app_state::AppState) -> Vec<ActionDefinition> {
51        session_actions()
52    }
53    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
54        let mut all = vec![reply()];
55        all.extend(session_actions());
56        all
57    }
58    fn get_event_types(&self) -> Vec<EventType> {
59        vec![
60            CONNECTED_EVENT.clone(),
61            STREAM_EVENT.clone(),
62            DATAGRAM_EVENT.clone(),
63            STREAM_REPLY_EVENT.clone(),
64        ]
65    }
66    fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
67        let p =
68            |name: &str, kind: &str, description: &str, example: Value, default: Option<Value>| {
69                ParameterDefinition {
70                    name: name.into(),
71                    type_hint: kind.into(),
72                    description: description.into(),
73                    required: false,
74                    example,
75                    default,
76                }
77            };
78        vec![
79            p("path", "string", "The session path requested from the server", json!("/chat"), Some(json!(super::DEFAULT_PATH))),
80            p("certificate_sha256", "string", "Trust exactly the server certificate with this SHA-256 (hex, as browsers' serverCertificateHashes; ECDSA P-256, valid at most 14 days)", json!("3f1a…64 hex digits"), None),
81            p("ca_cert_path", "string", "PEM CA or certificate to trust instead of the system roots", json!("ca.pem"), None),
82            p("headers", "object", "Extra request header fields, e.g. {\"origin\": \"https://example.com\"}", json!({"origin": "https://example.com"}), None),
83            p("idle_timeout_secs", "number", "Close the session when idle this long, 1 to 3600 seconds", json!(30), Some(json!(crate::server::webtransport::IDLE_TIMEOUT.as_secs()))),
84        ]
85    }
86    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
87        use crate::protocol::metadata::*;
88        ProtocolMetadataV2::builder()
89            .state(DevelopmentState::Experimental)
90            .privilege_requirement(PrivilegeRequirement::None)
91            .implementation("wtransport 0.7.2 (vendored with settings, driver and QPACK bounds) over quinn: certificate-pinned, CA or system-root TLS, extended CONNECT, streams read whole, datagrams")
92            .llm_control("What the client sends on streams and datagrams, its answers to server-opened streams, and when it closes")
93            .e2e_testing("tests/client/webtransport: an aioquic 1.3.0 (independent, Python) WebTransport server echoes the client's streams and datagrams and opens streams to it")
94            .notes("A refused session reports only that it was refused (wtransport does not surface the status). Streams are delivered whole, up to 1 MiB within 30 s.")
95            .build()
96    }
97    fn example_prompt(&self) -> &'static str {
98        "Open a WebTransport session to https://127.0.0.1:4433/echo and send hello on a stream"
99    }
100    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
101        let llm = json!({"type":"open_client","protocol":"webtransport","remote_addr":"127.0.0.1:4433","instruction":"Send hello on a bidirectional stream and report the answer","startup_params":{"path":"/echo","certificate_sha256":"<server certificate sha-256>"}});
102        let mut static_example = llm.clone();
103        static_example["event_handlers"] = json!([{"event_pattern":"webtransport_connected","handler":{"type":"static","actions":[{"type":"webtransport_open_bi","data":"hello"}]}}]);
104        let mut scripted = llm.clone();
105        scripted["event_handlers"] = json!([{"event_pattern":"webtransport_datagram","handler":{"type":"script","language":"python","code":"import json,sys\ne=json.load(sys.stdin)['event']\nprint(json.dumps({'actions':[{'type':'webtransport_send_datagram','data':'ack '+e['data']}]}))"}}]);
106        crate::llm::actions::StartupExamples::new(llm, scripted, static_example)
107    }
108    fn group_name(&self) -> &'static str {
109        "Application"
110    }
111}
112
113impl Client for WebTransportClientProtocol {
114    fn connect(
115        &self,
116        ctx: ConnectContext,
117    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
118    {
119        Box::pin(super::connect(ctx))
120    }
121    fn execute_action(&self, v: Value) -> Result<ClientActionResult> {
122        if matches!(
123            v["type"].as_str(),
124            Some("webtransport_accept" | "webtransport_reject")
125        ) {
126            bail!("a client does not admit sessions");
127        }
128        validate(&v)?;
129        Ok(ClientActionResult::Custom {
130            name: v["type"].as_str().unwrap_or_default().into(),
131            data: v,
132        })
133    }
134}