Skip to main content

netget/client/ntp/
actions.rs

1//! NTP client protocol actions implementation
2
3use crate::llm::actions::{
4    client_trait::{Client, ClientActionResult},
5    protocol_trait::Protocol,
6    ActionDefinition, Parameter,
7};
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14/// NTP client connected event
15pub static NTP_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16    EventType::new(
17        "ntp_connected",
18        "NTP client ready to query time servers",
19        json!({"type": "placeholder", "event_id": "ntp_connected"}),
20    )
21    .with_parameters(vec![Parameter {
22        name: "remote_addr".to_string(),
23        type_hint: "string".to_string(),
24        description: "NTP server address".to_string(),
25        required: true,
26    }])
27});
28
29/// NTP client response received event
30pub static NTP_CLIENT_RESPONSE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
31    EventType::new(
32        "ntp_response_received",
33        "NTP time response received from server",
34        json!({"type": "analyze_response"}),
35    )
36    .with_parameters(vec![
37        Parameter {
38            name: "origin_timestamp".to_string(),
39            type_hint: "number".to_string(),
40            description: "Origin timestamp (Unix epoch)".to_string(),
41            required: true,
42        },
43        Parameter {
44            name: "receive_timestamp".to_string(),
45            type_hint: "number".to_string(),
46            description: "Server receive timestamp (Unix epoch)".to_string(),
47            required: true,
48        },
49        Parameter {
50            name: "transmit_timestamp".to_string(),
51            type_hint: "number".to_string(),
52            description: "Server transmit timestamp (Unix epoch)".to_string(),
53            required: true,
54        },
55        Parameter {
56            name: "stratum".to_string(),
57            type_hint: "number".to_string(),
58            description: "Server stratum level (0-15)".to_string(),
59            required: true,
60        },
61        Parameter {
62            name: "precision".to_string(),
63            type_hint: "number".to_string(),
64            description: "Server precision (log2 seconds)".to_string(),
65            required: true,
66        },
67    ])
68});
69
70/// NTP client protocol action handler
71pub struct NtpClientProtocol;
72
73impl NtpClientProtocol {
74    pub fn new() -> Self {
75        Self
76    }
77}
78
79impl Default for NtpClientProtocol {
80    fn default() -> Self {
81        Self::new()
82    }
83}
84
85// Implement Protocol trait (common functionality)
86impl Protocol for NtpClientProtocol {
87    fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
88        vec![
89            ActionDefinition {
90                name: "query_time".to_string(),
91                description: "Query NTP server for current time".to_string(),
92                parameters: vec![],
93                example: json!({
94                    "type": "query_time"
95                }),
96                log_template: None,
97            },
98            ActionDefinition {
99                name: "disconnect".to_string(),
100                description: "Close NTP client".to_string(),
101                parameters: vec![],
102                example: json!({
103                    "type": "disconnect"
104                }),
105                log_template: None,
106            },
107        ]
108    }
109    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
110        vec![ActionDefinition {
111            name: "analyze_response".to_string(),
112            description: "Analyze NTP response (no action needed, just for LLM understanding)"
113                .to_string(),
114            parameters: vec![],
115            example: json!({
116                "type": "analyze_response"
117            }),
118            log_template: None,
119        }]
120    }
121    fn protocol_name(&self) -> &'static str {
122        "NTP"
123    }
124    fn get_event_types(&self) -> Vec<EventType> {
125        vec![
126            EventType::new(
127                "ntp_connected",
128                "Triggered when NTP client is ready",
129                json!({"type": "placeholder", "event_id": "ntp_connected"}),
130            ),
131            EventType::new(
132                "ntp_response_received",
133                "Triggered when NTP response is received",
134                json!({"type": "placeholder", "event_id": "ntp_response_received"}),
135            ),
136        ]
137    }
138    fn stack_name(&self) -> &'static str {
139        "ETH>IP>UDP>NTP"
140    }
141    fn keywords(&self) -> Vec<&'static str> {
142        vec!["ntp", "ntp client", "time sync", "network time"]
143    }
144    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
145        use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
146
147        ProtocolMetadataV2::builder()
148            .state(DevelopmentState::Experimental)
149            .implementation("UDP-based NTP client with manual packet construction")
150            .llm_control("Query time servers and interpret timestamps, stratum, precision")
151            .e2e_testing("Public NTP servers (pool.ntp.org)")
152            .build()
153    }
154    fn description(&self) -> &'static str {
155        "NTP client for querying network time servers"
156    }
157    fn example_prompt(&self) -> &'static str {
158        "Query time.google.com:123 and show the time offset"
159    }
160    fn group_name(&self) -> &'static str {
161        "Network Infrastructure"
162    }
163
164    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
165        use crate::llm::actions::StartupExamples;
166        use serde_json::json;
167
168        StartupExamples::new(
169            // LLM mode: LLM controls NTP queries
170            json!({
171                "type": "open_client",
172                "remote_addr": "time.google.com:123",
173                "base_stack": "ntp",
174                "instruction": "Query the time server and report the current time offset and stratum level"
175            }),
176            // Script mode: Code-based deterministic responses
177            json!({
178                "type": "open_client",
179                "remote_addr": "pool.ntp.org:123",
180                "base_stack": "ntp",
181                "event_handlers": [{
182                    "event_pattern": "ntp_response_received",
183                    "handler": {
184                        "type": "script",
185                        "language": "python",
186                        "code": "<ntp_client_handler>"
187                    }
188                }]
189            }),
190            // Static mode: Fixed NTP query on connect
191            json!({
192                "type": "open_client",
193                "remote_addr": "time.google.com:123",
194                "base_stack": "ntp",
195                "event_handlers": [
196                    {
197                        "event_pattern": "ntp_connected",
198                        "handler": {
199                            "type": "static",
200                            "actions": [{
201                                "type": "query_time"
202                            }]
203                        }
204                    },
205                    {
206                        "event_pattern": "ntp_response_received",
207                        "handler": {
208                            "type": "static",
209                            "actions": [{
210                                "type": "disconnect"
211                            }]
212                        }
213                    }
214                ]
215            }),
216        )
217    }
218}
219
220// Implement Client trait (client-specific functionality)
221impl Client for NtpClientProtocol {
222    fn connect(
223        &self,
224        ctx: crate::protocol::ConnectContext,
225    ) -> std::pin::Pin<
226        Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
227    > {
228        Box::pin(async move {
229            use crate::client::ntp::NtpClient;
230            NtpClient::connect_with_llm_actions(
231                ctx.remote_addr,
232                ctx.llm_client,
233                ctx.state,
234                ctx.status_tx,
235                ctx.client_id,
236            )
237            .await
238        })
239    }
240    fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
241        let action_type = action
242            .get("type")
243            .and_then(|v| v.as_str())
244            .context("Missing 'type' field in action")?;
245
246        match action_type {
247            "query_time" => Ok(ClientActionResult::Custom {
248                name: "ntp_query".to_string(),
249                data: json!({}),
250            }),
251            "disconnect" => Ok(ClientActionResult::Disconnect),
252            "analyze_response" => Ok(ClientActionResult::WaitForMore),
253            _ => Err(anyhow::anyhow!(
254                "Unknown NTP client action: {}",
255                action_type
256            )),
257        }
258    }
259}