Skip to main content

rpi_server/
lib.rs

1mod server;
2mod transport;
3
4use rpi_plugin_sdk::{
5    register_entrypoint_unified, EventTag, FreeStringFn, PluginApi, RuntimeActionFn, RuntimeActionId,
6    StablePluginEvent, StableToolSchema, StbString, StbStringRef, StepHandle, StepResult,
7    ToolPartialCb,
8};
9use serde_json::{json, Value};
10use std::collections::HashMap;
11use std::sync::atomic::{AtomicBool, AtomicPtr, Ordering};
12use std::sync::{Arc, Mutex, OnceLock};
13use std::sync::mpsc;
14use std::thread;
15use std::time::{Duration, Instant};
16use server::{generate_token, launch_args, run_server, TcpServerHandle};
17use transport::{ManagedConnection, TcpTransport};
18
19const DEFAULT_TIMEOUT: u64 = 30;
20
21// ── Data ──────────────────────────────────────────────────────────────────────
22
23static SERVERS: OnceLock<Mutex<HashMap<String, Arc<TcpServerHandle>>>> = OnceLock::new();
24
25fn servers() -> &'static Mutex<HashMap<String, Arc<TcpServerHandle>>> {
26    SERVERS.get_or_init(|| Mutex::new(HashMap::new()))
27}
28
29// The host's `runtime_action` entry point + its opaque `user_data` (the host's
30// `ActionBridge`), copied OUT of the vtable during `register`. The vtable is the
31// host's to own — we must not retain the `api` pointer (that is a
32// use-after-free: the host builds it on the register stack). `GetCliFlag` needs
33// both the entry point and the `user_data`.
34static RUNTIME_ACTION: OnceLock<RuntimeActionFn> = OnceLock::new();
35static RUNTIME_USER_DATA: AtomicPtr<std::ffi::c_void> = AtomicPtr::new(std::ptr::null_mut());
36// The host's `free_string`, for reclaiming host-produced `out` strings.
37static HOST_FREE_STRING: OnceLock<FreeStringFn> = OnceLock::new();
38
39/// Call the host's `GetCliFlag` runtime action to read a parsed CLI flag value.
40fn get_cli_flag(name: &str) -> Option<Value> {
41    let runtime_action = *RUNTIME_ACTION.get()?;
42    let user_data = RUNTIME_USER_DATA.load(Ordering::Acquire);
43    if user_data.is_null() {
44        return None;
45    }
46
47    let args = json!({"name": name});
48    let args_str = args.to_string();
49    let args_ref = StbStringRef::from_str(&args_str);
50    let mut out = StbString::empty();
51
52    let rc = runtime_action(
53        RuntimeActionId::GetCliFlag as u32,
54        args_ref,
55        &mut out,
56        user_data,
57    );
58
59    // The host may have produced `out` even on an error path; reclaim it via the
60    // host's free so we don't leak the host allocation.
61    let result_str = if rc == 0 {
62        Some(out.to_string_lossy())
63    } else {
64        None
65    };
66    if let Some(free) = HOST_FREE_STRING.get() {
67        free(out);
68    }
69
70    let result: Value = serde_json::from_str(&result_str?).ok()?;
71    result.get("value").cloned()
72}
73
74struct Drive {
75    receiver: mpsc::Receiver<Result<String, String>>,
76    cancelled: Arc<AtomicBool>,
77    done: bool,
78}
79
80// ── Param parsing ─────────────────────────────────────────────────────────────
81
82fn string_param(params: &Value, key: &str) -> Result<String, String> {
83    params
84        .get(key)
85        .and_then(Value::as_str)
86        .map(str::to_owned)
87        .filter(|s| !s.trim().is_empty())
88        .ok_or_else(|| format!("{key} is required"))
89}
90
91fn optional_string(params: &Value, key: &str) -> Result<Option<String>, String> {
92    match params.get(key) {
93        None | Some(Value::Null) => Ok(None),
94        Some(Value::String(s)) if !s.trim().is_empty() => Ok(Some(s.clone())),
95        Some(Value::String(_)) => Err(format!("{key} must not be empty")),
96        Some(_) => Err(format!("{key} must be a string")),
97    }
98}
99
100fn timeout_param(params: &Value) -> Result<Duration, String> {
101    match params.get("timeoutSeconds") {
102        None | Some(Value::Null) => Ok(Duration::from_secs(DEFAULT_TIMEOUT)),
103        Some(v) => v
104            .as_u64()
105            .filter(|v| (1..=300).contains(v))
106            .map(Duration::from_secs)
107            .ok_or_else(|| "timeoutSeconds must be an integer from 1 to 300".into()),
108    }
109}
110
111// ── Server lifecycle ──────────────────────────────────────────────────────────
112
113fn do_start(params: &Value) -> Result<String, String> {
114    let bind_addr = optional_string(params, "bind")?.unwrap_or_else(|| "127.0.0.1".into());
115    let port: u16 = match params.get("port") {
116        Some(Value::Number(n)) => n.as_u64()
117            .and_then(|n| u16::try_from(n).ok())
118            .ok_or("port must be an integer 0–65535")?,
119        None => 9800,
120        Some(_) => return Err("port must be an integer".into()),
121    };
122
123    let (executable, args) = launch_args(params)?;
124    let no_token = params
125        .get("noToken")
126        .and_then(Value::as_bool)
127        .unwrap_or(false);
128    let token = if no_token {
129        // Authentication disabled: any client may connect (trusted local use).
130        String::new()
131    } else {
132        params
133            .get("token")
134            .and_then(Value::as_str)
135            .map(str::to_owned)
136            .filter(|t| !t.is_empty())
137            .unwrap_or_else(generate_token)
138    };
139
140    let exec_clone = executable.clone();
141    let args_clone = args.clone();
142    let token_clone = token.clone();
143    let bind_clone = bind_addr.clone();
144
145    let (ready_tx, ready_rx) = mpsc::channel();
146    thread::spawn(move || {
147        let runtime = match tokio::runtime::Builder::new_multi_thread()
148            .enable_all()
149            .worker_threads(2)
150            .build()
151        {
152            Ok(r) => r,
153            Err(e) => {
154                let _ = ready_tx.send(Err(format!("failed to create runtime: {e}")));
155                return;
156            }
157        };
158
159        let result = runtime.block_on(async {
160            let transport = Arc::new(TcpTransport::new());
161            run_server(transport, &bind_clone, port, token_clone, exec_clone, args_clone).await
162        });
163
164        match result {
165            Ok(server) => {
166                if let Ok(mut registry) = servers().lock() {
167                    registry.insert(server.id.clone(), server.clone());
168                }
169                let _ = ready_tx.send(Ok(server));
170            }
171            Err(e) => {
172                let _ = ready_tx.send(Err(e));
173            }
174        }
175
176        runtime.block_on(async { loop { tokio::time::sleep(Duration::from_secs(3600)).await; } });
177    });
178
179    let server = match ready_rx.recv_timeout(Duration::from_secs(10)) {
180        Ok(Ok(s)) => s,
181        Ok(Err(e)) => return Err(e),
182        Err(_) => return Err("timed out waiting for server startup".into()),
183    };
184
185    Ok(json!({
186        "id": server.id,
187        "address": server.address,
188        "token": token,
189        "state": "running",
190        "protocol": "jsonl",
191        "transport": "tcp",
192    }).to_string())
193}
194
195fn do_stop(id: &str) -> Result<String, String> {
196    let server = servers()
197        .lock()
198        .map_err(|_| "server registry poisoned")?
199        .remove(id)
200        .ok_or("RPC server not found")?;
201
202    let (tx, rx) = mpsc::channel();
203    let server_clone = Arc::clone(&server);
204    thread::spawn(move || {
205        let runtime = tokio::runtime::Builder::new_current_thread()
206            .enable_all()
207            .build()
208            .unwrap();
209        let result = runtime.block_on(server_clone.stop());
210        let _ = tx.send(result);
211    });
212
213    match rx.recv_timeout(Duration::from_secs(5)) {
214        Ok(Ok(())) => Ok(json!({"id": id, "state": "stopped"}).to_string()),
215        Ok(Err(e)) => Err(e),
216        Err(_) => Err("timed out waiting for server stop".into()),
217    }
218}
219
220async fn server_status(server: &Arc<TcpServerHandle>) -> Result<Value, String> {
221    let info = server.info().await;
222    Ok(json!({
223        "id": info.id,
224        "address": info.address,
225        "token": info.token,
226        "state": info.state,
227        "protocol": "jsonl",
228        "transport": "tcp",
229        "sessions": info.sessions,
230    }))
231}
232
233async fn do_status(id: &str) -> Result<String, String> {
234    let server = servers()
235        .lock()
236        .map_err(|_| "server registry poisoned")?
237        .get(id)
238        .cloned()
239        .ok_or("RPC server not found")?;
240    Ok(server_status(&server).await?.to_string())
241}
242
243async fn do_list() -> Result<String, String> {
244    let registry = servers().lock().map_err(|_| "server registry poisoned")?;
245    let mut list: Vec<Value> = Vec::new();
246    for server in registry.values() {
247        list.push(server_status(server).await?);
248    }
249    Ok(json!(list).to_string())
250}
251
252async fn server_tool(params: &Value) -> Result<String, String> {
253    match params.get("action").and_then(Value::as_str).unwrap_or("start") {
254        "start" => do_start(params),
255        "stop" => {
256            let id = string_param(params, "id")?;
257            do_stop(&id)
258        }
259        "status" => {
260            let id = string_param(params, "id")?;
261            do_status(&id).await
262        }
263        "list" => do_list().await,
264        other => Err(format!("action must be one of: start, stop, status, list (got {other})")),
265    }
266}
267
268fn stop_all_servers() {
269    let ids: Vec<String> = match servers().lock() {
270        Ok(registry) => registry.keys().cloned().collect(),
271        Err(_) => return,
272    };
273    for id in ids {
274        let _ = do_stop(&id);
275    }
276}
277
278extern "C" fn on_session_shutdown(event: StablePluginEvent, _: *mut std::ffi::c_void) -> i32 {
279    if event.tag == EventTag::SessionShutdown {
280        stop_all_servers();
281    }
282    0
283}
284
285/// Whether a `GetCliFlag` result means the switch is ON. `GetCliFlag` yields
286/// `true` for a bare `--flag`, a string when `--flag=v`, and `null` when unset.
287fn flag_is_on(value: Option<&Value>) -> bool {
288    match value {
289        Some(Value::Bool(b)) => *b,
290        Some(Value::String(s)) => !s.is_empty() && !s.eq_ignore_ascii_case("false"),
291        _ => false,
292    }
293}
294
295/// On SessionStart, check if --server CLI flag was passed and auto-start the TCP server.
296extern "C" fn on_session_start(event: StablePluginEvent, _: *mut std::ffi::c_void) -> i32 {
297    if event.tag != EventTag::SessionStart {
298        return 0;
299    }
300
301    // Check if --server flag was passed. `GetCliFlag` returns `{"value": null}`
302    // for an *unset* flag, so a bare `is_none()` would wrongly treat that as
303    // present — require an explicit truthy value.
304    if !flag_is_on(get_cli_flag("server").as_ref()) {
305        return 0;
306    }
307
308    // Read optional --port flag (default 9800)
309    let port: u16 = match get_cli_flag("port") {
310        Some(Value::Number(n)) => n.as_u64().and_then(|n| u16::try_from(n).ok()).unwrap_or(9800),
311        Some(Value::String(s)) => s.parse().unwrap_or(9800),
312        _ => 9800,
313    };
314
315    // Read optional --bind flag (default 127.0.0.1)
316    let bind = match get_cli_flag("bind") {
317        Some(Value::String(s)) if !s.is_empty() => s,
318        _ => "127.0.0.1".to_string(),
319    };
320
321    eprintln!("[rpi-server] --server detected, starting TCP server on {bind}:{port}");
322
323    let token = match get_cli_flag("token") {
324        Some(Value::String(s)) if !s.is_empty() => Some(s),
325        _ => None,
326    };
327    let no_token = flag_is_on(get_cli_flag("no-token").as_ref());
328
329    let params = json!({
330        "bind": bind,
331        "port": port,
332        "token": token,
333        "noToken": no_token,
334    });
335
336    match do_start(&params) {
337        Ok(info) => eprintln!("[rpi-server] TCP server started: {info}"),
338        Err(e) => eprintln!("[rpi-server] failed to start: {e}"),
339    }
340
341    0
342}
343
344// ── Client ────────────────────────────────────────────────────────────────────
345
346fn client_tool(params: &Value, cancelled: &AtomicBool) -> Result<String, String> {
347    let server_id = string_param(params, "serverId")?;
348    let method = string_param(params, "method")?;
349    let rpc_params = params.get("params").cloned().unwrap_or(json!({}));
350
351    let server = servers()
352        .lock()
353        .map_err(|_| "server registry poisoned")?
354        .get(&server_id)
355        .cloned()
356        .ok_or("RPC server not found")?;
357
358    if !server.is_running() {
359        return Err("RPC server is not running".into());
360    }
361
362    let wait = timeout_param(params)?;
363    let subscribe = params.get("subscribe").and_then(Value::as_bool).unwrap_or(false);
364
365    let (tx, rx) = mpsc::channel();
366    let cancelled_clone = Arc::new(AtomicBool::new(cancelled.load(Ordering::Acquire)));
367    let cancelled_inner = Arc::clone(&cancelled_clone);
368
369    thread::spawn(move || {
370        let runtime = tokio::runtime::Builder::new_current_thread()
371            .enable_all()
372            .build()
373            .unwrap();
374
375        let result = runtime.block_on(async {
376            let transport = Arc::new(TcpTransport::new());
377            let managed = ManagedConnection::new(transport);
378
379            // Connect with retry
380            managed.connect_with_retry(&server.address).await
381                .map_err(|e| format!("failed to connect: {e}"))?;
382
383            // Authenticate when the server requires a token.
384            if !server.token.is_empty() {
385                let auth = json!({
386                    "jsonrpc": "2.0",
387                    "id": 0,
388                    "method": "authenticate",
389                    "params": {"token": server.token},
390                });
391                managed
392                    .send(auth)
393                    .await
394                    .map_err(|e| format!("auth send failed: {e}"))?;
395                match tokio::time::timeout(Duration::from_secs(5), managed.recv()).await {
396                    Ok(Ok(Some(response))) => {
397                        if let Some(error) = response.get("error") {
398                            return Err(format!("authentication failed: {error}"));
399                        }
400                    }
401                    Ok(Ok(None)) => {
402                        return Err("authentication failed: connection closed".into())
403                    }
404                    Ok(Err(e)) => return Err(format!("authentication failed: {e}")),
405                    Err(_) => return Err("authentication failed: timed out".into()),
406                }
407            }
408
409            // Send request
410            let request = json!({
411                "jsonrpc": "2.0",
412                "method": method,
413                "params": rpc_params,
414                "id": 1,
415            });
416            managed.send(request).await
417                .map_err(|e| format!("send failed: {e}"))?;
418
419            if subscribe {
420                // Collect streaming events
421                let mut events = Vec::new();
422                let deadline = Instant::now() + wait;
423
424                loop {
425                    if cancelled_inner.load(Ordering::Acquire) { break; }
426                    if Instant::now() >= deadline { break; }
427
428                    match tokio::time::timeout(Duration::from_millis(100), managed.recv()).await {
429                        Ok(Ok(Some(event))) => {
430                            // Check for session_end before pushing
431                            let is_end = event.get("type").and_then(Value::as_str) == Some("session_end");
432                            events.push(event);
433                            if is_end {
434                                break;
435                            }
436                        }
437                        Ok(Ok(None)) => break,
438                        Ok(Err(e)) => return Err(format!("recv error: {e}")),
439                        Err(_) => continue, // timeout, loop again
440                    }
441                }
442
443                Ok(json!({
444                    "serverId": server_id,
445                    "method": method,
446                    "events": events,
447                    "eventCount": events.len(),
448                }).to_string())
449            } else {
450                // Wait for response
451                let deadline = Instant::now() + wait;
452                loop {
453                    if cancelled_inner.load(Ordering::Acquire) {
454                        return Err("request cancelled".into());
455                    }
456                    if Instant::now() >= deadline {
457                        return Err("request timed out".into());
458                    }
459
460                    match tokio::time::timeout(Duration::from_millis(100), managed.recv()).await {
461                        Ok(Ok(Some(response))) => {
462                            return Ok(json!({
463                                "serverId": server_id,
464                                "method": method,
465                                "result": response,
466                            }).to_string());
467                        }
468                        Ok(Ok(None)) => return Err("connection closed".into()),
469                        Ok(Err(e)) => return Err(format!("recv error: {e}")),
470                        Err(_) => continue, // timeout, loop again
471                    }
472                }
473            }
474        });
475
476        let _ = tx.send(result);
477    });
478
479    let deadline = Instant::now() + wait;
480    loop {
481        if cancelled.load(Ordering::Acquire) {
482            return Err("request cancelled".into());
483        }
484        if Instant::now() >= deadline {
485            return Err("request timed out".into());
486        }
487        match rx.recv_timeout(Duration::from_millis(100)) {
488            Ok(result) => return result,
489            Err(mpsc::RecvTimeoutError::Timeout) => continue,
490            Err(mpsc::RecvTimeoutError::Disconnected) => {
491                return Err("worker stopped without result".into());
492            }
493        }
494    }
495}
496
497// ── SDK integration ───────────────────────────────────────────────────────────
498
499extern "C" fn execute_server(
500    tool_call_id: StbStringRef,
501    params: StbString,
502    free: Option<FreeStringFn>,
503) -> StepHandle {
504    let _ = tool_call_id;
505    let text = params.to_string_lossy();
506    params.free_with(free);
507    let cancelled = Arc::new(AtomicBool::new(false));
508    let (tx, rx) = mpsc::channel();
509    thread::spawn(move || {
510        let result = serde_json::from_str::<Value>(&text)
511            .map_err(|e| format!("invalid parameters: {e}"))
512            .and_then(|p| {
513                let runtime = tokio::runtime::Builder::new_current_thread()
514                    .enable_all()
515                    .build()
516                    .unwrap();
517                runtime.block_on(async {
518                    server_tool(&p).await
519                })
520            });
521        let _ = tx.send(result);
522    });
523    Box::into_raw(Box::new(Drive { receiver: rx, cancelled, done: false })) as StepHandle
524}
525
526extern "C" fn execute_client(
527    tool_call_id: StbStringRef,
528    params: StbString,
529    free: Option<FreeStringFn>,
530) -> StepHandle {
531    let _ = tool_call_id;
532    let text = params.to_string_lossy();
533    params.free_with(free);
534    let cancelled = Arc::new(AtomicBool::new(false));
535    let cancelled_clone = Arc::clone(&cancelled);
536    let (tx, rx) = mpsc::channel();
537    thread::spawn(move || {
538        let result = serde_json::from_str::<Value>(&text)
539            .map_err(|e| format!("invalid parameters: {e}"))
540            .and_then(|p| client_tool(&p, &cancelled_clone));
541        let _ = tx.send(result);
542    });
543    Box::into_raw(Box::new(Drive { receiver: rx, cancelled, done: false })) as StepHandle
544}
545
546extern "C" fn poll(
547    handle: StepHandle,
548    _: Option<ToolPartialCb>,
549    _: *mut std::ffi::c_void,
550) -> StepResult {
551    if handle.is_null() {
552        return StepResult::err(StbString::from_string("null handle".into()));
553    }
554    let drive = unsafe { &mut *(handle as *mut Drive) };
555    if drive.done {
556        return StepResult::err(StbString::from_string("handle polled after completion".into()));
557    }
558    if drive.cancelled.load(Ordering::Acquire) {
559        drive.done = true;
560        return StepResult::err(StbString::from_string("request cancelled".into()));
561    }
562    match drive.receiver.try_recv() {
563        Ok(Ok(text)) => {
564            drive.done = true;
565            StepResult::done(StbString::from_string(
566                json!({"content": [{"type": "text", "text": text}]}).to_string(),
567            ))
568        }
569        Ok(Err(e)) => {
570            drive.done = true;
571            StepResult::err(StbString::from_string(e))
572        }
573        Err(mpsc::TryRecvError::Empty) => StepResult::pending(StbString::empty()),
574        Err(mpsc::TryRecvError::Disconnected) => {
575            drive.done = true;
576            StepResult::err(StbString::from_string("worker stopped without result".into()))
577        }
578    }
579}
580
581extern "C" fn cancel(handle: StepHandle) {
582    if !handle.is_null() {
583        unsafe { (&*(handle as *mut Drive)).cancelled.store(true, Ordering::Release); }
584    }
585}
586
587extern "C" fn destroy(handle: StepHandle) {
588    if !handle.is_null() {
589        unsafe {
590            let drive = Box::from_raw(handle as *mut Drive);
591            drive.cancelled.store(true, Ordering::Release);
592        }
593    }
594}
595
596extern "C" fn free_string(s: StbString) {
597    if !s.is_empty() && !s.ptr.is_null() {
598        unsafe {
599            let bytes = std::slice::from_raw_parts(s.ptr as *const u8, s.len);
600            let _ = Box::from_raw(bytes as *const [u8] as *mut [u8]);
601        }
602    }
603}
604
605// ── Schemas ───────────────────────────────────────────────────────────────────
606
607const SERVER_PARAMETERS: &str = r#"{
608  "type":"object",
609  "properties":{
610    "action":{"type":"string","enum":["start","stop","status","list"]},
611    "id":{"type":"string"},
612    "bind":{"type":"string"},
613    "port":{"type":"integer","minimum":0,"maximum":65535},
614    "token":{"type":"string"},
615    "provider":{"type":"string"},
616    "model":{"type":"string"},
617    "baseUrl":{"type":"string"},
618    "systemPrompt":{"type":"string"},
619    "appendSystemPrompt":{"type":"array","items":{"type":"string"}},
620    "thinking":{"type":"string","enum":["off","minimal","low","medium","high","xhigh","max"]},
621    "name":{"type":"string"},
622    "session":{"type":"string"},
623    "sessionId":{"type":"string"},
624    "sessionDir":{"type":"string"},
625    "noSession":{"type":"boolean"},
626    "tools":{"type":"array","items":{"type":"string"}},
627    "excludeTools":{"type":"array","items":{"type":"string"}},
628    "noTools":{"type":"boolean"},
629    "noBuiltinTools":{"type":"boolean"},
630    "noSkills":{"type":"boolean"},
631    "noPromptTemplates":{"type":"boolean"},
632    "noContextFiles":{"type":"boolean"},
633    "noExtensions":{"type":"boolean"},
634    "enablePiPackages":{"type":"boolean"},
635    "extensionsDir":{"type":"array","items":{"type":"string"}},
636    "extension":{"type":"array","items":{"type":"string"}},
637    "skill":{"type":"array","items":{"type":"string"}},
638    "promptTemplate":{"type":"array","items":{"type":"string"}},
639    "offline":{"type":"boolean"},
640    "approve":{"type":"boolean"}
641  }
642}"#;
643
644const CLIENT_PARAMETERS: &str = r#"{
645  "type":"object",
646  "properties":{
647    "serverId":{"type":"string"},
648    "method":{"type":"string"},
649    "params":{"type":"object"},
650    "subscribe":{"type":"boolean","description":"Enable streaming subscription"},
651    "timeoutSeconds":{"type":"integer","minimum":1,"maximum":300}
652  },
653  "required":["serverId","method"]
654}"#;
655
656// ── Plugin entry ──────────────────────────────────────────────────────────────
657
658#[no_mangle]
659pub extern "C" fn rpi_plugin_register(api: *const PluginApi) -> i32 {
660    unsafe {
661        register_entrypoint_unified(api, |api| {
662            // Copy the host function pointers/handles OUT of the vtable during
663            // register — the `api` pointer is only valid for this call.
664            let _ = RUNTIME_ACTION.set(api.runtime_action);
665            RUNTIME_USER_DATA.store(api.user_data, Ordering::Release);
666            let _ = HOST_FREE_STRING.set(api.free_string);
667        let Some(register) = api.register_tool else {
668            return 1;
669        };
670
671        let server_schema = Box::new(StableToolSchema {
672            name: StbString::from_string("rpc_server".into()),
673            description: StbString::from_string(
674                "Start and manage a TCP JSONL server with streaming subscriptions.".into(),
675            ),
676            parameters: StbString::from_string(SERVER_PARAMETERS.into()),
677        });
678
679        let client_schema = Box::new(StableToolSchema {
680            name: StbString::from_string("rpc_client".into()),
681            description: StbString::from_string(
682                "Send a JSON-RPC request to a running TCP server. Set subscribe=true for streaming responses.".into(),
683            ),
684            parameters: StbString::from_string(CLIENT_PARAMETERS.into()),
685        });
686
687        let first = register(
688            &*server_schema,
689            execute_server,
690            poll,
691            cancel,
692            destroy,
693            free_string,
694        );
695
696        let second = if first == 0 {
697            register(
698                &*client_schema,
699                execute_client,
700                poll,
701                cancel,
702                destroy,
703                free_string,
704            )
705        } else {
706            first
707        };
708
709        let third = if second == 0 {
710            api.register_event_handler
711                .map(|register| {
712                    register(
713                        EventTag::SessionShutdown,
714                        on_session_shutdown,
715                        std::ptr::null_mut(),
716                    )
717                })
718                .unwrap_or(0)
719        } else {
720            second
721        };
722
723        // Register SessionStart handler to auto-start server when --server flag is passed
724        let fourth = if third == 0 {
725            api.register_event_handler
726                .map(|register| {
727                    register(
728                        EventTag::SessionStart,
729                        on_session_start,
730                        std::ptr::null_mut(),
731                    )
732                })
733                .unwrap_or(0)
734        } else {
735            third
736        };
737
738        drop(server_schema);
739        drop(client_schema);
740            fourth
741        })
742    }
743}
744
745#[cfg(test)]
746mod tests {
747    use super::*;
748
749    #[test]
750    fn launch_args_basic() {
751        let (_, args) = launch_args(&json!({
752            "provider": "anthropic",
753            "model": "claude-3",
754            "noSession": true,
755        }))
756        .unwrap();
757        assert_eq!(&args[0..2], ["--mode", "rpc"]);
758        assert!(args.windows(2).any(|w| w == ["--provider", "anthropic"]));
759        assert!(args.windows(2).any(|w| w == ["--model", "claude-3"]));
760        assert!(args.contains(&"--no-session".to_string()));
761    }
762
763    #[test]
764    fn schemas_are_valid_json() {
765        assert!(serde_json::from_str::<Value>(SERVER_PARAMETERS).is_ok());
766        assert!(serde_json::from_str::<Value>(CLIENT_PARAMETERS).is_ok());
767    }
768
769    #[test]
770    fn generate_token_is_32_hex_chars() {
771        let t = generate_token();
772        assert_eq!(t.len(), 32);
773        assert!(t.chars().all(|c| c.is_ascii_hexdigit()));
774    }
775}