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}