1#![forbid(unsafe_code)]
4
5mod 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 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(¤t), &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(¤t), &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 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}