Skip to main content

kcode_k1_daemon_lib/
lib.rs

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}