Skip to main content

kcode_k1_daemon_lib/
lib.rs

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}