Skip to main content

harness_codex/
lib.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3#![forbid(unsafe_code)]
4
5//! Codex harness edge and isolated configuration planning.
6
7mod app_server;
8#[cfg(unix)]
9mod app_server_process;
10mod app_server_runtime;
11#[cfg(unix)]
12mod s08_owned_child_turn;
13mod sse;
14mod wire;
15
16pub use app_server::{
17    AppServerAuthMode, AppServerRpc, AppServerTransport, CURRENT_DESKTOP_APP_VERSION,
18    CURRENT_DESKTOP_BUILD, CURRENT_DESKTOP_CODEX_VERSION, CURRENT_STANDALONE_CLI_VERSION,
19    CapabilityInputError, CodexAppServerCapabilityReceipt, CodexAppServerProbe, CodexCliVersion,
20    CodexSubscriptionUnavailableReason, InstalledCodexCli, InstalledCodexDesktop,
21    SUPPORTED_DISCOVERY_QUOTA_RPCS,
22};
23#[cfg(unix)]
24pub use app_server_process::{
25    APP_SERVER_DISCOVERY_SCHEMA_VERSION, ActiveCodexTurn, AppServerDiscoveryReceipt,
26    AppServerLineTransport, AppServerProcessError, CODEX_APP_SERVER_PROTOCOL_VERSION,
27    CodexAppServerClient, OwnedAppServerConfig, OwnedAppServerTransport, ShutdownReceipt,
28    TurnRunReceipt,
29};
30pub use app_server_runtime::{
31    CodexAgentRuntime, CodexRole, CodexRoleProjection, CodexToolPolicy, ManagedAccountProjection,
32    ModelPageProjection, ModelProjection, OPENAI_ROLE_PROJECTIONS, RateLimitWindowProjection,
33    RateLimitsProjection, ReasoningEffort, ReplayDisposition, RuntimeError, RuntimeEvent,
34    TerminalFailureClass, TerminalStatus, ToolCallStatus, TurnCompletionReceipt,
35};
36#[cfg(unix)]
37pub use s08_owned_child_turn::{
38    S08_OPENAI_MODEL, S08_OWNED_CHILD_TURN_SCHEMA, S08OwnedChildTurnReceipt,
39    run_s08_owned_child_turn,
40};
41pub use sse::{CodexSseEncodeError, CodexSseEncoder};
42pub use wire::{
43    CodexRequestIdentity, CodexSubagentRequestContext, CodexWireError, CodexWireErrorKind,
44    MAV2_AGENTS_NAMESPACE, MAV2_COLLABORATION_NAMESPACE, MAV2_TOOL_NAMES, MULTI_AGENT_NAMESPACE,
45    Mav2ForkTurns, Mav2SpawnAgentContract, Mav2SpawnAgentError, NAMESPACE_ID_SMUGGLE_MARKER,
46    NamespaceIdSmuggleError, canonical_subagent_request_context, decode_namespace_call_id,
47    encode_namespace_call_id, is_mav2_tool_name, is_subagent_namespace,
48    normalize_mav2_provider_tool_call, normalize_provider_tool_call, normalize_responses_request,
49    validate_mav2_spawn_agent_arguments,
50};
51
52use serde::{Deserialize, Serialize};
53use sha2::{Digest, Sha256};
54use std::fmt;
55use std::fs::{self, File, OpenOptions};
56use std::io::{self, Read, Write};
57use std::path::{Component, Path, PathBuf};
58use subtle::ConstantTimeEq;
59use thiserror::Error;
60
61#[derive(Clone)]
62pub struct AuthToken {
63    digest: [u8; 32],
64}
65
66impl AuthToken {
67    pub fn from_secret(secret: &[u8]) -> Result<Self, AuthError> {
68        if secret.len() < 16 || secret.len() > 4096 {
69            return Err(AuthError::InvalidTokenMaterial);
70        }
71        Ok(Self {
72            digest: Sha256::digest(secret).into(),
73        })
74    }
75
76    pub fn verify_bearer(&self, header: &str) -> bool {
77        let candidate = header.strip_prefix("Bearer ").unwrap_or_default();
78        let candidate_digest: [u8; 32] = Sha256::digest(candidate.as_bytes()).into();
79        bool::from(self.digest.ct_eq(&candidate_digest))
80    }
81}
82
83impl fmt::Debug for AuthToken {
84    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
85        formatter.write_str("AuthToken([REDACTED])")
86    }
87}
88
89#[derive(Debug, Error, PartialEq, Eq)]
90pub enum AuthError {
91    #[error("invalid local authentication material")]
92    InvalidTokenMaterial,
93}
94
95#[derive(Clone, Debug, PartialEq, Eq)]
96pub struct UserConfigTarget {
97    path: PathBuf,
98}
99
100impl UserConfigTarget {
101    pub fn under_config_root(config_root: impl AsRef<Path>) -> Result<Self, ConfigError> {
102        let config_root = config_root.as_ref();
103        if !config_root.is_absolute()
104            || config_root
105                .components()
106                .any(|component| component == Component::ParentDir)
107        {
108            return Err(ConfigError::UnsafeTarget);
109        }
110        let metadata = fs::symlink_metadata(config_root)?;
111        if metadata.file_type().is_symlink() || !metadata.is_dir() {
112            return Err(ConfigError::UnsafeTarget);
113        }
114        let canonical_root = fs::canonicalize(config_root)?;
115        Ok(Self {
116            path: canonical_root.join("config.toml"),
117        })
118    }
119
120    pub fn path(&self) -> &Path {
121        &self.path
122    }
123}
124
125#[derive(Clone)]
126pub struct AuthCommand {
127    command: PathBuf,
128    args: Vec<String>,
129}
130
131impl AuthCommand {
132    pub fn new(command: impl Into<PathBuf>, args: Vec<String>) -> Result<Self, ConfigError> {
133        let command = command.into();
134        if !command.is_absolute()
135            || command
136                .to_string_lossy()
137                .chars()
138                .any(|character| character.is_control())
139            || args
140                .iter()
141                .any(|argument| argument.chars().any(char::is_control))
142        {
143            return Err(ConfigError::InvalidAuthCommand);
144        }
145        Ok(Self { command, args })
146    }
147
148    fn validate_host(&self) -> Result<(), ConfigError> {
149        let metadata =
150            fs::symlink_metadata(&self.command).map_err(|_| ConfigError::AuthCommandUnavailable)?;
151        if metadata.file_type().is_symlink() || !metadata.file_type().is_file() {
152            return Err(ConfigError::AuthCommandUnavailable);
153        }
154        #[cfg(unix)]
155        {
156            use std::os::unix::fs::PermissionsExt;
157            let mode = metadata.permissions().mode();
158            if mode & 0o111 == 0 || mode & 0o022 != 0 {
159                return Err(ConfigError::AuthCommandUnavailable);
160            }
161        }
162        Ok(())
163    }
164}
165
166impl fmt::Debug for AuthCommand {
167    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
168        formatter.write_str("AuthCommand([REDACTED])")
169    }
170}
171
172struct PlanCore {
173    target: UserConfigTarget,
174    auth_command: AuthCommand,
175    original: OriginalFile,
176    desired: Vec<u8>,
177    installed_fingerprint: String,
178    preview_digest: String,
179}
180
181enum OriginalFile {
182    Absent,
183    Present { bytes: Vec<u8>, mode: Option<u32> },
184}
185
186pub struct ConfigPreview {
187    core: PlanCore,
188    pub human_diff: String,
189    pub machine_plan: MachinePlan,
190}
191
192#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
193pub struct MachinePlan {
194    pub target: &'static str,
195    pub operation: &'static str,
196    pub original_present: bool,
197    pub original_byte_length: usize,
198    pub original_fingerprint: String,
199    pub installed_fingerprint: String,
200    pub preview_digest: String,
201    pub project_config_access: &'static str,
202}
203
204pub struct ConfirmedConfigPlan {
205    core: PlanCore,
206}
207
208pub struct InstalledConfig {
209    target: UserConfigTarget,
210    backup_path: PathBuf,
211    original: OriginalFile,
212    installed_fingerprint: String,
213    receipt_path: PathBuf,
214}
215
216const BACKUP_FILE_NAME: &str = "config.toml.continuity-router-backup";
217const RECEIPT_FILE_NAME: &str = "config.toml.continuity-router-receipt.json";
218const LOCK_FILE_NAME: &str = ".config.toml.continuity-router.lock";
219const MAX_CONFIG_BYTES: u64 = 1024 * 1024;
220const MAX_RECEIPT_BYTES: u64 = 64 * 1024;
221
222impl fmt::Debug for ConfigPreview {
223    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
224        formatter
225            .debug_struct("ConfigPreview")
226            .field("human_diff", &"[REDACTED]")
227            .field("machine_plan", &self.machine_plan)
228            .field("config_contents", &"[REDACTED]")
229            .finish()
230    }
231}
232
233impl fmt::Debug for ConfirmedConfigPlan {
234    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
235        formatter.write_str("ConfirmedConfigPlan([REDACTED])")
236    }
237}
238
239impl fmt::Debug for InstalledConfig {
240    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
241        formatter.write_str("InstalledConfig([REDACTED])")
242    }
243}
244
245#[derive(Debug, Serialize, Deserialize)]
246#[serde(deny_unknown_fields)]
247struct InstallationReceipt {
248    version: u8,
249    target: PathBuf,
250    original_present: bool,
251    original_mode: Option<u32>,
252    original_fingerprint: String,
253    installed_fingerprint: String,
254}
255
256#[derive(Debug, Error)]
257pub enum ConfigError {
258    #[error("user configuration is not valid UTF-8")]
259    InvalidUtf8,
260    #[error("user configuration is not valid TOML")]
261    InvalidToml,
262    #[error("the continuity_router provider already exists")]
263    ConflictingProvider,
264    #[error("the authentication command or arguments are invalid")]
265    InvalidAuthCommand,
266    #[error("the authentication command is unavailable or not executable")]
267    AuthCommandUnavailable,
268    #[error("user configuration must be a regular file")]
269    UnsupportedFileType,
270    #[error("user configuration target is outside a canonical config root")]
271    UnsafeTarget,
272    #[error("the installation receipt is invalid")]
273    InvalidReceipt,
274    #[error("the installation receipt targets a different file")]
275    ReceiptTargetMismatch,
276    #[error("the user configuration changed after installation")]
277    ConcurrentModification,
278    #[error("the supplied configuration preview digest is stale or invalid")]
279    PreviewDigestMismatch,
280    #[error("configuration I/O failed")]
281    Io(#[from] io::Error),
282}
283
284impl ConfigPreview {
285    pub fn build(
286        target: UserConfigTarget,
287        port: u16,
288        auth_command: &AuthCommand,
289    ) -> Result<Self, ConfigError> {
290        let original = match read_regular_file(target.path(), MAX_CONFIG_BYTES) {
291            Ok((bytes, metadata)) => {
292                let text = std::str::from_utf8(&bytes).map_err(|_| ConfigError::InvalidUtf8)?;
293                let parsed: toml::Value =
294                    toml::from_str(text).map_err(|_| ConfigError::InvalidToml)?;
295                if parsed
296                    .get("model_providers")
297                    .and_then(|providers| providers.get("continuity_router"))
298                    .is_some()
299                {
300                    return Err(ConfigError::ConflictingProvider);
301                }
302                OriginalFile::Present {
303                    bytes,
304                    mode: file_mode(&metadata),
305                }
306            }
307            Err(error) if error.kind() == io::ErrorKind::NotFound => OriginalFile::Absent,
308            Err(error) => return Err(ConfigError::Io(error)),
309        };
310
311        let original_text = match &original {
312            OriginalFile::Absent => "",
313            OriginalFile::Present { bytes, .. } => {
314                std::str::from_utf8(bytes).map_err(|_| ConfigError::InvalidUtf8)?
315            }
316        };
317        let desired = desired_config(original_text, port, auth_command).into_bytes();
318        let installed_fingerprint = fingerprint(&desired);
319        let original_bytes = match &original {
320            OriginalFile::Absent => &[][..],
321            OriginalFile::Present { bytes, .. } => bytes.as_slice(),
322        };
323        let original_fingerprint = fingerprint(original_bytes);
324        let preview_digest = config_preview_digest(
325            target.path(),
326            &original_fingerprint,
327            &installed_fingerprint,
328            port,
329            auth_command,
330        );
331        let human_diff = redacted_config_diff(original_text, port)?;
332        let machine_plan = MachinePlan {
333            target: "[USER_CONFIG]",
334            operation: "replace_routing_settings_and_append_provider",
335            original_present: matches!(original, OriginalFile::Present { .. }),
336            original_byte_length: original_bytes.len(),
337            original_fingerprint,
338            installed_fingerprint: installed_fingerprint.clone(),
339            preview_digest: preview_digest.clone(),
340            project_config_access: "none",
341        };
342        Ok(Self {
343            core: PlanCore {
344                target,
345                auth_command: auth_command.clone(),
346                original,
347                desired,
348                installed_fingerprint,
349                preview_digest,
350            },
351            human_diff,
352            machine_plan,
353        })
354    }
355
356    /// Preserves the in-process configuration API used by existing trusted
357    /// callers. Operational alpha command execution must use
358    /// [`Self::confirm_digest`] so the separately rendered preview is bound.
359    pub fn confirm(self) -> ConfirmedConfigPlan {
360        ConfirmedConfigPlan { core: self.core }
361    }
362
363    pub fn confirm_digest(
364        self,
365        supplied_preview_digest: &str,
366    ) -> Result<ConfirmedConfigPlan, ConfigError> {
367        if !constant_time_fingerprint_eq(&self.core.preview_digest, supplied_preview_digest) {
368            return Err(ConfigError::PreviewDigestMismatch);
369        }
370        Ok(ConfirmedConfigPlan { core: self.core })
371    }
372}
373
374fn config_preview_digest(
375    target: &Path,
376    original_fingerprint: &str,
377    installed_fingerprint: &str,
378    port: u16,
379    auth_command: &AuthCommand,
380) -> String {
381    let mut digest = Sha256::new();
382    digest.update(b"steadymux-codex-config-preview-v1\0");
383    digest.update(target.as_os_str().as_encoded_bytes());
384    digest.update(b"\0");
385    digest.update(original_fingerprint.as_bytes());
386    digest.update(b"\0");
387    digest.update(installed_fingerprint.as_bytes());
388    digest.update(b"\0");
389    digest.update(port.to_be_bytes());
390    digest.update(b"\0");
391    digest.update(auth_command.command.as_os_str().as_encoded_bytes());
392    for argument in &auth_command.args {
393        digest.update(b"\0");
394        digest.update(argument.as_bytes());
395    }
396    let digest: [u8; 32] = digest.finalize().into();
397    hex_digest(&digest)
398}
399
400fn hex_digest(bytes: &[u8]) -> String {
401    bytes.iter().map(|byte| format!("{byte:02x}")).collect()
402}
403
404fn original_still_matches(original: &OriginalFile, path: &Path) -> Result<bool, ConfigError> {
405    match original {
406        OriginalFile::Absent => match fs::symlink_metadata(path) {
407            Ok(_) => Ok(false),
408            Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(true),
409            Err(error) => Err(ConfigError::Io(error)),
410        },
411        OriginalFile::Present { bytes, .. } => match read_regular_file(path, MAX_CONFIG_BYTES) {
412            Ok((current, _)) => Ok(current == *bytes),
413            Err(error) if error.kind() == io::ErrorKind::InvalidInput => Ok(false),
414            Err(error) => Err(ConfigError::Io(error)),
415        },
416    }
417}
418
419impl ConfirmedConfigPlan {
420    pub fn apply(self) -> Result<InstalledConfig, ConfigError> {
421        self.core.auth_command.validate_host()?;
422        let _lock = ConfigOperationLock::acquire(self.core.target.path())?;
423        if !original_still_matches(&self.core.original, self.core.target.path())? {
424            return Err(ConfigError::ConcurrentModification);
425        }
426        let parent =
427            self.core.target.path().parent().ok_or_else(|| {
428                io::Error::new(io::ErrorKind::InvalidInput, "missing config parent")
429            })?;
430        fs::create_dir_all(parent)?;
431        set_directory_private(parent)?;
432
433        let backup_path = parent.join(BACKUP_FILE_NAME);
434        let receipt_path = parent.join(RECEIPT_FILE_NAME);
435        if backup_path.exists() || receipt_path.exists() {
436            return Err(ConfigError::Io(io::Error::new(
437                io::ErrorKind::AlreadyExists,
438                "configuration recovery artifacts already exist",
439            )));
440        }
441        match &self.core.original {
442            OriginalFile::Absent => write_atomic(&backup_path, &[], Some(0o600))?,
443            OriginalFile::Present { bytes, .. } => write_atomic(&backup_path, bytes, Some(0o600))?,
444        }
445        write_atomic(self.core.target.path(), &self.core.desired, Some(0o600))?;
446
447        let (original_present, original_mode) = match &self.core.original {
448            OriginalFile::Absent => (false, None),
449            OriginalFile::Present { mode, .. } => (true, *mode),
450        };
451        let receipt = InstallationReceipt {
452            version: 2,
453            target: self.core.target.path().to_path_buf(),
454            original_present,
455            original_mode,
456            original_fingerprint: match &self.core.original {
457                OriginalFile::Absent => fingerprint(&[]),
458                OriginalFile::Present { bytes, .. } => fingerprint(bytes),
459            },
460            installed_fingerprint: self.core.installed_fingerprint.clone(),
461        };
462        let receipt_bytes =
463            serde_json::to_vec(&receipt).map_err(|_| ConfigError::InvalidReceipt)?;
464        if let Err(error) = write_atomic(&receipt_path, &receipt_bytes, Some(0o600)) {
465            restore_original(self.core.target.path(), &self.core.original)?;
466            let _ = fs::remove_file(&backup_path);
467            return Err(ConfigError::Io(error));
468        }
469
470        Ok(InstalledConfig {
471            target: self.core.target,
472            backup_path,
473            original: self.core.original,
474            installed_fingerprint: self.core.installed_fingerprint,
475            receipt_path,
476        })
477    }
478}
479
480impl InstalledConfig {
481    pub fn uninstall(self) -> Result<(), ConfigError> {
482        let _lock = ConfigOperationLock::acquire(self.target.path())?;
483        let (current, _) = read_regular_file(self.target.path(), MAX_CONFIG_BYTES)?;
484        if !constant_time_fingerprint_eq(&fingerprint(&current), &self.installed_fingerprint) {
485            return Err(ConfigError::ConcurrentModification);
486        }
487        match self.original {
488            OriginalFile::Absent => fs::remove_file(self.target.path())?,
489            OriginalFile::Present { bytes, mode } => {
490                write_atomic(self.target.path(), &bytes, mode)?;
491            }
492        }
493        remove_if_present(&self.backup_path)?;
494        remove_if_present(&self.receipt_path)
495    }
496
497    pub fn backup_path(&self) -> &Path {
498        &self.backup_path
499    }
500
501    pub fn receipt_path(&self) -> &Path {
502        &self.receipt_path
503    }
504}
505
506pub fn restore_from_receipt(target: UserConfigTarget) -> Result<(), ConfigError> {
507    let _lock = ConfigOperationLock::acquire(target.path())?;
508    let parent = target
509        .path()
510        .parent()
511        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing config parent"))?;
512    let backup_path = parent.join(BACKUP_FILE_NAME);
513    let receipt_path = parent.join(RECEIPT_FILE_NAME);
514    let (receipt_bytes, _) = read_regular_file(&receipt_path, MAX_RECEIPT_BYTES)
515        .map_err(|_| ConfigError::InvalidReceipt)?;
516    let (backup_bytes, _) = read_regular_file(&backup_path, MAX_CONFIG_BYTES)
517        .map_err(|_| ConfigError::InvalidReceipt)?;
518    let receipt: InstallationReceipt =
519        serde_json::from_slice(&receipt_bytes).map_err(|_| ConfigError::InvalidReceipt)?;
520    if receipt.version != 2 || receipt.target != target.path() {
521        return Err(ConfigError::ReceiptTargetMismatch);
522    }
523    if !constant_time_fingerprint_eq(&fingerprint(&backup_bytes), &receipt.original_fingerprint) {
524        return Err(ConfigError::InvalidReceipt);
525    }
526    let (current, _) = read_regular_file(target.path(), MAX_CONFIG_BYTES)?;
527    if !constant_time_fingerprint_eq(&fingerprint(&current), &receipt.installed_fingerprint) {
528        return Err(ConfigError::ConcurrentModification);
529    }
530    if receipt.original_present {
531        write_atomic(target.path(), &backup_bytes, receipt.original_mode)?;
532    } else {
533        fs::remove_file(target.path())?;
534    }
535    remove_if_present(&backup_path)?;
536    remove_if_present(&receipt_path)
537}
538
539fn restore_original(path: &Path, original: &OriginalFile) -> Result<(), ConfigError> {
540    match original {
541        OriginalFile::Absent => {
542            remove_if_present(path)?;
543            Ok(())
544        }
545        OriginalFile::Present { bytes, mode } => {
546            write_atomic(path, bytes, *mode)?;
547            Ok(())
548        }
549    }
550}
551
552fn remove_if_present(path: &Path) -> Result<(), ConfigError> {
553    match fs::remove_file(path) {
554        Ok(()) => Ok(()),
555        Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
556        Err(error) => Err(ConfigError::Io(error)),
557    }
558}
559
560struct ConfigOperationLock(PathBuf);
561
562impl ConfigOperationLock {
563    fn acquire(target: &Path) -> Result<Self, ConfigError> {
564        let parent = target
565            .parent()
566            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing config parent"))?;
567        let path = parent.join(LOCK_FILE_NAME);
568        let mut options = OpenOptions::new();
569        options.write(true).create_new(true);
570        #[cfg(unix)]
571        {
572            use std::os::unix::fs::OpenOptionsExt;
573            options.mode(0o600);
574        }
575        let file = options.open(&path)?;
576        file.sync_all()?;
577        Ok(Self(path))
578    }
579}
580
581impl Drop for ConfigOperationLock {
582    fn drop(&mut self) {
583        let _ = fs::remove_file(&self.0);
584    }
585}
586
587fn read_regular_file(path: &Path, limit: u64) -> Result<(Vec<u8>, fs::Metadata), io::Error> {
588    let before = fs::symlink_metadata(path)?;
589    if before.file_type().is_symlink() || !before.is_file() || before.len() > limit {
590        return Err(io::Error::new(
591            io::ErrorKind::InvalidInput,
592            "path is not a bounded regular file",
593        ));
594    }
595    let mut file = File::open(path)?;
596    let opened = file.metadata()?;
597    let after_open = fs::symlink_metadata(path)?;
598    if !same_file_identity(&before, &opened) || !same_file_identity(&opened, &after_open) {
599        return Err(io::Error::new(
600            io::ErrorKind::InvalidInput,
601            "file identity changed while opening",
602        ));
603    }
604    let mut bytes = Vec::with_capacity(opened.len().min(limit) as usize);
605    Read::by_ref(&mut file)
606        .take(limit.saturating_add(1))
607        .read_to_end(&mut bytes)?;
608    let final_metadata = file.metadata()?;
609    if bytes.len() as u64 > limit
610        || !same_file_identity(&opened, &final_metadata)
611        || final_metadata.len() != bytes.len() as u64
612    {
613        return Err(io::Error::new(
614            io::ErrorKind::InvalidInput,
615            "file changed while reading",
616        ));
617    }
618    Ok((bytes, final_metadata))
619}
620
621#[cfg(unix)]
622fn same_file_identity(left: &fs::Metadata, right: &fs::Metadata) -> bool {
623    use std::os::unix::fs::MetadataExt;
624    left.dev() == right.dev() && left.ino() == right.ino()
625}
626
627#[cfg(not(unix))]
628fn same_file_identity(left: &fs::Metadata, right: &fs::Metadata) -> bool {
629    left.len() == right.len()
630        && left.modified().ok() == right.modified().ok()
631        && left.created().ok() == right.created().ok()
632}
633
634fn provider_snippet(port: u16, auth_command: &AuthCommand) -> String {
635    let command =
636        toml::Value::String(auth_command.command.to_string_lossy().into_owned()).to_string();
637    let args = toml::Value::Array(
638        auth_command
639            .args
640            .iter()
641            .cloned()
642            .map(toml::Value::String)
643            .collect(),
644    )
645    .to_string();
646    format!(
647        "\n[model_providers.continuity_router]\nname = \"Local Continuity Router\"\nbase_url = \"http://127.0.0.1:{port}/v1\"\nwire_api = \"responses\"\n\n[model_providers.continuity_router.auth]\ncommand = {command}\nargs = {args}\ntimeout_ms = 5000\nrefresh_interval_ms = 0\n"
648    )
649}
650
651fn redacted_provider_section(port: u16) -> String {
652    format!(
653        "[model_providers.continuity_router]\nname = \"Local Continuity Router\"\nbase_url = \"http://127.0.0.1:{port}/v1\"\nwire_api = \"responses\"\n\n[model_providers.continuity_router.auth]\ncommand = \"[REDACTED_AUTH_COMMAND]\"\nargs = [\"[REDACTED_AUTH_ARG]\"]\ntimeout_ms = 5000\nrefresh_interval_ms = 0\n"
654    )
655}
656
657fn redacted_config_diff(original: &str, port: u16) -> Result<String, ConfigError> {
658    let parsed: toml::Value = if original.is_empty() {
659        toml::Value::Table(toml::map::Map::new())
660    } else {
661        toml::from_str(original).map_err(|_| ConfigError::InvalidToml)?
662    };
663    let table = parsed.as_table().ok_or(ConfigError::InvalidToml)?;
664    let desired_settings = [
665        ("model", toml::Value::String("router-auto".to_owned())),
666        (
667            "model_provider",
668            toml::Value::String("continuity_router".to_owned()),
669        ),
670        ("web_search", toml::Value::String("disabled".to_owned())),
671    ];
672    let mut lines = vec![
673        "--- config.toml (current; unchanged fields omitted)".to_owned(),
674        "+++ config.toml (preview; auth command redacted)".to_owned(),
675        "@@ top-level routing settings @@".to_owned(),
676    ];
677    for (key, desired) in desired_settings {
678        match table.get(key) {
679            Some(current) if current == &desired => {
680                lines.push(format!("  {key} = {desired}"));
681            }
682            Some(current) => {
683                lines.push(format!("- {key} = {current}"));
684                lines.push(format!("+ {key} = {desired}"));
685            }
686            None => lines.push(format!("+ {key} = {desired}")),
687        }
688    }
689    lines.push("@@ model_providers.continuity_router (new) @@".to_owned());
690    lines.extend(
691        redacted_provider_section(port)
692            .lines()
693            .map(|line| format!("+ {line}")),
694    );
695    Ok(lines.join("\n"))
696}
697
698fn desired_config(original: &str, port: u16, auth_command: &AuthCommand) -> String {
699    let mut rewritten = String::with_capacity(original.len() + 512);
700    let mut in_top_level = true;
701    let mut wrote_model = false;
702    let mut wrote_provider = false;
703    let mut wrote_web_search = false;
704
705    for line in original.split_inclusive('\n') {
706        let trimmed = line.trim_start();
707        if in_top_level && trimmed.starts_with('[') {
708            append_missing_top_level(
709                &mut rewritten,
710                &mut wrote_model,
711                &mut wrote_provider,
712                &mut wrote_web_search,
713            );
714            in_top_level = false;
715        }
716        let key = in_top_level
717            .then(|| trimmed.split_once('=').map(|(key, _)| key.trim()))
718            .flatten();
719        let ending = if line.ends_with("\r\n") {
720            "\r\n"
721        } else if line.ends_with('\n') {
722            "\n"
723        } else {
724            ""
725        };
726        match key {
727            Some("model") => {
728                rewritten.push_str("model = \"router-auto\"");
729                rewritten.push_str(ending);
730                wrote_model = true;
731            }
732            Some("model_provider") => {
733                rewritten.push_str("model_provider = \"continuity_router\"");
734                rewritten.push_str(ending);
735                wrote_provider = true;
736            }
737            Some("web_search") => {
738                rewritten.push_str("web_search = \"disabled\"");
739                rewritten.push_str(ending);
740                wrote_web_search = true;
741            }
742            _ => rewritten.push_str(line),
743        }
744    }
745    append_missing_top_level(
746        &mut rewritten,
747        &mut wrote_model,
748        &mut wrote_provider,
749        &mut wrote_web_search,
750    );
751    if !rewritten.is_empty() && !rewritten.ends_with('\n') {
752        rewritten.push('\n');
753    }
754    rewritten.push_str(&provider_snippet(port, auth_command));
755    rewritten
756}
757
758fn append_missing_top_level(
759    output: &mut String,
760    wrote_model: &mut bool,
761    wrote_provider: &mut bool,
762    wrote_web_search: &mut bool,
763) {
764    if (!*wrote_model || !*wrote_provider || !*wrote_web_search)
765        && !output.is_empty()
766        && !output.ends_with('\n')
767    {
768        output.push('\n');
769    }
770    if !*wrote_model {
771        output.push_str("model = \"router-auto\"\n");
772        *wrote_model = true;
773    }
774    if !*wrote_provider {
775        output.push_str("model_provider = \"continuity_router\"\n");
776        *wrote_provider = true;
777    }
778    if !*wrote_web_search {
779        output.push_str("web_search = \"disabled\"\n");
780        *wrote_web_search = true;
781    }
782}
783
784fn fingerprint(bytes: &[u8]) -> String {
785    Sha256::digest(bytes)
786        .iter()
787        .map(|byte| format!("{byte:02x}"))
788        .collect()
789}
790
791fn constant_time_fingerprint_eq(left: &str, right: &str) -> bool {
792    if left.len() != right.len() {
793        return false;
794    }
795    bool::from(left.as_bytes().ct_eq(right.as_bytes()))
796}
797
798fn write_atomic(path: &Path, bytes: &[u8], mode: Option<u32>) -> Result<(), io::Error> {
799    let file_name = path
800        .file_name()
801        .and_then(|name| name.to_str())
802        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid config file name"))?;
803    let temporary = path.with_file_name(format!(".{file_name}.continuity-router.tmp"));
804    let mut options = OpenOptions::new();
805    options.write(true).create_new(true);
806    let mut file = match options.open(&temporary) {
807        Ok(file) => file,
808        Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
809            return Err(io::Error::new(
810                io::ErrorKind::AlreadyExists,
811                "atomic configuration temporary already exists",
812            ));
813        }
814        Err(error) => return Err(error),
815    };
816    if let Some(mode) = mode {
817        set_file_mode(&file, mode)?;
818    }
819    file.write_all(bytes)?;
820    file.sync_all()?;
821    drop(file);
822    if let Err(error) = fs::rename(&temporary, path) {
823        let _ = fs::remove_file(&temporary);
824        return Err(error);
825    }
826    sync_parent_directory(path)
827}
828
829#[cfg(unix)]
830fn sync_parent_directory(path: &Path) -> Result<(), io::Error> {
831    let parent = path
832        .parent()
833        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing parent"))?;
834    OpenOptions::new().read(true).open(parent)?.sync_all()
835}
836
837#[cfg(not(unix))]
838fn sync_parent_directory(_path: &Path) -> Result<(), io::Error> {
839    // `std::fs` cannot open Windows directories for `sync_all`; the file itself
840    // was flushed before the atomic rename above.
841    Ok(())
842}
843
844#[cfg(unix)]
845fn file_mode(metadata: &fs::Metadata) -> Option<u32> {
846    use std::os::unix::fs::PermissionsExt;
847    Some(metadata.permissions().mode() & 0o777)
848}
849
850#[cfg(not(unix))]
851fn file_mode(_metadata: &fs::Metadata) -> Option<u32> {
852    None
853}
854
855#[cfg(unix)]
856fn set_file_mode(file: &fs::File, mode: u32) -> Result<(), io::Error> {
857    use std::os::unix::fs::PermissionsExt;
858    file.set_permissions(fs::Permissions::from_mode(mode))
859}
860
861#[cfg(not(unix))]
862fn set_file_mode(_file: &fs::File, _mode: u32) -> Result<(), io::Error> {
863    Ok(())
864}
865
866#[cfg(unix)]
867fn set_directory_private(path: &Path) -> Result<(), io::Error> {
868    use std::os::unix::fs::PermissionsExt;
869    fs::set_permissions(path, fs::Permissions::from_mode(0o700))
870}
871
872#[cfg(not(unix))]
873fn set_directory_private(_path: &Path) -> Result<(), io::Error> {
874    Ok(())
875}
876
877#[cfg(test)]
878mod tests {
879    use super::*;
880    use tempfile::tempdir;
881
882    fn fixture_auth_command() -> AuthCommand {
883        AuthCommand::new(
884            std::env::current_exe().expect("current test executable"),
885            vec!["CONTINUITY_ROUTER_TOKEN".to_owned()],
886        )
887        .expect("fixture auth command")
888    }
889
890    fn confirm_preview(preview: ConfigPreview) -> ConfirmedConfigPlan {
891        let digest = preview.machine_plan.preview_digest.clone();
892        preview
893            .confirm_digest(&digest)
894            .expect("matching preview digest")
895    }
896
897    #[test]
898    fn token_debug_and_verification_are_redacted() {
899        let token = AuthToken::from_secret(b"synthetic-token-canary-12345").expect("fixture token");
900        assert!(token.verify_bearer("Bearer synthetic-token-canary-12345"));
901        assert!(!token.verify_bearer("Bearer wrong-token-value"));
902        assert_eq!(format!("{token:?}"), "AuthToken([REDACTED])");
903    }
904
905    #[test]
906    fn atomic_write_replaces_file_with_exact_bytes() {
907        let root = tempdir().expect("temporary config root");
908        let path = root.path().join("config.toml");
909        fs::write(&path, b"original\r\nbytes\r\n").expect("original fixture");
910
911        let replacement = b"replacement\r\nbytes\r\n";
912        write_atomic(&path, replacement, Some(0o600)).expect("atomic replacement");
913
914        assert_eq!(fs::read(&path).expect("replacement bytes"), replacement);
915        #[cfg(unix)]
916        assert_eq!(
917            file_mode(&fs::metadata(&path).expect("replacement metadata")),
918            Some(0o600)
919        );
920    }
921
922    #[test]
923    fn config_plan_debug_never_exposes_original_or_desired_secrets() {
924        let root = tempdir().expect("temporary config root");
925        let target = UserConfigTarget::under_config_root(root.path()).expect("canonical root");
926        let original_canary = "existing-config-secret-canary";
927        fs::write(target.path(), format!("model = \"{original_canary}\"\n"))
928            .expect("fixture config");
929        let auth_canary = "auth-command-secret-canary";
930        let command = AuthCommand::new(
931            std::env::current_exe().expect("current test executable"),
932            vec![auth_canary.to_owned()],
933        )
934        .expect("fixture auth command");
935        let preview = ConfigPreview::build(target, 49151, &command).expect("preview");
936        let debug = format!("{preview:?}");
937        assert!(debug.contains("[REDACTED]"));
938        assert!(!debug.contains(original_canary));
939        assert!(!debug.contains(auth_canary));
940        assert_eq!(
941            format!("{:?}", confirm_preview(preview)),
942            "ConfirmedConfigPlan([REDACTED])"
943        );
944    }
945
946    #[test]
947    fn preview_is_non_mutating_and_restore_is_byte_exact() {
948        let root = tempdir().expect("temporary config root");
949        let config_root = root.path().join("user-codex");
950        fs::create_dir_all(&config_root).expect("config directory");
951        let target =
952            UserConfigTarget::under_config_root(&config_root).expect("canonical config root");
953        let original = b"# user comment\r\nmodel = \"native\"\r\nmodel_provider = \"openai\"\r\nweb_search = \"live\"\r\n";
954        fs::write(target.path(), original).expect("fixture config");
955
956        let preview = ConfigPreview::build(target.clone(), 49152, &fixture_auth_command())
957            .expect("preview should succeed");
958        assert_eq!(fs::read(target.path()).expect("read unchanged"), original);
959        assert!(
960            preview
961                .human_diff
962                .contains("+ base_url = \"http://127.0.0.1:49152/v1\"")
963        );
964        assert!(preview.human_diff.contains("- model = \"native\""));
965        assert!(preview.human_diff.contains("+ model = \"router-auto\""));
966        assert!(preview.human_diff.contains("- model_provider = \"openai\""));
967        assert!(
968            preview
969                .human_diff
970                .contains("+ model_provider = \"continuity_router\"")
971        );
972        assert!(preview.human_diff.contains("- web_search = \"live\""));
973        assert!(preview.human_diff.contains("+ web_search = \"disabled\""));
974        assert!(!preview.human_diff.contains("# user comment"));
975        assert!(preview.human_diff.contains("[REDACTED_AUTH_COMMAND]"));
976        assert_eq!(preview.machine_plan.project_config_access, "none");
977        assert_eq!(
978            preview.machine_plan.operation,
979            "replace_routing_settings_and_append_provider"
980        );
981
982        let installed = confirm_preview(preview).apply().expect("confirmed install");
983        assert_ne!(fs::read(target.path()).expect("installed bytes"), original);
984        let backup = installed.backup_path().to_path_buf();
985        assert_eq!(fs::read(&backup).expect("backup bytes"), original);
986        #[cfg(unix)]
987        assert_eq!(
988            file_mode(&fs::metadata(&backup).expect("backup metadata")),
989            Some(0o600)
990        );
991
992        installed.uninstall().expect("uninstall should restore");
993        assert_eq!(fs::read(target.path()).expect("restored bytes"), original);
994        assert!(!backup.exists());
995    }
996
997    #[test]
998    fn uninstall_refuses_to_clobber_concurrent_edits() {
999        let root = tempdir().expect("temporary config root");
1000        let target = UserConfigTarget::under_config_root(root.path()).expect("canonical root");
1001        fs::write(target.path(), b"model = \"native\"\n").expect("fixture config");
1002        let preview =
1003            ConfigPreview::build(target.clone(), 49153, &fixture_auth_command()).expect("preview");
1004        let installed = confirm_preview(preview).apply().expect("install");
1005        fs::write(
1006            target.path(),
1007            b"# concurrent user edit\nmodel = \"native\"\n",
1008        )
1009        .expect("concurrent edit");
1010        assert!(matches!(
1011            installed.uninstall(),
1012            Err(ConfigError::ConcurrentModification)
1013        ));
1014        assert_eq!(
1015            fs::read(target.path()).expect("preserved user edit"),
1016            b"# concurrent user edit\nmodel = \"native\"\n"
1017        );
1018    }
1019
1020    #[test]
1021    fn invalid_or_conflicting_config_is_rejected() {
1022        let root = tempdir().expect("temporary config root");
1023        let target = UserConfigTarget::under_config_root(root.path()).expect("canonical root");
1024        fs::write(target.path(), b"not = [valid").expect("invalid fixture");
1025        assert!(matches!(
1026            ConfigPreview::build(target.clone(), 49154, &fixture_auth_command()),
1027            Err(ConfigError::InvalidToml)
1028        ));
1029        fs::write(
1030            target.path(),
1031            b"[model_providers.continuity_router]\nname = \"existing\"\n",
1032        )
1033        .expect("conflict fixture");
1034        assert!(matches!(
1035            ConfigPreview::build(target, 49154, &fixture_auth_command()),
1036            Err(ConfigError::ConflictingProvider)
1037        ));
1038    }
1039
1040    #[test]
1041    fn absent_config_is_removed_on_uninstall() {
1042        let root = tempdir().expect("temporary config root");
1043        let target = UserConfigTarget::under_config_root(root.path()).expect("canonical root");
1044        let preview =
1045            ConfigPreview::build(target.clone(), 49155, &fixture_auth_command()).expect("preview");
1046        let installed = confirm_preview(preview).apply().expect("install");
1047        installed.uninstall().expect("uninstall");
1048        assert!(!target.path().exists());
1049    }
1050
1051    #[test]
1052    fn project_config_is_never_read_or_written() {
1053        let root = tempdir().expect("temporary root");
1054        let user_root = root.path().join("user-codex");
1055        let project_config = root.path().join("project/.codex/config.toml");
1056        fs::create_dir_all(project_config.parent().expect("project parent"))
1057            .expect("project config directory");
1058        let project_bytes = b"model_provider = \"project-native\"\n";
1059        fs::write(&project_config, project_bytes).expect("project fixture");
1060        fs::create_dir_all(&user_root).expect("user config root");
1061
1062        let target = UserConfigTarget::under_config_root(&user_root).expect("canonical user root");
1063        let preview =
1064            ConfigPreview::build(target.clone(), 49156, &fixture_auth_command()).expect("preview");
1065        assert_eq!(preview.machine_plan.project_config_access, "none");
1066        let installed = confirm_preview(preview).apply().expect("install");
1067        assert_eq!(
1068            fs::read(&project_config).expect("project config after install"),
1069            project_bytes
1070        );
1071        installed.uninstall().expect("uninstall");
1072        assert_eq!(
1073            fs::read(&project_config).expect("project config after uninstall"),
1074            project_bytes
1075        );
1076    }
1077
1078    #[cfg(unix)]
1079    #[test]
1080    fn config_root_and_config_file_symlinks_are_refused() {
1081        use std::os::unix::fs::symlink;
1082
1083        let root = tempdir().expect("temporary root");
1084        let real_root = root.path().join("real");
1085        let linked_root = root.path().join("linked");
1086        fs::create_dir(&real_root).expect("real root");
1087        symlink(&real_root, &linked_root).expect("linked root");
1088        assert!(matches!(
1089            UserConfigTarget::under_config_root(&linked_root),
1090            Err(ConfigError::UnsafeTarget)
1091        ));
1092
1093        let target = UserConfigTarget::under_config_root(&real_root).expect("canonical root");
1094        let real_config = root.path().join("other.toml");
1095        fs::write(&real_config, b"model = \"native\"\n").expect("real config");
1096        symlink(&real_config, target.path()).expect("config symlink");
1097        assert!(matches!(
1098            ConfigPreview::build(target, 49157, &fixture_auth_command()),
1099            Err(ConfigError::Io(_))
1100        ));
1101    }
1102
1103    #[test]
1104    fn restore_refuses_tampered_backup_and_preserves_installed_config() {
1105        let root = tempdir().expect("temporary root");
1106        let target = UserConfigTarget::under_config_root(root.path()).expect("canonical root");
1107        fs::write(target.path(), b"model = \"native\"\n").expect("original config");
1108        let preview =
1109            ConfigPreview::build(target.clone(), 49158, &fixture_auth_command()).expect("preview");
1110        let installed = confirm_preview(preview).apply().expect("install");
1111        let installed_bytes = fs::read(target.path()).expect("installed bytes");
1112        fs::write(installed.backup_path(), b"tampered backup\n").expect("tamper backup");
1113        drop(installed);
1114        assert!(matches!(
1115            restore_from_receipt(target.clone()),
1116            Err(ConfigError::InvalidReceipt)
1117        ));
1118        assert_eq!(
1119            fs::read(target.path()).expect("preserved installed config"),
1120            installed_bytes
1121        );
1122    }
1123
1124    #[test]
1125    fn apply_refuses_a_competing_installer_lock() {
1126        let root = tempdir().expect("temporary root");
1127        let target = UserConfigTarget::under_config_root(root.path()).expect("canonical root");
1128        fs::write(target.path(), b"model = \"native\"\n").expect("original config");
1129        let preview =
1130            ConfigPreview::build(target.clone(), 49159, &fixture_auth_command()).expect("preview");
1131        fs::write(root.path().join(LOCK_FILE_NAME), b"").expect("competing lock");
1132        assert!(matches!(
1133            confirm_preview(preview).apply(),
1134            Err(ConfigError::Io(_))
1135        ));
1136        assert_eq!(
1137            fs::read(target.path()).expect("unchanged config"),
1138            b"model = \"native\"\n"
1139        );
1140    }
1141}