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_launch_nodes::K1AccessLaunchNodes;
7use kcode_k1_access_persons::K1AccessPersons;
8use kcode_k1_access_profiles::K1AccessProfiles;
9use kcode_k1_accounting::Accounting;
10use kcode_k1_accounts::K1Accounts;
11use kcode_k1_audio_classification::AudioClassification;
12use kcode_k1_authority_filters::K1AuthorityFilters;
13use kcode_k1_chat_service::K1ChatService;
14use kcode_k1_codex_adapter::Adapter as CodexAdapter;
15use kcode_k1_codex_websearch::Runner as WebSearchRunner;
16use kcode_k1_daemon_files::DaemonFiles;
17use kcode_k1_daemon_http_boundary::{
18    Boundary, PUBLIC_ORIGIN, api_not_found, warn_if_slow, write_readiness,
19};
20use kcode_k1_daemon_provider_config::{
21    CODEX_EXECUTABLE_ENV, audio_access_model, chat_access_model, codex_configs, codex_executable,
22    people_models, persons_access_model, resolve_ffmpeg,
23};
24use kcode_k1_daemon_startup_error::{StartupError, stage};
25use kcode_k1_daemon_vault_unlock::VaultUnlock;
26use kcode_k1_full_audio::K1FullAudio;
27use kcode_k1_groups::K1Groups;
28use kcode_k1_http::{Config as HttpConfig, K1Http};
29use kcode_k1_http_access_context::K1HttpAccessContext;
30use kcode_k1_http_accounts::K1HttpAccounts;
31use kcode_k1_http_people::K1HttpPeople;
32use kcode_k1_http_replay::{ReplayConfig, ReplayWindow};
33use kcode_k1_invites::K1Invites;
34use kcode_k1_launch_nodes::LaunchNodes;
35use kcode_k1_objects::K1Objects;
36use kcode_k1_peering::K1Peering;
37use kcode_k1_persons::K1Persons;
38use kcode_k1_txn_ordering::K1TxnOrdering;
39use kcode_k1_users::K1Users;
40use kcode_k1_vault::{ExposeSecret, K1Vault};
41use kcode_speaker_v3_analysis::Analyzer;
42use std::path::{Path, PathBuf};
43use std::process::ExitCode;
44use std::sync::Arc;
45use std::time::Instant;
46
47const INVITE_LINK_URL: &str = "http://localhost:4321/lib/kcode-k1-ui/*/account.html";
48const GEMINI_API_KEY: &str = "gemini-api-key";
49
50struct Prepared {
51    boundary: Boundary,
52    unused_invites: usize,
53    vault: Arc<K1Vault>,
54}
55
56pub fn run(k1_root: PathBuf) -> ExitCode {
57    let runtime = match tokio::runtime::Builder::new_multi_thread()
58        .enable_all()
59        .build()
60    {
61        Ok(runtime) => runtime,
62        Err(_) => {
63            eprintln!("kcode-k1-daemon: startup failed");
64            return ExitCode::from(1);
65        }
66    };
67    let unlock = match VaultUnlock::prompt() {
68        Ok(unlock) => unlock,
69        Err(_) => {
70            eprintln!("kcode-k1-daemon: startup failed");
71            return ExitCode::from(1);
72        }
73    };
74    runtime.block_on(run_async(k1_root, unlock))
75}
76
77async fn run_async(k1_root: PathBuf, unlock: VaultUnlock) -> ExitCode {
78    let started = Instant::now();
79    let prepared = match startup(k1_root, unlock).await {
80        Ok(prepared) => prepared,
81        Err(error) => {
82            warn_if_slow(started.elapsed(), "error");
83            eprintln!("{error}");
84            return ExitCode::from(1);
85        }
86    };
87    let elapsed = started.elapsed();
88    if write_readiness(prepared.unused_invites).is_err() {
89        warn_if_slow(elapsed, "error");
90        eprintln!("kcode-k1-daemon: startup failed");
91        return ExitCode::from(1);
92    }
93    warn_if_slow(elapsed, "ready");
94    let Prepared {
95        boundary, vault, ..
96    } = prepared;
97    let result = boundary.serve().await;
98    drop(vault);
99    match result {
100        Ok(()) => ExitCode::SUCCESS,
101        Err(()) => {
102            eprintln!("kcode-k1-daemon: listener failed");
103            ExitCode::from(1)
104        }
105    }
106}
107
108async fn startup(k1_root: PathBuf, unlock: VaultUnlock) -> Result<Prepared, StartupError> {
109    let state_root = state_root(&k1_root);
110    let files = DaemonFiles::open(&state_root).map_err(stage("daemon files"))?;
111    let ordering = Arc::new(
112        K1TxnOrdering::open(&state_root.join("ordering")).map_err(stage("transaction ordering"))?,
113    );
114    let peering = Arc::new(
115        K1Peering::open(&state_root.join("peering"), Arc::clone(&ordering))
116            .map_err(stage("peering"))?,
117    );
118    let vault = unlock
119        .open(&state_root, Arc::clone(&ordering), Arc::clone(&peering))
120        .map_err(stage("Vault"))?;
121    let persons = Arc::new(
122        K1Persons::open(
123            &state_root.join("persons"),
124            Arc::clone(&ordering),
125            Arc::clone(&peering),
126        )
127        .map_err(stage("Persons"))?,
128    );
129    let invites = Arc::new(
130        K1Invites::open(
131            &state_root.join("invites"),
132            Arc::clone(&ordering),
133            Arc::clone(&peering),
134        )
135        .map_err(stage("Invites"))?,
136    );
137    let accounts = Arc::new(K1Accounts::open(Arc::clone(&invites)).map_err(stage("Accounts"))?);
138    let users = Arc::new(K1Users::new(Arc::clone(&accounts), Arc::clone(&persons)));
139    let groups = Arc::new(
140        K1Groups::open(
141            &state_root.join("groups"),
142            Arc::clone(&ordering),
143            Arc::clone(&peering),
144        )
145        .map_err(stage("Groups"))?,
146    );
147    let launch_nodes = Arc::new(
148        LaunchNodes::open(&state_root.join("launch-nodes")).map_err(stage("Launch Nodes"))?,
149    );
150    let profiles = Arc::new(
151        K1AccessProfiles::open(
152            &state_root.join("access-profiles"),
153            Arc::clone(&ordering),
154            Arc::clone(&peering),
155        )
156        .map_err(stage("Access Profiles"))?,
157    );
158    let filters = Arc::new(
159        K1AuthorityFilters::open(
160            &state_root.join("authority-filters"),
161            Arc::clone(&ordering),
162            Arc::clone(&peering),
163        )
164        .map_err(stage("Authority Filters"))?,
165    );
166    let gemini_key = vault
167        .secret(GEMINI_API_KEY)
168        .map_err(stage("Gemini API key"))?
169        .ok_or(StartupError::Stage("Gemini API key"))?;
170    let gemini = Gemini31Pro::new(
171        gemini_key.expose_secret().to_owned(),
172        Accounting::new(),
173        std::time::Duration::from_secs(30 * 60),
174    )
175    .map_err(stage("Gemini client"))?;
176    let executable = codex_executable(std::env::var_os(CODEX_EXECUTABLE_ENV));
177    let web_search = WebSearchRunner::new(executable.clone());
178    let working_directory = std::env::current_dir()
179        .map_err(stage("working directory"))?
180        .to_string_lossy()
181        .into_owned();
182    let (audio_config, chat_config) = codex_configs(executable, working_directory);
183    let audio_codex_adapter = CodexAdapter::open(audio_config)
184        .await
185        .map_err(StartupError::CodexAdapter)?;
186    let chat_codex_adapter = audio_codex_adapter
187        .with_config(chat_config)
188        .map_err(StartupError::CodexAdapter)?;
189    let analyzer = Analyzer::from_codex_adapter(gemini, audio_codex_adapter);
190    let objects = Arc::new(
191        K1Objects::open(Arc::clone(&ordering), Arc::clone(&peering)).map_err(stage("Objects"))?,
192    );
193    let classification = Arc::new(
194        AudioClassification::open(
195            &state_root.join("audio-classification-v2"),
196            Arc::clone(&ordering),
197            Arc::clone(&peering),
198            Arc::clone(&objects),
199            analyzer,
200        )
201        .map_err(StartupError::AudioClassification)?,
202    );
203    let ffmpeg = resolve_ffmpeg().map_err(stage("FFmpeg"))?;
204    let full_audio = Arc::new(
205        K1FullAudio::open(ffmpeg, Arc::clone(&objects), Arc::clone(&classification))
206            .map_err(stage("Full Audio"))?,
207    );
208    let access = Arc::new(
209        K1Access::open(
210            &state_root.join("access"),
211            Arc::clone(&ordering),
212            Arc::clone(&peering),
213            Arc::clone(&groups),
214        )
215        .map_err(stage("Access"))?,
216    );
217    let access_launch_nodes = Arc::new(
218        K1AccessLaunchNodes::open(Arc::clone(&access), Arc::clone(&groups), launch_nodes)
219            .map_err(stage("Access Launch Nodes"))?,
220    );
221    let chat = K1ChatService::open(
222        &chat_root(&state_root),
223        &state_root.join("kmap"),
224        Arc::clone(&ordering),
225        Arc::clone(&peering),
226        Arc::clone(&access),
227        Arc::clone(&profiles),
228        chat_codex_adapter,
229        web_search,
230    )
231    .map_err(StartupError::Chat)?;
232    let access_kmap = chat.access_kmap();
233    let access_persons = Arc::new(
234        K1AccessPersons::open(Arc::clone(&access), Arc::clone(&persons))
235            .map_err(stage("Access Persons"))?,
236    );
237    let audio = Arc::new(
238        K1AccessFullAudio::open(Arc::clone(&access), full_audio, classification)
239            .map_err(stage("Access Full Audio"))?,
240    );
241    let chat_context = K1HttpAccessContext::new(Arc::clone(&filters), chat_access_model());
242    let presentation_context = K1HttpAccessContext::new(Arc::clone(&filters), chat_access_model());
243    let launch_context = K1HttpAccessContext::new(Arc::clone(&filters), chat_access_model());
244    let persons_context = K1HttpAccessContext::new(Arc::clone(&filters), persons_access_model());
245    let audio_context = K1HttpAccessContext::new(Arc::clone(&filters), audio_access_model());
246    let replay = ReplayWindow::open(ReplayConfig {
247        epoch_file: files.replay_epoch_path().to_owned(),
248        max_nonces_per_epoch: usize::MAX,
249    })
250    .await
251    .map_err(stage("HTTP replay"))?;
252    let unused_invites = kcode_k1_daemon_invite_stock::reconcile(
253        &invites,
254        files.invite_links_path(),
255        INVITE_LINK_URL,
256    )
257    .map_err(stage("invite stock"))?;
258    if unused_invites < 100 {
259        return Err(StartupError::Stage("minimum invite stock"));
260    }
261    let adapter = K1HttpAccounts::new(
262        Arc::clone(&accounts),
263        Arc::clone(&invites),
264        Arc::clone(&users),
265    );
266    let people_models: Arc<[kcode_k1_http_people::LocalModel]> = Arc::from(people_models());
267    let people = K1HttpPeople::new_with_models(
268        accounts,
269        users,
270        groups,
271        Arc::clone(&profiles),
272        Arc::clone(&filters),
273        people_models,
274    )
275    .map_err(stage("People HTTP"))?;
276    let http = K1Http::new(
277        HttpConfig {
278            server_id: files.server_id().to_owned(),
279            public_origin: PUBLIC_ORIGIN.to_owned(),
280            max_body_bytes: usize::MAX,
281        },
282        replay,
283        adapter.identity_provider(),
284    )
285    .map_err(stage("K1 HTTP"))?;
286    let presentation_routes = kcode_k1_http_access_profile_presentation::authenticated_routes(
287        Arc::clone(&access),
288        Arc::clone(&profiles),
289        presentation_context,
290    );
291    let person_routes = kcode_k1_http_persons::authenticated_routes(
292        access_persons,
293        access,
294        Arc::clone(&profiles),
295        persons_context,
296    )
297    .map_err(stage("Persons HTTP"))?;
298    let launch_routes = kcode_k1_http_launch_nodes::router(
299        access_launch_nodes,
300        access_kmap,
301        Arc::clone(&profiles),
302        launch_context,
303    );
304    let audio_routes = kcode_k1_http_audio::authenticated_routes(
305        Arc::clone(&audio),
306        profiles,
307        audio_context.clone(),
308    );
309    let authenticated = adapter
310        .authenticated_routes()
311        .merge(people.authenticated_routes())
312        .merge(audio_routes)
313        .merge(kcode_k1_http_audio_artifacts::authenticated_routes(
314            audio,
315            audio_context,
316        ))
317        .merge(person_routes)
318        .merge(launch_routes)
319        .merge(kcode_k1_http_chat::router(chat, chat_context))
320        .merge(presentation_routes)
321        .fallback(api_not_found);
322    let api = http.router(
323        adapter.registration_endpoint(),
324        kcode_k1_terms::endpoint(),
325        authenticated,
326    );
327    let boundary = Boundary::bind(api, files.server_id().to_owned())
328        .await
329        .map_err(stage("listener bind"))?;
330    Ok(Prepared {
331        boundary,
332        unused_invites,
333        vault,
334    })
335}
336
337fn state_root(k1_root: &Path) -> PathBuf {
338    k1_root.join("state")
339}
340
341fn chat_root(state_root: &Path) -> PathBuf {
342    state_root.join("chat-v2")
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    #[test]
350    fn public_operation_and_state_roots_are_fixed() {
351        let _: fn(PathBuf) -> ExitCode = run;
352        let root = state_root(Path::new("/trusted/k1"));
353        assert_eq!(root, PathBuf::from("/trusted/k1/state"));
354        assert_eq!(
355            root.join("authority-filters"),
356            PathBuf::from("/trusted/k1/state/authority-filters")
357        );
358        assert_eq!(
359            root.join("launch-nodes"),
360            PathBuf::from("/trusted/k1/state/launch-nodes")
361        );
362        let chat = chat_root(&root);
363        assert_eq!(chat, PathBuf::from("/trusted/k1/state/chat-v2"));
364        assert_ne!(chat, PathBuf::from("/trusted/k1/state/chat"));
365    }
366
367    #[test]
368    fn fixed_provider_key_and_origins_remain_exact_and_distinct() {
369        assert_eq!(GEMINI_API_KEY, "gemini-api-key");
370        assert_eq!(
371            INVITE_LINK_URL,
372            "http://localhost:4321/lib/kcode-k1-ui/*/account.html"
373        );
374        assert_eq!(PUBLIC_ORIGIN, "http://localhost:4450");
375        assert_ne!(INVITE_LINK_URL, PUBLIC_ORIGIN);
376    }
377
378    #[test]
379    fn selected_composition_dependencies_are_current_and_compatible() {
380        kcode_k1_daemon_lib_testkit::verify_manifest(include_str!("../Cargo.toml"));
381        fn require_constructor(_: fn(Gemini31Pro, CodexAdapter) -> Analyzer) {}
382        require_constructor(Analyzer::from_codex_adapter);
383        let models = people_models();
384        assert_eq!(
385            models
386                .iter()
387                .map(kcode_k1_http_people::LocalModel::name)
388                .collect::<Vec<_>>(),
389            [
390                "All models — special; includes current and future models",
391                "GPT-5.6 Terra",
392                "GPT-5.6 Sol",
393                "GPT-5.6 Luna",
394                "Gemini 3.1 Pro",
395            ]
396        );
397    }
398}