1#![doc = include_str!("../Documentation.md")]
2
3use kcode_gemini_3_1_pro::Gemini31Pro;
4use kcode_k1_access::K1Access;
5use kcode_k1_access_full_audio::K1AccessFullAudio;
6use kcode_k1_access_persons::K1AccessPersons;
7use kcode_k1_access_profiles::K1AccessProfiles;
8use kcode_k1_accounting::Accounting;
9use kcode_k1_accounts::K1Accounts;
10use kcode_k1_audio_classification::AudioClassification;
11use kcode_k1_chat_service::K1ChatService;
12use kcode_k1_codex_adapter::{Adapter as CodexAdapter, Error as CodexAdapterError};
13use kcode_k1_daemon_files::DaemonFiles;
14use kcode_k1_daemon_http_boundary::{
15 Boundary, PUBLIC_ORIGIN, api_not_found, warn_if_slow, write_readiness,
16};
17use kcode_k1_daemon_provider_config::{
18 CODEX_EXECUTABLE_ENV, audio_models, chat_model, codex_configs, codex_executable, people_models,
19 resolve_ffmpeg,
20};
21use kcode_k1_daemon_vault_unlock::VaultUnlock;
22use kcode_k1_full_audio::K1FullAudio;
23use kcode_k1_groups::K1Groups;
24use kcode_k1_http::{Config as HttpConfig, K1Http};
25use kcode_k1_http_accounts::K1HttpAccounts;
26use kcode_k1_http_people::K1HttpPeople;
27use kcode_k1_http_replay::{ReplayConfig, ReplayWindow};
28use kcode_k1_invites::K1Invites;
29use kcode_k1_objects::K1Objects;
30use kcode_k1_peering::K1Peering;
31use kcode_k1_persons::K1Persons;
32use kcode_k1_txn_ordering::K1TxnOrdering;
33use kcode_k1_users::K1Users;
34use kcode_k1_vault::{ExposeSecret, K1Vault};
35use kcode_speaker_v3_analysis::Analyzer;
36use std::fmt;
37use std::path::{Path, PathBuf};
38use std::process::ExitCode;
39use std::sync::Arc;
40use std::time::Instant;
41
42const INVITE_LINK_URL: &str = "http://localhost:4321/lib/kcode-k1-ui/*/account.html";
43const GEMINI_API_KEY: &str = "gemini-api-key";
44
45struct Prepared {
46 boundary: Boundary,
47 unused_invites: usize,
48 vault: Arc<K1Vault>,
49}
50
51enum StartupError {
52 Generic,
53 CodexAdapter(CodexAdapterError),
54 Chat(String),
55}
56
57impl From<()> for StartupError {
58 fn from((): ()) -> Self {
59 Self::Generic
60 }
61}
62
63impl fmt::Display for StartupError {
64 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
65 match self {
66 Self::Generic => formatter.write_str("kcode-k1-daemon: startup failed"),
67 Self::CodexAdapter(error) => {
68 write!(formatter, "kcode-k1-daemon: startup failed: {error}")
69 }
70 Self::Chat(child) => {
71 write!(formatter, "kcode-k1-daemon: startup failed: {child}")
72 }
73 }
74 }
75}
76
77pub fn run(k1_root: PathBuf) -> ExitCode {
78 let runtime = match tokio::runtime::Builder::new_multi_thread()
79 .enable_all()
80 .build()
81 {
82 Ok(runtime) => runtime,
83 Err(_) => {
84 eprintln!("kcode-k1-daemon: startup failed");
85 return ExitCode::from(1);
86 }
87 };
88 let unlock = match VaultUnlock::prompt() {
89 Ok(unlock) => unlock,
90 Err(_) => {
91 eprintln!("kcode-k1-daemon: startup failed");
92 return ExitCode::from(1);
93 }
94 };
95 runtime.block_on(run_async(k1_root, unlock))
96}
97
98async fn run_async(k1_root: PathBuf, unlock: VaultUnlock) -> ExitCode {
99 let started = Instant::now();
100 let prepared = match startup(k1_root, unlock).await {
101 Ok(prepared) => prepared,
102 Err(error) => {
103 warn_if_slow(started.elapsed(), "error");
104 eprintln!("{error}");
105 return ExitCode::from(1);
106 }
107 };
108 let elapsed = started.elapsed();
109 if write_readiness(prepared.unused_invites).is_err() {
110 warn_if_slow(elapsed, "error");
111 eprintln!("kcode-k1-daemon: startup failed");
112 return ExitCode::from(1);
113 }
114 warn_if_slow(elapsed, "ready");
115 let Prepared {
116 boundary, vault, ..
117 } = prepared;
118 let result = boundary.serve().await;
119 drop(vault);
120 match result {
121 Ok(()) => ExitCode::SUCCESS,
122 Err(()) => {
123 eprintln!("kcode-k1-daemon: listener failed");
124 ExitCode::from(1)
125 }
126 }
127}
128
129async fn startup(k1_root: PathBuf, unlock: VaultUnlock) -> Result<Prepared, StartupError> {
130 let state_root = state_root(&k1_root);
131 let files = DaemonFiles::open(&state_root).map_err(|_| ())?;
132 let ordering = Arc::new(K1TxnOrdering::open(&state_root.join("ordering")).map_err(|_| ())?);
133 let peering = Arc::new(
134 K1Peering::open(&state_root.join("peering"), Arc::clone(&ordering)).map_err(|_| ())?,
135 );
136 let vault = unlock
137 .open(&state_root, Arc::clone(&ordering), Arc::clone(&peering))
138 .map_err(|_| ())?;
139 let persons = Arc::new(
140 K1Persons::open(
141 &state_root.join("persons"),
142 Arc::clone(&ordering),
143 Arc::clone(&peering),
144 )
145 .map_err(|_| ())?,
146 );
147 let invites = Arc::new(
148 K1Invites::open(
149 &state_root.join("invites"),
150 Arc::clone(&ordering),
151 Arc::clone(&peering),
152 )
153 .map_err(|_| ())?,
154 );
155 let accounts = Arc::new(K1Accounts::open(Arc::clone(&invites)).map_err(|_| ())?);
156 let users = Arc::new(K1Users::new(Arc::clone(&accounts), Arc::clone(&persons)));
157 let groups = Arc::new(
158 K1Groups::open(
159 &state_root.join("groups"),
160 Arc::clone(&ordering),
161 Arc::clone(&peering),
162 )
163 .map_err(|_| ())?,
164 );
165 let profiles = Arc::new(
166 K1AccessProfiles::open(
167 &state_root.join("access-profiles"),
168 Arc::clone(&ordering),
169 Arc::clone(&peering),
170 )
171 .map_err(|_| ())?,
172 );
173 let gemini_key = vault.secret(GEMINI_API_KEY).map_err(|_| ())?.ok_or(())?;
174 let gemini = Gemini31Pro::new(
175 gemini_key.expose_secret().to_owned(),
176 Accounting::new(),
177 std::time::Duration::from_secs(30 * 60),
178 )
179 .map_err(|_| ())?;
180 let executable = codex_executable(std::env::var_os(CODEX_EXECUTABLE_ENV));
181 let working_directory = std::env::current_dir()
182 .map_err(|_| ())?
183 .to_string_lossy()
184 .into_owned();
185 let (audio_config, chat_config) = codex_configs(executable, working_directory);
186 let audio_codex_adapter = CodexAdapter::open(audio_config)
187 .await
188 .map_err(StartupError::CodexAdapter)?;
189 let chat_codex_adapter = audio_codex_adapter
190 .with_config(chat_config)
191 .map_err(StartupError::CodexAdapter)?;
192 let analyzer = Analyzer::from_codex_adapter(gemini, audio_codex_adapter);
193 let objects =
194 Arc::new(K1Objects::open(Arc::clone(&ordering), Arc::clone(&peering)).map_err(|_| ())?);
195 let classification = Arc::new(
196 AudioClassification::open(
197 &state_root.join("audio-classification"),
198 Arc::clone(&ordering),
199 Arc::clone(&peering),
200 Arc::clone(&objects),
201 analyzer,
202 )
203 .map_err(|_| ())?,
204 );
205 let full_audio = Arc::new(
206 K1FullAudio::open(
207 resolve_ffmpeg()?,
208 Arc::clone(&objects),
209 Arc::clone(&classification),
210 )
211 .map_err(|_| ())?,
212 );
213 let access = Arc::new(
214 K1Access::open(
215 &state_root.join("access"),
216 Arc::clone(&ordering),
217 Arc::clone(&peering),
218 Arc::clone(&groups),
219 )
220 .map_err(|_| ())?,
221 );
222 let chat = K1ChatService::open(
223 &state_root.join("chat"),
224 Arc::clone(&ordering),
225 Arc::clone(&peering),
226 Arc::clone(&access),
227 Arc::clone(&profiles),
228 chat_codex_adapter,
229 )
230 .map_err(StartupError::Chat)?;
231 let access_persons = Arc::new(
232 K1AccessPersons::open(
233 Arc::clone(&access),
234 Arc::clone(&profiles),
235 Arc::clone(&persons),
236 )
237 .map_err(|_| ())?,
238 );
239 let models = audio_models();
240 let audio = Arc::new(
241 K1AccessFullAudio::open_for_models(
242 Arc::clone(&access),
243 Arc::clone(&profiles),
244 full_audio,
245 classification,
246 Arc::clone(&groups),
247 models.to_vec(),
248 )
249 .map_err(|_| ())?,
250 );
251 let replay = ReplayWindow::open(ReplayConfig {
252 epoch_file: files.replay_epoch_path().to_owned(),
253 max_nonces_per_epoch: usize::MAX,
254 })
255 .await
256 .map_err(|_| ())?;
257 let unused_invites = kcode_k1_daemon_invite_stock::reconcile(
258 &invites,
259 files.invite_links_path(),
260 INVITE_LINK_URL,
261 )
262 .map_err(|_| ())?;
263 if unused_invites < 100 {
264 return Err(().into());
265 }
266 let adapter = K1HttpAccounts::new(
267 Arc::clone(&accounts),
268 Arc::clone(&invites),
269 Arc::clone(&users),
270 );
271 let people_models: Arc<[kcode_k1_http_people::LocalModel]> = Arc::from(people_models());
272 let people = K1HttpPeople::new_with_models(accounts, users, groups, profiles, people_models)
273 .map_err(|_| ())?;
274 let http = K1Http::new(
275 HttpConfig {
276 server_id: files.server_id().to_owned(),
277 public_origin: PUBLIC_ORIGIN.to_owned(),
278 max_body_bytes: usize::MAX,
279 },
280 replay,
281 adapter.identity_provider(),
282 )
283 .map_err(|_| ())?;
284 let person_routes =
285 kcode_k1_http_persons::authenticated_routes(access_persons, access, models[0])
286 .map_err(|_| ())?;
287 let authenticated = adapter
288 .authenticated_routes()
289 .merge(people.authenticated_routes())
290 .merge(kcode_k1_http_audio::authenticated_routes(Arc::clone(
291 &audio,
292 )))
293 .merge(kcode_k1_http_audio_artifacts::authenticated_routes(audio))
294 .merge(person_routes)
295 .merge(kcode_k1_http_chat::router(chat, chat_model()))
296 .fallback(api_not_found);
297 let api = http.router(
298 adapter.registration_endpoint(),
299 kcode_k1_terms::endpoint(),
300 authenticated,
301 );
302 let boundary = Boundary::bind(api, files.server_id().to_owned())
303 .await
304 .map_err(|_| ())?;
305 Ok(Prepared {
306 boundary,
307 unused_invites,
308 vault,
309 })
310}
311
312fn state_root(k1_root: &Path) -> PathBuf {
313 k1_root.join("state")
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 #[test]
321 fn public_operation_and_state_root_are_fixed() {
322 let _: fn(PathBuf) -> ExitCode = run;
323 assert_eq!(
324 state_root(Path::new("/trusted/k1")),
325 PathBuf::from("/trusted/k1/state")
326 );
327 }
328
329 #[test]
330 fn fixed_provider_key_and_origins_remain_exact_and_distinct() {
331 assert_eq!(GEMINI_API_KEY, "gemini-api-key");
332 assert_eq!(
333 INVITE_LINK_URL,
334 "http://localhost:4321/lib/kcode-k1-ui/*/account.html"
335 );
336 assert_eq!(PUBLIC_ORIGIN, "http://localhost:4450");
337 assert_ne!(INVITE_LINK_URL, PUBLIC_ORIGIN);
338 }
339
340 #[test]
341 fn startup_error_rendering_preserves_safe_adapter_and_chat_messages() {
342 assert_eq!(
343 StartupError::from(()).to_string(),
344 "kcode-k1-daemon: startup failed"
345 );
346 let error = CodexAdapterError {
347 kind: kcode_k1_codex_adapter::ErrorKind::Unavailable,
348 message: "safe adapter display".to_owned(),
349 diagnostics: b"RAW_SECRET_DIAGNOSTIC".to_vec(),
350 };
351 let rendered = StartupError::CodexAdapter(error).to_string();
352 assert_eq!(
353 rendered,
354 "kcode-k1-daemon: startup failed: safe adapter display"
355 );
356 assert!(!rendered.contains("RAW_SECRET_DIAGNOSTIC"));
357 let rendered =
358 StartupError::Chat("open chat service: safe child failure".to_owned()).to_string();
359 assert_eq!(
360 rendered,
361 "kcode-k1-daemon: startup failed: open chat service: safe child failure"
362 );
363 }
364
365 #[test]
366 fn selected_composition_dependencies_and_constructor_are_compatible() {
367 const MANIFEST: &str = include_str!("../Cargo.toml");
368 for selected in [
369 "kcode-k1-access-full-audio = \"0.7.4\"",
370 "kcode-k1-audio-classification = \"0.5.6\"",
371 "kcode-k1-chat-service = \"0.2.0\"",
372 "kcode-k1-codex-adapter = \"0.5.0\"",
373 "kcode-k1-daemon-http-boundary = \"0.1.0\"",
374 "kcode-k1-daemon-provider-config = \"0.1.2\"",
375 "kcode-k1-daemon-vault-unlock = \"0.1.0\"",
376 "kcode-k1-full-audio = \"0.3.6\"",
377 "kcode-k1-groups = \"0.3.1\"",
378 "kcode-k1-http-audio = \"0.1.4\"",
379 "kcode-k1-http-audio-artifacts = \"0.1.0\"",
380 "kcode-k1-http-chat = \"0.1.0\"",
381 "kcode-speaker-v3-analysis = { version = \"0.3.4\", default-features = false, features = [\"adapter-providers\"] }",
382 ] {
383 assert!(MANIFEST.contains(selected));
384 }
385 assert!(!MANIFEST.lines().any(|line| {
386 line.trim_start()
387 .starts_with("kcode-speaker-v3-terra-analysis ")
388 }));
389 fn require_constructor(_: fn(Gemini31Pro, CodexAdapter) -> Analyzer) {}
390 require_constructor(Analyzer::from_codex_adapter);
391 let models = people_models();
392 assert_eq!(
393 models
394 .iter()
395 .map(kcode_k1_http_people::LocalModel::name)
396 .collect::<Vec<_>>(),
397 [
398 "All models — special; includes current and future models",
399 "GPT-5.6 Terra",
400 "GPT-5.6 Sol",
401 "GPT-5.6 Luna",
402 "Gemini 3.1 Pro",
403 ]
404 );
405 }
406}