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