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