Skip to main content

netget/llm/actions/
protocol_trait.rs

1//! Protocol trait - common interface for all network protocols
2//!
3//! This module defines the core Protocol trait that both Server and Client traits extend.
4//! It contains common functionality shared across all protocol implementations.
5
6use super::{ActionDefinition, ParameterDefinition, StartupExamples};
7use crate::protocol::dependencies::ProtocolDependency;
8use crate::state::app_state::AppState;
9
10/// Common trait for all protocol implementations (both servers and clients)
11///
12/// This trait defines the common interface that all protocol implementations
13/// must provide, regardless of whether they're servers or clients.
14pub trait Protocol: Send + Sync {
15    /// Get default binding parameters for this protocol
16    ///
17    /// Protocols can return `Some(BindingDefaults)` to opt into the flexible
18    /// binding system. Protocols that return `None` use the legacy listen_addr system.
19    ///
20    /// Examples:
21    /// - TCP: `Some(BindingDefaults::port_based("127.0.0.1", 0))`
22    /// - ICMP: `Some(BindingDefaults::interface_based("lo"))`
23    /// - Unmigrated protocols: `None`
24    ///
25    /// Default implementation returns `None` (unmigrated, use legacy system).
26    fn default_binding(&self) -> Option<crate::protocol::BindingDefaults> {
27        None
28    }
29
30    /// Get startup parameters that can be provided when starting this protocol
31    ///
32    /// These parameters configure the protocol before it starts. Examples:
33    /// - HTTP: request_headers, user_agent, follow_redirects (client)
34    /// - HTTP: certificate_mode, request_filter_mode (server)
35    /// - SSH: username, password, private_key_path (client)
36    /// - SSH: host_key_path, banner_message (server)
37    ///
38    /// Default implementation returns empty vector (no startup parameters).
39    fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
40        Vec::new()
41    }
42
43    /// Get async actions that can be executed anytime from user input
44    ///
45    /// These actions don't require network context. Examples:
46    /// - HTTP client: send_request(method, path, headers, body)
47    /// - HTTP server: close_connection(id), send_to_connection(id, data)
48    /// - Redis client: execute_command(cmd, args)
49    /// - Generic: disconnect(), reconnect()
50    fn get_async_actions(&self, state: &AppState) -> Vec<ActionDefinition>;
51
52    /// Get sync actions available during network events
53    ///
54    /// These actions only make sense in response to network events. Examples:
55    /// - TCP: send_tcp_data(output), wait_for_more()
56    /// - HTTP: send_http_response(status, headers, body)
57    /// - SSH: handle_auth_challenge(), send_command_output()
58    fn get_sync_actions(&self) -> Vec<ActionDefinition>;
59
60    /// Get protocol name for debugging and identification
61    ///
62    /// This should be a short, uppercase identifier. Examples:
63    /// - "TCP", "HTTP", "SSH", "DNS", "WireGuard"
64    fn protocol_name(&self) -> &'static str;
65
66    /// Get the event types that this protocol can emit
67    ///
68    /// Each event type includes:
69    /// - A unique ID (e.g., "http_request", "ssh_auth")
70    /// - A description of when it occurs
71    /// - The actions that can be used to respond to this event
72    ///
73    /// # Returns
74    /// A vector of EventType definitions for this protocol
75    ///
76    /// Default implementation returns empty vector (protocol hasn't migrated to event system yet)
77    fn get_event_types(&self) -> Vec<crate::protocol::EventType> {
78        Vec::new()
79    }
80
81    /// Get the stack name (e.g., "ETH>IP>TCP>HTTP")
82    ///
83    /// This represents the network stack layers used by this protocol.
84    /// Used for display in UI and logging.
85    fn stack_name(&self) -> &'static str;
86
87    /// Get parsing keywords for protocol detection
88    ///
89    /// Returns a list of keywords that can be used to identify this protocol
90    /// from user input. Examples:
91    /// - HTTP: ["http", "http server", "via http", "hyper"]
92    /// - SSH: ["ssh"]
93    /// - mDNS: ["mdns", "bonjour", "dns-sd", "zeroconf"]
94    ///
95    /// Keywords are matched case-insensitively as substrings.
96    fn keywords(&self) -> Vec<&'static str>;
97
98    /// Get protocol metadata with implementation details
99    ///
100    /// Returns detailed metadata including:
101    /// - Protocol state (Incomplete, Experimental, Beta, Stable)
102    /// - Implementation approach description
103    /// - LLM control scope description
104    /// - E2E testing approach description
105    /// - Privilege requirements
106    /// - Optional notes
107    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2;
108
109    /// Get a short description of this protocol
110    ///
111    /// This should be a concise, one-line description of what this protocol does.
112    /// Examples:
113    /// - HTTP: "Web server serving HTTP traffic"
114    /// - SSH: "Secure shell server for remote access"
115    /// - DNS: "Domain name resolution server"
116    fn description(&self) -> &'static str;
117
118    /// Get an example prompt that would trigger this protocol
119    ///
120    /// This should be a realistic, engaging example that demonstrates
121    /// how a user would ask the LLM to start this protocol.
122    /// Examples:
123    /// - HTTP server: "Pretend to be a sassy HTTP server on port 8080 serving cooking recipes"
124    /// - HTTP client: "Connect to http://example.com:8080 and fetch /api/status every 10 seconds"
125    /// - SSH server: "Pretend to be a shell via SSH on port 2222"
126    fn example_prompt(&self) -> &'static str;
127
128    /// Get startup examples for this protocol with different handler modes
129    ///
130    /// Returns examples showing how to start this protocol with:
131    /// - LLM mode: LLM handles all responses intelligently
132    /// - Script mode: Code-based deterministic responses
133    /// - Static mode: Fixed, unchanging responses
134    ///
135    /// These examples are used in:
136    /// - Protocol documentation (shown when user requests docs)
137    /// - Prompt templates to guide LLM in creating servers
138    ///
139    /// All examples must be valid `open_server` or `open_client` actions
140    /// that can be directly executed. They are validated by parameterized tests.
141    ///
142    /// This method is REQUIRED for all protocols. Every protocol must provide
143    /// valid startup examples for all three modes (llm, script, static).
144    fn get_startup_examples(&self) -> StartupExamples;
145
146    /// Get the group name for categorizing this protocol
147    ///
148    /// Protocols are grouped in documentation by category. Valid groups:
149    /// - "Core" - Stable, well-tested protocols (TCP, HTTP, UDP, DNS, etc.)
150    /// - "Application" - IRC, Telnet, SMTP, IMAP, MQTT, etc.
151    /// - "Database" - MySQL, PostgreSQL, Redis, Kafka, etcd, etc.
152    /// - "Web & File" - WebDAV, NFS, SMB, IPP, Git, S3
153    /// - "Proxy & Network" - HTTP Proxy, SOCKS5, STUN, TURN
154    /// - "VPN & Routing" - WireGuard, OpenVPN, IPSec, BGP
155    /// - "AI & API" - OpenAI, gRPC, JSON-RPC, MCP, etc.
156    /// - "Network Services" - VNC, Tor Directory, Tor Relay
157    ///
158    /// This method is mandatory and must be implemented by all protocols.
159    fn group_name(&self) -> &'static str;
160
161    /// Get runtime dependencies required for this protocol to function
162    ///
163    /// Returns a list of dependencies that must be available for this protocol
164    /// to work at runtime. Examples:
165    /// - ARP/DataLink: vec![ProtocolDependency::SystemLibrary("pcap"), ProtocolDependency::RawSocketAccess]
166    /// - WireGuard: vec![ProtocolDependency::TunDeviceAccess, ProtocolDependency::RootAccess]
167    /// - gRPC: vec![ProtocolDependency::ToolInPath("protoc")] (for .proto file support)
168    /// - SSH on port 22: vec![ProtocolDependency::PrivilegedPort(22)]
169    ///
170    /// The default **derives dependencies from `metadata().privilege_requirement`** rather
171    /// than returning an empty vector, so every protocol reports correct data without
172    /// declaring anything twice.
173    ///
174    /// This mechanism was fully plumbed — `get_excluded_protocols()` is called from the event
175    /// handler and the TUI footer — but not one protocol overrode this method, so the exclusion
176    /// map was always empty and the whole feature did nothing. Deriving is better than asking
177    /// 116 protocols to restate what they already declare: two sources of the same fact drift,
178    /// and `privilege_requirement` is the one that already gates startup, so it is the one kept
179    /// honest.
180    ///
181    /// Note the two consumers differ in force. `privilege_requirement` is a hard gate in
182    /// `server_startup.rs` — fail it and the server refuses to start. Dependencies are mostly
183    /// **informational**: they tell a user, and the model, why a protocol will not work here,
184    /// with an installation hint. The one exception is narrow on purpose: startup refuses a
185    /// system library or tool the probe *established* is absent
186    /// (`protocol::dependencies::startup_blocker`, fed by [`Self::startup_dependencies`]); a
187    /// probe that could not answer never refuses.
188    ///
189    /// `DeviceAccess` maps to nothing deliberately. There is no `ProtocolDependency` variant for
190    /// a Bluetooth adapter, USB device or NFC reader, and no probe that would answer honestly;
191    /// claiming an unmet dependency we cannot check would exclude protocols that work.
192    ///
193    /// Override this to add what privilege cannot express — a system library, or a tool in
194    /// PATH. Call `default_dependencies_from_privilege(self)` and extend it rather than
195    /// replacing it, so the privilege-derived entries are not silently dropped.
196    fn get_dependencies(&self) -> Vec<ProtocolDependency> {
197        default_dependencies_from_privilege(self)
198    }
199
200    /// The runtime dependencies a start with these `startup_params` actually needs.
201    ///
202    /// This is what the startup gate checks (`protocol::dependencies::startup_blocker`), so a
203    /// protocol whose dependency applies to only some configurations can say so and not be
204    /// refused a start that would work — gRPC needs `protoc` to compile `.proto` text, and not
205    /// at all for a pre-compiled descriptor set. Defaults to [`Self::get_dependencies`].
206    fn startup_dependencies(
207        &self,
208        _startup_params: Option<&serde_json::Value>,
209    ) -> Vec<ProtocolDependency> {
210        self.get_dependencies()
211    }
212
213    /// The privilege this particular start needs, given its startup parameters.
214    ///
215    /// This is what the startup privilege gate checks. It defaults to the declared
216    /// `metadata().privilege_requirement`, which describes the transport an operator actually
217    /// uses. A protocol with an unprivileged test transport — the same frames over a UDP socket
218    /// instead of raw Ethernet or IP — overrides it with [`unless_udp_transport`], so a start
219    /// that needs no privilege is not refused. Declaring `None` instead would misdescribe the
220    /// real transport.
221    fn startup_privilege_requirement(
222        &self,
223        _startup_params: Option<&serde_json::Value>,
224    ) -> crate::protocol::metadata::PrivilegeRequirement {
225        self.metadata().privilege_requirement
226    }
227}
228
229/// `declared`, unless the startup parameters select the unprivileged UDP test transport
230/// (`transport: "udp"`, or CDP's alias `"test"`), which needs nothing.
231pub fn unless_udp_transport(
232    declared: crate::protocol::metadata::PrivilegeRequirement,
233    startup_params: Option<&serde_json::Value>,
234) -> crate::protocol::metadata::PrivilegeRequirement {
235    match startup_params
236        .and_then(|p| p.get("transport"))
237        .and_then(|t| t.as_str())
238    {
239        Some("udp" | "test") => crate::protocol::metadata::PrivilegeRequirement::None,
240        _ => declared,
241    }
242}
243
244/// Translate a protocol's declared `privilege_requirement` into runtime dependencies.
245///
246/// This is the body of [`Protocol::get_dependencies`]'s default. It is a free function so an
247/// overriding protocol can extend the derived list instead of replacing it:
248///
249/// ```rust,ignore
250/// fn get_dependencies(&self) -> Vec<ProtocolDependency> {
251///     let mut deps = default_dependencies_from_privilege(self);
252///     deps.push(ProtocolDependency::SystemLibrary("pcap"));
253///     deps
254/// }
255/// ```
256///
257/// `DeviceAccess` yields nothing on purpose — see the note on `get_dependencies`.
258pub fn default_dependencies_from_privilege<P: Protocol + ?Sized>(
259    protocol: &P,
260) -> Vec<ProtocolDependency> {
261    use crate::protocol::metadata::PrivilegeRequirement as P9;
262
263    match protocol.metadata().privilege_requirement {
264        P9::None => Vec::new(),
265        // A privileged *default* port is not a missing dependency: the port is a
266        // startup parameter, so the protocol runs perfectly well on 1179, 8080 or
267        // 5353 with no privilege at all. Treating it as a dependency excluded 26
268        // protocols from the catalogue on any non-root run — DNS, HTTP, SMTP, SSH,
269        // BGP and the rest — and `/env` said outright that they were "hidden from
270        // the model", so the LLM could not choose them however the user phrased the
271        // request. Nothing was unable to run; they were only unable to be picked.
272        //
273        // The check that matters still happens, and is the one that knows the real
274        // port: `server_startup.rs` refuses a start only when the port actually
275        // requested is below 1024 and privilege is absent, and its error names the
276        // high ports to use instead. Advisory listing lives in
277        // `privileged_default_ports()`.
278        P9::PrivilegedPort(_) => Vec::new(),
279        P9::RawSockets => vec![ProtocolDependency::RawSocketAccess],
280        P9::PacketCapture => vec![ProtocolDependency::PromiscuousMode],
281        // No probe can answer honestly for an adapter/reader, so claim nothing rather than
282        // exclude a protocol that works.
283        P9::DeviceAccess(_) => Vec::new(),
284        P9::Root => vec![ProtocolDependency::RootAccess],
285    }
286}
287
288/// Result of executing a protocol action
289#[derive(Debug)]
290pub enum ActionResult {
291    /// Data to send over the connection/socket
292    Output(Vec<u8>),
293
294    /// Close the connection (connection-oriented protocols only)
295    CloseConnection,
296
297    /// Wait for more data before responding (accumulating state)
298    WaitForMore,
299
300    /// No action needed (e.g., logging, state update)
301    NoAction,
302
303    /// Multiple results (e.g., send data + close connection)
304    Multiple(Vec<ActionResult>),
305
306    /// Custom protocol-specific result with structured data
307    ///
308    /// This is used when a protocol needs to return structured information
309    /// that isn't just "send these bytes". Protocols encode their responses
310    /// as JSON in the 'data' field, and the protocol handler decodes and
311    /// processes them.
312    ///
313    /// Examples:
314    /// - SSH auth: {"name": "ssh_auth", "data": {"allowed": true}}
315    /// - MySQL: {"name": "mysql_query", "data": {"columns": [...], "rows": [...]}}
316    /// - Redis: {"name": "redis_string", "data": {"value": "OK"}}
317    Custom {
318        name: String,
319        data: serde_json::Value,
320    },
321}
322
323impl ActionResult {
324    /// Check if this result contains output data
325    pub fn has_output(&self) -> bool {
326        match self {
327            ActionResult::Output(_) => true,
328            ActionResult::Multiple(results) => results.iter().any(|r| r.has_output()),
329            _ => false,
330        }
331    }
332
333    /// Check if this result closes the connection
334    pub fn closes_connection(&self) -> bool {
335        match self {
336            ActionResult::CloseConnection => true,
337            ActionResult::Multiple(results) => results.iter().any(|r| r.closes_connection()),
338            _ => false,
339        }
340    }
341
342    /// Check if this result waits for more data
343    pub fn waits_for_more(&self) -> bool {
344        match self {
345            ActionResult::WaitForMore => true,
346            ActionResult::Multiple(results) => results.iter().any(|r| r.waits_for_more()),
347            _ => false,
348        }
349    }
350
351    /// Extract all output data from results
352    pub fn get_all_output(&self) -> Vec<Vec<u8>> {
353        match self {
354            ActionResult::Output(data) => vec![data.clone()],
355            ActionResult::Multiple(results) => {
356                results.iter().flat_map(|r| r.get_all_output()).collect()
357            }
358            _ => Vec::new(),
359        }
360    }
361}
362
363/// Trait for protocol server implementations
364///
365/// Each server protocol implements both the Protocol trait (for common functionality)
366/// and this Server trait (for server-specific functionality like spawning).
367///
368/// The Server trait provides:
369/// 1. Server spawning - how to start the protocol server
370/// 2. Action executor - parses and executes protocol actions
371pub trait Server: Protocol {
372    /// Spawn a server instance for this protocol
373    ///
374    /// This is called when a server needs to be started. The implementation
375    /// should bind to the listen address, set up any necessary resources,
376    /// and return the actual bound address.
377    ///
378    /// # Arguments
379    /// * `ctx` - Spawn context with all necessary dependencies
380    ///
381    /// # Returns
382    /// * `Ok(SocketAddr)` - The actual address the server bound to
383    /// * `Err(_)` - If server spawning failed
384    fn spawn(
385        &self,
386        ctx: crate::protocol::SpawnContext,
387    ) -> std::pin::Pin<
388        Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
389    >;
390
391    /// Execute a protocol-specific action
392    ///
393    /// # Arguments
394    /// * `action` - The action JSON object from LLM
395    ///
396    /// # Returns
397    /// * `Ok(ActionResult)` - Result of execution (data to send, close connection, etc.)
398    /// * `Err(_)` - If action execution failed
399    fn execute_action(&self, action: serde_json::Value) -> anyhow::Result<ActionResult>;
400
401    /// Execute a protocol-specific action with access to application state.
402    ///
403    /// Actions are dispatched on the *stateless* protocol struct held by the registry — a
404    /// zero-sized description with no channel, socket or peer table. That is fine for sync
405    /// actions, which are answers carried back to the connection task that raised the event,
406    /// but it leaves async actions (`list_peers`, `get_server_info` and their kin) with
407    /// nothing to read from, so the only honest thing they could return was `NoAction`.
408    ///
409    /// Overriding this method is how a protocol reaches its running instance: call
410    /// [`AppState::server_handle`] with the handle type its `spawn()` registered via
411    /// [`AppState::register_server_handle`], and talk to the live server.
412    ///
413    /// The default delegates to [`Self::execute_action`], so every protocol that does not
414    /// need live state is unaffected and nothing has to opt in.
415    fn execute_action_with_state<'a>(
416        &'a self,
417        action: serde_json::Value,
418        state: AppState,
419        server_id: Option<crate::state::ServerId>,
420    ) -> std::pin::Pin<
421        Box<dyn std::future::Future<Output = anyhow::Result<ActionResult>> + Send + 'a>,
422    > {
423        // AppState is an Arc internally, so taking it by value is cheap.
424        let _ = (state, server_id);
425        let result = self.execute_action(action);
426        Box::pin(async move { result })
427    }
428}
429
430/// Report event types that leave the model without a protocol vocabulary to answer them.
431///
432/// This is the item-56 defect: the model answering such an event is handed only
433/// `set_memory`/`append_memory`/`show_message`/`append_to_log`, so every protocol action it
434/// returns is rejected as unknown, retried twice, and fails. It silently disabled sixteen
435/// protocols. An `EventType` that genuinely needs no actions declares that with
436/// `.with_no_actions()`, which this ignores.
437///
438/// Returns one human-readable finding per offending event; empty means the protocol is clean.
439///
440/// # The hole this used to have
441///
442/// It early-returned when `get_sync_actions()` was empty, on the reasoning that "a protocol
443/// with nothing to advertise is not withholding anything". That reasoning is backwards, and it
444/// made the guard **pass hardest on the most broken protocol in the tree**: `usb-fido2`
445/// declared zero sync actions *and* three events with no actions, so it could not answer any
446/// event at all, and the audit waved it through with `0 findings`. A protocol that offers the
447/// model nothing is the worst case, not the exempt case.
448///
449/// The one legitimate shape is **delegation**: `doh` and `dot` return `DnsProtocol`'s sync
450/// actions from their own `get_sync_actions()` and attach them to their events, so they are
451/// non-empty on both counts and never reach this branch. Delegation that forwards another
452/// protocol's actions is therefore already covered — what is flagged is an event with an
453/// empty, non-intentional action list, whether or not the protocol declares sync actions of
454/// its own. The deliberate case is still `EventType::with_no_actions()`.
455///
456/// # Where this belongs
457///
458/// Call it **once per protocol at server-startup time** — `src/cli/server_startup.rs`, after
459/// the protocol object is resolved from the registry and before `spawn()` — so a
460/// misdeclared event is reported against a named server the user just tried to start, and
461/// the failure is attributable. The equivalent check must not live on the per-event path:
462/// that runs once per connection inside a tokio task, where a `debug_assert!` panic is
463/// swallowed by the task and leaves the server still reporting `Running`.
464///
465/// `tests/event_action_declarations_test.rs` runs this over the whole registry, which
466/// catches the defect in CI for every protocol rather than only for the ones a run happens
467/// to start — though only for the features that run happens to compile in, which is why the
468/// test also asserts a lower bound on how many protocols it inspected.
469pub fn audit_event_action_declarations(protocol: &dyn Protocol) -> Vec<String> {
470    let sync_action_count = protocol.get_sync_actions().len();
471
472    protocol
473        .get_event_types()
474        .into_iter()
475        .filter(|event_type| event_type.has_no_usable_actions())
476        .map(|event_type| {
477            if sync_action_count == 0 {
478                format!(
479                    "event '{}' of protocol '{}' declares no actions, and the protocol declares \
480                     no sync actions either, so this protocol offers the model nothing: every \
481                     protocol-specific action it could return would be rejected as unknown and \
482                     the event cannot be answered at all. Fix by declaring sync actions and \
483                     attaching them with .with_actions(...), by delegating another protocol's \
484                     action set (as doh/dot do for DNS), or by .with_no_actions() if the event \
485                     genuinely needs none.",
486                    event_type.id,
487                    protocol.protocol_name(),
488                )
489            } else {
490                format!(
491                    "event '{}' of protocol '{}' declares no actions of its own, so the model \
492                     would be offered none of the protocol's {} sync action(s) and anything \
493                     protocol-specific it returned would be rejected as an unknown action. Fix by \
494                     adding .with_actions(...) to the event type, or .with_no_actions() if it \
495                     genuinely needs none.",
496                    event_type.id,
497                    protocol.protocol_name(),
498                    sync_action_count
499                )
500            }
501        })
502        .collect()
503}