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_persons = Arc::new(
230 K1AccessPersons::open(Arc::clone(&access), Arc::clone(&persons))
231 .map_err(stage("Access Persons"))?,
232 );
233 let audio = Arc::new(
234 K1AccessFullAudio::open(Arc::clone(&access), full_audio, classification)
235 .map_err(stage("Access Full Audio"))?,
236 );
237 let chat_context = K1HttpAccessContext::new(Arc::clone(&filters), chat_access_model());
238 let presentation_context = K1HttpAccessContext::new(Arc::clone(&filters), chat_access_model());
239 let launch_context = K1HttpAccessContext::new(Arc::clone(&filters), chat_access_model());
240 let persons_context = K1HttpAccessContext::new(Arc::clone(&filters), persons_access_model());
241 let audio_context = K1HttpAccessContext::new(Arc::clone(&filters), audio_access_model());
242 let replay = ReplayWindow::open(ReplayConfig {
243 epoch_file: files.replay_epoch_path().to_owned(),
244 max_nonces_per_epoch: usize::MAX,
245 })
246 .await
247 .map_err(stage("HTTP replay"))?;
248 let unused_invites = kcode_k1_daemon_invite_stock::reconcile(
249 &invites,
250 files.invite_links_path(),
251 INVITE_LINK_URL,
252 )
253 .map_err(stage("invite stock"))?;
254 if unused_invites < 100 {
255 return Err(StartupError::Stage("minimum invite stock"));
256 }
257 let adapter = K1HttpAccounts::new(
258 Arc::clone(&accounts),
259 Arc::clone(&invites),
260 Arc::clone(&users),
261 );
262 let people_models: Arc<[kcode_k1_http_people::LocalModel]> = Arc::from(people_models());
263 let people = K1HttpPeople::new_with_models(
264 accounts,
265 users,
266 groups,
267 Arc::clone(&profiles),
268 Arc::clone(&filters),
269 people_models,
270 )
271 .map_err(stage("People HTTP"))?;
272 let http = K1Http::new(
273 HttpConfig {
274 server_id: files.server_id().to_owned(),
275 public_origin: PUBLIC_ORIGIN.to_owned(),
276 max_body_bytes: usize::MAX,
277 },
278 replay,
279 adapter.identity_provider(),
280 )
281 .map_err(stage("K1 HTTP"))?;
282 let presentation_routes = kcode_k1_http_access_profile_presentation::authenticated_routes(
283 Arc::clone(&access),
284 Arc::clone(&profiles),
285 presentation_context,
286 );
287 let person_routes = kcode_k1_http_persons::authenticated_routes(
288 access_persons,
289 access,
290 Arc::clone(&profiles),
291 persons_context,
292 )
293 .map_err(stage("Persons HTTP"))?;
294 let launch_routes = kcode_k1_http_launch_nodes::router(
295 access_launch_nodes,
296 Arc::clone(&profiles),
297 launch_context,
298 );
299 let audio_routes = kcode_k1_http_audio::authenticated_routes(
300 Arc::clone(&audio),
301 profiles,
302 audio_context.clone(),
303 );
304 let authenticated = adapter
305 .authenticated_routes()
306 .merge(people.authenticated_routes())
307 .merge(audio_routes)
308 .merge(kcode_k1_http_audio_artifacts::authenticated_routes(
309 audio,
310 audio_context,
311 ))
312 .merge(person_routes)
313 .merge(launch_routes)
314 .merge(kcode_k1_http_chat::router(chat, chat_context))
315 .merge(presentation_routes)
316 .fallback(api_not_found);
317 let api = http.router(
318 adapter.registration_endpoint(),
319 kcode_k1_terms::endpoint(),
320 authenticated,
321 );
322 let boundary = Boundary::bind(api, files.server_id().to_owned())
323 .await
324 .map_err(stage("listener bind"))?;
325 Ok(Prepared {
326 boundary,
327 unused_invites,
328 vault,
329 })
330}
331
332fn state_root(k1_root: &Path) -> PathBuf {
333 k1_root.join("state")
334}
335
336fn chat_root(state_root: &Path) -> PathBuf {
337 state_root.join("chat-v2")
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343
344 #[test]
345 fn public_operation_and_state_roots_are_fixed() {
346 let _: fn(PathBuf) -> ExitCode = run;
347 let root = state_root(Path::new("/trusted/k1"));
348 assert_eq!(root, PathBuf::from("/trusted/k1/state"));
349 assert_eq!(
350 root.join("authority-filters"),
351 PathBuf::from("/trusted/k1/state/authority-filters")
352 );
353 assert_eq!(
354 root.join("launch-nodes"),
355 PathBuf::from("/trusted/k1/state/launch-nodes")
356 );
357 let chat = chat_root(&root);
358 assert_eq!(chat, PathBuf::from("/trusted/k1/state/chat-v2"));
359 assert_ne!(chat, PathBuf::from("/trusted/k1/state/chat"));
360 }
361
362 #[test]
363 fn fixed_provider_key_and_origins_remain_exact_and_distinct() {
364 assert_eq!(GEMINI_API_KEY, "gemini-api-key");
365 assert_eq!(
366 INVITE_LINK_URL,
367 "http://localhost:4321/lib/kcode-k1-ui/*/account.html"
368 );
369 assert_eq!(PUBLIC_ORIGIN, "http://localhost:4450");
370 assert_ne!(INVITE_LINK_URL, PUBLIC_ORIGIN);
371 }
372
373 #[test]
374 fn selected_composition_dependencies_are_current_and_compatible() {
375 kcode_k1_daemon_lib_testkit::verify_manifest(include_str!("../Cargo.toml"));
376 fn require_constructor(_: fn(Gemini31Pro, CodexAdapter) -> Analyzer) {}
377 require_constructor(Analyzer::from_codex_adapter);
378 let models = people_models();
379 assert_eq!(
380 models
381 .iter()
382 .map(kcode_k1_http_people::LocalModel::name)
383 .collect::<Vec<_>>(),
384 [
385 "All models — special; includes current and future models",
386 "GPT-5.6 Terra",
387 "GPT-5.6 Sol",
388 "GPT-5.6 Luna",
389 "Gemini 3.1 Pro",
390 ]
391 );
392 }
393}