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, 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}