zagens_runtime/runtime_serve/
mod.rs1mod http;
4
5use std::path::PathBuf;
6
7use anyhow::{Context, Result};
8use clap::Parser;
9
10use crate::cli::configure_windows_console_utf8;
11use crate::config::Config;
12
13pub use http::{RuntimeApiOptions, run_http_server};
14
15#[derive(Parser, Debug)]
17#[command(
18 name = "zagens-runtime",
19 about = "DeepSeek runtime HTTP/SSE sidecar (Zagens desktop; no TUI)",
20 version
21)]
22pub struct RuntimeServeCli {
23 #[arg(short, long)]
25 pub config: Option<PathBuf>,
26 #[arg(long)]
28 pub profile: Option<String>,
29 #[arg(short, long)]
31 pub workspace: Option<PathBuf>,
32 #[arg(long, default_value = "127.0.0.1")]
34 pub host: String,
35 #[arg(long, default_value_t = 7878)]
37 pub port: u16,
38 #[arg(long, default_value_t = 8)]
40 pub workers: usize,
41 #[arg(long = "cors-origin", value_name = "URL")]
43 pub cors_origin: Vec<String>,
44 #[arg(long = "auth-token", value_name = "TOKEN")]
46 pub auth_token: Option<String>,
47 #[arg(short, long)]
49 pub verbose: bool,
50}
51
52pub fn resolve_cors_origins(config: &Config, flag_origins: &[String]) -> Vec<String> {
54 let mut out: Vec<String> = Vec::new();
55 let mut push = |raw: &str| {
56 let trimmed = raw.trim();
57 if trimmed.is_empty() {
58 return;
59 }
60 if !out.iter().any(|existing| existing == trimmed) {
61 out.push(trimmed.to_string());
62 }
63 };
64 for o in flag_origins {
65 push(o);
66 }
67 if let Ok(env_value) = std::env::var("DEEPSEEK_CORS_ORIGINS") {
68 for piece in env_value.split(',') {
69 push(piece);
70 }
71 }
72 if let Some(rt) = &config.runtime_api
73 && let Some(list) = &rt.cors_origins
74 {
75 for o in list {
76 push(o);
77 }
78 }
79 out
80}
81
82fn load_config(cli: &RuntimeServeCli) -> Result<Config> {
83 let profile = cli
84 .profile
85 .clone()
86 .or_else(|| std::env::var("DEEPSEEK_PROFILE").ok());
87 Config::load(cli.config.clone(), profile.as_deref()).context("load config")
88}
89
90fn resolve_workspace(cli: &RuntimeServeCli) -> PathBuf {
91 if let Some(ws) = cli.workspace.clone() {
92 return ws;
93 }
94 if let Some(docs) = dirs::document_dir() {
96 let zagens = docs.join("Zagens");
97 if zagens.is_dir() {
98 return zagens;
99 }
100 if std::fs::create_dir_all(&zagens).is_ok() {
101 return zagens;
102 }
103 }
104 std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
105}
106
107pub async fn run(cli: RuntimeServeCli) -> Result<()> {
109 configure_windows_console_utf8();
110 crate::logging::set_verbose(cli.verbose || crate::logging::env_requests_verbose_logging());
111
112 if cli.host != "127.0.0.1" && cli.host != "localhost" {
113 eprintln!(
114 "⚠ deepseek-runtime is binding to {} (not localhost).\n\
115 The runtime API will be reachable from other machines on the network.\n\
116 Make sure you have set --auth-token (or DEEPSEEK_RUNTIME_TOKEN) and\n\
117 configured restrictive CORS origins via --cors-origin or config.toml.",
118 cli.host,
119 );
120 }
121
122 let _ = crate::config::ensure_config_file_exists(cli.config.clone());
123
124 let config = load_config(&cli)?;
125 let workspace = resolve_workspace(&cli);
126 crate::symbol_index::warmup_if_needed(&workspace);
127 let skills_dir = config.skills_dir();
128 tokio::spawn(async move {
129 if let Err(e) = crate::skills::install_system_skills(&skills_dir) {
130 crate::logging::warn(format!("Failed to install system skills: {e}"));
131 }
132 });
133
134 let cors_origins = resolve_cors_origins(&config, &cli.cors_origin);
135 run_http_server(
136 config,
137 workspace,
138 RuntimeApiOptions {
139 host: cli.host,
140 port: cli.port,
141 workers: cli.workers.clamp(1, 16),
142 cors_origins,
143 auth_token: cli.auth_token,
144 },
145 )
146 .await
147}
148
149pub async fn run_or_exit(cli: RuntimeServeCli) -> ! {
151 match run(cli).await {
152 Ok(()) => {
153 eprintln!("[deepseek-runtime] server shut down cleanly, exiting");
154 std::process::exit(0);
155 }
156 Err(e) => {
157 eprintln!("[deepseek-runtime] fatal: {:#}", e);
158 std::process::exit(1);
159 }
160 }
161}
162
163pub async fn run_from_args<I, T>(args: I) -> !
165where
166 I: IntoIterator<Item = T>,
167 T: Into<std::ffi::OsString> + Clone,
168{
169 let cli = RuntimeServeCli::parse_from(args);
170 run_or_exit(cli).await
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn resolve_cors_dedupes() {
179 let config = Config::default();
180 let out = resolve_cors_origins(&config, &["http://a".into(), "http://a".into()]);
181 assert_eq!(out, vec!["http://a".to_string()]);
182 }
183}