Skip to main content

netget/protocol/
spawn_context.rs

1//! Server spawn context
2//!
3//! Provides all the necessary context for spawning a protocol server.
4
5use crate::llm::actions::ParameterDefinition;
6use crate::llm::OllamaClient;
7use crate::state::app_state::AppState;
8use crate::state::ServerId;
9use std::collections::HashSet;
10use std::fmt;
11use std::net::SocketAddr;
12use std::sync::Arc;
13use tokio::sync::mpsc;
14
15/// Error returned when startup parameters supplied by the LLM or an MCP client
16/// cannot be used by the protocol.
17///
18/// These values are untrusted model/client input, so every failure is reported
19/// rather than panicking the task that is starting the server.
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub enum StartupParamError {
22    /// A key was supplied (or accessed) that the protocol never declared in
23    /// `get_startup_parameters()`.
24    Undeclared {
25        /// Offending parameter name
26        key: String,
27        /// Parameter names the protocol does declare, sorted
28        allowed: Vec<String>,
29        /// True when the protocol code itself asked for the undeclared key
30        /// (a bug in the protocol) rather than the caller supplying it.
31        accessed_by_protocol: bool,
32    },
33    /// A required parameter was absent or held the wrong JSON type.
34    Invalid {
35        /// Offending parameter name
36        key: String,
37        /// Human readable description of what went wrong
38        detail: String,
39    },
40}
41
42impl fmt::Display for StartupParamError {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        match self {
45            StartupParamError::Undeclared {
46                key,
47                allowed,
48                accessed_by_protocol,
49            } => {
50                if *accessed_by_protocol {
51                    write!(
52                        f,
53                        "Attempted to access undeclared startup parameter '{}'. Protocol must declare this parameter in get_startup_parameters(). Allowed parameters: {:?}",
54                        key, allowed
55                    )
56                } else {
57                    write!(
58                        f,
59                        "Undeclared startup parameter '{}'. Protocol must declare this parameter in get_startup_parameters(). Allowed parameters: {:?}",
60                        key, allowed
61                    )
62                }
63            }
64            StartupParamError::Invalid { key, detail } => {
65                write!(f, "Invalid startup parameter '{}': {}", key, detail)
66            }
67        }
68    }
69}
70
71impl std::error::Error for StartupParamError {}
72
73/// Convenience alias for fallible startup-parameter access.
74pub type StartupParamResult<T> = std::result::Result<T, StartupParamError>;
75
76/// Type-safe wrapper for startup parameters
77///
78/// Validates that parameters can only be accessed if they were declared
79/// in the protocol's `get_startup_parameters()` implementation.
80///
81/// Every accessor returns a [`StartupParamResult`]: the JSON originates from the
82/// LLM (`open_server`) or an MCP client (`start_server`), so malformed values
83/// must surface as errors to the caller instead of aborting the task.
84#[derive(Clone)]
85pub struct StartupParams {
86    /// The actual JSON parameter values provided by the LLM
87    params: serde_json::Value,
88    /// Set of allowed parameter names (from ParameterDefinition)
89    allowed_params: HashSet<String>,
90}
91
92/// Maximum retained startup JSON content, including keys and node overhead.
93pub const MAX_STARTUP_PARAM_BYTES: usize = 8 * 1024 * 1024;
94/// Maximum nodes in a constructed startup parameter object.
95pub const MAX_STARTUP_PARAM_NODES: usize = 65_536;
96/// Maximum nesting depth in a constructed startup parameter object.
97pub const MAX_STARTUP_PARAM_DEPTH: usize = 64;
98
99impl fmt::Debug for StartupParams {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        f.debug_struct("StartupParams")
102            .field(
103                "params",
104                &crate::utils::redact::redact_sensitive(&self.params),
105            )
106            .field("allowed_params", &self.allowed_params)
107            .finish()
108    }
109}
110
111impl StartupParams {
112    /// Check borrowed JSON before cloning or formatting untrusted startup data.
113    pub fn preflight(params: &serde_json::Value) -> StartupParamResult<()> {
114        if crate::utils::json_budget::within_budget(
115            params,
116            MAX_STARTUP_PARAM_BYTES,
117            MAX_STARTUP_PARAM_NODES,
118            MAX_STARTUP_PARAM_DEPTH,
119        ) {
120            Ok(())
121        } else {
122            Err(StartupParamError::Invalid {
123                key: "startup_params".into(),
124                detail: "startup_params exceeds the JSON size, node or depth budget".into(),
125            })
126        }
127    }
128
129    /// Refuse an excessive owned value without recursively dropping its children.
130    /// Call this before an earlier error path can dispose of a constructed value.
131    pub fn preflight_owned(params: &mut Option<serde_json::Value>) -> StartupParamResult<()> {
132        if let Some(value) = params.as_ref() {
133            if let Err(error) = Self::preflight(value) {
134                crate::utils::json_budget::drop_iteratively(params.take().expect("present above"));
135                return Err(error);
136            }
137        }
138        Ok(())
139    }
140
141    /// Create new StartupParams with validation
142    ///
143    /// # Arguments
144    /// * `params` - JSON object containing parameter values
145    /// * `schema` - Parameter definitions from protocol's `get_startup_parameters()`
146    ///
147    /// # Errors
148    /// Returns [`StartupParamError::Undeclared`] if any key in `params` is not
149    /// defined in `schema`, or [`StartupParamError::Invalid`] if `params` is not
150    /// an object. An absent parameter set should be represented by `None` at the caller.
151    pub fn new(
152        params: serde_json::Value,
153        schema: Vec<ParameterDefinition>,
154    ) -> StartupParamResult<Self> {
155        if let Err(error) = Self::preflight(&params) {
156            crate::utils::json_budget::drop_iteratively(params);
157            return Err(error);
158        }
159        let allowed_params: HashSet<String> = schema.iter().map(|p| p.name.clone()).collect();
160
161        let obj = params
162            .as_object()
163            .ok_or_else(|| StartupParamError::Invalid {
164                key: "startup_params".to_string(),
165                detail: "startup_params must be a JSON object".to_string(),
166            })?;
167        // Validate that all provided parameters are in the schema.
168        for key in obj.keys() {
169            if !allowed_params.contains(key) {
170                return Err(StartupParamError::Undeclared {
171                    key: key.clone(),
172                    allowed: sorted(&allowed_params),
173                    accessed_by_protocol: false,
174                });
175            }
176        }
177
178        Ok(Self {
179            params,
180            allowed_params,
181        })
182    }
183
184    /// Validate declared JSON shapes and required fields before any resource is
185    /// stopped or registered. Protocol-specific I/O checks still occur at spawn.
186    pub fn new_validated(
187        params: serde_json::Value,
188        schema: Vec<ParameterDefinition>,
189    ) -> StartupParamResult<Self> {
190        let result = Self::new(params, schema.clone())?;
191        for parameter in &schema {
192            let value = result.params.get(&parameter.name);
193            if value.is_none_or(serde_json::Value::is_null) {
194                if parameter.required {
195                    return Err(result.invalid(
196                        &parameter.name,
197                        "required parameter is missing or null".into(),
198                    ));
199                }
200                continue;
201            }
202            let value = value.expect("present above");
203            let hint = parameter.type_hint.replace(" or ", "|");
204            if !hint
205                .split('|')
206                .any(|kind| parameter_type_matches(value, kind.trim()))
207            {
208                return Err(result.invalid(
209                    &parameter.name,
210                    format!(
211                        "expected {}, got {}",
212                        parameter.type_hint,
213                        diagnostic_value(&parameter.name, value)
214                    ),
215                ));
216            }
217        }
218        Ok(result)
219    }
220
221    /// Names of the parameters this protocol declares, sorted.
222    pub fn allowed_parameters(&self) -> Vec<String> {
223        sorted(&self.allowed_params)
224    }
225
226    /// Get a required string parameter
227    ///
228    /// # Errors
229    /// - If the parameter was not declared in `get_startup_parameters()`
230    /// - If the parameter is missing
231    /// - If the parameter is not a string
232    pub fn get_string(&self, key: &str) -> StartupParamResult<String> {
233        self.validate_key(key)?;
234        match self.params.get(key).and_then(|v| v.as_str()) {
235            Some(s) => Ok(s.to_string()),
236            None => Err(self.invalid(
237                key,
238                format!(
239                    "Required string parameter '{}' is missing or not a string. Params: {}",
240                    key,
241                    crate::utils::redact::redact_sensitive(&self.params)
242                ),
243            )),
244        }
245    }
246
247    /// Get an optional string parameter
248    ///
249    /// # Errors
250    /// - If the parameter was not declared in `get_startup_parameters()`
251    /// - If the parameter exists but is not a string
252    pub fn get_optional_string(&self, key: &str) -> StartupParamResult<Option<String>> {
253        self.validate_key(key)?;
254        match self.params.get(key) {
255            None | Some(serde_json::Value::Null) => Ok(None),
256            Some(v) => match v.as_str() {
257                Some(s) => Ok(Some(s.to_string())),
258                None => Err(self.invalid(
259                    key,
260                    format!(
261                        "Optional string parameter '{}' exists but is not a string. Value: {}",
262                        key,
263                        diagnostic_value(key, v)
264                    ),
265                )),
266            },
267        }
268    }
269
270    /// Get a required boolean parameter
271    ///
272    /// # Errors
273    /// - If the parameter was not declared in `get_startup_parameters()`
274    /// - If the parameter is missing
275    /// - If the parameter is not a boolean
276    pub fn get_bool(&self, key: &str) -> StartupParamResult<bool> {
277        self.validate_key(key)?;
278        match self.params.get(key).and_then(|v| v.as_bool()) {
279            Some(b) => Ok(b),
280            None => Err(self.invalid(
281                key,
282                format!(
283                    "Required boolean parameter '{}' is missing or not a boolean. Params: {}",
284                    key,
285                    crate::utils::redact::redact_sensitive(&self.params)
286                ),
287            )),
288        }
289    }
290
291    /// Get an optional boolean parameter
292    ///
293    /// # Errors
294    /// - If the parameter was not declared in `get_startup_parameters()`
295    /// - If the parameter exists but is not a boolean
296    pub fn get_optional_bool(&self, key: &str) -> StartupParamResult<Option<bool>> {
297        self.validate_key(key)?;
298        match self.params.get(key) {
299            None | Some(serde_json::Value::Null) => Ok(None),
300            Some(v) => match v.as_bool() {
301                Some(b) => Ok(Some(b)),
302                None => Err(self.invalid(
303                    key,
304                    format!(
305                        "Optional boolean parameter '{}' exists but is not a boolean. Value: {}",
306                        key,
307                        diagnostic_value(key, v)
308                    ),
309                )),
310            },
311        }
312    }
313
314    /// Get a required integer parameter
315    ///
316    /// # Errors
317    /// - If the parameter was not declared in `get_startup_parameters()`
318    /// - If the parameter is missing
319    /// - If the parameter is not an integer
320    pub fn get_i64(&self, key: &str) -> StartupParamResult<i64> {
321        self.validate_key(key)?;
322        match self.params.get(key).and_then(|v| v.as_i64()) {
323            Some(n) => Ok(n),
324            None => Err(self.invalid(
325                key,
326                format!(
327                    "Required integer parameter '{}' is missing or not an integer. Params: {}",
328                    key,
329                    crate::utils::redact::redact_sensitive(&self.params)
330                ),
331            )),
332        }
333    }
334
335    /// Get an optional integer parameter
336    ///
337    /// # Errors
338    /// - If the parameter was not declared in `get_startup_parameters()`
339    /// - If the parameter exists but is not an integer
340    pub fn get_optional_i64(&self, key: &str) -> StartupParamResult<Option<i64>> {
341        self.validate_key(key)?;
342        match self.params.get(key) {
343            None | Some(serde_json::Value::Null) => Ok(None),
344            Some(v) => match v.as_i64() {
345                Some(n) => Ok(Some(n)),
346                None => Err(self.invalid(
347                    key,
348                    format!(
349                        "Optional integer parameter '{}' exists but is not an integer. Value: {}",
350                        key,
351                        diagnostic_value(key, v)
352                    ),
353                )),
354            },
355        }
356    }
357
358    /// Get an optional number parameter (integer or fraction)
359    ///
360    /// # Errors
361    /// - If the parameter was not declared in `get_startup_parameters()`
362    /// - If the parameter exists but is not a number
363    pub fn get_optional_f64(&self, key: &str) -> StartupParamResult<Option<f64>> {
364        self.validate_key(key)?;
365        match self.params.get(key) {
366            None | Some(serde_json::Value::Null) => Ok(None),
367            Some(v) => match v.as_f64() {
368                Some(n) => Ok(Some(n)),
369                None => Err(self.invalid(
370                    key,
371                    format!(
372                        "Optional number parameter '{}' exists but is not a number. Value: {}",
373                        key,
374                        diagnostic_value(key, v)
375                    ),
376                )),
377            },
378        }
379    }
380
381    /// Get a required unsigned integer parameter
382    ///
383    /// # Errors
384    /// - If the parameter was not declared in `get_startup_parameters()`
385    /// - If the parameter is missing
386    /// - If the parameter is not an unsigned integer
387    pub fn get_u64(&self, key: &str) -> StartupParamResult<u64> {
388        self.validate_key(key)?;
389        match self.params.get(key).and_then(|v| v.as_u64()) {
390            Some(n) => Ok(n),
391            None => Err(self.invalid(
392                key,
393                format!(
394                    "Required unsigned integer parameter '{}' is missing or not an unsigned integer. Params: {}",
395                    key, crate::utils::redact::redact_sensitive(&self.params)
396                ),
397            )),
398        }
399    }
400
401    /// Get an optional unsigned integer parameter
402    ///
403    /// # Errors
404    /// - If the parameter was not declared in `get_startup_parameters()`
405    /// - If the parameter exists but is not an unsigned integer
406    pub fn get_optional_u64(&self, key: &str) -> StartupParamResult<Option<u64>> {
407        self.validate_key(key)?;
408        match self.params.get(key) {
409            None | Some(serde_json::Value::Null) => Ok(None),
410            Some(v) => match v.as_u64() {
411                Some(n) => Ok(Some(n)),
412                None => Err(self.invalid(
413                    key,
414                    format!(
415                        "Optional unsigned integer parameter '{}' exists but is not an unsigned integer. Value: {}",
416                        key, diagnostic_value(key, v)
417                    ),
418                )),
419            },
420        }
421    }
422
423    /// Get an optional u32 parameter
424    ///
425    /// # Errors
426    /// - If the parameter was not declared in `get_startup_parameters()`
427    /// - If the parameter exists but is not an unsigned integer or exceeds u32::MAX
428    pub fn get_optional_u32(&self, key: &str) -> StartupParamResult<Option<u32>> {
429        self.validate_key(key)?;
430        match self.params.get(key) {
431            None | Some(serde_json::Value::Null) => Ok(None),
432            Some(v) => {
433                let val = match v.as_u64() {
434                    Some(n) => n,
435                    None => {
436                        return Err(self.invalid(
437                            key,
438                            format!(
439                                "Optional u32 parameter '{}' exists but is not an unsigned integer. Value: {}",
440                                key, diagnostic_value(key, v)
441                            ),
442                        ))
443                    }
444                };
445                if val > u32::MAX as u64 {
446                    return Err(self.invalid(
447                        key,
448                        format!(
449                            "Optional u32 parameter '{}' exceeds u32::MAX ({}). Value: {}",
450                            key,
451                            u32::MAX,
452                            diagnostic_value(key, v)
453                        ),
454                    ));
455                }
456                Ok(Some(val as u32))
457            }
458        }
459    }
460
461    /// Get a required object/map parameter
462    ///
463    /// # Errors
464    /// - If the parameter was not declared in `get_startup_parameters()`
465    /// - If the parameter is missing
466    /// - If the parameter is not an object
467    pub fn get_object(
468        &self,
469        key: &str,
470    ) -> StartupParamResult<&serde_json::Map<String, serde_json::Value>> {
471        self.validate_key(key)?;
472        match self.params.get(key).and_then(|v| v.as_object()) {
473            Some(o) => Ok(o),
474            None => Err(self.invalid(
475                key,
476                format!(
477                    "Required object parameter '{}' is missing or not an object. Params: {}",
478                    key,
479                    crate::utils::redact::redact_sensitive(&self.params)
480                ),
481            )),
482        }
483    }
484
485    /// Get an optional object/map parameter
486    ///
487    /// # Errors
488    /// - If the parameter was not declared in `get_startup_parameters()`
489    /// - If the parameter exists but is not an object
490    pub fn get_optional_object(
491        &self,
492        key: &str,
493    ) -> StartupParamResult<Option<&serde_json::Map<String, serde_json::Value>>> {
494        self.validate_key(key)?;
495        match self.params.get(key) {
496            None | Some(serde_json::Value::Null) => Ok(None),
497            Some(v) => match v.as_object() {
498                Some(o) => Ok(Some(o)),
499                None => Err(self.invalid(
500                    key,
501                    format!(
502                        "Optional object parameter '{}' exists but is not an object. Value: {}",
503                        key,
504                        diagnostic_value(key, v)
505                    ),
506                )),
507            },
508        }
509    }
510
511    /// Get a required array parameter
512    ///
513    /// # Errors
514    /// - If the parameter was not declared in `get_startup_parameters()`
515    /// - If the parameter is missing
516    /// - If the parameter is not an array
517    pub fn get_array(&self, key: &str) -> StartupParamResult<&Vec<serde_json::Value>> {
518        self.validate_key(key)?;
519        match self.params.get(key).and_then(|v| v.as_array()) {
520            Some(a) => Ok(a),
521            None => Err(self.invalid(
522                key,
523                format!(
524                    "Required array parameter '{}' is missing or not an array. Params: {}",
525                    key,
526                    crate::utils::redact::redact_sensitive(&self.params)
527                ),
528            )),
529        }
530    }
531
532    /// Get an optional array parameter
533    ///
534    /// # Errors
535    /// - If the parameter was not declared in `get_startup_parameters()`
536    /// - If the parameter exists but is not an array
537    pub fn get_optional_array(
538        &self,
539        key: &str,
540    ) -> StartupParamResult<Option<&Vec<serde_json::Value>>> {
541        self.validate_key(key)?;
542        match self.params.get(key) {
543            None | Some(serde_json::Value::Null) => Ok(None),
544            Some(v) => match v.as_array() {
545                Some(a) => Ok(Some(a)),
546                None => Err(self.invalid(
547                    key,
548                    format!(
549                        "Optional array parameter '{}' exists but is not an array. Value: {}",
550                        key,
551                        diagnostic_value(key, v)
552                    ),
553                )),
554            },
555        }
556    }
557
558    /// Build an `Invalid` error for `key`.
559    fn invalid(&self, key: &str, detail: String) -> StartupParamError {
560        StartupParamError::Invalid {
561            key: key.to_string(),
562            detail,
563        }
564    }
565
566    /// Validate that a key was declared in get_startup_parameters()
567    ///
568    /// # Errors
569    /// If the key is not in the allowed parameters set
570    fn validate_key(&self, key: &str) -> StartupParamResult<()> {
571        if !self.allowed_params.contains(key) {
572            return Err(StartupParamError::Undeclared {
573                key: key.to_string(),
574                allowed: sorted(&self.allowed_params),
575                accessed_by_protocol: true,
576            });
577        }
578        Ok(())
579    }
580}
581
582/// A parameter's value as an error message may show it: masked by the same rule the
583/// redactor applies to a key/value pair (`is_shown_redacted`, which also hides a string
584/// `auth`), and otherwise with any credential nested inside it masked.
585fn diagnostic_value(key: &str, value: &serde_json::Value) -> serde_json::Value {
586    if crate::utils::redact::is_shown_redacted(key, value) {
587        serde_json::Value::String(crate::utils::redact::REDACTED.into())
588    } else {
589        crate::utils::redact::redact_sensitive(value)
590    }
591}
592
593/// Sorted copy of a name set, so error messages are deterministic.
594fn sorted(set: &HashSet<String>) -> Vec<String> {
595    let mut v: Vec<String> = set.iter().cloned().collect();
596    v.sort();
597    v
598}
599
600/// Context passed to protocol servers during spawning
601///
602/// Contains all the dependencies and configuration needed to start a server.
603#[derive(Clone)]
604pub struct SpawnContext {
605    /// Address to listen on (DEPRECATED - use host/port fields instead)
606    ///
607    /// This field is maintained for backwards compatibility with unmigrated protocols.
608    /// New protocols should use the flexible binding system (mac_address, interface, host, port).
609    #[deprecated(since = "1.0.0", note = "Use host/port fields instead")]
610    pub listen_addr: SocketAddr,
611
612    // === NEW FLEXIBLE BINDING FIELDS ===
613    /// MAC address for Layer 2 protocols (e.g., ARP spoofing with specific MAC)
614    ///
615    /// Protocol defaults are already applied. Use this value directly.
616    pub mac_address: Option<String>,
617
618    /// Network interface for raw protocols (e.g., "lo", "eth0", "en0")
619    ///
620    /// Protocol defaults are already applied. Use this value directly.
621    pub interface: Option<String>,
622
623    /// Host address (IPv4, IPv6, or hostname) for socket-based protocols
624    ///
625    /// Protocol defaults are already applied. Use this value directly.
626    /// Examples: "127.0.0.1", "0.0.0.0", "::", "localhost"
627    pub host: Option<String>,
628
629    /// Port number for socket-based protocols
630    ///
631    /// Protocol defaults are already applied. Use this value directly.
632    /// Some(0) means automatic port assignment.
633    pub port: Option<u16>,
634
635    /// LLM client for generating responses
636    pub llm_client: OllamaClient,
637
638    /// Application state
639    pub state: Arc<AppState>,
640
641    /// Channel for sending status updates to UI
642    pub status_tx: mpsc::UnboundedSender<String>,
643
644    /// Unique identifier for this server instance
645    pub server_id: ServerId,
646
647    /// Optional type-safe startup parameters specific to the protocol
648    ///
649    /// Parameters can only be accessed if they were declared in the protocol's
650    /// `get_startup_parameters()` implementation. Accessing an undeclared
651    /// parameter, or one holding the wrong JSON type, returns a
652    /// [`StartupParamError`] rather than panicking.
653    ///
654    /// For example:
655    /// - HTTP Proxy: certificate_mode, request_filter_mode, response_filter_mode
656    /// - gRPC: proto_schema, enable_reflection
657    /// - DataLink: interface, filter
658    pub startup_params: Option<StartupParams>,
659}
660
661impl SpawnContext {
662    /// Helper method to get socket address from host and port
663    ///
664    /// Port-based protocols can use this to construct a SocketAddr from
665    /// the flexible binding fields.
666    ///
667    /// # Returns
668    /// * `Some(SocketAddr)` - If both host and port are available
669    /// * `None` - If host or port is missing
670    ///
671    /// # Example
672    /// ```ignore
673    /// let addr = ctx.socket_addr().context("TCP requires host and port")?;
674    /// ```
675    pub fn socket_addr(&self) -> Option<SocketAddr> {
676        match (&self.host, self.port) {
677            (Some(host), Some(port)) => host
678                .parse::<std::net::IpAddr>()
679                .ok()
680                .map(|ip| SocketAddr::new(ip, port)),
681            _ => None,
682        }
683    }
684
685    /// Helper method to get interface name
686    ///
687    /// Interface-based protocols can use this to get the interface name
688    /// with proper error context.
689    ///
690    /// # Returns
691    /// * `Some(&str)` - If interface is available
692    /// * `None` - If interface is not set
693    ///
694    /// # Example
695    /// ```ignore
696    /// let interface = ctx.interface()
697    ///     .context("ICMP requires network interface")?;
698    /// ```
699    pub fn interface(&self) -> Option<&str> {
700        self.interface.as_deref()
701    }
702
703    /// Helper method to get MAC address
704    ///
705    /// Layer 2 protocols can use this to get the MAC address
706    /// with proper error context.
707    ///
708    /// # Returns
709    /// * `Some(&str)` - If MAC address is available
710    /// * `None` - If MAC address is not set
711    ///
712    /// # Example
713    /// ```ignore
714    /// let mac = ctx.mac_address()
715    ///     .context("ARP spoofing requires MAC address")?;
716    /// ```
717    pub fn mac_address(&self) -> Option<&str> {
718        self.mac_address.as_deref()
719    }
720
721    /// Get the legacy listen address (for unmigrated protocols)
722    ///
723    /// This method provides access to the deprecated `listen_addr` field without
724    /// triggering deprecation warnings. Use this during migration or for protocols
725    /// that haven't been migrated to the flexible binding system yet.
726    ///
727    /// Once a protocol is migrated to use `socket_addr()`, `interface()`, or
728    /// `mac_address()`, it should no longer use this method.
729    ///
730    /// # Example
731    /// ```ignore
732    /// // Unmigrated protocol:
733    /// let addr = ctx.legacy_listen_addr();
734    /// TcpListener::bind(addr).await?;
735    ///
736    /// // After migration:
737    /// let addr = ctx.socket_addr().context("requires host and port")?;
738    /// TcpListener::bind(addr).await?;
739    /// ```
740    #[allow(deprecated)]
741    pub fn legacy_listen_addr(&self) -> SocketAddr {
742        self.listen_addr
743    }
744}
745
746/// Type hints intentionally admit unions and typed arrays. Unknown descriptive
747/// hints retain protocol validation rather than inventing an interpretation.
748fn parameter_type_matches(value: &serde_json::Value, kind: &str) -> bool {
749    match kind {
750        "string" => value.is_string(),
751        "number" => value.is_number(),
752        "integer" => value.is_i64() || value.is_u64(),
753        "boolean" => value.is_boolean(),
754        "object" => value.is_object(),
755        "array" => value.is_array(),
756        "null" => value.is_null(),
757        "array of strings" => value
758            .as_array()
759            .is_some_and(|items| items.iter().all(|item| item.is_string())),
760        "array of objects" => value
761            .as_array()
762            .is_some_and(|items| items.iter().all(|item| item.is_object())),
763        "array of integers" => value
764            .as_array()
765            .is_some_and(|items| items.iter().all(|item| item.is_i64() || item.is_u64())),
766        "array of booleans" => value
767            .as_array()
768            .is_some_and(|items| items.iter().all(|item| item.is_boolean())),
769        _ => true,
770    }
771}