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}