1#![doc = include_str!("../Documentation.md")]
2
3use axum::body::{Body, to_bytes};
4use axum::extract::Request;
5use axum::http::header::{CACHE_CONTROL, CONTENT_LENGTH, CONTENT_TYPE, HOST};
6use axum::http::{HeaderValue, StatusCode};
7use axum::middleware::{self, Next};
8use axum::response::Response;
9use axum::routing::get;
10use axum::{Json, Router};
11use kcode_gemini_3_1_pro::Gemini31Pro;
12use kcode_k1_access::K1Access;
13use kcode_k1_access_full_audio::K1AccessFullAudio;
14use kcode_k1_access_persons::K1AccessPersons;
15use kcode_k1_access_profiles::K1AccessProfiles;
16use kcode_k1_accounting::Accounting;
17use kcode_k1_accounts::K1Accounts;
18use kcode_k1_audio_classification::AudioClassification;
19use kcode_k1_chat_service::{K1ChatService, current_time_tool};
20use kcode_k1_codex_adapter::{
21 Adapter as CodexAdapter, Config as CodexConfig, Error as CodexAdapterError,
22};
23use kcode_k1_daemon_files::DaemonFiles;
24use kcode_k1_full_audio::K1FullAudio;
25use kcode_k1_groups::{K1Groups, ModelId};
26use kcode_k1_http::{Config as HttpConfig, K1Http};
27use kcode_k1_http_accounts::K1HttpAccounts;
28use kcode_k1_http_people::K1HttpPeople;
29use kcode_k1_http_replay::{ReplayConfig, ReplayWindow};
30use kcode_k1_invites::K1Invites;
31use kcode_k1_objects::K1Objects;
32use kcode_k1_peering::K1Peering;
33use kcode_k1_persons::K1Persons;
34use kcode_k1_txn_ordering::K1TxnOrdering;
35use kcode_k1_users::K1Users;
36use kcode_k1_vault::{ExposeSecret, K1Vault, SecretString};
37use kcode_speaker_v3_analysis::Analyzer;
38use serde::Serialize;
39use serde_json::Value;
40use std::ffi::OsString;
41use std::fmt;
42use std::io::Write as _;
43use std::path::{Path, PathBuf};
44use std::process::ExitCode;
45use std::sync::Arc;
46use std::time::{Duration, Instant};
47use tokio::net::TcpListener;
48use tokio::signal::unix::{Signal, SignalKind, signal};
49
50const LISTEN_ADDRESS: &str = "127.0.0.1:4450";
51const PUBLIC_ORIGIN: &str = "http://localhost:4450";
52const INVITE_LINK_URL: &str = "http://localhost:4321/lib/kcode-k1-ui/*/account.html";
53const AUTHORITY: &str = "localhost:4450";
54const STARTUP_BOUND: Duration = Duration::from_millis(100);
55const GEMINI_OPERATION_TIMEOUT: Duration = Duration::from_secs(30 * 60);
56const GEMINI_API_KEY: &str = "gemini-api-key";
57const CODEX_EXECUTABLE_ENV: &str = "K1_CODEX_EXECUTABLE";
58const AUDIO_CODEX_MODEL: &str = "gpt-5.6-terra";
59const CHAT_CODEX_MODEL: &str = "gpt-5.6-sol";
60const GEMINI_MODEL_BYTES: [u8; 32] = *b"gemini-3.1-pro-preview..........";
61const TERRA_MODEL_BYTES: [u8; 32] = *b"gpt-5.6-terra...................";
62const API_OPERATION: &str = "serve API request";
63
64#[derive(Clone, Serialize)]
65struct PublicConfig {
66 protocol: &'static str,
67 server_id: String,
68 public_origin: &'static str,
69}
70
71#[derive(Serialize)]
72struct Ready {
73 event: &'static str,
74 public_origin: &'static str,
75 unused_invites: usize,
76}
77
78struct Prepared {
79 app: Router,
80 listener: TcpListener,
81 signals: Signals,
82 unused_invites: usize,
83 vault: Arc<K1Vault>,
84 chat: K1ChatService,
85}
86
87struct Signals {
88 interrupt: Signal,
89 terminate: Signal,
90}
91
92enum StartupError {
93 Generic,
94 CodexAdapter(CodexAdapterError),
95 Chat(String),
96}
97
98impl From<()> for StartupError {
99 fn from((): ()) -> Self {
100 Self::Generic
101 }
102}
103
104impl fmt::Display for StartupError {
105 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
106 match self {
107 Self::Generic => formatter.write_str("kcode-k1-daemon: startup failed"),
108 Self::CodexAdapter(error) => {
109 write!(formatter, "kcode-k1-daemon: startup failed: {error}")
110 }
111 Self::Chat(child) => {
112 write!(formatter, "kcode-k1-daemon: startup failed: {child}")
113 }
114 }
115 }
116}
117
118pub fn run(k1_root: PathBuf) -> ExitCode {
119 let runtime = match tokio::runtime::Builder::new_multi_thread()
120 .enable_all()
121 .build()
122 {
123 Ok(runtime) => runtime,
124 Err(_) => {
125 eprintln!("kcode-k1-daemon: startup failed");
126 return ExitCode::from(1);
127 }
128 };
129 let passphrase = match rpassword::prompt_password("Unlock K1 vault: ") {
130 Ok(passphrase) => match protect_passphrase(passphrase) {
131 Ok(passphrase) => passphrase,
132 Err(()) => {
133 eprintln!("kcode-k1-daemon: startup failed");
134 return ExitCode::from(1);
135 }
136 },
137 Err(_) => {
138 eprintln!("kcode-k1-daemon: startup failed");
139 return ExitCode::from(1);
140 }
141 };
142 runtime.block_on(run_async(k1_root, passphrase))
143}
144
145fn protect_passphrase(passphrase: String) -> Result<SecretString, ()> {
146 (!passphrase.is_empty())
147 .then(|| SecretString::from(passphrase))
148 .ok_or(())
149}
150
151async fn run_async(k1_root: PathBuf, passphrase: SecretString) -> ExitCode {
152 let started = Instant::now();
153 let prepared = match startup(k1_root, passphrase).await {
154 Ok(prepared) => prepared,
155 Err(error) => {
156 warn_if_slow(started.elapsed(), "error");
157 eprintln!("{error}");
158 return ExitCode::from(1);
159 }
160 };
161 let elapsed = started.elapsed();
162 if write_readiness(prepared.unused_invites).is_err() {
163 warn_if_slow(elapsed, "error");
164 eprintln!("kcode-k1-daemon: startup failed");
165 return ExitCode::from(1);
166 }
167 warn_if_slow(elapsed, "ready");
168 let Prepared {
169 app,
170 listener,
171 signals,
172 vault,
173 chat,
174 ..
175 } = prepared;
176 let result = axum::serve(listener, app)
177 .with_graceful_shutdown(signals.wait())
178 .await;
179 drop((chat, vault));
180 match result {
181 Ok(()) => ExitCode::SUCCESS,
182 Err(_) => {
183 eprintln!("kcode-k1-daemon: listener failed");
184 ExitCode::from(1)
185 }
186 }
187}
188
189async fn startup(k1_root: PathBuf, passphrase: SecretString) -> Result<Prepared, StartupError> {
190 let state_root = state_root(&k1_root);
191 let files = DaemonFiles::open(&state_root).map_err(|_| ())?;
192 let ordering = Arc::new(K1TxnOrdering::open(&state_root.join("ordering")).map_err(|_| ())?);
193 let peering = Arc::new(
194 K1Peering::open(&state_root.join("peering"), Arc::clone(&ordering)).map_err(|_| ())?,
195 );
196 let vault = open_vault(
197 &state_root,
198 passphrase,
199 Arc::clone(&ordering),
200 Arc::clone(&peering),
201 )?;
202 let persons = Arc::new(
203 K1Persons::open(
204 &state_root.join("persons"),
205 Arc::clone(&ordering),
206 Arc::clone(&peering),
207 )
208 .map_err(|_| ())?,
209 );
210 let invites = Arc::new(
211 K1Invites::open(
212 &state_root.join("invites"),
213 Arc::clone(&ordering),
214 Arc::clone(&peering),
215 )
216 .map_err(|_| ())?,
217 );
218 let accounts = Arc::new(K1Accounts::open(Arc::clone(&invites)).map_err(|_| ())?);
219 let users = Arc::new(K1Users::new(Arc::clone(&accounts), Arc::clone(&persons)));
220 let groups = Arc::new(
221 K1Groups::open(
222 &state_root.join("groups"),
223 Arc::clone(&ordering),
224 Arc::clone(&peering),
225 )
226 .map_err(|_| ())?,
227 );
228 let profiles = Arc::new(
229 K1AccessProfiles::open(
230 &state_root.join("access-profiles"),
231 Arc::clone(&ordering),
232 Arc::clone(&peering),
233 )
234 .map_err(|_| ())?,
235 );
236 let gemini_key = vault.secret(GEMINI_API_KEY).map_err(|_| ())?.ok_or(())?;
237 let gemini = Gemini31Pro::new(
238 gemini_key.expose_secret().to_owned(),
239 Accounting::new(),
240 GEMINI_OPERATION_TIMEOUT,
241 )
242 .map_err(|_| ())?;
243 let executable = codex_executable(std::env::var_os(CODEX_EXECUTABLE_ENV));
244 let working_directory = std::env::current_dir()
245 .map_err(|_| ())?
246 .to_string_lossy()
247 .into_owned();
248 let (audio_config, chat_config) = codex_configs(executable, working_directory);
249 let audio_codex_adapter = CodexAdapter::open(audio_config)
250 .await
251 .map_err(StartupError::CodexAdapter)?;
252 let chat_codex_adapter = audio_codex_adapter
253 .with_config(chat_config)
254 .map_err(StartupError::CodexAdapter)?;
255 let analyzer = Analyzer::from_codex_adapter(gemini, audio_codex_adapter);
256 let objects =
257 Arc::new(K1Objects::open(Arc::clone(&ordering), Arc::clone(&peering)).map_err(|_| ())?);
258 let classification = Arc::new(
259 AudioClassification::open(
260 &state_root.join("audio-classification"),
261 Arc::clone(&ordering),
262 Arc::clone(&peering),
263 Arc::clone(&objects),
264 analyzer,
265 )
266 .map_err(|_| ())?,
267 );
268 let full_audio = Arc::new(
269 K1FullAudio::open(
270 resolve_ffmpeg()?,
271 Arc::clone(&objects),
272 Arc::clone(&classification),
273 )
274 .map_err(|_| ())?,
275 );
276 let access = Arc::new(
277 K1Access::open(
278 &state_root.join("access"),
279 Arc::clone(&ordering),
280 Arc::clone(&peering),
281 Arc::clone(&groups),
282 )
283 .map_err(|_| ())?,
284 );
285 let chat = K1ChatService::open(
286 &state_root.join("chat"),
287 Arc::clone(&ordering),
288 Arc::clone(&peering),
289 Arc::clone(&access),
290 Arc::clone(&profiles),
291 chat_codex_adapter,
292 )
293 .map_err(StartupError::Chat)?;
294 let access_persons = Arc::new(
295 K1AccessPersons::open(
296 Arc::clone(&access),
297 Arc::clone(&profiles),
298 Arc::clone(&persons),
299 )
300 .map_err(|_| ())?,
301 );
302 let audio = Arc::new(
303 K1AccessFullAudio::open_for_models(
304 Arc::clone(&access),
305 Arc::clone(&profiles),
306 full_audio,
307 classification,
308 Arc::clone(&groups),
309 audio_models().to_vec(),
310 )
311 .map_err(|_| ())?,
312 );
313 let replay = ReplayWindow::open(ReplayConfig {
314 epoch_file: files.replay_epoch_path().to_owned(),
315 max_nonces_per_epoch: usize::MAX,
316 })
317 .await
318 .map_err(|_| ())?;
319 let unused_invites = kcode_k1_daemon_invite_stock::reconcile(
320 &invites,
321 files.invite_links_path(),
322 INVITE_LINK_URL,
323 )
324 .map_err(|_| ())?;
325 if unused_invites < 100 {
326 return Err(().into());
327 }
328 let adapter = K1HttpAccounts::new(
329 Arc::clone(&accounts),
330 Arc::clone(&invites),
331 Arc::clone(&users),
332 );
333 let people = K1HttpPeople::new(accounts, users, groups, profiles);
334 let http = K1Http::new(
335 HttpConfig {
336 server_id: files.server_id().to_owned(),
337 public_origin: PUBLIC_ORIGIN.to_owned(),
338 max_body_bytes: usize::MAX,
339 },
340 replay,
341 adapter.identity_provider(),
342 )
343 .map_err(|_| ())?;
344 let person_routes =
345 kcode_k1_http_persons::authenticated_routes(access_persons, access, audio_models()[0])
346 .map_err(|_| ())?;
347 let authenticated = adapter
348 .authenticated_routes()
349 .merge(people.authenticated_routes())
350 .merge(kcode_k1_http_audio::authenticated_routes(audio))
351 .merge(person_routes)
352 .fallback(api_not_found);
353 let api = http
354 .router(
355 adapter.registration_endpoint(),
356 kcode_k1_terms::endpoint(),
357 authenticated,
358 )
359 .layer(middleware::from_fn(contextualize_api_error));
360 let config = PublicConfig {
361 protocol: "K1-HTTP-1",
362 server_id: files.server_id().to_owned(),
363 public_origin: PUBLIC_ORIGIN,
364 };
365 let config_route = get(move || {
366 let config = config.clone();
367 async move { ([(CACHE_CONTROL, "no-store")], Json(config)) }
368 });
369 let app = Router::new()
370 .route("/config.json", config_route)
371 .merge(api)
372 .layer(middleware::from_fn(require_authority));
373 Ok(Prepared {
374 app,
375 listener: TcpListener::bind(LISTEN_ADDRESS).await.map_err(|_| ())?,
376 signals: Signals::install()?,
377 unused_invites,
378 vault,
379 chat,
380 })
381}
382
383fn codex_configs(executable: PathBuf, working_directory: String) -> (CodexConfig, CodexConfig) {
384 let audio = CodexConfig {
385 executable: executable.clone(),
386 working_directory: working_directory.clone(),
387 model: AUDIO_CODEX_MODEL.to_owned(),
388 reasoning_effort: Some("medium".to_owned()),
389 base_instructions: String::new(),
390 tools: kcode_speaker_v3_terra_analysis::codex_adapter_tools(),
391 };
392 let chat = CodexConfig {
393 executable,
394 working_directory,
395 model: CHAT_CODEX_MODEL.to_owned(),
396 reasoning_effort: Some("xhigh".to_owned()),
397 base_instructions: String::new(),
398 tools: vec![current_time_tool()],
399 };
400 (audio, chat)
401}
402
403fn codex_executable(executable_override: Option<OsString>) -> PathBuf {
404 executable_override.map_or_else(|| PathBuf::from("codex-safe"), PathBuf::from)
405}
406
407fn audio_models() -> [ModelId; 2] {
408 [
409 ModelId::from_bytes(GEMINI_MODEL_BYTES),
410 ModelId::from_bytes(TERRA_MODEL_BYTES),
411 ]
412}
413
414fn resolve_ffmpeg() -> Result<PathBuf, ()> {
415 let path = std::env::var_os("PATH").ok_or(())?;
416 resolve_executable("ffmpeg", std::env::split_paths(&path))
417}
418
419fn resolve_executable(name: &str, paths: impl IntoIterator<Item = PathBuf>) -> Result<PathBuf, ()> {
420 paths
421 .into_iter()
422 .find_map(|directory| {
423 let candidate = directory.join(name);
424 executable(&candidate)
425 .then(|| std::fs::canonicalize(candidate).ok())
426 .flatten()
427 .filter(|path| path.is_absolute())
428 })
429 .ok_or(())
430}
431
432#[cfg(unix)]
433fn executable(path: &Path) -> bool {
434 use std::os::unix::fs::PermissionsExt as _;
435 std::fs::metadata(path)
436 .is_ok_and(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
437}
438
439#[cfg(not(unix))]
440fn executable(path: &Path) -> bool {
441 std::fs::metadata(path).is_ok_and(|metadata| metadata.is_file())
442}
443
444fn open_vault(
445 state_root: &Path,
446 passphrase: SecretString,
447 ordering: Arc<K1TxnOrdering>,
448 peering: Arc<K1Peering>,
449) -> Result<Arc<K1Vault>, ()> {
450 K1Vault::open(&state_root.join("vault"), passphrase, ordering, peering)
451 .map(Arc::new)
452 .map_err(|_| ())
453}
454
455fn state_root(k1_root: &Path) -> PathBuf {
456 k1_root.join("state")
457}
458
459async fn api_not_found() -> Response {
460 json_error(
461 StatusCode::NOT_FOUND,
462 "not_found",
463 "authenticated API route not found",
464 )
465}
466
467async fn contextualize_api_error(request: Request, next: Next) -> Response {
468 let response = next.run(request).await;
469 if !(response.status().is_client_error() || response.status().is_server_error()) {
470 return response;
471 }
472 let (mut parts, body) = response.into_parts();
473 let bytes = match to_bytes(body, usize::MAX).await {
474 Ok(bytes) => bytes,
475 Err(_) => return Response::from_parts(parts, Body::empty()),
476 };
477 let Some(contextualized) = contextualize_error_body(&bytes) else {
478 return Response::from_parts(parts, Body::from(bytes));
479 };
480 parts.headers.remove(CONTENT_LENGTH);
481 Response::from_parts(parts, Body::from(contextualized))
482}
483
484fn contextualize_error_body(bytes: &[u8]) -> Option<Vec<u8>> {
485 let mut payload: Value = serde_json::from_slice(bytes).ok()?;
486 let object = payload.as_object_mut()?;
487 let code = object.get("error")?.as_str()?.to_owned();
488 let source = object
489 .get("message")
490 .and_then(Value::as_str)
491 .map(str::to_owned)
492 .unwrap_or_else(|| format!("error code {code}"));
493 object.insert(
494 "message".to_owned(),
495 Value::String(format!("{API_OPERATION}: {source}")),
496 );
497 Some(payload.to_string().into_bytes())
498}
499
500async fn require_authority(request: Request, next: Next) -> Response {
501 let mut values = request.headers().get_all(HOST).iter();
502 if values
503 .next()
504 .is_some_and(|value| value.as_bytes() == AUTHORITY.as_bytes())
505 && values.next().is_none()
506 {
507 next.run(request).await
508 } else {
509 json_error(
510 StatusCode::MISDIRECTED_REQUEST,
511 "invalid_request_authority",
512 "validate request authority: request authority is invalid",
513 )
514 }
515}
516
517fn json_error(status: StatusCode, code: &'static str, message: &'static str) -> Response {
518 let mut response = Response::new(Body::from(
519 serde_json::json!({"error": code, "message": message}).to_string(),
520 ));
521 *response.status_mut() = status;
522 response
523 .headers_mut()
524 .insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
525 response
526 .headers_mut()
527 .insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
528 response
529}
530
531fn write_readiness(unused_invites: usize) -> Result<(), ()> {
532 let stdout = std::io::stdout();
533 let mut output = stdout.lock();
534 serde_json::to_writer(
535 &mut output,
536 &Ready {
537 event: "ready",
538 public_origin: PUBLIC_ORIGIN,
539 unused_invites,
540 },
541 )
542 .map_err(|_| ())?;
543 output.write_all(b"\n").map_err(|_| ())?;
544 output.flush().map_err(|_| ())
545}
546
547fn warn_if_slow(elapsed: Duration, outcome: &'static str) {
548 if elapsed > STARTUP_BOUND {
549 eprintln!(
550 "{{\"module\":\"kcode-k1-daemon\",\"operation\":\"startup\",\"elapsed_us\":{},\"outcome\":\"{outcome}\"}}",
551 elapsed.as_micros()
552 );
553 }
554}
555
556impl Signals {
557 fn install() -> Result<Self, ()> {
558 Ok(Self {
559 interrupt: signal(SignalKind::interrupt()).map_err(|_| ())?,
560 terminate: signal(SignalKind::terminate()).map_err(|_| ())?,
561 })
562 }
563
564 async fn wait(mut self) {
565 tokio::select! {
566 _ = self.interrupt.recv() => {}
567 _ = self.terminate.recv() => {}
568 }
569 }
570}
571
572#[cfg(test)]
573mod tests {
574 use super::*;
575
576 #[test]
577 fn public_operation_accepts_only_the_state_root() {
578 let _: fn(PathBuf) -> ExitCode = run;
579 }
580
581 #[test]
582 fn accepted_passphrase_boundary_is_strict_and_protected() {
583 assert!(protect_passphrase(String::new()).is_err());
584 let text = "conspicuous-fake-passphrase-never-real";
585 let protected = protect_passphrase(text.to_owned()).unwrap();
586 assert!(!format!("{protected:?}").contains(text));
587 }
588
589 #[test]
590 fn vault_composition_persists_at_the_fixed_path() {
591 let root =
592 std::env::temp_dir().join(format!("kcode-k1-daemon-vault-test-{}", std::process::id()));
593 let _ = std::fs::remove_dir_all(&root);
594 let state = state_root(&root);
595 assert_eq!(state.join("vault"), root.join("state/vault"));
596 let parts = || {
597 let ordering = Arc::new(K1TxnOrdering::open(&state.join("ordering")).unwrap());
598 let peering =
599 Arc::new(K1Peering::open(&state.join("peering"), ordering.clone()).unwrap());
600 (ordering, peering)
601 };
602 let password = || SecretString::from("fake-test-password-never-real");
603 let (ordering, peering) = parts();
604 let vault = open_vault(&state, password(), ordering.clone(), peering.clone()).unwrap();
605 vault
606 .set(
607 "fake-provider-secret",
608 SecretString::from("conspicuous-fake-value-never-real"),
609 )
610 .unwrap();
611 drop((vault, peering, ordering));
612 let (ordering, peering) = parts();
613 let vault = open_vault(&state, password(), ordering.clone(), peering.clone()).unwrap();
614 drop((vault, peering, ordering));
615 let (ordering, peering) = parts();
616 assert!(
617 open_vault(
618 &state,
619 SecretString::from("wrong-fake-password-never-real"),
620 ordering,
621 peering
622 )
623 .is_err()
624 );
625 std::fs::remove_dir_all(root).unwrap();
626 }
627
628 #[test]
629 fn audio_model_ids_are_fixed_distinct_and_in_order() {
630 assert_eq!(GEMINI_MODEL_BYTES, *b"gemini-3.1-pro-preview..........");
631 assert_eq!(TERRA_MODEL_BYTES, *b"gpt-5.6-terra...................");
632 assert_eq!(GEMINI_MODEL_BYTES.len(), 32);
633 assert_eq!(TERRA_MODEL_BYTES.len(), 32);
634 let models = audio_models();
635 assert_eq!(models[0].as_bytes(), &GEMINI_MODEL_BYTES);
636 assert_eq!(models[1].as_bytes(), &TERRA_MODEL_BYTES);
637 assert_ne!(models[0], models[1]);
638 }
639
640 #[test]
641 fn only_the_fixed_gemini_vault_key_is_selected() {
642 assert_eq!(GEMINI_API_KEY, "gemini-api-key");
643 }
644
645 #[test]
646 fn codex_executable_override_and_default_are_exact_without_environment_mutation() {
647 assert_eq!(CODEX_EXECUTABLE_ENV, "K1_CODEX_EXECUTABLE");
648 assert_eq!(codex_executable(None), PathBuf::from("codex-safe"));
649 let selected = codex_executable(Some(OsString::from("/trusted/bin/k1-codex")));
650 assert_eq!(selected, PathBuf::from("/trusted/bin/k1-codex"));
651 }
652
653 #[test]
654 fn audio_and_chat_codex_configs_are_exact_and_isolated_without_process_io() {
655 let (audio, chat) = codex_configs(
656 PathBuf::from("/trusted/bin/k1-codex"),
657 "/trusted/current-directory".to_owned(),
658 );
659 assert_eq!(audio.executable, PathBuf::from("/trusted/bin/k1-codex"));
660 assert_eq!(chat.executable, PathBuf::from("/trusted/bin/k1-codex"));
661 assert_eq!(audio.executable, chat.executable);
662 assert_eq!(audio.working_directory, "/trusted/current-directory");
663 assert_eq!(chat.working_directory, "/trusted/current-directory");
664 assert_eq!(audio.working_directory, chat.working_directory);
665 assert_eq!(audio.model, "gpt-5.6-terra");
666 assert_eq!(chat.model, "gpt-5.6-sol");
667 assert_eq!(audio.reasoning_effort.as_deref(), Some("medium"));
668 assert_eq!(chat.reasoning_effort.as_deref(), Some("xhigh"));
669 assert!(audio.base_instructions.is_empty());
670 assert!(chat.base_instructions.is_empty());
671 assert_eq!(
672 audio
673 .tools
674 .iter()
675 .map(|tool| tool.name.as_str())
676 .collect::<Vec<_>>(),
677 ["record_speaker_labels", "record_speaker_analysis"]
678 );
679 assert_eq!(
680 chat.tools
681 .iter()
682 .map(|tool| tool.name.as_str())
683 .collect::<Vec<_>>(),
684 ["CurrentTime"]
685 );
686 }
687
688 #[test]
689 fn startup_error_rendering_includes_safe_adapter_display_but_not_diagnostics() {
690 assert_eq!(
691 StartupError::from(()).to_string(),
692 "kcode-k1-daemon: startup failed"
693 );
694 let error = CodexAdapterError {
695 kind: kcode_k1_codex_adapter::ErrorKind::Unavailable,
696 message: "safe adapter display".to_owned(),
697 diagnostics: b"RAW_SECRET_DIAGNOSTIC".to_vec(),
698 };
699 let rendered = StartupError::CodexAdapter(error).to_string();
700 assert_eq!(
701 rendered,
702 "kcode-k1-daemon: startup failed: safe adapter display"
703 );
704 assert!(!rendered.contains("RAW_SECRET_DIAGNOSTIC"));
705 }
706
707 #[test]
708 fn chat_startup_error_rendering_preserves_the_safe_child_message() {
709 let rendered =
710 StartupError::Chat("open chat service: safe child failure".to_owned()).to_string();
711 assert_eq!(
712 rendered,
713 "kcode-k1-daemon: startup failed: open chat service: safe child failure"
714 );
715 }
716
717 #[test]
718 fn adapter_only_dependency_and_constructor_identity_are_compatible() {
719 const MANIFEST: &str = include_str!("../Cargo.toml");
720 for selected in [
721 "kcode-k1-access-full-audio = \"0.7.3\"",
722 "kcode-k1-audio-classification = \"0.5.5\"",
723 "kcode-k1-chat-service = \"0.1.0\"",
724 "kcode-k1-codex-adapter = \"0.4.1\"",
725 "kcode-k1-full-audio = \"0.3.6\"",
726 "kcode-k1-http-audio = \"0.1.4\"",
727 "kcode-speaker-v3-analysis = { version = \"0.3.2\", default-features = false, features = [\"adapter-providers\"] }",
728 "kcode-speaker-v3-terra-analysis = { version = \"0.2.1\", default-features = false, features = [\"adapter\"] }",
729 ] {
730 assert!(MANIFEST.contains(selected));
731 }
732 assert!(!MANIFEST.contains("kcode-codex-terra"));
733
734 fn require_constructor(_: fn(Gemini31Pro, CodexAdapter) -> Analyzer) {}
735 require_constructor(Analyzer::from_codex_adapter);
736 }
737
738 #[cfg(unix)]
739 #[test]
740 fn executable_resolver_returns_an_absolute_path_for_a_fake_ffmpeg() {
741 use std::os::unix::fs::PermissionsExt as _;
742 let root = std::env::temp_dir().join(format!(
743 "kcode-k1-daemon-ffmpeg-test-{}",
744 std::process::id()
745 ));
746 let _ = std::fs::remove_dir_all(&root);
747 std::fs::create_dir(&root).unwrap();
748 let fake = root.join("ffmpeg");
749 std::fs::write(&fake, b"#!/bin/sh\nexit 0\n").unwrap();
750 std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o700)).unwrap();
751 assert_eq!(
752 resolve_executable("ffmpeg", [root.clone()]).unwrap(),
753 std::fs::canonicalize(&fake).unwrap()
754 );
755 std::fs::remove_dir_all(root).unwrap();
756 }
757
758 #[test]
759 fn invite_link_and_backend_origins_remain_distinct() {
760 assert_eq!(
761 INVITE_LINK_URL,
762 "http://localhost:4321/lib/kcode-k1-ui/*/account.html"
763 );
764 assert_eq!(PUBLIC_ORIGIN, "http://localhost:4450");
765 assert_ne!(INVITE_LINK_URL, PUBLIC_ORIGIN);
766 }
767
768 #[test]
769 fn existing_child_message_is_preserved_under_daemon_context() {
770 let body = contextualize_error_body(
771 br#"{"error":"group_failed","message":"load group: child failure","detail":7}"#,
772 )
773 .unwrap();
774 let payload: Value = serde_json::from_slice(&body).unwrap();
775 assert_eq!(payload["error"], "group_failed");
776 assert_eq!(payload["detail"], 7);
777 assert_eq!(
778 payload["message"],
779 "serve API request: load group: child failure"
780 );
781 }
782
783 #[test]
784 fn missing_child_message_is_derived_from_stable_code() {
785 let body = contextualize_error_body(br#"{"error":"invalid_signature"}"#).unwrap();
786 let payload: Value = serde_json::from_slice(&body).unwrap();
787 assert_eq!(payload["error"], "invalid_signature");
788 assert_eq!(
789 payload["message"],
790 "serve API request: error code invalid_signature"
791 );
792 }
793
794 #[test]
795 fn supplied_root_maps_only_to_state() {
796 assert_eq!(
797 state_root(Path::new("/trusted/k1")),
798 PathBuf::from("/trusted/k1/state")
799 );
800 }
801}