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