Skip to main content

rpi_server/
lib.rs

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