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