From 804d6306e84f570393cdd1eec94c41464f503b1a Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Tue, 29 Sep 2026 19:38:59 +0000 Subject: [PATCH 01/24] Allow approved filesystem escalation while preserving denied reads (#49353) ## Why Denied-read policies previously kept explicitly approved commands within their original filesystem restrictions, preventing operations that need broader write access, such as updating Git metadata. ## What changed - Apply broader filesystem permissions after approval of explicit escalation while retaining the sandbox, denied reads, and network policy. Command allow rules alone do not widen filesystem access. - Reject escalation when the required local sandbox is unavailable or a remote exec server lacks support for preserving the restrictions. - Track escalated terminal access so subsequent `write_stdin` calls still require review when stdin approval is enabled. ## Testing Add coverage for approved and denied escalation, allow rules, remote server compatibility, and stdin approval before input delivery. Add a Windows integration test verifying that an approved `git pull` updates Git metadata while exact-path and glob-based read denials remain enforced. Serialize Windows sandbox integration tests across processes to protect shared test account credentials. GitOrigin-RevId: 5dce6d41487ee4d39bd795c8d086398ab9794d5b --- codex-rs/core/src/tools/orchestrator.rs | 104 ++++++-- .../core/src/tools/runtimes/unified_exec.rs | 11 + codex-rs/core/src/tools/sandboxing.rs | 25 +- codex-rs/core/src/tools/sandboxing_tests.rs | 14 +- .../core/src/unified_exec/stdin_approval.rs | 32 ++- .../src/unified_exec/stdin_approval_tests.rs | 1 + codex-rs/core/tests/suite/approvals.rs | 96 ++++++++ .../suite/unified_exec_process_events.rs | 231 ++++++++++++++++-- .../suite/unified_exec_stdin_approval.rs | 121 +++++++++ codex-rs/core/tests/suite/windows_sandbox.rs | 187 +++++++++++++- 10 files changed, 756 insertions(+), 66 deletions(-) diff --git a/codex-rs/core/src/tools/orchestrator.rs b/codex-rs/core/src/tools/orchestrator.rs index 3473cd50d6a7..2d94f27d8662 100644 --- a/codex-rs/core/src/tools/orchestrator.rs +++ b/codex-rs/core/src/tools/orchestrator.rs @@ -29,6 +29,9 @@ use codex_otel::ToolDecisionSource; use codex_protocol::error::CodexErrorDetails; use codex_protocol::error::SandboxErr; use codex_protocol::exec_output::ExecToolCallOutput; +use codex_protocol::models::PermissionProfile; +use codex_protocol::permissions::PROTECTED_METADATA_PATH_NAMES; +use codex_protocol::permissions::file_system_root; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::ReviewDecision; use codex_sandboxing::SandboxManager; @@ -222,15 +225,71 @@ impl ToolOrchestrator { // 2) First attempt under the selected sandbox. let unsandboxed_allowed = unsandboxed_execution_allowed(&file_system_sandbox_policy); - let sandbox_override = if unsandboxed_allowed { - sandbox_override_for_first_attempt( - tool.sandbox_permissions(req), - &requirement, - &file_system_sandbox_policy, - ) + let sandbox_override = sandbox_override_for_first_attempt( + tool.sandbox_permissions(req), + &requirement, + &file_system_sandbox_policy, + already_approved, + ); + let (escalated_profile, writable_root_metadata) = if sandbox_override + == SandboxOverride::EscalatedSandboxWithRestrictions + { + let context = environment.sandbox_context(/*additional_permissions*/ None); + let policy_context = context.policy_context(); + let baseline = permission_profile.file_system_sandbox_policy(); + let file_system = baseline.for_approved_command(&policy_context); + let writable_root_metadata = file_system_root(&policy_context).is_some_and(|root| { + file_system.can_write_path(&root, &policy_context) + && PROTECTED_METADATA_PATH_NAMES.iter().any(|name| { + root.join_descendant(name) + .is_ok_and(|path| file_system.can_write_path(&path, &policy_context)) + }) + }); + let escalated_profile = (file_system != baseline + && file_system + != baseline.materialize_project_roots_with_path_uris(workspace_roots)) + .then(|| { + PermissionProfile::from_runtime_permissions_with_enforcement( + permission_profile.enforcement(), + &file_system, + permission_profile.network_sandbox_policy(), + ) + }); + (escalated_profile, writable_root_metadata) } else { - SandboxOverride::NoOverride + (None, false) }; + if (escalated_profile.is_some() || writable_root_metadata) + && environment.environment.is_remote() + { + let info = environment.environment.info().await.map_err(|err| { + ToolError::Rejected(format!( + "could not verify exec-server support for escalation with denied reads: {err}" + )) + })?; + if !matches!(info.platform_os.as_deref(), Some("windows" | "macos")) + && !info + .capabilities + .linux_approved_root_write_preserves_restrictions + { + return Err(ToolError::Rejected( + "this exec-server cannot safely escalate with denied reads; upgrade the exec-server and retry" + .to_string(), + )); + } + } + let escalated_local_profile = escalated_profile + .as_ref() + .filter(|_| !executor_managed_process_sandbox) + .map(|profile| { + profile + .clone() + .materialize_project_roots_with_path_uris(workspace_roots) + }); + let initial_permissions = escalated_local_profile + .as_ref() + .or(escalated_profile.as_ref()) + .unwrap_or(&permissions); let network_approval_spec = tool.network_approval_spec(req, tool_ctx); // An explicit owner proxy requirement permits filtered egress, like an enabled // controller proxy. Traffic-only owner policies stay offline unless command @@ -265,11 +324,13 @@ impl ToolOrchestrator { let sandbox_preference = tool.sandbox_preference(); let sandbox_requested = match sandbox_override { SandboxOverride::BypassSandboxFirstAttempt => false, - SandboxOverride::NoOverride => sandbox_manager.should_sandbox( - &permissions, - sandbox_preference, - managed_network_active, - ), + SandboxOverride::NoOverride | SandboxOverride::EscalatedSandboxWithRestrictions => { + sandbox_manager.should_sandbox( + initial_permissions, + sandbox_preference, + managed_network_active, + ) + } }; let windows_sandbox_type = codex_protocol::sandbox::effective_windows_sandbox_type( sandbox_config.windows_sandbox_type, @@ -277,7 +338,7 @@ impl ToolOrchestrator { ); let initial_sandbox = if sandbox_requested && !executor_managed_process_sandbox { sandbox_manager.select_initial( - &permissions, + initial_permissions, sandbox_preference, windows_sandbox_type, managed_network_active, @@ -285,6 +346,9 @@ impl ToolOrchestrator { } else { SandboxType::None }; + if !executor_managed_process_sandbox { + sandbox_override.ensure_native_sandbox(initial_sandbox)?; + } let sandbox_policy_cwd = tool .sandbox_cwd(req) @@ -298,8 +362,8 @@ impl ToolOrchestrator { let initial_attempt = SandboxAttempt { sandbox: initial_sandbox, sandbox_requested, - permissions: &permissions, - exec_server_permissions: permission_profile, + permissions: initial_permissions, + exec_server_permissions: escalated_profile.as_ref().unwrap_or(permission_profile), enforce_managed_network: managed_network_active, manager: &sandbox_manager, sandbox_cwd: &sandbox_policy_cwd, @@ -449,14 +513,14 @@ impl ToolOrchestrator { let retry_sandbox_requested = !unsandboxed_allowed && sandbox_manager.should_sandbox( - &permissions, + initial_permissions, sandbox_preference, managed_network_active, ); let retry_sandbox = if retry_sandbox_requested && !executor_managed_process_sandbox { sandbox_manager.select_initial( - &permissions, + initial_permissions, sandbox_preference, windows_sandbox_type, managed_network_active, @@ -472,8 +536,10 @@ impl ToolOrchestrator { let retry_attempt = SandboxAttempt { sandbox: retry_sandbox, sandbox_requested: retry_sandbox_requested, - permissions: &permissions, - exec_server_permissions: permission_profile, + permissions: initial_permissions, + exec_server_permissions: escalated_profile + .as_ref() + .unwrap_or(permission_profile), enforce_managed_network: managed_network_active, manager: &sandbox_manager, sandbox_cwd: &sandbox_policy_cwd, diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index 8e7c256b566e..4c662e02b3a9 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -629,6 +629,17 @@ impl<'a> ToolRuntime for UnifiedExecRunt req.additional_permissions.as_ref(), internal_permissions.as_ref(), ); + let baseline_file_system = req + .turn_environment + .permission_profile() + .file_system_sandbox_policy(); + let permissions = if baseline_file_system.has_denied_read_restrictions() + && attempt.exec_server_permissions.file_system_sandbox_policy() != baseline_file_system + { + permissions.with_filesystem_escalation() + } else { + permissions + }; if let UnifiedExecShellMode::ZshFork(zsh_fork_config) = &self.shell_mode { let command = build_unified_exec_sandbox_command( diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index 85185947d940..bf1fd2d79e6a 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -233,19 +233,36 @@ pub(crate) fn default_exec_approval_requirement( #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum SandboxOverride { NoOverride, + EscalatedSandboxWithRestrictions, BypassSandboxFirstAttempt, } +impl SandboxOverride { + pub(crate) fn ensure_native_sandbox(self, sandbox: SandboxType) -> Result<(), ToolError> { + if self == Self::EscalatedSandboxWithRestrictions && sandbox == SandboxType::None { + return Err(ToolError::Rejected( + "command escalation with denied reads requires an available filesystem sandbox" + .to_string(), + )); + } + Ok(()) + } +} + pub(crate) fn sandbox_override_for_first_attempt( sandbox_permissions: SandboxPermissions, exec_approval_requirement: &ExecApprovalRequirement, file_system_sandbox_policy: &FileSystemSandboxPolicy, + already_approved: bool, ) -> SandboxOverride { - // Deny-read restrictions are part of the active permission policy. Running - // without a filesystem sandbox would discard them, even if the command was - // otherwise approved by rules or explicit escalation. + // Only actual approval of an explicit escalation may widen the filesystem; + // a command allow rule does not authorize removing filesystem restrictions. if !unsandboxed_execution_allowed(file_system_sandbox_policy) { - return SandboxOverride::NoOverride; + return if sandbox_permissions.requires_escalated_permissions() && already_approved { + SandboxOverride::EscalatedSandboxWithRestrictions + } else { + SandboxOverride::NoOverride + }; } // ExecPolicy `Allow` can intentionally imply full trust (Skip + bypass_sandbox=true), diff --git a/codex-rs/core/src/tools/sandboxing_tests.rs b/codex-rs/core/src/tools/sandboxing_tests.rs index 0142440f6c95..95731415baf4 100644 --- a/codex-rs/core/src/tools/sandboxing_tests.rs +++ b/codex-rs/core/src/tools/sandboxing_tests.rs @@ -163,6 +163,7 @@ fn additional_permissions_allow_bypass_sandbox_first_attempt_when_execpolicy_ski proposed_execpolicy_amendment: None, }, &FileSystemSandboxPolicy::default(), + /*already_approved*/ false, ), SandboxOverride::BypassSandboxFirstAttempt ); @@ -178,13 +179,14 @@ fn guardian_bypasses_sandbox_for_explicit_escalation_on_first_attempt() { proposed_execpolicy_amendment: None, }, &FileSystemSandboxPolicy::default(), + /*already_approved*/ true, ), SandboxOverride::BypassSandboxFirstAttempt ); } #[test] -fn deny_read_blocks_explicit_escalation_and_policy_bypass() { +fn deny_read_preserves_the_sandbox_for_explicit_escalation_and_blocks_policy_bypass() { let file_system_policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern: "**/*.env".to_string(), @@ -201,11 +203,16 @@ fn deny_read_blocks_explicit_escalation_and_policy_bypass() { proposed_execpolicy_amendment: None, }, &file_system_policy, + /*already_approved*/ true, ), - SandboxOverride::NoOverride, - "explicit escalation would drop deny-read filesystem policy, so keep the first attempt sandboxed", + SandboxOverride::EscalatedSandboxWithRestrictions, + "explicit escalation must widen the first attempt without dropping the sandbox", ); assert!(!unsandboxed_execution_allowed(&file_system_policy)); + assert!(matches!( + SandboxOverride::EscalatedSandboxWithRestrictions.ensure_native_sandbox(SandboxType::None), + Err(ToolError::Rejected(reason)) if reason.contains("requires an available filesystem sandbox"), + )); assert_eq!( sandbox_permissions_preserving_denied_reads( SandboxPermissions::RequireEscalated, @@ -235,6 +242,7 @@ fn deny_read_blocks_explicit_escalation_and_policy_bypass() { proposed_execpolicy_amendment: None, }, &file_system_policy, + /*already_approved*/ true, ), SandboxOverride::NoOverride, "exec-policy allow rules would drop deny-read filesystem policy, so keep the first attempt sandboxed", diff --git a/codex-rs/core/src/unified_exec/stdin_approval.rs b/codex-rs/core/src/unified_exec/stdin_approval.rs index cef5a1331c85..646ea8ec9be9 100644 --- a/codex-rs/core/src/unified_exec/stdin_approval.rs +++ b/codex-rs/core/src/unified_exec/stdin_approval.rs @@ -33,6 +33,7 @@ pub(crate) struct TerminalPermissions { policy: TerminalPolicy, sandbox_source: TerminalSandboxSource, launch_permissions: SandboxPermissions, + filesystem_escalated: bool, additional_permissions: Option, internal_permissions: Option, } @@ -100,14 +101,19 @@ impl TerminalPermissions { merge_permission_profiles(additional_permissions, internal_permissions), ), sandbox_source, - // A bypass is a property of the successful attempt, not a difference - // between settings: a full-access environment can still bypass a proxy. + // Retain the successful attempt's authority independently from its baseline. launch_permissions, + filesystem_escalated: false, additional_permissions: additional_permissions.cloned(), internal_permissions: internal_permissions.cloned(), } } + pub(crate) fn with_filesystem_escalation(mut self) -> Self { + self.filesystem_escalated = true; + self + } + /// Compares launch policy with current policy including retained grants, /// without reading turn state or touching a process. fn review_requirement( @@ -129,15 +135,17 @@ impl TerminalPermissions { } // Runtime-internal grants are part of an ordinary launch, so only permissions // beyond the baseline plus those grants need a fresh stdin approval. - Ok(if bypassed || &self.policy != current { - SandboxPermissions::RequireEscalated - } else if self.policy.sandbox.permissions - == effective_permission_profile(baseline, self.internal_permissions.as_ref()) - { - SandboxPermissions::UseDefault - } else { - SandboxPermissions::WithAdditionalPermissions - }) + Ok( + if bypassed || self.filesystem_escalated || &self.policy != current { + SandboxPermissions::RequireEscalated + } else if self.policy.sandbox.permissions + == effective_permission_profile(baseline, self.internal_permissions.as_ref()) + { + SandboxPermissions::UseDefault + } else { + SandboxPermissions::WithAdditionalPermissions + }, + ) } fn approval_reason( @@ -146,6 +154,8 @@ impl TerminalPermissions { ) -> Result { let authority = if self.launch_permissions.requires_escalated_permissions() { "This terminal was launched outside the sandbox, bypassing any managed network proxy." + } else if self.filesystem_escalated { + "This terminal has approved broader filesystem access; denied reads are still enforced." } else if self.policy.sandbox.permissions == PermissionProfile::Disabled { "This terminal runs without a filesystem sandbox." } else { diff --git a/codex-rs/core/src/unified_exec/stdin_approval_tests.rs b/codex-rs/core/src/unified_exec/stdin_approval_tests.rs index 69a701c5a1ee..170edefa49a2 100644 --- a/codex-rs/core/src/unified_exec/stdin_approval_tests.rs +++ b/codex-rs/core/src/unified_exec/stdin_approval_tests.rs @@ -29,6 +29,7 @@ fn terminal_permissions(profile: &PermissionProfile) -> TerminalPermissions { }, sandbox_source: TerminalSandboxSource::Native, launch_permissions: SandboxPermissions::UseDefault, + filesystem_escalated: false, additional_permissions: None, internal_permissions: None, } diff --git a/codex-rs/core/tests/suite/approvals.rs b/codex-rs/core/tests/suite/approvals.rs index fd229e37d733..8d3668f7dabf 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -3802,6 +3802,102 @@ touch {outside_path:?} Ok(()) } +#[cfg(unix)] +#[test_case(Some(ReviewDecision::Approved); "approved")] +#[test_case(Some(ReviewDecision::denied("rejected by user")); "denied")] +#[test_case(None; "allow_rule_skips_approval_without_widening")] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn explicit_escalation_with_denied_reads(decision: Option) -> Result<()> { + skip_if_no_network!(Ok(())); + let server = start_mock_server().await; + let home = Arc::new(TempDir::new()?); + if decision.is_none() { + let rules = home.path().join("rules"); + fs::create_dir(&rules)?; + fs::write( + rules.join("default.rules"), + r#"prefix_rule(pattern=["/bin/sh", "escalation.sh"], decision="allow")"#, + )?; + } + let outside_marker = home.path().join("escalated-marker"); + let quoted_marker = shlex::try_join([outside_marker.to_string_lossy().as_ref()])?; + let approval_policy = AskForApproval::OnRequest; + let mut builder = test_codex() + .with_home(home) + .with_config(move |config| { + config.permissions.approval_policy = Constrained::allow_any(approval_policy); + config.features.enable(Feature::UnifiedExec).unwrap(); + config.features.disable(Feature::ShellZshFork).unwrap(); + let mut file_system = + restrictive_workspace_write_profile().file_system_sandbox_policy(); + file_system.entries.push(FileSystemSandboxEntry::new( + config.cwd.join("secret.txt").into(), + FileSystemAccessMode::Deny, + )); + config + .permissions + .set_permission_profile(PermissionProfile::from_runtime_permissions( + &file_system, + NetworkSandboxPolicy::Restricted, + )) + .unwrap(); + }) + .with_workspace_setup(move |cwd, _fs| async move { + fs::write(cwd.join("secret.txt"), "secret")?; + fs::write( + cwd.join("escalation.sh"), + format!( + "printf ran > ran || exit 10; (printf outside > {quoted_marker}) 2> errors; \ + if cat secret.txt > errors 2>&1; then exit 11; fi; \ + grep -q secret.txt errors || exit 12; printf 'protected\\n'" + ), + )?; + Ok(()) + }); + let test = builder.build(&server).await?; + let workspace_marker = test.cwd.path().join("ran"); + let command = "/bin/sh escalation.sh"; + let call_id = "explicit-deny-read-escalation"; + let event = exec_command_event( + call_id, + command, + Some(10_000), + SandboxPermissions::RequireEscalated, + /*justification*/ None, + )?; + mount_sse_once(&server, sse(vec![event, ev_completed("run")])).await; + let response = mount_sse_once(&server, sse(vec![ev_completed("done")])).await; + submit_turn_preserving_active_permission_profile(&test, "run the command", approval_policy) + .await?; + let approved = decision == Some(ReviewDecision::Approved); + let rejected = decision.is_some() && !approved; + if let Some(decision) = decision { + let request = expect_exec_approval(&test, command).await; + assert!(!outside_marker.exists() && !workspace_marker.exists()); + test.codex + .submit(Op::ExecApproval { + id: request.effective_approval_id(), + turn_id: None, + decision, + }) + .await?; + } + wait_for_completion_without_approval(&test).await; + let result = parse_result(&response.single_request().function_call_output(call_id)); + if rejected { + assert!(result.stdout.contains("rejected by user"), "{result:?}"); + assert!(!workspace_marker.exists()); + } else { + assert_eq!( + (result.exit_code, result.stdout.as_str()), + (Some(0), "protected\n") + ); + assert_eq!(fs::read_to_string(workspace_marker)?, "ran"); + } + assert_eq!(outside_marker.exists(), approved); + Ok(()) +} + /// Verifies that zsh-fork applies an inner script's allow rule even when the /// model invokes an outer wrapper, and that the escalated script retains the /// named profile needed to reconstruct the original sandbox remotely without diff --git a/codex-rs/core/tests/suite/unified_exec_process_events.rs b/codex-rs/core/tests/suite/unified_exec_process_events.rs index ff85b4fba468..39d8db419093 100644 --- a/codex-rs/core/tests/suite/unified_exec_process_events.rs +++ b/codex-rs/core/tests/suite/unified_exec_process_events.rs @@ -4,6 +4,7 @@ use base64::Engine; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use codex_config::test_support::CloudConfigBundleFixture; use codex_core::TurnInputRequest; +use codex_exec_server::ExecParams; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::ModeKind; @@ -13,8 +14,11 @@ use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::FileSystemSandboxPolicyContext; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_protocol::permissions::ReadDenyMatcher; +use codex_protocol::permissions::project_roots_glob_pattern; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::EventMsg; @@ -44,6 +48,9 @@ use pretty_assertions::assert_eq; use serde_json::Value; use serde_json::json; use std::fs; +use std::sync::Arc; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; use std::time::Duration; use test_case::test_case; use tokio::net::TcpListener; @@ -65,6 +72,11 @@ const REPLAY_RETAINED_OUTPUT_SEQ: u64 = 800; enum PushedExecScenario { Complete, DirectDenied, + DenyWin, + DenyWinUnchanged, + DenyOldLinux, + DenyOldLinuxWithRoot(FileSystemAccessMode), + DenyLinux, ElevatedPowerShell, RejectedLongWindowsDangerousCommand, SandboxedInterceptedPatch, @@ -156,18 +168,34 @@ async fn respond_environment_info( ) { let shell = if matches!( scenario, - PushedExecScenario::ElevatedPowerShell + PushedExecScenario::DenyWin + | PushedExecScenario::DenyWinUnchanged + | PushedExecScenario::ElevatedPowerShell | PushedExecScenario::RejectedLongWindowsDangerousCommand ) { json!({ "name": "powershell", "path": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" }) } else { json!({ "name": "zsh", "path": "/bin/zsh" }) }; - let platform_os = matches!( + let platform_os = match scenario { + PushedExecScenario::DenyWin + | PushedExecScenario::DenyWinUnchanged + | PushedExecScenario::RejectedLongWindowsDangerousCommand => Some("windows"), + PushedExecScenario::DenyOldLinux + | PushedExecScenario::DenyOldLinuxWithRoot(_) + | PushedExecScenario::DenyLinux => Some("linux"), + _ => None, + }; + let mut capabilities = json!({ "networkProxyLaunch": true }); + if matches!( scenario, - PushedExecScenario::RejectedLongWindowsDangerousCommand - ) - .then_some("windows"); + PushedExecScenario::DenyOldLinuxWithRoot(_) | PushedExecScenario::DenyLinux + ) { + capabilities["linuxRootWritePreservesDevices"] = json!(true); + } + if matches!(scenario, PushedExecScenario::DenyLinux) { + capabilities["linuxApprovedRootWritePreservesRestrictions"] = json!(true); + } send_exec_server_json( websocket, json!({ @@ -175,7 +203,7 @@ async fn respond_environment_info( "result": { "shell": shell, "platformOs": platform_os, - "capabilities": { "networkProxyLaunch": true } + "capabilities": capabilities, } }), ) @@ -185,6 +213,7 @@ async fn respond_environment_info( async fn serve_exec_with_pushed_events( listener: TcpListener, scenario: PushedExecScenario, + process_started: Arc, ) -> PushedExecServerResult { let unrestricted_patch = matches!( scenario, @@ -198,7 +227,10 @@ async fn serve_exec_with_pushed_events( // The runtime may still be finishing local setup before its first tool call. let request = read_exec_server_json(&mut websocket, STARTUP_TIMEOUT).await; match request["method"].as_str() { - Some("process/start") => break request, + Some("process/start") => { + process_started.store(true, Ordering::SeqCst); + break request; + } Some("environment/info") => { respond_environment_info(&mut websocket, &request["id"], scenario).await; } @@ -372,7 +404,13 @@ async fn serve_exec_with_pushed_events( .await; match scenario { - PushedExecScenario::Complete | PushedExecScenario::ElevatedPowerShell => { + PushedExecScenario::Complete + | PushedExecScenario::DenyWin + | PushedExecScenario::DenyWinUnchanged + | PushedExecScenario::DenyOldLinux + | PushedExecScenario::DenyOldLinuxWithRoot(_) + | PushedExecScenario::DenyLinux + | PushedExecScenario::ElevatedPowerShell => { let encoded_output = BASE64_STANDARD.encode(COMPLETE_OUTPUT); for message in [ json!({ @@ -475,8 +513,13 @@ async fn serve_exec_with_pushed_events( "failure": null, "sandboxDenied": true, }), - PushedExecScenario::ElevatedPowerShell => { - panic!("elevated remote PowerShell must not read a remote process") + PushedExecScenario::DenyWin + | PushedExecScenario::DenyWinUnchanged + | PushedExecScenario::DenyOldLinux + | PushedExecScenario::DenyOldLinuxWithRoot(_) + | PushedExecScenario::DenyLinux + | PushedExecScenario::ElevatedPowerShell => { + panic!("completed remote PowerShell must not read a remote process") } PushedExecScenario::RejectedLongWindowsDangerousCommand => { panic!("dangerous command must not read a remote process") @@ -571,6 +614,12 @@ async fn serve_exec_with_pushed_events( #[test_case(PushedExecScenario::Complete, ManagedNetworkScenario::Disabled, false ; "disabled_managed_network_omits_executor_proxy_launch")] #[cfg_attr(not(windows), test_case(PushedExecScenario::Complete, ManagedNetworkScenario::Enabled { policy_callbacks: true }, true ; "foreign_windows_managed_network_preserves_approval_registration"))] #[cfg_attr(not(windows), test_case(PushedExecScenario::Complete, ManagedNetworkScenario::None, true ; "foreign_windows_workspace_sandbox"))] +#[test_case(PushedExecScenario::DenyWin, ManagedNetworkScenario::None, true ; "old_windows_deny_read_allows_approved_escalation")] +#[test_case(PushedExecScenario::DenyWinUnchanged, ManagedNetworkScenario::None, true ; "old_windows_deny_read_keeps_unmaterializable_policy_unchanged")] +#[cfg_attr(not(windows), test_case(PushedExecScenario::DenyOldLinux, ManagedNetworkScenario::None, false ; "old_linux_deny_read_rejects_approved_escalation"))] +#[cfg_attr(not(windows), test_case(PushedExecScenario::DenyOldLinuxWithRoot(FileSystemAccessMode::Deny), ManagedNetworkScenario::None, false ; "old_linux_deny_read_keeps_root_deny_unchanged"))] +#[cfg_attr(not(windows), test_case(PushedExecScenario::DenyOldLinuxWithRoot(FileSystemAccessMode::Write), ManagedNetworkScenario::None, false ; "device_only_linux_deny_read_rejects_unchanged_writable_root_metadata"))] +#[cfg_attr(not(windows), test_case(PushedExecScenario::DenyLinux, ManagedNetworkScenario::None, false ; "linux_deny_read_allows_approved_escalation"))] #[test_case(PushedExecScenario::ElevatedPowerShell, ManagedNetworkScenario::None, true ; "windows_elevated_powershell_disables_profile")] #[cfg_attr(not(windows), test_case(PushedExecScenario::RejectedLongWindowsDangerousCommand, ManagedNetworkScenario::None, true ; "remote_windows_dangerous_command_rejection_is_bounded"))] #[cfg_attr(not(windows), test_case(PushedExecScenario::SandboxedInterceptedPatch, ManagedNetworkScenario::None, true ; "foreign_windows_intercepted_patch_is_sandboxed"))] @@ -593,9 +642,27 @@ async fn exec_command_consumes_pushed_remote_process_events( policy_callbacks: true } ); + let deny_read = matches!( + scenario, + PushedExecScenario::DenyWin + | PushedExecScenario::DenyWinUnchanged + | PushedExecScenario::DenyOldLinux + | PushedExecScenario::DenyOldLinuxWithRoot(_) + | PushedExecScenario::DenyLinux + ); let listener = TcpListener::bind("127.0.0.1:0").await?; let server = start_mock_server().await; let tool_call = match scenario { + _ if deny_read => ev_function_call( + CALL_ID, + "exec_command", + &json!({ + "cmd": "codex-deny-read-escalation-test", + "sandbox_permissions": "require_escalated", + "justification": "Can I run outside the workspace?", + }) + .to_string(), + ), PushedExecScenario::SandboxedDirectPatch | PushedExecScenario::SandboxedDirectPatchDenied | PushedExecScenario::SandboxedDirectPatchRetry => ev_apply_patch_custom_tool_call( @@ -642,7 +709,12 @@ async fn exec_command_consumes_pushed_remote_process_events( ) .await; let exec_server_url = format!("ws://{}", listener.local_addr()?); - let exec_server = tokio::spawn(serve_exec_with_pushed_events(listener, scenario)); + let process_started = Arc::new(AtomicBool::new(false)); + let exec_server = tokio::spawn(serve_exec_with_pushed_events( + listener, + scenario, + Arc::clone(&process_started), + )); let mut builder = test_codex().with_exec_server_url(exec_server_url); if managed_network_configured { let cloud_config_bundle = match managed_network { @@ -705,7 +777,8 @@ timeout = 900 } let mut builder = builder.with_config(move |config| { config.project_doc_max_bytes = 0; - if matches!(scenario, PushedExecScenario::ElevatedPowerShell) { + if matches!(scenario, PushedExecScenario::ElevatedPowerShell) || (foreign_cwd && deny_read) + { config.set_windows_elevated_sandbox_enabled(/*value*/ true); } if managed_network_configured { @@ -719,6 +792,45 @@ timeout = 900 let turn_permission_profile = if managed_network_configured { test.session_configured.permission_profile.clone() + } else if deny_read { + let mut filesystem = FileSystemSandboxPolicy::read_only(); + if let PushedExecScenario::DenyOldLinuxWithRoot(access) = scenario { + filesystem.entries[0].access = access; + } + filesystem.entries.extend( + [ + FileSystemPath::Special { + value: FileSystemSpecialPath::project_roots(Some("secret.txt".into())), + }, + FileSystemPath::GlobPattern { + pattern: project_roots_glob_pattern("*.token".as_ref()), + }, + ] + .map(|path| FileSystemSandboxEntry::new(path, FileSystemAccessMode::Deny)), + ); + if matches!(scenario, PushedExecScenario::DenyWinUnchanged) { + filesystem.entries.push(FileSystemSandboxEntry::new( + FileSystemPath::Special { + value: FileSystemSpecialPath::Tmpdir, + }, + FileSystemAccessMode::Deny, + )); + } + if matches!( + scenario, + PushedExecScenario::DenyOldLinuxWithRoot(FileSystemAccessMode::Write) + ) { + let cwd = PathUri::parse("file:///workspace")?; + let context = FileSystemSandboxPolicyContext { + cwd: &cwd, + workspace_roots: std::slice::from_ref(&cwd), + user_home_dir: None, + temporary_directories: None, + }; + filesystem = filesystem.for_approved_command(&context); + assert_eq!(filesystem.for_approved_command(&context), filesystem); + } + PermissionProfile::from_runtime_permissions(&filesystem, NetworkSandboxPolicy::Enabled) } else if matches!(scenario, PushedExecScenario::FullDiskInterceptedPatch) { PermissionProfile::from_runtime_permissions( &FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry::new( @@ -757,7 +869,7 @@ timeout = 900 PermissionProfile::Disabled }; let (sandbox_policy, permission_profile) = - turn_permission_fields(turn_permission_profile, test.config.cwd.as_path()); + turn_permission_fields(turn_permission_profile.clone(), test.config.cwd.as_path()); test.codex .start_or_steer_turn( TurnInputRequest::user_input(vec![UserInput::Text { @@ -765,18 +877,26 @@ timeout = 900 text_elements: Vec::new(), }]) .with_thread_settings(ThreadSettingsOverrides { - environments: foreign_cwd.then(|| { - let cwd = PathUri::parse("file:///C:/workspace").expect("valid Windows cwd"); + environments: (foreign_cwd || deny_read).then(|| { + let cwd = PathUri::parse(if foreign_cwd { + "file:///C:/workspace" + } else { + "file:///workspace" + }) + .expect("valid executor cwd"); + let mut workspace_roots = vec![cwd.clone()]; + if foreign_cwd { + workspace_roots.push( + PathUri::parse("file:///D:/other-workspace") + .expect("valid Windows workspace root"), + ); + } TurnEnvironmentSelections::new( test.config.cwd.clone(), vec![TurnEnvironmentSelection { environment_id: codex_exec_server::REMOTE_ENVIRONMENT_ID.to_string(), - cwd: cwd.clone(), - workspace_roots: vec![ - cwd, - PathUri::parse("file:///D:/other-workspace") - .expect("valid Windows workspace root"), - ], + cwd, + workspace_roots, config: EnvironmentConfigState::FromThread, }], ) @@ -788,6 +908,7 @@ timeout = 900 PushedExecScenario::SandboxedDirectPatchDenied | PushedExecScenario::SandboxedDirectPatchRetry ) + || deny_read { AskForApproval::OnRequest } else { @@ -820,6 +941,16 @@ timeout = 900 EventMsg::ExecCommandBegin(event) if event.call_id == CALL_ID => { saw_exec_command_begin = true; } + EventMsg::ExecApprovalRequest(approval) if deny_read => { + assert_eq!(approval.call_id, CALL_ID); + test.codex + .submit(Op::ExecApproval { + id: approval.effective_approval_id(), + turn_id: None, + decision: ReviewDecision::Approved, + }) + .await?; + } EventMsg::ApplyPatchApprovalRequest(approval) if matches!( scenario, @@ -849,6 +980,24 @@ timeout = 900 } } } + if matches!( + scenario, + PushedExecScenario::DenyOldLinux + | PushedExecScenario::DenyOldLinuxWithRoot(FileSystemAccessMode::Write) + ) { + let request = response_mock.last_request().context("upgrade response")?; + let (output, _) = request + .function_call_output_content_and_success(CALL_ID) + .context("upgrade output")?; + assert!( + output + .expect("upgrade rejection should include output") + .contains("upgrade the exec-server") + ); + assert!(!process_started.load(Ordering::SeqCst)); + exec_server.abort(); + return Ok(()); + } if matches!( scenario, PushedExecScenario::RejectedLongWindowsDangerousCommand @@ -998,7 +1147,7 @@ timeout = 900 "toolCallId": CALL_ID, }), ); - if foreign_cwd && !managed_network_configured { + if foreign_cwd && !managed_network_configured && !deny_read { let permissions = response_mock.requests()[0] .message_input_texts("developer") .join("\n") @@ -1036,10 +1185,40 @@ timeout = 900 .is_some_and(|argv| argv.iter().any(|arg| arg == "-NoProfile")), "elevated remote PowerShell must not load a user profile" ); + } else if deny_read { + assert_eq!(params["sandbox"]["windowsSandboxLevel"], "elevated"); } else { assert_eq!(params["sandbox"]["windowsSandboxLevel"], "restricted-token"); } } + if deny_read { + let params = &exec_server_result.process_start["params"]; + let params: ExecParams = serde_json::from_value(params.clone())?; + let sandbox = params.sandbox.context("sandbox with denied reads")?; + let context = sandbox.policy_context(); + let policy = sandbox.permissions.file_system_sandbox_policy(); + let policy = policy.materialize_project_roots_with_path_uris(context.workspace_roots); + if matches!( + scenario, + PushedExecScenario::DenyWinUnchanged | PushedExecScenario::DenyOldLinuxWithRoot(_) + ) { + let baseline = turn_permission_profile.file_system_sandbox_policy(); + assert_eq!( + policy, + baseline.materialize_project_roots_with_path_uris(context.workspace_roots) + ); + assert!(!policy.can_write_path(&context.cwd.join("../outside")?, &context)); + } else { + let matcher = + ReadDenyMatcher::from_context(&policy, &context).context("denied reads")?; + assert!(policy.can_write_path(&context.cwd.join("../outside")?, &context)); + for root in context.workspace_roots { + for path in ["secret.txt", "private.token"] { + assert!(matcher.is_read_denied_uri(&root.join(path)?, &context)); + } + } + } + } if managed_network_enabled { let params = &exec_server_result.process_start["params"]; assert_eq!(params["enforceManagedNetwork"], true); @@ -1078,7 +1257,13 @@ timeout = 900 let output = output.context("exec_command output should contain text")?; let process_read_requests = exec_server_result.process_read_requests; match scenario { - PushedExecScenario::Complete | PushedExecScenario::ElevatedPowerShell => { + PushedExecScenario::Complete + | PushedExecScenario::DenyWin + | PushedExecScenario::DenyWinUnchanged + | PushedExecScenario::DenyOldLinux + | PushedExecScenario::DenyOldLinuxWithRoot(_) + | PushedExecScenario::DenyLinux + | PushedExecScenario::ElevatedPowerShell => { assert_ne!(success, Some(false)); assert!(saw_exec_command_begin); assert!(output.contains("Process exited with code 0")); diff --git a/codex-rs/core/tests/suite/unified_exec_stdin_approval.rs b/codex-rs/core/tests/suite/unified_exec_stdin_approval.rs index a8e769e1fdc2..6df66e35ffa6 100644 --- a/codex-rs/core/tests/suite/unified_exec_stdin_approval.rs +++ b/codex-rs/core/tests/suite/unified_exec_stdin_approval.rs @@ -9,6 +9,10 @@ use codex_features::Feature; use codex_protocol::approvals::ExecApprovalKind; use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::PermissionProfile; +use codex_protocol::permissions::FileSystemAccessMode; +use codex_protocol::permissions::FileSystemPath; +use codex_protocol::permissions::FileSystemSandboxEntry; +use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::GranularApprovalConfig; @@ -192,6 +196,123 @@ async fn stdin_reviews_retained_grants_after_turn_permissions_expire() -> Result Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn stdin_reviews_escalated_terminal_with_denied_reads_before_delivering_input() -> Result<()> +{ + skip_if_target_windows!(Ok(()), "uses a POSIX interactive shell"); + skip_if_no_network!(Ok(())); + skip_if_sandbox!(Ok(())); + let harness = TestCodexHarness::with_auto_env_builder(test_codex().with_config(|config| { + config.features.enable(Feature::WriteStdinApproval).unwrap(); + config + .features + .enable(Feature::ExecPermissionApprovals) + .unwrap(); + config.features.enable(Feature::UnifiedExec).unwrap(); + config.features.disable(Feature::ShellZshFork).unwrap(); + config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); + let mut file_system = PermissionProfile::read_only().file_system_sandbox_policy(); + file_system.entries.push(FileSystemSandboxEntry::new( + FileSystemPath::GlobPattern { + pattern: "**/secret.env".into(), + }, + FileSystemAccessMode::Deny, + )); + config + .permissions + .set_permission_profile(PermissionProfile::from_runtime_permissions( + &file_system, + NetworkSandboxPolicy::Restricted, + )) + .unwrap(); + })) + .await?; + let codex = &harness.test().codex; + harness.write_file("secret.env", "secret").await?; + let _open = mount_function_call_agent_response( + harness.server(), + "open", + &json!({"cmd":"/bin/bash --noprofile --norc", "tty":true, "yield_time_ms":200, "sandbox_permissions":"require_escalated", "justification":"open an escalated terminal"}).to_string(), + "exec_command", + ).await; + codex + .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { + text: "open terminal".into(), + text_elements: Vec::new(), + }])) + .await?; + let launch = wait_for_event_match(codex, |event| match event { + EventMsg::ExecApprovalRequest(request) => Some(request.clone()), + _ => None, + }) + .await; + assert_eq!(launch.kind, ExecApprovalKind::Command); + codex + .submit(Op::ExecApproval { + id: launch.effective_approval_id(), + turn_id: Some(launch.turn_id), + decision: ReviewDecision::Approved, + }) + .await?; + wait_for_event(codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; + + let _writes = mount_sse_sequence(harness.server(), vec![ + tool_response("denied", "write_stdin", json!({"session_id":1000,"chars":"printf denied > denied-marker\n"})), + tool_response("allowed", "write_stdin", json!({"session_id":1000,"chars":"if test -e denied-marker || cat secret.env; then exit 12; fi; printf approved > approved-marker; exit\n"})), + sse(vec![ev_completed("done")]), + ]).await; + codex + .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { + text: "send input".into(), + text_elements: Vec::new(), + }])) + .await?; + for (id, decision) in [ + ("denied", ReviewDecision::denied("blocked input")), + ("allowed", ReviewDecision::Approved), + ] { + let request = wait_for_event_match(codex, |event| match event { + EventMsg::ExecApprovalRequest(request) => Some(request.clone()), + _ => None, + }) + .await; + assert_eq!( + ( + request.kind, + request.call_id.as_str(), + request.effective_approval_id().as_str() + ), + (ExecApprovalKind::WriteStdin, "open", id) + ); + assert!(!harness.path_exists("denied-marker").await?); + assert!(!harness.path_exists("approved-marker").await?); + codex + .submit(Op::ExecApproval { + id: request.effective_approval_id(), + turn_id: Some(request.turn_id), + decision, + }) + .await?; + } + let mut process_end = None; + let mut turn_completed = false; + while process_end.is_none() || !turn_completed { + match wait_for_event(codex, |_| true).await { + EventMsg::ExecCommandEnd(event) if event.call_id == "open" => process_end = Some(event), + EventMsg::TurnComplete(_) => turn_completed = true, + EventMsg::ExecApprovalRequest(request) => { + anyhow::bail!("unexpected additional stdin approval: {request:?}") + } + _ => {} + } + } + let process_end = process_end.expect("retained terminal should finish"); + assert_eq!(process_end.exit_code, 0); + assert!(!harness.path_exists("denied-marker").await?); + assert_eq!(harness.read_file_text("approved-marker").await?, "approved"); + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn strict_stdin_review_reaches_guardian_with_sandbox_prompts_disabled() -> Result<()> { skip_if_target_windows!(Ok(()), "uses a POSIX interactive shell"); diff --git a/codex-rs/core/tests/suite/windows_sandbox.rs b/codex-rs/core/tests/suite/windows_sandbox.rs index b9d7b085a886..ea830aed64e5 100644 --- a/codex-rs/core/tests/suite/windows_sandbox.rs +++ b/codex-rs/core/tests/suite/windows_sandbox.rs @@ -1,4 +1,7 @@ use anyhow::Context; +use codex_config::types::ApprovalsReviewer; +use codex_core::TurnInputRequest; +use codex_core::config::Constrained; use codex_core::exec::ExecCapturePolicy; use codex_core::exec::ExecParams; use codex_core::exec::process_exec_tool_call; @@ -17,6 +20,11 @@ use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::EventMsg; +use codex_protocol::protocol::Op; +use codex_protocol::protocol::ReviewDecision; +use codex_protocol::user_input::UserInput; use core_test_support::PathExt; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -26,14 +34,18 @@ use core_test_support::responses::mount_sse_sequence; use core_test_support::responses::sse; use core_test_support::test_codex::TestCodexHarness; use core_test_support::test_codex::test_codex; +use core_test_support::wait_for_event; +use core_test_support::wait_for_event_with_timeout; use pretty_assertions::assert_eq; use serde_json::json; use serial_test::serial; use std::collections::HashMap; use std::ffi::OsString; +use std::fs::File; use std::path::Path; use std::path::PathBuf; use std::process::Command; +use std::time::Duration; use tempfile::TempDir; struct EnvVarGuard { @@ -63,20 +75,32 @@ impl Drop for EnvVarGuard { } enum TestCodexHome { - Persistent(PathBuf), - Temporary(TempDir), + Persistent(PathBuf, File), + Temporary(TempDir, File), } impl TestCodexHome { fn path(&self) -> &Path { match self { - Self::Persistent(path) => path.as_path(), - Self::Temporary(temp_dir) => temp_dir.path(), + Self::Persistent(path, _account_lock) => path.as_path(), + Self::Temporary(temp_dir, _account_lock) => temp_dir.path(), } } } fn codex_home_for_windows_sandbox_test(name: &str) -> anyhow::Result { + // Bazel shards and nextest processes share Windows accounts. Hold this lock + // through the test so they cannot rotate each other's credentials mid-launch. + let lock_path = dirs::data_local_dir() + .context("resolve local app data for the Windows sandbox test lock")? + .join("codex-windows-sandbox-integration-test.lock"); + let lock = File::options() + .read(true) + .append(true) + .create(true) + .open(lock_path)?; + lock.lock().context("lock Windows sandbox test accounts")?; + if let Some(test_tmpdir) = std::env::var_os("TEST_TMPDIR") { // The elevated backend provisions machine-local sandbox users. Bazel // retries run in the same Windows VM, so keep CODEX_HOME stable within @@ -84,10 +108,10 @@ fn codex_home_for_windows_sandbox_test(name: &str) -> anyhow::Result anyhow::Result<()> { @@ -825,3 +849,154 @@ async fn windows_elevated_unified_exec_enforces_large_recursive_deny_reads() -> Ok(()) } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[serial(codex_home)] +async fn windows_elevated_approved_git_pull_preserves_deny_read() -> anyhow::Result<()> { + use EventMsg::ExecApprovalRequest; + use EventMsg::ExecCommandEnd; + use EventMsg::TurnComplete; + + let codex_home = codex_home_for_windows_sandbox_test("windows-elevated-git-pull-deny-read")?; + let _codex_home_guard = EnvVarGuard::set("CODEX_HOME", codex_home.path().as_os_str()); + stage_windows_sandbox_helpers()?; + let home = dunce::canonicalize(codex_home.path())?.abs(); + let builder = test_codex() + .with_windows_cmd_shell() + .with_config(move |config| { + config.codex_home = home; + config.set_windows_elevated_sandbox_enabled(true); + config.prefer_mxc = false; + config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); + config.approvals_reviewer = ApprovalsReviewer::User; + config.features.enable(Feature::UnifiedExec).unwrap(); + let mut filesystem = FileSystemSandboxPolicy::workspace_write( + &[], + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ true, + ); + filesystem.entries.extend( + [ + config.cwd.join("exact-secret.txt").into(), + FileSystemPath::GlobPattern { + pattern: "**/*.env".into(), + }, + ] + .map(|path| FileSystemSandboxEntry::new(path, FileSystemAccessMode::Deny)), + ); + config + .permissions + .set_permission_profile(PermissionProfile::from_runtime_permissions( + &filesystem, + NetworkSandboxPolicy::Restricted, + )) + .unwrap(); + }) + .with_workspace_setup(|cwd, _fs| async move { + std::fs::write(cwd.join("exact-secret.txt"), "exact secret")?; + std::fs::write(cwd.join("secret.env"), "glob secret")?; + Ok(()) + }); + let harness = TestCodexHarness::with_builder(builder).await?; + let git = |cwd: &Path, args: &[&str]| -> anyhow::Result { + let output = Command::new("git") + .args(["-c", "commit.gpgsign=false"]) + .current_dir(cwd) + .args(args) + .output()?; + anyhow::ensure!( + output.status.success(), + "git {args:?}: {}", + String::from_utf8_lossy(&output.stderr) + ); + Ok(String::from_utf8(output.stdout)?.trim().to_string()) + }; + let upstream = TempDir::new()?; + let upstream = upstream.path(); + let upstream_path = upstream.to_str().context("UTF-8 upstream path")?; + let cwd = harness.cwd(); + let read = |name| std::fs::read_to_string(cwd.join(name)); + let init = ["init", "--initial-branch=main"]; + let commit = ["commit", "-am", "fixture"]; + git(upstream, &init)?; + git(upstream, &["config", "user.name", "Codex Test"])?; + git(upstream, &["config", "user.email", "codex@test.com"])?; + std::fs::write(upstream.join("tracked.txt"), "before\n")?; + git(upstream, &["add", "tracked.txt"])?; + git(upstream, &commit)?; + git(cwd, &init)?; + git(cwd, &["pull", "-q", "--ff-only", upstream_path, "main"])?; + let fetch_head = cwd.join(".git/FETCH_HEAD"); + std::fs::remove_file(&fetch_head)?; + std::fs::write(upstream.join("tracked.txt"), "after\n")?; + git(upstream, &commit)?; + let expected = git(upstream, &["rev-parse", "HEAD"])?; + let bundle = cwd.join("upstream.bundle"); + let bundle_path = bundle.to_str().context("UTF-8 bundle path")?; + git(upstream, &["bundle", "create", bundle_path, "--all"])?; + + let args = json!({"cmd": concat!( + "git pull --quiet --ff-only .\\upstream.bundle main || exit /b 10 & ", + "type tracked.txt >NUL || exit /b 11 & ", + "type exact-secret.txt >NUL 2>NUL && exit /b 12 & ", + "type secret.env >NUL 2>NUL && exit /b 13 & ", + "echo git-and-denials-ok>git-and-denials-ok.txt || exit /b 14 & exit /b 0" + ), "sandbox_permissions": "require_escalated", + "justification": "Can I pull the fixture repository?", + "yield_time_ms": 30_000, "tty": false, "login": false, + }); + let call_id = "git-pull"; + core_test_support::responses::mount_function_call_agent_response( + harness.server(), + call_id, + &args.to_string(), + "exec_command", + ) + .await; + let codex = &harness.test().codex; + codex + .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { + text: "pull the fixture repository".into(), + text_elements: Vec::new(), + }])) + .await?; + let approval = wait_for_event(codex, |event| { + matches!(event, ExecApprovalRequest(_) | TurnComplete(_)) + }) + .await; + let ExecApprovalRequest(approval) = approval else { + anyhow::bail!("expected approval before git ran: {approval:?}"); + }; + assert_eq!(approval.call_id, call_id); + assert!(!fetch_head.exists()); + assert!(!cwd.join("git-and-denials-ok.txt").exists()); + assert_eq!(read("tracked.txt")?.trim(), "before"); + codex + .submit(Op::ExecApproval { + id: approval.effective_approval_id(), + turn_id: None, + decision: ReviewDecision::Approved, + }) + .await?; + let mut process_end = None; + let mut turn_completed = false; + while process_end.is_none() || !turn_completed { + match wait_for_event_with_timeout(codex, |_| true, Duration::from_secs(120)).await { + ExecCommandEnd(event) if event.call_id == call_id => process_end = Some(event), + TurnComplete(_) => turn_completed = true, + ExecApprovalRequest(request) => { + anyhow::bail!("unexpected additional Git approval: {request:?}") + } + _ => {} + } + } + let result = process_end.expect("approved Git process should finish"); + let output = &result.aggregated_output; + assert_eq!(result.exit_code, 0, "sandbox output: {output}"); + assert_eq!(read("git-and-denials-ok.txt")?.trim(), "git-and-denials-ok"); + assert!(read(".git/FETCH_HEAD")?.starts_with(&expected)); + assert_eq!(read("tracked.txt")?.trim(), "after"); + assert_eq!(read("exact-secret.txt")?, "exact secret"); + assert_eq!(read("secret.env")?, "glob secret"); + Ok(()) +} From 1983c48fd186614529e4fad418ac38a1ae00c6b6 Mon Sep 17 00:00:00 2001 From: Benjamin Carlsson Date: Tue, 29 Sep 2026 20:06:27 +0000 Subject: [PATCH 02/24] Continue Markdown blockquotes when pasting multiline text (#49357) ## Why Pasting multiple lines into a blockquote previously left subsequent lines unquoted. ## What changed - Continue the current line's `> ` prefix across pasted lines, including blank and trailing lines, in the TUI composer and embedded answer fields. - Append two unquoted newlines so typing resumes in the next Markdown block. Preserve quoting when large paste placeholders expand on submission. - Use a shared edit target for paste context and insertion so selection replacement uses the prefix at the replacement start. - Keep shell input and provisional startup pastes literal. ## Testing Add unit tests and rendering snapshots for blockquote pastes, embedded answers, cursor placement, selection replacement, and large-paste expansion. Add textarea tests for forward and backward selections, adjacent elements, and Vim Replace backspace recovery. GitOrigin-RevId: 525a7951aa16a25d49cc3be98822d0dd0709de97 --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 9 +- .../chat_composer/blockquote_paste_tests.rs | 144 ++++++++++++++++++ .../bottom_pane/chat_composer/paste_input.rs | 47 +++++- ...hat_composer__tests__blockquote_paste.snap | 14 ++ ...sts__blockquote_paste_embedded_answer.snap | 14 ++ codex-rs/tui/src/bottom_pane/textarea.rs | 25 +-- .../tui/src/bottom_pane/textarea/editing.rs | 81 ++++++++++ .../src/bottom_pane/textarea/editing_tests.rs | 74 +++++++++ .../tui/src/bottom_pane/textarea/mouse.rs | 10 -- codex-rs/tui/src/startup_draft.rs | 6 +- 10 files changed, 380 insertions(+), 44 deletions(-) create mode 100644 codex-rs/tui/src/bottom_pane/chat_composer/blockquote_paste_tests.rs create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__blockquote_paste.snap create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__blockquote_paste_embedded_answer.snap create mode 100644 codex-rs/tui/src/bottom_pane/textarea/editing.rs create mode 100644 codex-rs/tui/src/bottom_pane/textarea/editing_tests.rs diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 72722559fcac..0988bf7cd154 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -114,6 +114,7 @@ //! //! # Submission and Prompt Expansion //! +//! Multiline pastes continue blockquote prefixes. //! `Enter` submits immediately. `Tab` requests queuing while a task is running; if no task is //! running, `Tab` submits just like Enter so input is never dropped. //! Vim Replace shares Insert's composer actions; only textarea editing differs. @@ -562,6 +563,7 @@ pub(crate) struct ChatComposerConfig { pub(crate) shell_commands_enabled: bool, /// Whether pasting a file path can attach local images. pub(crate) image_paste_enabled: bool, + pub(crate) blockquote_paste_enabled: bool, /// Strip leading and trailing whitespace from submissions. pub(crate) trim_submission: bool, /// Embedded editors reset Vim only when their owner accepts the answer. @@ -575,6 +577,7 @@ impl Default for ChatComposerConfig { slash_commands_enabled: true, shell_commands_enabled: true, image_paste_enabled: true, + blockquote_paste_enabled: true, trim_submission: true, reset_vim_on_submission: true, } @@ -582,16 +585,14 @@ impl Default for ChatComposerConfig { } impl ChatComposerConfig { - /// A minimal preset for plain-text inputs embedded in other surfaces. - /// - /// This disables popups, slash and shell commands, and image-path attachment behavior - /// so the composer behaves like a simple notes field. + /// Text answers support Markdown, without popups, commands, or image attachments. pub(crate) const fn plain_text() -> Self { Self { popups_enabled: false, slash_commands_enabled: false, shell_commands_enabled: false, image_paste_enabled: false, + blockquote_paste_enabled: true, trim_submission: true, reset_vim_on_submission: true, } diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/blockquote_paste_tests.rs b/codex-rs/tui/src/bottom_pane/chat_composer/blockquote_paste_tests.rs new file mode 100644 index 000000000000..fcd393739ab0 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/chat_composer/blockquote_paste_tests.rs @@ -0,0 +1,144 @@ +use super::super::tests::new_test_composer; +use super::super::tests::snapshot_composer_state_with_width; +use super::super::*; +use crossterm::event::MouseEvent; +use crossterm::event::MouseEventKind; +use pretty_assertions::assert_eq; + +#[test] +fn blockquote_paste_continues_current_line_prefix() { + for (draft, pasted, expected) in [ + ("> ", "first\nsecond", "> first\n> second\n\n"), + ("intro\n> ", "α\r\n\r\nβ\r", "intro\n> α\n> \n> β\n> \n\n"), + ( + "> existing ", + "first\nsecond", + "> existing first\n> second\n\n", + ), + ("text > ", "first\nsecond", "text > first\nsecond"), + (">", "first\nsecond", ">first\nsecond"), + ( + "> quote\nplain ", + "first\nsecond", + "> quote\nplain first\nsecond", + ), + ("> ", "single", "> single"), + ("!> ", "out\npwd", "!> out\npwd"), + (" !echo hi\n> ", "out\npwd", " !echo hi\n> out\npwd"), + ] { + let (mut composer, _rx) = new_test_composer(); + composer.insert_str(draft); + composer.handle_paste(pasted.to_string()); + assert_eq!(composer.current_text(), expected); + } +} + +#[test] +fn blockquote_paste_uses_cursor_line_and_preserves_suffix() { + let (mut composer, _rx) = new_test_composer(); + composer.insert_str("intro\n> suffix\nend"); + composer.draft.textarea.set_cursor("intro\n> ".len()); + composer.handle_paste("first\nsecond".to_string()); + assert_eq!( + composer.draft.textarea.text(), + "intro\n> first\n> second\n\nsuffix\nend" + ); + composer.insert_str("next "); + assert_eq!( + composer.current_text(), + "intro\n> first\n> second\n\nnext suffix\nend" + ); +} + +#[test] +fn blockquote_paste_large_expands_quoted_text_on_submit() { + let (mut composer, _rx) = new_test_composer(); + composer.insert_str("> "); + let first = "x".repeat(LARGE_PASTE_CHAR_THRESHOLD); + composer.handle_paste(format!("{first}\nsecond")); + let quoted = format!("{first}\n> second"); + let placeholder = format!("[Pasted Content {} chars]", quoted.chars().count()); + assert_eq!( + composer.draft.textarea.text(), + format!("> {placeholder}\n\n") + ); + assert_eq!( + composer.draft.pending_pastes, + vec![(placeholder, quoted.clone())] + ); + composer.insert_str("Next block"); + let (result, _) = composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + let InputResult::Submitted { text, .. } = result else { + panic!("expected Submitted"); + }; + assert_eq!(text, format!("> {quoted}\n\nNext block")); +} + +#[test] +fn blockquote_paste_rendered() { + snapshot_composer_state_with_width( + "blockquote_paste", + /*width*/ 60, + /*enhanced_keys_supported*/ false, + |composer| { + composer.insert_str("Please explain:\n> "); + composer.handle_paste("first line\n\nlast line".to_string()); + composer.insert_str("Next block"); + }, + ); +} + +#[test] +fn blockquote_paste_replacing_selected_prefix_stays_literal() { + let (mut composer, _rx) = new_test_composer(); + composer.insert_str("> selected"); + let area = Rect::new( + /*x*/ 0, /*y*/ 0, /*width*/ 80, /*height*/ 10, + ); + composer.render(area, &mut Buffer::empty(area)); + let (x, y) = composer.cursor_pos(area).unwrap(); + for (kind, column) in [ + ( + MouseEventKind::Down(crossterm::event::MouseButton::Left), + x - 10, + ), + (MouseEventKind::Drag(crossterm::event::MouseButton::Left), x), + ] { + let event = MouseEvent { + kind, + column, + row: y, + modifiers: KeyModifiers::NONE, + }; + assert!(composer.prepare_mouse(event)); + composer.render(area, &mut Buffer::empty(area)); + assert!(composer.handle_mouse(event)); + } + composer.handle_paste("first\nsecond".to_string()); + assert_eq!(composer.current_text(), "first\nsecond"); +} + +#[test] +fn blockquote_paste_embedded_answer_starts_next_block() { + let (mut composer, _rx) = new_test_composer(); + composer.config = ChatComposerConfig::plain_text(); + composer.insert_str("> "); + composer.handle_paste("first\nsecond".to_string()); + composer.insert_str("Next block"); + assert_eq!(composer.current_text(), "> first\n> second\n\nNext block"); +} + +#[test] +fn blockquote_paste_embedded_answer_rendered() { + snapshot_composer_state_with_width( + "blockquote_paste_embedded_answer", + /*width*/ 60, + /*enhanced_keys_supported*/ false, + |composer| { + composer.config = ChatComposerConfig::plain_text(); + composer.insert_str("> "); + composer.handle_paste("first line\n\nlast line".to_string()); + composer.insert_str("Next block"); + }, + ); +} diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/paste_input.rs b/codex-rs/tui/src/bottom_pane/chat_composer/paste_input.rs index 48ea632c1c35..eecc926631d3 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer/paste_input.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer/paste_input.rs @@ -1,20 +1,29 @@ //! Classify and integrate composer pastes and capture raw paste tabs before completion or submission. +//! Multiline pastes on a line beginning with `> ` continue that prefix after every newline, +//! including blank and trailing lines. This happens before large pastes are collapsed, so their +//! expanded text retains the quoting. Two unquoted newlines after the inserted text or placeholder +//! leave the cursor in the next Markdown block, including in embedded answer fields. +//! Provisional startup input, search queries, and shell input remain literal. +//! Paste classification and insertion share one textarea edit target, including selection replacement. use super::*; use crate::bottom_pane::paste_burst::FlushResult; +use crate::bottom_pane::textarea::EditTarget; impl ChatComposer { pub(crate) fn insert_str(&mut self, text: &str) { + self.insert_str_at_target(self.draft.textarea.edit_target(), text); + } + + fn insert_str_at_target(&mut self, target: EditTarget, text: &str) { if !text.is_empty() && self.sparkle.draft.get() == sparkle::SparkleDraft::Untouched { self.dismiss_sparkle(); } let started_vim_edit = self.begin_direct_vim_edit(); - let elements_before = self - .draft - .textarea - .mouse_selection_range() - .map(|_| self.draft.textarea.element_payloads()); - self.draft.textarea.insert_str(text); + let elements_before = target + .replaces_text() + .then(|| self.draft.textarea.element_payloads()); + self.draft.textarea.insert_str_at_target(target, text); if let Some(elements_before) = elements_before { self.reconcile_deleted_elements(elements_before); } @@ -130,10 +139,24 @@ impl ChatComposer { } let started_vim_edit = self.begin_direct_vim_edit(); let elements_before = self.draft.textarea.element_payloads(); + let target = self.draft.textarea.edit_target(); + let before_cursor = &self.draft.textarea.text()[..target.start()]; + let current_line = before_cursor.rsplit('\n').next().unwrap_or_default(); + let is_blockquote = self.config.blockquote_paste_enabled + && !self.is_bang_shell_command() + && current_line.starts_with("> ") + && pasted.contains('\n'); + let pasted = if is_blockquote { + std::borrow::Cow::Owned(pasted.replace('\n', "\n> ")) + } else { + pasted + }; let char_count = pasted.chars().count(); if char_count > LARGE_PASTE_CHAR_THRESHOLD { let placeholder = self.next_large_paste_placeholder(char_count); - self.draft.textarea.insert_element(&placeholder); + self.draft + .textarea + .insert_element_at_target(target, &placeholder); self.draft .pending_pastes .push((placeholder, pasted.into_owned())); @@ -144,7 +167,11 @@ impl ChatComposer { let cursor = self.draft.textarea.cursor(); self.draft.textarea.insert_str_at(cursor, " "); } else { - self.insert_str(&pasted); + self.insert_str_at_target(target, &pasted); + } + if is_blockquote { + let textarea = &mut self.draft.textarea; + textarea.insert_str_at(textarea.cursor(), "\n\n"); } self.draft.paste_burst.clear_after_explicit_paste(); self.reconcile_deleted_elements(elements_before); @@ -191,3 +218,7 @@ impl ChatComposer { false } } + +#[cfg(test)] +#[path = "blockquote_paste_tests.rs"] +mod blockquote_paste_tests; diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__blockquote_paste.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__blockquote_paste.snap new file mode 100644 index 000000000000..69bbc367af1e --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__blockquote_paste.snap @@ -0,0 +1,14 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +assertion_line: 5302 +expression: terminal.backend() +--- +" " +"› Please explain: " +" > first line " +" > " +" > last line " +" " +" Next block " +" " +" 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__blockquote_paste_embedded_answer.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__blockquote_paste_embedded_answer.snap new file mode 100644 index 000000000000..e53f1d94ce6b --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__blockquote_paste_embedded_answer.snap @@ -0,0 +1,14 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +assertion_line: 5302 +expression: terminal.backend() +--- +" " +"› > first line " +" > " +" > last line " +" " +" Next block " +" " +" " +" 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/textarea.rs b/codex-rs/tui/src/bottom_pane/textarea.rs index 60bd8e7e7068..51f988b83069 100644 --- a/codex-rs/tui/src/bottom_pane/textarea.rs +++ b/codex-rs/tui/src/bottom_pane/textarea.rs @@ -16,6 +16,7 @@ //! Visible web URLs carry their complete terminal hyperlink destination across wrapped rows; //! masked rendering never exposes hyperlink destinations. //! Mouse selection uses those same visual rows and keeps graphemes and elements atomic. +//! The editing module resolves replacement targets shared by insertion and paste-context inspection. use crate::key_hint::KeyBindingListExt; use crate::key_hint::is_altgr; @@ -50,12 +51,14 @@ use std::ops::Range; use std::sync::Arc; use unicode_segmentation::UnicodeSegmentation; +mod editing; mod hyperlinks; mod mouse; mod vim; mod vim_commands; mod vim_search; mod wrapping; + use self::vim::VimMode; use self::vim::VimMotion; use self::vim::VimOperator; @@ -67,6 +70,7 @@ use self::vim_commands::VimCommandState; use self::vim_commands::VimEditTarget; use self::vim_commands::VimInsertPosition; pub(crate) use self::vim_commands::VimPersistentState; +pub(crate) use editing::EditTarget; const WORD_SEPARATORS: &str = "`~!@#$%^&*()-=+[{]}\\|;:'\",.<>/?"; @@ -441,16 +445,6 @@ impl TextArea { &self.text } - pub fn insert_str(&mut self, text: &str) { - let replaced_selection = !text.is_empty() && self.delete_mouse_selection(); - self.record_vim_inserted_text(text); - if self.is_vim_replace_mode() && !replaced_selection { - self.replace_vim_text(text); - } else { - self.insert_str_at(self.cursor_pos, text); - } - } - pub fn insert_str_at(&mut self, pos: usize, text: &str) { self.mouse_selection = None; self.clear_vim_replace_recovery(); @@ -1723,17 +1717,6 @@ impl TextArea { true } - pub fn insert_element(&mut self, text: &str) -> u64 { - self.delete_mouse_selection(); - let start = self.clamp_pos_for_insertion(self.cursor_pos); - self.insert_str_at(start, text); - let end = start + text.len(); - let id = self.add_element(start..end); - // Place cursor at end of inserted element - self.set_cursor(end); - id - } - fn add_element(&mut self, range: Range) -> u64 { let id = self.next_element_id(); self.elements.push(TextElement { id, range }); diff --git a/codex-rs/tui/src/bottom_pane/textarea/editing.rs b/codex-rs/tui/src/bottom_pane/textarea/editing.rs new file mode 100644 index 000000000000..6c97ea69ddfc --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/textarea/editing.rs @@ -0,0 +1,81 @@ +//! Resolve the replacement range once for context-sensitive edits, then consume it on insertion. +//! Targets respect character and atomic-element boundaries and must be used before another edit +//! or cursor/selection change. Text insertion retains Vim Replace and repeat policies; atomic +//! elements always replace the target literally. + +use super::*; + +/// The selection to replace, or an empty range at the insertion cursor. +/// Vim Replace without a selection computes its overwrite span as it inserts each grapheme. +#[derive(Debug)] +pub(crate) struct EditTarget { + range: Range, +} + +impl EditTarget { + pub(crate) fn start(&self) -> usize { + self.range.start + } + + pub(crate) fn replaces_text(&self) -> bool { + !self.range.is_empty() + } +} + +impl TextArea { + pub(crate) fn edit_target(&self) -> EditTarget { + let range = self.mouse_selection_range().map_or_else( + || { + let cursor = self.clamp_pos_for_insertion(self.cursor_pos); + cursor..cursor + }, + |range| self.expand_range_to_element_boundaries(range), + ); + EditTarget { range } + } + + pub fn insert_str(&mut self, text: &str) { + self.insert_str_at_target(self.edit_target(), text); + } + + pub(crate) fn insert_str_at_target(&mut self, target: EditTarget, text: &str) { + // Empty text must not delete a selection or create a Vim replacement step. + if text.is_empty() { + if !self.is_vim_replace_mode() { + self.insert_str_at(self.cursor_pos, text); + } + return; + } + let replaces_selection = target.replaces_text(); + if replaces_selection { + // Pointer-selected edits cannot be replayed as keyboard-relative Vim commands. + self.vim_commands = VimCommandState::default(); + } + self.record_vim_inserted_text(text); + if self.is_vim_replace_mode() && !replaces_selection { + self.replace_vim_text(text); + } else { + self.replace_range(target.range, text); + } + } + + pub fn insert_element(&mut self, text: &str) -> u64 { + self.insert_element_at_target(self.edit_target(), text) + } + + pub(crate) fn insert_element_at_target(&mut self, target: EditTarget, text: &str) -> u64 { + let start = target.start(); + if target.replaces_text() { + self.vim_commands = VimCommandState::default(); + } + self.replace_range(target.range, text); + let end = start + text.len(); + let id = self.add_element(start..end); + self.set_cursor(end); + id + } +} + +#[cfg(test)] +#[path = "editing_tests.rs"] +mod tests; diff --git a/codex-rs/tui/src/bottom_pane/textarea/editing_tests.rs b/codex-rs/tui/src/bottom_pane/textarea/editing_tests.rs new file mode 100644 index 000000000000..8df7f7234d14 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/textarea/editing_tests.rs @@ -0,0 +1,74 @@ +use super::*; +use crossterm::event::MouseButton; +use crossterm::event::MouseEvent; +use crossterm::event::MouseEventKind; +use pretty_assertions::assert_eq; + +#[test] +fn target_replaces_forward_and_backward_selections() { + for (anchor, cursor) in [(1, 4), (4, 1)] { + let mut textarea = TextArea::new(); + textarea.insert_str("abcdef"); + let area = Rect::new( + /*x*/ 0, /*y*/ 0, /*width*/ 20, /*height*/ 2, + ); + let mut state = TextAreaState::default(); + StatefulWidgetRef::render_ref(&&textarea, area, &mut Buffer::empty(area), &mut state); + for (kind, column) in [ + (MouseEventKind::Down(MouseButton::Left), anchor), + (MouseEventKind::Drag(MouseButton::Left), cursor), + ] { + assert!(textarea.handle_mouse( + MouseEvent { + kind, + column, + row: 0, + modifiers: KeyModifiers::NONE + }, + state, + )); + } + let target = textarea.edit_target(); + assert_eq!(&textarea.text()[..target.start()], "a"); + textarea.insert_str_at_target(target, "界"); + assert_eq!((textarea.text(), textarea.cursor()), ("a界ef", "a界".len())); + assert_eq!(textarea.mouse_selection_range(), None); + } +} + +#[test] +fn target_element_insertion_preserves_adjacent_elements() { + let mut textarea = TextArea::new(); + textarea.insert_str("before "); + textarea.insert_element("[old]"); + textarea.insert_str(" after"); + textarea.set_cursor("before ".len()); + let target = textarea.edit_target(); + textarea.insert_element_at_target(target, "[new]"); + assert_eq!( + ( + textarea.text(), + textarea.cursor(), + textarea.element_payloads() + ), + ( + "before [new][old] after", + "before [new]".len(), + vec!["[new]".to_string(), "[old]".to_string()] + ), + ); +} + +#[test] +fn target_text_preserves_vim_replace_and_backspace_recovery() { + let mut textarea = TextArea::new(); + textarea.insert_str("abc"); + textarea.set_cursor(/*pos*/ 0); + textarea.set_vim_enabled(/*enabled*/ true); + textarea.input(KeyCode::Char('R').into()); + let target = textarea.edit_target(); + textarea.insert_str_at_target(target, "界"); + assert_eq!((textarea.text(), textarea.cursor()), ("界bc", "界".len())); + textarea.input(KeyCode::Backspace.into()); + assert_eq!((textarea.text(), textarea.cursor()), ("abc", 0)); +} diff --git a/codex-rs/tui/src/bottom_pane/textarea/mouse.rs b/codex-rs/tui/src/bottom_pane/textarea/mouse.rs index 00c2c2adfedb..ccbe221a6779 100644 --- a/codex-rs/tui/src/bottom_pane/textarea/mouse.rs +++ b/codex-rs/tui/src/bottom_pane/textarea/mouse.rs @@ -63,16 +63,6 @@ impl TextArea { (!range.is_empty()).then_some(range) } - pub(super) fn delete_mouse_selection(&mut self) -> bool { - let Some(range) = self.mouse_selection_range() else { - return false; - }; - // A pointer-selected edit cannot be replayed as a keyboard-relative Vim command. - self.vim_commands = VimCommandState::default(); - self.replace_range(range, ""); - true - } - pub(crate) fn handle_mouse(&mut self, event: MouseEvent, state: TextAreaState) -> bool { let area = self.rendered_area.get(); let dragging = self.mouse_selection.as_ref().is_some_and(|s| s.dragging); diff --git a/codex-rs/tui/src/startup_draft.rs b/codex-rs/tui/src/startup_draft.rs index 96ffcecd709c..258e78070c56 100644 --- a/codex-rs/tui/src/startup_draft.rs +++ b/codex-rs/tui/src/startup_draft.rs @@ -530,7 +530,11 @@ fn startup_draft_bottom_pane( effects: Default::default(), skills: None, }, - ChatComposerConfig::plain_text(), + ChatComposerConfig { + // Keep partial pastes literal until the startup draft is handed off. + blockquote_paste_enabled: false, + ..ChatComposerConfig::plain_text() + }, ); bottom_pane.set_context_window_pending(/*pending*/ true); bottom_pane From 995138d71ac06b9df5996f049e69dea414ad9764 Mon Sep 17 00:00:00 2001 From: "Adam Perry @ OpenAI" Date: Tue, 29 Sep 2026 20:27:36 +0000 Subject: [PATCH 03/24] Carry shell invocation metadata and report executor PATH directories (#49360) ## What changed - Carry the selected shell and login mode through unified exec using `ShellInvocation`. Use this metadata for shell snapshots and credential brokerage instead of inferring it from command arguments. - Add `prependPathDirs` to exec-server environment metadata, populated from the executor's package PATH directory. Default to an empty list for older servers and omit empty lists from serialized output. ## Testing Extend environment metadata round-trip coverage for `prependPathDirs` and the remote exec-server integration test to verify the packaged `codex-path` directory is reported. GitOrigin-RevId: c571c068effcb305d196a701ad9a335ec7a3f5a2 --- codex-rs/Cargo.lock | 1 + codex-rs/cli/tests/exec_server.rs | 11 +++++ codex-rs/core/src/shell.rs | 17 +++++++ .../core/src/tools/handlers/unified_exec.rs | 41 ++++++++--------- .../handlers/unified_exec/exec_command.rs | 3 +- .../src/tools/handlers/unified_exec_tests.rs | 2 +- .../core/src/tools/runtimes/unified_exec.rs | 46 +++++++++++-------- codex-rs/core/src/unified_exec/mod.rs | 4 +- .../core/src/unified_exec/process_manager.rs | 2 +- .../src/unified_exec/process_manager_tests.rs | 11 ++++- .../core/src/unified_exec/shell_snapshot.rs | 11 ++--- codex-rs/exec-server-protocol/Cargo.toml | 1 + codex-rs/exec-server-protocol/src/protocol.rs | 13 ++++++ 13 files changed, 107 insertions(+), 56 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index e8b1096596fb..c9162751bb88 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -3250,6 +3250,7 @@ version = "0.0.0" dependencies = [ "base64 0.22.1", "codex-file-system", + "codex-install-context", "codex-mxc-sandbox", "codex-network-proxy", "codex-protocol", diff --git a/codex-rs/cli/tests/exec_server.rs b/codex-rs/cli/tests/exec_server.rs index 75bb8344cfb5..001a69e61988 100644 --- a/codex-rs/cli/tests/exec_server.rs +++ b/codex-rs/cli/tests/exec_server.rs @@ -187,6 +187,8 @@ metrics_exporter = {{ otlp-http = {{ endpoint = "{collector_url}/v1/metrics", pr std::fs::create_dir(&bin_dir)?; let executable = bin_dir.join(format!("codex{}", std::env::consts::EXE_SUFFIX)); copy_executable(&codex_utils_cargo_bin::cargo_bin("codex")?, &executable)?; + let codex_path_dir = package.path().join("codex-path"); + std::fs::create_dir(&codex_path_dir)?; let manifest = package.path().join("codex-package.json"); std::fs::write(&manifest, r#"{"version":"1.2.3-alpha.4"}"#)?; @@ -250,10 +252,19 @@ metrics_exporter = {{ otlp-http = {{ endpoint = "{collector_url}/v1/metrics", pr .context("remote harness did not connect")???; let environment_info = client.environment_info().await?; + assert_eq!( + environment_info + .prepend_path_dirs + .iter() + .map(|path| std::fs::canonicalize(path.inferred_native_path_string())) + .collect::>>()?, + vec![codex_path_dir.canonicalize()?] + ); let expected_info = EnvironmentInfo { executor_version: "1.2.3-alpha.4".to_string(), // The build identity belongs to the spawned CLI, not this test process. provider_id: environment_info.provider_id.clone(), + prepend_path_dirs: environment_info.prepend_path_dirs.clone(), ..EnvironmentInfo::local() }; assert_eq!(environment_info, expected_info); diff --git a/codex-rs/core/src/shell.rs b/codex-rs/core/src/shell.rs index 37a35bfe3f44..c253383610fa 100644 --- a/codex-rs/core/src/shell.rs +++ b/codex-rs/core/src/shell.rs @@ -76,6 +76,23 @@ impl Shell { } } +/// The shell and startup mode Codex chose for an exec command. +#[derive(Debug, Clone)] +pub(crate) struct ShellInvocation { + pub(crate) shell: Shell, + pub(crate) use_login_shell: bool, +} + +impl ShellInvocation { + pub(crate) fn is_posix_login(&self) -> bool { + self.use_login_shell + && matches!( + self.shell.shell_type, + ShellType::Bash | ShellType::Zsh | ShellType::Sh + ) + } +} + #[cfg(all(test, unix))] fn ultimate_fallback_shell() -> Shell { codex_shell_command::shell_detect::ultimate_fallback_shell().into() diff --git a/codex-rs/core/src/tools/handlers/unified_exec.rs b/codex-rs/core/src/tools/handlers/unified_exec.rs index 9f9f8a8cf1ea..763fa155766d 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec.rs @@ -1,5 +1,6 @@ use crate::sandboxing::SandboxPermissions; use crate::shell::Shell; +use crate::shell::ShellInvocation; use crate::shell::ShellType; use crate::shell::get_shell_by_model_provided_path; use crate::tools::context::ToolInvocation; @@ -74,7 +75,7 @@ fn default_tty() -> bool { #[derive(Debug)] pub(crate) struct ResolvedCommand { pub(crate) command: Vec, - pub(crate) shell_type: ShellType, + pub(crate) shell: ShellInvocation, } fn post_unified_exec_tool_use_payload( @@ -112,35 +113,31 @@ pub(crate) fn get_command( None => allow_login_shell, }; - match shell_mode { - UnifiedExecShellMode::Direct => { - let model_shell = args - .shell - .as_ref() - .map(|shell_str| get_shell_by_model_provided_path(&PathBuf::from(shell_str))); - let shell = model_shell.as_ref().unwrap_or(session_shell.as_ref()); - Ok(ResolvedCommand { - command: shell.derive_exec_args(&args.cmd, use_login_shell), - shell_type: shell.shell_type, - }) - } + let shell = match shell_mode { + UnifiedExecShellMode::Direct => args + .shell + .as_ref() + .map(|shell_str| get_shell_by_model_provided_path(&PathBuf::from(shell_str))) + .unwrap_or_else(|| session_shell.as_ref().clone()), UnifiedExecShellMode::ZshFork(zsh_fork_config) => { if args.shell.is_some() { return Err( "`shell` is not supported for local zsh-fork exec; omit `shell` to use zsh-fork, or target a remote environment where `shell` is supported.".to_string(), ); } - - Ok(ResolvedCommand { - command: vec![ - zsh_fork_config.shell_zsh_path.to_string_lossy().to_string(), - if use_login_shell { "-lc" } else { "-c" }.to_string(), - args.cmd.clone(), - ], + Shell { shell_type: ShellType::Zsh, - }) + shell_path: zsh_fork_config.shell_zsh_path.as_path().to_path_buf(), + } } - } + }; + Ok(ResolvedCommand { + command: shell.derive_exec_args(&args.cmd, use_login_shell), + shell: ShellInvocation { + shell, + use_login_shell, + }, + }) } pub(crate) fn shell_mode_for_environment( diff --git a/codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs b/codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs index bdf6877389ff..ba5dd79efba7 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs @@ -297,7 +297,6 @@ impl ExecCommandHandler { ) .map_err(FunctionCallError::RespondToModel)?; let command = resolved_command.command; - let shell_type = resolved_command.shell_type; let ExecCommandArgs { mut tty, yield_time_ms, @@ -418,7 +417,7 @@ impl ExecCommandHandler { let process_id = manager.allocate_process_id().await; let request = ExecCommandRequest { command, - shell_type, + shell: resolved_command.shell, hook_command: hook_command.clone(), process_id, yield_time_ms, diff --git a/codex-rs/core/src/tools/handlers/unified_exec_tests.rs b/codex-rs/core/src/tools/handlers/unified_exec_tests.rs index 46fa63dbbb5a..4657b64fbcb1 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec_tests.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec_tests.rs @@ -130,7 +130,7 @@ fn test_get_command_resolves_powershell_by_type() -> anyhow::Result<()> { resolved.command, expected_shell.derive_exec_args("echo hello", /*use_login_shell*/ true) ); - assert_eq!(resolved.shell_type, expected_shell.shell_type); + assert_eq!(resolved.shell.shell, expected_shell); Ok(()) } diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index 4c662e02b3a9..c6ef29c606e7 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -14,6 +14,7 @@ use crate::sandboxing::ExecOptions; use crate::sandboxing::ExecServerEnvConfig; use crate::sandboxing::SandboxPermissions; use crate::session::turn_context::TurnEnvironment; +use crate::shell::ShellInvocation; use crate::shell::ShellType; use crate::shell_snapshot::ShellSnapshotSandbox; use crate::shell_snapshot::snapshot_read_permissions; @@ -58,7 +59,6 @@ use codex_tools::UnifiedExecShellMode; use codex_utils_path_uri::PathUri; use std::collections::HashMap; use std::io; -use std::path::PathBuf; use std::time::Duration; use tokio_util::sync::CancellationToken; @@ -74,7 +74,7 @@ const REMOTE_NETWORK_POLICY_DECISION_MARGIN: Duration = Duration::from_secs(10); #[derive(Clone, Debug)] pub struct UnifiedExecRequest { pub command: Vec, - pub shell_type: ShellType, + pub(crate) shell: ShellInvocation, pub hook_command: String, pub process_id: i32, pub cwd: PathUri, @@ -271,6 +271,7 @@ impl<'a> ToolRuntime for UnifiedExecRunt ctx: &ToolCtx, ) -> Result { let base_command = &req.command; + let requested_shell = &req.shell.shell; let windows_sandbox_proxy_settings_mode = ctx.session.windows_sandbox_proxy_settings_mode; let session_shell = ctx.session.user_shell(); let environment_shell = req @@ -306,19 +307,11 @@ impl<'a> ToolRuntime for UnifiedExecRunt "credential brokerage does not yet support shell_zsh_fork; disable shell_zsh_fork to use the broker".to_string(), )); } - let requested_shell = (credential_broker_available - && (req.shell_type != environment_shell.shell_type - || base_command.first().is_some_and(|path| { - path != environment_shell.shell_path.to_string_lossy().as_ref() - }))) - .then(|| { - base_command.first().map(|path| crate::shell::Shell { - shell_type: req.shell_type, - shell_path: PathBuf::from(path), - }) - }) - .flatten(); - let shell = requested_shell.as_ref().unwrap_or(environment_shell); + let shell = if credential_broker_available { + requested_shell + } else { + environment_shell + }; let shell_snapshot = if environment_is_remote || credential_broker_available && launch_sandbox_permissions.requires_escalated_permissions() @@ -348,7 +341,7 @@ impl<'a> ToolRuntime for UnifiedExecRunt let shell_snapshot_location = shell_snapshot.as_ref().map(|snapshot| snapshot.path()); let mut env = exec_env_for_sandbox_permissions(&req.env, launch_sandbox_permissions); let snapshot_credential_context = if let Some(snapshot) = shell_snapshot.as_ref() - && (managed_network.is_some() || base_command.get(1).is_some_and(|flag| flag == "-lc")) + && (managed_network.is_some() || req.shell.is_posix_login()) { Some( snapshot @@ -576,7 +569,7 @@ impl<'a> ToolRuntime for UnifiedExecRunt // preparation is a no-op for PowerShell on other platforms. let command = prepare_powershell_command_for_windows_sandbox( &command, - Some(&req.shell_type), + Some(&requested_shell.shell_type), attempt.sandbox_requested, executor_windows_sandbox_selection( attempt.windows_sandbox_type, @@ -585,7 +578,7 @@ impl<'a> ToolRuntime for UnifiedExecRunt ), environment_is_remote, ); - let command = if matches!(req.shell_type, ShellType::PowerShell) { + let command = if matches!(requested_shell.shell_type, ShellType::PowerShell) { prefix_powershell_script_with_utf8(&command) } else { command @@ -761,6 +754,7 @@ mod tests { use crate::config::PermissionProfileSnapshot; use crate::environment_selection::EnvironmentConfigOrigin; use crate::exec::DEFAULT_EXEC_COMMAND_TIMEOUT_MS; + use crate::shell::Shell; use crate::tools::sandboxing::ToolRuntime; use codex_exec_server::Environment; use codex_exec_server::LOCAL_ENVIRONMENT_ID; @@ -864,7 +858,13 @@ mod tests { let runtime = UnifiedExecRuntime::new(&manager, UnifiedExecShellMode::Direct); let request = UnifiedExecRequest { command: vec!["pwd".to_string()], - shell_type: ShellType::Sh, + shell: ShellInvocation { + shell: Shell { + shell_type: ShellType::Sh, + shell_path: "sh".into(), + }, + use_login_shell: false, + }, hook_command: "pwd".to_string(), process_id: 1000, cwd: cwd.into(), @@ -967,7 +967,13 @@ mod tests { .expect("current dir is absolute"); UnifiedExecRequest { command: vec!["zsh".to_string(), "-c".to_string(), "echo hi".to_string()], - shell_type: ShellType::Zsh, + shell: ShellInvocation { + shell: Shell { + shell_type: ShellType::Zsh, + shell_path: "zsh".into(), + }, + use_login_shell: false, + }, hook_command: "echo hi".to_string(), process_id: 1000, cwd: cwd.clone().into(), diff --git a/codex-rs/core/src/unified_exec/mod.rs b/codex-rs/core/src/unified_exec/mod.rs index 3645306c27c8..28af9b02290f 100644 --- a/codex-rs/core/src/unified_exec/mod.rs +++ b/codex-rs/core/src/unified_exec/mod.rs @@ -43,7 +43,7 @@ use crate::session::session::Session; use crate::session::step_context::StepContext; use crate::session::turn_context::TurnContext; use crate::session::turn_context::TurnEnvironment; -use crate::shell::ShellType; +use crate::shell::ShellInvocation; use crate::tools::network_approval::DeferredNetworkApproval; use codex_core_plugins::PluginMetricsSidecar; @@ -113,7 +113,7 @@ impl UnifiedExecContext { #[derive(Debug)] pub(crate) struct ExecCommandRequest { pub command: Vec, - pub shell_type: ShellType, + pub shell: ShellInvocation, pub hook_command: String, pub process_id: i32, pub yield_time_ms: u64, diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index 3dc3e97d9124..565194441f2a 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -1505,7 +1505,7 @@ impl UnifiedExecProcessManager { .await; let req = UnifiedExecToolRequest { command: request.command.clone(), - shell_type: request.shell_type, + shell: request.shell.clone(), hook_command: request.hook_command.clone(), process_id: request.process_id, cwd, diff --git a/codex-rs/core/src/unified_exec/process_manager_tests.rs b/codex-rs/core/src/unified_exec/process_manager_tests.rs index 7edc743ce422..ce56681e68b4 100644 --- a/codex-rs/core/src/unified_exec/process_manager_tests.rs +++ b/codex-rs/core/src/unified_exec/process_manager_tests.rs @@ -1,4 +1,7 @@ use super::*; +use crate::shell::Shell; +use crate::shell::ShellInvocation; +use crate::shell::ShellType; use crate::unified_exec::clamp_yield_time; use codex_network_proxy::ManagedNetworkSandboxContext; use core_test_support::assert_regex_match; @@ -459,7 +462,13 @@ async fn failed_initial_end_for_unstored_process_uses_fallback_output() { "-lc".to_string(), "echo before".to_string(), ], - shell_type: crate::shell::ShellType::Sh, + shell: ShellInvocation { + shell: Shell { + shell_type: ShellType::Sh, + shell_path: "sh".into(), + }, + use_login_shell: true, + }, hook_command: "echo before".to_string(), process_id: 123, yield_time_ms: 1000, diff --git a/codex-rs/core/src/unified_exec/shell_snapshot.rs b/codex-rs/core/src/unified_exec/shell_snapshot.rs index dc79ea461947..8deffbaba9a2 100644 --- a/codex-rs/core/src/unified_exec/shell_snapshot.rs +++ b/codex-rs/core/src/unified_exec/shell_snapshot.rs @@ -179,15 +179,12 @@ pub(super) fn shell_snapshot_request( || !request.turn_environment.shell_snapshot_v2_supported || request.turn_environment.selection.cwd != *cwd || !matches!(request.shell_mode, UnifiedExecShellMode::Direct) - || !matches!( - request.shell_type, - ShellType::Bash | ShellType::Zsh | ShellType::Sh - ) - || request.command.get(1).is_none_or(|flag| flag != "-lc") + || !request.shell.is_posix_login() { return None; } + let shell = &request.shell.shell; Some(ShellSnapshotRequest { scope_id: format!( "{}:{}", @@ -195,8 +192,8 @@ pub(super) fn shell_snapshot_request( request.turn_environment.selection.environment_id ), shell: ShellInfo { - name: request.shell_type.name().to_string(), - path: request.command.first()?.clone(), + name: shell.name().to_string(), + path: shell.shell_path.to_string_lossy().into_owned(), }, }) } diff --git a/codex-rs/exec-server-protocol/Cargo.toml b/codex-rs/exec-server-protocol/Cargo.toml index 62841ed97314..eda1add2726e 100644 --- a/codex-rs/exec-server-protocol/Cargo.toml +++ b/codex-rs/exec-server-protocol/Cargo.toml @@ -15,6 +15,7 @@ workspace = true [dependencies] base64 = { workspace = true } codex-file-system = { workspace = true } +codex-install-context = { workspace = true } codex-network-proxy = { workspace = true } codex-protocol = { workspace = true } codex-shell-command = { workspace = true } diff --git a/codex-rs/exec-server-protocol/src/protocol.rs b/codex-rs/exec-server-protocol/src/protocol.rs index 84b266432fa0..97402d5e1260 100644 --- a/codex-rs/exec-server-protocol/src/protocol.rs +++ b/codex-rs/exec-server-protocol/src/protocol.rs @@ -6,6 +6,7 @@ use codex_file_system::FileSystemSandboxContext; pub use codex_file_system::WalkOptions; pub use codex_file_system::WalkOutcome; use codex_file_system::WireFileSystemSandboxContext; +use codex_install_context::InstallContext; use codex_network_proxy::ManagedNetworkSandboxContext; use codex_network_proxy::RemoteNetworkProxyLaunchConfig; use codex_protocol::ThreadId; @@ -116,6 +117,9 @@ pub struct EnvironmentInfo { /// Operating system reported by the executor; absent for legacy exec-servers. #[serde(default, skip_serializing_if = "Option::is_none")] pub platform_os: Option, + /// Executor directories to prepend to `PATH` when missing, in priority order. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub prepend_path_dirs: Vec, /// Executor-local default directories for resolving `:tmpdir`, when reported. /// On Windows, a command's `TEMP` or `TMP` overrides take precedence. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -252,6 +256,13 @@ impl EnvironmentInfo { cwd: cwd.and_then(|cwd| PathUri::from_host_native_path(cwd).ok()), user_home_dir: PathUri::from_host_native_path("~").ok(), platform_os: Some(std::env::consts::OS.to_string()), + prepend_path_dirs: InstallContext::current() + .package_layout + .as_ref() + .and_then(|layout| layout.path_dir.as_ref()) + .into_iter() + .map(PathUri::from_abs_path) + .collect(), temporary_directories: Some(temporary_directories), temp_dir, capabilities: EnvironmentCapabilities { @@ -1372,6 +1383,7 @@ mod tests { cwd: None, user_home_dir: None, platform_os: None, + prepend_path_dirs: Vec::new(), temporary_directories: None, temp_dir: None, capabilities: EnvironmentCapabilities::default(), @@ -1447,6 +1459,7 @@ mod tests { "cwd": null, "userHomeDir": "file:///C:/Users/remote", "platformOs": "windows", + "prependPathDirs": ["file:///C:/tools/bin", "file:///D:/tools/bin"], "temporaryDirectories": ["file:///C:/Temp", "file:///D:/Temp"], "capabilities": { "networkProxyLaunch": false, From 94d642d8b40e45e2e544770f0d1f28df9a717f06 Mon Sep 17 00:00:00 2001 From: Celia Chen Date: Tue, 29 Sep 2026 20:34:44 +0000 Subject: [PATCH 04/24] Clarify credential storage wording across authentication UI and docs (#49361) ## Why Credentials can use a keyring backend, so describing all login and logout operations as reads or writes to `auth.json` is misleading. ## What changed - Remove the API-key onboarding claim that keys are stored in `auth.json`. - Describe authentication storage and credential-removal errors in terms of stored credentials and the selected backend. - Label the doctor output's auth-file path as the file backend or fallback, and document why startup checks also consider `config.toml` when credentials may be in a keyring. ## Testing Add API-key entry snapshots for terminal widths of 80 and 40 columns. GitOrigin-RevId: 87189f422448b88721ea2d7448930ece63102803 --- codex-rs/cli/src/doctor/output/detail.rs | 1 + codex-rs/login/src/auth/manager.rs | 27 ++++++++++--------- codex-rs/tui/src/onboarding/auth.rs | 25 ++++++++++++++++- ...nboarding__auth__tests__api_key_entry.snap | 15 +++++++++++ ...ng__auth__tests__api_key_entry_narrow.snap | 15 +++++++++++ codex-rs/tui/src/startup_preflight.rs | 4 +++ 6 files changed, 73 insertions(+), 14 deletions(-) create mode 100644 codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__auth__tests__api_key_entry.snap create mode 100644 codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__auth__tests__api_key_entry_narrow.snap diff --git a/codex-rs/cli/src/doctor/output/detail.rs b/codex-rs/cli/src/doctor/output/detail.rs index bca2baeaa49f..d7e730c3c5df 100644 --- a/codex-rs/cli/src/doctor/output/detail.rs +++ b/codex-rs/cli/src/doctor/output/detail.rs @@ -750,6 +750,7 @@ fn value<'a>(parsed: &'a [ParsedDetail], label: &str) -> Option<&'a str> { fn display_label(label: &str) -> String { match label { + "auth file" => "auth.json path (file backend/fallback)", "codex-linux-sandbox helper" => "linux helper", "optional reachability failed" => "optional reachability", "check for update on startup" => "startup update check", diff --git a/codex-rs/login/src/auth/manager.rs b/codex-rs/login/src/auth/manager.rs index 14cfee92212f..66c20a656fb9 100644 --- a/codex-rs/login/src/auth/manager.rs +++ b/codex-rs/login/src/auth/manager.rs @@ -976,8 +976,8 @@ fn read_non_empty_env_var(key: &str) -> Option { .filter(|value| !value.is_empty()) } -/// Delete the auth.json file inside `codex_home` if it exists. Returns `Ok(true)` -/// if a file was removed, `Ok(false)` if no auth file was present. +/// Delete credentials for `codex_home` through the selected storage backend. +/// Returns `Ok(true)` if credentials were removed, `Ok(false)` if none were present. pub fn logout( codex_home: &Path, auth_credentials_store_mode: AuthCredentialsStoreMode, @@ -1018,7 +1018,7 @@ pub async fn logout_with_revoke( ) } -/// Writes an `auth.json` that contains only the API key. +/// Store API-key credentials through the selected storage backend. pub fn login_with_api_key( codex_home: &Path, api_key: &str, @@ -1043,7 +1043,8 @@ pub fn login_with_api_key( ) } -/// Writes an `auth.json` that contains only the access token. +/// Validate and store personal-access-token or Agent Identity credentials through +/// the selected storage backend. pub async fn login_with_access_token( codex_home: &Path, access_token: &str, @@ -1454,7 +1455,7 @@ fn logout_with_message( ); let error_message = match removal_result { Ok(_) => message, - Err(err) => format!("{message}. Failed to remove auth.json: {err}"), + Err(err) => format!("{message}. Failed to remove stored credentials: {err}"), }; Err(std::io::Error::other(error_message)) } @@ -2031,12 +2032,12 @@ impl UnauthorizedRecovery { } } -/// Central manager providing a single source of truth for auth.json derived +/// Central manager providing a single source of truth for stored /// authentication data. It loads once (or on preference change) and then /// hands out cloned `CodexAuth` values so the rest of the program has a /// consistent snapshot. /// -/// External modifications to `auth.json` will NOT be observed until +/// External modifications to stored credentials will NOT be observed until /// `reload()` is called explicitly. This matches the design goal of avoiding /// different parts of the program seeing inconsistent auth data mid‑run. pub struct AuthManager { @@ -2939,10 +2940,10 @@ impl AuthManager { result } - /// Log out by deleting the on‑disk auth.json (if present). Returns Ok(true) - /// if a file was removed, Ok(false) if no auth file existed. On success, - /// reloads the in‑memory auth cache so callers immediately observe the - /// unauthenticated state. + /// Log out by deleting credentials from the selected storage backend and the + /// ephemeral store. Returns `Ok(true)` if credentials were removed, `Ok(false)` + /// if none were present. On success, reloads the in-memory auth cache so + /// callers immediately observe the unauthenticated state. pub async fn logout(&self) -> std::io::Result { self.ensure_logout_allowed()?; let removed = logout_all_stores( @@ -2950,7 +2951,7 @@ impl AuthManager { self.auth_credentials_store_mode, self.keyring_backend_kind, )?; - // Always reload to clear any cached auth (even if file absent). + // Always reload to clear cached auth, even if no stored credentials were present. self.clear_external_auth(); self.reload().await; Ok(removed) @@ -2970,7 +2971,7 @@ impl AuthManager { self.auth_credentials_store_mode, self.keyring_backend_kind, )?; - // Always reload to clear any cached auth (even if file absent). + // Always reload to clear cached auth, even if no stored credentials were present. self.clear_external_auth(); self.reload().await; Ok(result) diff --git a/codex-rs/tui/src/onboarding/auth.rs b/codex-rs/tui/src/onboarding/auth.rs index 29022d525beb..2cdc98ca68e4 100644 --- a/codex-rs/tui/src/onboarding/auth.rs +++ b/codex-rs/tui/src/onboarding/auth.rs @@ -704,7 +704,7 @@ impl AuthModeWidget { "Use your own OpenAI API key for usage-based billing".bold(), ]), "".into(), - " Paste or type your API key below. It will be stored locally in auth.json.".into(), + " Paste or type your API key below.".into(), "".into(), ]; if state.prepopulated_from_env { @@ -1277,6 +1277,29 @@ mod tests { (widget, codex_home) } + #[tokio::test] + async fn api_key_entry_snapshots() { + let (mut widget, _tmp) = widget_forced_chatgpt().await; + widget.auth_config.forced_login_method = None; + *widget.sign_in_state.write().unwrap() = + SignInState::ApiKeyEntry(ApiKeyInputState::default()); + + for (name, width) in [("api_key_entry", 80), ("api_key_entry_narrow", 40)] { + let height = 14; + let area = Rect::new(/*x*/ 0, /*y*/ 0, width, height); + let mut terminal = crate::custom_terminal::Terminal::with_options( + crate::test_backend::VT100Backend::new(width, height), + ) + .expect("terminal"); + terminal.set_viewport_area(area); + terminal + .draw(|frame| widget.render_ref(area, frame.buffer_mut())) + .expect("draw"); + + insta::assert_snapshot!(name, terminal.backend().to_string()); + } + } + #[tokio::test] async fn api_key_flow_disabled_when_chatgpt_forced() { let (mut widget, _tmp) = widget_forced_chatgpt().await; diff --git a/codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__auth__tests__api_key_entry.snap b/codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__auth__tests__api_key_entry.snap new file mode 100644 index 000000000000..376f264c5f20 --- /dev/null +++ b/codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__auth__tests__api_key_entry.snap @@ -0,0 +1,15 @@ +--- +source: tui/src/onboarding/auth.rs +expression: terminal.backend().to_string() +--- +> Use your own OpenAI API key for usage-based billing + + Paste or type your API key below. + + + +╭API key───────────────────────────────────────────────────────────────────────╮ +│Paste or type your API key │ +╰──────────────────────────────────────────────────────────────────────────────╯ + Press enter to save + Press esc to go back diff --git a/codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__auth__tests__api_key_entry_narrow.snap b/codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__auth__tests__api_key_entry_narrow.snap new file mode 100644 index 000000000000..499103514f6e --- /dev/null +++ b/codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__auth__tests__api_key_entry_narrow.snap @@ -0,0 +1,15 @@ +--- +source: tui/src/onboarding/auth.rs +expression: terminal.backend().to_string() +--- +> Use your own OpenAI API key for +usage-based billing + + Paste or type your API key below. + + +╭API key───────────────────────────────╮ +│Paste or type your API key │ +╰──────────────────────────────────────╯ + Press enter to save + Press esc to go back diff --git a/codex-rs/tui/src/startup_preflight.rs b/codex-rs/tui/src/startup_preflight.rs index 7741319b76cd..cd284e401bfb 100644 --- a/codex-rs/tui/src/startup_preflight.rs +++ b/codex-rs/tui/src/startup_preflight.rs @@ -45,6 +45,10 @@ pub(super) fn should_delay_startup_composer_for_first_login( match std::fs::metadata(codex_home) { Ok(metadata) if !metadata.is_dir() => return false, Ok(_) => { + // With file auth as the default, auth.json is a hint of existing credentials. + // An existing config.toml may select keyring storage, which need not create + // auth.json. Keep the composer visible when any of these files exists; + // the normal account lookup decides whether sign-in is required. for state_file in ["auth.json", "config.toml", "environments.toml"] { if !matches!(codex_home.join(state_file).try_exists(), Ok(false)) { return false; From 2a34aef79484bf769b4b225f3f595755f2962170 Mon Sep 17 00:00:00 2001 From: Celia Chen Date: Tue, 29 Sep 2026 21:12:49 +0000 Subject: [PATCH 05/24] Update Bedrock GPT-6 Sol catalog tests to expect multi-agent V2 (#49369) Update the runtime catalog test expectations for `global.openai.gpt-6-sol` and `us.openai.gpt-6-sol` from `MultiAgentVersion::V1` to `MultiAgentVersion::V2`. GitOrigin-RevId: 1b038ab988cae221a641ee54002bffc25109f45c --- .../src/amazon_bedrock/runtime_catalog_tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codex-rs/model-provider/src/amazon_bedrock/runtime_catalog_tests.rs b/codex-rs/model-provider/src/amazon_bedrock/runtime_catalog_tests.rs index f769d034810f..2b3e4e2d4a49 100644 --- a/codex-rs/model-provider/src/amazon_bedrock/runtime_catalog_tests.rs +++ b/codex-rs/model-provider/src/amazon_bedrock/runtime_catalog_tests.rs @@ -76,7 +76,7 @@ fn runtime_catalog_disables_web_search_without_overriding_review_models() { "global.openai.gpt-6-sol", None, false, - Some(MultiAgentVersion::V1), + Some(MultiAgentVersion::V2), ), ( "global.openai.gpt-6-luna", @@ -118,7 +118,7 @@ fn runtime_catalog_disables_web_search_without_overriding_review_models() { "us.openai.gpt-6-sol", None, false, - Some(MultiAgentVersion::V1), + Some(MultiAgentVersion::V2), ), ( "us.openai.gpt-6-luna", From bd4204efc24a888c6671681f5cd41fbf9377c163 Mon Sep 17 00:00:00 2001 From: jif Date: Tue, 29 Sep 2026 21:49:04 +0000 Subject: [PATCH 06/24] Compile hook matchers during discovery (#49379) ## Why Hook dispatch recompiles regex matchers for each input even though discovery already validates them. ## What changed Store compiled `HookMatcher` values on configured handlers and reuse them during dispatch. Preserve match-all, exact-name, pipe-alternative, and regex matching semantics, along with the original pattern spelling for configuration equality. Invalid regexes continue to be rejected during discovery. GitOrigin-RevId: 117a8a19c7bc0e55ccbedcd5940b806a17ebba10 --- codex-rs/hooks/src/engine/discovery.rs | 49 ++++--- codex-rs/hooks/src/engine/dispatcher.rs | 9 +- codex-rs/hooks/src/engine/matcher.rs | 49 +++++++ codex-rs/hooks/src/engine/mod.rs | 5 +- codex-rs/hooks/src/events/common.rs | 126 ++++++++++-------- codex-rs/hooks/src/events/post_tool_use.rs | 2 +- codex-rs/hooks/src/events/pre_tool_use.rs | 2 +- .../hooks/src/events/session_end_tests.rs | 3 +- 8 files changed, 158 insertions(+), 87 deletions(-) create mode 100644 codex-rs/hooks/src/engine/matcher.rs diff --git a/codex-rs/hooks/src/engine/discovery.rs b/codex-rs/hooks/src/engine/discovery.rs index 5003dbd24f08..0c5a28ef35ef 100644 --- a/codex-rs/hooks/src/engine/discovery.rs +++ b/codex-rs/hooks/src/engine/discovery.rs @@ -29,8 +29,8 @@ use super::HookListEntry; use super::HookListEntryHandler; use super::dispatcher::hook_event_name_label; use crate::config_rules::hook_states_from_stack; +use crate::engine::HookMatcher; use crate::events::common::matcher_pattern_for_event; -use crate::events::common::validate_matcher_pattern; use crate::events::session_end::SESSION_END_DEFAULT_TIMEOUT_SEC; use crate::events::session_end::SESSION_END_MAX_TIMEOUT_SEC; use crate::output_spill::AdditionalContextLimit; @@ -486,20 +486,22 @@ fn append_matcher_groups( ) { for (group_index, group) in groups.into_iter().enumerate() { let matcher = matcher_pattern_for_event(event_name, group.matcher.as_deref()); - if let Some(matcher) = matcher - && let Err(err) = validate_matcher_pattern(matcher) - { - let warning = format!( - "invalid matcher {matcher:?} in {}: {err}", - source.path.display() - ); - if group.hooks.is_empty() { - warnings.push(warning); - } else { - source.record_load_failure(warning, warnings); + let compiled_matcher = match matcher.map(HookMatcher::new).transpose() { + Ok(matcher) => matcher, + Err(err) => { + let matcher = matcher.unwrap_or_default(); + let warning = format!( + "invalid matcher {matcher:?} in {}: {err}", + source.path.display() + ); + if group.hooks.is_empty() { + warnings.push(warning); + } else { + source.record_load_failure(warning, warnings); + } + continue; } - continue; - } + }; for (handler_index, handler) in group.hooks.iter().cloned().enumerate() { let normalized = match handler { HookHandlerConfig::Command { @@ -720,7 +722,7 @@ fn append_matcher_groups( handlers.push(ConfiguredHandler { builtin, event_name, - matcher: matcher.map(ToOwned::to_owned), + matcher: compiled_matcher.clone(), timeout_sec, status_message, additional_context_limit: AdditionalContextLimit::from_config( @@ -862,6 +864,7 @@ fn hook_source_for_requirement_source(source: Option<&RequirementSource>) -> Hoo #[cfg(test)] mod tests { + use super::HookMatcher; use codex_config::ConfigLayerEntry; use codex_config::ConfigLayerSource; use codex_config::HookEventsToml; @@ -1296,7 +1299,7 @@ mod tests { vec![ConfiguredHandler { builtin: false, event_name: HookEventName::PreToolUse, - matcher: Some("^Bash$".to_string()), + matcher: Some(HookMatcher::new("^Bash$").expect("valid matcher")), timeout_sec: 600, status_message: None, additional_context_limit: Default::default(), @@ -1366,7 +1369,7 @@ mod tests { assert_eq!( handlers .iter() - .map(|handler| handler.matcher.as_deref()) + .map(|handler| handler.matcher.as_ref().map(HookMatcher::as_str)) .collect::>(), vec![Some("other"), Some("other")] ); @@ -1446,7 +1449,7 @@ mod tests { .iter() .map(|handler| ( handler.timeout_sec, - handler.matcher.as_deref(), + handler.matcher.as_ref().map(HookMatcher::as_str), handler.execution_mode() )) .collect::>(), @@ -1558,7 +1561,10 @@ mod tests { assert_eq!(warnings, Vec::::new()); assert_eq!(handlers.len(), 1); - assert_eq!(handlers[0].matcher.as_deref(), Some("*")); + assert_eq!( + handlers[0].matcher.as_ref().map(HookMatcher::as_str), + Some("*") + ); } #[test] @@ -1582,7 +1588,10 @@ mod tests { assert_eq!(warnings, Vec::::new()); assert_eq!(handlers.len(), 1); assert_eq!(handlers[0].event_name, HookEventName::PostToolUse); - assert_eq!(handlers[0].matcher.as_deref(), Some("Edit|Write")); + assert_eq!( + handlers[0].matcher.as_ref().map(HookMatcher::as_str), + Some("Edit|Write") + ); } #[test] diff --git a/codex-rs/hooks/src/engine/dispatcher.rs b/codex-rs/hooks/src/engine/dispatcher.rs index c5ee0d2ce1da..b34118d6f2b8 100644 --- a/codex-rs/hooks/src/engine/dispatcher.rs +++ b/codex-rs/hooks/src/engine/dispatcher.rs @@ -60,11 +60,11 @@ pub(crate) fn select_handlers_for_matcher_inputs( | HookEventName::PreCompact | HookEventName::PostCompact => { if matcher_inputs.is_empty() { - matches_matcher(handler.matcher.as_deref(), /*input*/ None) + matches_matcher(handler.matcher.as_ref(), /*input*/ None) } else { matcher_inputs .iter() - .any(|input| matches_matcher(handler.matcher.as_deref(), Some(input))) + .any(|input| matches_matcher(handler.matcher.as_ref(), Some(input))) } } HookEventName::UserPromptSubmit | HookEventName::Stop | HookEventName::Interrupt => { @@ -352,7 +352,8 @@ mod tests { ConfiguredHandler { builtin: false, event_name, - matcher: matcher.map(str::to_owned), + matcher: matcher + .map(|pattern| crate::engine::HookMatcher::new(pattern).expect("valid matcher")), timeout_sec: 5, status_message: None, additional_context_limit: Default::default(), @@ -609,7 +610,7 @@ mod tests { ), make_handler( HookEventName::UserPromptSubmit, - Some("["), + Some("^unmatched$"), "echo second", /*display_order*/ 1, ), diff --git a/codex-rs/hooks/src/engine/matcher.rs b/codex-rs/hooks/src/engine/matcher.rs new file mode 100644 index 000000000000..3cd95aacd1e1 --- /dev/null +++ b/codex-rs/hooks/src/engine/matcher.rs @@ -0,0 +1,49 @@ +//! Compiled hook matchers retain their original spelling for configuration identity. +//! Regex parsing happens during discovery; dispatch only evaluates admitted matchers. + +#[derive(Debug, Clone)] +pub(crate) enum HookMatcher { + All(String), + Exact(String), + Regex(regex::Regex), +} + +impl HookMatcher { + pub(crate) fn new(pattern: &str) -> Result { + if pattern.is_empty() || pattern == "*" { + Ok(Self::All(pattern.to_string())) + } else if pattern + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '|') + { + Ok(Self::Exact(pattern.to_string())) + } else { + regex::Regex::new(pattern).map(Self::Regex) + } + } + + pub(crate) fn as_str(&self) -> &str { + match self { + Self::All(pattern) | Self::Exact(pattern) => pattern, + Self::Regex(regex) => regex.as_str(), + } + } + + pub(crate) fn matches(&self, input: Option<&str>) -> bool { + match self { + Self::All(_) => true, + Self::Exact(pattern) => { + input.is_some_and(|input| pattern.split('|').any(|candidate| candidate == input)) + } + Self::Regex(regex) => input.is_some_and(|input| regex.is_match(input)), + } + } +} + +impl PartialEq for HookMatcher { + fn eq(&self, other: &Self) -> bool { + self.as_str() == other.as_str() + } +} + +impl Eq for HookMatcher {} diff --git a/codex-rs/hooks/src/engine/mod.rs b/codex-rs/hooks/src/engine/mod.rs index deaf29679a8c..a007f3b1a459 100644 --- a/codex-rs/hooks/src/engine/mod.rs +++ b/codex-rs/hooks/src/engine/mod.rs @@ -1,10 +1,13 @@ pub(crate) mod command_runner; pub(crate) mod discovery; pub(crate) mod dispatcher; +mod matcher; pub(crate) mod mcp_runner; pub(crate) mod output_parser; pub(crate) mod schema_loader; +pub(crate) use matcher::HookMatcher; + use crate::events::compact::PostCompactRequest; use crate::events::compact::PreCompactOutcome; use crate::events::compact::PreCompactRequest; @@ -60,7 +63,7 @@ pub(crate) struct ConfiguredHandler { /// Internally admitted cleanup hook, enabled independently of per-hook state. pub builtin: bool, pub event_name: codex_protocol::protocol::HookEventName, - pub matcher: Option, + pub matcher: Option, pub timeout_sec: u64, pub status_message: Option, pub additional_context_limit: AdditionalContextLimit, diff --git a/codex-rs/hooks/src/events/common.rs b/codex-rs/hooks/src/events/common.rs index 7b4c4dcab31e..c6e1a1d324cd 100644 --- a/codex-rs/hooks/src/events/common.rs +++ b/codex-rs/hooks/src/events/common.rs @@ -127,28 +127,11 @@ pub(crate) fn matcher_pattern_for_event( } } -pub(crate) fn validate_matcher_pattern(matcher: &str) -> Result<(), regex::Error> { - if is_match_all_matcher(matcher) || is_exact_matcher(matcher) { - return Ok(()); - } - regex::Regex::new(matcher).map(|_| ()) -} - -pub(crate) fn matches_matcher(matcher: Option<&str>, input: Option<&str>) -> bool { - match matcher { - None => true, - Some(matcher) if is_match_all_matcher(matcher) => true, - Some(matcher) if is_exact_matcher(matcher) => input - .map(|input| matcher.split('|').any(|candidate| candidate == input)) - .unwrap_or(false), - Some(matcher) => input - .and_then(|input| { - regex::Regex::new(matcher) - .ok() - .map(|regex| regex.is_match(input)) - }) - .unwrap_or(false), - } +pub(crate) fn matches_matcher( + matcher: Option<&crate::engine::HookMatcher>, + input: Option<&str>, +) -> bool { + matcher.is_none_or(|matcher| matcher.matches(input)) } pub(crate) fn matcher_inputs<'a>( @@ -162,16 +145,6 @@ pub(crate) fn matcher_inputs<'a>( .collect() } -fn is_match_all_matcher(matcher: &str) -> bool { - matcher.is_empty() || matcher == "*" -} - -fn is_exact_matcher(matcher: &str) -> bool { - matcher - .chars() - .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '|') -} - #[cfg(test)] mod tests { use codex_protocol::protocol::HookEventName; @@ -179,7 +152,7 @@ mod tests { use super::matcher_pattern_for_event; use super::matches_matcher; - use super::validate_matcher_pattern; + use crate::engine::HookMatcher; #[test] fn matcher_omitted_matches_all_occurrences() { @@ -189,75 +162,110 @@ mod tests { #[test] fn matcher_star_matches_all_occurrences() { - assert!(matches_matcher(Some("*"), Some("Bash"))); - assert!(matches_matcher(Some("*"), Some("Edit"))); - assert_eq!(validate_matcher_pattern("*"), Ok(())); + assert!(matches_matcher( + Some(&HookMatcher::new("*").expect("valid matcher")), + Some("Bash") + )); + assert!(matches_matcher( + Some(&HookMatcher::new("*").expect("valid matcher")), + Some("Edit") + )); + assert_eq!(HookMatcher::new("*").map(|_| ()), Ok(())); } #[test] fn matcher_empty_string_matches_all_occurrences() { - assert!(matches_matcher(Some(""), Some("Bash"))); - assert!(matches_matcher(Some(""), Some("SessionStart"))); - assert_eq!(validate_matcher_pattern(""), Ok(())); + assert!(matches_matcher( + Some(&HookMatcher::new("").expect("valid matcher")), + Some("Bash") + )); + assert!(matches_matcher( + Some(&HookMatcher::new("").expect("valid matcher")), + Some("SessionStart") + )); + assert_eq!(HookMatcher::new("").map(|_| ()), Ok(())); } #[test] fn exact_matcher_supports_pipe_alternatives() { - assert!(matches_matcher(Some("Edit|Write"), Some("Edit"))); - assert!(matches_matcher(Some("Edit|Write"), Some("Write"))); - assert!(!matches_matcher(Some("Edit|Write"), Some("Bash"))); - assert_eq!(validate_matcher_pattern("Edit|Write"), Ok(())); + assert!(matches_matcher( + Some(&HookMatcher::new("Edit|Write").expect("valid matcher")), + Some("Edit") + )); + assert!(matches_matcher( + Some(&HookMatcher::new("Edit|Write").expect("valid matcher")), + Some("Write") + )); + assert!(!matches_matcher( + Some(&HookMatcher::new("Edit|Write").expect("valid matcher")), + Some("Bash") + )); + assert_eq!(HookMatcher::new("Edit|Write").map(|_| ()), Ok(())); } #[test] fn literal_matcher_uses_exact_matching() { - assert!(matches_matcher(Some("Bash"), Some("Bash"))); - assert!(!matches_matcher(Some("Bash"), Some("BashOutput"))); assert!(matches_matcher( - Some("mcp__memory__create_entities"), + Some(&HookMatcher::new("Bash").expect("valid matcher")), + Some("Bash") + )); + assert!(!matches_matcher( + Some(&HookMatcher::new("Bash").expect("valid matcher")), + Some("BashOutput") + )); + assert!(matches_matcher( + Some(&HookMatcher::new("mcp__memory__create_entities").expect("valid matcher")), Some("mcp__memory__create_entities") )); assert!(!matches_matcher( - Some("mcp__memory"), + Some(&HookMatcher::new("mcp__memory").expect("valid matcher")), Some("mcp__memory__create_entities") )); - assert_eq!(validate_matcher_pattern("mcp__memory"), Ok(())); + assert_eq!(HookMatcher::new("mcp__memory").map(|_| ()), Ok(())); } #[test] fn matcher_uses_regex_when_it_contains_regex_characters() { - assert!(matches_matcher(Some("^Bash"), Some("BashOutput"))); - assert_eq!(validate_matcher_pattern("^Bash"), Ok(())); + assert!(matches_matcher( + Some(&HookMatcher::new("^Bash").expect("valid matcher")), + Some("BashOutput") + )); + assert_eq!(HookMatcher::new("^Bash").map(|_| ()), Ok(())); } #[test] fn mcp_matchers_support_regex_wildcards() { assert!(matches_matcher( - Some("mcp__memory__.*"), + Some(&HookMatcher::new("mcp__memory__.*").expect("valid matcher")), Some("mcp__memory__create_entities") )); assert!(matches_matcher( - Some("mcp__.*__write.*"), + Some(&HookMatcher::new("mcp__.*__write.*").expect("valid matcher")), Some("mcp__filesystem__write_file") )); assert!(!matches_matcher( - Some("mcp__.*__write.*"), + Some(&HookMatcher::new("mcp__.*__write.*").expect("valid matcher")), Some("mcp__filesystem__read_file") )); - assert_eq!(validate_matcher_pattern("mcp__memory__.*"), Ok(())); + assert_eq!(HookMatcher::new("mcp__memory__.*").map(|_| ()), Ok(())); } #[test] fn matcher_supports_anchored_regexes() { - assert!(matches_matcher(Some("^Bash$"), Some("Bash"))); - assert!(!matches_matcher(Some("^Bash$"), Some("BashOutput"))); - assert_eq!(validate_matcher_pattern("^Bash$"), Ok(())); + assert!(matches_matcher( + Some(&HookMatcher::new("^Bash$").expect("valid matcher")), + Some("Bash") + )); + assert!(!matches_matcher( + Some(&HookMatcher::new("^Bash$").expect("valid matcher")), + Some("BashOutput") + )); + assert_eq!(HookMatcher::new("^Bash$").map(|_| ()), Ok(())); } #[test] fn invalid_regex_is_rejected() { - assert!(validate_matcher_pattern("[").is_err()); - assert!(!matches_matcher(Some("["), Some("Bash"))); + assert!(HookMatcher::new("[").is_err()); } #[test] diff --git a/codex-rs/hooks/src/events/post_tool_use.rs b/codex-rs/hooks/src/events/post_tool_use.rs index aeb0ed20a7a1..0ca0f6c673d8 100644 --- a/codex-rs/hooks/src/events/post_tool_use.rs +++ b/codex-rs/hooks/src/events/post_tool_use.rs @@ -591,7 +591,7 @@ mod tests { ConfiguredHandler { builtin: false, event_name: HookEventName::PostToolUse, - matcher: Some("^Bash$".to_string()), + matcher: Some(crate::engine::HookMatcher::new("^Bash$").expect("valid matcher")), timeout_sec: 5, status_message: Some("running post tool use hook".to_string()), additional_context_limit: Default::default(), diff --git a/codex-rs/hooks/src/events/pre_tool_use.rs b/codex-rs/hooks/src/events/pre_tool_use.rs index c1baebd4f01f..e57b823a1c86 100644 --- a/codex-rs/hooks/src/events/pre_tool_use.rs +++ b/codex-rs/hooks/src/events/pre_tool_use.rs @@ -775,7 +775,7 @@ mod tests { ConfiguredHandler { builtin: false, event_name: HookEventName::PreToolUse, - matcher: Some("^Bash$".to_string()), + matcher: Some(crate::engine::HookMatcher::new("^Bash$").expect("valid matcher")), timeout_sec: 5, status_message: None, additional_context_limit: Default::default(), diff --git a/codex-rs/hooks/src/events/session_end_tests.rs b/codex-rs/hooks/src/events/session_end_tests.rs index 0c100ee4a4c6..05fa107d4fa9 100644 --- a/codex-rs/hooks/src/events/session_end_tests.rs +++ b/codex-rs/hooks/src/events/session_end_tests.rs @@ -62,7 +62,8 @@ fn handler(matcher: Option<&str>) -> ConfiguredHandler { ConfiguredHandler { builtin: false, event_name: HookEventName::SessionEnd, - matcher: matcher.map(str::to_string), + matcher: matcher + .map(|pattern| crate::engine::HookMatcher::new(pattern).expect("valid matcher")), timeout_sec: 2, status_message: None, additional_context_limit: Default::default(), From a44afa527cf11114aed19ec893491bfc8d3931bb Mon Sep 17 00:00:00 2001 From: Celia Chen Date: Tue, 29 Sep 2026 22:06:54 +0000 Subject: [PATCH 07/24] Track credential storage outcomes and redact sensitive errors (#49384) ## Why Successful file fallback can hide a secure-store failure. Storage diagnostics need to distinguish that fallback from explicit file storage while keeping credential values out of error output. ## What changed - Record credential load, save, delete, and fallback-file cleanup outcomes, including secure-store failures and fallback reasons. - Preserve the initiating client's attribution across app-server requests, login tasks, sessions, turns, and blocking token persistence. - Preserve storage error context and typed causes while redacting credential JSON data errors and payload-bearing keyring errors. - Include sanitized fallback completion warnings in the default `codex login` file logs. ## Testing Add coverage for storage policy and metric attribution, credential redaction, login diagnostics, concurrent app-server clients, device-code login, and token refresh during a turn. GitOrigin-RevId: 6d5651c1718d184016fa77befa02aa528750b2ae --- codex-rs/Cargo.lock | 1 + codex-rs/app-server/src/message_processor.rs | 23 +- .../request_processors/account_processor.rs | 11 +- codex-rs/app-server/tests/suite/v2/account.rs | 11 +- .../suite/v2/auth_storage_originator_tests.rs | 252 ++++++++++ .../suite/v2/connection_handling_websocket.rs | 3 +- codex-rs/app-server/tests/suite/v2/mod.rs | 2 + codex-rs/cli/src/login.rs | 8 +- codex-rs/cli/src/login_storage_tests.rs | 87 ++++ ...er_records_sanitized_storage_fallback.snap | 7 + ...rage_error_displays_context_and_cause.snap | 6 + codex-rs/cli/tests/daemon_telemetry.rs | 10 +- codex-rs/core/src/session/mod.rs | 6 +- codex-rs/core/src/tasks/mod.rs | 77 +-- .../keyring-store/src/error_display_tests.rs | 51 ++ codex-rs/keyring-store/src/lib.rs | 20 +- codex-rs/login/Cargo.toml | 3 +- codex-rs/login/src/auth/storage.rs | 208 +++++--- codex-rs/login/src/auth/storage_error.rs | 49 ++ .../login/src/auth/storage_error_tests.rs | 146 ++++++ .../login/src/auth/storage_policy_tests.rs | 443 ++++++++++++++++++ codex-rs/login/src/auth/storage_telemetry.rs | 108 +++++ codex-rs/login/src/auth/storage_tests.rs | 206 +------- codex-rs/login/src/server.rs | 78 +-- 24 files changed, 1464 insertions(+), 352 deletions(-) create mode 100644 codex-rs/app-server/tests/suite/v2/auth_storage_originator_tests.rs create mode 100644 codex-rs/cli/src/login_storage_tests.rs create mode 100644 codex-rs/cli/src/snapshots/codex_cli__login__storage_tests__login_default_filter_records_sanitized_storage_fallback.snap create mode 100644 codex-rs/cli/src/snapshots/codex_cli__login__storage_tests__login_storage_error_displays_context_and_cause.snap create mode 100644 codex-rs/keyring-store/src/error_display_tests.rs create mode 100644 codex-rs/login/src/auth/storage_error.rs create mode 100644 codex-rs/login/src/auth/storage_error_tests.rs create mode 100644 codex-rs/login/src/auth/storage_policy_tests.rs create mode 100644 codex-rs/login/src/auth/storage_telemetry.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index c9162751bb88..c81c7467c73d 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -3814,6 +3814,7 @@ dependencies = [ "jsonwebtoken", "keyring", "once_cell", + "opentelemetry_sdk", "os_info", "pretty_assertions", "rand 0.9.3", diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 13a28a10535f..3e8f1fbdd0a5 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -83,6 +83,7 @@ use codex_feedback::CodexFeedback; use codex_goal_extension::GoalService; use codex_home::CodexHomeUserInstructionsProvider; use codex_login::AuthManager; +use codex_otel::auth_storage::AuthStorageOriginator; use codex_protocol::ThreadId; use codex_protocol::mcp::ClientMcpExtensions; use codex_protocol::protocol::SessionSource; @@ -1031,15 +1032,21 @@ impl MessageProcessor { return; } let processor_for_request = Arc::clone(&processor); + let originator = AuthStorageOriginator::from_client_name( + session.app_server_client_name().unwrap_or("none"), + ); // Keep queued requests small to avoid large stack temporaries during construction. - let result = Box::pin(processor_for_request.handle_initialized_client_request( - connection_request_id, - codex_request, - request_context, - session, - event_stream_ready, - )) - .await; + let result = originator + .scope(Box::pin( + processor_for_request.handle_initialized_client_request( + connection_request_id, + codex_request, + request_context, + session, + event_stream_ready, + ), + )) + .await; if let Err(error) = result { processor.outgoing.send_error(error_request_id, error).await; } diff --git a/codex-rs/app-server/src/request_processors/account_processor.rs b/codex-rs/app-server/src/request_processors/account_processor.rs index e1a0cd33d2fb..401b4cc58470 100644 --- a/codex-rs/app-server/src/request_processors/account_processor.rs +++ b/codex-rs/app-server/src/request_processors/account_processor.rs @@ -13,6 +13,7 @@ use codex_login::LoginOnboardingEntrypoint; use codex_login::login_with_bedrock_access_keys; use codex_mcp::ema_auth_scope; use codex_model_provider::is_supported_amazon_bedrock_region; +use codex_otel::auth_storage::AuthStorageOriginator; use codex_rmcp_client::EnterpriseOAuthCredentialGuard; mod bedrock_setup; @@ -678,7 +679,7 @@ impl AccountRequestProcessor { let processor = self.clone(); let active_login = self.active_login.clone(); let auth_url = server.auth_url.clone(); - tokio::spawn(async move { + let task = async move { let (success, error_msg, onboarding_entrypoint) = match tokio::time::timeout( LOGIN_CHATGPT_TIMEOUT, server.block_until_done_with_callback_result(), @@ -715,7 +716,8 @@ impl AccountRequestProcessor { if guard.as_ref().map(ActiveLogin::login_id) == Some(login_id) { *guard = None; } - }); + }; + tokio::spawn(AuthStorageOriginator::current().scope(task)); Ok(LoginAccountResponse::Chatgpt { login_id: login_id.to_string(), @@ -759,7 +761,7 @@ impl AccountRequestProcessor { let processor = self.clone(); let active_login = self.active_login.clone(); - tokio::spawn(async move { + let task = async move { let (success, error_msg) = tokio::select! { _ = cancel.cancelled() => { (false, Some("Login was not completed".to_string())) @@ -785,7 +787,8 @@ impl AccountRequestProcessor { if guard.as_ref().map(ActiveLogin::login_id) == Some(login_id) { *guard = None; } - }); + }; + tokio::spawn(AuthStorageOriginator::current().scope(task)); Ok(LoginAccountResponse::ChatgptDeviceCode { login_id: login_id.to_string(), diff --git a/codex-rs/app-server/tests/suite/v2/account.rs b/codex-rs/app-server/tests/suite/v2/account.rs index 239ed7097fc8..e2a1c1a1b22b 100644 --- a/codex-rs/app-server/tests/suite/v2/account.rs +++ b/codex-rs/app-server/tests/suite/v2/account.rs @@ -2166,6 +2166,7 @@ async fn login_account_chatgpt_device_code_succeeds_and_notifies() -> Result<()> }, )?; write_models_cache(codex_home.path()).await?; + let collector = super::auth_storage_originator::configure_collector(codex_home.path()).await?; mock_device_code_usercode(&mock_server, /*interval_seconds*/ 0).await; mock_device_code_token_success(&mock_server).await; @@ -2184,10 +2185,17 @@ async fn login_account_chatgpt_device_code_succeeds_and_notifies() -> Result<()> .without_auto_env() .with_env_overrides(&[ ("OPENAI_API_KEY", None), + ("OTEL_METRIC_EXPORT_INTERVAL", Some("200")), (LOGIN_ISSUER_ENV_VAR, Some(issuer.as_str())), ]) - .build_initialized_with_timeout(DEFAULT_READ_TIMEOUT) + .build() .await?; + mcp.initialize_with_client_info(ClientInfo { + name: "codex_vscode".into(), + title: None, + version: "1".into(), + }) + .await?; let request_id = mcp.send_login_account_chatgpt_device_code_request().await?; let login: LoginAccountResponse = @@ -2231,6 +2239,7 @@ async fn login_account_chatgpt_device_code_succeeds_and_notifies() -> Result<()> codex_home.path().join("auth.json").exists(), "auth.json should be created when device code login succeeds" ); + super::auth_storage_originator::assert_saved_originators(&collector, &["codex_vscode"]).await?; Ok(()) } diff --git a/codex-rs/app-server/tests/suite/v2/auth_storage_originator_tests.rs b/codex-rs/app-server/tests/suite/v2/auth_storage_originator_tests.rs new file mode 100644 index 000000000000..86817d5fe57d --- /dev/null +++ b/codex-rs/app-server/tests/suite/v2/auth_storage_originator_tests.rs @@ -0,0 +1,252 @@ +//! Checks client attribution on exported storage metrics through the public RPC API. + +use super::connection_handling_websocket::connect_websocket; +use super::connection_handling_websocket::read_response_for_id; +use super::connection_handling_websocket::send_initialize_request; +use super::connection_handling_websocket::send_request; +use super::connection_handling_websocket::spawn_websocket_server; +use anyhow::Context; +use anyhow::Result; +use app_test_support::ChatGptAuthFixture; +use app_test_support::MockResponsesConfig; +use app_test_support::TestAppServer; +use app_test_support::write_chatgpt_auth; +use app_test_support::write_models_cache; +use codex_app_server_protocol::ClientInfo; +use codex_app_server_protocol::ThreadStartParams; +use codex_app_server_protocol::ThreadStartResponse; +use codex_app_server_protocol::TurnCompletedNotification; +use codex_app_server_protocol::TurnStartParams; +use codex_app_server_protocol::TurnStartResponse; +use codex_app_server_protocol::TurnStatus; +use codex_app_server_protocol::UserInput; +use codex_login::AuthCredentialsStoreMode; +use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR; +use core_test_support::responses; +use pretty_assertions::assert_eq; +use serde_json::Value; +use serde_json::json; +use std::collections::BTreeMap; +use std::io::Write; +use std::path::Path; +use std::time::Duration; +use tempfile::TempDir; +use wiremock::Mock; +use wiremock::MockServer; +use wiremock::ResponseTemplate; +use wiremock::matchers::method; +use wiremock::matchers::path; + +pub(super) async fn configure_collector(codex_home: &Path) -> Result { + let collector = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/v1/metrics")) + .respond_with(ResponseTemplate::new(/*s*/ 200)) + .mount(&collector) + .await; + let endpoint = format!("{}/v1/metrics", collector.uri()); + let mut config = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(codex_home.join("config.toml"))?; + writeln!( + config, + "\n[analytics]\nenabled = true\n[otel.metrics_exporter.otlp-http]\nendpoint = {endpoint:?}\nprotocol = \"json\"" + )?; + Ok(collector) +} + +pub(super) async fn assert_saved_originators( + collector: &MockServer, + expected: &[&str], +) -> Result<()> { + let expected: BTreeMap<_, _> = expected + .iter() + .map(|value| (value.to_string(), 1_u64)) + .collect(); + let actual = tokio::time::timeout(Duration::from_secs(/*secs*/ 60), async { + loop { + let mut actual = BTreeMap::::new(); + for request in collector.received_requests().await.unwrap_or_default() { + let body: Value = serde_json::from_slice(&request.body)?; + for resource in body["resourceMetrics"].as_array().into_iter().flatten() { + for scope in resource["scopeMetrics"].as_array().into_iter().flatten() { + for metric in scope["metrics"].as_array().into_iter().flatten() { + if metric["name"] != "codex.auth_storage.operation" { + continue; + } + for point in + metric["sum"]["dataPoints"].as_array().into_iter().flatten() + { + let tags: BTreeMap<_, _> = point["attributes"] + .as_array() + .into_iter() + .flatten() + .filter_map(|tag| { + Some(( + tag["key"].as_str()?, + tag["value"]["stringValue"].as_str()?, + )) + }) + .collect(); + if tags.get("credential_kind") == Some(&"codex") + && tags.get("operation") == Some(&"save") + { + let count = point["asInt"] + .as_u64() + .or_else(|| point["asInt"].as_str()?.parse().ok()) + .context("storage count must be an integer")?; + *actual + .entry( + tags.get("originator") + .unwrap_or(&"missing") + .to_string(), + ) + .or_default() += count; + } + } + } + } + } + } + if actual.values().sum::() >= expected.len() as u64 { + break Ok::<_, anyhow::Error>(actual); + } + tokio::time::sleep(Duration::from_millis(/*millis*/ 25)).await; + } + }) + .await??; + assert_eq!(actual, expected); + Ok(()) +} + +#[tokio::test] +async fn concurrent_connections_export_their_own_storage_originator() -> Result<()> { + let codex_home = TempDir::new()?; + std::fs::write( + codex_home.path().join("config.toml"), + "cli_auth_credentials_store = \"file\"\n", + )?; + let collector = configure_collector(codex_home.path()).await?; + let (_process, address) = spawn_websocket_server(codex_home.path()).await?; + let mut clients = Vec::new(); + for name in ["Codex Desktop", "codex_vscode", "private-client-name"] { + let mut client = connect_websocket(address).await?; + send_initialize_request(&mut client, /*id*/ 1, name).await?; + read_response_for_id(&mut client, /*id*/ 1).await?; + clients.push(client); + } + // All clients initialize before any storage request; a process-global identity fails this test. + for client in &mut clients { + send_request( + client, + "account/login/start", + /*id*/ 2, + Some(json!({"type": "apiKey", "apiKey": "sk-test-key"})), + ) + .await?; + } + for client in &mut clients { + let response = read_response_for_id(client, /*id*/ 2).await?; + assert_eq!(response.result, json!({"type": "apiKey"})); + } + assert_saved_originators(&collector, &["codex_desktop", "codex_vscode", "other"]).await +} + +#[tokio::test] +async fn turn_token_refresh_exports_thread_storage_originator() -> Result<()> { + let codex_home = TempDir::new()?; + let backend = MockServer::start().await; + MockResponsesConfig::new(&backend.uri()) + .with_root_config("cli_auth_credentials_store = \"file\"") + .with_provider_config("requires_openai_auth = true") + .write(codex_home.path())?; + write_models_cache(codex_home.path()).await?; + write_chatgpt_auth( + codex_home.path(), + ChatGptAuthFixture::new("initial-access-token") + .account_id("account-123") + .chatgpt_user_id("user-123"), + AuthCredentialsStoreMode::File, + )?; + let collector = configure_collector(codex_home.path()).await?; + + Mock::given(method("POST")) + .and(path("/oauth/token")) + .respond_with(ResponseTemplate::new(/*s*/ 200).set_body_json(json!({ + "access_token": "refreshed-access-token", + "refresh_token": "refreshed-refresh-token", + }))) + .expect(/*r*/ 1) + .mount(&backend) + .await; + // Fresh credentials prevent startup refresh. The turn first reloads storage on 401, + // then refreshes and persists the tokens when the retry is also unauthorized. + let responses_mock = responses::mount_response_sequence( + &backend, + vec![ + ResponseTemplate::new(/*s*/ 401), + ResponseTemplate::new(/*s*/ 401), + responses::sse_response(responses::sse(vec![ + responses::ev_response_created("resp-turn"), + responses::ev_completed("resp-turn"), + ])), + ], + ) + .await; + let refresh_url = format!("{}/oauth/token", backend.uri()); + let mut server = TestAppServer::builder() + .with_codex_home(codex_home.path()) + .with_env_overrides(&[ + ("OPENAI_API_KEY", None), + ( + REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR, + Some(refresh_url.as_str()), + ), + ]) + .build() + .await?; + server + .initialize_with_client_info(ClientInfo { + name: "codex_vscode".to_string(), + title: None, + version: "0.1.0".to_string(), + }) + .await?; + let request = server + .send_thread_start_request_with_auto_env(ThreadStartParams::default()) + .await?; + let thread: ThreadStartResponse = server.read_response(request).await?; + let request = server + .send_turn_start_request(TurnStartParams { + thread_id: thread.thread.id, + input: vec![UserInput::Text { + text: "Hello".to_string(), + text_elements: Vec::new(), + }], + ..Default::default() + }) + .await?; + let _: TurnStartResponse = server.read_response(request).await?; + let notification = server + .read_stream_until_notification_message("turn/completed") + .await?; + let completed: TurnCompletedNotification = + serde_json::from_value(notification.params.context("turn/completed params")?)?; + assert_eq!(completed.turn.status, TurnStatus::Completed); + assert_eq!( + responses_mock + .requests() + .iter() + .map(|request| request.header("authorization")) + .collect::>(), + vec![ + Some("Bearer initial-access-token".to_string()), + Some("Bearer initial-access-token".to_string()), + Some("Bearer refreshed-access-token".to_string()), + ] + ); + assert_saved_originators(&collector, &["codex_vscode"]).await?; + backend.verify().await; + Ok(()) +} diff --git a/codex-rs/app-server/tests/suite/v2/connection_handling_websocket.rs b/codex-rs/app-server/tests/suite/v2/connection_handling_websocket.rs index 235d669d6288..e083aaa7655b 100644 --- a/codex-rs/app-server/tests/suite/v2/connection_handling_websocket.rs +++ b/codex-rs/app-server/tests/suite/v2/connection_handling_websocket.rs @@ -507,7 +507,8 @@ pub(super) async fn spawn_websocket_server_with_args( .stdout(Stdio::null()) .stderr(Stdio::piped()) .env("CODEX_HOME", codex_home) - .env("RUST_LOG", "warn"); + .env("RUST_LOG", "warn") + .env("OTEL_METRIC_EXPORT_INTERVAL", "200"); let mut process = cmd .kill_on_drop(true) diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs index d24896903618..2d82aa822106 100644 --- a/codex-rs/app-server/tests/suite/v2/mod.rs +++ b/codex-rs/app-server/tests/suite/v2/mod.rs @@ -9,6 +9,8 @@ mod app_list; mod app_read; mod application_network; mod attestation; +#[path = "auth_storage_originator_tests.rs"] +mod auth_storage_originator; mod auto_env; mod bedrock_setup; mod client_metadata; diff --git a/codex-rs/cli/src/login.rs b/codex-rs/cli/src/login.rs index 8b9f081dada9..1d6421b060ba 100644 --- a/codex-rs/cli/src/login.rs +++ b/codex-rs/cli/src/login.rs @@ -46,6 +46,8 @@ const API_KEY_LOGIN_DISABLED_MESSAGE: &str = const ACCESS_TOKEN_LOGIN_DISABLED_MESSAGE: &str = "Access token login is disabled. Use API key login instead."; const LOGIN_SUCCESS_MESSAGE: &str = "Successfully logged in"; +const DEFAULT_LOGIN_LOG_FILTER: &str = + "codex_cli=info,codex_core=info,codex_login=info,codex_otel::auth_storage=warn"; /// Installs a small file-backed tracing layer for direct `codex login` flows. /// @@ -94,7 +96,7 @@ fn init_login_file_logging(config: &Config) -> Option { let (non_blocking, guard) = non_blocking(log_file); let env_filter = EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::new("codex_cli=info,codex_core=info,codex_login=info")); + .unwrap_or_else(|_| EnvFilter::new(DEFAULT_LOGIN_LOG_FILTER)); let file_layer = tracing_subscriber::fmt::layer() .with_writer(non_blocking) .with_target(true) @@ -619,6 +621,10 @@ fn safe_format_key(key: &str) -> String { format!("{prefix}***{suffix}") } +#[cfg(test)] +#[path = "login_storage_tests.rs"] +mod storage_tests; + #[cfg(test)] mod tests { use codex_config::types::AuthCredentialsStoreMode; diff --git a/codex-rs/cli/src/login_storage_tests.rs b/codex-rs/cli/src/login_storage_tests.rs new file mode 100644 index 000000000000..ded91dcfa1d7 --- /dev/null +++ b/codex-rs/cli/src/login_storage_tests.rs @@ -0,0 +1,87 @@ +//! Verifies storage failures remain visible in direct login diagnostics. + +use super::DEFAULT_LOGIN_LOG_FILTER; +use codex_config::types::AuthCredentialsStoreMode; +use codex_login::AuthKeyringBackendKind; +use codex_login::login_with_api_key; +use pretty_assertions::assert_eq; +use tempfile::tempdir; +use tracing_subscriber::EnvFilter; + +#[test] +fn login_default_filter_records_sanitized_storage_fallback() -> anyhow::Result<()> { + let home = tempdir()?; + // Fail before accessing the real keyring, then allow the plaintext fallback to succeed. + std::fs::write(home.path().join("secrets"), "not a directory")?; + let log_path = home.path().join("login.log"); + let subscriber = tracing_subscriber::fmt() + .without_time() + .with_ansi(false) + .with_env_filter(EnvFilter::new(DEFAULT_LOGIN_LOG_FILTER)) + .with_writer(std::fs::File::create(&log_path)?) + .finish(); + tracing::subscriber::with_default(subscriber, || { + login_with_api_key( + home.path(), + "private-api-key", + AuthCredentialsStoreMode::Auto, + AuthKeyringBackendKind::Secrets, + ) + })?; + + let logs = std::fs::read_to_string(log_path)?; + assert_eq!(logs.lines().count(), 2); + let failure = logs.lines().next().unwrap(); + assert!( + failure.contains("failed to save auth to keyring, falling back to file storage"), + "{logs}" + ); + assert!(failure.contains("failed to create secrets dir"), "{logs}"); + assert!( + logs.contains("credential storage file fallback completed"), + "{logs}" + ); + assert!(logs.contains("fallback_reason=\"secure_error\""), "{logs}"); + assert!(logs.contains("outcome=\"success\""), "{logs}"); + assert!(!logs.contains("private-api-key")); + let completion = logs.lines().last().unwrap(); + assert!(!completion.contains(home.path().to_str().unwrap())); + let cause = std::fs::create_dir_all(home.path().join("secrets")) + .unwrap_err() + .to_string(); + insta::assert_snapshot!( + logs.replace( + home.path().join("secrets").to_str().unwrap(), + "[SECRETS_PATH]" + ) + .replace(&cause, "[OS_ERROR]") + ); + Ok(()) +} + +#[test] +fn login_storage_error_displays_context_and_cause() -> anyhow::Result<()> { + let home = tempdir()?; + let secrets_path = home.path().join("secrets"); + std::fs::write(&secrets_path, "not a directory")?; + let cause = std::fs::create_dir_all(&secrets_path) + .unwrap_err() + .to_string(); + let error = login_with_api_key( + home.path(), + "private-api-key", + AuthCredentialsStoreMode::Keyring, + AuthKeyringBackendKind::Secrets, + ) + .unwrap_err(); + let message = error.to_string(); + assert!(message.contains(&cause), "{message}"); + assert!(!message.contains("private-api-key")); + // The temporary path and native filesystem error differ across platforms. + insta::assert_snapshot!( + message + .replace(secrets_path.to_str().unwrap(), "[SECRETS_PATH]") + .replace(&cause, "[OS_ERROR]") + ); + Ok(()) +} diff --git a/codex-rs/cli/src/snapshots/codex_cli__login__storage_tests__login_default_filter_records_sanitized_storage_fallback.snap b/codex-rs/cli/src/snapshots/codex_cli__login__storage_tests__login_default_filter_records_sanitized_storage_fallback.snap new file mode 100644 index 000000000000..8536486dcc8a --- /dev/null +++ b/codex-rs/cli/src/snapshots/codex_cli__login__storage_tests__login_default_filter_records_sanitized_storage_fallback.snap @@ -0,0 +1,7 @@ +--- +source: cli/src/login_storage_tests.rs +assertion_line: 47 +expression: "logs.replace(home.path().join(\"secrets\").to_str().unwrap(),\n\"[SECRETS_PATH]\").replace(&cause, \"[OS_ERROR]\")" +--- + WARN codex_login::auth::storage: failed to save auth to keyring, falling back to file storage: failed to write OAuth tokens to encrypted auth storage: failed to create secrets dir [SECRETS_PATH]: [OS_ERROR] + WARN codex_otel::auth_storage: credential storage file fallback completed credential_kind="codex" store_mode="auto" selected_store="secrets" actual_store="file" operation="save" secure_outcome="error" outcome="success" fallback_reason="secure_error" secure_error="other" storage_phase="policy" originator="none" diff --git a/codex-rs/cli/src/snapshots/codex_cli__login__storage_tests__login_storage_error_displays_context_and_cause.snap b/codex-rs/cli/src/snapshots/codex_cli__login__storage_tests__login_storage_error_displays_context_and_cause.snap new file mode 100644 index 000000000000..f4f7250e386c --- /dev/null +++ b/codex-rs/cli/src/snapshots/codex_cli__login__storage_tests__login_storage_error_displays_context_and_cause.snap @@ -0,0 +1,6 @@ +--- +source: cli/src/login_storage_tests.rs +assertion_line: 58 +expression: "message.replace(secrets_path.to_str().unwrap(),\n\"[SECRETS_PATH]\").replace(&cause, \"[OS_ERROR]\")" +--- +failed to write OAuth tokens to encrypted auth storage: failed to create secrets dir [SECRETS_PATH]: [OS_ERROR] diff --git a/codex-rs/cli/tests/daemon_telemetry.rs b/codex-rs/cli/tests/daemon_telemetry.rs index 48ea0b207eb3..5e265992f3e4 100644 --- a/codex-rs/cli/tests/daemon_telemetry.rs +++ b/codex-rs/cli/tests/daemon_telemetry.rs @@ -53,8 +53,14 @@ async fn foreground_update_respects_consent_and_reports_unconfirmed() -> anyhow: continue; } let body: Value = serde_json::from_slice(&requests[0].body)?; - let metric = &body["resourceMetrics"][0]["scopeMetrics"][0]["metrics"][0]; - assert_eq!(metric["name"], "codex.daemon.update"); + let metric = body["resourceMetrics"] + .as_array() + .into_iter() + .flatten() + .flat_map(|resource| resource["scopeMetrics"].as_array().into_iter().flatten()) + .flat_map(|scope| scope["metrics"].as_array().into_iter().flatten()) + .find(|metric| metric["name"] == "codex.daemon.update") + .context("daemon update metric")?; assert!( metric["sum"]["dataPoints"][0]["attributes"] .as_array() diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 99066080af9a..056f9f0ce923 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -91,6 +91,7 @@ use codex_models_manager::manager::SharedModelsManager; use codex_network_proxy::NetworkProxy; use codex_network_proxy::NetworkProxyAuditMetadata; use codex_network_proxy::normalize_host; +use codex_otel::auth_storage::AuthStorageOriginator; use codex_otel::current_span_trace_id; use codex_otel::current_span_w3c_trace_context; use codex_otel::set_parent_from_w3c_trace_context; @@ -822,6 +823,7 @@ impl Session { ); let service_tier = get_service_tier(config.service_tier.clone(), fast_mode_enabled, &model_info); + let storage_originator = AuthStorageOriginator::from_client_name(&originator); let session_configuration = SessionConfiguration { provider: create_model_provider( config.model_provider.clone(), @@ -930,11 +932,11 @@ impl Session { // This task will run until Op::Shutdown is received. let session_for_loop = Arc::clone(&session); - let session_loop_handle = tokio::spawn(async move { + let session_loop_handle = tokio::spawn(storage_originator.scope(async move { submission_loop(session_for_loop, configured_config, rx_sub) .instrument(info_span!("session_loop", thread_id = %thread_id)) .await; - }); + })); let io = SessionIo { tx_sub, rx_event, diff --git a/codex-rs/core/src/tasks/mod.rs b/codex-rs/core/src/tasks/mod.rs index 28a3b54b1c6f..4666142887a7 100644 --- a/codex-rs/core/src/tasks/mod.rs +++ b/codex-rs/core/src/tasks/mod.rs @@ -46,6 +46,7 @@ use codex_otel::TURN_MEMORY_METRIC; use codex_otel::TURN_NETWORK_PROXY_METRIC; use codex_otel::TURN_TOOL_CALL_METRIC; use codex_otel::TURN_UNIFIED_EXEC_RUNNING_PROCESSES_METRIC; +use codex_otel::auth_storage::AuthStorageOriginator; use codex_protocol::models::ResponseItem; use codex_protocol::protocol::ErrorEvent; use codex_protocol::protocol::EventMsg; @@ -361,46 +362,46 @@ impl Session { codex.turn.token_usage.reasoning_output_tokens = field::Empty, codex.turn.token_usage.total_tokens = field::Empty, ); - let handle = tokio::spawn( - async move { - let ctx_for_finish = Arc::clone(&ctx); - let task_result = task_for_run - .run( - Arc::clone(&session), - ctx, - task_input, - task_cancellation_token.child_token(), - ) - .instrument(trace_span!("session_task.run")) - .await; - let sess = Arc::clone(&session); - // Private reviewers save their transcript together with the terminal event. - // Errors and cancellation retain their existing save path. - if (!sess.is_private_guardian_reviewer().await - || task_cancellation_token.is_cancelled() - || task_result.is_err()) - && let Err(err) = sess.flush_rollout().await - { - warn!("failed to flush rollout before completing turn: {err}"); - sess.send_event( - ctx_for_finish.as_ref(), - EventMsg::Warning(WarningEvent { - message: format!( - "Failed to save the conversation transcript; Codex will continue retrying. Error: {err}" - ), - }), - ) + let storage_originator = AuthStorageOriginator::from_client_name(&turn_context.originator); + let task_future = async move { + let ctx_for_finish = Arc::clone(&ctx); + let task_result = task_for_run + .run( + Arc::clone(&session), + ctx, + task_input, + task_cancellation_token.child_token(), + ) + .instrument(trace_span!("session_task.run")) + .await; + let sess = Arc::clone(&session); + // Private reviewers save their transcript together with the terminal event. + // Errors and cancellation retain their existing save path. + if (!sess.is_private_guardian_reviewer().await + || task_cancellation_token.is_cancelled() + || task_result.is_err()) + && let Err(err) = sess.flush_rollout().await + { + warn!("failed to flush rollout before completing turn: {err}"); + sess.send_event( + ctx_for_finish.as_ref(), + EventMsg::Warning(WarningEvent { + message: format!( + "Failed to save the conversation transcript; Codex will continue retrying. Error: {err}" + ), + }), + ) + .await; + } + if !task_cancellation_token.is_cancelled() { + // Finish uniformly from the spawn site so all tasks share the same lifecycle. + sess.on_task_finished(Arc::clone(&ctx_for_finish), task_result) .await; - } - if !task_cancellation_token.is_cancelled() { - // Finish uniformly from the spawn site so all tasks share the same lifecycle. - sess.on_task_finished(Arc::clone(&ctx_for_finish), task_result) - .await; - } - done_clone.notify_waiters(); } - .instrument(task_span), - ); + done_clone.notify_waiters(); + } + .instrument(task_span); + let handle = tokio::spawn(storage_originator.scope(task_future)); let timer = turn_context .session_telemetry .start_timer(TURN_E2E_DURATION_METRIC, &[]) diff --git a/codex-rs/keyring-store/src/error_display_tests.rs b/codex-rs/keyring-store/src/error_display_tests.rs new file mode 100644 index 000000000000..a7ea7f244f7b --- /dev/null +++ b/codex-rs/keyring-store/src/error_display_tests.rs @@ -0,0 +1,51 @@ +//! Ensures credential payloads cannot escape through diagnostic error formatting. + +use super::*; +use keyring::credential::CredentialApi; +use pretty_assertions::assert_eq; + +#[test] +fn payload_bearing_errors_redact_credentials_and_end_the_source_chain() { + let credential = keyring::mock::MockCredential::default(); + credential.set_password("private-credential-value").unwrap(); + for (error, message) in [ + ( + KeyringError::Ambiguous(vec![Box::new(credential) as Box]), + "Entry is matched by 1 credentials", + ), + ( + KeyringError::BadEncoding(b"private-credential-value".to_vec()), + "Data is not UTF-8 encoded", + ), + ] { + let error = CredentialStoreError::new(error); + assert_eq!( + ( + error.to_string(), + error.message(), + format!("{error:?}"), + error.source().is_none() + ), + ( + message.to_string(), + message.to_string(), + message.to_string(), + true + ), + ); + } +} + +#[test] +fn debug_preserves_native_error_details() { + let error = CredentialStoreError::new(KeyringError::PlatformFailure(Box::new( + std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "permission denied by keyring", + ), + ))); + assert_eq!( + format!("{error:?}"), + "Platform secure storage failure: permission denied by keyring", + ); +} diff --git a/codex-rs/keyring-store/src/lib.rs b/codex-rs/keyring-store/src/lib.rs index 3b0a1c414fe2..bd38bc6ecd75 100644 --- a/codex-rs/keyring-store/src/lib.rs +++ b/codex-rs/keyring-store/src/lib.rs @@ -5,7 +5,6 @@ use std::fmt; use std::fmt::Debug; use tracing::trace; -#[derive(Debug)] pub enum CredentialStoreError { Other(KeyringError), } @@ -16,9 +15,7 @@ impl CredentialStoreError { } pub fn message(&self) -> String { - match self { - Self::Other(error) => error.to_string(), - } + self.to_string() } pub fn into_error(self) -> KeyringError { @@ -31,19 +28,34 @@ impl CredentialStoreError { impl fmt::Display for CredentialStoreError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Self::Other(KeyringError::Ambiguous(items)) => { + write!(f, "Entry is matched by {} credentials", items.len()) + } Self::Other(error) => write!(f, "{error}"), } } } +impl fmt::Debug for CredentialStoreError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + impl Error for CredentialStoreError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { + // Do not expose payload-bearing variants to error-chain formatters. + Self::Other(KeyringError::Ambiguous(_) | KeyringError::BadEncoding(_)) => None, Self::Other(error) => Some(error), } } } +#[cfg(test)] +#[path = "error_display_tests.rs"] +mod error_display_tests; + mod error_kind; impl From for std::io::Error { diff --git a/codex-rs/login/Cargo.toml b/codex-rs/login/Cargo.toml index 8772f46b5867..7762770be8c9 100644 --- a/codex-rs/login/Cargo.toml +++ b/codex-rs/login/Cargo.toml @@ -9,6 +9,7 @@ workspace = true [dependencies] codex-utils-process = { workspace = true } +anyhow = { workspace = true } base64 = { workspace = true } chrono = { workspace = true, features = ["serde"] } codex-agent-identity = { workspace = true } @@ -44,7 +45,7 @@ urlencoding = { workspace = true } webbrowser = { workspace = true } [dev-dependencies] -anyhow = { workspace = true } +opentelemetry_sdk = { workspace = true, features = ["testing"] } core_test_support = { workspace = true } jsonwebtoken = { workspace = true } keyring = { workspace = true } diff --git a/codex-rs/login/src/auth/storage.rs b/codex-rs/login/src/auth/storage.rs index 8a9d75f44876..4dccbc9138e8 100644 --- a/codex-rs/login/src/auth/storage.rs +++ b/codex-rs/login/src/auth/storage.rs @@ -1,5 +1,7 @@ use chrono::DateTime; use chrono::Utc; +use codex_otel::auth_storage::Operation; +use codex_otel::auth_storage::Store; use serde::Deserialize; use serde::Serialize; use sha2::Digest; @@ -18,6 +20,12 @@ use std::sync::Arc; use std::sync::Mutex; use tracing::warn; +#[path = "storage_error.rs"] +mod storage_error; + +#[path = "storage_telemetry.rs"] +mod storage_telemetry; + use super::BedrockAccessKeysAuth; use super::BedrockApiKeyAuth; use crate::token_data::TokenData; @@ -251,44 +259,50 @@ fn compute_store_key(codex_home: &Path) -> std::io::Result { #[derive(Clone, Debug)] struct DirectKeyringAuthStorage { codex_home: PathBuf, + mode: AuthCredentialsStoreMode, keyring_store: Arc, } impl DirectKeyringAuthStorage { - fn new(codex_home: PathBuf, keyring_store: Arc) -> Self { + fn new( + codex_home: PathBuf, + keyring_store: Arc, + mode: AuthCredentialsStoreMode, + ) -> Self { Self { codex_home, keyring_store, + mode, } } fn load_from_keyring(&self, key: &str) -> std::io::Result> { - match self.keyring_store.load(KEYRING_SERVICE, key) { - Ok(Some(serialized)) => serde_json::from_str(&serialized).map(Some).map_err(|err| { - std::io::Error::other(format!( - "failed to deserialize CLI auth from keyring: {err}" - )) + match self + .keyring_store + .load(KEYRING_SERVICE, key) + .map_err(std::io::Error::from) + .map_err(|error| { + storage_error::with_context("failed to load CLI auth from keyring", error) + })? { + Some(serialized) => serde_json::from_str(&serialized).map(Some).map_err(|err| { + storage_error::with_context("failed to deserialize CLI auth from keyring", err) }), - Ok(None) => Ok(None), - Err(error) => Err(std::io::Error::other(format!( - "failed to load CLI auth from keyring: {}", - error.message() - ))), + None => Ok(None), } } fn save_to_keyring(&self, key: &str, value: &str) -> std::io::Result<()> { - match self.keyring_store.save(KEYRING_SERVICE, key, value) { - Ok(()) => Ok(()), - Err(error) => { - let message = format!( - "failed to write OAuth tokens to keyring: {}", - error.message() - ); - warn!("{message}"); - Err(std::io::Error::other(message)) - } - } + self.keyring_store + .save(KEYRING_SERVICE, key, value) + .map_err(std::io::Error::from) + .map_err(|error| { + let error = + storage_error::with_context("failed to write OAuth tokens to keyring", error); + if self.mode == AuthCredentialsStoreMode::Keyring { + warn!("{error}"); + } + error + }) } } @@ -303,9 +317,17 @@ impl AuthStorageBackend for DirectKeyringAuthStorage { // Simpler error mapping per style: prefer method reference over closure let serialized = serde_json::to_string(auth).map_err(std::io::Error::other)?; self.save_to_keyring(&key, &serialized)?; - if let Err(err) = delete_file_if_exists(&self.codex_home) { + let mut telemetry = storage_telemetry::telemetry( + self.mode, + AuthKeyringBackendKind::Direct, + Operation::Cleanup, + ); + let result = delete_file_if_exists(&self.codex_home); + telemetry.record_delete_attempt(Store::File, &result); + if let Err(err) = &result { warn!("failed to remove CLI auth fallback file: {err}"); } + drop(telemetry); Ok(()) } @@ -314,8 +336,9 @@ impl AuthStorageBackend for DirectKeyringAuthStorage { let keyring_removed = self .keyring_store .delete(KEYRING_SERVICE, &key) - .map_err(|err| { - std::io::Error::other(format!("failed to delete auth from keyring: {err}")) + .map_err(std::io::Error::from) + .map_err(|error| { + storage_error::with_context("failed to delete auth from keyring", error) })?; let file_removed = delete_file_if_exists(&self.codex_home)?; Ok(keyring_removed || file_removed) @@ -325,6 +348,7 @@ impl AuthStorageBackend for DirectKeyringAuthStorage { #[derive(Clone)] struct SecretsKeyringAuthStorage { codex_home: PathBuf, + mode: AuthCredentialsStoreMode, direct_storage: DirectKeyringAuthStorage, secrets_manager: SecretsManager, } @@ -338,9 +362,13 @@ impl Debug for SecretsKeyringAuthStorage { } impl SecretsKeyringAuthStorage { - fn new(codex_home: PathBuf, keyring_store: Arc) -> Self { + fn new( + codex_home: PathBuf, + keyring_store: Arc, + mode: AuthCredentialsStoreMode, + ) -> Self { let direct_storage = - DirectKeyringAuthStorage::new(codex_home.clone(), Arc::clone(&keyring_store)); + DirectKeyringAuthStorage::new(codex_home.clone(), Arc::clone(&keyring_store), mode); let secrets_manager = SecretsManager::new_with_keyring_store_and_namespace( codex_home.clone(), SecretsBackendKind::Local, @@ -349,6 +377,7 @@ impl SecretsKeyringAuthStorage { ); Self { codex_home, + mode, direct_storage, secrets_manager, } @@ -360,15 +389,17 @@ impl AuthStorageBackend for SecretsKeyringAuthStorage { match self .secrets_manager .get(&SecretScope::Global, &CODEX_AUTH_SECRET_NAME) - .map_err(|err| { - std::io::Error::other(format!( - "failed to load CLI auth from encrypted auth storage: {err}" - )) + .map_err(|error| { + storage_error::with_context( + "failed to load CLI auth from encrypted auth storage", + error, + ) })? { Some(serialized) => serde_json::from_str(&serialized).map(Some).map_err(|err| { - std::io::Error::other(format!( - "failed to deserialize CLI auth from encrypted auth storage: {err}" - )) + storage_error::with_context( + "failed to deserialize CLI auth from encrypted auth storage", + err, + ) }), None => Ok(None), } @@ -378,15 +409,27 @@ impl AuthStorageBackend for SecretsKeyringAuthStorage { let serialized = serde_json::to_string(auth).map_err(std::io::Error::other)?; self.secrets_manager .set(&SecretScope::Global, &CODEX_AUTH_SECRET_NAME, &serialized) - .map_err(|err| { - let message = - format!("failed to write OAuth tokens to encrypted auth storage: {err}"); - warn!("{message}"); - std::io::Error::other(message) + .map_err(|error| { + let error = storage_error::with_context( + "failed to write OAuth tokens to encrypted auth storage", + error, + ); + if self.mode == AuthCredentialsStoreMode::Keyring { + warn!("{error}"); + } + error })?; - if let Err(err) = delete_file_if_exists(&self.codex_home) { + let mut telemetry = storage_telemetry::telemetry( + self.mode, + AuthKeyringBackendKind::Secrets, + Operation::Cleanup, + ); + let result = delete_file_if_exists(&self.codex_home); + telemetry.record_delete_attempt(Store::File, &result); + if let Err(err) = &result { warn!("failed to remove CLI auth fallback file: {err}"); } + drop(telemetry); Ok(()) } @@ -394,10 +437,11 @@ impl AuthStorageBackend for SecretsKeyringAuthStorage { let keyring_removed = self .secrets_manager .delete(&SecretScope::Global, &CODEX_AUTH_SECRET_NAME) - .map_err(|err| { - std::io::Error::other(format!( - "failed to delete auth from encrypted auth storage: {err}" - )) + .map_err(|error| { + storage_error::with_context( + "failed to delete auth from encrypted auth storage", + error, + ) })?; let file_removed = delete_file_if_exists(&self.codex_home)?; let direct_removed = self.direct_storage.delete()?; @@ -409,6 +453,7 @@ impl AuthStorageBackend for SecretsKeyringAuthStorage { struct AutoAuthStorage { keyring_storage: Arc, file_storage: Arc, + keyring_backend_kind: AuthKeyringBackendKind, } impl AutoAuthStorage { @@ -422,37 +467,76 @@ impl AutoAuthStorage { codex_home.clone(), keyring_store, keyring_backend_kind, + AuthCredentialsStoreMode::Auto, ), file_storage: Arc::new(FileAuthStorage::new(codex_home)), + keyring_backend_kind, } } } impl AuthStorageBackend for AutoAuthStorage { fn load(&self) -> std::io::Result> { - match self.keyring_storage.load() { + let mut telemetry = storage_telemetry::telemetry( + AuthCredentialsStoreMode::Auto, + self.keyring_backend_kind, + Operation::Load, + ); + let result = self.keyring_storage.load(); + telemetry.record_load_attempt( + storage_telemetry::keyring_store(self.keyring_backend_kind), + &result, + ); + match result { Ok(Some(auth)) => Ok(Some(auth)), - Ok(None) => self.file_storage.load(), + Ok(None) => { + let result = self.file_storage.load(); + telemetry.record_load_attempt(Store::File, &result); + result + } Err(err) => { warn!("failed to load CLI auth from keyring, falling back to file storage: {err}"); - self.file_storage.load() + telemetry.record_secure_error(&err); + let result = self.file_storage.load(); + telemetry.record_load_attempt(Store::File, &result); + result } } } fn save(&self, auth: &AuthDotJson) -> std::io::Result<()> { - match self.keyring_storage.save(auth) { + let mut telemetry = storage_telemetry::telemetry( + AuthCredentialsStoreMode::Auto, + self.keyring_backend_kind, + Operation::Save, + ); + let result = self.keyring_storage.save(auth); + telemetry.record_save_attempt( + storage_telemetry::keyring_store(self.keyring_backend_kind), + &result, + ); + match result { Ok(()) => Ok(()), Err(err) => { warn!("failed to save auth to keyring, falling back to file storage: {err}"); - self.file_storage.save(auth) + telemetry.record_secure_error(&err); + let result = self.file_storage.save(auth); + telemetry.record_save_attempt(Store::File, &result); + result } } } fn delete(&self) -> std::io::Result { // Keyring storage will delete from disk as well - self.keyring_storage.delete() + let mut telemetry = storage_telemetry::telemetry( + AuthCredentialsStoreMode::Auto, + self.keyring_backend_kind, + Operation::Delete, + ); + let result = self.keyring_storage.delete(); + telemetry.record_delete_attempt(Store::Multiple, &result); + result } } @@ -514,10 +598,10 @@ fn create_auth_storage_with_store( keyring_store: Arc, keyring_backend_kind: AuthKeyringBackendKind, ) -> Arc { - match mode { + let storage: Arc = match mode { AuthCredentialsStoreMode::File => Arc::new(FileAuthStorage::new(codex_home)), AuthCredentialsStoreMode::Keyring => { - create_keyring_auth_storage(codex_home, keyring_store, keyring_backend_kind) + create_keyring_auth_storage(codex_home, keyring_store, keyring_backend_kind, mode) } AuthCredentialsStoreMode::Auto => Arc::new(AutoAuthStorage::new( codex_home, @@ -525,21 +609,27 @@ fn create_auth_storage_with_store( keyring_backend_kind, )), AuthCredentialsStoreMode::Ephemeral => Arc::new(EphemeralAuthStorage::new(codex_home)), - } + }; + storage_telemetry::observe(storage, mode, keyring_backend_kind) } fn create_keyring_auth_storage( codex_home: PathBuf, keyring_store: Arc, keyring_backend_kind: AuthKeyringBackendKind, + mode: AuthCredentialsStoreMode, ) -> Arc { match keyring_backend_kind { - AuthKeyringBackendKind::Direct => { - Arc::new(DirectKeyringAuthStorage::new(codex_home, keyring_store)) - } - AuthKeyringBackendKind::Secrets => { - Arc::new(SecretsKeyringAuthStorage::new(codex_home, keyring_store)) - } + AuthKeyringBackendKind::Direct => Arc::new(DirectKeyringAuthStorage::new( + codex_home, + keyring_store, + mode, + )), + AuthKeyringBackendKind::Secrets => Arc::new(SecretsKeyringAuthStorage::new( + codex_home, + keyring_store, + mode, + )), } } diff --git a/codex-rs/login/src/auth/storage_error.rs b/codex-rs/login/src/auth/storage_error.rs new file mode 100644 index 000000000000..b25daa02470f --- /dev/null +++ b/codex-rs/login/src/auth/storage_error.rs @@ -0,0 +1,49 @@ +//! Removes credential JSON data errors while preserving native storage diagnostics and typed causes. + +#[derive(thiserror::Error)] +struct CredentialStorageError { + context: &'static str, + #[source] + source: anyhow::Error, +} + +impl std::fmt::Display for CredentialStorageError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}: {:#}", self.context, self.source) + } +} + +impl std::fmt::Debug for CredentialStorageError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self, f) + } +} + +pub(super) fn with_context( + context: &'static str, + source: impl Into, +) -> std::io::Error { + let mut source = source.into(); + let mut cause: Option<&(dyn std::error::Error + 'static)> = Some(source.as_ref()); + while let Some(error) = cause { + if let Some(error) = error.downcast_ref::() + && error.is_data() + { + // Discard the entire chain: serde errors and their context may contain credentials. + source = anyhow::anyhow!( + "invalid credential JSON ({:?} at line {} column {})", + error.classify(), + error.line(), + error.column(), + ); + break; + } + cause = if let Some(error) = error.downcast_ref::() { + // io::Error::source() skips its contained error. + error.get_ref().map(|error| error as &dyn std::error::Error) + } else { + error.source() + }; + } + std::io::Error::other(CredentialStorageError { context, source }) +} diff --git a/codex-rs/login/src/auth/storage_error_tests.rs b/codex-rs/login/src/auth/storage_error_tests.rs new file mode 100644 index 000000000000..8b44b7f6d2c3 --- /dev/null +++ b/codex-rs/login/src/auth/storage_error_tests.rs @@ -0,0 +1,146 @@ +//! Checks that storage diagnostics preserve useful causes without exposing credential values. + +use super::*; +use pretty_assertions::assert_eq; + +#[test] +fn secure_storage_errors_display_the_underlying_cause() -> anyhow::Result<()> { + for kind in [ + AuthKeyringBackendKind::Direct, + AuthKeyringBackendKind::Secrets, + ] { + for (error_kind, cause) in [ + ( + std::io::ErrorKind::PermissionDenied, + "permission denied by keyring", + ), + (std::io::ErrorKind::NotConnected, "keyring unavailable"), + ] { + let home = tempdir()?; + let keyring = MockKeyringStore::default(); + let storage = create_auth_storage_with_store( + home.path().to_path_buf(), + AuthCredentialsStoreMode::Keyring, + Arc::new(keyring.clone()), + kind, + ); + let auth = auth_with_prefix("private credential value"); + storage.save(&auth)?; + let key = match kind { + AuthKeyringBackendKind::Direct => compute_store_key(home.path())?, + AuthKeyringBackendKind::Secrets => { + compute_keyring_account(home.path(), LocalSecretsNamespace::CodexAuth) + } + }; + for operation in [Operation::Load, Operation::Save, Operation::Delete] { + keyring.set_error( + &key, + KeyringError::PlatformFailure(Box::new(std::io::Error::new(error_kind, cause))), + ); + let error = match operation { + Operation::Load => storage.load().unwrap_err(), + Operation::Save => storage.save(&auth).unwrap_err(), + Operation::Delete => storage.delete().unwrap_err(), + Operation::Cleanup | Operation::RefreshPersist => unreachable!(), + }; + let message = error.to_string(); + assert!(message.starts_with("failed to "), "{message}"); + assert!(message.contains(cause), "{message}"); + assert!(!message.contains("private credential value")); + let debug = format!("{error:?}"); + assert!(debug.contains(cause), "{debug}"); + assert!(!debug.contains("private credential value")); + } + } + } + Ok(()) +} + +#[test] +fn json_syntax_and_eof_errors_preserve_diagnostics() { + for (input, category) in [ + ( + r#"{"OPENAI_API_KEY" "private-credential-value"}"#, + serde_json::error::Category::Syntax, + ), + ( + r#"{"OPENAI_API_KEY":"private-credential-value""#, + serde_json::error::Category::Eof, + ), + ] { + let source = serde_json::from_str::(input).unwrap_err(); + assert_eq!(source.classify(), category); + let expected = format!("failed to load auth: decrypted auth JSON: {source}"); + let error = storage_error::with_context( + "failed to load auth", + anyhow::Error::new(source).context("decrypted auth JSON"), + ); + let inner = error.get_ref().unwrap(); + assert_eq!( + (error.to_string(), format!("{inner:?}")), + (expected.clone(), expected) + ); + assert!(!format!("{error:?}").contains("private-credential-value")); + } +} + +#[test] +fn json_data_errors_redact_credential_values() { + enum Wrapper { + Direct, + AnyhowContext, + Io, + Nested, + } + + for wrapper in [ + Wrapper::Direct, + Wrapper::AnyhowContext, + Wrapper::Io, + Wrapper::Nested, + ] { + let source = + serde_json::from_str::(r#"{"auth_mode":"private-credential-value"}"#) + .unwrap_err(); + assert!(source.to_string().contains("private-credential-value")); + let expected = format!( + "failed to load auth: invalid credential JSON (Data at line {} column {})", + source.line(), + source.column(), + ); + let source = match wrapper { + Wrapper::Direct => anyhow::Error::new(source), + Wrapper::AnyhowContext => anyhow::Error::new(source).context("decrypted auth JSON"), + Wrapper::Io => anyhow::Error::new(std::io::Error::other(source)), + Wrapper::Nested => anyhow::Error::new(std::io::Error::other( + anyhow::Error::new(std::io::Error::other(source)) + .context("private-credential-value"), + )) + .context("decrypted auth JSON"), + }; + let error = storage_error::with_context("failed to load auth", source); + let inner = error.get_ref().unwrap(); + assert_eq!( + (error.to_string(), format!("{inner:?}")), + (expected.clone(), expected) + ); + assert!(!format!("{error:?}").contains("private-credential-value")); + + let error = anyhow::Error::new(error).context("credential storage failed"); + for formatted in [format!("{error:#}"), format!("{error:?}")] { + assert!( + !formatted.contains("private-credential-value"), + "{formatted}" + ); + } + for cause in error.chain() { + assert!(cause.downcast_ref::().is_none()); + for formatted in [cause.to_string(), format!("{cause:?}")] { + assert!( + !formatted.contains("private-credential-value"), + "{formatted}" + ); + } + } + } +} diff --git a/codex-rs/login/src/auth/storage_policy_tests.rs b/codex-rs/login/src/auth/storage_policy_tests.rs new file mode 100644 index 000000000000..0d389b756c97 --- /dev/null +++ b/codex-rs/login/src/auth/storage_policy_tests.rs @@ -0,0 +1,443 @@ +//! Exercises configured storage policy and its emitted metadata. + +use super::*; +use opentelemetry_sdk::metrics::data::AggregatedMetrics; +use opentelemetry_sdk::metrics::data::MetricData; +use pretty_assertions::assert_eq; + +#[test] +fn storage_metrics_distinguish_explicit_file_from_secure_failure_fallback() -> anyhow::Result<()> { + if std::env::var_os("CODEX_AUTH_STORAGE_TEST_CHILD").is_none() { + let output = std::process::Command::new(std::env::current_exe()?) + .args([ + "--exact", + std::thread::current().name().unwrap(), + "--nocapture", + ]) + .env("CODEX_AUTH_STORAGE_TEST_CHILD", "1") + .output()?; + assert!( + output.status.success(), + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + return Ok(()); + } + use std::collections::BTreeMap; + let metrics = codex_otel::install_global_metrics(codex_otel::MetricsClient::new( + codex_otel::MetricsConfig::in_memory("test", "test", "1", Default::default()) + .with_runtime_reader(), + )?); + let mut expected = BTreeMap::new(); + for kind in [ + AuthKeyringBackendKind::Direct, + AuthKeyringBackendKind::Secrets, + ] { + for (scenario, mode, operation, actual, secure, outcome, reason, error) in [ + ( + "success", + AuthCredentialsStoreMode::Auto, + "save", + storage_telemetry::keyring_store(kind).as_ref(), + "success", + "success", + "none", + "none", + ), + ( + "cleanup_failed", + AuthCredentialsStoreMode::Auto, + "save", + storage_telemetry::keyring_store(kind).as_ref(), + "success", + "success", + "none", + "none", + ), + ( + "success", + AuthCredentialsStoreMode::Keyring, + "save", + storage_telemetry::keyring_store(kind).as_ref(), + "success", + "success", + "none", + "none", + ), + ( + "cleanup_failed", + AuthCredentialsStoreMode::Keyring, + "save", + storage_telemetry::keyring_store(kind).as_ref(), + "success", + "success", + "none", + "none", + ), + ( + "denied", + AuthCredentialsStoreMode::Auto, + "save", + "file", + "error", + "success", + "secure_error", + "access_denied", + ), + ( + "explicit", + AuthCredentialsStoreMode::File, + "save", + "file", + "not_attempted", + "success", + "none", + "none", + ), + ( + "missing", + AuthCredentialsStoreMode::Auto, + "load", + "file", + "not_found", + "success", + "secure_entry_missing", + "none", + ), + ( + "failed_file", + AuthCredentialsStoreMode::Auto, + "load", + "file", + "error", + "error", + "secure_error", + "access_denied", + ), + ] { + let home = tempdir()?; + let keyring = MockKeyringStore::default(); + let key = match kind { + AuthKeyringBackendKind::Direct => compute_store_key(home.path())?, + AuthKeyringBackendKind::Secrets => { + compute_keyring_account(home.path(), LocalSecretsNamespace::CodexAuth) + } + }; + let auth = auth_with_prefix("private credential value"); + if operation == "load" || scenario == "success" { + FileAuthStorage::new(home.path().to_path_buf()).save(&auth)?; + } + if scenario == "cleanup_failed" { + std::fs::create_dir(get_auth_file(home.path()))?; + } + if scenario == "failed_file" { + std::fs::write(get_auth_file(home.path()), "invalid private file contents")?; + } + if error == "access_denied" { + keyring.set_error( + &key, + KeyringError::PlatformFailure(Box::new(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "private account path error", + ))), + ); + // Secrets loads consult the keyring only when an encrypted file exists. + if operation == "load" && kind == AuthKeyringBackendKind::Secrets { + let manager = SecretsManager::new_with_keyring_store_and_namespace( + home.path().to_path_buf(), + SecretsBackendKind::Local, + Arc::new(MockKeyringStore::default()), + LocalSecretsNamespace::CodexAuth, + ); + manager.set(&SecretScope::Global, &CODEX_AUTH_SECRET_NAME, "{}")?; + } + } + let storage = create_auth_storage_with_store( + home.path().to_path_buf(), + mode, + Arc::new(keyring), + kind, + ); + if operation == "load" { + let result = storage.load(); + if outcome == "error" { + assert!(result.is_err()); + } else { + assert_eq!(result?, Some(auth.clone())); + } + } else { + storage.save(&auth)?; + } + let store_mode = match mode { + AuthCredentialsStoreMode::Auto => "auto", + AuthCredentialsStoreMode::Keyring => "keyring", + AuthCredentialsStoreMode::File => "file", + AuthCredentialsStoreMode::Ephemeral => "ephemeral", + }; + let tags: BTreeMap = BTreeMap::from([ + ("credential_kind".into(), "codex".into()), + ("store_mode".into(), store_mode.into()), + ( + "selected_store".into(), + storage_telemetry::keyring_store(kind).as_ref().into(), + ), + ("actual_store".into(), actual.into()), + ("operation".into(), operation.into()), + ("secure_outcome".into(), secure.into()), + ("outcome".into(), outcome.into()), + ("fallback_reason".into(), reason.into()), + ("secure_error".into(), error.into()), + ("storage_phase".into(), "policy".into()), + ("originator".into(), "none".into()), + ]); + if secure == "success" { + let mut cleanup_tags = tags.clone(); + cleanup_tags.extend([ + ("actual_store".into(), "file".into()), + ("operation".into(), "cleanup".into()), + ("secure_outcome".into(), "not_attempted".into()), + ( + "outcome".into(), + if scenario == "cleanup_failed" { + "error" + } else { + "success" + } + .into(), + ), + ]); + *expected.entry(cleanup_tags).or_insert(0) += 1; + } + *expected.entry(tags).or_insert(0) += 1; + } + } + let snapshot = metrics.snapshot()?; + let mut actual = Vec::new(); + for metric in snapshot + .scope_metrics() + .flat_map(opentelemetry_sdk::metrics::data::ScopeMetrics::metrics) + { + if metric.name() == "codex.auth_storage.operation" + && let AggregatedMetrics::U64(MetricData::Sum(sum)) = metric.data() + { + for point in sum.data_points() { + let tags: BTreeMap = point + .attributes() + .map(|a| (a.key.to_string(), a.value.to_string())) + .collect(); + actual.push((point.value(), tags)); + } + } + } + let mut expected: Vec<_> = expected + .into_iter() + .map(|(tags, count)| (count, tags)) + .collect(); + actual.sort(); + expected.sort(); + assert_eq!(actual, expected); + metrics.shutdown()?; + Ok(()) +} + +#[test] +fn auto_auth_storage_load_prefers_keyring_value() -> anyhow::Result<()> { + let codex_home = tempdir()?; + let mock_keyring = MockKeyringStore::default(); + let storage = AutoAuthStorage::new( + codex_home.path().to_path_buf(), + Arc::new(mock_keyring.clone()), + AuthKeyringBackendKind::Secrets, + ); + let keyring_auth = auth_with_prefix("keyring"); + seed_secrets_backend_with_auth(&mock_keyring, codex_home.path(), &keyring_auth)?; + + let file_auth = auth_with_prefix("file"); + storage.file_storage.save(&file_auth)?; + + let loaded = storage.load()?; + assert_eq!(loaded, Some(keyring_auth)); + Ok(()) +} + +#[test] +fn auto_auth_storage_load_uses_file_when_keyring_empty() -> anyhow::Result<()> { + let codex_home = tempdir()?; + let mock_keyring = MockKeyringStore::default(); + let storage = AutoAuthStorage::new( + codex_home.path().to_path_buf(), + Arc::new(mock_keyring), + AuthKeyringBackendKind::Secrets, + ); + + let expected = auth_with_prefix("file-only"); + storage.file_storage.save(&expected)?; + + let loaded = storage.load()?; + assert_eq!(loaded, Some(expected)); + Ok(()) +} + +#[test] +fn auto_auth_storage_load_falls_back_when_keyring_errors() -> anyhow::Result<()> { + let codex_home = tempdir()?; + let mock_keyring = MockKeyringStore::default(); + let storage = AutoAuthStorage::new( + codex_home.path().to_path_buf(), + Arc::new(mock_keyring.clone()), + AuthKeyringBackendKind::Secrets, + ); + let key = compute_keyring_account(codex_home.path(), LocalSecretsNamespace::CodexAuth); + + let encrypted = auth_with_prefix("encrypted"); + seed_secrets_backend_with_auth(&mock_keyring, codex_home.path(), &encrypted)?; + mock_keyring.set_error(&key, KeyringError::Invalid("error".into(), "load".into())); + + let expected = auth_with_prefix("fallback"); + storage.file_storage.save(&expected)?; + + let loaded = storage.load()?; + assert_eq!(loaded, Some(expected)); + Ok(()) +} + +#[test] +fn auto_auth_storage_save_prefers_keyring() -> anyhow::Result<()> { + let codex_home = tempdir()?; + let mock_keyring = MockKeyringStore::default(); + let storage = AutoAuthStorage::new( + codex_home.path().to_path_buf(), + Arc::new(mock_keyring.clone()), + AuthKeyringBackendKind::Secrets, + ); + let stale = auth_with_prefix("stale"); + storage.file_storage.save(&stale)?; + + let expected = auth_with_prefix("to-save"); + storage.save(&expected)?; + + assert_keyring_saved_auth_and_removed_fallback(&mock_keyring, codex_home.path(), &expected)?; + Ok(()) +} + +#[test] +fn auto_auth_storage_save_falls_back_when_keyring_errors() -> anyhow::Result<()> { + let codex_home = tempdir()?; + let mock_keyring = MockKeyringStore::default(); + let storage = AutoAuthStorage::new( + codex_home.path().to_path_buf(), + Arc::new(mock_keyring.clone()), + AuthKeyringBackendKind::Secrets, + ); + let key = compute_keyring_account(codex_home.path(), LocalSecretsNamespace::CodexAuth); + mock_keyring.set_error(&key, KeyringError::Invalid("error".into(), "save".into())); + + let auth = auth_with_prefix("fallback"); + storage.save(&auth)?; + + let auth_file = get_auth_file(codex_home.path()); + assert!( + auth_file.exists(), + "fallback auth.json should be created when keyring save fails" + ); + let saved = storage + .file_storage + .load()? + .context("fallback auth should exist")?; + assert_eq!(saved, auth); + assert!( + mock_keyring.saved_value(&key).is_none(), + "keyring should not contain value when save fails" + ); + Ok(()) +} + +#[test] +fn auto_auth_storage_delete_removes_keyring_and_file() -> anyhow::Result<()> { + let codex_home = tempdir()?; + let mock_keyring = MockKeyringStore::default(); + let storage = AutoAuthStorage::new( + codex_home.path().to_path_buf(), + Arc::new(mock_keyring.clone()), + AuthKeyringBackendKind::Secrets, + ); + let auth = auth_with_prefix("to-delete"); + let auth_file = seed_secrets_backend_and_fallback_auth_file_for_delete( + &mock_keyring, + codex_home.path(), + &auth, + )?; + + let removed = storage.delete()?; + + assert!(removed, "delete should report removal"); + assert_eq!(storage.load()?, None, "encrypted auth should be removed"); + assert!( + !auth_file.exists(), + "fallback auth.json should be removed after delete" + ); + Ok(()) +} + +#[test] +fn direct_keyring_auth_storage_saves_legacy_keyring_entry() -> anyhow::Result<()> { + let codex_home = tempdir()?; + let mock_keyring = MockKeyringStore::default(); + let storage = DirectKeyringAuthStorage::new( + codex_home.path().to_path_buf(), + Arc::new(mock_keyring.clone()), + AuthCredentialsStoreMode::Keyring, + ); + let auth_file = get_auth_file(codex_home.path()); + std::fs::write(&auth_file, "stale")?; + let auth = auth_with_prefix("direct"); + + storage.save(&auth)?; + + let legacy_key = compute_store_key(codex_home.path())?; + let saved_value = mock_keyring + .saved_value(&legacy_key) + .context("direct keyring auth entry should exist")?; + assert_eq!(saved_value, serde_json::to_string(&auth)?); + assert!(!encrypted_auth_file(codex_home.path()).exists()); + assert!( + !auth_file.exists(), + "fallback auth.json should be removed after keyring save" + ); + assert_eq!(storage.load()?, Some(auth)); + Ok(()) +} + +#[test] +fn secrets_keyring_auth_storage_save_persists_and_removes_fallback_file() -> anyhow::Result<()> { + let codex_home = tempdir()?; + let mock_keyring = MockKeyringStore::default(); + let storage = SecretsKeyringAuthStorage::new( + codex_home.path().to_path_buf(), + Arc::new(mock_keyring.clone()), + AuthCredentialsStoreMode::Keyring, + ); + let auth_file = get_auth_file(codex_home.path()); + std::fs::write(&auth_file, "stale")?; + let auth = AuthDotJson { + auth_mode: Some(AuthMode::Chatgpt), + openai_api_key: None, + tokens: Some(TokenData { + id_token: Default::default(), + access_token: "access".to_string(), + refresh_token: "refresh".to_string(), + account_id: Some("account".to_string()), + }), + last_refresh: Some(Utc::now()), + agent_identity: None, + personal_access_token: None, + bedrock_api_key: None, + bedrock_access_keys: None, + }; + + storage.save(&auth)?; + + assert_keyring_saved_auth_and_removed_fallback(&mock_keyring, codex_home.path(), &auth)?; + Ok(()) +} diff --git a/codex-rs/login/src/auth/storage_telemetry.rs b/codex-rs/login/src/auth/storage_telemetry.rs new file mode 100644 index 000000000000..9fb46399817d --- /dev/null +++ b/codex-rs/login/src/auth/storage_telemetry.rs @@ -0,0 +1,108 @@ +//! Observes logical Codex credential operations without inspecting authentication material. + +use super::AuthDotJson; +use super::AuthStorageBackend; +use codex_config::types::AuthCredentialsStoreMode; +use codex_config::types::AuthKeyringBackendKind; +use codex_otel::auth_storage::AuthStorageOriginator; +use codex_otel::auth_storage::CredentialKind; +use codex_otel::auth_storage::Operation; +use codex_otel::auth_storage::StorageTelemetry; +use codex_otel::auth_storage::Store; +use codex_otel::auth_storage::StoreMode; +use std::sync::Arc; + +pub(super) fn keyring_store(kind: AuthKeyringBackendKind) -> Store { + match kind { + AuthKeyringBackendKind::Direct => Store::DirectKeyring, + AuthKeyringBackendKind::Secrets => Store::Secrets, + } +} + +pub(super) fn telemetry( + mode: AuthCredentialsStoreMode, + kind: AuthKeyringBackendKind, + operation: Operation, +) -> StorageTelemetry { + let mode = match mode { + AuthCredentialsStoreMode::File => StoreMode::File, + AuthCredentialsStoreMode::Auto => StoreMode::Auto, + AuthCredentialsStoreMode::Keyring => StoreMode::Keyring, + AuthCredentialsStoreMode::Ephemeral => StoreMode::Ephemeral, + }; + StorageTelemetry::new( + CredentialKind::Codex, + mode, + keyring_store(kind), + operation, + AuthStorageOriginator::current(), + ) +} + +pub(super) fn observe( + inner: Arc, + mode: AuthCredentialsStoreMode, + kind: AuthKeyringBackendKind, +) -> Arc { + // Auto records both attempts together so file fallback cannot hide a secure-store failure. + if mode == AuthCredentialsStoreMode::Auto { + return inner; + } + Arc::new(ObservedStorage { inner, mode, kind }) +} + +#[derive(Debug)] +struct ObservedStorage { + inner: Arc, + mode: AuthCredentialsStoreMode, + kind: AuthKeyringBackendKind, +} + +impl ObservedStorage { + fn store(&self) -> Store { + match self.mode { + AuthCredentialsStoreMode::File => Store::File, + AuthCredentialsStoreMode::Ephemeral => Store::Ephemeral, + AuthCredentialsStoreMode::Keyring | AuthCredentialsStoreMode::Auto => { + keyring_store(self.kind) + } + } + } +} + +impl AuthStorageBackend for ObservedStorage { + fn load(&self) -> std::io::Result> { + let mut telemetry = telemetry(self.mode, self.kind, Operation::Load); + let result = self.inner.load(); + telemetry.record_load_attempt(self.store(), &result); + if self.mode == AuthCredentialsStoreMode::Keyring + && let Err(error) = &result + { + telemetry.record_secure_error(error); + } + result + } + + fn save(&self, auth: &AuthDotJson) -> std::io::Result<()> { + let mut telemetry = telemetry(self.mode, self.kind, Operation::Save); + let result = self.inner.save(auth); + telemetry.record_save_attempt(self.store(), &result); + if self.mode == AuthCredentialsStoreMode::Keyring + && let Err(error) = &result + { + telemetry.record_secure_error(error); + } + result + } + + fn delete(&self) -> std::io::Result { + let store = match self.mode { + AuthCredentialsStoreMode::File | AuthCredentialsStoreMode::Ephemeral => self.store(), + AuthCredentialsStoreMode::Auto | AuthCredentialsStoreMode::Keyring => Store::Multiple, + }; + let mut telemetry = telemetry(self.mode, self.kind, Operation::Delete); + let result = self.inner.delete(); + telemetry.record_delete_attempt(store, &result); + result + } +} diff --git a/codex-rs/login/src/auth/storage_tests.rs b/codex-rs/login/src/auth/storage_tests.rs index 79c51c871e7d..01c2e6f575e1 100644 --- a/codex-rs/login/src/auth/storage_tests.rs +++ b/codex-rs/login/src/auth/storage_tests.rs @@ -458,6 +458,7 @@ fn secrets_keyring_auth_storage_load_returns_deserialized_auth() -> anyhow::Resu let storage = SecretsKeyringAuthStorage::new( codex_home.path().to_path_buf(), Arc::new(mock_keyring.clone()), + AuthCredentialsStoreMode::Keyring, ); let expected = AuthDotJson { auth_mode: Some(AuthMode::ApiKey), @@ -486,34 +487,6 @@ fn keyring_auth_storage_compute_store_key_for_home_directory() -> anyhow::Result Ok(()) } -#[test] -fn direct_keyring_auth_storage_saves_legacy_keyring_entry() -> anyhow::Result<()> { - let codex_home = tempdir()?; - let mock_keyring = MockKeyringStore::default(); - let storage = DirectKeyringAuthStorage::new( - codex_home.path().to_path_buf(), - Arc::new(mock_keyring.clone()), - ); - let auth_file = get_auth_file(codex_home.path()); - std::fs::write(&auth_file, "stale")?; - let auth = auth_with_prefix("direct"); - - storage.save(&auth)?; - - let legacy_key = compute_store_key(codex_home.path())?; - let saved_value = mock_keyring - .saved_value(&legacy_key) - .context("direct keyring auth entry should exist")?; - assert_eq!(saved_value, serde_json::to_string(&auth)?); - assert!(!encrypted_auth_file(codex_home.path()).exists()); - assert!( - !auth_file.exists(), - "fallback auth.json should be removed after keyring save" - ); - assert_eq!(storage.load()?, Some(auth)); - Ok(()) -} - #[test] fn direct_keyring_auth_storage_delete_removes_keyring_and_file() -> anyhow::Result<()> { let codex_home = tempdir()?; @@ -521,6 +494,7 @@ fn direct_keyring_auth_storage_delete_removes_keyring_and_file() -> anyhow::Resu let storage = DirectKeyringAuthStorage::new( codex_home.path().to_path_buf(), Arc::new(mock_keyring.clone()), + AuthCredentialsStoreMode::Keyring, ); let auth = auth_with_prefix("direct-delete"); storage.save(&auth)?; @@ -586,38 +560,6 @@ fn factory_uses_secrets_backend_only_when_requested() -> anyhow::Result<()> { Ok(()) } -#[test] -fn secrets_keyring_auth_storage_save_persists_and_removes_fallback_file() -> anyhow::Result<()> { - let codex_home = tempdir()?; - let mock_keyring = MockKeyringStore::default(); - let storage = SecretsKeyringAuthStorage::new( - codex_home.path().to_path_buf(), - Arc::new(mock_keyring.clone()), - ); - let auth_file = get_auth_file(codex_home.path()); - std::fs::write(&auth_file, "stale")?; - let auth = AuthDotJson { - auth_mode: Some(AuthMode::Chatgpt), - openai_api_key: None, - tokens: Some(TokenData { - id_token: Default::default(), - access_token: "access".to_string(), - refresh_token: "refresh".to_string(), - account_id: Some("account".to_string()), - }), - last_refresh: Some(Utc::now()), - agent_identity: None, - personal_access_token: None, - bedrock_api_key: None, - bedrock_access_keys: None, - }; - - storage.save(&auth)?; - - assert_keyring_saved_auth_and_removed_fallback(&mock_keyring, codex_home.path(), &auth)?; - Ok(()) -} - #[test] fn secrets_keyring_auth_storage_delete_removes_keyring_and_file() -> anyhow::Result<()> { let codex_home = tempdir()?; @@ -625,6 +567,7 @@ fn secrets_keyring_auth_storage_delete_removes_keyring_and_file() -> anyhow::Res let storage = SecretsKeyringAuthStorage::new( codex_home.path().to_path_buf(), Arc::new(mock_keyring.clone()), + AuthCredentialsStoreMode::Keyring, ); let auth = auth_with_prefix("to-delete"); let auth_file = seed_secrets_backend_and_fallback_auth_file_for_delete( @@ -651,11 +594,13 @@ fn secrets_keyring_auth_storage_delete_removes_legacy_direct_keyring_entry() -> let direct_storage = DirectKeyringAuthStorage::new( codex_home.path().to_path_buf(), Arc::new(mock_keyring.clone()), + AuthCredentialsStoreMode::Keyring, ); direct_storage.save(&auth_with_prefix("legacy-direct"))?; let storage = SecretsKeyringAuthStorage::new( codex_home.path().to_path_buf(), Arc::new(mock_keyring.clone()), + AuthCredentialsStoreMode::Keyring, ); let auth = auth_with_prefix("to-delete"); let auth_file = seed_secrets_backend_and_fallback_auth_file_for_delete( @@ -680,141 +625,8 @@ fn secrets_keyring_auth_storage_delete_removes_legacy_direct_keyring_entry() -> Ok(()) } -#[test] -fn auto_auth_storage_load_prefers_keyring_value() -> anyhow::Result<()> { - let codex_home = tempdir()?; - let mock_keyring = MockKeyringStore::default(); - let storage = AutoAuthStorage::new( - codex_home.path().to_path_buf(), - Arc::new(mock_keyring.clone()), - AuthKeyringBackendKind::Secrets, - ); - let keyring_auth = auth_with_prefix("keyring"); - seed_secrets_backend_with_auth(&mock_keyring, codex_home.path(), &keyring_auth)?; +#[path = "storage_policy_tests.rs"] +mod policy; - let file_auth = auth_with_prefix("file"); - storage.file_storage.save(&file_auth)?; - - let loaded = storage.load()?; - assert_eq!(loaded, Some(keyring_auth)); - Ok(()) -} - -#[test] -fn auto_auth_storage_load_uses_file_when_keyring_empty() -> anyhow::Result<()> { - let codex_home = tempdir()?; - let mock_keyring = MockKeyringStore::default(); - let storage = AutoAuthStorage::new( - codex_home.path().to_path_buf(), - Arc::new(mock_keyring), - AuthKeyringBackendKind::Secrets, - ); - - let expected = auth_with_prefix("file-only"); - storage.file_storage.save(&expected)?; - - let loaded = storage.load()?; - assert_eq!(loaded, Some(expected)); - Ok(()) -} - -#[test] -fn auto_auth_storage_load_falls_back_when_keyring_errors() -> anyhow::Result<()> { - let codex_home = tempdir()?; - let mock_keyring = MockKeyringStore::default(); - let storage = AutoAuthStorage::new( - codex_home.path().to_path_buf(), - Arc::new(mock_keyring.clone()), - AuthKeyringBackendKind::Secrets, - ); - let key = compute_keyring_account(codex_home.path(), LocalSecretsNamespace::CodexAuth); - - let encrypted = auth_with_prefix("encrypted"); - seed_secrets_backend_with_auth(&mock_keyring, codex_home.path(), &encrypted)?; - mock_keyring.set_error(&key, KeyringError::Invalid("error".into(), "load".into())); - - let expected = auth_with_prefix("fallback"); - storage.file_storage.save(&expected)?; - - let loaded = storage.load()?; - assert_eq!(loaded, Some(expected)); - Ok(()) -} - -#[test] -fn auto_auth_storage_save_prefers_keyring() -> anyhow::Result<()> { - let codex_home = tempdir()?; - let mock_keyring = MockKeyringStore::default(); - let storage = AutoAuthStorage::new( - codex_home.path().to_path_buf(), - Arc::new(mock_keyring.clone()), - AuthKeyringBackendKind::Secrets, - ); - let stale = auth_with_prefix("stale"); - storage.file_storage.save(&stale)?; - - let expected = auth_with_prefix("to-save"); - storage.save(&expected)?; - - assert_keyring_saved_auth_and_removed_fallback(&mock_keyring, codex_home.path(), &expected)?; - Ok(()) -} - -#[test] -fn auto_auth_storage_save_falls_back_when_keyring_errors() -> anyhow::Result<()> { - let codex_home = tempdir()?; - let mock_keyring = MockKeyringStore::default(); - let storage = AutoAuthStorage::new( - codex_home.path().to_path_buf(), - Arc::new(mock_keyring.clone()), - AuthKeyringBackendKind::Secrets, - ); - let key = compute_keyring_account(codex_home.path(), LocalSecretsNamespace::CodexAuth); - mock_keyring.set_error(&key, KeyringError::Invalid("error".into(), "save".into())); - - let auth = auth_with_prefix("fallback"); - storage.save(&auth)?; - - let auth_file = get_auth_file(codex_home.path()); - assert!( - auth_file.exists(), - "fallback auth.json should be created when keyring save fails" - ); - let saved = storage - .file_storage - .load()? - .context("fallback auth should exist")?; - assert_eq!(saved, auth); - assert!( - mock_keyring.saved_value(&key).is_none(), - "keyring should not contain value when save fails" - ); - Ok(()) -} - -#[test] -fn auto_auth_storage_delete_removes_keyring_and_file() -> anyhow::Result<()> { - let codex_home = tempdir()?; - let mock_keyring = MockKeyringStore::default(); - let storage = AutoAuthStorage::new( - codex_home.path().to_path_buf(), - Arc::new(mock_keyring.clone()), - AuthKeyringBackendKind::Secrets, - ); - let auth = auth_with_prefix("to-delete"); - let auth_file = seed_secrets_backend_and_fallback_auth_file_for_delete( - &mock_keyring, - codex_home.path(), - &auth, - )?; - - let removed = storage.delete()?; - - assert!(removed, "delete should report removal"); - assert_eq!(storage.load()?, None, "encrypted auth should be removed"); - assert!( - !auth_file.exists(), - "fallback auth.json should be removed after delete" - ); - Ok(()) -} +#[path = "storage_error_tests.rs"] +mod errors; diff --git a/codex-rs/login/src/server.rs b/codex-rs/login/src/server.rs index d9dcb321563f..d65c95287c3b 100644 --- a/codex-rs/login/src/server.rs +++ b/codex-rs/login/src/server.rs @@ -61,6 +61,7 @@ use codex_http_client::HttpClient; use codex_http_client::HttpClientBuilder; use codex_http_client::HttpClientFactory; use codex_http_client::OutboundProxyPolicy; +use codex_otel::auth_storage::AuthStorageOriginator; use codex_protocol::auth::AuthMode; use codex_utils_template::Template; use serde_json::Value as JsonValue; @@ -226,7 +227,7 @@ pub fn run_login_server(opts: ServerOptions) -> io::Result { let server_handle = { let shutdown_notify = shutdown_notify.clone(); let server = server; - tokio::spawn(async move { + let task = async move { let mut callback_result = LoginCallbackResult::default(); let result = loop { tokio::select! { @@ -311,7 +312,8 @@ pub fn run_login_server(opts: ServerOptions) -> io::Result { // running `server.recv()` in a loop exits cleanly. server.unblock(); result - }) + }; + tokio::spawn(AuthStorageOriginator::current().scope(task)) }; Ok(LoginServer { @@ -839,38 +841,46 @@ pub(crate) async fn persist_tokens_async( ) -> io::Result<()> { // Reuse existing synchronous logic but run it off the async runtime. let codex_home = codex_home.to_path_buf(); - tokio::task::spawn_blocking(move || { - let mut tokens = TokenData { - id_token: parse_chatgpt_jwt_claims(&id_token).map_err(io::Error::other)?, - access_token, - refresh_token, - account_id: None, - }; - if let Some(acc) = jwt_auth_claims(&id_token) - .get("chatgpt_account_id") - .and_then(|v| v.as_str()) - { - tokens.account_id = Some(acc.to_string()); - } - let auth = AuthDotJson { - auth_mode: Some(AuthMode::Chatgpt), - openai_api_key: api_key, - tokens: Some(tokens), - last_refresh: Some(Utc::now()), - agent_identity: None, - personal_access_token: None, - bedrock_api_key: None, - bedrock_access_keys: None, - }; - save_auth( - &codex_home, - &auth, - auth_credentials_store_mode, - keyring_backend_kind, - ) - }) - .await - .map_err(|e| io::Error::other(format!("persist task failed: {e}")))? + // Blocking tasks don't inherit task-local context. + // Preserve the initiating client for credential-storage metrics. + let originator = AuthStorageOriginator::current(); + + let persist_task = tokio::task::spawn_blocking(move || { + originator.sync_scope(|| { + let mut tokens = TokenData { + id_token: parse_chatgpt_jwt_claims(&id_token).map_err(io::Error::other)?, + access_token, + refresh_token, + account_id: None, + }; + if let Some(acc) = jwt_auth_claims(&id_token) + .get("chatgpt_account_id") + .and_then(|v| v.as_str()) + { + tokens.account_id = Some(acc.to_string()); + } + let auth = AuthDotJson { + auth_mode: Some(AuthMode::Chatgpt), + openai_api_key: api_key, + tokens: Some(tokens), + last_refresh: Some(Utc::now()), + agent_identity: None, + personal_access_token: None, + bedrock_api_key: None, + bedrock_access_keys: None, + }; + save_auth( + &codex_home, + &auth, + auth_credentials_store_mode, + keyring_backend_kind, + ) + }) + }); + + persist_task + .await + .map_err(|e| io::Error::other(format!("persist task failed: {e}")))? } /// Validates the ID token against an optional workspace restriction. From d6571322d270b77c7d044fc714272929a6b467c7 Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Tue, 29 Sep 2026 22:31:27 +0000 Subject: [PATCH 08/24] Fix Windows path inference for opaque URIs with slash prefixes (#49388) ## Why Opaque Windows paths encoded as UTF-16LE could be mistaken for POSIX paths when they began with `/`, or fail Windows convention inference when their leading separators mixed `/` and `\`. This prevented them from retaining the Windows path convention across URI serialization. ## What changed Distinguish a UTF-16LE slash from a POSIX root and recognize either Windows separator in both positions of an opaque UNC prefix. ## Testing Add round-trip coverage for Windows drive, UNC, device, and namespace paths, including forward and mixed separators. Cover JSON serialization, absolute-path joins, relative-path separator handling, and Windows-only host-native conversions. GitOrigin-RevId: d4e8a40a314cfc030c30fa8c124bfdd1e565076d --- codex-rs/utils/path-uri/src/lib.rs | 6 +- codex-rs/utils/path-uri/src/tests.rs | 137 +++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 2 deletions(-) diff --git a/codex-rs/utils/path-uri/src/lib.rs b/codex-rs/utils/path-uri/src/lib.rs index c35142fd9a3c..7a42363dd1ad 100644 --- a/codex-rs/utils/path-uri/src/lib.rs +++ b/codex-rs/utils/path-uri/src/lib.rs @@ -810,7 +810,8 @@ fn native_path_segments_start_with( } fn infer_opaque_path_convention(path_bytes: &[u8]) -> Option { - if path_bytes.starts_with(b"/") { + // A UTF-16LE Windows slash starts with `/\0`, unlike a POSIX root. + if path_bytes.starts_with(b"/") && !path_bytes.starts_with(b"/\0") { return Some(PathConvention::Posix); } if !path_bytes.len().is_multiple_of(2) { @@ -824,7 +825,8 @@ fn infer_opaque_path_convention(path_bytes: &[u8]) -> Option { let second = path_wide.next()?; let has_drive = u8::try_from(first).is_ok_and(|drive| drive.is_ascii_alphabetic()) && second == u16::from(b':'); - let has_unc_prefix = first == u16::from(b'\\') && second == u16::from(b'\\'); + let is_separator = |character| character == u16::from(b'\\') || character == u16::from(b'/'); + let has_unc_prefix = is_separator(first) && is_separator(second); (has_drive || has_unc_prefix).then_some(PathConvention::Windows) } diff --git a/codex-rs/utils/path-uri/src/tests.rs b/codex-rs/utils/path-uri/src/tests.rs index fab23b0c467f..49dbd2781fb5 100644 --- a/codex-rs/utils/path-uri/src/tests.rs +++ b/codex-rs/utils/path-uri/src/tests.rs @@ -532,6 +532,38 @@ fn file_uri_round_trips_literal_percent_characters() { assert_eq!(uri.basename(), Some("file".to_string())); } +#[cfg(windows)] +#[test] +fn host_windows_paths_round_trip_through_file_uris() { + for native in [ + r"C:\temp\test-file.txt", + r"C:/temp/test-file.txt", + r"C:\temp/test-file.txt", + r"\\127.0.0.1\c$\temp\test-file.txt", + r"//127.0.0.1/c$/temp/test-file.txt", + r"\\LOCALHOST\c$\temp\test-file.txt", + r"//LOCALHOST/c$/temp/test-file.txt", + r"\\.\C:\temp\test-file.txt", + r"\\?\C:\temp\test-file.txt", + r"\\.\UNC\LOCALHOST\c$\temp\test-file.txt", + r"\\?\UNC\server\share\temp\test-file.txt", + r"\\.\COM1", + r"\\?\Volume{00000000-0000-0000-0000-000000000000}\file.txt", + ] { + let expected = + codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path_checked(native) + .expect(native); + let uri = PathUri::from_host_native_path(native).expect(native); + let restored = PathUri::parse(&uri.to_string()).expect("parse URI"); + assert_eq!( + restored.infer_path_convention(), + Some(PathConvention::Windows), + "{native}" + ); + assert_eq!(restored.to_abs_path().expect(native), expected, "{native}"); + } +} + #[test] #[cfg(windows)] fn file_uri_round_trips_windows_unc_paths() { @@ -957,6 +989,93 @@ fn join_normalizes_absolute_parent_segments() { } } +#[test] +fn absolute_windows_paths_round_trip() { + for (native, expected) in [ + (r"c:\temp\test-file.txt", r"C:\temp\test-file.txt"), + (r"C:/temp/test-file.txt", r"C:\temp\test-file.txt"), + (r"C:\temp/test-file.txt", r"C:\temp\test-file.txt"), + ( + r"\\127.0.0.1\c$\temp\test-file.txt", + r"\\127.0.0.1\c$\temp\test-file.txt", + ), + ( + r"//127.0.0.1/c$/temp/test-file.txt", + r"\\127.0.0.1\c$\temp\test-file.txt", + ), + ( + r"\\server/share\temp/test-file.txt", + r"\\server\share\temp\test-file.txt", + ), + ( + r"\\LOCALHOST\c$\temp\test-file.txt", + r"\\LOCALHOST\c$\temp\test-file.txt", + ), + ( + r"//LOCALHOST/c$/temp/test-file.txt", + r"//LOCALHOST/c$/temp/test-file.txt", + ), + ( + r"/\LOCALHOST/c$/temp/test-file.txt", + r"/\LOCALHOST/c$/temp/test-file.txt", + ), + ( + r"\/LOCALHOST/c$/temp/test-file.txt", + r"\/LOCALHOST/c$/temp/test-file.txt", + ), + (r"\\.\c:\temp\test-file.txt", r"C:\temp\test-file.txt"), + (r"\\?\c:\temp\test-file.txt", r"C:\temp\test-file.txt"), + ( + r"\\.\UNC\server\c$\temp\test-file.txt", + r"\\server\c$\temp\test-file.txt", + ), + ( + r"\\?\UNC\server\c$\temp\test-file.txt", + r"\\server\c$\temp\test-file.txt", + ), + ( + r"\\.\UNC\LOCALHOST\c$\temp\test-file.txt", + r"\\.\UNC\LOCALHOST\c$\temp\test-file.txt", + ), + (r"\\.\COM1", r"\\.\COM1"), + ( + r"\\?\Volume{00000000-0000-0000-0000-000000000000}\file.txt", + r"\\?\Volume{00000000-0000-0000-0000-000000000000}\file.txt", + ), + (r"//./COM1", r"//./COM1"), + (r"//./C:/temp/test-file.txt", r"//./C:/temp/test-file.txt"), + (r"//?/C:/temp/test-file.txt", r"//?/C:/temp/test-file.txt"), + ( + r"//./UNC/server/share/file.txt", + r"//./UNC/server/share/file.txt", + ), + ( + r"//?/UNC/server/share/file.txt", + r"//?/UNC/server/share/file.txt", + ), + ( + r"C:/a b/日本語/100%#file.txt:stream", + r"C:\a b\日本語\100%#file.txt:stream", + ), + ] { + let uri = LegacyAppPathString::from_string(native) + .to_path_uri(PathConvention::Windows) + .expect(native); + let serialized = serde_json::to_string(&uri).expect("serialize URI"); + let restored: PathUri = serde_json::from_str(&serialized).expect("deserialize URI"); + assert_eq!( + ( + restored.infer_path_convention(), + restored.inferred_native_path_string() + ), + (Some(PathConvention::Windows), expected.to_string()), + "{native}" + ); + let base = PathUri::parse("file:///D:/workspace").expect("base URI"); + assert_eq!(base.join(native).expect(native), restored, "{native}"); + } +} + #[test] fn windows_namespace_normalization_preserves_opaque_paths() { let base = PathUri::parse("file:///C:/workspace").expect("valid Windows base URI"); @@ -1059,6 +1178,24 @@ fn join_replaces_windows_absolute_path() { ); } +#[test] +fn windows_relative_paths_accept_both_separators() { + let base = PathUri::parse("file:///C:/workspace/src").expect("base URI"); + for (native, expected) in [ + (r"..\tests/file.rs", "file:///C:/workspace/tests/file.rs"), + (r"../tests\file.rs", "file:///C:/workspace/tests/file.rs"), + (r"/temp/file.rs", "file:///C:/temp/file.rs"), + (r"\temp/file.rs", "file:///C:/temp/file.rs"), + (r"c:tests/file.rs", "file:///C:/workspace/src/tests/file.rs"), + ] { + assert_eq!( + base.join(native).expect(native), + PathUri::parse(expected).unwrap() + ); + } + assert!(base.join("D:tests/file.rs").is_err()); +} + #[test] fn join_windows_root_relative_path_preserves_drive_or_share() { for (base, path, expected) in [ From 9212b3eca86e6eca061b15fe9f8971ca57ebe3c9 Mon Sep 17 00:00:00 2001 From: jgershen-oai Date: Tue, 29 Sep 2026 22:33:28 +0000 Subject: [PATCH 09/24] Serialize tests that share Windows sandbox accounts (#49389) ## Why Elevated sandbox tests share machine-local Windows accounts. Concurrent setup can rotate account passwords between another test's setup and logon. ## What changed - Add a shared file-lock guard for elevated sandbox tests in `codex-core`, `codex-exec-server`, and `codex-windows-sandbox`, held through sandbox commands and cleanup. - Queue these tests in a single-threaded Nextest group before their timeouts start, while retaining the file lock to coordinate other test processes. - Limit account locking to tests that use elevated accounts and remove the sandbox crate's blanket Bazel `exclusive-if-local` tag. GitOrigin-RevId: 26d4606cb1fdcac65926d29835bed4eac04c19c0 --- codex-rs/.config/nextest.toml | 20 ++++++++ codex-rs/Cargo.lock | 10 ++++ codex-rs/Cargo.toml | 2 + codex-rs/core/Cargo.toml | 1 + codex-rs/core/tests/suite/windows_sandbox.rs | 32 +++++------- codex-rs/exec-server/Cargo.toml | 1 + .../exec-server/tests/file_system_windows.rs | 2 + codex-rs/windows-sandbox-rs/BUILD.bazel | 4 -- codex-rs/windows-sandbox-rs/Cargo.toml | 1 + .../src/unified_exec/tests.rs | 3 ++ .../tests/support/BUILD.bazel | 8 +++ .../tests/support/Cargo.toml | 15 ++++++ .../tests/support/src/lib.rs | 49 +++++++++++++++++++ 13 files changed, 125 insertions(+), 23 deletions(-) create mode 100644 codex-rs/windows-sandbox-rs/tests/support/BUILD.bazel create mode 100644 codex-rs/windows-sandbox-rs/tests/support/Cargo.toml create mode 100644 codex-rs/windows-sandbox-rs/tests/support/src/lib.rs diff --git a/codex-rs/.config/nextest.toml b/codex-rs/.config/nextest.toml index 6d873605633f..17decdc0b855 100644 --- a/codex-rs/.config/nextest.toml +++ b/codex-rs/.config/nextest.toml @@ -43,6 +43,9 @@ max-threads = 1 [test-groups.windows_sandbox_legacy_sessions] max-threads = 1 +[test-groups.windows_sandbox_accounts] +max-threads = 1 + [test-groups.windows_process_heavy] max-threads = 2 @@ -82,6 +85,23 @@ test-group = 'app_server_integration_local' filter = 'package(codex-core) & kind(test) & test(apply_patch_cli)' test-group = 'core_apply_patch_cli_integration' +[[profile.default.overrides]] +# These cases share the runner's elevated sandbox accounts. Queue them before +# starting their timeout; their file lock still coordinates other test processes. +platform = 'cfg(windows)' +filter = ''' + (package(codex-core) & ( + test(windows_sandbox_cli_preserves_managed_deny_reads_across_launches) | + test(windows_elevated_does_not_create_missing_workspace_metadata) | + test(windows_elevated_enforces_deny_read_and_protects_setup_marker) | + test(windows_elevated_unified_exec_enforces_large_recursive_deny_reads) | + test(windows_elevated_approved_git_pull_preserves_deny_read) + )) | + (package(codex-exec-server) & test(file_system_elevated_relative_read_denial_uses_policy_cwd)) | + (package(codex-windows-sandbox) & test(elevated_non_tty_cmd_forwards_env_output_and_exit)) +''' +test-group = 'windows_sandbox_accounts' + [[profile.default.overrides]] # These tests create restricted-token Windows child processes and private desktops. # Serialize them to avoid exhausting Windows session/global desktop resources in CI. diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index c81c7467c73d..234023d280da 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2976,6 +2976,7 @@ dependencies = [ "codex-utils-string", "codex-web-search-extension", "codex-windows-sandbox", + "codex-windows-sandbox-test-support", "core_test_support", "ctor 0.6.3", "dirs", @@ -3213,6 +3214,7 @@ dependencies = [ "codex-utils-rustls-provider", "codex-websocket-auth", "codex-websocket-client", + "codex-windows-sandbox-test-support", "ctor 0.6.3", "dirs", "futures", @@ -5282,6 +5284,7 @@ dependencies = [ "codex-utils-process", "codex-utils-pty", "codex-utils-string", + "codex-windows-sandbox-test-support", "dirs-next", "dunce", "glob", @@ -5313,6 +5316,13 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "codex-windows-sandbox-test-support" +version = "0.0.0" +dependencies = [ + "dirs", +] + [[package]] name = "codex-workload-identity" version = "0.0.0" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 4a063dc3a85f..0e01c4534be3 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -112,6 +112,7 @@ members = [ "v8-poc", "websocket-auth", "websocket-client", + "windows-sandbox-rs/tests/support", "windows-sandbox-service", "worktree", "workload-identity", @@ -316,6 +317,7 @@ codex-user-verification = { path = "user-verification" } codex-v8-poc = { path = "v8-poc" } codex-workload-identity = { path = "workload-identity" } codex-windows-sandbox = { path = "windows-sandbox-rs" } +codex-windows-sandbox-test-support = { path = "windows-sandbox-rs/tests/support" } core_test_support = { path = "core/tests/common" } # External diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 53c64fff363e..661bfdd43a99 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -151,6 +151,7 @@ codex-test-binary-support = { workspace = true } codex-utils-cargo-bin = { workspace = true } codex-utils-redacted-string = { workspace = true } codex-web-search-extension = { workspace = true } +codex-windows-sandbox-test-support = { workspace = true } core_test_support = { workspace = true } ctor = { workspace = true } insta = { workspace = true } diff --git a/codex-rs/core/tests/suite/windows_sandbox.rs b/codex-rs/core/tests/suite/windows_sandbox.rs index ea830aed64e5..ccbabef97f74 100644 --- a/codex-rs/core/tests/suite/windows_sandbox.rs +++ b/codex-rs/core/tests/suite/windows_sandbox.rs @@ -25,6 +25,7 @@ use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; use codex_protocol::protocol::ReviewDecision; use codex_protocol::user_input::UserInput; +use codex_windows_sandbox_test_support::WindowsSandboxAccountTestGuard; use core_test_support::PathExt; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -41,7 +42,6 @@ use serde_json::json; use serial_test::serial; use std::collections::HashMap; use std::ffi::OsString; -use std::fs::File; use std::path::Path; use std::path::PathBuf; use std::process::Command; @@ -75,32 +75,21 @@ impl Drop for EnvVarGuard { } enum TestCodexHome { - Persistent(PathBuf, File), - Temporary(TempDir, File), + Persistent(PathBuf), + Temporary(TempDir), } impl TestCodexHome { fn path(&self) -> &Path { match self { - Self::Persistent(path, _account_lock) => path.as_path(), - Self::Temporary(temp_dir, _account_lock) => temp_dir.path(), + Self::Persistent(path) => path.as_path(), + Self::Temporary(temp_dir) => temp_dir.path(), } } } fn codex_home_for_windows_sandbox_test(name: &str) -> anyhow::Result { - // Bazel shards and nextest processes share Windows accounts. Hold this lock - // through the test so they cannot rotate each other's credentials mid-launch. - let lock_path = dirs::data_local_dir() - .context("resolve local app data for the Windows sandbox test lock")? - .join("codex-windows-sandbox-integration-test.lock"); - let lock = File::options() - .read(true) - .append(true) - .create(true) - .open(lock_path)?; - lock.lock().context("lock Windows sandbox test accounts")?; - + // Only tests that use the elevated accounts also take the shared account guard. if let Some(test_tmpdir) = std::env::var_os("TEST_TMPDIR") { // The elevated backend provisions machine-local sandbox users. Bazel // retries run in the same Windows VM, so keep CODEX_HOME stable within @@ -108,10 +97,10 @@ fn codex_home_for_windows_sandbox_test(name: &str) -> anyhow::Result anyhow::Result<()> { @@ -205,6 +194,7 @@ fn assert_managed_deny_probe(output: &std::process::Output, launch: usize) -> an #[test] #[serial(codex_home)] fn windows_sandbox_cli_preserves_managed_deny_reads_across_launches() -> anyhow::Result<()> { + let _account_guard = WindowsSandboxAccountTestGuard::acquire()?; let codex_home = codex_home_for_windows_sandbox_test("windows-cli-managed-deny-read-codex-home")?; @@ -415,6 +405,7 @@ async fn windows_restricted_token_rejects_exact_and_glob_deny_read_policy() -> a #[tokio::test] #[serial(codex_home)] async fn windows_elevated_does_not_create_missing_workspace_metadata() -> anyhow::Result<()> { + let _account_guard = WindowsSandboxAccountTestGuard::acquire()?; let codex_home = codex_home_for_windows_sandbox_test("windows-elevated-missing-metadata-codex-home")?; let _codex_home_guard = EnvVarGuard::set("CODEX_HOME", codex_home.path().as_os_str()); @@ -540,6 +531,7 @@ $rules = foreach ($name in @('codex_sandbox_offline_block_inbound', 'codex_sandb #[tokio::test] #[serial(codex_home)] async fn windows_elevated_enforces_deny_read_and_protects_setup_marker() -> anyhow::Result<()> { + let _account_guard = WindowsSandboxAccountTestGuard::acquire()?; let codex_home = codex_home_for_windows_sandbox_test("windows-elevated-deny-read-codex-home")?; let _codex_home_guard = EnvVarGuard::set("CODEX_HOME", codex_home.path().as_os_str()); stage_windows_sandbox_helpers()?; @@ -680,6 +672,7 @@ async fn windows_elevated_enforces_deny_read_and_protects_setup_marker() -> anyh #[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[serial(codex_home)] async fn windows_elevated_unified_exec_enforces_large_recursive_deny_reads() -> anyhow::Result<()> { + let _account_guard = WindowsSandboxAccountTestGuard::acquire()?; let codex_home = codex_home_for_windows_sandbox_test("windows-elevated-tool-runtime-deny-read-codex-home")?; let _codex_home_guard = EnvVarGuard::set("CODEX_HOME", codex_home.path().as_os_str()); @@ -857,6 +850,7 @@ async fn windows_elevated_approved_git_pull_preserves_deny_read() -> anyhow::Res use EventMsg::ExecCommandEnd; use EventMsg::TurnComplete; + let _account_guard = WindowsSandboxAccountTestGuard::acquire()?; let codex_home = codex_home_for_windows_sandbox_test("windows-elevated-git-pull-deny-read")?; let _codex_home_guard = EnvVarGuard::set("CODEX_HOME", codex_home.path().as_os_str()); stage_windows_sandbox_helpers()?; diff --git a/codex-rs/exec-server/Cargo.toml b/codex-rs/exec-server/Cargo.toml index 3c78a5021c49..25500aaaf5fb 100644 --- a/codex-rs/exec-server/Cargo.toml +++ b/codex-rs/exec-server/Cargo.toml @@ -95,6 +95,7 @@ anyhow = { workspace = true } codex-exec-server-test-support = { workspace = true } codex-test-binary-support = { workspace = true } codex-utils-cargo-bin = { workspace = true } +codex-windows-sandbox-test-support = { workspace = true } ctor = { workspace = true } http = { workspace = true } opentelemetry_sdk = { workspace = true } diff --git a/codex-rs/exec-server/tests/file_system_windows.rs b/codex-rs/exec-server/tests/file_system_windows.rs index 1b9830a1ce2b..8193b570ea08 100644 --- a/codex-rs/exec-server/tests/file_system_windows.rs +++ b/codex-rs/exec-server/tests/file_system_windows.rs @@ -33,6 +33,7 @@ use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::SandboxPolicy; use codex_sandboxing::SandboxType; use codex_utils_path_uri::PathUri; +use codex_windows_sandbox_test_support::WindowsSandboxAccountTestGuard; use futures::TryStreamExt; use pretty_assertions::assert_eq; use test_case::test_case; @@ -456,6 +457,7 @@ async fn file_system_remote_fs_helper_respects_windows_sandbox_write_policy( async fn file_system_elevated_relative_read_denial_uses_policy_cwd( implementation: FileSystemImplementation, ) -> Result<()> { + let _account_guard = WindowsSandboxAccountTestGuard::acquire()?; // Both implementations re-enter this test binary; the elevated backend finds its helpers // next to that binary, while Cargo and Bazel provide them separately. let test_exe = std::env::current_exe()?; diff --git a/codex-rs/windows-sandbox-rs/BUILD.bazel b/codex-rs/windows-sandbox-rs/BUILD.bazel index 50255d45406f..8531cb4d664c 100644 --- a/codex-rs/windows-sandbox-rs/BUILD.bazel +++ b/codex-rs/windows-sandbox-rs/BUILD.bazel @@ -55,8 +55,4 @@ codex_rust_crate( ":codex-command-runner", ":codex-windows-sandbox-setup", ], - # Setup rotates machine-wide account passwords. The process-local test lock - # cannot prevent exec-server's elevated filesystem tests from rotating them - # between this crate's setup and logon. Windows tests execute locally in CI. - test_tags = ["exclusive-if-local"], ) diff --git a/codex-rs/windows-sandbox-rs/Cargo.toml b/codex-rs/windows-sandbox-rs/Cargo.toml index 65332f3506bf..356991b1ca87 100644 --- a/codex-rs/windows-sandbox-rs/Cargo.toml +++ b/codex-rs/windows-sandbox-rs/Cargo.toml @@ -102,4 +102,5 @@ features = [ version = "0.52" [dev-dependencies] +codex-windows-sandbox-test-support = { workspace = true } pretty_assertions = { workspace = true } diff --git a/codex-rs/windows-sandbox-rs/src/unified_exec/tests.rs b/codex-rs/windows-sandbox-rs/src/unified_exec/tests.rs index f35ee56b3821..4520a51a88f8 100644 --- a/codex-rs/windows-sandbox-rs/src/unified_exec/tests.rs +++ b/codex-rs/windows-sandbox-rs/src/unified_exec/tests.rs @@ -16,6 +16,7 @@ use codex_protocol::models::PermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_pty::ProcessDriver; use codex_utils_pty::ProcessSignal; +use codex_windows_sandbox_test_support::WindowsSandboxAccountTestGuard; use pretty_assertions::assert_eq; use std::collections::HashMap; use std::fs; @@ -302,6 +303,8 @@ fn legacy_non_tty_cmd_emits_output() { #[test] fn elevated_non_tty_cmd_forwards_env_output_and_exit() { + let _account_guard = + WindowsSandboxAccountTestGuard::acquire().expect("lock Windows sandbox test accounts"); let _guard = legacy_process_test_guard(); let runtime = current_thread_runtime(); runtime.block_on(async move { diff --git a/codex-rs/windows-sandbox-rs/tests/support/BUILD.bazel b/codex-rs/windows-sandbox-rs/tests/support/BUILD.bazel new file mode 100644 index 000000000000..dea523964c5b --- /dev/null +++ b/codex-rs/windows-sandbox-rs/tests/support/BUILD.bazel @@ -0,0 +1,8 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "support", + crate_name = "codex_windows_sandbox_test_support", + # This support library has no tests of its own. + test_tags = ["manual"], +) diff --git a/codex-rs/windows-sandbox-rs/tests/support/Cargo.toml b/codex-rs/windows-sandbox-rs/tests/support/Cargo.toml new file mode 100644 index 000000000000..6cfb1fb78e85 --- /dev/null +++ b/codex-rs/windows-sandbox-rs/tests/support/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "codex-windows-sandbox-test-support" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lib] +test = false +doctest = false + +[lints] +workspace = true + +[target.'cfg(windows)'.dependencies] +dirs = { workspace = true } diff --git a/codex-rs/windows-sandbox-rs/tests/support/src/lib.rs b/codex-rs/windows-sandbox-rs/tests/support/src/lib.rs new file mode 100644 index 000000000000..7c69fd1fee49 --- /dev/null +++ b/codex-rs/windows-sandbox-rs/tests/support/src/lib.rs @@ -0,0 +1,49 @@ +//! Coordinates tests that share the runner's Windows sandbox accounts across processes. + +#![cfg(windows)] + +use std::fs::File; +use std::fs::OpenOptions; +use std::io; + +/// Serializes tests that provision or use the runner's shared Windows sandbox accounts. +/// Hold this from before setup until all sandbox commands and cleanup have finished. +#[must_use = "the account lock is released when the guard is dropped"] +pub struct WindowsSandboxAccountTestGuard { + _file: File, +} + +impl WindowsSandboxAccountTestGuard { + /// Acquires the same file lock across all test processes run by this Windows user. + pub fn acquire() -> io::Result { + let directory = dirs::data_local_dir().ok_or_else(|| { + io::Error::new( + io::ErrorKind::NotFound, + "Windows local application data directory is unavailable", + ) + })?; + // Bazel uses per-test temporary directories, so the lock lives in the runner user's + // Windows known folder instead. It is separate from the production setup lock. + let path = directory.join("codex-windows-sandbox-integration-test.lock"); + // Leave the file in place: deleting it would let processes lock different files at this path. + let file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(path) + .map_err(|error| { + io::Error::new( + error.kind(), + format!("open Windows sandbox test lock: {error}"), + ) + })?; + file.lock().map_err(|error| { + io::Error::new( + error.kind(), + format!("lock Windows sandbox test accounts: {error}"), + ) + })?; + Ok(Self { _file: file }) + } +} From 05ea5f757e2d696b13a2da0f660c694e381a1a8d Mon Sep 17 00:00:00 2001 From: Celia Chen Date: Tue, 29 Sep 2026 22:42:25 +0000 Subject: [PATCH 10/24] Add attributed MCP OAuth credential storage telemetry (#49392) ## What changed - Instrument MCP OAuth loads, saves, deletes, fallback cleanup, and refresh persistence with credential-free storage metrics, distinguishing configured policy from operations on a pinned store. - Retain the originating client across background tasks, blocking storage operations, and session recovery, including recovery after startup found no credentials. - Preserve typed keyring and lock error categories for metrics and remove raw error details from storage warnings. ## Testing Add tests for policy and pinned-store outcomes, best-effort cleanup failures, wrapped lock errors, and successful or failed refresh persistence without duplicate observations. Add recovery tests that verify client attribution after anonymous startup and across threads with different originators. GitOrigin-RevId: 5ea4528bcdcf500c8ffdf1d9c7008ea52112c726 --- codex-rs/Cargo.lock | 1 + .../src/request_processors/mcp_processor.rs | 21 +- codex-rs/codex-mcp/src/connection_manager.rs | 21 +- codex-rs/codex-mcp/src/rmcp_client.rs | 12 +- codex-rs/core/src/session/mcp.rs | 18 +- codex-rs/core/src/session/mcp_runtime.rs | 5 +- .../suite/mcp_storage_telemetry_tests.rs | 177 +++++++ codex-rs/core/tests/suite/rmcp_client.rs | 2 + codex-rs/rmcp-client/Cargo.toml | 11 +- .../rmcp-client/src/ema_identity_tests.rs | 8 +- codex-rs/rmcp-client/src/oauth.rs | 148 +++--- .../rmcp-client/src/oauth/credential_store.rs | 8 +- .../rmcp-client/src/oauth/ema_identity.rs | 2 +- .../src/oauth/ema_identity_tests.rs | 4 +- .../src/oauth/refresh_transaction.rs | 12 +- .../rmcp-client/src/oauth/resolved_store.rs | 250 +++++++--- codex-rs/rmcp-client/src/oauth/telemetry.rs | 97 ++++ .../src/oauth/tests/credential_store_tests.rs | 10 +- .../src/oauth/tests/persistor_tests.rs | 14 +- .../src/oauth/tests/store_lock_tests.rs | 2 +- .../src/oauth/tests/telemetry_tests.rs | 460 ++++++++++++++++++ .../rmcp-client/src/perform_oauth_login.rs | 6 +- codex-rs/rmcp-client/src/rmcp_client.rs | 22 +- .../tests/streamable_http_recovery.rs | 3 + .../telemetry_tests.rs | 152 ++++++ 25 files changed, 1279 insertions(+), 187 deletions(-) create mode 100644 codex-rs/core/tests/suite/mcp_storage_telemetry_tests.rs create mode 100644 codex-rs/rmcp-client/src/oauth/telemetry.rs create mode 100644 codex-rs/rmcp-client/src/oauth/tests/telemetry_tests.rs create mode 100644 codex-rs/rmcp-client/tests/streamable_http_recovery/telemetry_tests.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 234023d280da..56a74c464dca 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -4389,6 +4389,7 @@ dependencies = [ "libc", "memchr", "oauth2", + "opentelemetry_sdk", "pretty_assertions", "rmcp", "serde", diff --git a/codex-rs/app-server/src/request_processors/mcp_processor.rs b/codex-rs/app-server/src/request_processors/mcp_processor.rs index ac2a55ba798d..16adf8eb2133 100644 --- a/codex-rs/app-server/src/request_processors/mcp_processor.rs +++ b/codex-rs/app-server/src/request_processors/mcp_processor.rs @@ -7,6 +7,7 @@ use codex_mcp::McpServerSource; use codex_mcp::ReadResourceRequestParams; use codex_mcp::ema_auth_scope; use codex_mcp::resolve_oauth_callback; +use codex_otel::auth_storage::AuthStorageOriginator; use codex_rmcp_client::EnterpriseOAuthLoginRequest; use std::future::Future; @@ -379,12 +380,13 @@ impl McpRequestProcessor { None => None, }; let processor = self.clone(); - tokio::spawn(async move { + let task = async move { let result = processor .list_mcp_server_status_response(params, thread) .await; outgoing.send_result(request, result).await; - }); + }; + tokio::spawn(AuthStorageOriginator::current().scope(task)); Ok(()) } @@ -598,7 +600,7 @@ impl McpRequestProcessor { let (_, thread) = self.load_thread(&thread_id).await?; let request_id = request_id.clone(); - tokio::spawn(async move { + let task = async move { let origin_call_id = origin_call_id.filter(|_| server == codex_mcp::CODEX_APPS_MCP_SERVER_NAME); let result = match origin_call_id.as_deref() { @@ -611,7 +613,8 @@ impl McpRequestProcessor { }; Self::send_mcp_resource_read_response(outgoing, request_id, result, origin_call_id) .await; - }); + }; + tokio::spawn(AuthStorageOriginator::current().scope(task)); return Ok(()); } @@ -633,7 +636,7 @@ impl McpRequestProcessor { McpRuntimeContext::new(Arc::clone(&environment_manager), config.cwd.to_path_buf()); let request_id = request_id.clone(); - tokio::spawn(async move { + let task = async move { let result = read_mcp_resource_without_thread( &mcp_config, auth.as_ref(), @@ -649,7 +652,8 @@ impl McpRequestProcessor { outgoing, request_id, result, /*origin_call_id*/ None, ) .await; - }); + }; + tokio::spawn(AuthStorageOriginator::current().scope(task)); Ok(()) } @@ -687,14 +691,15 @@ impl McpRequestProcessor { let meta = with_mcp_tool_call_thread_id_meta(params.meta, &thread_id); let request_id = request_id.clone(); - tokio::spawn(async move { + let task = async move { let result = thread .call_mcp_tool(¶ms.server, ¶ms.tool, params.arguments, meta) .await .map(McpServerToolCallResponse::from) .map_err(mcp_operation_error); outgoing.send_result(request_id, result).await; - }); + }; + tokio::spawn(AuthStorageOriginator::current().scope(task)); Ok(()) } } diff --git a/codex-rs/codex-mcp/src/connection_manager.rs b/codex-rs/codex-mcp/src/connection_manager.rs index ad4fbf482881..65f7c5b4457e 100644 --- a/codex-rs/codex-mcp/src/connection_manager.rs +++ b/codex-rs/codex-mcp/src/connection_manager.rs @@ -65,6 +65,7 @@ use codex_config::McpServerTransportConfig; use codex_config::McpStartupReadiness; use codex_diagnostics::Gauge; use codex_diagnostics::GaugeGuard; +use codex_otel::auth_storage::AuthStorageOriginator; use codex_protocol::mcp::CallToolResult; use codex_protocol::mcp::McpServerInfo; use codex_protocol::protocol::Event; @@ -772,6 +773,7 @@ impl McpConnectionSet { (server_name, outcome) }; + let startup = AuthStorageOriginator::current().scope(startup); if defer_startup { // Dormant servers must not hold the initial startup summary open. tokio::spawn(startup); @@ -893,15 +895,18 @@ impl McpConnectionSet { return Vec::new(); } + let originator = AuthStorageOriginator::current(); match tokio::task::spawn_blocking(move || { - candidates - .into_iter() - .filter_map(|(server_name, identity, config)| { - identity - .oauth_credentials_changed(&server_name, &config) - .then_some(server_name) - }) - .collect() + originator.sync_scope(|| { + candidates + .into_iter() + .filter_map(|(server_name, identity, config)| { + identity + .oauth_credentials_changed(&server_name, &config) + .then_some(server_name) + }) + .collect() + }) }) .await { diff --git a/codex-rs/codex-mcp/src/rmcp_client.rs b/codex-rs/codex-mcp/src/rmcp_client.rs index f40ca172240e..6d1d90b74ca8 100644 --- a/codex-rs/codex-mcp/src/rmcp_client.rs +++ b/codex-rs/codex-mcp/src/rmcp_client.rs @@ -57,6 +57,7 @@ use codex_connectors::ConnectorRuntimeContext; use codex_connectors::ConnectorRuntimeFetchSource; use codex_exec_server::Environment; use codex_login::AuthChangeState; +use codex_otel::auth_storage::AuthStorageOriginator; use codex_protocol::mcp::ClientMcpExtensions; use codex_protocol::mcp::McpServerInfo; use codex_protocol::protocol::Event; @@ -286,6 +287,7 @@ fn codex_apps_reconnect_backoff(consecutive_failures: u32) -> Duration { #[derive(Clone)] struct ManagedClientStartup { + originator: AuthStorageOriginator, server_name: String, server: EffectiveMcpServer, store_mode: OAuthCredentialsStoreMode, @@ -316,6 +318,7 @@ impl ManagedClientStartup { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) = None; let Self { + originator, server_name, server, store_mode, @@ -343,7 +346,7 @@ impl ManagedClientStartup { .startup_timeout_sec .unwrap_or(DEFAULT_STARTUP_TIMEOUT); let cancel_token_for_fut = cancel_token; - async move { + let startup = async move { let tool_catalog_fetch_ticket = tool_catalog_cache_context .as_ref() .map(McpToolCatalogCacheContext::begin_fetch); @@ -420,10 +423,8 @@ impl ManagedClientStartup { startup_complete.store(true, Ordering::Release); outcome - } - .in_current_span() - .boxed() - .shared() + }; + originator.scope(startup).in_current_span().boxed().shared() } } @@ -480,6 +481,7 @@ impl AsyncManagedClient { let startup_complete = Arc::new(AtomicBool::new(false)); let server_capabilities = Arc::new(StdMutex::new(None)); let startup = Arc::new(ManagedClientStartup { + originator: AuthStorageOriginator::current(), server_name, server, store_mode, diff --git a/codex-rs/core/src/session/mcp.rs b/codex-rs/core/src/session/mcp.rs index 889ae89a2e3a..e7c95576db7a 100644 --- a/codex-rs/core/src/session/mcp.rs +++ b/codex-rs/core/src/session/mcp.rs @@ -11,6 +11,7 @@ use codex_mcp::ElicitationReviewRequest; use codex_mcp::ElicitationReviewer; use codex_mcp::ElicitationReviewerHandle; use codex_mcp::MCP_TOOL_CODEX_APPS_META_KEY; +use codex_otel::auth_storage::AuthStorageOriginator; use codex_prompts::ResolvedModelMessages; use codex_protocol::capabilities::CapabilityRootLocation; use codex_protocol::capabilities::SelectedCapabilityRoot; @@ -343,7 +344,9 @@ impl Session { input.mcp_servers.contains_key(CODEX_APPS_MCP_SERVER_NAME), "unknown MCP server '{CODEX_APPS_MCP_SERVER_NAME}'" ); - let refreshed = self.services.mcp_runtime.replace_fresh(input).await; + let refreshed = AuthStorageOriginator::from_client_name(&desired.originator) + .scope(self.services.mcp_runtime.replace_fresh(input)) + .await; self.services.thread_extension_data.insert(selected_plugins); refreshed } @@ -375,11 +378,14 @@ impl Session { self.mark_mcp_runtime_dirty(); } - let recovered_oauth_servers = self - .services - .mcp_runtime - .updated_oauth_credentials_after_auth_failure() - .await; + let recovered_oauth_servers = + AuthStorageOriginator::from_client_name(&turn_context.originator) + .scope( + self.services + .mcp_runtime + .updated_oauth_credentials_after_auth_failure(), + ) + .await; if !recovered_oauth_servers.is_empty() && let Ok(_refresh) = self.mcp_refresh.acquire().await && self diff --git a/codex-rs/core/src/session/mcp_runtime.rs b/codex-rs/core/src/session/mcp_runtime.rs index 800527900f2f..375f88c45fdc 100644 --- a/codex-rs/core/src/session/mcp_runtime.rs +++ b/codex-rs/core/src/session/mcp_runtime.rs @@ -16,6 +16,7 @@ use codex_mcp::McpServerSource; use codex_mcp::McpStartupPolicy; use codex_mcp::PreparedMcpCall; use codex_mcp::ToolInfo; +use codex_otel::auth_storage::AuthStorageOriginator; use codex_protocol::capabilities::SelectedCapabilityRoot; use std::collections::HashSet; @@ -300,7 +301,9 @@ impl Session { ready_selected_capability_roots, elicitation_reviewer, ); - self.services.mcp_runtime.replace(input).await; + AuthStorageOriginator::from_client_name(&desired.originator) + .scope(self.services.mcp_runtime.replace(input)) + .await; self.services.thread_extension_data.insert(selected_plugins); } diff --git a/codex-rs/core/tests/suite/mcp_storage_telemetry_tests.rs b/codex-rs/core/tests/suite/mcp_storage_telemetry_tests.rs new file mode 100644 index 000000000000..71a970115002 --- /dev/null +++ b/codex-rs/core/tests/suite/mcp_storage_telemetry_tests.rs @@ -0,0 +1,177 @@ +//! Exercises thread-owned storage attribution through turn-time MCP authentication recovery. + +use anyhow::Result; +use codex_config::McpServerConfig; +use codex_config::types::OAuthCredentialsStoreMode; +use codex_core::StartThreadOptions; +use codex_core::TurnInputRequest; +use codex_history::InitialHistory; +use codex_history::RolloutItem; +use codex_otel::MetricsClient; +use codex_otel::MetricsConfig; +use codex_protocol::protocol::EventMsg; +use codex_protocol::protocol::SessionMeta; +use codex_protocol::protocol::SessionMetaLine; +use codex_protocol::user_input::UserInput; +use core_test_support::responses; +use core_test_support::skip_if_no_network; +use core_test_support::test_codex::test_codex; +use core_test_support::wait_for_event; +use opentelemetry_sdk::metrics::data::AggregatedMetrics; +use opentelemetry_sdk::metrics::data::MetricData; +use pretty_assertions::assert_eq; +use serde_json::json; +use std::collections::BTreeMap; +use std::sync::Arc; +use wiremock::Mock; +use wiremock::ResponseTemplate; +use wiremock::matchers::method; +use wiremock::matchers::path; + +fn storage_loads(metrics: &MetricsClient) -> Result> { + let mut counts = BTreeMap::new(); + for metric in metrics + .snapshot()? + .scope_metrics() + .flat_map(opentelemetry_sdk::metrics::data::ScopeMetrics::metrics) + { + if metric.name() == "codex.auth_storage.operation" + && let AggregatedMetrics::U64(MetricData::Sum(sum)) = metric.data() + { + for point in sum.data_points() { + let tags: BTreeMap<_, _> = point + .attributes() + .map(|tag| (tag.key.to_string(), tag.value.to_string())) + .collect(); + if tags.get("credential_kind").map(String::as_str) == Some("mcp") + && tags.get("operation").map(String::as_str) == Some("load") + { + *counts.entry(tags["originator"].clone()).or_default() += point.value(); + } + } + } + } + Ok(counts) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn recovery_without_a_snapshot_keeps_each_threads_originator() -> Result<()> { + skip_if_no_network!(Ok(())); + // Isolate global metrics and CODEX_HOME from other integration tests. + if std::env::var_os("CODEX_MCP_TELEMETRY_TEST_CHILD").is_none() { + let home = tempfile::tempdir()?; + let output = tokio::process::Command::new(std::env::current_exe()?) + .args(["--exact", "suite::rmcp_client::storage_telemetry_tests::recovery_without_a_snapshot_keeps_each_threads_originator", "--nocapture"]) + .env("CODEX_MCP_TELEMETRY_TEST_CHILD", "1") + .env("CODEX_HOME", home.path()) + .output().await?; + assert!( + output.status.success(), + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + return Ok(()); + } + let server = responses::start_mock_server().await; + let mcp = responses::start_mock_server().await; + Mock::given(method("POST")) + .and(path("/mcp")) + .respond_with(ResponseTemplate::new(/*s*/ 401).append_header("www-authenticate", "Bearer")) + .mount(&mcp) + .await; + Mock::given(method("GET")) + .and(path("/.well-known/oauth-authorization-server/mcp")) + .respond_with(ResponseTemplate::new(/*s*/ 200).set_body_json(json!({ + "authorization_endpoint": format!("{}/authorize", mcp.uri()), + "token_endpoint": format!("{}/token", mcp.uri()), + }))) + .mount(&mcp) + .await; + let home = Arc::new(tempfile::tempdir()?); + // The OAuth endpoint remains on the host when the test uses a remote executor. + let fixture = test_codex() + .with_home(home) + .build_with_remote_and_local_env(&server) + .await?; + let server_config: McpServerConfig = + serde_json::from_value(json!({"url": format!("{}/mcp", mcp.uri())}))?; + let mut config = fixture.config.clone(); + config.mcp_oauth_credentials_store_mode = OAuthCredentialsStoreMode::File; + config + .mcp_servers + .set([("reauth".into(), server_config)].into())?; + let metrics = codex_otel::install_global_metrics(MetricsClient::new( + MetricsConfig::in_memory("test", "test", "1", Default::default()).with_runtime_reader(), + )?); + let originators = ["codex_desktop", "codex_vscode"]; + let mut threads = Vec::new(); + for originator in originators { + let thread = fixture + .thread_manager + .start_thread(StartThreadOptions { + initial_history: InitialHistory::Forked(vec![RolloutItem::SessionMeta( + SessionMetaLine { + meta: SessionMeta { + originator: originator.into(), + ..Default::default() + }, + git: None, + }, + )]), + environments: Some(fixture.codex.environment_selections().await), + ..StartThreadOptions::new(config.clone()) + }) + .await? + .thread; + let EventMsg::McpStartupComplete(startup) = wait_for_event(&thread, |event| { + matches!(event, EventMsg::McpStartupComplete(_)) + }) + .await + else { + unreachable!(); + }; + assert_eq!( + startup + .failed + .iter() + .map(|failure| failure.server.as_str()) + .collect::>(), + vec!["reauth"] + ); + threads.push(thread); + responses::mount_sse_once( + &server, + responses::sse(vec![ + responses::ev_assistant_message("msg", "Sign in to continue."), + responses::ev_completed("resp"), + ]), + ) + .await; + } + // The runtime reader exports deltas; drain startup observations before recovery. + let _ = storage_loads(&metrics)?; + futures::future::try_join_all(threads.iter().map(|thread| async { + thread + .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { + text: "Continue".into(), + text_elements: Vec::new(), + }])) + .await?; + wait_for_event(thread, |event| matches!(event, EventMsg::TurnComplete(_))).await; + Ok::<_, anyhow::Error>(()) + })) + .await?; + let mut after = storage_loads(&metrics)?; + after.retain(|_, count| *count > 0); + assert_eq!( + after.keys().map(String::as_str).collect::>(), + originators + ); + for thread in threads { + thread.shutdown_and_wait().await?; + } + fixture.codex.shutdown_and_wait().await?; + metrics.shutdown()?; + Ok(()) +} diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index 2833288bb678..5400f0f4efa2 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -132,6 +132,8 @@ use wiremock::MockServer; #[path = "mcp_oauth_refresh_tests.rs"] mod oauth_refresh_tests; +#[path = "mcp_storage_telemetry_tests.rs"] +mod storage_telemetry_tests; static OPENAI_PNG: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAD0AAAA9CAYAAAAeYmHpAAAE6klEQVR4Aeyau44UVxCGx1fZsmRLlm3Zoe0XcGQ5cUiCCIgJeS9CHgAhMkISQnIuGQgJEkBcxLW+nqnZ6uqqc+nuWRC7q/P3qetf9e+MtOwyX25O4Nep6JPyop++0qev9HrfgZ+F6r2DuB/vHOrt/UIkqdDHYvujOW6fO7h/CNEI+a5jc+pBR8uy0jVFsziYu5HtfSUk+Io34q921hLNctFSX0gwww+S8wce8K1LfCU+cYW4888aov8NxqvQILUPPReLOrm6zyLxa4i+6VZuFbJo8d1MOHZm+7VUtB/aIvhPWc/3SWg49JcwFLlHxuXKjtyloo+YNhuW3VS+WPBuUEMvCFKjEDVgFBQHXrnazpqiSxNZCkQ1kYiozsbm9Oz7l4i2Il7vGccGNWAc3XosDrZe/9P3ZnMmzHNEQw4smf8RQ87XEAMsC7Az0Au+dgXerfH4+sHvEc0SYGic8WBBUGqFH2gN7yDrazy7m2pbRTeRmU3+MjZmr1h6LJgPbGy23SI6GlYT0brQ71IY8Us4PNQCm+zepSbaD2BY9xCaAsD9IIj/IzFmKMSdHHonwdZATbTnYREf6/VZGER98N9yCWIvXQwXDoDdhZJoT8jwLnJXDB9w4Sb3e6nK5ndzlkTLnP3JBu4LKkbrYrU69gCVceV0JvpyuW1xlsUVngzhwMetn/XamtTORF9IO5YnWNiyeF9zCAfqR3fUW+vZZKLtgP+ts8BmQRBREAdRDhH3o8QuRh/YucNFz2BEjxbRN6LGzphfKmvP6v6QhqIQyZ8XNJ0W0X83MR1PEcJBNO2KC2Z1TW/v244scp9FwRViZxIOBF0Lctk7ZVSavdLvRlV1hz/ysUi9sr8CIcB3nvWBwA93ykTz18eAYxQ6N/K2DkPA1lv3iXCwmDUT7YkjIby9siXueIJj9H+pzSqJ9oIuJWTUgSSt4WO7o/9GGg0viR4VinNRUDoIj34xoCd6pxD3aK3zfdbnx5v1J3ZNNEJsE0sBG7N27ReDrJc4sFxz7dI/ZAbOmmiKvHBitQXpAdR6+F7v+/ol/tOouUV01EeMZQF2BoQDn6dP4XNr+j9GZEtEK1/L8pFw7bd3a53tsTa7WD+054jOFmPg1XBKPQgnqFfmFcy32ZRvjmiIIQTYFvyDxQ8nH8WIwwGwlyDjDznnilYyFr6njrlZwsKkBpO59A7OwgdzPEWRm+G+oeb7IfyNuzjEEVLrOVxJsxvxwF8kmCM6I2QYmJunz4u4TrADpfl7mlbRTWQ7VmrBzh3+C9f6Grc3YoGN9dg/SXFthpRsT6vobfXRs2VBlgBHXVMLHjDNbIZv1sZ9+X3hB09cXdH1JKViyG0+W9bWZDa/r2f9zAFR71sTzGpMSWz2iI4YssWjWo3REy1MDGjdwe5e0dFSiAC1JakBvu4/CUS8Eh6dqHdU0Or0ioY3W5ClSqDXAy7/6SRfgw8vt4I+tbvvNtFT2kVDhY5+IGb1rCqYaXNF08vSALsXCPmt0kQNqJT1p5eI1mkIV/BxCY1z85lOzeFbPBQHURkkPTlwTYK9gTVE25l84IbFFN+YJDHjdpn0gq6mrHht0dkcjbM4UL9283O5p77GN+SPW/QwVB4IUYg7Or+Kp7naR6qktP98LNF2UxWo9yObPIT9KYg+hK4i56no4rfnM0qeyFf6AwAAAP//trwR3wAAAAZJREFUAwBZ0sR75itw5gAAAABJRU5ErkJggg=="; diff --git a/codex-rs/rmcp-client/Cargo.toml b/codex-rs/rmcp-client/Cargo.toml index b133ddaf46f4..671bfd9ecc97 100644 --- a/codex-rs/rmcp-client/Cargo.toml +++ b/codex-rs/rmcp-client/Cargo.toml @@ -69,6 +69,7 @@ webbrowser = { workspace = true } which = { workspace = true } [dev-dependencies] +opentelemetry_sdk = { workspace = true, features = ["testing"] } codex-http-client = { workspace = true } codex-utils-cargo-bin = { workspace = true } pretty_assertions = { workspace = true } @@ -94,10 +95,14 @@ windows-sys = { version = "0.52", features = ["Win32_Storage_FileSystem"] } [target.'cfg(any(target_os = "freebsd", target_os = "openbsd"))'.dependencies] keyring = { workspace = true, features = ["sync-secret-service"] } -# This test is compiled through `#[path]` inside the inline `oauth::tests` module. Cargo-shear -# cannot resolve that nested module path and otherwise reports the linked file as unlinked. +# These tests are compiled through `#[path]` inside the inline `oauth::tests` module. Cargo-shear +# cannot resolve those nested module paths and otherwise reports the linked files as unlinked. [package.metadata.cargo-shear] -ignored-paths = ["src/oauth/tests/persistor_tests.rs", "src/oauth/tests/credential_store_tests.rs"] +ignored-paths = [ + "src/oauth/tests/persistor_tests.rs", + "src/oauth/tests/credential_store_tests.rs", + "src/oauth/tests/telemetry_tests.rs", +] [lib] doctest = false diff --git a/codex-rs/rmcp-client/src/ema_identity_tests.rs b/codex-rs/rmcp-client/src/ema_identity_tests.rs index 53c7ef497ad4..56222f744dca 100644 --- a/codex-rs/rmcp-client/src/ema_identity_tests.rs +++ b/codex-rs/rmcp-client/src/ema_identity_tests.rs @@ -233,7 +233,7 @@ impl KeyringStore for GatedKeyringStore { async fn refresh_subject_reread_is_cancellable_and_releases_guard_on_failure() -> Result<()> { let _home = TempCodexHome::new(); let (_server, issuer) = discovery().await; - let store = ResolvedOAuthCredentialStore::Keyring(AuthKeyringBackendKind::Direct); + let store = ResolvedOAuthCredentialStore::keyring(AuthKeyringBackendKind::Direct); let stored = credentials(&issuer, "user", /*expires_at*/ 0); let snapshot = StoredOAuthCredentialSnapshot::new(stored.clone(), store); for outcome in [ @@ -312,7 +312,7 @@ async fn refresh_subject_reread_is_cancellable_and_releases_guard_on_failure() - async fn refresh_subject_rereads_pinned_credentials_after_id_token_expiry() -> Result<()> { let _home = TempCodexHome::new(); let (_server, issuer) = discovery().await; - let store = ResolvedOAuthCredentialStore::Keyring(AuthKeyringBackendKind::Direct); + let store = ResolvedOAuthCredentialStore::keyring(AuthKeyringBackendKind::Direct); let stored = credentials(&issuer, "user", /*expires_at*/ 0); let snapshot = StoredOAuthCredentialSnapshot::new(stored.clone(), store); let keyring = MockKeyringStore::default(); @@ -341,7 +341,7 @@ async fn refresh_subject_rejects_removed_replaced_or_missing_credentials() -> Re let _home = TempCodexHome::new(); let (server, issuer) = discovery().await; let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); - let store = ResolvedOAuthCredentialStore::Keyring(AuthKeyringBackendKind::Direct); + let store = ResolvedOAuthCredentialStore::keyring(AuthKeyringBackendKind::Direct); let original = credentials(&issuer, "user", /*expires_at*/ 0); for change in [ "deleted", @@ -357,7 +357,7 @@ async fn refresh_subject_rejects_removed_replaced_or_missing_credentials() -> Re let snapshot = StoredOAuthCredentialSnapshot::new( original.clone(), if change == "file" { - ResolvedOAuthCredentialStore::File + ResolvedOAuthCredentialStore::file() } else { store }, diff --git a/codex-rs/rmcp-client/src/oauth.rs b/codex-rs/rmcp-client/src/oauth.rs index c4fa7a1f9550..f3127d399ad1 100644 --- a/codex-rs/rmcp-client/src/oauth.rs +++ b/codex-rs/rmcp-client/src/oauth.rs @@ -25,6 +25,7 @@ mod refresh_transaction; mod resolved_store; mod runtime; mod store_lock; +mod telemetry; #[cfg(test)] #[path = "oauth/test_support.rs"] @@ -35,6 +36,8 @@ use anyhow::Error; use anyhow::Result; use codex_config::types::AuthKeyringBackendKind; use codex_config::types::OAuthCredentialsStoreMode; +use codex_otel::auth_storage::Operation; +use codex_otel::auth_storage::Store; use codex_secrets::LocalSecretsNamespace; use codex_secrets::SecretName; use codex_secrets::SecretScope; @@ -214,7 +217,7 @@ impl StoredOAuthCredentialSnapshot { store_mode: OAuthCredentialsStoreMode, keyring_backend_kind: AuthKeyringBackendKind, ) -> Result> { - if self.store == ResolvedOAuthCredentialStore::File + if self.store == ResolvedOAuthCredentialStore::file() && store_mode == OAuthCredentialsStoreMode::Auto { return Self::for_runtime_refresh( @@ -227,7 +230,15 @@ impl StoredOAuthCredentialSnapshot { .map(|snapshot| snapshot.map(|snapshot| snapshot.credentials)); } - let credentials = match self.store.try_load(&DefaultKeyringStore, server_name, url) { + let mut observation = telemetry::resolved(self.store, Operation::Load); + let result = self.store.try_load(&DefaultKeyringStore, server_name, url); + observation.record_load_attempt(telemetry::store(self.store), &result); + if matches!(self.store.backend, resolved_store::Backend::Keyring(_)) + && let Err(error) = &result + { + telemetry::record_secure_error(&mut observation, error.as_ref()); + } + let credentials = match result { Ok(credentials) => credentials, Err(error) if oauth_store_is_contended(&error) => return Ok(None), Err(error) => return Err(error), @@ -393,7 +404,7 @@ fn load_oauth_tokens_from_direct_keyring( Ok(Some(tokens)) } Ok(None) => Ok(None), - Err(error) => Err(Error::new(error.into_error())), + Err(error) => Err(Error::new(std::io::Error::from(error))), } } @@ -440,7 +451,7 @@ enum OAuthKeyringLoadError { #[error(transparent)] StoreLock(#[from] OAuthStoreLockFailure), /// The selected keyring backend itself was unavailable or its data was invalid. - #[error(transparent)] + #[error("{0}")] Backend(#[from] anyhow::Error), } @@ -463,6 +474,7 @@ pub(crate) fn save_oauth_tokens_with_lock_held( keyring_backend_kind: AuthKeyringBackendKind, ) -> Result<()> { let keyring_store = DefaultKeyringStore; + let mut observation = telemetry::policy(store_mode, keyring_backend_kind, Operation::Save); match store_mode { OAuthCredentialsStoreMode::Auto => save_oauth_tokens_with_keyring_with_fallback_to_file( &keyring_store, @@ -470,13 +482,25 @@ pub(crate) fn save_oauth_tokens_with_lock_held( server_name, tokens, ), - OAuthCredentialsStoreMode::File => save_oauth_tokens_to_file(tokens), - OAuthCredentialsStoreMode::Keyring => save_oauth_tokens_with_keyring_and_cleanup_file( - &keyring_store, - keyring_backend_kind, - server_name, - tokens, - ), + OAuthCredentialsStoreMode::File => { + let result = save_oauth_tokens_to_file(tokens); + observation.record_save_attempt(Store::File, &result); + result + } + OAuthCredentialsStoreMode::Keyring => { + let result = save_oauth_tokens_with_keyring_and_cleanup_file( + &keyring_store, + store_mode, + keyring_backend_kind, + server_name, + tokens, + ); + observation.record_save_attempt(telemetry::keyring(keyring_backend_kind), &result); + if let Err(error) = &result { + telemetry::record_secure_error(&mut observation, error.as_ref()); + } + result + } } } @@ -506,17 +530,10 @@ fn save_oauth_tokens_to_direct_keyring( let serialized = serde_json::to_string(tokens).context("failed to serialize OAuth tokens")?; let key = compute_store_key(server_name, &tokens.url)?; - match keyring_store.save(KEYRING_SERVICE, &key, &serialized) { - Ok(()) => Ok(()), - Err(error) => { - let message = format!( - "failed to write OAuth tokens to keyring: {}", - error.message() - ); - warn!("{message}"); - Err(Error::new(error.into_error()).context(message)) - } - } + keyring_store + .save(KEYRING_SERVICE, &key, &serialized) + .map_err(std::io::Error::from) + .context("failed to write OAuth tokens to keyring") } /// Saves one credential while holding the Secrets aggregate-store lock across the mutation. @@ -558,20 +575,20 @@ fn save_oauth_tokens_to_secrets_keyring_with_lock_held( keyring_store: &K, + store_mode: OAuthCredentialsStoreMode, keyring_backend_kind: AuthKeyringBackendKind, server_name: &str, tokens: &StoredOAuthTokens, ) -> Result<()> { save_oauth_tokens_with_keyring(keyring_store, keyring_backend_kind, server_name, tokens)?; let key = compute_store_key(server_name, &tokens.url)?; - if let Err(error) = delete_oauth_tokens_from_file(&key) { - warn!( - server_name, - keyring_backend = ?keyring_backend_kind, - error = %error, - "failed to remove OAuth tokens from fallback storage" - ); + let mut observation = telemetry::policy(store_mode, keyring_backend_kind, Operation::Cleanup); + let result = delete_oauth_tokens_from_file(&key); + observation.record_delete_attempt(Store::File, &result); + if result.is_err() { + warn!("failed to remove OAuth tokens from fallback storage"); } + drop(observation); Ok(()) } @@ -581,22 +598,34 @@ fn save_oauth_tokens_with_keyring_with_fallback_to_file Result<()> { - match save_oauth_tokens_with_keyring_and_cleanup_file( + let mut observation = telemetry::policy( + OAuthCredentialsStoreMode::Auto, + keyring_backend_kind, + Operation::Save, + ); + let result = save_oauth_tokens_with_keyring_and_cleanup_file( keyring_store, + OAuthCredentialsStoreMode::Auto, keyring_backend_kind, server_name, tokens, - ) { + ); + observation.record_save_attempt(telemetry::keyring(keyring_backend_kind), &result); + match result { Ok(()) => Ok(()), // As on load, a store lock failure is a coordination failure rather than evidence that // the keyring backend is unavailable. Falling back could leave a newer File token hidden // behind a stale Secrets entry. - Err(error) if error.downcast_ref::().is_some() => Err(error), + Err(error) if error.downcast_ref::().is_some() => { + telemetry::record_secure_error(&mut observation, error.as_ref()); + Err(error) + } Err(error) => { let message = error.to_string(); - warn!("falling back to file storage for OAuth tokens: {message}"); - save_oauth_tokens_to_file(tokens) - .with_context(|| format!("failed to write OAuth tokens to keyring: {message}")) + telemetry::record_secure_error(&mut observation, error.as_ref()); + let result = save_oauth_tokens_to_file(tokens); + observation.record_save_attempt(Store::File, &result); + result.with_context(|| format!("failed to write OAuth tokens to keyring: {message}")) } } } @@ -636,25 +665,28 @@ fn delete_oauth_tokens_from_keyring_and_file( server_name: &str, url: &str, ) -> Result { - let key = compute_store_key(server_name, url)?; - let keyring_result = - delete_oauth_tokens_from_keyring(keyring_store, keyring_backend_kind, server_name, url); - let keyring_removed = match keyring_result { - Ok(removed) => removed, - Err(error) => { - let message = error.to_string(); - warn!("failed to delete OAuth tokens from keyring: {message}"); - match store_mode { - OAuthCredentialsStoreMode::Auto | OAuthCredentialsStoreMode::Keyring => { - return Err(error).context("failed to delete OAuth tokens from keyring"); + let mut observation = telemetry::policy(store_mode, keyring_backend_kind, Operation::Delete); + let result = compute_store_key(server_name, url).and_then(|key| { + let keyring_result = + delete_oauth_tokens_from_keyring(keyring_store, keyring_backend_kind, server_name, url); + let keyring_removed = match keyring_result { + Ok(removed) => removed, + Err(error) => { + warn!("failed to delete OAuth tokens from keyring"); + match store_mode { + OAuthCredentialsStoreMode::Auto | OAuthCredentialsStoreMode::Keyring => { + return Err(error).context("failed to delete OAuth tokens from keyring"); + } + OAuthCredentialsStoreMode::File => false, } - OAuthCredentialsStoreMode::File => false, } - } - }; + }; - let file_removed = delete_oauth_tokens_from_file(&key)?; - Ok(keyring_removed || file_removed) + let file_removed = delete_oauth_tokens_from_file(&key)?; + Ok(keyring_removed || file_removed) + }); + observation.record_delete_attempt(Store::Multiple, &result); + result } fn delete_oauth_tokens_from_keyring( @@ -685,7 +717,7 @@ fn delete_oauth_tokens_from_direct_keyring( let key = compute_store_key(server_name, url)?; keyring_store .delete(KEYRING_SERVICE, &key) - .map_err(|error| Error::new(error.into_error())) + .map_err(|error| Error::new(std::io::Error::from(error))) } fn delete_oauth_tokens_from_secrets_keyring( @@ -786,7 +818,7 @@ impl OAuthPersistor { expires_at, }; if last_credentials.as_ref() != Some(&stored) { - self.inner.credential_store.save( + self.inner.credential_store.save_with_refresh_telemetry( &DefaultKeyringStore, &self.inner.server_name, &stored, @@ -1160,6 +1192,8 @@ mod tests { mod credential_store_tests; #[path = "persistor_tests.rs"] mod persistor_tests; + #[path = "telemetry_tests.rs"] + mod telemetry_tests; use super::test_support::TempCodexHome; @@ -1224,7 +1258,7 @@ mod tests { .expect("tokens should load from keyring"); assert_eq!( resolved.store, - ResolvedOAuthCredentialStore::Keyring(AuthKeyringBackendKind::Direct) + ResolvedOAuthCredentialStore::keyring(AuthKeyringBackendKind::Direct) ); assert_tokens_match_without_expiry(&resolved.tokens, &expected); Ok(()) @@ -1247,7 +1281,7 @@ mod tests { AuthKeyringBackendKind::Direct, )? .expect("tokens should load from fallback"); - assert_eq!(resolved.store, ResolvedOAuthCredentialStore::File); + assert_eq!(resolved.store, ResolvedOAuthCredentialStore::file()); assert_tokens_match_without_expiry(&resolved.tokens, &expected); Ok(()) } @@ -1271,7 +1305,7 @@ mod tests { AuthKeyringBackendKind::Direct, )? .expect("tokens should load from fallback"); - assert_eq!(resolved.store, ResolvedOAuthCredentialStore::File); + assert_eq!(resolved.store, ResolvedOAuthCredentialStore::file()); assert_tokens_match_without_expiry(&resolved.tokens, &expected); Ok(()) } @@ -1298,7 +1332,7 @@ mod tests { )?; assert_eq!(fs::read(fallback_path)?, fallback_before); - let loaded = ResolvedOAuthCredentialStore::Keyring(AuthKeyringBackendKind::Direct) + let loaded = ResolvedOAuthCredentialStore::keyring(AuthKeyringBackendKind::Direct) .load(&store, &keyring_tokens.server_name, &keyring_tokens.url)? .expect("tokens should load from the selected keyring store"); assert_tokens_match_without_expiry(&loaded, &keyring_tokens); diff --git a/codex-rs/rmcp-client/src/oauth/credential_store.rs b/codex-rs/rmcp-client/src/oauth/credential_store.rs index 2d79fe403b68..c191e6936a48 100644 --- a/codex-rs/rmcp-client/src/oauth/credential_store.rs +++ b/codex-rs/rmcp-client/src/oauth/credential_store.rs @@ -243,9 +243,11 @@ impl CredentialStore for OAuthCredentialStore .map_err(credential_store_error)?; let tokens = tokio::task::spawn_blocking(move || -> Result { let _guard = guard; - inner - .store - .save(&inner.keyring, &inner.server_name, &tokens)?; + inner.store.save_with_refresh_telemetry( + &inner.keyring, + &inner.server_name, + &tokens, + )?; Ok(tokens) }) .await diff --git a/codex-rs/rmcp-client/src/oauth/ema_identity.rs b/codex-rs/rmcp-client/src/oauth/ema_identity.rs index 6f804a245f63..bf4ea0d17a33 100644 --- a/codex-rs/rmcp-client/src/oauth/ema_identity.rs +++ b/codex-rs/rmcp-client/src/oauth/ema_identity.rs @@ -45,7 +45,7 @@ impl StoredOAuthCredentialSnapshot { &self, keyring_store: &K, ) -> Result { - if !matches!(self.store, ResolvedOAuthCredentialStore::Keyring(_)) { + if self.store == ResolvedOAuthCredentialStore::file() { bail!("enterprise IdP credentials require keyring storage"); } let previous = &self.credentials; diff --git a/codex-rs/rmcp-client/src/oauth/ema_identity_tests.rs b/codex-rs/rmcp-client/src/oauth/ema_identity_tests.rs index c66721f3959b..bd804998d220 100644 --- a/codex-rs/rmcp-client/src/oauth/ema_identity_tests.rs +++ b/codex-rs/rmcp-client/src/oauth/ema_identity_tests.rs @@ -25,7 +25,7 @@ async fn keyring_failure_does_not_reuse_the_pinned_refresh_token() -> Result<()> let _lock = RefreshCredentialLock::acquire_for_server(&tokens.server_name, &tokens.url).await?; let snapshot = StoredOAuthCredentialSnapshot::new( tokens, - ResolvedOAuthCredentialStore::Keyring(AuthKeyringBackendKind::Direct), + ResolvedOAuthCredentialStore::keyring(AuthKeyringBackendKind::Direct), ); let keyring = MockKeyringStore::default(); keyring.set_error( @@ -64,7 +64,7 @@ fn ordinary_oauth_names_cannot_alias_enterprise_credential_keys() -> Result<()> ); let keyring = MockKeyringStore::default(); - let store = ResolvedOAuthCredentialStore::Keyring(AuthKeyringBackendKind::Direct); + let store = ResolvedOAuthCredentialStore::keyring(AuthKeyringBackendKind::Direct); let enterprise_tokens: StoredOAuthTokens = serde_json::from_value(json!({ "server_name": enterprise_name, "url": issuer, "issuer": issuer, "client_id": "idp-client", "token_response": { diff --git a/codex-rs/rmcp-client/src/oauth/refresh_transaction.rs b/codex-rs/rmcp-client/src/oauth/refresh_transaction.rs index e5341ca73b97..641f930c9265 100644 --- a/codex-rs/rmcp-client/src/oauth/refresh_transaction.rs +++ b/codex-rs/rmcp-client/src/oauth/refresh_transaction.rs @@ -246,13 +246,13 @@ impl OAuthPersistor { // TODO: Add a bounded persistence retry only if telemetry shows this is common; never // silently switch stores or continue with an unpersisted credential. debug!("persisting refreshed MCP OAuth credentials to the resolved store"); - if let Err(error) = - self.inner - .credential_store - .save(keyring_store, &self.inner.server_name, &refreshed) - { + let result = self.inner.credential_store.save_with_refresh_telemetry( + keyring_store, + &self.inner.server_name, + &refreshed, + ); + if let Err(error) = result { warn!( - error = %error, "failed to persist refreshed MCP OAuth credentials; returning the error and restoring the previous in-process credentials" ); install_tokens_in_manager(&mut guard, &latest, self.inner.oauth_config.as_ref()) diff --git a/codex-rs/rmcp-client/src/oauth/resolved_store.rs b/codex-rs/rmcp-client/src/oauth/resolved_store.rs index 904f5d967056..bdd29b58f94e 100644 --- a/codex-rs/rmcp-client/src/oauth/resolved_store.rs +++ b/codex-rs/rmcp-client/src/oauth/resolved_store.rs @@ -1,11 +1,15 @@ //! Resolves the configured MCP OAuth store and pins that concrete source for one client lifecycle. +//! Retains the originating client for later background storage observations. +use super::telemetry; use anyhow::Context; use anyhow::Result; use codex_config::types::AuthKeyringBackendKind; use codex_config::types::OAuthCredentialsStoreMode; use codex_keyring_store::KeyringStore; -use tracing::warn; +use codex_otel::auth_storage::AuthStorageOriginator; +use codex_otel::auth_storage::Operation; +use codex_otel::auth_storage::Store; use super::OAuthKeyringLoadError; use super::OAuthStore; @@ -29,13 +33,57 @@ use super::save_oauth_tokens_with_keyring; /// client that loaded credentials from one store must reread, refresh, persist, and remove only /// through that store. A mid-lifecycle backend failure is unexpected and must return an error /// rather than falling back to another possibly stale refresh token. +#[derive(Debug, Clone, Copy)] +pub(crate) struct ResolvedOAuthCredentialStore { + pub(super) backend: Backend, + pub(super) mode: OAuthCredentialsStoreMode, + pub(super) kind: AuthKeyringBackendKind, + pub(super) originator: AuthStorageOriginator, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum ResolvedOAuthCredentialStore { +pub(super) enum Backend { File, Keyring(AuthKeyringBackendKind), } +// Telemetry metadata must not change credential identity or trigger a reconnect. +impl PartialEq for ResolvedOAuthCredentialStore { + fn eq(&self, other: &Self) -> bool { + self.backend == other.backend + } +} +impl Eq for ResolvedOAuthCredentialStore {} + impl ResolvedOAuthCredentialStore { + pub(crate) fn file() -> Self { + Self { + backend: Backend::File, + mode: OAuthCredentialsStoreMode::File, + kind: AuthKeyringBackendKind::Direct, + originator: AuthStorageOriginator::current(), + } + } + + pub(crate) fn keyring(kind: AuthKeyringBackendKind) -> Self { + Self { + backend: Backend::Keyring(kind), + mode: OAuthCredentialsStoreMode::Keyring, + kind, + originator: AuthStorageOriginator::current(), + } + } + + fn with_policy( + mut self, + mode: OAuthCredentialsStoreMode, + kind: AuthKeyringBackendKind, + ) -> Self { + self.mode = mode; + self.kind = kind; + self + } + /// Loads credentials only from this already-resolved authority. /// /// Unlike `resolve_oauth_tokens_from_store_policy`, this never evaluates configured @@ -46,10 +94,11 @@ impl ResolvedOAuthCredentialStore { server_name: &str, url: &str, ) -> Result> { - match self { - Self::File => load_oauth_tokens_from_file(server_name, url) + let mut observation = telemetry::resolved(self, Operation::Load); + let result = match self.backend { + Backend::File => load_oauth_tokens_from_file(server_name, url) .context("failed to reread OAuth tokens from resolved file storage"), - Self::Keyring(keyring_backend_kind) => load_oauth_tokens_from_keyring( + Backend::Keyring(keyring_backend_kind) => load_oauth_tokens_from_keyring( keyring_store, keyring_backend_kind, server_name, @@ -59,7 +108,14 @@ impl ResolvedOAuthCredentialStore { .context( "failed to reread OAuth tokens from resolved keyring storage; refusing file fallback", ), + }; + observation.record_load_attempt(telemetry::store(self), &result); + if matches!(self.backend, Backend::Keyring(_)) + && let Err(error) = &result + { + telemetry::record_secure_error(&mut observation, error.as_ref()); } + result } /// Reads the selected authority without waiting for its aggregate-store lock. @@ -69,16 +125,18 @@ impl ResolvedOAuthCredentialStore { server_name: &str, url: &str, ) -> Result> { - match self { - Self::File => { + match self.backend { + Backend::File => { let _store_lock = OAuthStoreLock::try_acquire_for_read(OAuthStore::File)?; load_oauth_tokens_from_file_with_lock_held(server_name, url) .context("failed to probe OAuth tokens from resolved file storage") } - Self::Keyring(AuthKeyringBackendKind::Direct) => { - self.load(keyring_store, server_name, url) + Backend::Keyring(AuthKeyringBackendKind::Direct) => { + load_oauth_tokens_from_keyring(keyring_store, AuthKeyringBackendKind::Direct, server_name, url) + .map_err(anyhow::Error::from) + .context("failed to reread OAuth tokens from resolved keyring storage; refusing file fallback") } - Self::Keyring(AuthKeyringBackendKind::Secrets) => { + Backend::Keyring(AuthKeyringBackendKind::Secrets) => { let _store_lock = OAuthStoreLock::try_acquire_for_read(OAuthStore::Secrets)?; load_oauth_tokens_from_secrets_keyring_with_lock_held( keyring_store, @@ -97,15 +155,41 @@ impl ResolvedOAuthCredentialStore { server_name: &str, tokens: &StoredOAuthTokens, ) -> Result<()> { - match self { - Self::File => save_oauth_tokens_to_file(tokens), - Self::Keyring(keyring_backend_kind) => save_oauth_tokens_with_keyring( + let mut observation = telemetry::resolved(self, Operation::Save); + let result = match self.backend { + Backend::File => save_oauth_tokens_to_file(tokens), + Backend::Keyring(keyring_backend_kind) => save_oauth_tokens_with_keyring( keyring_store, keyring_backend_kind, server_name, tokens, ), + }; + observation.record_save_attempt(telemetry::store(self), &result); + if matches!(self.backend, Backend::Keyring(_)) + && let Err(error) = &result + { + telemetry::record_secure_error(&mut observation, error.as_ref()); } + result + } + + /// Records the refresh-persistence milestone in addition to the underlying pinned save. + pub(crate) fn save_with_refresh_telemetry( + self, + keyring_store: &K, + server_name: &str, + tokens: &StoredOAuthTokens, + ) -> Result<()> { + let mut observation = telemetry::resolved(self, Operation::RefreshPersist); + let result = self.save(keyring_store, server_name, tokens); + observation.record_save_attempt(telemetry::store(self), &result); + if matches!(self.backend, Backend::Keyring(_)) + && let Err(error) = &result + { + telemetry::record_secure_error(&mut observation, error.as_ref()); + } + result } /// Deletes credentials only from this already-resolved authority. @@ -115,18 +199,24 @@ impl ResolvedOAuthCredentialStore { server_name: &str, url: &str, ) -> Result { - match self { - Self::File => { - let key = compute_store_key(server_name, url)?; - delete_oauth_tokens_from_file(&key) - } - Self::Keyring(AuthKeyringBackendKind::Direct) => { + let mut observation = telemetry::resolved(self, Operation::Delete); + let result = match self.backend { + Backend::File => compute_store_key(server_name, url) + .and_then(|key| delete_oauth_tokens_from_file(&key)), + Backend::Keyring(AuthKeyringBackendKind::Direct) => { delete_oauth_tokens_from_direct_keyring(keyring_store, server_name, url) } - Self::Keyring(AuthKeyringBackendKind::Secrets) => { + Backend::Keyring(AuthKeyringBackendKind::Secrets) => { delete_oauth_tokens_from_secrets_keyring(keyring_store, server_name, url) } + }; + observation.record_delete_attempt(telemetry::store(self), &result); + if matches!(self.backend, Backend::Keyring(_)) + && let Err(error) = &result + { + telemetry::record_secure_error(&mut observation, error.as_ref()); } + result } } @@ -143,6 +233,7 @@ pub(crate) fn resolve_oauth_tokens_from_store_policy Result> { + let mut observation = telemetry::policy(store_mode, keyring_backend_kind, Operation::Load); match store_mode { OAuthCredentialsStoreMode::Auto => { // Auto remains keyring-first at lifecycle startup. The returned source is then pinned @@ -152,59 +243,80 @@ pub(crate) fn resolve_oauth_tokens_from_store_policy Ok(Some(ResolvedOAuthTokens { tokens, - store: ResolvedOAuthCredentialStore::Keyring(keyring_backend_kind), + store: ResolvedOAuthCredentialStore::keyring(keyring_backend_kind) + .with_policy(store_mode, keyring_backend_kind), })), - Ok(None) => Ok( - load_oauth_tokens_from_file(server_name, url)?.map(|tokens| { - ResolvedOAuthTokens { - tokens, - store: ResolvedOAuthCredentialStore::File, - } - }), - ), + Ok(None) => { + let result = load_oauth_tokens_from_file(server_name, url); + observation.record_load_attempt(Store::File, &result); + Ok(result?.map(|tokens| ResolvedOAuthTokens { + tokens, + store: ResolvedOAuthCredentialStore::file() + .with_policy(store_mode, keyring_backend_kind), + })) + } // Auto may fall back when the keyring backend is unavailable, but a Secrets // aggregate-lock failure means authority may be changing. Consulting File in // that state could replay credentials hidden behind a newer Secrets entry. - Err(OAuthKeyringLoadError::StoreLock(error)) => Err(error.into()), + Err(OAuthKeyringLoadError::StoreLock(error)) => { + telemetry::record_secure_error(&mut observation, &error); + Err(error.into()) + } Err(error) => { - warn!("failed to read OAuth tokens from keyring: {error}"); - Ok(load_oauth_tokens_from_file(server_name, url) + telemetry::record_secure_error(&mut observation, &error); + let result = load_oauth_tokens_from_file(server_name, url); + observation.record_load_attempt(Store::File, &result); + Ok(result .with_context(|| { format!("failed to read OAuth tokens from keyring: {error}") })? .map(|tokens| ResolvedOAuthTokens { tokens, - store: ResolvedOAuthCredentialStore::File, + store: ResolvedOAuthCredentialStore::file() + .with_policy(store_mode, keyring_backend_kind), })) } } } - OAuthCredentialsStoreMode::File => Ok(load_oauth_tokens_from_file(server_name, url)?.map( - |tokens| ResolvedOAuthTokens { + OAuthCredentialsStoreMode::File => { + let result = load_oauth_tokens_from_file(server_name, url); + observation.record_load_attempt(Store::File, &result); + Ok(result?.map(|tokens| ResolvedOAuthTokens { tokens, - store: ResolvedOAuthCredentialStore::File, - }, - )), - OAuthCredentialsStoreMode::Keyring => Ok(load_oauth_tokens_from_keyring( - keyring_store, - keyring_backend_kind, - server_name, - url, - ) - .map_err(anyhow::Error::from) - .context("failed to read OAuth tokens from keyring")? - .map(|tokens| ResolvedOAuthTokens { - tokens, - store: ResolvedOAuthCredentialStore::Keyring(keyring_backend_kind), - })), + store: ResolvedOAuthCredentialStore::file() + .with_policy(store_mode, keyring_backend_kind), + })) + } + OAuthCredentialsStoreMode::Keyring => { + let result = load_oauth_tokens_from_keyring( + keyring_store, + keyring_backend_kind, + server_name, + url, + ); + observation.record_load_attempt(telemetry::keyring(keyring_backend_kind), &result); + if let Err(error) = &result { + telemetry::record_secure_error(&mut observation, error); + } + Ok(result + .map_err(anyhow::Error::from) + .context("failed to read OAuth tokens from keyring")? + .map(|tokens| ResolvedOAuthTokens { + tokens, + store: ResolvedOAuthCredentialStore::keyring(keyring_backend_kind) + .with_policy(store_mode, keyring_backend_kind), + })) + } } } @@ -215,24 +327,34 @@ pub(crate) fn try_resolve_oauth_tokens_from_store_policy Result> { - let load = |store: ResolvedOAuthCredentialStore| { - store - .try_load(keyring_store, server_name, url) - .map(|tokens| tokens.map(|tokens| ResolvedOAuthTokens { tokens, store })) + let mut observation = telemetry::policy(store_mode, keyring_backend_kind, Operation::Load); + let mut load = |store: ResolvedOAuthCredentialStore| { + let result = store.try_load(keyring_store, server_name, url); + observation.record_load_attempt(telemetry::store(store), &result); + if matches!(store.backend, Backend::Keyring(_)) + && let Err(error) = &result + { + telemetry::record_secure_error(&mut observation, error.as_ref()); + } + result.map(|tokens| tokens.map(|tokens| ResolvedOAuthTokens { tokens, store })) }; - let keyring = ResolvedOAuthCredentialStore::Keyring(keyring_backend_kind); + let keyring = ResolvedOAuthCredentialStore::keyring(keyring_backend_kind) + .with_policy(store_mode, keyring_backend_kind); match store_mode { - OAuthCredentialsStoreMode::File => load(ResolvedOAuthCredentialStore::File), + OAuthCredentialsStoreMode::File => { + load(ResolvedOAuthCredentialStore::file().with_policy(store_mode, keyring_backend_kind)) + } OAuthCredentialsStoreMode::Keyring => load(keyring), OAuthCredentialsStoreMode::Auto => match load(keyring) { Ok(Some(tokens)) => Ok(Some(tokens)), - Ok(None) => load(ResolvedOAuthCredentialStore::File), + Ok(None) => load( + ResolvedOAuthCredentialStore::file().with_policy(store_mode, keyring_backend_kind), + ), Err(error) if error.downcast_ref::().is_some() => Err(error), - Err(error) => { - warn!("failed to read OAuth tokens from keyring: {error}"); - load(ResolvedOAuthCredentialStore::File) - .with_context(|| format!("failed to read OAuth tokens from keyring: {error}")) - } + Err(error) => load( + ResolvedOAuthCredentialStore::file().with_policy(store_mode, keyring_backend_kind), + ) + .with_context(|| format!("failed to read OAuth tokens from keyring: {error}")), }, } } diff --git a/codex-rs/rmcp-client/src/oauth/telemetry.rs b/codex-rs/rmcp-client/src/oauth/telemetry.rs new file mode 100644 index 000000000000..cbfa47aeb74e --- /dev/null +++ b/codex-rs/rmcp-client/src/oauth/telemetry.rs @@ -0,0 +1,97 @@ +//! Maps MCP storage policy, pinned stores, and typed failures to credential-free metric dimensions. + +use super::OAuthKeyringLoadError; +use super::OAuthStoreLockFailure; +use super::ResolvedOAuthCredentialStore; +use super::resolved_store::Backend; +use codex_config::types::AuthKeyringBackendKind; +use codex_config::types::OAuthCredentialsStoreMode; +use codex_otel::auth_storage::AuthStorageOriginator; +use codex_otel::auth_storage::CredentialKind; +use codex_otel::auth_storage::Operation; +use codex_otel::auth_storage::StoragePhase; +use codex_otel::auth_storage::StorageTelemetry; +use codex_otel::auth_storage::Store; +use codex_otel::auth_storage::StoreMode; +use std::error::Error; +use std::io::ErrorKind; + +/// Preserve aggregate-lock categories even when a transparent load error hides their source. +pub(super) fn record_secure_error( + observation: &mut StorageTelemetry, + error: &(dyn Error + 'static), +) { + let mut cause = Some(error); + while let Some(current) = cause { + let lock = match current.downcast_ref::() { + Some(OAuthKeyringLoadError::StoreLock(lock)) => Some(lock), + Some(OAuthKeyringLoadError::Backend(_)) | None => { + current.downcast_ref::() + } + }; + if let Some(OAuthStoreLockFailure::Timeout { + acquire_timeout, .. + }) = lock + { + let kind = if acquire_timeout.is_zero() { + ErrorKind::WouldBlock + } else { + ErrorKind::TimedOut + }; + observation.record_secure_error(&std::io::Error::from(kind)); + return; + } + cause = current.source(); + } + observation.record_secure_error(error); +} + +pub(super) fn keyring(kind: AuthKeyringBackendKind) -> Store { + match kind { + AuthKeyringBackendKind::Direct => Store::DirectKeyring, + AuthKeyringBackendKind::Secrets => Store::Secrets, + } +} + +pub(super) fn store(store: ResolvedOAuthCredentialStore) -> Store { + match store.backend { + Backend::File => Store::File, + Backend::Keyring(kind) => keyring(kind), + } +} + +pub(super) fn policy( + mode: OAuthCredentialsStoreMode, + kind: AuthKeyringBackendKind, + operation: Operation, +) -> StorageTelemetry { + StorageTelemetry::new( + CredentialKind::Mcp, + store_mode(mode), + keyring(kind), + operation, + AuthStorageOriginator::current(), + ) +} + +pub(super) fn resolved( + source: ResolvedOAuthCredentialStore, + operation: Operation, +) -> StorageTelemetry { + StorageTelemetry::new( + CredentialKind::Mcp, + store_mode(source.mode), + keyring(source.kind), + operation, + source.originator, + ) + .with_phase(StoragePhase::Pinned) +} + +fn store_mode(mode: OAuthCredentialsStoreMode) -> StoreMode { + match mode { + OAuthCredentialsStoreMode::File => StoreMode::File, + OAuthCredentialsStoreMode::Auto => StoreMode::Auto, + OAuthCredentialsStoreMode::Keyring => StoreMode::Keyring, + } +} diff --git a/codex-rs/rmcp-client/src/oauth/tests/credential_store_tests.rs b/codex-rs/rmcp-client/src/oauth/tests/credential_store_tests.rs index 86882fc46490..575ed835cdb0 100644 --- a/codex-rs/rmcp-client/src/oauth/tests/credential_store_tests.rs +++ b/codex-rs/rmcp-client/src/oauth/tests/credential_store_tests.rs @@ -36,7 +36,7 @@ async fn mutations_require_and_retain_the_transaction_guard() -> Result<()> { save_oauth_tokens_to_file(&initial)?; let store = OAuthCredentialStore::new( initial.clone(), - ResolvedOAuthCredentialStore::File, + ResolvedOAuthCredentialStore::file(), MockKeyringStore::default(), /*oauth_config*/ None, ); @@ -97,7 +97,7 @@ async fn save_publishes_only_persisted_credentials() -> Result<()> { let _env = TempCodexHome::new(); let initial = sample_tokens(); let keyring = MockKeyringStore::default(); - let authority = ResolvedOAuthCredentialStore::Keyring(AuthKeyringBackendKind::Direct); + let authority = ResolvedOAuthCredentialStore::keyring(AuthKeyringBackendKind::Direct); authority.save(&keyring, &initial.server_name, &initial)?; let mut fallback = initial.clone(); fallback @@ -167,7 +167,7 @@ async fn pinned_read_failure_does_not_adopt_fallback_credentials() -> Result<()> keyring.set_error(&key, KeyringError::Invalid("test".into(), "load".into())); let store = OAuthCredentialStore::new( initial.clone(), - ResolvedOAuthCredentialStore::Keyring(AuthKeyringBackendKind::Direct), + ResolvedOAuthCredentialStore::keyring(AuthKeyringBackendKind::Direct), keyring, /*oauth_config*/ None, ); @@ -194,7 +194,7 @@ async fn replacement_or_removal_does_not_acknowledge_a_new_runtime_snapshot() -> save_oauth_tokens_to_file(&initial)?; let store = OAuthCredentialStore::new( initial.clone(), - ResolvedOAuthCredentialStore::File, + ResolvedOAuthCredentialStore::file(), MockKeyringStore::default(), /*oauth_config*/ None, ); @@ -238,7 +238,7 @@ async fn storage_roundtrip_preserves_absolute_and_unknown_expiry() -> Result<()> save_oauth_tokens_to_file(&initial)?; let store = OAuthCredentialStore::new( initial.clone(), - ResolvedOAuthCredentialStore::File, + ResolvedOAuthCredentialStore::file(), MockKeyringStore::default(), /*oauth_config*/ None, ); diff --git a/codex-rs/rmcp-client/src/oauth/tests/persistor_tests.rs b/codex-rs/rmcp-client/src/oauth/tests/persistor_tests.rs index 2951504e5953..e938bcec488a 100644 --- a/codex-rs/rmcp-client/src/oauth/tests/persistor_tests.rs +++ b/codex-rs/rmcp-client/src/oauth/tests/persistor_tests.rs @@ -179,7 +179,7 @@ async fn coordinated_401_refresh_rereads_and_persists_before_retry() -> Result<( let mut manager = authorization_manager_for(&initial).await?; manager.set_credential_store(OAuthCredentialStore::new( initial.clone(), - ResolvedOAuthCredentialStore::File, + ResolvedOAuthCredentialStore::file(), DefaultKeyringStore, /*oauth_config*/ None, )); @@ -396,7 +396,7 @@ async fn resolved_keyring_read_error_preserves_in_memory_credentials() -> Result initial.server_name.clone(), initial.url.clone(), Arc::clone(&manager), - ResolvedOAuthCredentialStore::Keyring(AuthKeyringBackendKind::Direct), + ResolvedOAuthCredentialStore::keyring(AuthKeyringBackendKind::Direct), Some(initial.clone()), /*oauth_config*/ None, ); @@ -875,7 +875,7 @@ async fn coordinated_manager_for( let mut manager = authorization_manager_for(tokens).await?; let store = OAuthCredentialStore::new( tokens.clone(), - ResolvedOAuthCredentialStore::File, + ResolvedOAuthCredentialStore::file(), DefaultKeyringStore, /*oauth_config*/ None, ); @@ -888,13 +888,13 @@ async fn persistor_for(tokens: &StoredOAuthTokens) -> Result { tokens.server_name.clone(), tokens.url.clone(), Arc::new(TokioMutex::new(authorization_manager_for(tokens).await?)), - ResolvedOAuthCredentialStore::File, + ResolvedOAuthCredentialStore::file(), Some(tokens.clone()), /*oauth_config*/ None, )) } -async fn test_context() -> Result<(TempCodexHome, MockServer, StoredOAuthTokens)> { +pub(super) async fn test_context() -> Result<(TempCodexHome, MockServer, StoredOAuthTokens)> { let env = TempCodexHome::new(); let server = MockServer::start().await; mount_oauth_metadata(&server).await; @@ -902,7 +902,9 @@ async fn test_context() -> Result<(TempCodexHome, MockServer, StoredOAuthTokens) Ok((env, server, tokens)) } -async fn authorization_manager_for(tokens: &StoredOAuthTokens) -> Result { +pub(super) async fn authorization_manager_for( + tokens: &StoredOAuthTokens, +) -> Result { let oauth_http_client = Arc::new(OAuthHttpClientAdapter::new( Arc::new(RouteAwareHttpClient::new(HttpClientFactory::new( OutboundProxyPolicy::ReqwestDefault, diff --git a/codex-rs/rmcp-client/src/oauth/tests/store_lock_tests.rs b/codex-rs/rmcp-client/src/oauth/tests/store_lock_tests.rs index b8428f5e5e97..8213c5ce4252 100644 --- a/codex-rs/rmcp-client/src/oauth/tests/store_lock_tests.rs +++ b/codex-rs/rmcp-client/src/oauth/tests/store_lock_tests.rs @@ -176,7 +176,7 @@ fn legacy_rmcp_oauth_keyring_credentials_remain_readable() -> Result<()> { assert_eq!( resolved.store, - crate::oauth::ResolvedOAuthCredentialStore::Keyring(AuthKeyringBackendKind::Direct) + crate::oauth::ResolvedOAuthCredentialStore::keyring(AuthKeyringBackendKind::Direct) ); assert_tokens_match_without_expiry(&resolved.tokens, &expected); assert!(crate::oauth::oauth_tokens_are_usable(&resolved.tokens)); diff --git a/codex-rs/rmcp-client/src/oauth/tests/telemetry_tests.rs b/codex-rs/rmcp-client/src/oauth/tests/telemetry_tests.rs new file mode 100644 index 000000000000..a3af6f2a4723 --- /dev/null +++ b/codex-rs/rmcp-client/src/oauth/tests/telemetry_tests.rs @@ -0,0 +1,460 @@ +//! Exercises emitted metrics through MCP storage policy and pinned-store operations. + +use super::persistor_tests::authorization_manager_for; +use super::persistor_tests::test_context; +use super::*; +use codex_otel::auth_storage::AuthStorageOriginator; +use opentelemetry_sdk::metrics::data::AggregatedMetrics; +use opentelemetry_sdk::metrics::data::MetricData; +use pretty_assertions::assert_eq; +use std::collections::BTreeMap; + +type MetricRecord = (String, u64, BTreeMap); + +#[test] +fn policy_and_pinned_operations_keep_distinct_outcomes_and_originator() -> Result<()> { + if isolated_process()? { + return Ok(()); + } + let _env = TempCodexHome::new(); + keyring::set_default_credential_builder(keyring::mock::default_credential_builder()); + let metrics = test_metrics()?; + let store = MockKeyringStore::default(); + let tokens = sample_tokens(); + let key = compute_store_key(&tokens.server_name, &tokens.url)?; + store.set_error( + &key, + KeyringError::PlatformFailure(std::io::Error::from(std::io::ErrorKind::Unsupported).into()), + ); + save_oauth_tokens_with_keyring_with_fallback_to_file( + &store, + AuthKeyringBackendKind::Direct, + &tokens.server_name, + &tokens, + )?; + store.save(KEYRING_SERVICE, &key, &serde_json::to_string(&tokens)?)?; + let resolved = AuthStorageOriginator::from_client_name("codex_vscode") + .sync_scope(|| { + resolve_oauth_tokens_from_store_policy( + &store, + &tokens.server_name, + &tokens.url, + OAuthCredentialsStoreMode::Auto, + AuthKeyringBackendKind::Direct, + ) + })? + .unwrap(); + let snapshot = StoredOAuthCredentialSnapshot::new(resolved.tokens, resolved.store); + store.set_error( + &key, + KeyringError::PlatformFailure(std::io::Error::from(std::io::ErrorKind::TimedOut).into()), + ); + // Pinned operations retain the original Auto policy and client on another thread. + std::thread::scope(|scope| { + scope + .spawn(|| { + // The SDK mock is empty; this probe must emit exactly one pinned observation. + assert_eq!( + snapshot.reload( + &tokens.server_name, + &tokens.url, + OAuthCredentialsStoreMode::Auto, + AuthKeyringBackendKind::Direct, + )?, + None + ); + assert!( + resolved + .store + .save(&store, &tokens.server_name, &tokens) + .is_err() + ); + Ok::<_, anyhow::Error>(()) + }) + .join() + .unwrap() + })?; + let mut expected = vec![ + expected_metric( + /*count*/ 1, + &[ + ("operation", "save"), + ("actual_store", "file"), + ("secure_outcome", "error"), + ("fallback_reason", "secure_error"), + ("secure_error", "unsupported"), + ("originator", "none"), + ], + ), + expected_metric(/*count*/ 1, &[]), + expected_metric( + /*count*/ 1, + &[ + ("storage_phase", "pinned"), + ("secure_outcome", "not_found"), + ("outcome", "not_found"), + ], + ), + expected_metric( + /*count*/ 1, + &[ + ("storage_phase", "pinned"), + ("operation", "save"), + ("secure_outcome", "error"), + ("outcome", "error"), + ("secure_error", "timeout"), + ], + ), + ]; + expected.sort(); + assert_eq!(operation_metrics(&metrics)?, expected); + metrics.shutdown()?; + Ok(()) +} + +fn isolated_process() -> Result { + if std::env::var_os("CODEX_AUTH_STORAGE_TEST_CHILD").is_none() { + let output = std::process::Command::new(std::env::current_exe()?) + .args([ + "--exact", + std::thread::current().name().unwrap(), + "--nocapture", + ]) + .env("CODEX_AUTH_STORAGE_TEST_CHILD", "1") + .output()?; + assert!( + output.status.success(), + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + return Ok(true); + } + Ok(false) +} + +fn operation_metrics(metrics: &codex_otel::MetricsClient) -> Result> { + let snapshot = metrics.snapshot()?; + let mut actual = Vec::new(); + for metric in snapshot + .scope_metrics() + .flat_map(opentelemetry_sdk::metrics::data::ScopeMetrics::metrics) + { + if matches!( + metric.name(), + "codex.auth_storage.operation" | "codex.auth_storage.refresh_persist" + ) && let AggregatedMetrics::U64(MetricData::Sum(sum)) = metric.data() + { + for point in sum.data_points() { + actual.push(( + metric.name().to_string(), + point.value(), + point + .attributes() + .map(|a| (a.key.to_string(), a.value.to_string())) + .collect::>(), + )); + } + } + } + actual.sort(); + Ok(actual) +} + +fn expected_metric(count: u64, tags: &[(&str, &str)]) -> MetricRecord { + let mut attributes = BTreeMap::from([ + ("credential_kind", "mcp"), + ("store_mode", "auto"), + ("selected_store", "direct_keyring"), + ("actual_store", "direct_keyring"), + ("operation", "load"), + ("secure_outcome", "success"), + ("outcome", "success"), + ("fallback_reason", "none"), + ("secure_error", "none"), + ("storage_phase", "policy"), + ("originator", "codex_vscode"), + ]); + attributes.extend(tags.iter().copied()); + ( + if attributes["operation"] == "refresh_persist" { + "codex.auth_storage.refresh_persist" + } else { + "codex.auth_storage.operation" + } + .into(), + count, + attributes + .into_iter() + .map(|(k, v)| (k.into(), v.into())) + .collect(), + ) +} + +fn test_metrics() -> Result { + Ok(codex_otel::install_global_metrics( + codex_otel::MetricsClient::new( + codex_otel::MetricsConfig::in_memory("test", "test", "1", Default::default()) + .with_runtime_reader(), + )?, + )) +} + +#[test] +fn cleanup_failure_retains_policy_and_remains_best_effort() -> Result<()> { + if isolated_process()? { + return Ok(()); + } + let _env = TempCodexHome::new(); + let metrics = test_metrics()?; + let tokens = sample_tokens(); + let key = compute_store_key(&tokens.server_name, &tokens.url)?; + std::fs::write(fallback_file_path()?, "{")?; + let mut expected = Vec::new(); + AuthStorageOriginator::from_client_name("codex_vscode").sync_scope(|| -> Result<()> { + for (mode, mode_tag) in [ + (OAuthCredentialsStoreMode::Auto, "auto"), + (OAuthCredentialsStoreMode::Keyring, "keyring"), + ] { + let store = MockKeyringStore::default(); + save_oauth_tokens_with_keyring_and_cleanup_file( + &store, + mode, + AuthKeyringBackendKind::Direct, + &tokens.server_name, + &tokens, + )?; + assert_eq!( + store.saved_value(&key), + Some(serde_json::to_string(&tokens)?) + ); + expected.push(expected_metric( + /*count*/ 1, + &[ + ("store_mode", mode_tag), + ("operation", "cleanup"), + ("actual_store", "file"), + ("secure_outcome", "not_attempted"), + ("outcome", "error"), + ], + )); + } + Ok(()) + })?; + expected.sort(); + assert_eq!(operation_metrics(&metrics)?, expected); + metrics.shutdown()?; + Ok(()) +} + +#[test] +fn lock_errors_retain_typed_categories_through_wrappers() -> Result<()> { + if isolated_process()? { + return Ok(()); + } + let metrics = test_metrics()?; + let mut expected = Vec::new(); + AuthStorageOriginator::from_client_name("codex_vscode").sync_scope(|| { + for (duration, category) in [ + (Duration::ZERO, "locked"), + (Duration::from_secs(/*secs*/ 1), "timeout"), + ] { + for wrapped in [false, true] { + let error = OAuthStoreLockFailure::Timeout { + store: OAuthStore::Secrets, + path: "private-lock-path".into(), + acquire_timeout: duration, + }; + let error = if wrapped { + Error::from(OAuthKeyringLoadError::StoreLock(error)).context("private context") + } else { + Error::from(error) + }; + let result: Result> = Err(error); + let mut observation = telemetry::policy( + OAuthCredentialsStoreMode::Auto, + AuthKeyringBackendKind::Secrets, + Operation::Load, + ); + observation.record_load_attempt(Store::Secrets, &result); + telemetry::record_secure_error( + &mut observation, + result.as_ref().unwrap_err().as_ref(), + ); + } + expected.push(expected_metric( + /*count*/ 2, + &[ + ("selected_store", "secrets"), + ("actual_store", "secrets"), + ("secure_outcome", "error"), + ("outcome", "error"), + ("secure_error", category), + ], + )); + } + }); + expected.sort(); + assert_eq!(operation_metrics(&metrics)?, expected); + metrics.shutdown()?; + Ok(()) +} + +#[tokio::test(flavor = "current_thread")] +async fn coordinated_refresh_persistence_records_success_and_failure() -> Result<()> { + use rmcp::transport::auth::CredentialStore; + + if isolated_process()? { + return Ok(()); + } + let _env = TempCodexHome::new(); + let metrics = test_metrics()?; + for fail_save in [false, true] { + let keyring = MockKeyringStore::default(); + let tokens = sample_tokens(); + let key = compute_store_key(&tokens.server_name, &tokens.url)?; + keyring.save(KEYRING_SERVICE, &key, &serde_json::to_string(&tokens)?)?; + let resolved = AuthStorageOriginator::from_client_name("codex_vscode") + .sync_scope(|| { + resolve_oauth_tokens_from_store_policy( + &keyring, + &tokens.server_name, + &tokens.url, + OAuthCredentialsStoreMode::Auto, + AuthKeyringBackendKind::Direct, + ) + })? + .unwrap(); + let store = OAuthCredentialStore::new( + tokens, + resolved.store, + keyring.clone(), + /*oauth_config*/ None, + ); + let _guard = store.acquire_transaction_guard().await?; + let mut credentials = store.load().await?.unwrap(); + credentials + .token_response + .as_mut() + .unwrap() + .set_access_token(AccessToken::new("refreshed-access".into())); + if fail_save { + keyring.set_error( + &key, + KeyringError::PlatformFailure( + std::io::Error::from(std::io::ErrorKind::TimedOut).into(), + ), + ); + } + let _ = operation_metrics(&metrics)?; + assert_eq!(store.save(credentials).await.is_err(), fail_save); + let outcome = if fail_save { "error" } else { "success" }; + let expected = ["save", "refresh_persist"].map(|operation| { + expected_metric( + /*count*/ 1, + &[ + ("operation", operation), + ("storage_phase", "pinned"), + ("secure_outcome", outcome), + ("outcome", outcome), + ("secure_error", if fail_save { "timeout" } else { "none" }), + ], + ) + }); + assert_eq!(operation_metrics(&metrics)?, expected); + } + metrics.shutdown()?; + Ok(()) +} + +#[expect( + clippy::await_holding_invalid_type, + reason = "The legacy authorization manager serializes refresh through its Tokio mutex" +)] +#[tokio::test(flavor = "current_thread")] +async fn legacy_refresh_persistence_records_each_write_once() -> Result<()> { + use wiremock::Mock; + use wiremock::ResponseTemplate; + use wiremock::matchers::method; + use wiremock::matchers::path; + + if isolated_process()? { + return Ok(()); + } + let metrics = test_metrics()?; + for proactive in [false, true] { + for fail_save in [false, true] { + let (_env, server, initial) = test_context().await?; + save_oauth_tokens_to_file(&initial)?; + let file = fallback_file_path()?; + Mock::given(method("POST")) + .and(path("/oauth/token")) + .respond_with(move |_: &wiremock::Request| { + if fail_save { + std::fs::write(&file, "{").unwrap(); + } + ResponseTemplate::new(/*s*/ 200).set_body_json(serde_json::json!({ + "access_token": "refreshed-access", "token_type": "Bearer", + "refresh_token": "rotated-refresh", "expires_in": 3600, + })) + }) + .expect(1) + .mount(&server) + .await; + let manager = Arc::new(Mutex::new(authorization_manager_for(&initial).await?)); + let authority = AuthStorageOriginator::from_client_name("codex_vscode") + .sync_scope(ResolvedOAuthCredentialStore::file); + let persistor = OAuthPersistor::new( + initial.server_name.clone(), + initial.url.clone(), + Arc::clone(&manager), + authority, + Some(initial), + /*oauth_config*/ None, + ); + let result = if proactive { + persistor.refresh_if_needed().await + } else { + manager.lock().await.refresh_token().await?; + persistor.persist_if_needed().await + }; + assert_eq!(result.is_err(), fail_save); + let mut expected = Vec::new(); + for operation in if proactive { + vec!["load", "save", "refresh_persist"] + } else { + vec!["save", "refresh_persist"] + } { + expected.push(expected_metric( + /*count*/ 1, + &[ + ("operation", operation), + ("store_mode", "file"), + ("actual_store", "file"), + ("storage_phase", "pinned"), + ("secure_outcome", "not_attempted"), + ( + "outcome", + if fail_save && operation != "load" { + "error" + } else { + "success" + }, + ), + ], + )); + } + expected.sort(); + assert_eq!(operation_metrics(&metrics)?, expected); + if !fail_save { + // The proactive path installs the persisted response; the reactive path updates + // its snapshot. Neither should emit another milestone without another write. + persistor.persist_if_needed().await?; + assert_eq!(operation_metrics(&metrics)?, Vec::new()); + } + server.verify().await; + } + } + metrics.shutdown()?; + Ok(()) +} diff --git a/codex-rs/rmcp-client/src/perform_oauth_login.rs b/codex-rs/rmcp-client/src/perform_oauth_login.rs index a271ed342e09..a9053f6b7f48 100644 --- a/codex-rs/rmcp-client/src/perform_oauth_login.rs +++ b/codex-rs/rmcp-client/src/perform_oauth_login.rs @@ -11,6 +11,7 @@ use anyhow::anyhow; use anyhow::bail; use codex_config::McpServerOAuthConfig; use codex_exec_server::HttpClient; +use codex_otel::auth_storage::AuthStorageOriginator; use rmcp::transport::AuthorizationManager; use rmcp::transport::AuthorizationSession; use rmcp::transport::auth::AuthorizationMetadata; @@ -846,14 +847,15 @@ impl OauthLoginFlow { let server_name = self.server_name.clone(); let (tx, rx) = oneshot::channel(); - tokio::spawn(async move { + let task = async move { let result = self.finish(/*emit_browser_url*/ false).await; if let Err(err) = &result { eprintln!("Failed to complete OAuth login for '{server_name}': {err:#}"); } let _ = tx.send(result); - }); + }; + tokio::spawn(AuthStorageOriginator::current().scope(task)); rx } diff --git a/codex-rs/rmcp-client/src/rmcp_client.rs b/codex-rs/rmcp-client/src/rmcp_client.rs index b8817d2ebadc..39dc2cdda454 100644 --- a/codex-rs/rmcp-client/src/rmcp_client.rs +++ b/codex-rs/rmcp-client/src/rmcp_client.rs @@ -21,6 +21,7 @@ use codex_config::types::AuthKeyringBackendKind; use codex_config::types::McpServerEnvVar; use codex_exec_server::HttpClient; use codex_keyring_store::DefaultKeyringStore; +use codex_otel::auth_storage::AuthStorageOriginator; use futures::FutureExt; use futures::future::BoxFuture; use http::HeaderMap; @@ -168,6 +169,7 @@ enum TransportRecipe { store_mode: OAuthCredentialsStoreMode, keyring_backend_kind: AuthKeyringBackendKind, pinned_credential_store: Arc>, + originator: AuthStorageOriginator, http_client: Arc, auth_provider: Option, redirect_mode: StreamableHttpRedirectMode, @@ -608,6 +610,7 @@ impl RmcpClient { store_mode, keyring_backend_kind, pinned_credential_store: Arc::new(OnceLock::new()), + originator: AuthStorageOriginator::current(), http_client, auth_provider, redirect_mode, @@ -1113,6 +1116,7 @@ impl RmcpClient { store_mode, keyring_backend_kind, pinned_credential_store, + originator, http_client, auth_provider, redirect_mode, @@ -1148,13 +1152,18 @@ impl RmcpClient { let oauth_keyring_backend_kind = *keyring_backend_kind; let pinned_credential_store = Arc::clone(pinned_credential_store); - tokio::task::spawn_blocking(move || -> Result> { + // Recovery can run after the startup scope ends, even when startup found + // no credentials and therefore could not pin a store with this attribution. + let originator = *originator; + let load = move || -> Result> { if let Some(store) = pinned_credential_store.get().copied() { // Rebuilds reread the source selected during first construction. Only // initial construction below evaluates configured store policy. return store .load(&DefaultKeyringStore, &oauth_server_name, &oauth_url) - .map(|tokens| tokens.map(|tokens| ResolvedOAuthTokens { tokens, store })); + .map(|tokens| { + tokens.map(|tokens| ResolvedOAuthTokens { tokens, store }) + }); } match resolve_oauth_tokens_from_store_policy( @@ -1183,9 +1192,12 @@ impl RmcpClient { Ok(None) } } - }) - .await - .map_err(|error| anyhow!("OAuth credential loading task failed: {error}"))?? + }; + tokio::task::spawn_blocking(move || originator.sync_scope(load)) + .await + .map_err(|error| { + anyhow!("OAuth credential loading task failed: {error}") + })?? } else { None }; diff --git a/codex-rs/rmcp-client/tests/streamable_http_recovery.rs b/codex-rs/rmcp-client/tests/streamable_http_recovery.rs index 817587a9b76a..8ac200fe0356 100644 --- a/codex-rs/rmcp-client/tests/streamable_http_recovery.rs +++ b/codex-rs/rmcp-client/tests/streamable_http_recovery.rs @@ -1,5 +1,8 @@ mod streamable_http_test_support; +#[path = "streamable_http_recovery/telemetry_tests.rs"] +mod telemetry_tests; + use std::sync::Arc; use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; diff --git a/codex-rs/rmcp-client/tests/streamable_http_recovery/telemetry_tests.rs b/codex-rs/rmcp-client/tests/streamable_http_recovery/telemetry_tests.rs new file mode 100644 index 000000000000..71171c5f73f5 --- /dev/null +++ b/codex-rs/rmcp-client/tests/streamable_http_recovery/telemetry_tests.rs @@ -0,0 +1,152 @@ +//! Checks attribution when a session rebuild discovers credentials after anonymous startup. + +use super::streamable_http_test_support::arm_session_post_failure; +use super::streamable_http_test_support::call_echo_tool; +use super::streamable_http_test_support::expected_echo_result; +use super::streamable_http_test_support::initialize_client; +use super::streamable_http_test_support::spawn_streamable_http_server; +use codex_config::types::AuthKeyringBackendKind; +use codex_config::types::OAuthCredentialsStoreMode; +use codex_exec_server::Environment; +use codex_otel::MetricsClient; +use codex_otel::MetricsConfig; +use codex_otel::auth_storage::AuthStorageOriginator; +use codex_rmcp_client::RmcpClient; +use codex_rmcp_client::StoredOAuthTokens; +use codex_rmcp_client::WrappedOAuthTokenResponse; +use codex_rmcp_client::save_oauth_tokens; +use oauth2::AccessToken; +use oauth2::basic::BasicTokenType; +use opentelemetry_sdk::metrics::data::AggregatedMetrics; +use opentelemetry_sdk::metrics::data::MetricData; +use pretty_assertions::assert_eq; +use rmcp::transport::auth::OAuthTokenResponse; +use std::collections::BTreeMap; + +#[tokio::test] +async fn recovery_retains_originator_without_initial_credentials() -> anyhow::Result<()> { + if std::env::var_os("CODEX_MCP_RECOVERY_TELEMETRY_TEST_CHILD").is_none() { + let home = tempfile::tempdir()?; + let output = tokio::process::Command::new(std::env::current_exe()?) + .args([ + "--exact", + "telemetry_tests::recovery_retains_originator_without_initial_credentials", + "--nocapture", + ]) + .env("CODEX_MCP_RECOVERY_TELEMETRY_TEST_CHILD", "1") + .env("CODEX_HOME", home.path()) + .output() + .await?; + assert!( + output.status.success(), + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + return Ok(()); + } + let metrics = codex_otel::install_global_metrics(MetricsClient::new( + MetricsConfig::in_memory("test", "test", "1", Default::default()).with_runtime_reader(), + )?); + let (_server, base_url) = spawn_streamable_http_server().await?; + let url = format!("{base_url}/mcp"); + let client = AuthStorageOriginator::from_client_name("codex_vscode") + .scope(async { + let client = RmcpClient::new_streamable_http_client( + "recovery-telemetry", + &url, + /*bearer_token*/ None, + /*http_headers*/ None, + /*env_http_headers*/ None, + OAuthCredentialsStoreMode::File, + AuthKeyringBackendKind::Direct, + Environment::default_for_tests().get_http_client(), + /*auth_provider*/ None, + ) + .await?; + initialize_client(&client).await?; + anyhow::Ok(client) + }) + .await?; + assert_eq!( + call_echo_tool(&client, "warmup").await?, + expected_echo_result("warmup") + ); + + for recovery in 0..3 { + if recovery == 1 { + // Credentials arrive after startup and the first anonymous recovery. + save_oauth_tokens( + "recovery-telemetry", + &StoredOAuthTokens { + server_name: "recovery-telemetry".into(), + url: url.clone(), + issuer: Some(url.clone()), + client_id: "client-id".into(), + token_response: WrappedOAuthTokenResponse(OAuthTokenResponse::new( + AccessToken::new("saved-access-token".into()), + BasicTokenType::Bearer, + Default::default(), + )), + expires_at: None, + }, + OAuthCredentialsStoreMode::File, + AuthKeyringBackendKind::Direct, + ) + .await?; + } + arm_session_post_failure( + &base_url, + /*status*/ 404, + /*remaining*/ 1, + /*www_authenticate_headers*/ &[], + ) + .await?; + assert_eq!( + call_echo_tool(&client, "recovered").await?, + expected_echo_result("recovered") + ); + } + + let snapshot = metrics.snapshot()?; + let mut loads = Vec::new(); + for metric in snapshot + .scope_metrics() + .flat_map(opentelemetry_sdk::metrics::data::ScopeMetrics::metrics) + { + if metric.name() == "codex.auth_storage.operation" + && let AggregatedMetrics::U64(MetricData::Sum(sum)) = metric.data() + { + for point in sum.data_points() { + let tags: BTreeMap<_, _> = point + .attributes() + .map(|tag| (tag.key.to_string(), tag.value.to_string())) + .collect(); + if tags["credential_kind"] == "mcp" && tags["operation"] == "load" { + loads.push(( + tags["originator"].clone(), + tags["storage_phase"].clone(), + tags["outcome"].clone(), + point.value(), + )); + } + } + } + } + loads.sort(); + assert_eq!( + loads, + vec![ + ("codex_vscode".into(), "pinned".into(), "success".into(), 1), + ( + "codex_vscode".into(), + "policy".into(), + "not_found".into(), + 2 + ), + ("codex_vscode".into(), "policy".into(), "success".into(), 1), + ] + ); + metrics.shutdown()?; + Ok(()) +} From ceea67163f93b4317c9352d1cd2af9fee32537b9 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Tue, 29 Sep 2026 22:58:34 +0000 Subject: [PATCH 11/24] Remove randomized greetings from TUI session headers (#49395) ## What changed Remove startup greeting phrases and the shared greeting state from startup drafts, session headers, and the empty-state animation. Raw session headers now consistently include `model:` and `directory:` fields. ## Testing Update startup and fresh-thread snapshots to omit greetings, and adjust raw-header assertions to check model and directory fields. GitOrigin-RevId: 531b5ba42d7a88f86b1c7f69030f3560abc25198 --- .../src/app/empty_state_animation_tests.rs | 38 +----- codex-rs/tui/src/app/history_ui.rs | 9 +- codex-rs/tui/src/app/session_lifecycle.rs | 2 +- ..._animation_tests__fresh_thread_header.snap | 4 +- .../tests/background_task_defaults_tests.rs | 10 -- ...rame_tests__startup_tip_in_transcript.snap | 9 +- .../tui/src/app/tests/startup_frame_tests.rs | 12 +- codex-rs/tui/src/chatwidget/constructor.rs | 3 +- codex-rs/tui/src/chatwidget/session_flow.rs | 6 +- codex-rs/tui/src/empty_state_animation.rs | 13 +-- .../src/empty_state_animation/greetings.rs | 108 ------------------ codex-rs/tui/src/history_cell/session.rs | 31 ----- ...__layout__tests__owned_startup_layout.snap | 2 +- ...artup_draft_fresh_session_transitions.snap | 13 --- ...aft__tests__startup_draft_full_frames.snap | 7 -- ...__startup_draft_onboarding_transition.snap | 3 - ...raft__tests__terminal_app_ssh_startup.snap | 5 +- codex-rs/tui/src/startup_draft.rs | 4 +- codex-rs/tui/src/startup_draft_tests.rs | 22 +--- 19 files changed, 22 insertions(+), 279 deletions(-) delete mode 100644 codex-rs/tui/src/empty_state_animation/greetings.rs diff --git a/codex-rs/tui/src/app/empty_state_animation_tests.rs b/codex-rs/tui/src/app/empty_state_animation_tests.rs index 6647d1bd0840..a1a9a66c1cc6 100644 --- a/codex-rs/tui/src/app/empty_state_animation_tests.rs +++ b/codex-rs/tui/src/app/empty_state_animation_tests.rs @@ -181,14 +181,6 @@ async fn empty_state_animation_preserves_header_cursor_and_footer() -> Result<() let size = Size::new(/*width*/ 120, /*height*/ 44); let mut tui = crate::tui::test_support::make_test_tui()?; tui.set_owned_screen(/*owned*/ true)?; - app.chat_widget - .empty_state_animation - .borrow() - .greeting - .set(crate::empty_state_animation::Greeting { - phrase: "Pull up a prompt.", - }) - .expect("initial greeting"); app.queue_clear_ui_header(&mut tui); app.transcript_cells .push(Arc::new(history_cell::StartupWarningsCell::mcp( @@ -209,7 +201,6 @@ async fn empty_state_animation_preserves_header_cursor_and_footer() -> Result<() let after = crate::custom_terminal::test_support::last_rendered_buffer(&tui.terminal); assert!(has_blossom(&tui)); assert_eq!(tui.terminal.last_known_cursor_pos, cursor); - assert!(text(&before).contains("Pull up a prompt.")); assert_eq!( &after.content[after.index_of(/*x*/ 0, before_bottom.y)..], &before.content[before.index_of(/*x*/ 0, before_bottom.y)..] @@ -222,14 +213,6 @@ async fn empty_state_animation_preserves_header_cursor_and_footer() -> Result<() text(&before) ) ); - // Editing, clearing, and resizing must never choose another phrase. - let phrase_pos = after - .content - .iter() - .position(|cell| cell.symbol() == "P") - .expect("the phrase is visible above the composer"); - assert_eq!(after.content[phrase_pos].fg, crate::style::accent_color()); - let short = Size::new(/*width*/ 120, /*height*/ 12); draw(&mut app, &mut tui, short)?; assert!(!has_blossom(&tui)); @@ -242,12 +225,6 @@ async fn empty_state_animation_preserves_header_cursor_and_footer() -> Result<() app.chat_widget.apply_external_edit(String::new()); draw(&mut app, &mut tui, size)?; assert!(has_blossom(&tui)); - assert!( - text(crate::custom_terminal::test_support::last_rendered_buffer( - &tui.terminal - )) - .contains("Pull up a prompt.") - ); assert_eq!(app.transcript_cells.len(), history_len); app.local_settings.tui.animations = false; draw(&mut app, &mut tui, size)?; @@ -269,7 +246,7 @@ async fn empty_state_animation_preserves_header_cursor_and_footer() -> Result<() } #[tokio::test] -async fn submitting_a_draft_keeps_greeting_and_dismisses_logo_even_after_clear() -> Result<()> { +async fn submitting_a_draft_dismisses_logo_even_after_clear() -> Result<()> { let (mut app, _events, _ops) = crate::app::tests::make_test_app_with_channels().await; app.local_settings.tui.animations = true; let size = Size::new(/*width*/ 120, /*height*/ 44); @@ -282,20 +259,12 @@ async fn submitting_a_draft_keeps_greeting_and_dismisses_logo_even_after_clear() .start_fresh(); draw(&mut app, &mut tui, size)?; assert!(has_blossom(&tui)); - let greeting = *app - .chat_widget - .empty_state_animation - .borrow() - .greeting - .get() - .expect("choose a greeting for the fresh thread"); app.chat_widget .apply_external_edit("first prompt".to_string()); draw(&mut app, &mut tui, size)?; let drafting = crate::custom_terminal::test_support::last_rendered_buffer(&tui.terminal); assert!(!has_blossom(&tui)); assert!(text(drafting).contains("first prompt")); - assert!(text(drafting).contains(greeting.phrase)); app.chat_widget.apply_external_edit(String::new()); draw(&mut app, &mut tui, size)?; assert!(has_blossom(&tui)); @@ -305,14 +274,9 @@ async fn submitting_a_draft_keeps_greeting_and_dismisses_logo_even_after_clear() .handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); draw(&mut app, &mut tui, size)?; assert!(!has_blossom(&tui)); - let submitted = crate::custom_terminal::test_support::last_rendered_buffer(&tui.terminal); - assert_eq!(text(submitted).matches(greeting.phrase).count(), 1); - // The header is also stable when it scrolls out and back into the viewport. let short = Size::new(/*width*/ 120, /*height*/ 8); draw(&mut app, &mut tui, short)?; draw(&mut app, &mut tui, size)?; - let restored = crate::custom_terminal::test_support::last_rendered_buffer(&tui.terminal); - assert!(text(restored).contains(greeting.phrase)); assert!(!has_blossom(&tui)); app.reset_transcript_state_after_clear(); draw(&mut app, &mut tui, size)?; diff --git a/codex-rs/tui/src/app/history_ui.rs b/codex-rs/tui/src/app/history_ui.rs index 52e7e0203ed8..f0b72ac3106d 100644 --- a/codex-rs/tui/src/app/history_ui.rs +++ b/codex-rs/tui/src/app/history_ui.rs @@ -316,18 +316,13 @@ impl App { &self, version: &'static str, ) -> history_cell::SessionHeaderHistoryCell { - let mut header = history_cell::SessionHeaderHistoryCell::new( + history_cell::SessionHeaderHistoryCell::new( self.chat_widget.model_display_name().to_string(), self.chat_widget.current_reasoning_effort(), self.config.cwd.to_path_buf(), version, ) - .with_yolo_mode(history_cell::is_yolo_mode(&self.config)); - history_cell::set_session_greeting( - &mut header, - &self.chat_widget.empty_state_animation.borrow().greeting, - ); - header + .with_yolo_mode(history_cell::is_yolo_mode(&self.config)) } pub(super) fn clear_ui_header_lines(&self, width: u16) -> Vec> { diff --git a/codex-rs/tui/src/app/session_lifecycle.rs b/codex-rs/tui/src/app/session_lifecycle.rs index 545577d8403e..4723dec35306 100644 --- a/codex-rs/tui/src/app/session_lifecycle.rs +++ b/codex-rs/tui/src/app/session_lifecycle.rs @@ -1108,7 +1108,7 @@ impl App { ThreadAttachPresentation::Fresh | ThreadAttachPresentation::FreshWithDraft ) { self.chat_widget.mark_fresh_task_for_sparkle(&started); - // FreshWithDraft inherits its provisional greeting and replay at the handoff. + // FreshWithDraft inherits its provisional replay at the handoff. if matches!(presentation, ThreadAttachPresentation::Fresh) { self.chat_widget .empty_state_animation diff --git a/codex-rs/tui/src/app/snapshots/codex_tui__app__owned_transcript__empty_state_animation_tests__fresh_thread_header.snap b/codex-rs/tui/src/app/snapshots/codex_tui__app__owned_transcript__empty_state_animation_tests__fresh_thread_header.snap index 1261499117df..789d6e4bda53 100644 --- a/codex-rs/tui/src/app/snapshots/codex_tui__app__owned_transcript__empty_state_animation_tests__fresh_thread_header.snap +++ b/codex-rs/tui/src/app/snapshots/codex_tui__app__owned_transcript__empty_state_animation_tests__fresh_thread_header.snap @@ -7,7 +7,7 @@ enabled: >_ OpenAI Codex (v0.0.0) /tmp/project - Pull up a prompt. + @@ -53,7 +53,7 @@ disabled: >_ OpenAI Codex (v0.0.0) /tmp/project - Pull up a prompt. + diff --git a/codex-rs/tui/src/app/tests/background_task_defaults_tests.rs b/codex-rs/tui/src/app/tests/background_task_defaults_tests.rs index 5512cdb0e82b..0162d1e1a655 100644 --- a/codex-rs/tui/src/app/tests/background_task_defaults_tests.rs +++ b/codex-rs/tui/src/app/tests/background_task_defaults_tests.rs @@ -86,16 +86,6 @@ async fn command_center_new_keeps_startup_draft_visible_through_handoff() -> Res app.chat_widget.composer_text_with_pending(), "draft during startup" ); - assert_eq!( - app.chat_widget - .empty_state_animation - .borrow() - .greeting - .get() - .unwrap() - .phrase, - "Pull up a prompt." - ); while let Ok(event) = events.try_recv() { Box::pin(app.handle_event(&mut tui, &mut server, event)).await?; diff --git a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__startup_frame_tests__startup_tip_in_transcript.snap b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__startup_frame_tests__startup_tip_in_transcript.snap index 8ed7c18cff6d..d45000ee78fe 100644 --- a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__startup_frame_tests__startup_tip_in_transcript.snap +++ b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__startup_frame_tests__startup_tip_in_transcript.snap @@ -1,6 +1,5 @@ --- source: tui/src/app/tests/startup_frame_tests.rs -assertion_line: 72 expression: "normalize_snapshot_paths(normalize_agent_center_snapshot(snapshots.join(\"\\n\\n\")))" --- 80 columns @@ -8,8 +7,6 @@ expression: "normalize_snapshot_paths(normalize_agent_center_snapshot(snapshots. >_ OpenAI Codex (v0.0.0) /tmp/project - Pull up a prompt. - • Conversation continues here. @@ -20,6 +17,8 @@ expression: "normalize_snapshot_paths(normalize_agent_center_snapshot(snapshots. + + › Ask Codex to do anything GPT-5.6-Sol default · /tmp/project @@ -30,8 +29,6 @@ expression: "normalize_snapshot_paths(normalize_agent_center_snapshot(snapshots. >_ OpenAI Codex (v0.0.0) /tmp/project - Pull up a prompt. - • Conversation continues here. @@ -42,6 +39,8 @@ expression: "normalize_snapshot_paths(normalize_agent_center_snapshot(snapshots. + + › Ask Codex to do anything GPT-5.6-Sol default · /tmp/pr… diff --git a/codex-rs/tui/src/app/tests/startup_frame_tests.rs b/codex-rs/tui/src/app/tests/startup_frame_tests.rs index c836d543eebb..46cf092e35c5 100644 --- a/codex-rs/tui/src/app/tests/startup_frame_tests.rs +++ b/codex-rs/tui/src/app/tests/startup_frame_tests.rs @@ -21,14 +21,8 @@ async fn owned_startup_hides_tip_in_transcript() -> Result<()> { let mut app = crate::app::test_support::make_test_app().await; app.local_settings.tui.show_tooltips = true; app.local_settings.tui.animations = false; - let greeting = Arc::new(std::sync::OnceLock::new()); - greeting - .set(crate::empty_state_animation::Greeting { - phrase: "Pull up a prompt.", - }) - .unwrap(); let session = test_thread_session(ThreadId::new(), app.config.cwd.to_path_buf()); - let mut session_info = new_session_info( + let session_info = new_session_info( &app.config, &app.local_settings, &session.model, @@ -38,7 +32,6 @@ async fn owned_startup_hides_tip_in_transcript() -> Result<()> { Some("Use /mcp to list configured MCP tools.".into()), /*auth_plan*/ None, ); - history_cell::set_session_greeting(&mut session_info, &greeting); app.transcript_cells = vec![ Arc::new(session_info), Arc::new(AgentMessageCell::new( @@ -80,9 +73,8 @@ async fn owned_startup_hides_tip_in_transcript() -> Result<()> { .map(ToString::to_string) .collect::>() .join("\n"); - assert!(text.contains("Pull up a prompt.")); assert!(text.contains("Use /mcp")); - assert!(!text.contains("model:")); + assert_eq!(text.contains("model:"), mode == HistoryRenderMode::Raw); assert!(!text.contains('╭')); } app.transcript_view.begin_search(); diff --git a/codex-rs/tui/src/chatwidget/constructor.rs b/codex-rs/tui/src/chatwidget/constructor.rs index fb4575ab0db4..35a684b3bb76 100644 --- a/codex-rs/tui/src/chatwidget/constructor.rs +++ b/codex-rs/tui/src/chatwidget/constructor.rs @@ -64,8 +64,7 @@ impl ChatWidget { }; let empty_state_animation = crate::empty_state_animation::EmptyStateAnimation::default(); - let mut header = Self::placeholder_session_header_cell(&config); - history_cell::set_session_greeting(header.as_mut(), &empty_state_animation.greeting); + let header = Self::placeholder_session_header_cell(&config); let active_cell = Some(header); let current_cwd = Some(config.cwd.to_path_buf()); diff --git a/codex-rs/tui/src/chatwidget/session_flow.rs b/codex-rs/tui/src/chatwidget/session_flow.rs index e03b2d5679be..f1e33dd29dbc 100644 --- a/codex-rs/tui/src/chatwidget/session_flow.rs +++ b/codex-rs/tui/src/chatwidget/session_flow.rs @@ -201,7 +201,7 @@ impl ChatWidget { let model_for_header = self.current_model().to_string(); if display == SessionConfiguredDisplay::Normal { let startup_tooltip_override = self.startup_tooltip_override.take(); - let mut session_info_cell = history_cell::new_session_info( + let session_info_cell = history_cell::new_session_info( &self.config, &self.local_settings, &model_for_header, @@ -211,10 +211,6 @@ impl ChatWidget { startup_tooltip_override, self.plan_type, ); - history_cell::set_session_greeting( - &mut session_info_cell, - &self.empty_state_animation.borrow().greeting, - ); self.apply_session_info_cell(session_info_cell); } else if self .transcript diff --git a/codex-rs/tui/src/empty_state_animation.rs b/codex-rs/tui/src/empty_state_animation.rs index 1d57c8761814..8003d2df7faf 100644 --- a/codex-rs/tui/src/empty_state_animation.rs +++ b/codex-rs/tui/src/empty_state_animation.rs @@ -2,14 +2,12 @@ //! Visible time pauses while hidden. Fresh conversations start settled and can replay on a click. mod geometry; -mod greetings; mod lighting; mod paths; mod policy; mod renderer; mod sequence; -use std::sync::Arc; use std::sync::OnceLock; use std::time::Duration; use std::time::Instant; @@ -24,7 +22,6 @@ use ratatui::style::Color; use crate::motion::MotionMode; use crate::terminal_palette; -pub(crate) use greetings::Greeting; use lighting::Lighting; pub(crate) use policy::Presentation; pub(crate) use policy::is_startup_cell; @@ -45,9 +42,6 @@ pub(crate) enum ComposerState { #[derive(Default)] pub(crate) struct EmptyStateAnimation { eligible: bool, - // Initialized once for fresh threads; never initialized for a resumed or forked thread. - // Headers retain the selection after the temporary blossom is dismissed. - pub(crate) greeting: Arc>, spin_elapsed: Duration, last_frame: Option, fade_elapsed: Duration, @@ -67,7 +61,6 @@ impl EmptyStateAnimation { pub(crate) fn start_fresh(&mut self) { self.cancel_replay(); self.eligible = true; - self.greeting.get_or_init(Greeting::choose); self.spin_elapsed = Duration::ZERO; self.last_frame = None; self.fade_elapsed = Duration::ZERO; @@ -75,16 +68,12 @@ impl EmptyStateAnimation { self.opacity = 1.0; } - /// Keep the provisional pose and phrase in headers already bound to the live thread. + /// Keep the provisional pose when handing off to the live thread. pub(crate) fn continue_from(&mut self, source: &mut Self) { let mut previous = std::mem::take(source); if !previous.is_eligible() { previous.start_fresh(); } - if let Some(greeting) = previous.greeting.get() { - let _ = self.greeting.set(*greeting); - } - previous.greeting = Arc::clone(&self.greeting); *self = previous; } diff --git a/codex-rs/tui/src/empty_state_animation/greetings.rs b/codex-rs/tui/src/empty_state_animation/greetings.rs deleted file mode 100644 index 6dce8f65bd0a..000000000000 --- a/codex-rs/tui/src/empty_state_animation/greetings.rs +++ /dev/null @@ -1,108 +0,0 @@ -//! Startup phrases retained from the approved greeting list (PR #10248). -//! One selection is shared by the provisional, live, and committed session headers. - -use rand::Rng; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) struct Greeting { - pub(crate) phrase: &'static str, -} - -impl Greeting { - pub(super) fn choose() -> Self { - Self { - phrase: GREETINGS[rand::rng().random_range(0..GREETINGS.len())], - } - } -} - -const GREETINGS: &[&str] = &[ - "Whoa, fancy meeting you here!", - "Look who’s at the keyboard.", - "Hello, you. Got an idea?", - "Nice to see you in these parts.", - "Pull up a prompt.", - "What brings you to this corner of the terminal?", - "Hey there. Big plans or little fixes?", - "What are we getting into today?", - "Got something you want to try?", - "What’s today’s little adventure?", - "Anything interesting on the docket?", - "Take your time. The cursor can wait.", - "A half-formed idea will do.", - "Come on in. There’s room for an idea.", - "You, me, and a blinking cursor.", - "It takes two to tango. What’s our first step?", - "You bring the idea. I’ll bring the brackets.", - "Your move, teammate.", - "What are we cooking up?", - "Shall we turn “what if” into something?", - "What’s the first thing on our napkin sketch?", - "Bring your unfinished thoughts.", - "You set the direction. We’ll work out the steps.", - "Ah, the terminal. A classic meeting spot.", - "Nice place you’ve got here. Very monospace.", - "Welcome to our little rectangle of possibility.", - "A cursor blinks. The plot thickens.", - "The prompt is yours.", - "Fancy a little quality terminal time?", - "How’s life between the brackets?", - "Shall we give this cursor a purpose?", - "A fresh prompt. An open question.", - "Your keyboard has entered the chat.", - "Hello, world. Hello, you.", - "Welcome to the blinking edge of possibility.", - "Any loose ends? Semicolons count.", - "Shall we make this terminal earn its keep?", - "Got an itch to fix a thing?", - "Show me the bit that’s being weird.", - "Greetings, fellow tinkerer.", - "Well, well, well. An idea approaches.", - "What’s today’s side quest?", - "Is this a plan day or a poke-around day?", - "Practical, peculiar, or a little of both?", - "Rough sketches welcome.", - "Shall we see where this goes?", - "All right. What have you got?", - "Back for another round?", - "Same terminal, new possibilities.", - "Welcome back. Familiar territory or a fresh adventure?", - "Follow the white cursor.", - "Welcome to the command line, Neo.", - "A glitch in the Matrix, or just a missing semicolon?", - "How deep does this codebase go?", - "Speak, friend, and enter a prompt.", - "One prompt to begin the journey.", - "May the source be with you.", - "These might be the bugs you’re looking for.", - "A new prompt awakens.", - "Forty-two is an answer. What’s the question?", - "It’s dangerous to code alone. Take a prompt.", - "The code must flow.", - "You rang? Metaphorically. You typed.", - "Hello again, carbon-based collaborator.", - "This looks like the start of a perfectly reasonable rabbit hole.", - "Shall we turn “huh?” into “aha!”?", - "Well, this terminal just got interesting.", - "The source is strong with this one.", - "There’s a perfectly good prompt with your name on it.", - "Here for a quick fix or the extended edition?", - "What’s the latest from your side of the keyboard?", - "Got a minute and a mildly unreasonable idea?", - "A hunch is a perfectly respectable starting point.", - "Hello again. What’s the plot this time?", - "Shall we make a little something out of nothing?", - "Welcome to the neighborhood. Lots of characters here.", - "Nice terminal. Does it come in widescreen?", - "Your cursor called. It wants a plot.", - "Shall we put some verbs after that cursor?", - "Welcome. There’s no dress code, just code.", - "This terminal has excellent conversational potential.", - "Got an idea that won’t stay on the napkin?", - "What are we building in this episode?", - "Bring a question. Bonus points if it’s a weird one.", - "What are we poking with a metaphorical stick?", - "Shall we make the thing that makes the other thing easier?", - "Welcome to the part where the idea gets interesting.", - "A long time ago, in a directory not so far away…", -]; diff --git a/codex-rs/tui/src/history_cell/session.rs b/codex-rs/tui/src/history_cell/session.rs index 11cce0ecf1e5..48295d4b85ce 100644 --- a/codex-rs/tui/src/history_cell/session.rs +++ b/codex-rs/tui/src/history_cell/session.rs @@ -1,10 +1,6 @@ //! Session headers, onboarding guidance, and transcript cards. -use std::sync::Arc; -use std::sync::OnceLock; - use super::*; -use crate::empty_state_animation::Greeting; use crate::line_truncation::line_width; use crate::line_truncation::truncate_line_with_ellipsis_if_overflow; use crate::style::accent_color; @@ -110,17 +106,6 @@ impl HistoryCell for SessionNoticeCell { #[derive(Debug)] pub struct SessionInfoCell(CompositeHistoryCell); -/// Bind provisional and configured banners to the thread's chosen greeting. -pub(crate) fn set_session_greeting(cell: &mut dyn HistoryCell, greeting: &Arc>) { - if let Some(header) = cell.as_any_mut().downcast_mut::() { - header.greeting = Arc::clone(greeting); - } else if let Some(info) = cell.as_any_mut().downcast_mut::() { - for part in &mut info.0.parts { - set_session_greeting(part.as_mut(), greeting); - } - } -} - /// Fullscreen transcript presentation omits tips; scrollback retains the original cells. pub(crate) fn fullscreen_session_lines( cell: &dyn HistoryCell, @@ -292,7 +277,6 @@ pub(crate) struct SessionHeaderHistoryCell { reasoning_effort: Option, directory: PathBuf, yolo_mode: bool, - greeting: Arc>, } impl SessionHeaderHistoryCell { @@ -308,7 +292,6 @@ impl SessionHeaderHistoryCell { reasoning_effort, directory, yolo_mode: false, - greeting: Default::default(), } } @@ -371,13 +354,6 @@ impl HistoryCell for SessionHeaderHistoryCell { "YOLO mode".magenta().bold(), ])); } - if let Some(greeting) = self.greeting.get() { - // The tip/help that follows has its own normal composite separator. - lines.extend([ - Line::default(), - Line::from(vec![" ".into(), greeting.phrase.fg(accent_color())]), - ]); - } lines .into_iter() .map(|line| truncate_line_with_ellipsis_if_overflow(line, width)) @@ -385,13 +361,6 @@ impl HistoryCell for SessionHeaderHistoryCell { } fn raw_lines(&self) -> Vec> { - if self.greeting.get().is_some() { - return self - .display_lines(u16::MAX) - .into_iter() - .map(|line| Line::from(line.to_string())) - .collect(); - } let mut lines = vec![ Line::from(format!("OpenAI Codex (v{})", self.version)), Line::from(format!( diff --git a/codex-rs/tui/src/snapshots/codex_tui__startup_draft__layout__tests__owned_startup_layout.snap b/codex-rs/tui/src/snapshots/codex_tui__startup_draft__layout__tests__owned_startup_layout.snap index 957e7539d5fa..6065eb840688 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__startup_draft__layout__tests__owned_startup_layout.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__startup_draft__layout__tests__owned_startup_layout.snap @@ -7,7 +7,7 @@ cursor=Some((13, 12)) >_ OpenAI Codex (v) loading - Pull up a prompt. + diff --git a/codex-rs/tui/src/snapshots/codex_tui__startup_draft__tests__startup_draft_fresh_session_transitions.snap b/codex-rs/tui/src/snapshots/codex_tui__startup_draft__tests__startup_draft_fresh_session_transitions.snap index fb019c0e5810..a01b3b6d9d21 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__startup_draft__tests__startup_draft_fresh_session_transitions.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__startup_draft__tests__startup_draft_fresh_session_transitions.snap @@ -1,6 +1,5 @@ --- source: tui/src/startup_draft_tests.rs -assertion_line: 285 expression: "snapshots.join(\"\\n---\\n\")" --- resume lookup fallback: @@ -8,8 +7,6 @@ before: >_ OpenAI Codex (v) loading - - Pull up a prompt. Resuming session… › draft while loading @@ -20,8 +17,6 @@ after: >_ OpenAI Codex (v) loading - Pull up a prompt. - › draft while loading @@ -32,8 +27,6 @@ before: >_ OpenAI Codex (v) loading - - Pull up a prompt. Forking session… › draft while loading @@ -44,8 +37,6 @@ after: >_ OpenAI Codex (v) loading - Pull up a prompt. - › draft while loading @@ -59,8 +50,6 @@ after: >_ OpenAI Codex (v) loading - Pull up a prompt. - › Ask Codex to do anything @@ -74,8 +63,6 @@ after: >_ OpenAI Codex (v) loading - Pull up a prompt. - › Ask Codex to do anything diff --git a/codex-rs/tui/src/snapshots/codex_tui__startup_draft__tests__startup_draft_full_frames.snap b/codex-rs/tui/src/snapshots/codex_tui__startup_draft__tests__startup_draft_full_frames.snap index 13af775d7e38..fc4ea3030117 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__startup_draft__tests__startup_draft_full_frames.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__startup_draft__tests__startup_draft_full_frames.snap @@ -1,6 +1,5 @@ --- source: tui/src/startup_draft_tests.rs -assertion_line: 154 expression: "snapshots.join(\"\\n---\\n\")" --- empty (48 columns): @@ -8,8 +7,6 @@ empty (48 columns): >_ OpenAI Codex (v) loading - Pull up a prompt. - › Ask Codex to do anything @@ -40,8 +37,6 @@ multiline (48 columns): >_ OpenAI Codex (v) loading - Pull up a prompt. - › first startup line second startup line @@ -53,8 +48,6 @@ narrow (18 columns): >_ OpenAI Codex… loading - Pull up a promp… - › first startup line diff --git a/codex-rs/tui/src/snapshots/codex_tui__startup_draft__tests__startup_draft_onboarding_transition.snap b/codex-rs/tui/src/snapshots/codex_tui__startup_draft__tests__startup_draft_onboarding_transition.snap index 681cfd873b88..74e7b7d1f08c 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__startup_draft__tests__startup_draft_onboarding_transition.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__startup_draft__tests__startup_draft_onboarding_transition.snap @@ -1,6 +1,5 @@ --- source: tui/src/startup_draft_tests.rs -assertion_line: 802 expression: frames --- before onboarding: hidden (0x0 viewport) @@ -10,8 +9,6 @@ after onboarding: >_ OpenAI Codex (v) loading - Pull up a prompt. - › Ask Codex to do anything diff --git a/codex-rs/tui/src/snapshots/codex_tui__startup_draft__tests__terminal_app_ssh_startup.snap b/codex-rs/tui/src/snapshots/codex_tui__startup_draft__tests__terminal_app_ssh_startup.snap index 2ec0e1e64159..ae2fe6147157 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__startup_draft__tests__terminal_app_ssh_startup.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__startup_draft__tests__terminal_app_ssh_startup.snap @@ -1,6 +1,5 @@ --- source: tui/src/startup_draft_tests.rs -assertion_line: 194 expression: "frames.join(\"\\n---\\n\").replace(crate::version::CODEX_CLI_VERSION, \"\")" --- terminal_app_over_ssh=false, owned=true @@ -8,7 +7,7 @@ terminal_app_over_ssh=false, owned=true >_ OpenAI Codex (v) loading - Pull up a prompt. + @@ -26,8 +25,6 @@ terminal_app_over_ssh=true, owned=false >_ OpenAI Codex (v) loading - Pull up a prompt. - › Ask Codex to do anything diff --git a/codex-rs/tui/src/startup_draft.rs b/codex-rs/tui/src/startup_draft.rs index 258e78070c56..7af33c518533 100644 --- a/codex-rs/tui/src/startup_draft.rs +++ b/codex-rs/tui/src/startup_draft.rs @@ -202,8 +202,7 @@ impl StartupDraftPump { ) { blossom.start_fresh(); } - let mut header = startup_session_header(/*config*/ None); - history_cell::set_session_greeting(header.as_mut(), &blossom.greeting); + let header = startup_session_header(/*config*/ None); Self { header, blossom: std::cell::RefCell::new(blossom), @@ -262,7 +261,6 @@ impl StartupDraftPump { local_settings.tui.animations && local_settings.tui.effects.welcome, ); self.header = startup_session_header(Some(config)); - history_cell::set_session_greeting(self.header.as_mut(), &self.blossom.borrow().greeting); self.bottom_pane.set_status_line_enabled( local_settings .tui diff --git a/codex-rs/tui/src/startup_draft_tests.rs b/codex-rs/tui/src/startup_draft_tests.rs index baaa0ebdfeff..db2476edf3dc 100644 --- a/codex-rs/tui/src/startup_draft_tests.rs +++ b/codex-rs/tui/src/startup_draft_tests.rs @@ -29,15 +29,8 @@ where { let (tx, rx) = unbounded_channel(); let mut blossom = crate::empty_state_animation::EmptyStateAnimation::default(); - blossom - .greeting - .set(crate::empty_state_animation::Greeting { - phrase: "Pull up a prompt.", - }) - .expect("initial greeting"); blossom.start_fresh(); - let mut header = startup_session_header(/*config*/ None); - crate::history_cell::set_session_greeting(header.as_mut(), &blossom.greeting); + let header = startup_session_header(/*config*/ None); StartupDraftPump { header, blossom: std::cell::RefCell::new(blossom), @@ -100,13 +93,6 @@ fn startup_draft_renders_full_empty_and_multiline_composer_frames() { ] { let mut pump = startup_test_pump(std::iter::empty()); pump.session_action = session_action; - // Resume and fork have no fresh-thread greeting. Exercise the real initial state. - if matches!( - session_action, - StartupDraftSessionAction::Resume | StartupDraftSessionAction::Fork - ) { - pump.header = startup_session_header(/*config*/ None); - } pump.bottom_pane .set_composer_text(text.to_string(), Vec::new(), Vec::new()); let renderable = @@ -314,15 +300,15 @@ async fn startup_draft_hydrates_its_header_without_moving_the_composer() { startup_draft_renderable(&pump.header, &pump.bottom_pane, pump.session_action) .desired_height(width); - assert_eq!(pump.header.raw_lines()[2].to_string().trim(), "loading"); + assert_eq!(pump.header.raw_lines()[2].to_string(), "directory: loading"); pump.apply_config(&config); let expected_directory = crate::history_cell::SessionHeaderHistoryCell::format_directory_inner( config.cwd.as_path(), /*max_width*/ None, ); assert_eq!( - pump.header.raw_lines()[2].to_string().trim(), - expected_directory + pump.header.raw_lines()[2].to_string(), + format!("directory: {expected_directory}") ); assert_eq!( startup_draft_renderable(&pump.header, &pump.bottom_pane, pump.session_action) From 87d3e068478cb299cb25937e9793b5e5d5a7c668 Mon Sep 17 00:00:00 2001 From: ningyi-oai Date: Tue, 29 Sep 2026 23:02:25 +0000 Subject: [PATCH 12/24] Preserve live tool-call metadata across request windows (#49401) ## Why Persistence and aggregate metadata budgets can omit tool-call observations that are still relevant to the current request, including tool result metadata needed during compaction. ## What changed - Retain live direct-call observations separately from bounded persisted outputs, keyed by response item ID so reused call IDs cannot mix results. - Restore those observations for sampling and compaction, pruning them only when sampling confirms they have left the history window. Preserve observations across shortened compaction retries. - Remove aggregate prompt metadata caps while retaining per-call argument limits and the outgoing message size limit. Invalidate cell completeness when argument normalization truncates a recorded call. ## Testing Add regression coverage for exhausted persistence budgets, reused call IDs and reversed completion order, shortened compaction retries, and completeness after argument truncation. Extend integration coverage for large Code Mode metadata, provider metadata support, and fork/resume behavior. GitOrigin-RevId: 004082b69bff9cc8b05f36d1052057f163c67f07 --- codex-rs/core/src/client_tests.rs | 14 +- codex-rs/core/src/session/mod.rs | 2 +- .../core/src/tools/executed_tool_calls.rs | 17 +- .../executed_tool_calls/request_metadata.rs | 214 ++---- .../request_metadata_tests.rs | 639 +++++------------- .../tools/executed_tool_calls_direct_tests.rs | 114 ++++ codex-rs/core/src/tools/metadata_metrics.rs | 14 - .../core/src/tools/metadata_metrics_tests.rs | 58 -- codex-rs/core/tests/suite/code_mode.rs | 16 +- .../core/tests/suite/direct_tool_metadata.rs | 226 +++++-- codex-rs/protocol/src/models.rs | 3 +- .../src/models/executed_tool_calls.rs | 155 +---- .../src/models/executed_tool_calls_tests.rs | 513 +------------- 13 files changed, 637 insertions(+), 1348 deletions(-) diff --git a/codex-rs/core/src/client_tests.rs b/codex-rs/core/src/client_tests.rs index 07d57f5536d6..1562fb19588e 100644 --- a/codex-rs/core/src/client_tests.rs +++ b/codex-rs/core/src/client_tests.rs @@ -523,10 +523,14 @@ async fn responses_request_includes_internal_metadata_for_provider_grant_or_firs recorder.attach_direct_call_to_output(&mut item, Some((recorded, permit))); outputs.push(item); } - let original_outputs = outputs.clone(); + let persisted_outputs = outputs.clone(); recorder.attach_to_prompt(&mut outputs, &mut Default::default()); - assert_eq!(outputs, original_outputs); - let recorded = serde_json::to_value(&outputs)?; + let restored = serde_json::to_value(&outputs)?; + assert_eq!( + restored[1]["internal_chat_message_metadata_passthrough"]["executed_tool_calls"][0]["tool_result_metadata"], + resource_metadata, + ); + let recorded = serde_json::to_value(&persisted_outputs)?; assert_eq!( recorded[0]["internal_chat_message_metadata_passthrough"]["executed_tool_calls"][0]["tool_result_metadata"], resource_metadata, @@ -537,11 +541,12 @@ async fn responses_request_includes_internal_metadata_for_provider_grant_or_firs .as_str() .is_some_and(|value| value.starts_with("omitted_due_to_size_limit (overage_bytes=")) ); - let omitted_output = outputs.pop().expect("second direct output"); + let omitted_output = persisted_outputs[1].clone(); let mut without_omitted_metadata = ResponseItem::from(ResponseInputItem::FunctionCallOutput { call_id: "second".to_string(), output: FunctionCallOutputPayload::from_text("result for second".to_string()), }); + without_omitted_metadata.set_id(omitted_output.id().cloned()); without_omitted_metadata.append_executed_tool_calls(vec![ExecutedToolCall::new( "mcp__apps__read".to_string(), json!({"query": "second"}), @@ -801,7 +806,6 @@ fn responses_request_preserves_result_metadata_above_previous_aggregate_budget() let metadata_bytes = serde_json::to_vec(&body)?.len() - serde_json::to_vec(&without_metadata)?.len(); assert!(metadata_bytes > 128 * 1024); - assert!(metadata_bytes <= 2 * 1024 * 1024); assert_eq!(serde_json::to_value(&history)?, original_history); Ok(()) } diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 056f9f0ce923..b87697684690 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -3393,7 +3393,7 @@ impl Session { items } - fn assign_missing_response_item_id(item: &mut ResponseItem) { + pub(crate) fn assign_missing_response_item_id(item: &mut ResponseItem) { if item.id().is_some_and(|id| !id.is_empty()) { return; } diff --git a/codex-rs/core/src/tools/executed_tool_calls.rs b/codex-rs/core/src/tools/executed_tool_calls.rs index ee05b483bd41..636d3cbd92ae 100644 --- a/codex-rs/core/src/tools/executed_tool_calls.rs +++ b/codex-rs/core/src/tools/executed_tool_calls.rs @@ -20,19 +20,20 @@ use codex_code_mode::CellId; use codex_features::Feature; use codex_features::Features; use codex_history::InitialHistory; +use codex_protocol::ResponseItemId; use codex_protocol::mcp::McpAttribution; use codex_protocol::mcp::McpAttributionSource; use codex_protocol::models::ExecutedToolCall; use codex_protocol::models::ExecutedToolCallArguments; use codex_protocol::models::ResponseItem; use codex_protocol::models::ToolResultMetadata; -use codex_protocol::models::bound_executed_tool_calls_for_prompt; -use codex_protocol::models::bound_executed_tool_calls_for_prompt_prioritizing_recent; use codex_protocol::models::executed_tool_call_metadata_bytes; +use codex_protocol::models::normalize_executed_tool_call_arguments; use codex_protocol::openai_models::ToolMode; use indexmap::IndexMap; use serde_json::Value as JsonValue; +use crate::session::session::Session; use crate::session::step_context::StepContext; use crate::tools::context::ToolCallSource; use crate::tools::context::ToolOutput; @@ -97,6 +98,8 @@ impl Drop for DirectCallPermit { struct ExecutedToolCallRecorderState { // Disabling drops this lifetime, permanently invalidating prepared Direct records. recording: Arc<()>, + // Full live Direct observations; persisted outputs keep their bounded projection. + direct_calls: HashMap, cells: HashMap, output_cells: HashMap, retained_calls: HashMap<(std::mem::Discriminant, String), RetainedToolCalls>, @@ -399,13 +402,19 @@ impl ExecutedToolCalls { let Some((call, permit)) = prepared else { return; }; - let state = self.lock_state(); - let Some(state) = state.as_ref() else { + let mut state = self.lock_state(); + let Some(state) = state.as_mut() else { return; }; if !permit.recording.ptr_eq(&Arc::downgrade(&state.recording)) { return; } + // Assign the output's ordinary history ID here so reused model call IDs + // cannot associate this observation with a different output. + Session::assign_missing_response_item_id(item); + if let Some(id) = item.id() { + state.direct_calls.insert(id.clone(), call.clone()); + } let complete = matches!(call.arguments(), ExecutedToolCallArguments::Raw(_)); item.append_executed_tool_calls(vec![call]); if complete { diff --git a/codex-rs/core/src/tools/executed_tool_calls/request_metadata.rs b/codex-rs/core/src/tools/executed_tool_calls/request_metadata.rs index 762eccee8923..1ae9eefb166e 100644 --- a/codex-rs/core/src/tools/executed_tool_calls/request_metadata.rs +++ b/codex-rs/core/src/tools/executed_tool_calls/request_metadata.rs @@ -1,59 +1,74 @@ -//! Attaches host observations to request metadata and applies its byte budget. -//! Final request budgeting runs after the recorder lock is released; it never changes tool execution. +//! Attaches host observations to request metadata without changing tool execution. use super::*; #[derive(Clone, Copy, PartialEq, Eq)] -enum MetadataBudgetScope { - AllCalls, - CodeModeOnly, +enum RequestKind { + Sampling, + Compaction, } impl ExecutedToolCalls { - /// Attaches Code Mode observations while preserving Direct records captured in history. pub(crate) fn attach_to_compaction_prompt(&self, items: &mut [ResponseItem]) { - let mut state = self.lock_state(); - let Some(state) = state.as_mut() else { - clear_direct_call_metadata(items); - return; - }; - Self::attach_pending_to_prompt_with_state( - state, - items, - &mut HashMap::new(), - MetadataBudgetScope::CodeModeOnly, - ); + self.attach_to_request(items, &mut HashMap::new(), RequestKind::Compaction); } - /// Attaches trusted completeness; request budgeting only revokes damaged inventories. pub(crate) fn attach_to_prompt( &self, items: &mut [ResponseItem], retry_cache: &mut ExecutedToolCallCache, ) { - let attached = { + self.attach_to_request(items, retry_cache, RequestKind::Sampling); + } + + fn attach_to_request( + &self, + items: &mut [ResponseItem], + retry_cache: &mut ExecutedToolCallCache, + request_kind: RequestKind, + ) { + let recording = { let mut state = self.lock_state(); - state.as_mut().map(|state| { - Self::attach_pending_to_prompt_with_state( - state, - items, - retry_cache, - MetadataBudgetScope::AllCalls, - ) - }) - }; - let Some(attached) = attached else { - // Direct records now live in history; disabling capture also stops replaying them. - clear_direct_call_metadata(items); - return; + let Some(state) = state.as_mut() else { + // Disabling capture also stops replaying Direct records from history. + clear_direct_call_metadata(items); + return; + }; + for item in items.iter_mut() { + if let Some(call) = item.id().and_then(|id| state.direct_calls.get(id)) { + item.clear_executed_tool_calls(); + item.append_executed_tool_calls(vec![call.clone()]); + if matches!(call.arguments(), ExecutedToolCallArguments::Raw(_)) { + item.mark_tool_calls_complete(); + } + } + } + // A shortened compaction attempt is not an installed history window. + // Only ordinary sampling can discard observations absent from the window. + if request_kind == RequestKind::Sampling && !state.direct_calls.is_empty() { + let output_ids = items + .iter() + .filter_map(ResponseItem::id) + .collect::>(); + state.direct_calls.retain(|id, _| output_ids.contains(id)); + } + Self::attach_pending_to_prompt_with_state(state, items, retry_cache, request_kind); + Arc::downgrade(&state.recording) }; - if attached || items.iter().any(has_direct_call_metadata) { - metadata_metrics::bound_prompt_metadata( - items, - bound_executed_tool_calls_for_prompt, - "request", - codex_otel::global().as_ref(), - ); + + // Existing history can contain arguments recorded by an older client. Keep the + // per-call limit, and prevent newly truncated cells from claiming completeness + // on a later wait. Do the serialization outside the recorder lock. + let truncated_origins = normalize_executed_tool_call_arguments(items); + if !truncated_origins.is_empty() { + let mut state = self.lock_state(); + if let Some(state) = state.as_mut() + && recording.ptr_eq(&Arc::downgrade(&state.recording)) + { + for origin in truncated_origins { + state.invalidate_origin(&origin); + } + } } } @@ -61,7 +76,7 @@ impl ExecutedToolCalls { state: &mut ExecutedToolCallRecorderState, items: &mut [ResponseItem], retry_cache: &mut ExecutedToolCallCache, - budget_scope: MetadataBudgetScope, + request_kind: RequestKind, ) -> bool { // Failed wrappers have no callback; only their bounded bitmap observation survives. state.pending_wrapper_origins.clear(); @@ -317,7 +332,7 @@ impl ExecutedToolCalls { } // Compaction may retry with a shortened history without installing that history. // Keep absent observations until a normal sampling request confirms the live window. - if budget_scope == MetadataBudgetScope::AllCalls && !pending_outputs.is_empty() { + if request_kind == RequestKind::Sampling && !pending_outputs.is_empty() { state .retained_calls .retain(|key, _| !pending_outputs.contains(key)); @@ -378,6 +393,7 @@ impl ExecutedToolCalls { .is_some_and(|cell_id| invalid_cells.contains(cell_id)) { retained.complete = false; + retained.call_index_by_id.clear(); retained.clear_late_truncated_metadata(); } } @@ -400,122 +416,6 @@ impl ExecutedToolCalls { } } - let metadata_bytes = items - .iter() - .filter(|item| { - budget_scope == MetadataBudgetScope::AllCalls || !has_direct_call_metadata(item) - }) - .fold(0_usize, |bytes, item| { - bytes.saturating_add(executed_tool_call_metadata_bytes(item)) - }); - if metadata_bytes > MAX_EXECUTED_TOOL_CALL_FULL_ARGUMENT_BYTES_PER_OUTPUT { - match budget_scope { - MetadataBudgetScope::AllCalls => { - metadata_metrics::bound_prompt_metadata( - items, - bound_executed_tool_calls_for_prompt_prioritizing_recent, - "retained", - codex_otel::global().as_ref(), - ); - } - MetadataBudgetScope::CodeModeOnly => { - // Validate bindings against the complete history above, but do not make - // captured Direct records compete with Code Mode's bounded recorder. - let (indices, mut bounded): (Vec<_>, Vec<_>) = items - .iter() - .enumerate() - .filter(|(_, item)| { - !has_direct_call_metadata(item) - && item.executed_tool_call_metadata().is_some() - }) - .map(|(index, item)| (index, item.clone())) - .unzip(); - metadata_metrics::bound_prompt_metadata( - &mut bounded, - bound_executed_tool_calls_for_prompt_prioritizing_recent, - "compaction", - codex_otel::global().as_ref(), - ); - for (index, item) in indices.into_iter().zip(bounded) { - items[index] = item; - } - } - } - let retained_before_bounding = std::mem::take(&mut state.retained_calls); - let mut bounded_outputs = HashSet::new(); - for item in items.iter_mut() { - if has_direct_call_metadata(item) { - continue; - } - let Some(call_id) = output_call_id(item) else { - continue; - }; - let key = (std::mem::discriminant(&*item), call_id.to_string()); - // History already carries unmanaged records; retaining them would append them again. - let Some(previous) = retained_before_bounding.get(&key) else { - continue; - }; - let metadata = item.executed_tool_call_metadata(); - let unique_output = bounded_outputs.insert(key.clone()); - if !unique_output && let Some(retained) = state.retained_calls.get_mut(&key) { - retained.call_index_by_id.clear(); - retained.clear_late_truncated_metadata(); - } - if let Some(runtime_cell_id) = &previous.runtime_cell_id - && metadata - .is_none_or(|metadata| !metadata.has_same_tool_calls(&previous.calls)) - && let Some(cell) = state.cells.get_mut(runtime_cell_id) - { - cell.completion = CellCompletion::Incomplete; - } - if let Some(metadata) = metadata - && (metadata - .executed_tool_calls - .as_ref() - .is_some_and(|calls| !calls.is_empty()) - || metadata.tool_calls_complete.is_some()) - { - // Metadata-only shedding preserves slots; changed calls or duplicate outputs do not. - let same_calls = unique_output && metadata.has_same_tool_calls(&previous.calls); - let call_index_by_id = if same_calls { - previous.call_index_by_id.clone() - } else { - HashMap::new() - }; - let retained = state.retained_calls.entry(key).or_default(); - retained.runtime_cell_id = previous.runtime_cell_id.clone(); - retained.calls = metadata.executed_tool_calls.clone().unwrap_or_default(); - if let Some(cell_id) = metadata.cell_id.as_ref() { - retained.cell_id = Some(cell_id.clone()); - } - retained.complete |= metadata.tool_calls_complete == Some(true); - retained.call_index_by_id = call_index_by_id; - // Budgeting preserves call order; changed inventories lose their indices. - if same_calls { - retained.truncated_call_index_by_id = - previous.truncated_call_index_by_id.clone(); - retained.late_truncated_indices = previous.late_truncated_indices.clone(); - } else { - // The budgeter never reorders calls. Remove only the new late - // evidence before losing the indices that identify it. - for index in &previous.late_truncated_indices { - if let Some(call) = retained.calls.get_mut(*index) { - call.set_tool_result_metadata(ToolResultMetadata::default()); - } - } - } - retained.result_metadata_updated = previous.result_metadata_updated; - } - } - if budget_scope == MetadataBudgetScope::CodeModeOnly { - state.retained_calls.extend( - retained_before_bounding - .into_iter() - .filter(|(key, _)| pending_outputs.contains(key)), - ); - } - } - attached } } diff --git a/codex-rs/core/src/tools/executed_tool_calls/request_metadata_tests.rs b/codex-rs/core/src/tools/executed_tool_calls/request_metadata_tests.rs index c92841f41313..efa05e24e66e 100644 --- a/codex-rs/core/src/tools/executed_tool_calls/request_metadata_tests.rs +++ b/codex-rs/core/src/tools/executed_tool_calls/request_metadata_tests.rs @@ -20,12 +20,7 @@ impl ExecutedToolCalls { let Some(state) = state.as_mut() else { return false; }; - Self::attach_pending_to_prompt_with_state( - state, - items, - retry_cache, - MetadataBudgetScope::AllCalls, - ) + Self::attach_pending_to_prompt_with_state(state, items, retry_cache, RequestKind::Sampling) } } @@ -145,147 +140,133 @@ fn compaction_attaches_pending_and_retained_code_mode_metadata() { } #[test] -fn failed_compaction_preserves_observations_absent_from_trimmed_retries() { - let call_count = 100; - for rebudget in [false, true] { - let recorder = new_recorder(InitialHistory::New); - let mut history = Vec::new(); - for origin in ["old", "recent"] { - let cell = CellId::new(origin.to_string()); - recorder.start_cell(&cell, origin); - for index in 0..call_count { - record_nested_call(&recorder, &cell, &format!("nested-{index}")); - } - recorder.finish_cell_recording(&cell); - history.extend([exec_input(origin), exec_output(origin)]); - } - let mut full_attempt = history.clone(); - recorder.attach_to_compaction_prompt(&mut full_attempt); - assert_eq!(tool_calls_complete(&full_attempt[1]), Some(true)); - - if rebudget { - // Late results make the retained retry item exceed the budget independently of - // the older output that the compaction retry has temporarily removed. - let source = ToolCallSource::CodeMode { - cell_id: "recent".to_string(), - runtime_tool_call_id: "runtime".to_string(), - }; - for index in 0..call_count { - assert!(recorder.record_tool_result_metadata( - &source, - &format!("nested-{index}"), - &json!({"provider": "x".repeat(24 * 1024)}), - )); - } - } - let mut trimmed_attempt = history[2..].to_vec(); - recorder.attach_to_compaction_prompt(&mut trimmed_attempt); - assert_eq!(tool_calls_complete(&trimmed_attempt[1]), Some(true)); - if rebudget { - let encoded = serde_json::to_value(&trimmed_attempt[1]).unwrap(); - assert!( - encoded["internal_chat_message_metadata_passthrough"]["executed_tool_calls"][0]["tool_result_metadata"] - .as_str() - .is_some_and(|marker| marker.starts_with("omitted_due_to_size_limit (overage_bytes=")), - ); - } - - // Exhausted retries leave the session's original history intact. - let mut expected = full_attempt; - expected[2..].clone_from_slice(&trimmed_attempt); - // The restored inventory must fit without causing another round of raw-result shedding. - assert!( - expected - .iter() - .map(executed_tool_call_metadata_bytes) - .sum::() - <= 2 * 1024 * 1024 - ); - let mut resumed = history.clone(); - recorder.attach_to_prompt(&mut resumed, &mut HashMap::new()); - assert_eq!(resumed, expected); - // Once ordinary sampling advances to a smaller window, the old record is pruned. - recorder.attach_to_prompt(&mut history[2..], &mut HashMap::new()); - let mut old = vec![exec_input("old"), exec_output("old")]; - recorder.attach_to_prompt(&mut old, &mut HashMap::new()); - assert_eq!(old, vec![exec_input("old"), exec_output("old")]); - } -} - -#[test] -fn compaction_bounds_code_mode_metadata_without_rebudgeting_direct_history() { +fn large_direct_and_code_mode_metadata_survive_sampling_and_compaction() { let recorder = new_recorder(InitialHistory::New); - let mut history = Vec::new(); - let arguments = json!({"value": "x".repeat(7 * 1024)}); - let argument_bytes = serialized_json_bytes(&arguments).unwrap(); - for index in 0..96 { - let origin = format!("exec-{index}"); - let cell = CellId::new(format!("cell-{index}")); - recorder.start_cell(&cell, &origin); - let mut call = ExecutedToolCall::new("nested_tool".to_string(), arguments.clone()); - call.set_tool_result_metadata(ToolResultMetadata::new(&json!({ - "provider": "x".repeat(24 * 1024), - }))); - recorder.record_nested_tool_call( - cell.clone(), - format!("nested-{index}"), - call, - argument_bytes, - ); - recorder.finish_cell_recording(&cell); - history.extend([exec_input(&origin), exec_output(&origin)]); - } - let direct_start = history.len(); - for index in 0..24 { - let mut item = output(&format!("direct-{index}")); - let mut call = ExecutedToolCall::new("direct_tool".to_string(), arguments.clone()); - call.set_tool_result_metadata(ToolResultMetadata::new(&json!({"id": index}))); - recorder.attach_direct_call_to_output( - &mut item, - Some((call, recorder.reserve_direct_call().unwrap())), - ); - history.push(item); - } - assert!( - history[direct_start..] - .iter() - .map(executed_tool_call_metadata_bytes) - .sum::() - > 128 * 1024 + recorder + .retained_direct_metadata_bytes + .store(MAX_RETAINED_DIRECT_METADATA_BYTES, Ordering::Relaxed); + + let direct_metadata = json!({"openai/resource_access": {"data": "d".repeat(900 * 1024)}}); + let mut direct_call = ExecutedToolCall::new("direct_tool".to_string(), json!({})); + direct_call.set_tool_result_metadata(ToolResultMetadata::new(&direct_metadata)); + let ordinary_direct = output("direct"); + let mut direct = ordinary_direct.clone(); + recorder.attach_direct_call_to_output( + &mut direct, + Some((direct_call, recorder.reserve_direct_call().unwrap())), ); + assert!(direct.executed_tool_call_metadata().is_none()); + + let cell = CellId::new("code-cell".to_string()); + recorder.start_cell(&cell, "exec"); + record_nested_call(&recorder, &cell, "nested"); + let code_metadata = json!({"openai/resource_access": {"data": "c".repeat(1400 * 1024)}}); + let source = ToolCallSource::CodeMode { + cell_id: cell.as_str().to_string(), + runtime_tool_call_id: "nested".to_string(), + }; + assert!(recorder.record_tool_result_metadata(&source, "nested", &code_metadata)); + recorder.finish_cell_recording(&cell); + let ordinary_code = ResponseItem::from(ResponseInputItem::CustomToolCallOutput { + call_id: "exec".to_string(), + name: None, + output: FunctionCallOutputPayload::from_text("ordinary result".to_string()), + }); + let history = vec![direct, exec_input("exec"), ordinary_code.clone()]; - let mut code_mode_only = history[..direct_start].to_vec(); - recorder.attach_to_compaction_prompt(&mut code_mode_only); let mut compact = history.clone(); recorder.attach_to_compaction_prompt(&mut compact); - assert_eq!(&compact[direct_start..], &history[direct_start..]); - assert_eq!(&compact[..direct_start], &code_mode_only); assert!( - compact[..direct_start] + compact .iter() .map(executed_tool_call_metadata_bytes) .sum::() - <= 2 * 1024 * 1024 + > 2 * 1024 * 1024 ); - let mut newest = exec_output("exec-95"); - let mut newest_call = ExecutedToolCall::new("nested_tool".to_string(), arguments); - newest_call.set_tool_result_metadata(ToolResultMetadata::new(&json!({ - "provider": "x".repeat(24 * 1024), - }))); - newest.append_executed_tool_calls(vec![newest_call]); - newest.set_tool_call_cell_id("exec-95"); - newest.mark_tool_calls_complete(); - assert_eq!(compact[direct_start - 1], newest); - let oldest = serde_json::to_value(&compact[1]).unwrap(); - assert!( - oldest["internal_chat_message_metadata_passthrough"]["executed_tool_calls"][0]["tool_result_metadata"] - .as_str() - .is_some_and(|marker| marker.starts_with("omitted_due_to_size_limit (overage_bytes=")), + let direct_wire = serde_json::to_value(&compact[0]).unwrap(); + let code_wire = serde_json::to_value(&compact[2]).unwrap(); + assert_eq!( + direct_wire["output"], + serde_json::to_value(&ordinary_direct).unwrap()["output"] + ); + assert_eq!( + code_wire["output"], + serde_json::to_value(&ordinary_code).unwrap()["output"] + ); + assert_eq!( + direct_wire["internal_chat_message_metadata_passthrough"]["executed_tool_calls"][0]["tool_result_metadata"], + direct_metadata ); - assert_eq!(tool_calls_complete(&compact[1]), Some(true)); - let mut retry = history; - recorder.attach_to_compaction_prompt(&mut retry); - assert_eq!(retry, compact); + assert_eq!( + code_wire["internal_chat_message_metadata_passthrough"]["executed_tool_calls"][0]["tool_result_metadata"], + code_metadata + ); + assert_eq!(tool_calls_complete(&compact[2]), Some(true)); + + let mut sampled = history.clone(); + recorder.attach_to_prompt(&mut sampled, &mut HashMap::new()); + assert_eq!(sampled, compact); + + // A smaller window still has the full Code Mode result and its completion marker. + let mut next = history[1..].to_vec(); + recorder.attach_to_prompt(&mut next, &mut HashMap::new()); + assert_eq!(next[1], compact[2]); +} + +#[test] +fn failed_compaction_preserves_observations_absent_from_trimmed_retries() { + let recorder = new_recorder(InitialHistory::New); + recorder + .retained_direct_metadata_bytes + .store(MAX_RETAINED_DIRECT_METADATA_BYTES, Ordering::Relaxed); + let mut direct = output("direct"); + let mut direct_call = ExecutedToolCall::new("direct_tool".to_string(), json!({})); + direct_call.set_tool_result_metadata(ToolResultMetadata::new(&json!({"resource": "old"}))); + recorder.attach_direct_call_to_output( + &mut direct, + Some((direct_call, recorder.reserve_direct_call().unwrap())), + ); + assert!(direct.executed_tool_call_metadata().is_none()); + let mut history = vec![direct.clone()]; + for origin in ["old", "recent"] { + let cell = CellId::new(origin.to_string()); + recorder.start_cell(&cell, origin); + record_nested_call(&recorder, &cell, "nested"); + recorder.finish_cell_recording(&cell); + history.extend([exec_input(origin), exec_output(origin)]); + } + let mut full_attempt = history.clone(); + recorder.attach_to_compaction_prompt(&mut full_attempt); + assert_eq!(tool_calls_complete(&full_attempt[0]), Some(true)); + assert_eq!(tool_calls_complete(&full_attempt[2]), Some(true)); + + let source = ToolCallSource::CodeMode { + cell_id: "recent".to_string(), + runtime_tool_call_id: "runtime".to_string(), + }; + let large_metadata = json!({"provider": "x".repeat(2100 * 1024)}); + assert!(recorder.record_tool_result_metadata(&source, "nested", &large_metadata)); + let mut trimmed_attempt = history[3..].to_vec(); + recorder.attach_to_compaction_prompt(&mut trimmed_attempt); + assert_eq!(tool_calls_complete(&trimmed_attempt[1]), Some(true)); + let encoded = serde_json::to_value(&trimmed_attempt[1]).unwrap(); + assert_eq!( + encoded["internal_chat_message_metadata_passthrough"]["executed_tool_calls"][0]["tool_result_metadata"], + large_metadata + ); + + // Failed retries do not install the trimmed history or lose older observations. + let mut expected = full_attempt; + expected[3..].clone_from_slice(&trimmed_attempt); + let mut resumed = history.clone(); + recorder.attach_to_prompt(&mut resumed, &mut HashMap::new()); + assert_eq!(resumed, expected); + + // Normal sampling in the smaller window can prune the old observation. + recorder.attach_to_prompt(&mut history[3..], &mut HashMap::new()); + let mut old = vec![direct.clone(), exec_input("old"), exec_output("old")]; + recorder.attach_to_prompt(&mut old, &mut HashMap::new()); + assert_eq!(old, vec![direct, exec_input("old"), exec_output("old")]); } #[test] @@ -323,6 +304,7 @@ fn direct_retained_metadata_budget_sheds_results_then_calls_across_refresh() { Some((call.clone(), recorder.reserve_direct_call().unwrap())), ); let mut expected_first = output("first"); + expected_first.set_id(first.id().cloned()); expected_first.append_executed_tool_calls(vec![call.clone()]); expected_first.mark_tool_calls_complete(); assert_eq!(first, expected_first); @@ -356,6 +338,7 @@ fn direct_retained_metadata_budget_sheds_results_then_calls_across_refresh() { &mut resource_only_output, Some((call.clone(), recorder.reserve_direct_call().unwrap())), ); + expected_resource_only.set_id(resource_only_output.id().cloned()); assert_eq!(resource_only_output, expected_resource_only); assert_eq!( recorder @@ -380,6 +363,7 @@ fn direct_retained_metadata_budget_sheds_results_then_calls_across_refresh() { Some((call.clone(), recorder.reserve_direct_call().unwrap())), ); let mut expected_second = output("second"); + expected_second.set_id(second.id().cloned()); expected_second.append_executed_tool_calls(vec![ExecutedToolCall::new( "test_tool".to_string(), json!({"argument": "kept"}), @@ -704,79 +688,6 @@ fn executed_tool_call_recorder_bounds_pending_calls_and_preserves_overflow() { assert!(!state.cells.contains_key(&cell_id)); } -#[test] -fn executed_tool_call_recorder_bounds_retained_history_and_keeps_latest_calls() { - let recorder = new_recorder(InitialHistory::Forked(Vec::new())); - let mut history = Vec::new(); - let arguments = json!({ "payload": "x".repeat(7 * 1024) }); - let argument_bytes = serialized_json_bytes(&arguments).expect("tool arguments must serialize"); - let cell_count = 100; - let calls_per_cell = 4; - assert!(argument_bytes < MAX_EXECUTED_TOOL_CALL_ARGUMENT_BYTES); - assert!( - argument_bytes * calls_per_cell < MAX_EXECUTED_TOOL_CALL_FULL_ARGUMENT_BYTES_PER_OUTPUT - ); - assert!(argument_bytes * calls_per_cell * cell_count > 2 * 1024 * 1024); - let mut prompt = Vec::new(); - - for index in 0..cell_count { - let call_id = format!("retained-{index}"); - let cell_id = CellId::new(format!("retained-cell-{index}")); - recorder.start_cell(&cell_id, &call_id); - let expected_calls = (0..calls_per_cell) - .map(|nested_index| { - ExecutedToolCall::new( - format!("retained_tool_{index}_{nested_index}"), - arguments.clone(), - ) - }) - .collect::>(); - for (nested_index, call) in expected_calls.iter().enumerate() { - recorder.record_nested_tool_call( - cell_id.clone(), - format!("nested-{index}-{nested_index}"), - call.clone(), - argument_bytes, - ); - } - recorder.finish_cell_recording(&cell_id); - history.extend([exec_input(&call_id), exec_output(&call_id)]); - prompt = history.clone(); - assert!(recorder.attach_pending_to_prompt(&mut prompt, &mut HashMap::new())); - codex_protocol::models::bound_executed_tool_calls_for_prompt(&mut prompt); - let latest_calls = prompt - .last() - .and_then(ResponseItem::executed_tool_call_metadata) - .and_then(|metadata| metadata.executed_tool_calls.as_ref()) - .expect("latest tool calls must remain in retained metadata"); - assert_eq!(latest_calls, &expected_calls); - } - - let state = recorder.lock_state(); - let state = state.as_ref().unwrap(); - let retained_bytes = state - .retained_calls - .values() - .map(|retained| serialized_json_bytes(&retained.calls)) - .sum::>() - .expect("retained calls must serialize"); - assert!(retained_bytes <= 2 * 1024 * 1024); - - let metadata = prompt - .iter() - .filter_map(ResponseItem::executed_tool_call_metadata) - .filter_map(|metadata| metadata.executed_tool_calls.as_ref()) - .flatten() - .map(|call| serde_json::to_value(call).expect("retained call must serialize")) - .collect::>(); - assert!(metadata.len() < cell_count * calls_per_cell); - // Dropped outputs do not add their omission counts to unrelated surviving calls. - assert!(metadata.iter().all(|call| { - let omitted = &call["arguments"]["_codex_executed_tool_call_truncated"]["omitted_calls"]; - omitted.is_null() || *omitted == json!(calls_per_cell - 1) - })); -} - #[test] fn tool_call_completeness_requires_finished_lossless_recording() { for scenario in ["unfinished", "unobserved", "late_call"] { @@ -1047,64 +958,6 @@ fn tool_call_completeness_survives_waits_without_changing_deltas() { } } -#[test] -fn unrelated_raw_overflow_retains_a_complete_exec_within_the_request_budget() { - let request_budget = 2 * 1024 * 1024; - let recorder = new_recorder(InitialHistory::New); - let cell = CellId::new("clean-overflow-cell".to_string()); - recorder.start_cell(&cell, "exec-clean"); - let clean_call = record_nested_call(&recorder, &cell, "nested-clean"); - recorder.finish_cell_recording(&cell); - - let mut unrelated = output("direct-oversized"); - let arguments = json!("x".repeat(7 * 1024)); - assert!(serialized_json_bytes(&arguments).unwrap() <= MAX_EXECUTED_TOOL_CALL_ARGUMENT_BYTES); - unrelated.append_executed_tool_calls( - (0..request_budget / (7 * 1024) + 1) - .map(|index| ExecutedToolCall::new(format!("direct-{index}"), arguments.clone())) - .collect(), - ); - let original = [ - unrelated, - exec_input("exec-clean"), - exec_output("exec-clean"), - ]; - let mut prompt = original.clone(); - assert!( - prompt - .iter() - .map(executed_tool_call_metadata_bytes) - .sum::() - > request_budget - ); - recorder.attach_to_prompt(&mut prompt, &mut HashMap::new()); - let metadata = prompt[2] - .executed_tool_call_metadata() - .expect("the later clean output must retain its calls"); - assert_eq!(metadata.executed_tool_calls, Some(vec![clean_call])); - assert_eq!(metadata.tool_calls_complete, Some(true)); - assert!( - prompt[0] - .executed_tool_call_metadata() - .and_then(|metadata| metadata.executed_tool_calls.as_ref()) - .is_some_and(|calls| calls.iter().any(|call| matches!( - call.arguments(), - ExecutedToolCallArguments::Truncated { .. } - ))) - ); - assert!( - prompt - .iter() - .map(executed_tool_call_metadata_bytes) - .sum::() - <= request_budget - ); - - let mut retry = original; - recorder.attach_to_prompt(&mut retry, &mut HashMap::new()); - assert_eq!(retry, prompt); -} - #[test] fn paginated_history_allows_fresh_origins_but_rejects_known_ids() { for origin in ["fresh-exec", "old-exec"] { @@ -1298,72 +1151,46 @@ fn completeness_rejects_direct_id_collision_and_late_calls() { } #[test] -fn request_truncation_prevents_completion_after_compaction() { +fn newly_normalized_arguments_keep_later_wait_incomplete() { let recorder = new_recorder(InitialHistory::New); - let cell_id = CellId::new("compacted-cell".to_string()); - recorder.start_cell(&cell_id, "exec"); - + let cell = CellId::new("active-cell".to_string()); + recorder.start_cell(&cell, "exec"); let arguments = json!({ - "payload": "x".repeat(MAX_EXECUTED_TOOL_CALL_ARGUMENT_BYTES - r#"{"payload":""}"#.len()), + "_codex_executed_tool_call_truncated": true, + "padding": "x".repeat(8_120), + }) + .to_string(); + let (call, original_bytes) = recorded_call(&ToolCall { + tool_name: codex_tools::ToolName::plain("nested_tool"), + call_id: "nested".to_string(), + payload: ToolPayload::Function { arguments }, + encrypted_function_args: None, }); - for index in 0..4 { - recorder.record_nested_tool_call( - cell_id.clone(), - format!("nested-{index}"), - ExecutedToolCall::new("nested_tool".to_string(), arguments.clone()), - MAX_EXECUTED_TOOL_CALL_ARGUMENT_BYTES, - ); - } - - let mut initial = vec![exec_input("exec"), exec_output("exec")]; - for index in 0..63 { - let origin = format!("other-{index}"); - let other_cell = CellId::new(origin.clone()); - recorder.start_cell(&other_cell, &origin); - for nested in 0..4 { - recorder.record_nested_tool_call( - other_cell.clone(), - format!("other-{index}-{nested}"), - ExecutedToolCall::new("nested_tool".to_string(), arguments.clone()), - MAX_EXECUTED_TOOL_CALL_ARGUMENT_BYTES, - ); - } - recorder.finish_cell_recording(&other_cell); - initial.extend([exec_input(&origin), exec_output(&origin)]); - } - assert!(recorder.attach_pending_to_prompt(&mut initial, &mut HashMap::new())); + assert!(original_bytes <= MAX_EXECUTED_TOOL_CALL_ARGUMENT_BYTES); + assert!( + serialized_json_bytes(call.arguments()).unwrap() > MAX_EXECUTED_TOOL_CALL_ARGUMENT_BYTES + ); + recorder.record_nested_tool_call(cell.clone(), "nested".to_string(), call, original_bytes); + let mut initial = [exec_input("exec"), exec_output("exec")]; + recorder.attach_to_prompt(&mut initial, &mut HashMap::new()); assert!( initial[1] .executed_tool_call_metadata() - .and_then(|metadata| metadata.executed_tool_calls.as_ref()) - .is_some_and(|calls| calls.iter().any(|call| matches!( + .unwrap() + .executed_tool_calls + .as_ref() + .unwrap() + .iter() + .any(|call| matches!( call.arguments(), ExecutedToolCallArguments::Truncated { .. } - ))) - ); - let source = ToolCallSource::CodeMode { - cell_id: cell_id.as_str().to_string(), - runtime_tool_call_id: "runtime-call".to_string(), - }; - for index in 0..4 { - assert!(!recorder.record_tool_result_metadata( - &source, - &format!("nested-{index}"), - &json!({}), - )); - } - - recorder.attach_pending_to_prompt(&mut [], &mut HashMap::new()); - recorder.register_cell(&cell_id, "wait"); - recorder.finish_cell_recording(&cell_id); - let mut final_output = [wait_input("wait", &cell_id), output("wait")]; - recorder.attach_pending_to_prompt(&mut final_output, &mut HashMap::new()); - assert_eq!( - final_output[1] - .executed_tool_call_metadata() - .and_then(|metadata| metadata.tool_calls_complete), - None, + )) ); + recorder.register_cell(&cell, "wait"); + recorder.finish_cell_recording(&cell); + let mut wait = [wait_input("wait", &cell), output("wait")]; + recorder.attach_to_prompt(&mut wait, &mut HashMap::new()); + assert_eq!(tool_calls_complete(&wait[1]), None); } #[test] @@ -1720,7 +1547,31 @@ fn late_truncated_metadata_is_not_attached_to_ambiguous_outputs() { } #[test] -fn late_truncated_metadata_obeys_the_existing_request_budget() { +fn late_raw_metadata_is_not_attached_after_duplicate_outputs() { + let recorder = new_recorder(InitialHistory::New); + let cell = CellId::new("ambiguous-cell".to_string()); + recorder.start_cell(&cell, "exec"); + record_nested_call(&recorder, &cell, "nested"); + let mut duplicate = [exec_input("exec"), exec_output("exec"), exec_output("exec")]; + recorder.attach_to_prompt(&mut duplicate, &mut HashMap::new()); + + let source = ToolCallSource::CodeMode { + cell_id: cell.as_str().to_string(), + runtime_tool_call_id: "nested".to_string(), + }; + assert!(!recorder.record_tool_result_metadata( + &source, + "nested", + &json!({"openai/resource_access": {"resources": ["result"]}}), + )); + let mut retry = [exec_input("exec"), exec_output("exec")]; + recorder.attach_to_prompt(&mut retry, &mut HashMap::new()); + assert!(!retry[1].has_tool_result_metadata()); + assert_eq!(tool_calls_complete(&retry[1]), None); +} + +#[test] +fn late_truncated_metadata_preserves_large_result_on_retry() { let recorder = new_recorder(InitialHistory::New); let cell = CellId::new("late-truncated-cell".to_string()); recorder.start_cell(&cell, "exec"); @@ -1744,7 +1595,7 @@ fn late_truncated_metadata_obeys_the_existing_request_budget() { let actual = serde_json::to_value(&retry[1]).unwrap(); assert_eq!( actual["internal_chat_message_metadata_passthrough"]["executed_tool_calls"][0]["tool_result_metadata"], - json!({"openai/resource_access": {"resources": ["result"]}}), + metadata, ); } } @@ -1804,142 +1655,6 @@ fn late_truncated_metadata_is_not_reused_after_budget_changes_calls() { ); } -#[test] -fn untracked_history_budget_loss_keeps_later_wait_incomplete() { - let recorder = new_recorder(InitialHistory::New); - let cell = CellId::new("running-cell".to_string()); - recorder.start_cell(&cell, "exec"); - recorder.record_nested_tool_call( - cell.clone(), - "nested".to_string(), - ExecutedToolCall::new("nested_tool".to_string(), json!("x".repeat(8190))), - /*original_bytes*/ 8192, - ); - let mut prompt = vec![exec_input("exec"), exec_output("exec")]; - let untracked_count = 256; - let per_output_budget = 2 * 1024 * 1024 / untracked_count; - let mut empty_output = output("untracked"); - empty_output.append_executed_tool_calls(vec![ExecutedToolCall::new( - "other_tool".to_string(), - json!(""), - )]); - // Fill the budget with intact unmanaged outputs, leaving no room for the tracked record. - let payload_bytes = per_output_budget - executed_tool_call_metadata_bytes(&empty_output); - for index in 0..untracked_count { - let mut item = output(&format!("untracked-{index}")); - item.append_executed_tool_calls(vec![ExecutedToolCall::new( - "other_tool".to_string(), - json!("x".repeat(payload_bytes)), - )]); - assert_eq!(executed_tool_call_metadata_bytes(&item), per_output_budget); - prompt.push(item); - } - recorder.attach_to_prompt(&mut prompt, &mut HashMap::new()); - assert!(prompt[1].executed_tool_call_metadata().is_none()); - - recorder.attach_to_prompt(&mut [], &mut HashMap::new()); - recorder.register_cell(&cell, "wait"); - recorder.finish_cell_recording(&cell); - let mut wait = [wait_input("wait", &cell), output("wait")]; - recorder.attach_to_prompt(&mut wait, &mut HashMap::new()); - assert_eq!(tool_calls_complete(&wait[1]), None); -} - -#[test] -fn result_metadata_shedding_preserves_completion_after_compaction() { - let recorder = new_recorder(InitialHistory::New); - let cell_id = CellId::new("compacted-cell".to_string()); - recorder.start_cell(&cell_id, "exec"); - - let call_count = 79; - let argument_bytes = MAX_EXECUTED_TOOL_CALL_FULL_ARGUMENT_BYTES_PER_OUTPUT / call_count - 64; - let arguments = json!({ - "payload": "x".repeat(argument_bytes - r#"{"payload":""}"#.len()), - }); - for index in 0..call_count { - recorder.record_nested_tool_call( - cell_id.clone(), - format!("nested-{index}"), - ExecutedToolCall::new("nested_tool".to_string(), arguments.clone()), - argument_bytes, - ); - } - let source = ToolCallSource::CodeMode { - cell_id: cell_id.as_str().to_string(), - runtime_tool_call_id: "runtime-call".to_string(), - }; - let mut retry_cache = HashMap::new(); - recorder.attach_pending_to_prompt( - &mut [exec_input("exec"), exec_output("exec")], - &mut retry_cache, - ); - // Each snapshot fits alone; together they push the calls over the aggregate budget. - let large_metadata = json!({ "provider_data": "x".repeat(26 * 1024) }); - for index in (0..call_count).filter(|index| *index != 1) { - assert!(recorder.record_tool_result_metadata( - &source, - &format!("nested-{index}"), - &large_metadata, - )); - } - - assert!(recorder.record_tool_result_metadata( - &source, - "nested-1", - &json!({ "status": "unavailable" }) - )); - let mut initial = [exec_output("exec")]; - assert!(recorder.attach_pending_to_prompt(&mut initial, &mut retry_cache)); - let calls = initial[0] - .executed_tool_call_metadata() - .and_then(|metadata| metadata.executed_tool_calls.as_ref()) - .expect("metadata shedding must preserve recorded calls"); - let mut expected_calls = - vec![ExecutedToolCall::new("nested_tool".to_string(), arguments); call_count]; - for call in &mut expected_calls { - call.set_tool_result_metadata(ToolResultMetadata::new(&large_metadata)); - } - expected_calls[1] - .set_tool_result_metadata(ToolResultMetadata::new(&json!({ "status": "unavailable" }))); - let mut unbounded = exec_output("exec"); - unbounded.append_executed_tool_calls(expected_calls.clone()); - unbounded.set_tool_call_cell_id("exec"); - let overage = executed_tool_call_metadata_bytes(&unbounded) - 2 * 1024 * 1024; - expected_calls[0].set_tool_result_metadata(ToolResultMetadata::new(&json!(format!( - "omitted_due_to_size_limit (overage_bytes={overage})" - )))); - assert_eq!(calls, &expected_calls); - let mut retry = [exec_output("exec")]; - assert!(recorder.attach_pending_to_prompt(&mut retry, &mut retry_cache)); - assert_eq!(retry, initial); - - // A different call can still attach a small result after the oversized metadata is shed. - let metadata = json!({ "id": "R2" }); - assert!(recorder.record_tool_result_metadata(&source, "nested-2", &metadata)); - let mut expected_calls = calls.clone(); - expected_calls[2].set_tool_result_metadata(ToolResultMetadata::new(&metadata)); - let mut expected = exec_output("exec"); - expected.append_executed_tool_calls(expected_calls); - expected.set_tool_call_cell_id("exec"); - let mut retry = [exec_output("exec")]; - assert!(recorder.attach_pending_to_prompt(&mut retry, &mut retry_cache)); - assert_eq!(retry, [expected]); - - recorder.attach_pending_to_prompt(&mut [], &mut retry_cache); - assert!(retry_cache.is_empty()); - assert!(!recorder.record_tool_result_metadata(&source, "nested-2", &metadata)); - recorder.register_cell(&cell_id, "wait"); - recorder.finish_cell_recording(&cell_id); - let mut final_output = [wait_input("wait", &cell_id), output("wait")]; - recorder.attach_pending_to_prompt(&mut final_output, &mut HashMap::new()); - assert_eq!( - final_output[1] - .executed_tool_call_metadata() - .and_then(|metadata| metadata.tool_calls_complete), - Some(true), - ); -} - #[test] fn mapping_pressure_preserves_existing_cells_and_late_partial_records() { let recorder = new_recorder(InitialHistory::New); diff --git a/codex-rs/core/src/tools/executed_tool_calls_direct_tests.rs b/codex-rs/core/src/tools/executed_tool_calls_direct_tests.rs index 4d0f1ea7f193..23f2033d252d 100644 --- a/codex-rs/core/src/tools/executed_tool_calls_direct_tests.rs +++ b/codex-rs/core/src/tools/executed_tool_calls_direct_tests.rs @@ -1,6 +1,7 @@ //! Tests Direct metadata admission and permit lifetimes. //! Metadata limits must leave ordinary tool outputs unchanged. +use codex_protocol::ResponseItemId; use codex_protocol::models::FunctionCallOutputPayload; use codex_protocol::models::ResponseInputItem; use pretty_assertions::assert_eq; @@ -15,6 +16,118 @@ fn output(call_id: &str) -> ResponseItem { }) } +#[test] +fn direct_request_uses_live_window_after_persisted_budget_is_exhausted() { + let mut features = Features::default(); + features.enable(Feature::ExecutedToolCallMetadata); + let recorder = ExecutedToolCalls::new(&features, &InitialHistory::New); + recorder + .retained_direct_metadata_bytes + .store(MAX_RETAINED_DIRECT_METADATA_BYTES, Ordering::Relaxed); + let metadata = json!({"openai/resource_access": {"connector": "example"}}); + let make_output = |id: &str| { + let mut call = ExecutedToolCall::new("connector_tool".to_string(), json!({})); + call.set_tool_result_metadata(ToolResultMetadata::new(&metadata)); + let mut item = output(id); + recorder.attach_direct_call_to_output( + &mut item, + Some((call, recorder.reserve_direct_call().unwrap())), + ); + item + }; + let old = make_output("old-window"); + let mut first = vec![old.clone()]; + recorder.attach_to_prompt(&mut first, &mut HashMap::new()); + let current = make_output("new-window"); + assert!(current.executed_tool_call_metadata().is_none()); + let serialized = serde_json::to_string(&codex_history::RolloutItem::ResponseItem( + current.clone().into(), + )) + .unwrap(); + assert!(!serialized.contains("example")); + + // The input to the next window no longer contains the old output. + let mut next = vec![current.clone()]; + recorder.attach_to_prompt(&mut next, &mut HashMap::new()); + for item in [&first[0], &next[0]] { + let encoded = serde_json::to_value(item).unwrap(); + assert_eq!(encoded["output"], "tool result"); + assert_eq!( + encoded["internal_chat_message_metadata_passthrough"]["executed_tool_calls"][0]["tool_result_metadata"], + metadata, + ); + } + let mut pruned = vec![old]; + recorder.attach_to_compaction_prompt(&mut pruned); + assert!(pruned[0].executed_tool_call_metadata().is_none()); + + // Capture toggles must not revive a previous recorder's live-only metadata. + recorder.refresh(&Features::default()); + recorder.refresh(&features); + let mut after_refresh = vec![current]; + recorder.attach_to_prompt(&mut after_refresh, &mut HashMap::new()); + assert!(after_refresh[0].executed_tool_call_metadata().is_none()); +} + +#[test] +fn direct_metadata_follows_output_ids_with_reused_call_ids_and_reverse_completion() { + for supplied_ids in [false, true] { + let mut features = Features::default(); + features.enable(Feature::ExecutedToolCallMetadata); + let recorder = ExecutedToolCalls::new(&features, &InitialHistory::New); + recorder + .retained_direct_metadata_bytes + .store(MAX_RETAINED_DIRECT_METADATA_BYTES, Ordering::Relaxed); + let metadata = [json!({"result": "first"}), json!({"result": "second"})]; + let mut prepared = metadata + .iter() + .map(|metadata| { + let mut call = ExecutedToolCall::new("connector_tool".to_string(), json!({})); + call.set_tool_result_metadata(ToolResultMetadata::new(metadata)); + Some((call, recorder.reserve_direct_call().unwrap())) + }) + .collect::>(); + let mut outputs = [output("reused"), output("reused")]; + let expected_ids = [ + ResponseItemId::with_suffix("fco", "first"), + ResponseItemId::with_suffix("fco", "second"), + ]; + if supplied_ids { + for (item, id) in outputs.iter_mut().zip(&expected_ids) { + item.set_id(Some(id.clone())); + } + } + // Completion order and prompt order must not choose which observation belongs here. + for index in [1, 0] { + recorder.attach_direct_call_to_output(&mut outputs[index], prepared[index].take()); + assert!(outputs[index].executed_tool_call_metadata().is_none()); + if supplied_ids { + assert_eq!(outputs[index].id(), Some(&expected_ids[index])); + } + } + let ids = outputs.clone().map(|item| item.id().unwrap().clone()); + assert_ne!(ids[0], ids[1]); + let mut request = outputs; + for reversed in [false, true] { + if reversed { + request.reverse(); + } + recorder.attach_to_prompt(&mut request, &mut HashMap::new()); + for item in &request { + let index = usize::from(item.id() == Some(&ids[1])); + let encoded = serde_json::to_value(item).unwrap(); + assert_eq!(encoded["call_id"], "reused"); + assert_eq!(encoded["output"], "tool result"); + assert_eq!( + encoded["internal_chat_message_metadata_passthrough"]["executed_tool_calls"][0] + ["tool_result_metadata"], + metadata[index], + ); + } + } + } +} + #[test] fn direct_budget_keeps_an_omission_marker_when_it_fits() { for metadata in [ @@ -50,6 +163,7 @@ fn direct_budget_keeps_an_omission_marker_when_it_fits() { Some((call, recorder.reserve_direct_call().unwrap())), ); + expected.set_id(item.id().cloned()); assert_eq!(item, expected); assert_eq!( recorder diff --git a/codex-rs/core/src/tools/metadata_metrics.rs b/codex-rs/core/src/tools/metadata_metrics.rs index f4fee32b1fca..caae89dacfc9 100644 --- a/codex-rs/core/src/tools/metadata_metrics.rs +++ b/codex-rs/core/src/tools/metadata_metrics.rs @@ -11,20 +11,6 @@ const BYTE_BUCKETS: &[f64] = &[ 4194304.0, 8388608.0, 16777216.0, ]; -pub(super) fn bound_prompt_metadata( - items: &mut [ResponseItem], - bound: fn(&mut [ResponseItem]), - stage: &'static str, - metrics: Option<&MetricsClient>, -) { - let before = metrics.map(|_| metadata_bytes(items)); - bound(items); - if let Some(before) = before { - // Measure only the budget operation: attaching observations can itself add bytes. - record_shedding(stage, before, metadata_bytes(items), metrics); - } -} - pub(crate) fn metadata_bytes(items: &[ResponseItem]) -> usize { items.iter().fold(0_usize, |bytes, item| { bytes.saturating_add(executed_tool_call_metadata_bytes(item)) diff --git a/codex-rs/core/src/tools/metadata_metrics_tests.rs b/codex-rs/core/src/tools/metadata_metrics_tests.rs index 253f2d16fb9c..5d9a1b6cc968 100644 --- a/codex-rs/core/src/tools/metadata_metrics_tests.rs +++ b/codex-rs/core/src/tools/metadata_metrics_tests.rs @@ -1,17 +1,10 @@ use super::*; use codex_otel::MetricsConfig; -use codex_protocol::models::ExecutedToolCall; -use codex_protocol::models::FunctionCallOutputPayload; -use codex_protocol::models::ResponseInputItem; -use codex_protocol::models::ToolResultMetadata; -use codex_protocol::models::bound_executed_tool_calls_for_prompt; -use codex_protocol::models::bound_executed_tool_calls_for_prompt_prioritizing_recent; use opentelemetry_sdk::metrics::InMemoryMetricExporter; use opentelemetry_sdk::metrics::data::AggregatedMetrics; use opentelemetry_sdk::metrics::data::MetricData; use opentelemetry_sdk::metrics::data::ScopeMetrics; use pretty_assertions::assert_eq; -use serde_json::json; use std::collections::BTreeMap; fn metrics() -> MetricsClient { @@ -48,57 +41,6 @@ fn samples(metrics: &MetricsClient) -> BTreeMap { samples } -#[test] -fn prompt_reports_each_budget_application_without_changing_output() { - let metrics = metrics(); - let original = (0..96) - .map(|index| { - let mut item = ResponseItem::from(ResponseInputItem::FunctionCallOutput { - call_id: format!("call_{index}"), - output: FunctionCallOutputPayload::from_text("unchanged output".to_string()), - }); - let mut call = ExecutedToolCall::new("mcp__apps__read".to_string(), json!({})); - call.set_tool_result_metadata(ToolResultMetadata::new(&json!({ - "openai/resource_access": {"resources": ["x".repeat(24_000)]}, - }))); - item.append_executed_tool_calls(vec![call]); - item.mark_tool_calls_complete(); - item - }) - .collect::>(); - let mut expected_samples = BTreeMap::new(); - for (stage, bound) in [ - ( - "request", - bound_executed_tool_calls_for_prompt as fn(&mut [ResponseItem]), - ), - ( - "retained", - bound_executed_tool_calls_for_prompt_prioritizing_recent, - ), - ( - "compaction", - bound_executed_tool_calls_for_prompt_prioritizing_recent, - ), - ] { - let mut expected = original.clone(); - bound(&mut expected); - let shed = metadata_bytes(&original) - metadata_bytes(&expected); - assert!(shed > 0); - let mut actual = original.clone(); - bound_prompt_metadata(&mut actual, bound, stage, Some(&metrics)); - assert_eq!(actual, expected); - // Reapplying an already-satisfied budget must not add a zero-byte sample. - bound_prompt_metadata(&mut actual, bound, stage, Some(&metrics)); - assert_eq!(actual, expected); - let mut without_metrics = original.clone(); - bound_prompt_metadata(&mut without_metrics, bound, stage, /*metrics*/ None); - assert_eq!(without_metrics, expected); - expected_samples.insert(stage.to_string(), (1, shed as f64)); - } - assert_eq!(samples(&metrics), expected_samples); -} - #[test] fn direct_retention_reports_only_positive_known_shedding() { let metrics = metrics(); diff --git a/codex-rs/core/tests/suite/code_mode.rs b/codex-rs/core/tests/suite/code_mode.rs index 4094ed06f3f3..eb0802902a20 100644 --- a/codex-rs/core/tests/suite/code_mode.rs +++ b/codex-rs/core/tests/suite/code_mode.rs @@ -2330,11 +2330,11 @@ async fn result_metadata_preserves_results_within_request_budget( "payload": "l".repeat(31 * 1024), "openai/resource_access": resource_access, }), - serde_json::json!({ "payload": "m".repeat(20 * 1024) }), - serde_json::json!({ "payload": "m".repeat(20 * 1024) }), - serde_json::json!({ "payload": "m".repeat(20 * 1024) }), - serde_json::json!({ "payload": "m".repeat(20 * 1024) }), - serde_json::json!({ "payload": "m".repeat(20 * 1024) }), + serde_json::json!({ "payload": "m".repeat(450 * 1024) }), + serde_json::json!({ "payload": "m".repeat(450 * 1024) }), + serde_json::json!({ "payload": "m".repeat(450 * 1024) }), + serde_json::json!({ "payload": "m".repeat(450 * 1024) }), + serde_json::json!({ "payload": "m".repeat(450 * 1024) }), serde_json::json!({ "payload": "o".repeat(40 * 1024), "openai/resource_access": { @@ -2349,8 +2349,8 @@ async fn result_metadata_preserves_results_within_request_budget( .iter() .map(|metadata| serde_json::to_vec(metadata).unwrap().len()) .collect::>(); - assert!(metadata_sizes.iter().sum::() > 128 * 1024); - assert!(metadata_sizes.iter().sum::() < 1024 * 1024); + assert!(metadata_sizes.iter().sum::() > 2 * 1024 * 1024); + assert!(metadata_sizes.iter().sum::() < 15 * 1024 * 1024); assert!(serde_json::to_vec(&result_metadata[6]["openai/resource_access"])?.len() > 32 * 1024); for (arguments, metadata) in arguments.iter().zip(&result_metadata) { let result = serde_json::json!({ @@ -2488,7 +2488,7 @@ async fn result_metadata_preserves_results_within_request_budget( .iter() .map(codex_protocol::models::executed_tool_call_metadata_bytes) .sum::() - <= 2 * 1024 * 1024 + > 2 * 1024 * 1024 ); let captured = serde_json::to_value(captured)?; for (input, expected_metadata) in [ diff --git a/codex-rs/core/tests/suite/direct_tool_metadata.rs b/codex-rs/core/tests/suite/direct_tool_metadata.rs index 65c03e3b023f..22ab2c4a2597 100644 --- a/codex-rs/core/tests/suite/direct_tool_metadata.rs +++ b/codex-rs/core/tests/suite/direct_tool_metadata.rs @@ -1,6 +1,9 @@ //! Direct-call metadata coverage, including malformed calls and metadata budgets. use anyhow::Result; +use codex_core::ForkSnapshot; +use codex_core::StartThreadOptions; +use codex_core::TurnInputRequest; use codex_features::Feature; use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; use codex_model_provider::RemoteCompactionSupport; @@ -23,6 +26,7 @@ use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; +use codex_protocol::user_input::UserInput; use core_test_support::apps_test_server::configure_search_capable_model; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -495,7 +499,6 @@ async fn direct_function_and_tool_search_mark_complete_attempts( ) -> Result<()> { skip_if_no_network!(Ok(())); - let request_budget = 2 * 1024 * 1024; let server = start_mock_server().await; let mut builder = test_codex().with_config(move |config| { configure_search_capable_model(config); @@ -620,25 +623,27 @@ async fn direct_function_and_tool_search_mark_complete_attempts( }) ); } - assert!(metadata_bytes <= request_budget); if budget_calls > 0 { assert!(metadata_bytes > 128 * 1024); } Ok(()) } +#[test_case(false; "ungranted_provider")] +#[test_case(true; "granted_provider")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn direct_metadata_limit_retains_marker_without_sending_it_to_custom_provider() -> Result<()> -{ +async fn direct_metadata_limit_respects_provider_support( + include_internal_metadata: bool, +) -> Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await; let apps = AppsTestServer::mount(&server).await?; - let raw_metadata = json!({"openai/resource_access": {"payload": "x".repeat(600 * 1024)}}); - let raw_bytes = serde_json::to_vec(&raw_metadata)?.len(); + fn metadata_for_query(query: &str) -> Value { + json!({"openai/resource_access": {"payload": "x".repeat(600 * 1024), "query": query}}) + } + let raw_bytes = serde_json::to_vec(&metadata_for_query("first"))?.len(); assert!(raw_bytes < 1024 * 1024); assert!(raw_bytes * 2 > 1024 * 1024); - assert!(raw_bytes * 2 < 2 * 1024 * 1024); - let fixture_metadata = raw_metadata.clone(); Mock::given(method("POST")) .and(path_regex("^/api/codex/ps/mcp/?$")) .and(body_partial_json(json!({"method": "tools/call"}))) @@ -651,7 +656,7 @@ async fn direct_metadata_limit_retains_marker_without_sending_it_to_custom_provi "jsonrpc": "2.0", "id": body["id"], "result": { "content": [{"type": "text", "text": format!("result for {query}")}], - "_meta": fixture_metadata, + "_meta": metadata_for_query(query), "isError": false } })) @@ -664,35 +669,56 @@ async fn direct_metadata_limit_retains_marker_without_sending_it_to_custom_provi .respond_with(ResponseTemplate::new(/*s*/ 200)) .mount(&server) .await; - let responses = mount_sse_sequence( - &server, - vec![ - sse(vec![ - ev_response_created("resp-1"), - ev_function_call_with_namespace( - "first", - SEARCH_CALENDAR_NAMESPACE, - SEARCH_CALENDAR_LIST_TOOL, - r#"{"query":"first"}"#, - ), - ev_completed("resp-1"), - ]), - sse(vec![ - ev_response_created("resp-2"), + let mut events = vec![ + sse(vec![ + ev_response_created("resp-1"), + ev_function_call_with_namespace( + "first", + SEARCH_CALENDAR_NAMESPACE, + SEARCH_CALENDAR_LIST_TOOL, + r#"{"query":"first"}"#, + ), + ev_completed("resp-1"), + ]), + sse(vec![ + ev_response_created("resp-2"), + ev_function_call_with_namespace( + "second", + SEARCH_CALENDAR_NAMESPACE, + SEARCH_CALENDAR_LIST_TOOL, + r#"{"query":"second"}"#, + ), + ev_completed("resp-2"), + ]), + sse(vec![ev_response_created("resp-3"), ev_completed("resp-3")]), + ]; + if include_internal_metadata { + for query in ["fork", "resume"] { + events.push(sse(vec![ + ev_response_created(&format!("resp-{query}")), ev_function_call_with_namespace( - "second", + query, SEARCH_CALENDAR_NAMESPACE, SEARCH_CALENDAR_LIST_TOOL, - r#"{"query":"second"}"#, + &json!({"query": query}).to_string(), ), - ev_completed("resp-2"), - ]), - sse(vec![ev_response_created("resp-3"), ev_completed("resp-3")]), - ], - ) - .await; - let mut builder = search_capable_apps_builder(apps.chatgpt_base_url).with_config(|config| { - config.model_provider.include_internal_metadata = false; + ev_completed(&format!("resp-{query}")), + ])); + events.push(sse(vec![ + ev_response_created(&format!("resp-{query}-done")), + ev_completed(&format!("resp-{query}-done")), + ])); + } + } + let responses = mount_sse_sequence(&server, events).await; + let mock_url = server.uri(); + let configure_metadata = move |config: &mut codex_core::config::Config| { + assert_eq!(config.chatgpt_base_url, mock_url); + assert_eq!( + config.model_provider.base_url.as_deref(), + Some(format!("{mock_url}/v1").as_str()) + ); + config.model_provider.include_internal_metadata = include_internal_metadata; config.analytics_enabled = Some(true); config .features @@ -706,7 +732,9 @@ async fn direct_metadata_limit_retains_marker_without_sending_it_to_custom_provi .features .disable(Feature::CodeModeOnly) .expect("disable code-mode-only tools"); - }); + }; + let mut builder = search_capable_apps_builder(apps.chatgpt_base_url.clone()) + .with_config(configure_metadata.clone()); // build() always selects the local test environment; all network destinations are the mock. let test = builder.build(&server).await?; assert!(test.codex.analytics_enabled()); @@ -762,18 +790,20 @@ async fn direct_metadata_limit_retains_marker_without_sending_it_to_custom_provi recorded_metadata["executed_tool_calls"][0]["arguments"], json!({"query": query}) ); - // An ungranted custom Responses provider must not receive internal result metadata. - assert!( - wire_metadata["executed_tool_calls"][0] - .get("tool_result_metadata") - .is_none() - ); + let wire_result_metadata = + wire_metadata["executed_tool_calls"][0].get("tool_result_metadata"); + if include_internal_metadata { + assert_eq!(wire_result_metadata, Some(&metadata_for_query(query))); + } else { + // An ungranted provider must not receive internal result metadata. + assert!(wire_result_metadata.is_none()); + } } let first_metadata = tool_call_metadata(recorded_outputs[0].clone()); let second_metadata = tool_call_metadata(recorded_outputs[1].clone()); assert_eq!( first_metadata["executed_tool_calls"][0]["tool_result_metadata"], - raw_metadata + metadata_for_query("first") ); let marker = second_metadata["executed_tool_calls"][0]["tool_result_metadata"] .as_str() @@ -784,5 +814,117 @@ async fn direct_metadata_limit_retains_marker_without_sending_it_to_custom_provi .and_then(|value| value.parse::().ok()) .expect("marker should report omitted bytes"); assert!(overage > 0); + if !include_internal_metadata { + return Ok(()); + } + + let rollout_path = test.codex.rollout_path().expect("rollout path"); + test.codex.shutdown_and_wait().await?; + let rollout = tokio::fs::read_to_string(&rollout_path).await?; + let rollout = rollout + .lines() + .map(serde_json::from_str::) + .collect::>>()?; + for (call_id, expected) in [("first", &first_metadata), ("second", &second_metadata)] { + let item = rollout + .iter() + .find(|line| { + line["type"] == "response_item" + && line["payload"]["type"] == "function_call_output" + && line["payload"]["call_id"] == call_id + }) + .expect("persisted tool output"); + assert_eq!(tool_call_metadata(item["payload"].clone()), *expected); + } + let forked = test + .thread_manager + .fork_legacy_thread( + ForkSnapshot::Interrupted, + StartThreadOptions::new(test.config.clone()), + rollout_path.clone(), + ) + .await?; + wait_for_mcp_server(&forked.thread, CODEX_APPS_MCP_SERVER_NAME).await?; + forked + .thread + .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { + text: "List events in the fork.".to_string(), + text_elements: Vec::new(), + }])) + .await?; + wait_for_event(&forked.thread, |event| { + matches!(event, EventMsg::TurnComplete(_)) + }) + .await; + forked + .thread + .shutdown_and_wait() + .await + .expect("shutdown fork"); + + let resumed = search_capable_apps_builder(apps.chatgpt_base_url) + .with_config(configure_metadata) + .resume(&server, test.home.clone(), rollout_path) + .await + .expect("resume original thread"); + wait_for_mcp_server(&resumed.codex, CODEX_APPS_MCP_SERVER_NAME) + .await + .expect("start resumed MCP server"); + resumed + .submit_text_turn("List events after resume.") + .await + .expect("run resumed turn"); + resumed + .codex + .shutdown_and_wait() + .await + .expect("shutdown resumed thread"); + + let requests = responses.requests(); + assert_eq!(requests.len(), 7); + for request in &requests[3..] { + for original in [&first_output, &second_output] { + let call_id = original["call_id"].as_str().expect("call ID"); + let output = request.function_call_output(call_id); + assert_eq!(output["output"], original["output"]); + let passthrough = output.get("internal_chat_message_metadata_passthrough"); + assert!( + passthrough + .and_then(|meta| meta.get("executed_tool_calls")) + .is_none() + ); + assert!( + passthrough + .and_then(|meta| meta.get("tool_calls_complete")) + .is_none() + ); + } + } + for (request, query) in [(&requests[4], "fork"), (&requests[6], "resume")] { + let output = request.function_call_output(query); + assert!( + output["output"] + .to_string() + .contains(&format!("result for {query}")) + ); + let metadata = tool_call_metadata(output); + assert_eq!(metadata["tool_calls_complete"], true); + assert_eq!( + metadata["executed_tool_calls"].as_array().map(Vec::len), + Some(1) + ); + assert_eq!( + metadata["executed_tool_calls"][0]["name"], + format!("{SEARCH_CALENDAR_NAMESPACE}{SEARCH_CALENDAR_LIST_TOOL}") + ); + assert_eq!( + metadata["executed_tool_calls"][0]["arguments"], + json!({"query": query}) + ); + assert_eq!( + metadata["executed_tool_calls"][0]["tool_result_metadata"], + metadata_for_query(query) + ); + } Ok(()) } diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 318a4faa3df0..e31b88fdbbb0 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -49,9 +49,8 @@ pub use executed_tool_calls::ToolResultMetadata; pub use executed_tool_calls::ToolResultSource; pub use executed_tool_calls::ToolResultSources; pub use executed_tool_calls::bound_executed_tool_calls_for_message; -pub use executed_tool_calls::bound_executed_tool_calls_for_prompt; -pub use executed_tool_calls::bound_executed_tool_calls_for_prompt_prioritizing_recent; pub use executed_tool_calls::executed_tool_call_metadata_bytes; +pub use executed_tool_calls::normalize_executed_tool_call_arguments; pub use item_metadata::ContentItemKind; /// Controls the per-command sandbox override requested by a shell-like tool call. diff --git a/codex-rs/protocol/src/models/executed_tool_calls.rs b/codex-rs/protocol/src/models/executed_tool_calls.rs index 676c0919d8ff..5bd758d75cb7 100644 --- a/codex-rs/protocol/src/models/executed_tool_calls.rs +++ b/codex-rs/protocol/src/models/executed_tool_calls.rs @@ -13,8 +13,6 @@ const MAX_EXECUTED_TOOL_CALL_ARGUMENT_BYTES: usize = 8 * 1024; const MAX_TOOL_RESULT_SOURCES: usize = 32; /// Maximum UTF-8 bytes for each source's `type` and `id` separately, not the source list. pub const MAX_TOOL_RESULT_SOURCE_FIELD_BYTES: usize = 128; -/// Maximum serialized warehouse-only attempted-tool metadata in one request. -const MAX_EXECUTED_TOOL_CALL_METADATA_BYTES: usize = 2 * 1024 * 1024; const RESOURCE_ACCESS_METADATA_KEY: &str = "openai/resource_access"; const EXECUTED_TOOL_CALL_METADATA_FIELD_BYTES: usize = b"\"executed_tool_calls\":".len(); const INTERNAL_CHAT_MESSAGE_METADATA_PASSTHROUGH_FIELD_BYTES: usize = @@ -75,23 +73,12 @@ impl InternalChatMessageMetadataPassthrough { } } -/// Bounds attempted-tool metadata fairly across the complete serialized request. -pub fn bound_executed_tool_calls_for_prompt(items: &mut [ResponseItem]) { - bound_executed_tool_calls_for_prompt_with_priority(items, /*prioritize_recent*/ false); -} - -/// Bounds retained history without letting older calls displace the newest calls. -pub fn bound_executed_tool_calls_for_prompt_prioritizing_recent(items: &mut [ResponseItem]) { - items.reverse(); - bound_executed_tool_calls_for_prompt_with_priority(items, /*prioritize_recent*/ true); - items.reverse(); -} - -fn bound_executed_tool_calls_for_prompt_with_priority( - items: &mut [ResponseItem], - prioritize_recent: bool, -) { +/// Bounds recorded arguments and clears completion when any call in a cell is truncated. +/// Returns cells whose arguments were newly truncated. Ordinary tool-call arguments and +/// outputs are not changed. +pub fn normalize_executed_tool_call_arguments(items: &mut [ResponseItem]) -> HashSet { let mut damaged_cells = HashSet::new(); + let mut newly_truncated_cells = HashSet::new(); for item in items.iter_mut() { let Some(metadata) = item .internal_chat_message_metadata_passthrough_mut() @@ -100,6 +87,7 @@ fn bound_executed_tool_calls_for_prompt_with_priority( continue; }; let mut truncated = false; + let mut newly_truncated = false; for call in metadata.executed_tool_calls.iter_mut().flatten() { let argument_bytes = serde_json::to_vec(&call.arguments) .map(|bytes| bytes.len()) @@ -111,6 +99,7 @@ fn bound_executed_tool_calls_for_prompt_with_priority( MAX_EXECUTED_TOOL_CALL_ARGUMENT_BYTES, /*omitted_calls*/ None, ); + newly_truncated = true; } truncated |= call.truncation().is_some(); } @@ -118,15 +107,12 @@ fn bound_executed_tool_calls_for_prompt_with_priority( metadata.tool_calls_complete = None; damaged_cells.extend(metadata.cell_id.clone()); } + if newly_truncated { + newly_truncated_cells.extend(metadata.cell_id.clone()); + } } clear_damaged_cell_completeness(items, &damaged_cells); - - bound_executed_tool_calls_with_metadata_budget( - items, - MAX_EXECUTED_TOOL_CALL_METADATA_BYTES, - prioritize_recent, - /*whole_message*/ false, - ); + newly_truncated_cells } /// Bounds optional observations to the space left in the actual outgoing message. @@ -134,20 +120,6 @@ fn bound_executed_tool_calls_for_prompt_with_priority( pub fn bound_executed_tool_calls_for_message( items: &mut [ResponseItem], max_metadata_bytes: usize, -) { - bound_executed_tool_calls_with_metadata_budget( - items, - max_metadata_bytes, - /*prioritize_recent*/ false, - /*whole_message*/ true, - ); -} - -fn bound_executed_tool_calls_with_metadata_budget( - items: &mut [ResponseItem], - max_metadata_bytes: usize, - prioritize_recent: bool, - whole_message: bool, ) { let mut damaged_cells = HashSet::new(); @@ -155,19 +127,12 @@ fn bound_executed_tool_calls_with_metadata_budget( if total_metadata_bytes <= max_metadata_bytes { return; } - let mut remaining_bytes = shed_generic_result_metadata( - items, - max_metadata_bytes, - prioritize_recent, - whole_message, - total_metadata_bytes, - ); - - // At the message limit, shed optional sources and recorded arguments before - // resource-access evidence. The prompt budget keeps its existing order. - if whole_message && metadata_bytes(items) > max_metadata_bytes { - let overage_bytes = - shed_result_sources(items, max_metadata_bytes, /*whole_message*/ true); + let mut remaining_bytes = + shed_generic_result_metadata(items, max_metadata_bytes, total_metadata_bytes); + + // Shed optional sources and recorded arguments before resource-access evidence. + if metadata_bytes(items) > max_metadata_bytes { + let overage_bytes = shed_result_sources(items, max_metadata_bytes); if overage_bytes > 0 { truncate_call_arguments_to_fit( items, @@ -178,13 +143,7 @@ fn bound_executed_tool_calls_with_metadata_budget( } remaining_bytes = metadata_bytes(items); } - shed_remaining_result_metadata( - items, - max_metadata_bytes, - prioritize_recent, - whole_message, - remaining_bytes, - ); + shed_remaining_result_metadata(items, max_metadata_bytes, remaining_bytes); // Recheck the serialized request rather than relying on the incremental size accounting. if metadata_bytes(items) <= max_metadata_bytes { @@ -198,21 +157,7 @@ fn bound_executed_tool_calls_with_metadata_budget( if metadata_bytes(items) <= max_metadata_bytes { return; } - // The whole-message path already handled sources and arguments before resources. - if !whole_message { - shed_result_sources(items, max_metadata_bytes, /*whole_message*/ false); - } - - if metadata_bytes(items) <= max_metadata_bytes { - return; - } - - distribute_remaining_budget( - items, - max_metadata_bytes, - prioritize_recent, - &mut damaged_cells, - ); + distribute_remaining_budget(items, max_metadata_bytes, &mut damaged_cells); clear_damaged_cell_completeness(items, &damaged_cells); } @@ -224,23 +169,13 @@ fn metadata_bytes(items: &[ResponseItem]) -> usize { type ResultMetadataEntry<'a> = (usize, usize, usize, &'a mut ToolResultMetadata); -fn result_metadata_by_size( - items: &mut [ResponseItem], - prioritize_recent: bool, -) -> Vec> { +fn result_metadata_by_size(items: &mut [ResponseItem]) -> Vec> { let mut result_metadata = Vec::new(); for (item_index, item) in items.iter_mut().enumerate() { if let Some(metadata) = item .internal_chat_message_metadata_passthrough_mut() .and_then(Option::as_mut) { - // The retained-history entry point has already reversed the items. Break size ties - // by evicting older outputs, while keeping the original call order within an output. - let eviction_order = if prioritize_recent { - usize::MAX - item_index - } else { - item_index - }; for (call_index, call) in metadata .executed_tool_calls .iter_mut() @@ -252,7 +187,7 @@ fn result_metadata_by_size( .map_or(usize::MAX, |value| value.len()); result_metadata.push(( bytes, - eviction_order, + item_index, call_index, &mut call.tool_result_metadata, )); @@ -269,12 +204,10 @@ fn result_metadata_by_size( fn shed_generic_result_metadata( items: &mut [ResponseItem], max_metadata_bytes: usize, - prioritize_recent: bool, - whole_message: bool, mut total_metadata_bytes: usize, ) -> usize { // Shed the largest values first so one large result does not discard unrelated small results. - let mut result_metadata = result_metadata_by_size(items, prioritize_recent); + let mut result_metadata = result_metadata_by_size(items); for (bytes, _, _, metadata) in &mut result_metadata { if total_metadata_bytes <= max_metadata_bytes { break; @@ -294,19 +227,17 @@ fn shed_generic_result_metadata( } // Tiny generic values and markers can be cheaper than replacing them with a // new marker. Remove their complete fields before sacrificing resource evidence. - if whole_message { - for (bytes, _, _, metadata) in &mut result_metadata { - if total_metadata_bytes <= max_metadata_bytes { - break; - } - if metadata.retain_resource_access() { - continue; - } - **metadata = ToolResultMetadata::default(); - total_metadata_bytes = total_metadata_bytes - .saturating_sub(bytes.saturating_add(b",\"tool_result_metadata\":".len())); - *bytes = 0; + for (bytes, _, _, metadata) in &mut result_metadata { + if total_metadata_bytes <= max_metadata_bytes { + break; + } + if metadata.retain_resource_access() { + continue; } + **metadata = ToolResultMetadata::default(); + total_metadata_bytes = total_metadata_bytes + .saturating_sub(bytes.saturating_add(b",\"tool_result_metadata\":".len())); + *bytes = 0; } total_metadata_bytes } @@ -314,11 +245,9 @@ fn shed_generic_result_metadata( fn shed_remaining_result_metadata( items: &mut [ResponseItem], max_metadata_bytes: usize, - prioritize_recent: bool, - whole_message: bool, mut total_metadata_bytes: usize, ) { - let mut result_metadata = result_metadata_by_size(items, prioritize_recent); + let mut result_metadata = result_metadata_by_size(items); // Resource evidence is optional too; it must not displace the call inventory. for (bytes, _, _, metadata) in &mut result_metadata { if total_metadata_bytes <= max_metadata_bytes { @@ -333,9 +262,8 @@ fn shed_remaining_result_metadata( // Use the updated sizes: markers should be removed before smaller provider metadata. result_metadata.sort_by_key(|(bytes, order, call_index, metadata)| { ( - whole_message && !metadata.is_omitted_due_to_size_limit(), - std::cmp::Reverse(*bytes), !metadata.is_omitted_due_to_size_limit(), + std::cmp::Reverse(*bytes), *order, *call_index, ) @@ -354,11 +282,7 @@ fn shed_remaining_result_metadata( } } -fn shed_result_sources( - items: &mut [ResponseItem], - max_metadata_bytes: usize, - whole_message: bool, -) -> usize { +fn shed_result_sources(items: &mut [ResponseItem], max_metadata_bytes: usize) -> usize { // Source evidence is optional; dropping it must not discard calls or their completion proof. let mut overage_bytes = metadata_bytes(items).saturating_sub(max_metadata_bytes); for item in items.iter_mut() { @@ -367,7 +291,7 @@ fn shed_result_sources( .and_then(Option::as_mut) { for call in metadata.executed_tool_calls.iter_mut().flatten() { - if whole_message && overage_bytes == 0 { + if overage_bytes == 0 { break; } if let Some(sources) = call.tool_result_sources.take() { @@ -439,7 +363,6 @@ fn truncate_call_arguments_to_fit( fn distribute_remaining_budget( items: &mut [ResponseItem], max_metadata_bytes: usize, - prioritize_recent: bool, damaged_cells: &mut HashSet, ) { let mut remaining_items = items @@ -452,11 +375,7 @@ fn distribute_remaining_budget( if item_bytes == 0 { continue; } - let item_budget = if prioritize_recent { - remaining_bytes - } else { - remaining_bytes / remaining_items - }; + let item_budget = remaining_bytes / remaining_items; if item_bytes > item_budget { // Remember the cell before a too-small share removes its metadata entirely. damaged_cells.extend( diff --git a/codex-rs/protocol/src/models/executed_tool_calls_tests.rs b/codex-rs/protocol/src/models/executed_tool_calls_tests.rs index 9195a693afc9..422db7f82e63 100644 --- a/codex-rs/protocol/src/models/executed_tool_calls_tests.rs +++ b/codex-rs/protocol/src/models/executed_tool_calls_tests.rs @@ -88,253 +88,20 @@ fn result_metadata_comparison_tracks_raw_values_and_call_bindings() { } #[test] -fn executed_tool_call_prompt_budget_includes_metadata_fields() -> Result<()> { - let metadata_bytes = |items: &[ResponseItem]| -> Result { - items.iter().try_fold(0_usize, |bytes, item| { - let with_metadata = serde_json::to_vec(item)?.len(); - let mut without_metadata = item.clone(); - without_metadata.clear_executed_tool_calls(); - let without_metadata = serde_json::to_vec(&without_metadata)?.len(); - Ok(bytes + with_metadata.saturating_sub(without_metadata)) - }) - }; - - for with_turn_id in [true, false] { - let mut items = (0..4_000) - .map(|index| { - let mut item = output(&index.to_string()); - *item - .internal_chat_message_metadata_passthrough_mut() - .unwrap() = with_turn_id.then(|| passthrough_metadata("turn-1")); - item.append_executed_tool_calls(vec![ExecutedToolCall::new( - String::new(), - serde_json::Value::Null, - )]); - item.set_tool_call_cell_id(&format!("cell-\"\\{}", "x".repeat(512))); - item.mark_tool_calls_complete(); - item - }) - .collect::>(); - - assert!(metadata_bytes(&items)? > MAX_EXECUTED_TOOL_CALL_METADATA_BYTES); - bound_executed_tool_calls_for_prompt(&mut items); - assert!(metadata_bytes(&items)? <= MAX_EXECUTED_TOOL_CALL_METADATA_BYTES); - for metadata in items - .iter() - .filter_map(ResponseItem::executed_tool_call_metadata) - { - assert_eq!(metadata.tool_calls_complete, None); - } - - let calls = items - .iter() - .filter_map(ResponseItem::executed_tool_call_metadata) - .filter_map(|metadata| metadata.executed_tool_calls.as_ref()) - .flatten() - .collect::>(); - // Shares smaller than a local marker omit that output; surviving calls stay unchanged. - assert!(!calls.is_empty() && calls.len() < 4_000); - assert!(calls.iter().all(|call| { - **call == ExecutedToolCall::new(String::new(), serde_json::Value::Null) - })); - - let bounded_items = items.clone(); - bound_executed_tool_calls_for_prompt(&mut items); - assert_eq!(items, bounded_items); - - let oversized_names = - ["\0", "é"].map(|name| name.repeat(MAX_EXECUTED_TOOL_CALL_METADATA_BYTES)); - let mut oversized_items = oversized_names - .iter() - .enumerate() - .map(|(index, name)| { - let mut item = output(&index.to_string()); - *item - .internal_chat_message_metadata_passthrough_mut() - .unwrap() = with_turn_id.then(|| passthrough_metadata("turn-1")); - item.append_executed_tool_calls(vec![ExecutedToolCall::new( - name.clone(), - serde_json::Value::Null, - )]); - item.set_tool_call_cell_id("cell-\"\\"); - item - }) - .collect::>(); - - bound_executed_tool_calls_for_prompt(&mut oversized_items); - assert!(metadata_bytes(&oversized_items)? <= MAX_EXECUTED_TOOL_CALL_METADATA_BYTES); - let calls = oversized_items - .iter() - .filter_map(ResponseItem::executed_tool_call_metadata) - .filter_map(|metadata| metadata.executed_tool_calls.as_ref()) - .flatten() - .collect::>(); - assert_eq!(calls.len(), 2); - for ((item, call), oversized_name) in - oversized_items.iter().zip(calls).zip(&oversized_names) - { - assert_eq!( - item.executed_tool_call_metadata() - .unwrap() - .cell_id - .as_deref(), - Some("cell-\"\\"), - ); - assert!(call.name.len() < oversized_name.len()); - assert!(oversized_name.starts_with(&call.name)); - let truncation = call.truncation().expect("trusted omission marker"); - assert_eq!(truncation.omitted_calls, None); - assert_eq!(truncation.original_name_bytes, Some(oversized_name.len())); - assert_eq!( - serde_json::to_value(&call.arguments)?["_codex_executed_tool_call_truncated"]["original_name_bytes"], - serde_json::json!(oversized_name.len()), - ); - } - - let bounded_items = oversized_items.clone(); - bound_executed_tool_calls_for_prompt(&mut oversized_items); - assert_eq!(oversized_items, bounded_items); - } - - let arguments = serde_json::json!({ "payload": "x".repeat(7 * 1024) }); - let argument_bytes = serde_json::to_vec(&arguments)?.len(); - assert!(argument_bytes < MAX_EXECUTED_TOOL_CALL_ARGUMENT_BYTES); - let call_count = MAX_EXECUTED_TOOL_CALL_METADATA_BYTES / argument_bytes + 1; - assert!(call_count * argument_bytes > MAX_EXECUTED_TOOL_CALL_METADATA_BYTES); - let mut items = ["A", "wait-A", "B"].map(|call_id| { - let mut item = output(call_id); - item.mark_tool_calls_complete(); - item - }); - for item in &mut items[..2] { - item.set_tool_call_cell_id("cell-A"); - } - let calls = vec![ExecutedToolCall::new("test_tool".to_string(), arguments); call_count]; - items[0].append_executed_tool_calls(calls.clone()); - items[2].append_executed_tool_calls(vec![ExecutedToolCall::new( - "test_tool".to_string(), - serde_json::json!({}), - )]); - assert!(metadata_bytes(&items)? > MAX_EXECUTED_TOOL_CALL_METADATA_BYTES); - for bound in [ - bound_executed_tool_calls_for_prompt, - bound_executed_tool_calls_for_prompt_prioritizing_recent, - ] { - let mut bounded = items.clone(); - bound(&mut bounded); - assert!(metadata_bytes(&bounded)? <= MAX_EXECUTED_TOOL_CALL_METADATA_BYTES); - assert_eq!(bounded[2], items[2]); - let metadata = bounded[0].executed_tool_call_metadata().unwrap(); - assert_eq!(metadata.tool_calls_complete, None); - let recorded = metadata.executed_tool_calls.as_ref().unwrap(); - assert_eq!(recorded.len(), 1); - assert_eq!(recorded[0].name, "test_tool"); - let truncation = recorded[0].truncation().unwrap(); - assert_eq!(truncation.original_bytes, argument_bytes); - assert_eq!(truncation.omitted_calls, Some(call_count - 1)); - assert_eq!( - bounded[1] - .executed_tool_call_metadata() - .unwrap() - .tool_calls_complete, - None, - ); - bounded[0].append_executed_tool_calls(calls.clone()); - assert!(metadata_bytes(&bounded)? > MAX_EXECUTED_TOOL_CALL_METADATA_BYTES); - bound(&mut bounded); - assert!(metadata_bytes(&bounded)? <= MAX_EXECUTED_TOOL_CALL_METADATA_BYTES); - assert_eq!(bounded[2], items[2]); - assert_eq!( - first_executed_tool_call(&mut bounded[0]) - .unwrap() - .truncation() - .unwrap() - .omitted_calls, - Some(2 * call_count - 1), - ); - } - - let mut items = ["exec", "wait"].map(|call_id| { - let mut item = output(call_id); - item.set_tool_call_cell_id("cell-\"\\"); - item - }); - items[0].append_executed_tool_calls(vec![ - ExecutedToolCall::new( - "test_tool".to_string(), - serde_json::json!({ "payload": "x".repeat(MAX_EXECUTED_TOOL_CALL_ARGUMENT_BYTES - 256) }), - ); - 16 - ]); - items[1].mark_tool_calls_complete(); - // Leave room for calls, but not their sources, without exceeding the argument limit. - let padding_bytes = MAX_EXECUTED_TOOL_CALL_METADATA_BYTES - metadata_bytes(&items)? - 1024; - let calls = items[0] - .ensure_tool_call_metadata() - .unwrap() - .executed_tool_calls - .as_mut() - .unwrap(); - let call_count = calls.len(); - for (index, call) in calls.iter_mut().enumerate() { - call.name.push_str( - &"x".repeat( - padding_bytes / call_count + usize::from(index < padding_bytes % call_count), - ), - ); - } - let expected = items.clone(); - assert!( - first_executed_tool_call(&mut items[0]) - .expect("recorded call should exist") - .set_tool_result_sources(ToolResultSources::new( - (0..MAX_TOOL_RESULT_SOURCES) - .map(|index| ToolResultSource { - r#type: "document".to_string(), - id: format!( - "R{index:0width$}", - width = MAX_TOOL_RESULT_SOURCE_FIELD_BYTES - 1 - ), - }) - .collect(), - )) - ); - assert!(metadata_bytes(&items)? > MAX_EXECUTED_TOOL_CALL_METADATA_BYTES); - bound_executed_tool_calls_for_prompt(&mut items); - assert_eq!(items, expected); - - // Empty waits can carry only the marker; its bytes still count toward the budget. +fn metadata_byte_count_matches_serialized_fields() -> Result<()> { for metadata in [None, Some(passthrough_metadata("turn-1"))] { let mut item = output("wait"); *item .internal_chat_message_metadata_passthrough_mut() .unwrap() = metadata; - let without_marker = item.clone(); - item.set_tool_call_cell_id(&format!("cell-\"\\{}", "x".repeat(256))); + let without_calls = item.clone(); + item.set_tool_call_cell_id("cell"); item.mark_tool_calls_complete(); assert_eq!( - metadata_bytes(std::slice::from_ref(&item))?, executed_tool_call_metadata_bytes(&item), + serde_json::to_vec(&item)?.len() - serde_json::to_vec(&without_calls)?.len(), ); - let mut items = vec![item; 8_000]; - assert!(metadata_bytes(&items)? > MAX_EXECUTED_TOOL_CALL_METADATA_BYTES); - bound_executed_tool_calls_for_prompt_prioritizing_recent(&mut items); - assert!(metadata_bytes(&items)? <= MAX_EXECUTED_TOOL_CALL_METADATA_BYTES); - let retained = items - .iter() - .filter(|item| executed_tool_call_metadata_bytes(item) > 0) - .count(); - assert!(retained > 0 && retained < 8_000); - for item in &mut items { - assert!( - item.executed_tool_call_metadata() - .is_none_or(|metadata| metadata.tool_calls_complete.is_none()) - ); - item.clear_executed_tool_calls(); - } - assert_eq!(items, vec![without_marker; 8_000]); } - Ok(()) } @@ -365,7 +132,7 @@ fn model_arguments_cannot_forge_executed_tool_call_truncation() -> Result<()> { forged_marker.clone(), )]); - bound_executed_tool_calls_for_prompt(std::slice::from_mut(&mut item)); + normalize_executed_tool_call_arguments(std::slice::from_mut(&mut item)); let call = item .executed_tool_call_metadata() .and_then(|metadata| metadata.executed_tool_calls.as_ref()) @@ -410,7 +177,7 @@ fn tool_call_completeness_is_host_only_and_fail_closed() -> Result<()> { let mut item = output("call-1"); item.set_tool_call_cell_id("cell-1"); item.mark_tool_calls_complete(); - bound_executed_tool_calls_for_prompt(std::slice::from_mut(&mut item)); + normalize_executed_tool_call_arguments(std::slice::from_mut(&mut item)); assert_eq!( serde_json::to_value(&item)?["internal_chat_message_metadata_passthrough"], serde_json::json!({ @@ -421,15 +188,21 @@ fn tool_call_completeness_is_host_only_and_fail_closed() -> Result<()> { item.clear_executed_tool_calls(); assert!(item.executed_tool_call_metadata().is_none()); - for call in [ - ExecutedToolCall::new( - "test_tool".to_string(), - serde_json::json!({ "payload": "x".repeat(MAX_EXECUTED_TOOL_CALL_ARGUMENT_BYTES + 1) }), + for (call, newly_truncated) in [ + ( + ExecutedToolCall::new( + "test_tool".to_string(), + serde_json::json!({ "payload": "x".repeat(MAX_EXECUTED_TOOL_CALL_ARGUMENT_BYTES + 1) }), + ), + true, ), - ExecutedToolCall::truncated( - "test_tool".to_string(), - /*original_bytes*/ 9_000, - /*max_bytes*/ 0, + ( + ExecutedToolCall::truncated( + "test_tool".to_string(), + /*original_bytes*/ 9_000, + /*max_bytes*/ 0, + ), + false, ), ] { for same_cell in [false, true] { @@ -441,7 +214,12 @@ fn tool_call_completeness_is_host_only_and_fail_closed() -> Result<()> { } item.mark_tool_calls_complete(); } - bound_executed_tool_calls_for_prompt(&mut items); + let changed_cells = normalize_executed_tool_call_arguments(&mut items); + assert_eq!( + changed_cells.contains("cell-1"), + same_cell && newly_truncated + ); + assert!(normalize_executed_tool_call_arguments(&mut items).is_empty()); assert_eq!( items.map(|item| item .executed_tool_call_metadata() @@ -817,77 +595,9 @@ fn message_budget_preserves_names_and_turn_metadata_and_invalidates_shared_cell_ } } -#[test] -fn raw_result_metadata_is_shed_before_sources_calls_or_completion() -> Result<()> { - // Keep each result below its capture limit while filling the larger request budget. - let name = format!( - "test_tool{}", - "x".repeat(MAX_EXECUTED_TOOL_CALL_METADATA_BYTES / 8 - 16 * 1024) - ); - let mut call = ExecutedToolCall::new(name, serde_json::json!({})); - call.set_tool_result_sources(ToolResultSources::new(vec![ToolResultSource { - r#type: "test_resource".to_string(), - id: "R1".to_string(), - }])); - let mut items = [ - "first", - "second", - "third", - "fourth", - "fifth", - "sixth", - "seventh", - "without-metadata", - ] - .map(|call_id| { - let mut item = ResponseItem::from(ResponseInputItem::FunctionCallOutput { - call_id: call_id.to_string(), - output: FunctionCallOutputPayload::from_text("unchanged output".to_string()), - }); - item.append_executed_tool_calls(vec![call.clone()]); - item.set_tool_call_cell_id(call_id); - item.mark_tool_calls_complete(); - item - }); - let metadata = ToolResultMetadata::new(&serde_json::json!({ "large": "x".repeat(20_000) })); - for item in &mut items[..7] { - first_executed_tool_call(item) - .unwrap() - .set_tool_result_metadata(metadata.clone()); - } - let original_bytes = items - .iter() - .map(executed_tool_call_metadata_bytes) - .sum::(); - let mut expected = items.clone(); - first_executed_tool_call(&mut expected[0]) - .unwrap() - .set_tool_result_metadata(ToolResultMetadata::omitted_due_to_size_limit( - original_bytes - MAX_EXECUTED_TOOL_CALL_METADATA_BYTES, - )); - assert!(original_bytes > MAX_EXECUTED_TOOL_CALL_METADATA_BYTES); - for bound in [ - bound_executed_tool_calls_for_prompt, - bound_executed_tool_calls_for_prompt_prioritizing_recent, - ] { - let mut bounded = items.clone(); - bound(&mut bounded); - assert_eq!(bounded, expected); - assert!( - bounded - .iter() - .map(executed_tool_call_metadata_bytes) - .sum::() - <= MAX_EXECUTED_TOOL_CALL_METADATA_BYTES - ); - bound(&mut bounded); - assert_eq!(bounded, expected); - } - Ok(()) -} - #[test] fn result_metadata_shedding_keeps_smaller_results_within_one_output() { + let budget = 2 * 1024 * 1024; let resource_only = serde_json::json!({ "openai/resource_access": { "resource_coverage": "complete", "resources": [] }, }); @@ -928,7 +638,7 @@ fn result_metadata_shedding_keeps_smaller_results_within_one_output() { serde_json::json!({ "padding": "x".repeat(17_000) }); 6 ]); - let name_padding = (MAX_EXECUTED_TOOL_CALL_METADATA_BYTES - 128 * 1024) / metadata.len(); + let name_padding = (budget - 128 * 1024) / metadata.len(); let mut item = output("exec"); item.append_executed_tool_calls( metadata @@ -950,10 +660,10 @@ fn result_metadata_shedding_keeps_smaller_results_within_one_output() { ); item.set_tool_call_cell_id("exec"); item.mark_tool_calls_complete(); - assert!(executed_tool_call_metadata_bytes(&item) > MAX_EXECUTED_TOOL_CALL_METADATA_BYTES); + assert!(executed_tool_call_metadata_bytes(&item) > budget); let replacement = replacement.unwrap_or_else(|| { ToolResultMetadata::omitted_due_to_size_limit( - executed_tool_call_metadata_bytes(&item) - MAX_EXECUTED_TOOL_CALL_METADATA_BYTES, + executed_tool_call_metadata_bytes(&item) - budget, ) }); let mut expected = item.clone(); @@ -964,162 +674,11 @@ fn result_metadata_shedding_keeps_smaller_results_within_one_output() { .as_mut() .unwrap()[reduced_index] .set_tool_result_metadata(replacement); - for bound in [ - bound_executed_tool_calls_for_prompt, - bound_executed_tool_calls_for_prompt_prioritizing_recent, - |items: &mut [ResponseItem]| { - bound_executed_tool_calls_for_message(items, MAX_EXECUTED_TOOL_CALL_METADATA_BYTES) - }, - ] { - let mut bounded = item.clone(); - bound(std::slice::from_mut(&mut bounded)); - assert_eq!(bounded, expected); - assert!( - executed_tool_call_metadata_bytes(&bounded) - <= MAX_EXECUTED_TOOL_CALL_METADATA_BYTES - ); - bound(std::slice::from_mut(&mut bounded)); - assert_eq!(bounded, expected); - } - } -} - -#[test] -fn result_metadata_shedding_removes_only_markers_needed_to_fit() { - let mut item = output("exec"); - let mut call = ExecutedToolCall::new("test_tool".to_string(), serde_json::json!({})); - call.set_tool_result_sources(ToolResultSources::new(vec![ToolResultSource { - r#type: "test_resource".to_string(), - id: "R1".to_string(), - }])); - let mut calls = vec![call; 20]; - let omitted = ToolResultMetadata::omitted_due_to_size_limit(/*overage_bytes*/ 1); - let marker_bytes = serde_json::to_vec(&omitted).unwrap().len(); - // A real object tied with a marker must survive even though it is older. - let equal_sized = ToolResultMetadata::new(&serde_json::json!({ - "value": "x".repeat(marker_bytes - r#"{"value":""}"#.len()), - })); - assert_eq!( - serde_json::to_vec(&equal_sized).unwrap().len(), - marker_bytes - ); - calls[0].set_tool_result_metadata(equal_sized); - for index in [1, 3] { - calls[index].set_tool_result_metadata(omitted.clone()); - } - calls[2].set_tool_result_metadata(ToolResultMetadata::new(&serde_json::json!({}))); - item.append_executed_tool_calls(calls); - item.set_tool_call_cell_id("exec"); - item.mark_tool_calls_complete(); - let remaining_bytes = - MAX_EXECUTED_TOOL_CALL_METADATA_BYTES + 1 - executed_tool_call_metadata_bytes(&item); - let calls = item - .ensure_tool_call_metadata() - .unwrap() - .executed_tool_calls - .as_mut() - .unwrap(); - let call_count = calls.len(); - for (index, call) in calls.iter_mut().enumerate() { - // Pad names so the per-argument limit cannot preempt result-metadata shedding. - call.name.push_str(&"x".repeat( - remaining_bytes / call_count + usize::from(index < remaining_bytes % call_count), - )); - } - assert_eq!( - executed_tool_call_metadata_bytes(&item), - MAX_EXECUTED_TOOL_CALL_METADATA_BYTES + 1 - ); - let mut expected = item.clone(); - expected - .ensure_tool_call_metadata() - .unwrap() - .executed_tool_calls - .as_mut() - .unwrap()[1] - .set_tool_result_metadata(ToolResultMetadata::default()); - let tied_markers = item.clone(); - // The new marker has a larger overage value and therefore more bytes. Remove - // that marker first, while tied smaller markers still precede provider data. - item.ensure_tool_call_metadata() - .unwrap() - .executed_tool_calls - .as_mut() - .unwrap()[3] - .set_tool_result_metadata(ToolResultMetadata::new(&serde_json::json!({ - "large": "x".repeat(4 * 1024), - }))); - let mut larger_marker_expected = item.clone(); - larger_marker_expected - .ensure_tool_call_metadata() - .unwrap() - .executed_tool_calls - .as_mut() - .unwrap()[3] - .set_tool_result_metadata(ToolResultMetadata::default()); - for (item, expected) in [(tied_markers, expected), (item, larger_marker_expected)] { - for bound in [ - bound_executed_tool_calls_for_prompt, - bound_executed_tool_calls_for_prompt_prioritizing_recent, - ] { - let mut bounded = item.clone(); - bound(std::slice::from_mut(&mut bounded)); - assert_eq!(bounded, expected); - assert!( - executed_tool_call_metadata_bytes(&bounded) - <= MAX_EXECUTED_TOOL_CALL_METADATA_BYTES - ); - bound(std::slice::from_mut(&mut bounded)); - assert_eq!(bounded, expected); - } - } -} - -#[test] -fn result_metadata_markers_do_not_displace_existing_evidence() { - let mut item = ResponseItem::from(ResponseInputItem::FunctionCallOutput { - call_id: "exec".to_string(), - output: FunctionCallOutputPayload::from_text("unchanged output".to_string()), - }); - let mut call = ExecutedToolCall::new("test_tool".to_string(), serde_json::json!({})); - call.set_tool_result_sources(ToolResultSources::new(vec![ToolResultSource { - r#type: "test_resource".to_string(), - id: "R1".to_string(), - }])); - item.append_executed_tool_calls(vec![call; 20]); - item.set_tool_call_cell_id("exec"); - item.mark_tool_calls_complete(); - let remaining_bytes = - MAX_EXECUTED_TOOL_CALL_METADATA_BYTES - executed_tool_call_metadata_bytes(&item); - let calls = item - .ensure_tool_call_metadata() - .unwrap() - .executed_tool_calls - .as_mut() - .unwrap(); - let call_count = calls.len(); - for (index, call) in calls.iter_mut().enumerate() { - // Fill the budget exactly without increasing arguments past their individual limit. - call.name.push_str(&"x".repeat( - remaining_bytes / call_count + usize::from(index < remaining_bytes % call_count), - )); - } - assert_eq!( - executed_tool_call_metadata_bytes(&item), - MAX_EXECUTED_TOOL_CALL_METADATA_BYTES - ); - let expected = item.clone(); - for metadata in [ - serde_json::json!({ "too-large": "x".repeat(32 * 1024) }), - serde_json::json!({ - "openai/resource_access": { "resource_coverage": "complete", "resources": [] }, - }), - ] { - let mut item = expected.clone(); - first_executed_tool_call(&mut item) - .unwrap() - .set_tool_result_metadata(ToolResultMetadata::new(&metadata)); - bound_executed_tool_calls_for_prompt(std::slice::from_mut(&mut item)); - assert_eq!(item, expected); + let mut bounded = item.clone(); + bound_executed_tool_calls_for_message(std::slice::from_mut(&mut bounded), budget); + assert_eq!(bounded, expected); + assert!(executed_tool_call_metadata_bytes(&bounded) <= budget); + bound_executed_tool_calls_for_message(std::slice::from_mut(&mut bounded), budget); + assert_eq!(bounded, expected); } } From 17a9df60e420b58e3edc55efb1bb052e39492bbc Mon Sep 17 00:00:00 2001 From: "Adam Perry @ OpenAI" Date: Tue, 29 Sep 2026 23:16:10 +0000 Subject: [PATCH 13/24] Add an experimental flag for bundled tools in login shells (#49403) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What changed Register `login_shell_package_path` as a disabled-by-default experimental feature and add it to the configuration schema. Expose it in `/experimental` as “Bundled tools in login shells,” with a description explaining its intended use when login shell startup resets `PATH`. ## Testing Extend the experimental feature metadata snapshot test to cover the new feature's label, description, and unchecked default state. GitOrigin-RevId: dc110486ccd8d2d76ac788ba5a413a1374699ff8 --- codex-rs/core/config.schema.json | 6 +++ codex-rs/features/src/lib.rs | 12 ++++++ .../experimental_features_view_tests.rs | 41 +++++++++++-------- ...tal_features_login_shell_package_path.snap | 16 ++++++++ 4 files changed, 57 insertions(+), 18 deletions(-) create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_login_shell_package_path.snap diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index a3e5627aaccc..d5521009b99b 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -954,6 +954,9 @@ "local_thread_store_shared_compression": { "type": "boolean" }, + "login_shell_package_path": { + "type": "boolean" + }, "mcp_2026_07_28": { "type": "boolean" }, @@ -6978,6 +6981,9 @@ "local_thread_store_shared_compression": { "type": "boolean" }, + "login_shell_package_path": { + "type": "boolean" + }, "mcp_2026_07_28": { "type": "boolean" }, diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index db35cd4d448a..2b6d26acb330 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -168,6 +168,8 @@ pub enum Feature { UseLegacyLandlock, /// Experimental shell snapshotting. ShellSnapshot, + /// Restore bundled tools to PATH after Codex's login shell starts. + LoginShellPackagePath, /// Expose the selected PowerShell execution host's bounded major/minor version. PowerShellShellVersion, /// Keep policy-filtered shell snapshots entirely in executor memory. @@ -1018,6 +1020,16 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::Stable, default_enabled: true, }, + FeatureSpec { + id: Feature::LoginShellPackagePath, + key: "login_shell_package_path", + stage: Stage::Experimental { + name: "Bundled tools in login shells", + menu_description: "Keep bundled tools such as ripgrep available when login shell startup resets PATH.", + announcement: "", + }, + default_enabled: false, + }, FeatureSpec { id: Feature::PowerShellShellVersion, key: "powershell_shell_version", diff --git a/codex-rs/tui/src/bottom_pane/experimental_features_view_tests.rs b/codex-rs/tui/src/bottom_pane/experimental_features_view_tests.rs index 38110e635857..6c103f5f6105 100644 --- a/codex-rs/tui/src/bottom_pane/experimental_features_view_tests.rs +++ b/codex-rs/tui/src/bottom_pane/experimental_features_view_tests.rs @@ -47,25 +47,30 @@ fn experimental_features_keep_following_choice_at_wrap_boundary() { insta::assert_snapshot!(snapshots.join("\n\n")); } +/// Experimental settings show each feature's label, description, and default state. #[test] -fn experimental_features_analytics_plan_history() { - let feature = Feature::AnalyticsPlanHistory; - let stage = feature.stage(); - let (app_tx, _app_rx) = tokio::sync::mpsc::unbounded_channel(); - let view = ExperimentalFeaturesView::new( - vec![ExperimentalFeatureItem { - key: feature.key().to_string(), - name: stage.experimental_menu_name().unwrap().to_string(), - description: stage.experimental_menu_description().unwrap().to_string(), - enabled: feature.default_enabled(), - writable: true, - }], - ThreadId::new(), - /*catalog_rx*/ None, - AppEventSender::new(app_tx), - crate::keymap::RuntimeKeymap::defaults().list, - ); - snapshot_view("experimental_features_analytics_plan_history", &view); +fn experimental_features_display_metadata() { + for feature in [ + Feature::AnalyticsPlanHistory, + Feature::LoginShellPackagePath, + ] { + let stage = feature.stage(); + let (app_tx, _app_rx) = tokio::sync::mpsc::unbounded_channel(); + let view = ExperimentalFeaturesView::new( + vec![ExperimentalFeatureItem { + key: feature.key().to_string(), + name: stage.experimental_menu_name().unwrap().to_string(), + description: stage.experimental_menu_description().unwrap().to_string(), + enabled: feature.default_enabled(), + writable: true, + }], + ThreadId::new(), + /*catalog_rx*/ None, + AppEventSender::new(app_tx), + crate::keymap::RuntimeKeymap::defaults().list, + ); + snapshot_view(&format!("experimental_features_{}", feature.key()), &view); + } } fn server_feature(name: &str) -> ExperimentalFeature { diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_login_shell_package_path.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_login_shell_package_path.snap new file mode 100644 index 000000000000..3cb14de54905 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__experimental_features_login_shell_package_path.snap @@ -0,0 +1,16 @@ +--- +source: tui/src/bottom_pane/experimental_features_view_tests.rs +expression: buffer_text(&buffer) +--- + + Experimental features + Checked features are configured on. Some experimental features take effect + only in new tasks or after restarting the Codex server. + + +› [ ] Bundled tools in login shells Keep bundled tools such as ripgrep + available when login shell startup resets + PATH. + + + space toggle · enter save · esc save/close From 7c35e1551f72a96baba1222ff02b0cf9421ace75 Mon Sep 17 00:00:00 2001 From: Brian Lee Date: Tue, 29 Sep 2026 23:51:59 +0000 Subject: [PATCH 14/24] Support explicit cyber access programs with OpenAI API keys (#49406) ## What changed Forward explicit `cyberAccessProgram` selections for the built-in OpenAI provider when both `api_key_cyber_access_programs` and `api_key_model_discovery` are enabled. The new feature defaults to disabled and can be toggled through the app server's experimental feature enablement API. Reject explicit selections in OpenAI API-key sessions when either feature is disabled, instead of silently omitting the program. Leave entitlement and model restrictions to the server, and preserve the requested program when a server rejection triggers transport retries. ## Testing Add app-server coverage for both feature gates, forwarding all three program values, and server rejection without fallback. Update core coverage for API-key resumes with disabled features and custom-provider omission. GitOrigin-RevId: aa639afb841aa651b28f6f3f61198299372baa2d --- .../schema/json/ClientRequest.json | 2 +- .../codex_app_server_protocol.schemas.json | 2 +- .../codex_app_server_protocol.v2.schemas.json | 2 +- .../schema/json/v2/ModelListResponse.json | 2 +- .../schema/json/v2/TurnStartParams.json | 2 +- .../app-server-exports-experimental.json.zst | Bin 160124 -> 160155 bytes .../app-server-exports-stable.json.zst | Bin 155901 -> 155851 bytes .../typescript/v2/CyberAccessProgram.ts | 2 +- .../src/protocol/v2/turn.rs | 4 +- .../request_processors/config_processor.rs | 1 + .../tests/suite/v2/cyber_access_program.rs | 221 ++++++++++++++++++ codex-rs/core/config.schema.json | 6 + codex-rs/core/src/client.rs | 12 +- codex-rs/core/src/cyber_access_program.rs | 51 +++- codex-rs/core/src/session/session.rs | 1 + .../request_metadata_tests.rs | 2 + .../core/tests/suite/compact_program_tests.rs | 38 ++- .../core/tests/suite/cyber_access_program.rs | 4 +- codex-rs/features/src/lib.rs | 8 + codex-rs/protocol/src/turn_input.rs | 2 +- 20 files changed, 333 insertions(+), 29 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index dbac6c0f66ba..458e63efca2f 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -762,7 +762,7 @@ "type": "string" }, "CyberAccessProgram": { - "description": "Requested cyber treatment for a ChatGPT-authenticated Codex turn. Authorization and model-tier restrictions remain server-owned.", + "description": "Requested cyber treatment for an OpenAI model turn. Authorization and model-tier restrictions remain server-owned.", "enum": [ "standard", "daybreakBlue", diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index a4dc6548afbd..fb7684cabf94 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -10712,7 +10712,7 @@ "type": "object" }, "CyberAccessProgram": { - "description": "Requested cyber treatment for a ChatGPT-authenticated Codex turn. Authorization and model-tier restrictions remain server-owned.", + "description": "Requested cyber treatment for an OpenAI model turn. Authorization and model-tier restrictions remain server-owned.", "enum": [ "standard", "daybreakBlue", diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 75ecacf9c26f..acdc575506b7 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -6568,7 +6568,7 @@ "type": "object" }, "CyberAccessProgram": { - "description": "Requested cyber treatment for a ChatGPT-authenticated Codex turn. Authorization and model-tier restrictions remain server-owned.", + "description": "Requested cyber treatment for an OpenAI model turn. Authorization and model-tier restrictions remain server-owned.", "enum": [ "standard", "daybreakBlue", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ModelListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ModelListResponse.json index 8169ec1e8d3f..ee3ca9ae1449 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ModelListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ModelListResponse.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "CyberAccessProgram": { - "description": "Requested cyber treatment for a ChatGPT-authenticated Codex turn. Authorization and model-tier restrictions remain server-owned.", + "description": "Requested cyber treatment for an OpenAI model turn. Authorization and model-tier restrictions remain server-owned.", "enum": [ "standard", "daybreakBlue", diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json index c0cb6dfd4a0a..751bb8bc4e2b 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json @@ -121,7 +121,7 @@ "type": "object" }, "CyberAccessProgram": { - "description": "Requested cyber treatment for a ChatGPT-authenticated Codex turn. Authorization and model-tier restrictions remain server-owned.", + "description": "Requested cyber treatment for an OpenAI model turn. Authorization and model-tier restrictions remain server-owned.", "enum": [ "standard", "daybreakBlue", diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index 5838b537c5add6d35b98947d171ba1b0d2c7d420..6aa5e5bfed28d06a71ed127d174ec16efd8a3b27 100644 GIT binary patch delta 143920 zcmV(nK=Qx*;t8AM39tn|e~jY+%8dvZF5qm4+XBK7iIPG|#Gc)qM4h|9v4%gUI#81O zNEKlKexLL^&z>BRp$Rk0Fdq(Z0dN6#0S=BK&0LT0sam_B`JcZIL@08lTnn|rKQA(n z3WST5W(B_XkP5jAbS>99J#BwGTk`X*aFfbwz2?+%Kl!2$( zFN^NVqEnY#dqY&b(Jp>Q zS1#839ZUKaT>nmy$13j#eoxN~iqH2&hX_F3#Y>xUE^)11Y)`=VDbLqj{KMeM#WP?~ zz|uf@v7R1k;|RX2X}8era@@?{TJK!tl;Uu3EUc?XoK55=6l#OB!$M{fG~YDaObsi? zaL{mgWoT6Ee`TKS^2^z44YefnTVJ5Sg|gyQVHJ3R+x>i9*1n^)L$KBnWe^Z>c?~8^ z)NUbEs1BbsHdEi9j@x3aUAYWW2+5_bwpP*#rM?x7Xqp5eD>>&f%PF%kiuqpJY@LBU zJ*RNStG1^KRw8~zJuGL5A?Z5O0Wx8=wg`v_OEY)dUw);3V-*`M>T<0YO;_ zb)dw2f6aY5eg3}WIVTn>b)DFq!hKbE$iCBeE{e*?J1T6Qq1z14l*Mtj3Pdq$s8u4i z5BVR*T1fL@F`Dq=UFWXDC^aXm`rkV}AI6zp**mY-pyM!A3lF*ND!~+J2^LS|B@c?ls8XOXh1SAm@ zkcf!J0uTp>1Boyg*G|{F&bU9Rn3^S{r^gl3J|ZEc2Aq;axNx}maW~z6h;?TUaM6u zf3|bKy&&j4twKYRzpjb;bD65BUnWTnQ&JXXBckD>L+25J#ltvl@qz1%-WTpu@tW%0 z%(I#=rJq96Pw?d8{njIp!SU;6>#y+ZzOD-;#eu}qj!^p`4j8Qw`BiRNy|WRs)qe}W zEtJPb#!#W_P(|HQ+G%@0#xhOilqnQnlXXHFf1H3U73AU|ny5q(3)yus^nCz4&j153 z1vFsL;Cc4>Y&&7djh&*Q2`PfM7A2sQECmNZR*UX|K^tnUm0~05rIp*)V2R`R7*9ow z+Pl7gErH(-C>-nnV>gD(x|O=GBOY*F?b2A^DSJ+cKu};o!z!MvgRTU>AMD44AtZ|s zA#Nv*Cly;#dRm&r6&A*ZmM(j4a6cD24#R0CRCq_u^b?aULmhvl;I3+E%dw{BO_Dl9 z!qhx3&3^RAYi>}4xQBK4z23MO9}-i=ko}eAPe6|Jznv@!g<>>~_dg5pe9VooooqzK z^J8Vy7zob^EvFAFmkMO4tzm5cNg9%!=rZVbv9HS6iVwi$!$wo*zFCb5L&EbcZg+@T zEbi#n{z9ziW{-a>ewRTkG27^o>SDyZru#;YGwNsm6%;7VBE~gxS^IDj|FV#DTBfDA z$o^i#g_h~0o4!8Fds&PpmAkral}MrkfDlosd@7U216|pDgkZyx z504g$1`|ZWz$r8c35j7q8VH6Mh5~3nG71=Gj3EH{0N#HD<~LgzG3P-*#+8t_;xZQA z8o03d&hzt1PYlV&P|!!nS_K%qS4E&n3Yo*;HV^TOZ${Y@YCitu`l(EGpQCQIE~o zxQ4316dj&T5XlE3B@!qIsa5d0`)SeK@Ld7|Lh)m3KJ6Pi#kX+{z(-|4;fT`Y>W3 zm^9)N4z6wL+!YnxN>_hOa6-dVmAs;rk&DfBP3Fd+rNcGCQ^~{J1( zCv1f|#BI2A&kl--NlyXk9c%%Qk^(c#K_8BoCM|!E2?zi(Xm*};Q0?~6f}n;$ZZjnx z{K9|qWRO#=E&QIE@$!bq&h;Rj*|BG#$AKbNSOPOr(GeVJQL?0~cn19Ft41COdK!Z} zl3v{gC7Va6)fbj|BXNlNlXnV;oY^U=>M|?!?%Z8JWdl?PRBE|r1hxpb!n46h?yyz@ zmNs01D2Bbr5r6b3mw>K42~{tQmq;L*Abfu>rq9cc%o15tV~&krXfysbCnV_GEy_81 z$VT)6$X}-f)TWWvdB45}D_SqW5=mnT zr`zn>XjBfXVto|1Ppj`SKi@P=E%1nNqI!^T|v9*|2Q@= z^e_f?!vrH#Jn&+j1*w2VSLt1ybpZCx1w^&NQE8&xcKyp!GE6^S>6-|*i8a415xXeQ zMls82dLGTM9^8Pkq&Ht{e%5{!fVY2w`>w7XJExowAt$FRf(FnI$)Zc5D*1Keb#(Yn z+TK7Z2-fZ&VRhrf5}>32AotniL^lp0u29*F7sd$>%JNT}ylq7+bTwy)&|z8+j}A)V zKiSW!T|x7|ozn*Rttn4zD@6(q-20`Kvp=S^6&vYUy|C@EeV`ht#3j7qVibR-%Y`bR zsx}1Rg{KzE|D%yW>R)LnioX&aZlXhRmflD5OgnlmfzKb6WF?%9-$fqCUq_4Yh$^mr ztx15wrK@#3bu$DKK*y+RJfgBJSier@&6cW;DH0xYXx>KKPyBqCs?j-r*kah=@U4DZ z6vY06l{u)PWw8PbWi#EsP-1^Qe1*ZJ({6Ygz1@j4N5V5!e^RT;jPAyxEV}a0_Y1Y%rJeJkJrjcX8B-?tWPBv6KT@UE9+*o@Jy#ENC<<1l|=D?<>a6Jih& z@{rbkC#zj)GJ&rxSKz`?kx$D-gP=Fg7jggs`=Sh}Tjq1RqLXCNTrsuuOq8me`sg{8 z25t@Q7P^Vdh>;=YR!8{VU+81R^>K7e>0pc+8lS392vZ=as}V!pveb;S$A(;KUu zsk-o6JVicaicU@@A=9igZ&n^1aBcZ@(4exkKB+ocrzsH=09dQOQ0T!)m`nMJS7IdI z^bqF5?r*pnGQ!Yn<}aOnumdi_D+C?3ZA$efZOA^oVrn)}02hC6>H7dXIx|yjk7VZ5 z$6mEUtt_IM911+u)=gq;CW7f`#QKE}AZhg8=X@DOJifrk29NxohBFU6lmEk{ssJzKmMK@u-9}WqG3zK@8Ep7@9?>h&AR}rsHz>rX}-hW*tdamBaZ3@B<{3SRxaBx*#H%9PajL+KAKNfKf}Yi zhM`xhdUzK(!{@f~60TBcCE&s`j1@-nNUeM|DP%dMFd@kmT$#4;8Ld zAeUbzsW@@1Cl@kSzsa?7FU9{f0?$qq_8L%BJtLsGJ&*Bm*vdNKho=^Iwv;sm!$f+^ z|F#A94SB4gP@1CWFb=z;aWp4&OG)2g-j~{qou*BF+w&;IMl^r~~3HEl|hA zdWj%b+@~sJ^*FcZA8n6wpcr~{b5TtaARIUOEpsR;=k${>SN79+`SkP)ZGUNQ1y8l^ z3e;%)?}2xx;3gTr-DL*PUxM^Tt0L8)xGhywxl5?Zq7n8^@o1>li=cd6!BR?T5sH2D zeKda&M;Xo$>BcSv8k>t4&`DZ%?KRZQxSF?AmQt#hepJI*HQf@e#CP_W@cgw+F<~042oqbd0$oo0}1Rue26Jr zggyu|#rFh7P$^4L%)Gy4_Ql%>o=hwZ@Bnc4j|zREtdzFLBOo-8-bO?uZ~KM-bIN~0 zXjPq$*Op_K&o$RJo

c;Du)iKf@Ehz#}h#a9RQ=@Tg!LxL3ppFalP8A%i&yj%st~ z;|`Tn^n`iB;PMA*bg2Hqs4d_> zWMy%^e!~ov7Q=Fca{6meIa!m;Wo3UXam(szGW5wu6OjZ!$hx_pRtzPS*r|*+higFC zpS2ne$bLV?`nq+e@yJ1>pc3GMC?E!*1qg?%t`{e})NI~DJVR|?z+pDW4B(g*o7Nff z%WE4ABu1ta2M%yVvI?Ml`JK2mtXB3(lCP6GNBXtirN3=V%l3|4yXbEF^#*@SZlT*l z-0kcpV6P64iI453LH^7q$DjNZHe;Pda3Xx0hFWgifD3T_XzxO448Ejql49E@iS|Bxok zpU)D%iIppKY(i8q@qj=dTaPse^TJwMLnZ3-u;tEI4uhiz+BJ`lI91%({m$eBRIh#v z%+^Gm)4OW8z+FuT|9i;h}?`++t|HmPiD+;*-d{S6;pE~o(8LuDZehv_= zJ3D|Vw{xRKRQ}E+fy&ru3xHmOi!;c|^}Xx6&{@*4unrWYfFXZDn{qBB%)vKx=5lXC|XT{CmVD#U+fpn`K&1}4sW|YJDqKywPNfbj+22#zB2~FgrXZ^k^2L!oe<+DtT zl)Cp}h|DOzB~;K{Fyd{5F;12vFUh?+5?O^mKW)|Ls%lnJ25B9?_Z$cux zw0)YHmR-y!zMSkW$hkNC7>C|(#Qnt5!^9SQ@VkGHXCFpg$YKcy@CO;B>ov%6(U{c& zv;XI_jAt-Nwez3aad1X8UEA0bRs2pzdajvv>yVU1TCvTt1i03Ht8|3seCvticxtgbg2=A__S#O8h>+$2Auh4s#^F%#1Ky6 zxU7FqwWwWZAis6-o|2T71fc73HGfEoKh7y*&Z;=Xq zE9l8uO}dFeo4s}@ifHW^q1{Wa96m(MG9;kM5c~9TJRDh{sGTYZ>LfkLV-j%XzOm#Q^D6z-lC>*|7?p zSQpS*b&B7nZ_BIgRC`?o2$I(vjRqQq`{Q>QOcnahL7@&ui?$``-?2ccwWx5<|z0pT?u-UD)Uy6Y!`n}9D%zeE>`_ngruL*R$TZs}i@be$&dw_q4iG%iX zXCzeLdiL6R)wBl!N2{j~6W5|8Sj4~cO9ornS4(qZ+}BMhQtl~xBye+nCnD_FM-g;x ztE2_@5*aAq$3!ts0e{bAA5%@M8sG*5z+KgcU>XoF+;DFHDjA*)e9o>N9^Tlu4R1GnD-L4OGUeqa0tT&okM{%GA(l8Y^uH0ZWyz z43@90RG6->H!iZoVeW$IH|x(6cB33?N8!&6^g(1Oh#wq_EH^NS>47U54GIV1P!^{_ z#Fbfwz!4`qG&g$^_)+E77varsK@HjT$4Tswt7+1nWk7f^nDt(lp)`N!1BfiJSBD@H zS?I^5r!|}VO+!ZFn!6sr0ZTuWy*Iq5o)c~$T(!9dvj%E}q%`^mdQ{;CG*D1H2Ay;r zUH~RbZhX>bzQ{>xBE08{Q>?Xs)mHW61IG9{6FNl#I2(aSh-n?}AG(TJOtpeagGo)0 zcaPoSx}$AUsD9Cu2Yr8hHp{vf`afX)ZY(=gvJd?`wg#Hf&8gkMx_c47;^p$@3yr)d zot3*_bI#`R1nD&QV$CQ;TCDgfxFY7;Su_ncRXgJ5*w- zNP|%m*8GN`)R1nNEXqxF;LPdghBm1V7`GiV_EuH6E-!v9*}L5J3~0D|z;Scy-%~s4KTR zi3wr$cL|qs4-kLw&mkiNHUi$K2-CPfpbCrT*+3b77uO+`5ylFNh+^X5k-e+u?Ok4h zikNYVPNRus>X$Fv?z~!EwcyOd_d1LE?X0W;))>PfQzCwoXWLIqE(t zuyABk5gDT`0@2|5{zp^zVRX3m2g%ePJ3>lNLRZ)uB9h`A_m$D=gxGo#1Xii1F+g5U zeOer90>nwN3U`+=i7kxbonf?x74+bc^96{bKt;bQ}p}{B+2iv@~cAd++ zTMrH7^6q=z6sqoQ&obVu;Y;9i7$B8;Kp^o2OdtW!0aT{4VUQJJskv-np|(U^odmsz zL*4K=ju83*yqz~EHHr+wvqfK!f$(zhp2VNJGkJeZ7*6`dTI79pG8aOq2jSR_choa?ak91&=Gw=~w z#_Jg+s=-k_W^)7@oK+qQL+damG8Qn7z7Wy`UL9@w59%Q*DF{r2B{lN>o<;DB-*;Uy z2{tsw zginFMH}n6)nhXXe`2*P{&rBGCU8GOwq8Lg0US4JHa>Q}ve*XY#1a%`c`xQ2XCQyGr zl@=L?(euhXi~yiZ#T&utxou*cO)$F;0MUrA=2s`WmUcBmngXGxd|E43mZy|#3I34Y zoi>Ed%#3!shXk2U)=wF$KZmHH-`OZcQwb)O4q*|?_$Ft`AXQb|eH=t1BZ1!K$xUl+ z&L@}49dbD2L?%Bebl`tB&@2bu8XkWjRMwYrn#I25#& z1F9#YRAU13PDv$xzwp3@oPk_pi7hPK!rei3>6*h-a5Zh;6;E4~>kSm&)p>uUGX@x8 zAkO)=vu`vISVob9p;6Ndau@AuTA-r|MK+7Ol)GWCDoN>*QR~s5{?>wf=+6%YEkxbv zIR@}VhaL^w1;~^}5)9y9Wg`|+;R)h67R5q`kx5r2?#+tyihN^@vWVhVpY=AtDUF_{{0S3L!rI8S z)Rj5V_O~F>C#~Qb;f&3e%3VV>^OK6T8;`rK_(|1hp3(ylWam)JLgTdmQ_pc@Lo<^3$WM02mF~F z?%fh44WRdh2gYLno-i6Dp7P zjHpx|_2hUVNkbn2xp(aH1}#j+9l}PtssWly)?EA_Srq=pcxMR!d})ykqoX3|`h5(P z$Lv}H)l)+BZ{FYF$l$Rw*Ku@H$J+Id&oXr16HtFMQ(VSkl0G`C-P=6AoTvD~67}<; z{t-s-`83RL;6X*pA!n4>{#5C?ZH}J8j*jK#3M>4^g~Cu!GZfALxkDn42^V$lkB4kG zVqJ zT6%u~g7?q-on|H+l6OiD56Zhu#)f2%Q{}i ztDmY!*9K~#9XYJl>1&-JhZ0tVsU^X39_7TqekW!HRHf%=8upC>E684;DPzlk6>-KS zZ;_yYNgD|FqWFVpI7u7nv1}d4cz;G14jq32QZ9pT#FT+0(&wsy2j{C>#$itWaN)?> z5onh)=2IRy&|oQ%#5gr6WPrzC--Q;~9%fY8KNguqgK{gH`@@PcayD*yRYpo}9*LKc zu!~6-JCF}*3#zpZ7$zw;be<8fsM4GBPKw)6hmx(5QM7;D z-TE{Ch^FRUftEXzBLqD=u2nb{C^&eX$`*X8w<$21SZ%L*&K^mVRy}J_Vw!ypf`w}% zI`63AvDn88Pig>f;W{rIm-fL9vcq!9ITs;}RVzNiRB`V9Fc2$NN>}S9WKn-YbbX^zU5g?3F}^PgP18 zi{@&}l-vRrnr+c&g0HL>`UQU`WGoy7rlkS!*R1E3!Z2?B*)6^F_MFe6mRmk7vS{=X zQ5NIismfAdZX|+9g*^ah9>{N(`lAjBUvLXnVlnPMa0*R%aH?Vryw*OE5s9|zx03+N zY31cISr(O^;L9jQ@RK5N%A8N2tw@5OsaVE&cr|y5%&F78O_2O8I$3{zumNe%$5a90 z=xmvo_zVR&x;#|h0RGQ-NXA(HsXhU>;32G>+>RwM<@v!#(tH=20CszQwJr-OS@sO2 zSKR*6^xB!3z!9Pg>y`yeOo~(u`M2u0bjQegYyI)&Tl!db_E0VbMQpA}X4>NeKPQxj z9$26P==~7Ho3{6E7Y%>jWJg{;4DOJ&4w`rB75DihG!q{6jWnk72esfK-Wu?s%dpnM&$D@+(_mW?7n}gTL$^3YSjO3?K(xk2kSuJ zlYIB4HeB}WfiM^c3Pfe60?Ul&1O!6+*(E%Y0LSmg6yV}xpZj$uTX}G-3r<+~`(8=yHkcu|6eo`v&MaUX8PXZ1OV{3$ViTZNdx~M>^f-%0T;pcou4S zCGoQYz=eNyEHbzx8{}9Cjf}MvDqTaJ2Evf^mf?NITj%@e6-zQCTh-rBh`LZdikqGewl|u90}; zCQ@?Wq(6d9NsyykOL>}^1F>M9OhuOA8%R^6qz3tYbY+vfN0a>%ePt7hXYhDn4qbwH z!{SmKzYB=~kVnHDu=0ut8s6?Cn{J~_xpfLWKyaZa4we`R@EHoJT@`$hwpH~E(oZ1b zR1|+^lzWg7(@Obk>kwS{qt?)DBO?${afZOR6wVisy<7`qU0X?){X*LD*YRrw9Nf}L z6i-Fz@Tqp&e0!IN&G=NaH2r>+ctPiPJgeO|#KqDXub_$XQ$%Nb(b^!=flWXkfqM@w zyA`&hxK_dzS;esj|FtAl4e$JBj+xBOw!wd)5jVL`xnIp90=v1#%M9`q>JrtSVO|! ztqZ5kQRBdwqk~2e9T04F?f19}g0r5HJF4@gEo+gAN=h7dwdVlkt@rgz zFBOgRCRx1Vx}whC3(aihHg$keqS^AdXXH`xUt--ZA)hkPeN{*I*?j_N8)Wd9>8+9` zA{lSHOiiuK{MvmIr(K^kCXFHlJ;(N}Vlv^26UzdVto62F3I;bCSU(FlR)~L*xg_^z zbO`9UFBbZixOV93np1XoPexlA2v3O6z07vJsfe59fDrm=kUeSDYbocfa)}t18b!() zx^oaslEN4v8ir!V24z%)=s{y6?xS{d*R1d%Z}nqf^psTqiZ&~`a3ZsM$?NdCe%uW3 znF&6z($9&9=Kst8=P`Kf<`jSOGcRenDy<^tgnMTak=OWc$-;CgH^HvnkpBir;!nge zKuD1{AEU_OaV7;$2|VLc^Lb`33TC%|{GqfmJakXTm-aidvI_sJB#nQX!S_zbSSC?s)aL=X_Q77Xa92_W)3qzR?&p6TJxL-iLr@NJ zPcM`__l3x1E2M1pg~R=Uh!zDwG45zm!$o#R07o`Wkwf79JD@+IY*KS)C7+NTiU`4F zpk+U)8P+(BYAn|EQ82nkJ})Ofx!6x}GTxoA4^9bbsK3{Vti=XD>1)Q9Z(6n)P~(3h ziLjKWLjJhniOeBGP&JBcCc#zMN;k{$^$Kd#!BY`>fDuA`p6x(G@g zP+quxB{_UMgnkw^?_R-D#>k8{f?>U=It2&_98Q0NL6=zWd@L8NVa$lW zdnYmiDqGgBoPr?TP4x#CA@QL;j)gpvI_98$Az`Y80#AV3xfFQ(f-ELJdCw4*><^az zV^leCs#k=)1yX=g2HkZfp1I{sP1X_Y84s>0lBbJhMUK4J1zU6VHE;$K$c-?#>wnA6emLww z1hH?0HMI?3*zkNI(KMNuX)0EDjI$Fk&bUt|IN?4|J8$Y5VFm^>_yr$aN^N9#WFzYr zV`?s}hMwxqm3ej(C^!|BjIB1Ef&-$rx0TOz8Kw3LN?3nXE6E6}-Y;8B>;GRkw29>U zo>tkhqJg)rKMuI6P>OFLP`&izx?poufqZ4uBmNS&xrDQt1E`eRVQUXMT4FUew zs1LD(Gmg|aVZDh*D@9g&qvklNg{i1-5~w_D@K^A(8Qe)PROHN3#VCStz^2fhyo~ho z`|DZ3OB#R3Bc0FZRtrUJRx2z_uWC15UN=d@LBS)p-p(6K z`i1m^qzn%$5JX;)lG8M$ZYCH&rton_%ywtmc3@QXjTaE6f!Z(ttVVJq8U|FL?Hdnh zPh;o64>S^ZET<<#OO3Wg=-E@X;YJzMbUxx!v5lcz`M)^!cJf?%2XT;E!?lzZnMLFsP1JEExn-yH) z%#(kw&?75djeOzF`kf1;*=|BxtBp-SV-A=a@=_WW84{fvF85+6e037!pznROJ@ME_ zgps%Gt(LoBRzZgw_T(FFc zUDCp5r4Z2ef*TOawoe^Cduuo{HUSMy6y_~4w{VkSnKN3K8q^nNq~`ioXEu>bZ4)z0$A^@++^?AB174!O#s2@;%T~-5&*F5?Y*yVqb zHCTEEYxMT@vD9c($gW)lb?FurodVLH6iw-5!s$)O1;XV->+gfj89Hn;OV1C5xx{Lo zJRE8`clnao8k;q@p{B6#-D(+oX>E=dFL~5QJ$%XF5aP2^Ec##>dkWR;lS)e{UgS8K zG_sP9Plq6bVABrfI5#3)Ill&u&0ab~Og%#5xa+w+*K#N6%V}jqfq>i}DP} zUTsNJD72_5Y_?&bys0;KnK6Il0?Y_D#^!V#W#FLbM{q2ef0M{nCmz*>bz0(+1_xuH z)1%d>Nvt~)#G`5;_G2F+hk zMin2SXJUqug%Uzy+8Cs02tHO(Z(|o{keAGfMaYZ`8r-=NL&c`ktfYU~3YT+5GlDNd zOdRufn8lV13jbtDMc+GUgc@j5?!GMq)U25^cC zA$kyyfEX4@*61-b6wZJ44jCi;_-Zz6PD)Zvod-o11;=OzuhpJcMhJmvDkC76dYkzA zbLgVt^nCk{opRl^KQezV@*4uYE2_)rfg~UxOw&uOntV6Te2Y2ohR_-(z|2~3Bd@YE za~z=oSqUs!F>2Kx)r|dhZStDkbh&qZBM6HNmrbgK(qr~5!g1}k0-)G7#&_pZL)LoD^+Of3A63UkHQg zEsw5)C)`!qs^5PSj5rw1UN{)ZNrq~Dpm(9GW#mP!pBi!7Eb)fiX?);F5YU|;Lx7L( z2tNS!t~onwK_Goy0WxN5YwTv!I)-g1gMl_bbS45I)U?zkHl?8sGP(yk7F1-AIT&Qz zAc$~>ogYANWH)jpQs9W{6WC_*x#oHwipO;A(o2!0OId$jJ3{IsVG!zYCKvRsMaYos zS(29|lN>0iK+7S?k1U8Nxnb+sx^J)L%=^D$D*Z%MFcRYIu#RYnx+~078A4M)2x;&% zQIf>Ww81vY7_;GBtQ@*(K!_W7gaA_GbN7|;L4#mk6IOpTKtpkQWnCw7xK94ssA5XB zS$LNoqf39y29z7icRQ%$xVo^ZFNDCN|E}eHnM2|==R79`8*OJiMK6M=6OAW|UlhK( z0Qe(YIK@_&G5|fH(>^Z&mq6apPs~diRa5!8B16VlJ=jhR_w$4AqYTp(X(Xdoj^VCB zak#fC@rD9%OOPEnk^hC4jyCcWaa`R9Dx!x-UXXwLVP||RADKJ0Y4kpHd$iPCG-$t* zFv2S^S&q3$z3KMmK8HNj(0wA^1g}?rt0NJt>odkzn#6OVrz-(Uj$hFN9hnzSz@nB2 z4>B-W+~goQy&7Z42~~5CuS$VDc|)fxIjAOyq9_5o_5H?i9D~8qOFVRF2ChSfy-eBl>71_xN$O zKv``7MMSu;#}8as#Kq8aL+s70HMz>NsvB+9*i$Y_G}0tzRM6ABjf#Rh0#b6U3(Ye( zXI@gD4d*h8cY0~Kh0=nMZH=Y*sDd`h!S#Rea2Y@wcKwhwe(r630`0oIH$BjGHCp7P zik1>6s5+cBCu!ly#3yQe>Z>t2PHVSX>UIkC60YVAAVenx;1IcSSvrbi%g+ z4yYWA%w}(NwF>X>j4jp66(1XfEADx1+3yY?2vt$%KT&R75j=|>S4gxkBc}8`nFoKm zN3+WDnxroVw9fh-a$P`2flmyhpU9&Y0lIpQj2@peK6AnBM{4tBJZ9xKq*NYXAUf5> zstvMs*?2w!R$(CJ7UZa;#N$3mQ6L~>(gLO#>k09UkZv4v7xr;x6Ime8sXm6DsHLZ{ zr@I+JTgZRG=#DJB^qg<^M_~Uv0)N&BL08P zBGI}fjar?gj7Z7oKi_ytH+BK(GWUUn#=X|wY)_K}n3x$X#VSXPPZc$z;AZb%R^I|= zW;?Wz*$NB)6s)D!;jMh2F>0#)KOo2-I@9IiG*H#Xq+%3#MvDOcH){9^`bK{(#_I{B zQiJuEeiO{HbPM)V)rw^M#7qowxIip@|4P(fiTCnIMu_L5<`4r@jufoN9@P`Z6<5hA zGY$Vii_3X~6hAXsWUPZ!V*7s#94y;gh_|m@ z`Ykky=Jg%u^ESeS#x^QyB-OKT8^Ss-5ILwRAtD(tv7yZLE7R^?X0p!X)d>RHfXO~R zC)DNj-pt`znhPxuuFSl>N;55d&e?5$?_m%MXFEyje@u3~Fsv^#aj^xucq?ggH?Q#K z9spMVL>ZMIcFok)DW!k)x26Ui>C(*scoTwtkCNw&3;#sZ817H!pSs+}Te0m7^sq5K z9uqJWAJ~%_c7Y#zf*^#*Y9|c-Ls;LZy(SZN`Dp^XxUzAGWkFx@<;ZlqF@uiBw18e2 zn;-@bgquoKMm&ogaEBtMvu=VZtA0`cq|d5*=a_e0E@YhgfVO{X?eyqieH;d$)>cHc z78}$ddQ8E&8FQzBv$1smN3;utl`oQ@VpND2u5eEpZXL}29+gQ41ZN9p24X8M8IeXn z8Xiqf0d!S?oHx&c*r?V-#CXvtZrPTD2)Jehnu}SHNz~#*n4iEnc%sNHl>wv;8BgG< zy}6l*eT5B?Rv&*^IggTos)AC=M9kk>jof9d7QG%nTwFEryh%jg84!>?0|NLNOL1if z&=-HS#TdMoW*YGTF3ZjcTzNOdJfl?ZCT-1>Ie{+13#eKyAmjmY=oV``oE+OJ#MvV| z;uHsVSpmucNz|?JC&+qxg9$G#H|cKBsUAYK<7w!TF4)M z=*PKKXA228R_+o|2-WvrTGyGKyMuB5Q&CL*C*}tug~6Yyc1_EK z@=Ir}CO*1`fs{p^Qd;9cJTuSc0cabZ<&N(57ZPH?7^}(L@`nyMwEj`{*|9GoIRu@2 z9&UesROJDZs6G-8j~H|O$x#7t|E*D-Uot&)0T$AOhIo~nH^`tJWt({lU{XeKrf6e- zof#^m^I}0t1RG&YwU0x=9|wcUGO69;7MC{@__EO#YyR)=7*&g*-)@=x;N*H!-C_G# z+fk$Cm*{XG!XwpQ@jrw|m8^ERH;%Cv#|wYgwLz0;j2wZs!FrcB?WTGQ`eLf}Czp+5 z+i?p`fQq0X4T&__rA!&tD@qk+u||v-ZE7||U+@qiK0%wcTbl+nQiDemX@asI@e~qUMDUmzWc@i9J zz)PuC2cQ&kRF!*qgoI#QDIhDxcSNiK`b?0MgnBH0%>dN^$N&t~Ly8`e2#Sy>5lIqJ zk_R~d^&wBW-!#b043<)Js{3b)TK5sy5?>3{m#Lt5PvWD*YC7-}m83z;dpA^APE0>v z`Mj@={a(N@>^o6$TxSioFH82;{kL{KFCYedY06W@lvw+o+rCuo@(RC#GDtsE3GtpX z2k!NM()S|)3I{}>K!AY-&oh867?K?CSf=#Lt z60%3x-C$|OpSi?epW2{75=4Uo2n5RX;L!eAvOb2-c3>%m>zmkx;7UPJz@;Rm6m@yE zB-aQE2$?YIIQ=3|yj~CnIU)*cF5g;z!#&Qu2Xg?Jg-l_y1psVa?z^6Lm&_UaPSbZXdsct;t^3?r!&2}$6y}lpS+ai-v)cEv$J~rJa6$W+$xCOvjBcV4rsXR z+nX^e@OLTT-Cz9ok@YK?l%pfHybUsc2W&Ut>Gw$EtuUoq@w=w#(E_;gJ8YCA4gA5Q zE_m+eBLsiz)mZ|jv0tF6$*qOQ$x5E{04YG$zc5*)x5&ORtv5N@pt^+1CaAhApOwf= z_{p;LVJUhsKk3j1wXItc)eRXiFxuAz5P#XVbTeH0f@B)PuVtyX$0sQGCRUf{3z=uR zf2WUbPTfT#e8q_?A<0#ZK1a!MqSAr2B9$-`dq7^#2Qew3sxgkx zHs>y$u0c|X5?{la6E!6P46sDu`iT01ni{6(0mU@*r_IFV^uSxUQ>y$$aP0uK5>!Ex zLlHlTq{x;C#`8rQFYiN8)*6K)K)?WGf1sG7o<}Y*#5)`59%|CcR}R7dcRv>2Z5*L( zgmKLgY+l}pChvI=&x?Dy*eM-HGSBWN)BqecHv%o_%Vu~xDxsx_4sC9w#I zJ?YC|?s#=gw?CiDwhMWjJm9?C^vjGXea)-^1e*s+y#S7>t z5ugZGn5y^!ytHo9i&NWSX@UKQZfD^48}u zzW8`fs@TBJI~3a}CR@#WM8J#xXC0oir(Fk`km1Ddvea;709T$FZOCH$#kPu-WBEyq zBIP3FF%u!FSrS&_&mUH@x+Lcu@i3@r6KvVk-j>0O#HFgW1*nsLfAbPcLG+O;nF?fp zP0r{5OHC>Ym767G5E^Q--VuS5>Rd%;oQ`3hfUyudF${j5l9`EsW-pT6HN_VH0GuE7 z3aR%y?h7sVGNVWNs#(^>bU~Rt3+IX&n%Z0~^kINz1j@R9a$Ge~q0Rc(_IwaY7G;<2 z7b2Br*zvJByjOp7f9a4X2?=~RJRk$$sY32N7R+%HzP$`&ktARkX1rK!`Xnrk{0+{5FDE#BrRbye+IE!bpr*3B90)QYXM%>@R`e`Pp+CkpEIS8#ID zwL9_}$8|ASg`I2( z)osQP<48FPe~`(FFNCJN4U*|^d-*c>6=WDxzJQ?vK?ILFAjlL=q$xqU_5*+Zl)x^`h<&FcHdgq{M1(jhP-^>A8T!YApzNEo!K@!jK!Ld^$A@J+I+*O>XC?L70tf)87CQLDl!S5(Mj*cxi;5o`=9M! z-kK5We_!hv_DmT#{nDPAy`0RS2@gU}%zdC-+Zmgqc%Ydc-@Ab;4=(l@KExy&O@h<( z7XK>KvUx!b9^WAIeiGMr82cB*+ymr__M8a0Edy%O8L6}1GnCp59AYjs5FL>)(Z-$3V1Ew(>7uNmMI!23twT^G1e% zU06)>$q#4;X|O=C*R=`ZK`-<7lKiM4e|`r#0kK?;1Q9pWZl)}>8OGlRQ%Fh0M_AQ% z&~o|*rNs0$egQw}N#5~dEXhqnB~oQv3uawRv?iqx1u(w}9P#vGB?$9|uEi)ZeUeQi zMcQD_a4Hl4dHt4C<3oE}7AeP&oqcYWq-T>mzeHi$AA~-3PAW&q%daC$`OD#de_F6{ z=e&6CQkS7JkUb)U>(SWcjYn{#|6+fHdJhiNwvCp88_MiD3muDbXL?X!p*kNml zE_=XUTzSgs_b`|cB+5Y$mdtG`f2*;^7NCb)@GJ%K^l!xUY|4-fZ>0GUyg+BpNe3MF zVnoY^L`8{yzTZ0max2;}EZ+z4QRWXutFnIE?1@|s-A5P1%d2e_UPaOq;4UkY0hzRo zD9<~$@hUm{--Hb($4xhY!9H&rgdUEX?-7xW^dA=xbnEV$oIP2EYVheuf3Aoy{-ipF z_*p4&LezB_hrYq?a0u%UUQsgjKtPwbwzx-z-LIcV^YN~22l)vMzKxIAKV>v|Hrvo@ z*WvGBVY%+iv$n!JLM@GQQcw4J-P4SpOxr6N#wHV8Oov`!<4Rw@?b>l>9fhpkm+v0( zSH`r^sPxKpZ2&YPFBJ41e}%3Z7#-lAh`Vx~9Gm5=mtGpN>74+khcV}iEIyOtb#R&r zC~?m4s8y6UKFdfL7}l7hDXCacnf9MqF$i#ZY}>_sD;%mnFrq}|Vzp^*Krr;U=9I-O z-`&_IZ}7v+9*B|I`P3C~ZQ}F@(C{f+;r;}^6BSeZsMnj$LK*jye|TXdX(WBs<7BzY z;m|f;Y3?NTOdEhj^davIGI!;4>c~tG#P91?yXeUPG1(O?jG;ozk!8nnJcX+|0k-|O zAfN{)^Ars8`hjI73q$5!2MMb--vF!y45cmwy)_TpVk;Qo0l5%xi!xgAL7>)ri9srP z@L^r|O8!TLga=SLf50Wfm6UD(9nF8~_w++VN|spHib7v!%Zw+{UrY@k1`muQv4up4UV%fCf7bSONf{?P+%kefA(H_y z{#$Ok0_x1_8@f7Z#tjczZ9@JSv?q1*moRFJ@l)U_79MNMTxN9F^>+_ALR@o|-!T3P z2`PpGCfVchbwZH*H(akxwCOjx!2sn(QylFijtc%+M0KN=a#=&Hc&4GZRoEkiSk^)u zu>7!Y(jf!Te~2)Vh9$0+ohH=Lckm~pKT9lg@wL=(oem2bPWtnf5)nyG;$aob5427u zN(ctQpIeI5ZVgJ_yLA``rvS!A(w&!BrX^Sd6L$oMqnt=^*k4{8$g5vzpgLfAaK3!K z^)EBhu(h^JzXDAvXn2&^GemhFWptX^YNWbDOCPAkqbW_NFwMBWnw7HlYA{Y#fd^wR zESaT_f0ZbzTy0CHXceT{Zlms&thdtgh*yMc(Ata((pb6f_=|`f;xyI?2|&?ddc6cN zS@eOi8yHgS<5DYsv~<{20#Y-yLV}RynYD%Z#*{~u)wz2kpi}upMC6b=;-34vM$2$_ zrsYjxXJ73)HH*ChXKRPTDQN*o#XKvId=XY%e}FxYNRtwgW$t2wOP?E6B%@qDfJ?+; zXC;!v=xP;aFv>^*3yGyRsRK&1LO3>}MyXe5UTh%3&=h6`!1NC2OcNLe@>8u6W<9=` z99b&A`260}w~40?)+ZtX7jXdi1T5~!-k(Nmd-BekMKIUnh0L7mOmFWT1Z?gW^B!s3 zf4Q&|Z3~(37+hRA7GQl`&C?@KE&^vpuh)o`D;@Seex!3^Z}YdB;O%N)RBdE*dcfjS zvW#B!Qp-kzBgoJ+OJs*HFP|f6_}_mGHFK{_PwDkHLyFbh18TsB(*1%Jr6U(Px(B}n z{429|KQY3Lm?r6E)8^z0Xf1JFV{q2R zzfFmZUGa0Noax~riky1JMWmloUdWR2fNb%;CGVElP;Y3vB)1Vq@2am9rhsKurjHo} zk_Oz@JVK*NeouhJneqo*bcE{k7Tye=$vaS;etrx3@5&U(sjfRcX0Ala9B!IC`yh9?UGF>;qU9ZzMI=r{=0*q1S|XeRqg;!9bJ6}Z=QxtO zeWK7R{Q8bHt0YLSHvanLOE_dx6+oxQBZA_KhOg6hxbRec?`W%zq9VPvvGPQ^Q72&} z8NJ)Z!;=yhREsV24C@oze+{A62#_cuzzPGBNLvGzjLO=D$zSRavZM%TBwz`o z+1l{Sk<`%rp)oOdS(MUt)O8UHtx14L8ZQ^)_j)UOLD%x^GR?9d^ha8?(=AAh$~S-r z3d4PbGQZ$W#y4?t6Tq~aP1y4o;YEj3y5ls2BNEf$N5m}kg28t@YbKb1f33x;9m(l) z4V*257F&YWjE-2_1N4Y|##v%&Z2{$!;PJ+uHv-uWEb>M2#ioMbP~xcU<$Kxdj)M{> zh7{kMfldS+g`c(LxGlnx|A$(vok{al+lvSG1kkgL2+EF z{4RvtsYoog2$_EuKeg~;e`=O53dONDGC;k+EXX_>cPTohmvn-ZVt#f@Ynu>&F#l7* z8t{795T=fcdRskZ5yrfH!5`(RobYp!G8WTotF$G?f&8@8PYT)bpDX6~n@zN`hc#d| zan~Z9IvddFt2$}6lj_Ml^sROi$+x>|^_1gB4!sw$BGbJziYLkif1Aw#te+|D?A1SW zqFhpN8_%8;Sbf=cSyi&AJTAYitV^4R)$RIbeZvi}s^s+-f#AQI*CJjIBz? z9*tgnqM;&#lV}^L7oDPIB%Ov1Ej%i2uj0~{E*TZBoF>EvNhJjGe_|;}JcOeT& z((q-NiRjBz4!!X#jPFDt0LeasSwtxo%>+P0%L?rUf7D8&Zy3^tli+;ya=I~3N4q>6 z{bH$}3D(F`!Q{P46pc&Qi<6?4QQ;3`j$&(D>VoDQmgpB(5DWYNn7I&o-J=nrIWC$P zoh08rO{*(LV;bmz`?87BTg*c5tUYa2xGw**UFm2`i}%!&{YKqB@ol9Q*S0)ndtSbq zQ#!ZUf8fp6!|27hWm?I2L$v58;(|+d&ThuXc#lgQ74a(iawYZK*rKis%zF{ysLGXF zY%CTIo)06wGJI2HjVmCQkp*p`)*2@DMeM4V)#>xW9YD*s)f0m~qkOFwWT?%8Rc6gT z?sQiM?ds5i<1pP>c`nq<|4Uu8M;sH0P$F5@e@YOtjU08?@@Xa@y~|4S`+Z-!I+P^!T^YPcN$%T2yF(}~o;p^`sg(+S-@34f3a36(L4 zVWI;Piq%p>IDl7VAHn`T<-Q#Zov2Yv!)D~iv#lCT`sV#&`yg3{Wt>-N(mN$ zF}Zon5z80$FZHrohSfdXWJr^9e+GfEYzO;J&ktezk0G~<2dw?Ot>|>v6#0?CNbw9^ zC{ydlFw*_o6&4AhF6N}l$gQ&(&uY)O%ufkE=P4R>wxqumdCLLya`2S^ycW3iNBU_M z(d@5sus)T*(Rlv84rl44RhAUvdDgd0|V>mW`=U#s=$>4D(Kf9iLt?V}{b z2Ix{Fb5dnda&v|hwsHNFGUg%*VI;uZJ1Ui*`IEiPA-cag8)*m=STEF0$7a4Y=R!G$ zVvF3^jey>&n=LZ*2!;?Xbgm;mOJVKPxg`W-CHo1+g0K+b{uBTHL|yv75N6TtrR1P! zAxta_g(5XX*f#v3QA|+Ve=uf@Dkj8kL-Anw8XEbLMYCjY#-kf?KfoG=gioO0TVlW^0E8x=8qu{PSm z%6BMw(LW}$schrM2aprj(&!whUx`jV{e$u%L^V{$!R9KZxH#4d>sv$9F@3|TK+day z+f0)X0XiR6a0h!FfAw>OJn%VCfqe+-?GO=0%TeI_SHuHlEZgb(H+zVLeQwOpkY4^+ zu{w4E4H6HCl%Z#PT;05e@-z1K~K?cSIN&)MNGxf zZJ|^JH=4v$WtP*WUaDGX1r*vR6!`EYn@Pu~p|h!IcKr<)Ke4LsgBI{31y8J{n6a8~ zCf1oSXDsl~m6e}#n2MdQY^uF|b=5K~OAaJOgT-aV+xG6ctON`0LKx{4E@p9Z?%A@_ zwOvTmmf6-aUvNQm-8hoEOUd&Q)BTkQ;(b!)Rt$i$FMig*{XRs4E_6nX0tNo@5 z8bNIwespz+X_jU;Z=W=Rt~?)ILLFNwOMH74MBRCQUIpA7x)JzQyJ&b1m&NXf=PNEq z;~TCV_Ukb^Uk~ubSwRfwyIK$=f1so$dEV+~%{;F}-b6orKjk=G|4#Gi4k|75;{})Y^!pa+en+l&}upp;C6+)tJC2#Snv0wav7U zD})*gaeZZre!kf>xnMESh9MX%FBxB#prh953fm74vksD@ zacsUbZujR%i;}_efM6D-vzqvD{()OI`UM&eCLI9%Olsx~;buHjt8Yr9O1!~{%F|I) zI8cf>I7k+u_8>N&j|1@*vZZgMp0&o!l+mXae|~_~{u(2X5jy}NpSD&{Wn_Ns`XX(V zLS#&U9HmjgD?6$J*PGES!O2ySg9_{bVMJqI7xU2f`Bxf$QNEV^B)dhpL*_7&(B$2R zdsqm!wTv6Bz_3u?)xCOFJaMJ;!DJR>6jnZ^~ZS`~yh zWE3ZmprMt8??40iDrb&hihzXOk*%`LM1TSsJm@7{l%D$l7Y&aIoF(q1I2d9_c*sr& zZ$_z55CsXo5ZBa*qSa!GXy(ItGYFr_fBn1DR@z(*;2l8^ZF-y!fpmmF;2ojFqT`P; zrNoj*5WWB+?&JiOe%#`)7LW@snOCB12@t$uja^3a2u2Z`G$e*KY${;1-pIzsz)v&n zw99gd^NCc7%;?aL3mRGj_#jp6YXNNYhYxo+DyQf#s4{aESw)CHvkw|3-Vp?ufAj-= z8dD)Jp}wrL3PCzF;s7@2xQpfEaW9s4r7>iae@1jkSLe?YqFP)s8y&0VCR&f(cq#M# zBV>RQ5%BVc{Qd*p_}g-5nzJY5Q_2of%{=U1eQ_H?M%zgjWz;V(o*KBgMx|jev**K1 z@mKH@BU^6QFVD1Ick_>}UBiTTbz8Xs_<354E_2@_dr!jdkyJ7#&$T@6ak@fsn& z?rTL)>Ohh2lA$u-PqpFV5k6hw=3{LvByjHB-$mA|U=o1*x~-uyM;}d~X2Pr_TGy`6o7Iz8w^Q_Ay&&Ff3jzbPEsXLl8m9XDU()fT`nk#K5~6kYIKVzGPz5n!Rd&? z<@$Pj<>5l%>GsO$I@Zy=Ro5Zoxe-)aHxV!(kJ3BPL&tQCGE1T?ro2r4H1f&O>q|XI;oLQmB7`W)HRBodyJ%)1QO0Y*Ru>f2lFP>_ZJZ`!Yp| zU!UX0tx*qe>|;cWB}fGT2@N^mIw_He{W)pp43{i{ajWsJ))|mj_dH5R??T8sEby(X z(&TtP{M>hLhsb_Lh}mHrskJiThyzE;0p3`pdz?^p99);!G?ay8xnjDA!F9jb568J? zVeB%(6=B58jD8JLf0q5XbYM2+5mO@cDSXL3gWcM<@P_9+>q`8a=vF$O8j{LX&G#-NU2lw@jY$8e1BpU-X5)k0!NG5b8K!k#v^F4xPVN;Y%w&e zR;CF8CCgSoE4mI*n{vG}YGh_H6@6DNy(ACko>R6vuyg;!e+P9H29+M`Bf>1T!gO@@ z(^eVX_)9JP`sk1boGCXvj;Iayq!yb8T_Xng&oIyNvh-u<3{soqb4ktB`mhau>Xyyxj14o8eMxe=_5va=855*N6 zgM%;Q=7c$D3P6Ck}d-w|>@cF$wPz zf7jeeWvU2~QOpUkhgM-4<^=gW9^$V_(Y5YY`0sf58}IPhfqHA*Mvk4?lZQ=r-;mN` zlUwccAL2289~hIK-4O>#)NSn(ls{xZ;qk{6o_IQ{mRg4oxt;hoJD=?9yg{4zzWuc= zRS?PT#N{T7z-Ag(PV24!XkQ~FnkPukk)PJ ziwszrHpG%iVTM?c;q20ZQ`QR$q_oh8HuDhV42oluxR9>RkguU{%0|L&oY^RG(1C!j zO*sN#0yv#;mbuv=RIKRS&MI?3e?K5p0ESRGDIFWr!%@l0v1zV97=;e zP#mI`ULNj-5;wT~UQzlnc$3in9ZrHFzq`KeVu8jgT7hqfUoo0FT35%5f6C9+p&nns z365$pwp-(Q4%2P=(#K!%4UNrHP%37h!v-U4VE*Zy}SWgBO| z*a~#1U*^BB-4~(;6bi@>%s>1`M*)AX@L~iXa}62ictg|E(iIRspzfQ>Z21gj{5?h2 zj-|87gnGv0S5UU10J<3I0G{|vPA}e9ar8ZT$>MToS&3f`vFXm)3e`}R05e1gKsJh3khwwtD~SaFQ5N%Y$G0VtXG1y<7e}-a;;|IVCC1AnCUX}LC z3${tj#pao$@xB1x<6wyc==~>g!C3?)oj|BEUl!*lueJpINsLa0JqD^^HHj;e zIu6nq$(3)(p2sC1-4qMA`OhPHw5Qx zKMfOOe>*3?(M2$Q%??L(S)3>!xkq4DGofcZ`i-chr7NERARbm_1ex^=EI|E0b*AO6 zYrlBA^3+seECJMQ0*2-Ruq+u+7_M0%5_tK-)FzhcCAZOm*1@G# ze~^evXV^dzop&XpU99f_REAhqjus>(*j9<`tSe*%`a zWnIEN45RYnP&1eG5IYTcd}W9Y97w1RqJ8aus$Y{t1LsLr+t&e{TXw}=PeMVMK9VWN z>b3;*f!(j4F*qwYce}EnrmY5D`c^Qk6KH(m_0D~A?x9`XoxRS3#p@63Es+4vl-Bmg zX()+vd6Llp0+tIrP60?mQN4!``&{h-yAR*L#fj5Uj1Y72q=Y9H>1Ltz-Up~%B>TMJSUIY z!@|aH{(TltT~|n|yxLq_Ly(xtiO~@xD@(*Z(AWWZBD=HG5aWWsMHG*d>1y?_Xx zF~+OmsXoJ#rG}JW4ys3SP$af{^5u}=sAV#$;u(0MOSCL5FJu%;jI2-u>}1LmKEugmId|Ym6OQ+e?AHJmz9an6JLj2 zu{{3^B!+%sB=jN2DwN9;sJ~cUxa&2M9xG#6#B=3QXx>u;cpN z{ElE>*bj0UD5BLy{fS-%Xo;3gP?2k4(W`I=@~>thqG3g#mq1rwR-HsTFYGHKdN_R) zrtN!@N+Wwekx?R-YB!A7(*HMoY_$3Zc1>g{DKgs9kE+?;e@GnYt#crP<@YDkq0x_f z*D*V98`!%i*!!Fno-p>Uj9kGX%;iv%Kn0sBQdiFztd7a)T-u|}5>O(;uMFB3LiF)0 zLQS*2^+qRLs@*_A^N7T{d9@n&P8X0dV#_W-aWGg6W36GLue(FzuBlR#OKRdy*m!U{ zYN%wQHCKw+e?a0#Hni=#ph7M4Y4&Qv?mEwtf3TPl#T(H9K=?YN&2XdoLghn@1?K32 z6_B-NrPi-ck;@V$Zeh94s~OR{{5{-iv%y8H2BVmCv6Us;EUO~`i%A=+G;~qm9gTs%Tj=~H=v63S%!FuA*kRJA5qWoFtD{f^*W7VeKL zH;SZGWAEhkOAR*$;Wgan^kzm8VTA#Jcf(8A4zq45Z65z4%bpHk@%CYE&#{X4LLF4n*d)c?^b^{Fk9HxmuJ!{g|FCvRM6hEe2&J zf4!VT4zQAZH;R957Ze>Jj8llqn_^ue23!BbdY}lNSA06#afa2h`^gzFS3QR|AYX)j zOGDtYd{6TV9LsH`A_#&e1D!=gy!w1D4&6dV$D=y*wKMZ)E@L?-zKA0km0AvW_bR-) zwJ9jge}2%x=?qht;HARQB+obHNUPa$f80XJ*EpO5-gS#%3`RX}#-4W)e4fVghQ}4d zie-`S?tK0xN6c)~-rZ=?PJAOIlB8IE&ZX%ZjSFU;_)@KW7m#l9QP!I%#%07wqD{>h zQGrV=BIUr13SQ;JF$G8#>a~3ezNP3o1QIn>L#u$OQY14`at>XmC71w!g1IiqfAu=( zU^#A=R0zIfSz``8r%RN=(}-QT z@y_cm^NhBCGzCSXppe1yb)}ere^Yk;t%60Df3bQL>Nj0r5%u5&yJ;OG7g2d&brYAp z$Tqte=RXxXSNVg#$DPB8bcMme){AyMIW@j&aYxK`v2~maypGu8;uCkB$J8c!j)Vaa zogv767RXoT$^KJ11_|YTa;=B$JWaaZaUMd@%rCzlx^eL&p)Ndbk}=6Ke_KsMc6sBY z$K8mY{7j%lHkPFou{npm!z+Ovmn8Q|qjA#o6xOkJb=HBIJejXR7k0UQv$Si4Cl-0{ zq{TU_0~!-u4_%Q{&StlAI~vD$)m}=jVbYW&vCXQAv7Q?O)4uLRU^Va*@7FW64h5Bz zYH2AxS(oT?UUdGr(o%pDf5}j9V9vyKJrCWa8k-p>@V&LpVX1rNH+%Sgn?L76uk> zE<7mRh;~_qf=L6;%WYHC4vg!#++{<|>&>+cm-n=oCmz_a&jqM^e?H}Z1~5o2x&s}s zn*#?mnlf`8bU7_E!=A^qLr>{SLwIw)F#r@0<5i5nKSKcxq}1ZnatV?zKB`ihj0bCh z;YZ;pX!tHHHq)llU=88Gs9Z&xtwM;2As_snpctD7cnw8OK~lAnT$F@$#9bytgf~b7H$!_i2O&@#I=a@lY}9{oGe9O=BX2 zVA#FEv$=^qo^69L#{}}^)|0zd!}N>l6CugRL(cOFV-~UPYVq7mq0eU88Ae+^P@#a? zRvX4K{oW|Tr-%vZq88IE@NdmF`}Xj@D|JnoJ1<@p2|WKs3m| zfBb2qPo7H6Yhp9mzogeH5lB2tzWtqv)Tiw0rz*tKp(=Y3n+94yHNRQNrn>bOd*#On zAOixg*S)gIvUFpmdt684%FBtCGm!uJDnactHiDw(uk2qBagf&>!P-Pwpr)WIx*cWH#^un*oWl?T*u$hnt(be-b~V#2P3SMq(3;MMA{83KzRKd}FW3sMQenxB5;+j8;z1v%2XcD8AjiRHqSjUYZQfARrx zq39CdOD8U98`&9 zF^)ILOF<1)bjn!|&p%-rjYcf;c5m*$jR0x=d+VREYOv1c(${k3_Z7D#{ zu9L$pg^^a0PbV+c0JLb{0}9bfruLnH%i_P}l`48D95r5A zF3TR={f6ArK`by$K|kF)%37NH?*mpAx=L6gW@`M(e;~Iv)$8C!idaOc^gEhHlO$*L z-Pw>$Fpl$n#Q{*m*K^IA+g>l&-Z=Jtu|$SGWYe;bSaGG!zzDD$>T1Vr!>RxqhObna z3$PD^B3a*gh?!%T8qmeNp@JIst=_YOe(z$@WYkm-IJ8txtQ~&iE^~&Iy>+gi6o4gubx}W z!%1_s-bN;adEuj+Xz_yf;1Nw-(o7Ik0MTDzC<@b)q~I`!U?_}^DfRIK9My#@n9iA8 z#ia1msV?@s*3PFE4x(6)@}#4<;ncb&3JZ2Ce;*vcmt&w7EGWl{-xSS76ofioL3mfp zg4CJ?OXl`#Gx*8)x5Hb~L_x7r_s7pL_fHnIV4k3bBB-B}1-z5xQ`qkL_;wXRMj%1w zlz5cuPpFsBy1?afnuYPW9()lO7BdE-R*N)(FWWxWOgWrn+owVdu9DROpDdf{QAqYy zf0+s(&=9W9`eJSzeR7B=X1M~p0=u2|FAMm`-tbSy?%_Qx=H#H@9zF0k9aFDl)9*DZ zeT*(+19l{?_n=K|o9sN}r}M;6$N*D46gGXK_cAono~?+T9h&1)=5z`&1g6jhS3iQ_KA zX+x!1+L*kVBPxHS1;~dGV7-I;%8Bi2{fR_Kck^k23TEJNBa`F|Hr|s)r92qDfAvM9 zj$LXiHq3?1yU7KK_3M_!r^>92IwtB9up&lS9n(?Y^%e4NZ=8S+sF39MYE{56t1tQU_e{?zr|3C0C z2Cem?W>nyXPYbQu8wy&s^PEE&B@>`ceI51X5%6e*THIx3g!re4oH75r--6DG7ct)v zOkaD>z+OYI{gTEOn&Z3*SHUBz5?zGCTYZaY`;t_WY~|juJrh`UoRm|ADG0k76URMu zI(+&5{w)_?y(rZs6>ydUf429E5H0+)b@`E6@srNy6pR9 z9UQbFrLOphG>&%OF(sK0s?qM7CHdl5G1onpq$$Cv zIKuU2AWao`P%Q&!LMh`|EmvWt*||uj>N=g4@I>BG+HS>f(R$};pTDFQ%e=nn++B&s zshe)zvYal%5RXoEe+Hf)aD>jo=`ao9`!dV|CTvi7ZM(XX8CV8?Ke?6p`^JJh!B>(JpH ziCHflYrdJMQcVU&rMjiAc;&PxNB{=9YrJ=ON!P|ygXu^?^HO@w?;$CnCovc- zL+BA((}LjVe+q+1Rj3^2G)8FQS1TZ`van&2x3GpEy{)k3LoFjJoEo$Yta~?(Vxd0} z*xJC$Xa+OF0OvSPn3E^*f9>)!yw$%AqGIL4xXHO*sLq^k?n<2sPUa_1@YXs^Es^8998#@K))nY{F=Nfy%QY_WxVYnl(gDly9M?XeRB!MZgl009CfQo63{eO#Vw5t4Blgqz=DReq%_Z@4 zZ9ra&e|ReqLbP$VgZl!qE$mw#mkHEOK{(fd#wW!j=nGZ-XondWbI^8Cy%Lx_0mHn$ zz`yNWCG0+R+xpA!?IX5mvnU};zIda_#0^$<$PlXe=z0hs)QCOjG8F+lb^!?_5Kd8ZGJ{$ z2Z;QPQ#siJPl0=SWsQr~bJPwQxbg&Ac?2EIE%E6gk_bV|Y7#1iO5ii4u+{9T*6J(; zH>FQj9!EGa#FXCI`)yMQ002Dja z%IM)3@CLzme*EmySUyuCju;eElMx0FMiG~`J}5f&T^)K^MuOEM|3eJ<=-s~BKyRFR z4j(Q4>CJjW8vwv|pcvmhMZ2K#C{(50e_nRoT}eQlZEWs9;5D#OWCds_ZS*{r7^FtV zTidfsgCk4KhluN?XiF-@w9xH9_2>U89C$Cyt;tO2fXZ|rRv$!c z1o9Mi8>4LF12V#;4Td@8`dGe_YS!4*hs7ATQ-}EzbXHdpOZ0c1%VZ8uNqiV^f7fX0 zqX8Mm@e>crF1MZ}u*o5}z+d>Dp!bO4E8Z~kbg$ij14vwwDiX4n!4zs0+OMZc zg%N|JCkRnYB~L9gClzQ;adI9rNgl@eqGpBj9;%2V3J!o%jsvnlL1Wse=uX!#v@ZR z9=!dC4-fU%ESQG;;{c0e-6Iw$^b%20*J~dZZXv$_<}{ZhwSS>W`Xi;kK+fp56^YPmzyzp0(totG4%oIOoz%Y3-hz#7%3rDO;N1L&=e`qv&vaIh(7OFW} z9_nJv0w#~_ba|9(5IM2fk<`iwLxliEK)S#8d#riHyj{KI8vRU)XO_eZN2-NeTYC4Q zeIzy6{Wb=43QtHOE=!qZY|1fc&N&a8@khXm43Lj*(P7JXk7!6Rf0UqoTmX9&F0RxF zMD^TnQBPu`iNxt_niG!wdVkJ4plueQ0TLZmddNn?WJI(XwLPB^VSb5bpz#z6AJ-`kh)LiGU7<P*08(g{5(RRv}Zwka~?q%+R-XypeFvg2um6}nW!_MI9asi62HV3n+$E| z{jZ^Rom*rp8y)r+@$TC%s@TJE67`(*47wbVFe{3l3XkYp_8XFGQDk967D%}Aua}ib21s-+jpg;4jz3{oN3uAAZ517Hf$G0KoPW|gSmmwD_6fn+9+HlL z_%tm4v8-G7aud^y6c&ZU+K6D+W8HaVnMS+!-+VK$5;9k=w*0XGofhK`dR-0vnxVlv zA8ftA0_UK21N0h!Nc`CH0Ff{FIIfKUqN?*g1eO}5V8v~9{BAHvZ-9nT7M98Ukmao) z0g5?6>Q0>dSbv}y6K0dZ#yGGKU~1!{tj>KZO@8}?EG?@YR&gKnN`^U#7&QW}^XS-m&>XfZLd8{Xoe zH(WBlCEj%5F+Ps@^TF#0_|raPn2*}v{<}?0MPOi zq5)XBaDOr?&%_58A8TW?mK%PnU+Q^iGx;jT+fRXidT8f6#EqUs9os%VnP%l79bFEu z6Ok(9J9|vijiG`%PCLs4+r=RLID2*wu72Hmc^(O2BMcN)^r4&y74#M?{v``bYXXKT zvrYjWy|q1q$4on`5tJk}MQhDgZa@j{2W_WUl@InNe{sMgGp$H$Rtow^@;tE z{2g(U-5Zpkvxxft#4f7bc{HlYdhUS`F-3F7Qpv9kGvr66<>kp(b(X z8Glhz_eLzoal=UIQN1F~Mz$3;-m=|EC`5iA;B(Wnv%AUZ_%{m}?5rV3$knf{4lx9~ zwPK&K1gUAaQVkFzf^dpUBv_KLj{mJ8zWg-(D@*?d)(RT^KfiA z3$;wlC4cBrEfn9@%H#n0;cU-JeS*qTrGHh&|4bZiafbgF#vBuIdrUJf)te5HI4t{c zQW94rG>^S_3Bpw|>*}M=S;nbG{SkWxh6iLPaI{lbi(GMV5z(|3%A}yZ_6?pt9Q6|* z(K^F{nLh)5KYnKg-`n@GyGUu1){>0j-&1P>YVBA_P&xSFAj#z&X78})js}BHV9mjK)LD97%I|%que1A8dU?I8HpPGlGT z1gRgyE`;e()=?OS5(y+kzXEMXR$J&E4Pe`7)1oRLNw#>6ou@Z|Wpv&!DU^25;>1vT z69*V^OExHmf@O_DuEwsdEHVqaQ<nKC^0S^3TVITWNLbgIi z72)5TgZM7=zk;J3Z3L`+^lF|l+7EZR2WtFAgN3hGSU+)imcp@WyDswsc7G%5p%pLW%t z`lvAGQ}5C&RI>nl_kQJ19Dzt3Yw@Gjyt;&0W*me*2W7;|==+~Vc-C@g9E(hFgqtow z7z@-Ii&L63GOz_GfkM7vyQP2Y*plNGJz)okAo2>BCcpipj1OfY#bcQJlqo zIGJSSfgDu2P^i+bfu*g}s%S6r8hvyo`_H+zsl-(@TvmbAAZdJz;7>iAOj3_iL zvR{~2RdXNawM$y-bQcZ}SQMr4w*9`mEujoX@QvQf$WE)WI3PV_2&sJ5ni}`zgdDpe zT%#&l>k7V3t$)SC6=CFjGJfO&*G#lbARxNujVD#HDw22B^VA!mMUmG(TtX0#$iYv> zyGRu6X+>{3Q5|9j{VD*Me_BD$6H~u{B+%6N&$Qlx5X_ot%u*LyV}^wJcpF<0HtqXs zhz&(eoIUMkJlZLG$FNN0pqNsY%Mj=_!a|LesfS~E z2S;}vBQ^4E@^{`HD*(c{-?W|kOmu2?7Xb$Yc|*9s!aajR7e?7L zJR)PBVSn^RXE&z41h~;;4GgpZQ947Qi<8f(OAij=SAPF1F+S7)=+AuNh|&^?<=|=P zzo?-KO|;mipFknOKp}q(R=^w?I0Wwl0~~!?;^V`FLWR5rh8mm1ikZaGZzmPX@3r3*3>tf@fW^#eO zgwvlQQ@84=EpUd&Z$3RqZklnIdNpXXGo+MW&}v?bV8R9B00{-JC0)0R6!@$ zPztWqe@XzEXEr$fyHRAkIcYoUlnx%U41dw|`2}FWDA!hgW|9IqdQ+`BzUj5)n%_xY;V!O~DEhDe*+k@eC7`ME;Z7+9O{WwyGO)iUg~$Q_6vm*S zzCRE70lYu&#<|A=z1NQb@_%RgsG*4%UB|NMI?lJqZ{7&;TZx?{aHQa%G4>&|M!@Sx zNZ5QP-2j+YLpi1~f$Z^0H)QeXrLuaR0uolGLCx`Nf0lk^v;+Z|kfEwo?vx5e^scV0 zU~--{`sY8};<;$dOp>&4Tk=AqPQI8ku@A~|JMftDDiGUf$t!y+kbk3R65XdC=so$I zsRk)-UF?nFD0d?&`Fi1>=SdjFoXxG(1bk&a0k4Ar5IgvLu%gRo#*gQlp@2v_qc66k137-J_xsfV z9w2Qk^op%3sIzn-mnNv%FaGQ)=XsQY1@25ZB(Dk7bzoK6bU>3#jm?Q4;!l{-iRHg!Oed*kZhxnlD`)FS%p?B;^TVT1 zOE{l>p$QRXEjobfOHB(Bs8Rx{#=FtrNr6>NLu{XZKSk=3f_B3TEVVnwD}Z5_CDG8B z0=C}+hIu)?%?C6y-HvGGQVUnN(u6b86dx5V%+K^7lyFZOCpptXNx%4>7eH}S3*q9S zd72^#*C&EO8GmbAkKj>qs~j`|D_W`I<`J?mV8t+#@Eo`8OQjxY=LoU^nEmVf-IWGQ$ixZNPq1?rO8heP3* zwQIKl3`qn>fC(aOGhE20!~ibH(F0ha98$H-MX+O2u22S4E@#cl)brqKZD?)2MKcfE zn#nMb;+f04>0`dYsmkWT9wtBlpek7=!Q{jTWHwRKITYm~Ox9`!SvwdiGtLVNdf^dp zGLTgNR)5mge^{T@6jbydaurrz9iTvso~DMjWnTo{{r<@pj=|yzVj9Ae%J`(VY~&yKvwzSMt?$!twnarWE`}kpmORdy=|bts{?2s zVq$M`p!c9s!I%Ny#KbdIlw9xQ5JMi4r+w~^fM@5i>LijR*|_V6s9Q%a>nCKsIsai5 ztFr~{mxUn_&iRLy9r}g2_L%JogEwa|VN8{HS)<#jK3k6`VDhhFQs=$SP~)o=ZEd}5 zT7OSA>{@2X^A&TnNF(*3@+uU;L5Ic3YCh=^&1V2#oA;$keJ0ec3F^$!9*MC&kDcu-qo%j`!A!KGTfg0$SI%Hy5E{A~> zP?*U=jm~y;JsnxpoCL-u|-5ciZicA*h4EyM3x)EeVb`R%rbK6QOE~m)qia= z*y@tdhqAx2$8pO&OEVA0s71QV5gC*NpXs#gH}6D~Q;|r7db&1Kwf5to(@cSGpDaO* zRv>d@)Jp#~fa2MzL5!B*;!KAu)^W$!GPc;XvDiaZQciAwN27cfUvlK1UUyV}h`3RU{6TwV z@XnycV2+thLt=L!5&;#TxDQ3>u&`&*=%A{d$?t20W+d{c%>s;`yC%NB$k{&4HhMjk z2w5pUG3M#RSXAkFY{hb|3i`>*a|_7Wuwwb1`P^4<(qgwBdRU%n*%jHCynk~oJ&ChQ z+~rvNgC#>*P^i3IaUR*X++7J|$J?D(k+WBsNd?TU>zA_R=P}g763fM2FdH5ddcPl& z{mtDF_v51R;zpIqV!{1Q9B2Noe`9jv0x9)7h=~w1kZ?TE?}Up=c_EF}KL`?CTvIJ8 zVxYLfj7>vUB$Cn_DSbf*pMPZ&gM(i8%U89+nMwbZBr7a4dxEtGQm-`TfdA=s(fyr1 z1jY#OJY$5%c zi=31bHsql2YQxGKJe*RzAu6`CPk-zGeH0{fan5<@mO?|xYY1?yuOAb1=ypy;^F#cB zA{p9!86KUIwak%Ck(F}=zC)9-3xto-ZoH)G`W!g#RQer`Pojz^SkOa6a8m3r0wn=$ zMq}=2{PQ~Kyqk8CS%3QEK;pA1(w|2GM^uVf=x$5$K{StRDO~b59`t9td-v(@SoP!V z-(pqxm*g^&cQZ|9UGhL}HC(lRf)pv*I}1epFR2xV8E-R<^u+l>bNkLFlsVr#>N3oF}Y$YIXF#%2pI=GVvC9BU_ z8TlT(!qm~kzkji=`HC>^lgkcqLz4Fu#9{bCe+TAq2~WUN=arYUH~g6#OrH&4_@2JW zU84OtmlA^0Z!#-(0z1kwT0{7eRz1kASh=J~B^GI=-Lalk=N4luK##a`UYmqAEEV-U zRcTK}v*fByEeJ`di1HEU2k(1GVBp$Ysjqba0yGe>!GAkhKnwG+_JmOE4_j?zZeJqf z2>D?zqsKAB+7hy5Xr>D>NGR$=a4r!*zsJGuZA=-T}5&i%s&(I6D;s- zT?Ku{6Ms1^>tmt*%Fbt5@Agm?G^>wvHCn1ttEJ+hCMY;ZKL#2E3KSpQik=? zxauvFyP_Z`dHyWaFX6&&4xenUSRgr@QDRy(&ziE*c@G!&5$!br1!GHm;|Lpiz~BeF zusMOuDNZMJ5pyRXSP(;2LFzRYze+EM4Q7zU>3`kV}w}Qn&HAkh>qC4rda+@ zd4yP63S4RyOc*V{x$YimYsBI8%Q#ByC9)gRWhV=d8^A6fUWJRemTBCa$fs=N!yD3? zFrD0O#zfC^aRnH=P9BL%$S=AE*f%Y+W5UMDdEOM9xaPni`RU>tt7Q4H-f8DxKwHK| zlYjsB#|(f2x{*($=1X)o%fr@j>ulQsEAg z^@ZK@-#}l+;L#snsaE%-uSUV*?d@YpTC+=n3Cid=zVkZxqIEFgS0bN5K!ThA|8&V> zO(>Hzj!wt<(#^zVga;0PPB72;xaUxrwy_5=OY#o|-R~jE)y`S(9zhs-X{KIko_~8d zQSg`j7`3Tkt}Sv8-oxw}%4Og{9H&%RU;!IO%9^NokoBP&p7Mt0CIy46!VXG22L&Cd zM07aet4T)5f%fAd+Pdczne8v-bI#2eG! znYdQ(9w760ZR3YoBNpGE2nVKlj~d`87`U^F)Xy4?PC$Pv`C66I4}Wjq;f9h#JxTfzLnf>#)_8u!Iu_a}u#X*HE%7 zIq9_C@wM~=%;l~&xuUlEN`F}u@>1OQu&~l?!Zf|ORfC$4!=-UTRpW_tdp7No_qS6Q zO)Ff(8H<6q6%8-*m+Y@Nq{7ZhT;NiZFcBMD5JW%952(Z81{ExN4!%lori!Uc^0x`P z!V(2c*A+>RaBr^TQl&9=|I);qMv&{%s*j)t{O*dW-x&kc$qM$+h<{yKYW?+!au#Zs zu5IWpu_~4nSK{ihaXI(2VYfW{R?hMkU(Drf{Q`+*OQK`?ipU8^Qsl-WU@8ja6fAO& z%sbR+X=_$l8jyITIdQd?plIv{&69xjT**-^ALSu%Y;#g(ORhotRgRZB>r>pThVNCS zH#D$UqS=r+I~3Evx_@F&@1<+wy$vYu$l1~fuJgG_?4~+3*$6sgKh``hEuD>t+F5e? z;>yUx!tkn0+f0uxD^L$EGDBoh7Ey7lzm`lV)%#i*bw8?jUlFd|!c9`tSw|AwH62*g z_IRrJqVRVqlmt+32Y zg1Rb1v4Dxqjf@@m0Q6O&Gj1+$JtdP-NM>(Lwe$f z>8D7?USLVLI?W(Rjz0OjR7=+?0zk%(e{;Y@Wo6RXfhmsnpz!|gMXm>uW^scMB(vh{ z`Kn*IS_3|axyDsT3xmR`eJB?aM@kUN9_1B|# zCoOwbbQV_cm%;jdB}qz3{4)ILik437h7=UEK{Zt$#`=UlgjbK9`Wg4rDLH6 zzA#v+!Z9^^UL81t*LXIdoEU2h1=KMx44~fj_kS8cCSMFy(hELh612w&I^(NDqp*fZ z2PjS1-(VKU>8>on#iOPgZxIU1&peT!~{hQwH?fSN@%*x&%4+(TL1b=?Qe8RYn*K zP=9|KD$$O1rHa|hAksJn^!DaF1hd!op4iuWV$f{2uX(@l`>$e3t+#I+35?KMEW!oQ zC@15`;lB0dTR zgzd@b9Y!6>McHq_m) zRt5@mfT{3_=x4U91=PxJ49z@;yMK5XcZz5CkC+1VK0kK_m#p5JHG0jDILX zlE~H=!?QxzQIrc(S&C0i;PCd*jCPD3j{005psAmo{jMRllE>Yb>j9PO)neo|XzC1z z+SY2>7gv()ZH1*B~`)UNzGoq3KqM??VbzL`r%p75PJeB)6YK2f8emLe8{^ zw$GD6p-Z_FMe~eBhV5+zmjL_IT2yc?vtof&Tgib*e%B?=)W#17?^cSc7Jn{T5(1TP zrUN55lrOBQri%s0d*{be zymZ{XHfCcOC==`UdiNcD9j3=|t}6EPZkU;IOF19t^s z{;~5BDe*M{YCvpU^9RN!sqV`sNH_59b=V5o7o$UarP#(%w7mM*34gO|X+A{)hEL{T$624+o>RfmY0$ms4k;Q{U0Q0OKZF)`zD%k@K@2faLSk@>K0Z2RO` zXj0n1{7UJeJ1Mddke&VCCXT%&Tfu{Me_0QnTXG2|b}^DV5%aWJIgBw)&f@9SyvvHL zIF}q#i%G&{{C^o)&eY46G)d4g540UX#avz?I_%&f0X6sbsHHRfMKp%sZ?cm_TrV$e zCntt-_F|w6E|7LoG+1Mu1PGwrOk34USKUg!1Hqe04CBta}*oVdVV zQ(mT0BSEJEA^4mhCC8ap-nI?+Y&iY5?;gjW{ss}zy?=-NWd)&pTy^AV*gZx*VGA7& zL_8rb!k)wvnsM_-GK@4M3<4m{ASW)HH*PrPhK7yBwSR8`$N}1_;8?S^Uwu<6R8{~$ zZ*_DL0w@Ru0s@0Dpdt(q z12GhXM}J@nMUYHHPY3F7JW%LFP6zF9g8KTuRpA;HG-SaCU861fXdKq!au-Af+@Jfe zpk2sZp{*_?z!#yDc6xv4_W(Sm3F5>YG8e7UJ}gAW&jP7WxzW=x2!H0eAG6#`5DrL|0~j@DDhVBMKM+!r zb(oac0itlx0O|Iu9YF%sXohbk;TIH1OONpI(j;}&&Kv)CcD80b#d&igDk>(o=PdF` z!^|O~GAM|w$opW0o{}L;${8@&RtW~wDoNsw%4pldSyhSZ;DUvDz}zU;Kfg?kiPP!n zh<`nT^?XfsAYr1_;vSl0G?E}{8Gfo6gR2e;Q4j%2ko77=0p<205Sl2CQAjmido{LI zCP<@Dpiq%q{VjwB5bRhW?GmIUIr;n^#Cni17y;Th+_fcJ z(mXlet8|0}6D$HAX87VC8Nw+D9^(m?mVW{sJ#YMXzK}o*T9jl24awku9%w?y4wJm( z;AmUSx`-tS#$4`AwUJ077KFV>50gisVMRB-3Y(hQ(Iab~v6pcVXeBO`9z2!r6_|m}ZD4h#)&cDij8Fi?NZhv`xWi zw{6kVNy7lk{)RX6>(--iX;sBrmZhMVfV6awi*SjM4+gk%K(hnjh;Lc@m z^B~poKp~l!o63}v0wi93A`$t9&>Hkk2M-*FTbsQ6RktIT*h`5@H6o+8gA(xtZ>yS9 z%w?P4suqohB^v~W<=zR&R9!xiq^3d>5Jd`!+08-G$)hgGVOApkEeEdvqe3!|=};y? zHvu{$BTke68TmE!58;~=aymk=HS7*%)Lf|au zi(E2%v585T%&7(?lOI`-Ad#1jVx+bb#`cgMflAOdK+YLfh71};evC3+Bve~!Fs(<7 zOpIJWoKOnGhUax@0N6Mqbg>9S$|z2PFdO(GH$V)&gIHn7SX7jIqkmYxLY`(&T9D`j z{$uY={mWpbjgx0n+U=T=1(PY+trt*$WKh;BQEGqaSfYR8xvj=)soY5gKSkwY5TxK> z8x=H?)K41(^1e1m5SNC!)&X~lC@^YJf-PGKOAuCM%60b^166CpY)S|cUt#gebdw1` zHJI3lRc9z=zJc`LyIoN6U_unhTJN zm=-uHgZCYDCZT}@{ucVi(`qm* zbrqkeDPF=385I;#M6h&sXd02^DTv5`A<E)|e!G`+?$6JRvVzl&FT~$fkMxJunj2 z%bU0xo?FC@G*|q&fsCkC7I1r<%lHw4z2{2ShAEjpXGqU{%8ZJu)lMns$G_`R$X5 z3$7j2P0)!*;!J>ihO*>^N=zffDGV%FVW3Z#0aP6cVFC8tjw?bfl8}3Wl&B`wmN-N# za#)~{$ba22G7w-@5N2XQ=mI+9Lz6&MC}oL*9u15!R1q462dM#?u|SZx);+5PA4^22 zF;75JtpM#oI2=wi2VfENsz8mTMI%}xOJgs9GBm@+C;AOS86Z493M9pd``V0uw}B-c6&_(iWv zpl@m|)2Q<+7?woHtO!&#xRj4jwpJSEYgJP&z0L#~e+29yAmeSyf;FS@s-qIcETMp6 z;!W$n0Ar*n(=}z4G})(kqqtWGgn$!Aw+2Cea!L6xatH`Ro&dyo0}`r9xlS24NepLc zt$!wtck==Z4?0VTs(qFTEgk6d2_8agWen58r}-ojqgZn)9018Oq=-IT1utL$>)8TV zFlI{v;OkLekA`e`R1pr5%CQ27@S?L>N_>E$r0ofL+nE1CK(f&QQ%v>=$QVN5C_hw1 z{uUV)6(vMCnGUy=f#UzdL^UG%dODT!vVS}?2M~Bx2Zi{b7uE*Atlr|H2(B{U{!ZhD zz4ga#9-Iw?V%U8lI>#GM>6*&c3X|Bu-Ol8{aC_#PyZ3S+@NuP=jCO_Z6>c050>a#p zyYaFCq3(8cg8epP9TK-p_l8dmp^4zfl4M7m+BPM>~e7rwd&hA((g57T*L#Z)OKtbhy zcnO=*q_!tAn6mr$?IznZA_Ip8#TnM=F$$YP761)^W2kU zq*{u_)>gb>P%-6|Lx&Q|heSv4d4Dj;VwdF_(`6o{?xSzL1@52GFbZpOY}zzfQ=sRlR>*~j;7snKOi6%4uAO{IzwU{ zz`}(I1*T4pxG`Z}k{BVLFUkPnzk}WI#BQ)2yCJRY#>1$V z%%!H-qXh!fXAmb7{4WnfSjhpveq@?^edr=m^5$MEU3eK_Tv9avR~UgBgq-MOz!ID% zMpLEkBV$JE8y^u7NnG z)jf@xcbnq~UI0|0ntmheq&U?F6GbV$e<)H&nRe@u;z$aJFn_3Iew8f%Xy^b#000Ca zKxnXJG?&QZS(FIL4>SM)g@uF?g8`3$gMp2Kgnet&2^T0UH@-kx2zh>hX0RP)r; zN&e?6`sfa| zk6b0nH>Vd-wMTlYY>%jX+XY^sMOh+HrjD#{0-Rovbji}&(eDH|hL+bwLXEBVuag>G zCB$+KYkv^tDF$K2pTZk#m%5QoLq!tDZ&5Iy#PqV->y_7qD4lC^iv?ZW%q(P#W?i@h z;Q-M;zpHr=JgD+du*3R7hW*ggs#ir9rvAATn%g63f2H~u-`|g_VI>OTi;D29S4~FY z|3I=&onw%Z>8E~XFpOCwN<|Ogm1prKtpr2=l7Gq8y=*v9iUe^DjS#0qYTZR4-ZO!I zrQ-7xBR|Lod+zwRA5OC~QpJ*{J`LHs-!Kw}fN8{R&_t1X-W_#?5p=?o!%vUSfutj{ z8xnnw`QO8kQl52(YxrmM9ut@dr6QO}M4G~d9ZaO-K<7+P&e-&~<;p%q>2PJ2{SFmh z#(znMW+iqK0NXNy)#K5IEQ=GFuXq=`VHkj@z7w(0OPNeMcr16a0Qs^-*v)X47BEO# z$Cxl6w_xJ-ksvZ78?HSS_}Hjx_Zy4y)p7q3=Jt#KFqjnkt~FRbnB-JjP76F$DjZ@lz1K`h|_oY_^n`R zcr578pBLPsYg6$$EKtuQ6a|2YM1YT!h0&lxCN(O7vJrkEItBo^nuqycEm+ByqW1;q zMQ9+6_Bp?e=49X13O^6HzUR4jTYq>MDkhzY0-cNw{7O^{mGQ>MfY-TevIk$VC6@Kl zuqs;2B)bJ@h^QT~&kCsbq|La(nZS_;vq13N>D~ebVCzLq{)NzKT2o?~@GfoSgO3tG z_z+M#oI&`aYcig3WZA6o0$)+LKPx!dHNjr}9+27Zb)el-5Kg+Y?;9|l&3`uDWtzOJ z@ikK$XW;S>@nd@!YRBXYy|S^k8Ibo3m_o`WkXDHA0)<$e<>aZ2qYE6s2wepXGpJS4 z+x-Ep)U5rW`O!C1rx3{=jcSiQy@<>jgx%v$Ueitab5&sE-+K1$Y%w9m@c2bL0SK$+ z^s~ces;*i5mpAHz0hw5gD1YOxiY`szD;XM!SlsnY_QxA?jWwo(JsM*VuT=U`Dmq9G z`srt2U1Pxe)Sofc5n>Nkj>JdA)0ia8<0Quj9r{V3vw$Fv2#8%!uZ3|qJ}uUbCmy#T?;O8xXK_M?u}iC@O8@^O~{!akZUf*eJzgDt{TeCU@39Mggl7 z8t=b07!~AU&WkvKq4;Z-G5Q2~ZK2(A`NMe5CdPBP|3mY0MOr3*7K?GpxS@;S`PJ=k zAwDt%vm>pMZ2H8L5FcCEpy?bPNz^%Xla=AA&OzggEL+|m=ciaak}{zug`IL6_ET%bOkR6U?c2^Pvs>A6E)j>`g6`|A7k-jB0 zQil&P3XUXo(7HorL6{D=%=!aC@6XZxwx4Szv$n=E|5%$S$J6QPKd;lFz$iV3kB%OX zoh4l8a4XYZc%BgXF`wfGkt@@5bX+>Uz&yY8zFBW(L-^ZyD1T12Own`<^6e>V99$+o zN_WB7oa@h6WCVzKkLZVbGGr2QkkwhK^NMYm-ixQ}3oSPwdX^y`y_zzo{$)T>hmDrG( zdN<3k887CB`q)&(iOz&s7qNyloWrVtN8cr+F78D>&CFm>;`Fc>ujW{IFs7Sc%ONBV zJxirwI3irCJa~6fq*{5Xh||R1d=rs(64A9beSanqMQ)8vhTDKcB1&E-S{<@(xsHcI z22zKOmWkjhLXPP8D+BDYlGwL`)vyq98kxyU5|`8G5>MDnHv(Oqctk_WJ+ZKm7(nI& zE=X=XiqdTSub}=#Sfoa6&!{jW@$d4;@C2OlbbrIoE#V~Ts8Mn=OHSel%Gni(`4221 za(`lOSyD?u{ztT%4D}x+O_BJIoLoTrM~=HG{UfyjqWt4R^A`TmRW^{^qxg=}y-Nm;-{$$=g1w*PkYx$dK=inszA%`=_3|`PypcDEkIVi}Bo}-<3!4P+ z1E}SiGP0<}Edcxynffr1ZQfWp7$CTRg@3G&VIg;k|0o)jNuBtt)qsX1?W5~0yxC&7 z+)t?J>g4-@0V7WywvK@-HzSqsrJ5){faHjbn|M)x_E?Qx;! z5xOb~V?-S%;DFpOa15lQi3^|K7%++bUnC?m;Hx3$&S~CEvqX{_hR{XwPwn2*=YQ(* z7L|7UNj>|K(eYvo;KYw+sPD?~+gPuW%VHd$Z2IqBh!lPmR^$y|^P1;c_n#T9H^-J` zE*qzm!G~lo=Ra9SsA&a3$WBYAD$1OMu{0-nQa9gpl+;TzX^(-jdyM^}EQ>IcV8k#t zVL;YIe8D8dzqA&YyeH;<0K_CZwtrZjm*kIM+{+7@6OGtG3$2o|kt?xorjqlES0x%X z^X6oo=A7(x=j1aC!$KK_BBWMaCecuQ%jhdK?@!e`*^+!Gcj9t(_?={0tVpc06lxay zz=V{_y4?D$RHCpufnK>HZOY0dKE3mIC11}>*SE#xc&0Q&MuFCn+bz!3TYrZpqBpv- zolpV6Y%bAnvM$@Vn*W7OeZCzp|!I=uxK*C3OXBvUnRvSb-W5Z$3MJiBpH|crzRXaqQgAjY2bt153U*wXxIQ) zz3=&KPg{2%ke|p!%&2e{Ab)1m0r^9gojIdXNCE*kpVw}HCFFao6$MH`xWYp^HjPd# zL0BY8ZiTCOy=qB#-p+E7hx`7wr;_vf6gUUKnCcKHzL*{(SOvyCCY##Ip@mM8Mzd3R_~EC{ZuP-qf1 z13%dH)>#wEQ(JXQC>&PhK4kq>w4+Jlr&$9X1CP*M&8Kp^fe=Q5b59g^RwCiQn2?51 z@GqWCJ1dA;N-UHnzJC~%rt^lZs08*=e(N!1m$$*}GSw5Z-V+$TZKw*t_?XDw@>n2u zAcqIE=lG!-a1|Z^IG=Bmc8bz=YQSlqUS=Clv}_jmp1b*adZ=%93gb8o3m(MK!eQkN zJvl1$OtW;B+KQ)LWAQw)7S9FfgUmz==YfK=Ns)vs5}1!Db$=Wstd!cZYw8{8R*)$! z1tW%jOcNMTM|_3^b(Pg#;^x>36f1v${(Lad8s{fs1SmBdVl=da2{P6E?v`2_ng}<4 z$s!CqFV1u3JCXikR=$@SIRis3dAwIkpSuC3PQv;!oi#Bwx4$2hl{EnDBK_D?gK8kR zfZ}woCjbTKkbhkN=u_Y-qZPuJuJ_osZ(%1)RBd|oP@tGXK21yXb=b_G6hCF#C4hrP z5}n&_K~S+hLOVd%0!@M}y1>m~F+>&~TUp7_{@B`O*|I)`C^OyB*9rj<|NC0gnV|3>6fJA;OIdzl z|D^%u{oM@Ka6?}0Bx-O{Vym|sTXiO~RaJLN*(%N`YYi<4)Rt|)7Q*!yFq6@lbgBd@ z5C#&6sehceJEF~$$#Z4BTCc454VKk0#j=V&p^;^cRQV%6hbL@BUFaKI$D*1!UNJ4I zJMZj0pUNh|Ao3l=d4^f_eXr_Gk6Nmr-%E4fqiaHQUjkBh^5{N;vvgmwHYRiCh-!5L zaf%ZW7IFcp_)xX`spFdp&?3omGpe%Fg+_Isc7MgFr}9Kk5phJ7%sn0=QLUQmEcH`c zGkBlwz0`s<+XD_V;eK3E9|HXD;hzU`bVf=l%c}2zSydEiR&7c~Xg8}mV`tTkC9_~B zNm-6}w_AJr>)PAWzq)I z+JB3O9bCe!%^D4F1rUImN;sKYxHKx=tM0T1^bWfp|P4&OP0s%SW4E^JHw6HTkg*|#xU@b95t;)yM1I#0N08t>c-ZN1I6kPK) z?zqW2A&SuU(lyG09!GGtOrWpW5qiCu_I_w3M^|A28wL_T{h`!HhCvzx53&_cb$@N> zS!Z-%8mx2uBZy`^p#!!yBBfO?w6-T8w_BftE}o4&6|mJ-)vDIJ|Dz-RGhCY5&HpiI z0M!;=K!lE;uzZRZVUR7@zg40`idED4D@e?Cq`5yP=+-wqG?CWpy{8s3GE^M307gK$ zzb|N~$6~H+Is%E!qJ)CqO)ZYXseM+-yik8D{|@%P)E+vtaCXhSpf}I8H!A^&vaVyD zPBqEHJ$x|OX+bRG=0};ej&-MX_!&VU%XZct%Hbk^YA3`DD7_%O1g*X~4W)nlBcgVw zZ6fn%RP87?Imi~0c&N9O$>1n@OOd)HQU+I@v&=3F#(BStr%h*rV{H@hSQ~$s zXcXYCS?yv}-i)>;T018bDd7~ltMOR_f}10wmA-8q-v2rx2%J<12@&@Xh{Mk9X*@t0>hT!fouyRTgF{OTSmFxVM7k(zzEbDO3n%f2>*YGVcoP! zp+FafGW0Vn>bI)8NRTcp&}+k6OBZv|VX|^ur6IDOW#m+d|HUOeXljx3hH{Mn)R!gp zkN9SN9dhAJMp%NMJc86}wzJ*B`eO;{mf|QVg9MioyyLJYrc$15(%MI*n{}mLX;*p$ zaH`#v5-Ge=MGl6>$SV!;m12L6QvK>=j&8>emQ*S|W3)EmN}=-JP}znHgIHytPw{98 zVa}Hp{!r+nNjZc69${GdLa=*~IHA_+B<`>dw1oW0%GVv-u$Z#6UN-Ap{<-3(s{HxZ z#7+w%gftmYS+EpSaC2J4lTx(xmWx7Q)8psbHOa$QBN=~6xO-yeqzr#8%6+|R?S52Z z_W}gad{+a@+!o1kVhdVGsE-%oTx&oBzTK}lWDG+~_c$9;%h=!*G{UKW4pIh{(umwu z&N4YuUVRFY?tx*Uk~`-39E)UPT|%%}_Joa>Q-+T;QlB1-X8lVv#*CARduPT?azV7w z2W?Nw5^HHn`&SUONArKzu2{Y)v_UieB-+zPWWIuOM7p}Ye55VRv>f)1rbtL}Iqi{M z(v-0}FQD;1HlFm7$!jA%(8P*WVo!p}h2YX`BTlP68o(Q-dUk1~8A}hHgU^!vg^#I( zQ1q4~3u40X-{1HlGX%y0&`+}R3HfekEN8e^KpukxN|I(P(13prbK(_3d1<0p0=y%; zc(ir23#d(=u$;j&t>Cp4HR^MhsHQC^4N^*xiMNnl>#eZUoQ_(gP>Pj%I%WOGy%{n^ z?3N$d*0Jz~d$Fooe}g9H%V8dC@3wAP1{+pJ#5mCDyY z9}Y2>KGpqN%b0)3yUwEcB{^MVUaN#)yg`)1A-rJ)!V^+8pUDH}%Jg$3qZ=yl#{BoT zRSSATK5Hi|5F%C)N1Jy%DAjWmZo4tViiO|ii6=J2v?(|`*|Bfp2wpGtOceq* z>7U=7bV4g~unfxN@kH@eq=E@$E82#_;zek-Ao;21)bl3Jwu__LUnt@m|1deP;eLlq9 zA61ALYdU}K3G^l%fX0c(jh+J93!ot621HFz7yt=)SZ4d@wtu_D>37BM+$?4(6?%uV z%2TH-lATa~81J|nKfsMT5I!IAPl;v#Qyr4s7Y3~?4nO{&Nw9N39Q!MTrdflm6MA+E z@RTYv)o&e0<)0U#;;A5P#euN0%4{!$^avn4)}wz<)WZzX+c968bssI838gFim;gR= zY!3Fx#CyAa*)_53YCxnN!K=h^eiVR&Ujc}%CvZuR^5zMzR`NL3?n_Us5D3Q7sv^1K zJ6t#?G3mLRu2tLh87WWU)p#!9owrK3)CM7Bc8%Y#IxdB`JvV?>b!JG@29&!1az+&w zfNg&cga}C{KZ+ySxP~KIj2`*JiV(RyIiyb78pj#(PHK_jZ_IS>Og=Sk+!W@m3%ieL z)YS-V3L7|fA3S^9kO8RAPVCNs>L#}LfuerZYNKFZnupA&b6=BT4MQLhK!AX7n3z;5 z5C_9C$sqkBFmOO1Knw&3!9d^;5CnpPK;VB72nGUyKp|M*o2N{l1(fwN8Zr>zZy+zc_X8pOmX49+f&?WFseP`wcHWJxb#y!96>As*!0u3HiBSmBO?O52;w3V#kVt} zFrE?V)iZKQJ|m%^)HM8)#t5C>QtjAzo$==byBViDqoq$}VDvzXu5Tu@)BUUQ7D!2< zB5}Aw8m37diEQ zQE-OqI5W*8qdEnGg%c>PErp`MT<+@U7keTr(BY0QtLkCXUwK!c=vk`Zu#$g5r%ymD z3Ef7#v^KH7XZ2jb8pDAz5LwG2qP4EhBUHoa9GT3I!TA4=j{$`!a%4(YMvYE^5i25h z2Xm*vtkWj2;gS#a2IF9=Q4VH`1Ulw|r5e%Eg(*oX-KBv6nw(0jbz!r$vPVnQaEXf> zE-SpgOZOHXFL7z}vgxz41y6q{|E!bmqfQNpwXoD!%b`F;ho*)06fCBq(zip=h0SY7 zn2cmCBJbAX`nQ(l!?oziTJpxVh$oAu+PdV`gyQZQL0u2KC0ta@GYjAidO3mqni2Ob z=>VSv2J%_<{aHdyB2Ia_H=n0;=OgK|TG}M77Ei{KMJg@wv=cC@lJb8e&d!%2;_$af z;c0iYGHuE~tc<6Kn3 zb)m@2zJJLylv4h_!p3-AN?>d+|xw+MyDJAQLjXL*jr zMMab`sfZcXw!jiG!QkdF+bzA)t!PDTtqGb9&zqm+tJx4<2sVF%B@1XZ@{$o+%Q8ZqFp5m&GnavPn6|cqJ;FlVbnG?qv0Dlg= zdoQJ%LZds;%l;Ulv^-J5P57eManHk3K=2px;g<^ZFm`C{OLs2{83X@hiM(j^|7&h~ zw5Rmao)ThH0b|D>I#1toj$4_QsKIX$h7#d+0^D z2o2Dd5Q`YhrUphPJ$tTXU9pFYMM!W%WitaNd$=yqRxHZ4pbnuvVwwx+i$tIS6QEG% z6~%_8)#B@$BpzNC!)N!AN*=(}D*F^ysbwPo_)q0lx)Paz%!7D^>q?v;?jFba*1S78xS~zG9Ix8{>0}K+-W`1py%n zkEI1dAZc4F2dIFJZKf>m8scnujD#TLH5j5%{Xo|22jn(W|C6s+ zls6jGhv9!HBzV`dSA+zK3XA;92IpWl>K|rv_)LNjsCI&H1YuYpngAI@L*u)qxVKd0 zv5AGoSxG}_!!~=a1vc5O#)aQ%RCu_Wb(gXWXUBsP2sgaw7XkQlCgNKJ@|{jVYa)2S z3$4(E6rVm5&(&v!`lxsBT4kRhZu`s;v2(uMXQF@ZeP&SbGu7f}4xSvuyT33EHmDGB z@#}x*-4rU!xy}rL>84hUD(GF{`6BuJmU-XJRz4N ziffwYp8y8{<403a0-L0s6x_=-f4wM+AqBBLw_KdxNTA#BQ2Nr#Ov-Vw2HIQS%93PK zSW162mTswL9Dm#Z14Jz}0`d!FOJ#qZt-6=9c$lcu#M2@sk>7vVH1FkZ64QG!&vFIa zE_fvc6&n6-4LS(vCt~ZMhK{d36+0v)Xa1oW&g=5J;n`eoe0?L(uc$`Ct{Oq#Ls6Td zYN6R8Wf+VehEAaoPi?sYn$0{fnn_lZIlq6oNpsI~(iG&IEWe{Pv5ec;@!NczrMYMt zOdP7oJ-R)3@W@f`T$`~;wi#X6HnY7SOEElpe#1XCv#U^C;AMugX`z}N%}FMK)1Wz; z_58tA8=CbtBNG*kRjZ;Y@BkMQp&@Ok&1UM%!k$9}nPt|in-I;Q>M|~lnFl4)dANTJ z5-u*o;_<+(IIvQoi11|6ta=871!NL>K*kOGGyXGq0XlIiCnBV@7j`t}s}+1tp=ChN zXc-5zi~^O33b~KR7lXSSi{7aiNnsLjmNL7M(kD&_pt8fIam8_%p_&1ORdtgK8ZVyt zYrnTgD~1VAb%PGIM6;S5r)!!UCIEkKHT>@hh$=ae!$EyvVrwTpfL^sSaeqt)95~RUql^$W$u!& z3P}=>93+I~oQ6ow+-V7_40p;wT+^QAAx<&lKP}>jLy2!=yaapV;-@f+nZ0=T-Kg9b z*d$G;PFZHCm~G^X*mGo%bnXXPR_2D<2}p}RapQKqiJ}d(o+U}HQfLgF<_^$1=F?7M zo~wvB&W{sV}mR(J4^b%%YGK@#V8v}2c$cB)I-5$7y*B-kfm zv;*Eccr6`C@lZ#Bh&oc4sN?EEZUzW6C%N^^sJnb%3Pi7+dk$*Q8f8V1+pGfy1O|xe zuo)POLe%l(SRMV<8hRa~FC`Q(ygvyvf?OCSDCj?Y1rFJa=h6Tpmzsb07-55z49%eD z&t=F}KOK;1JNZXG=Nmb_bY1HwWIn7x$@bw$o*ql`ob_?di$~)Uz*bC8CXS27;BbLD z4;Q<2Rjkl_753y}^ozLjbV^buw@&<80Ai*b$~ki5&O5h1?}Va0$*!LrHqpp+h9Ti~ zM+yV^Sjo=wOm^aLd`^EFn<1(eNkxhy(!@zZ_@rmt)u^CsoVZfBf#Qtx*11mT{3*z)_ur~`2!>T5q3U?G)-<@ictU#^u=?(H z>=CW$L51!f;Fj%Z=+r`Gq}g4aLNw~U{=A5Y)_Eo>9$9}mWoiLW^0QDp!4$`-jgu*% zFQoxdXS1aehm<}9fw5=N6zF+bKF(2USuWzMHYkc%LtIwFm0=2wT#+Z4E2@e&5q2ks z7R%umff%7uT~DMA5-e@WPVrbLMNdL03f(RKq!hIt-WZ|k(VR?(iJ6R;a7|A`zMhzv zqNt%26aIgum0+wiYN$lq@Y@0z2#c5O^d?lW;DihnC$zOMaxF#Zlh=MiKLTgcUY51Y z^@s2`7W9Uiu{X4gMWy#}_*0d{z=?U_4Z8nj5la{n$M5)xKJQP6|nUC#LYhP~4>LOeV^<1^|`+eV3O zLkS)$aY=CQy1YLA(m{9_$XkHp!#zA^@lzs_b?KAJ%KFdyv>5@>A>lhm<#`Bv>fR~? zB|%fopZTZOM=hnnKyXwJ3Z3AFi+Yosx~YFCCduP&xCVNrX^32({1mgGse=G{RpGG# zv24l^tQ@8Kw4~fTa!s5L7cgp+2;QS$;YlVFTBYC!34HQ1?MI{B3vmDPuBf5+sLP1C zKzKC!V)TPXHLfOssL;Nl-XwZ0iMATRN9~cI21Wm64BhPrx>VK35UzU|itQ%-s_K8v zeYp0S*Bu#VtQqzJ&I-2k9JdaDd;EXuR2c);dqoma#eUGDP@Ow`V>4rp;?<@m;niK$ zY$ySry4o;8WEwox|HB(+(R6~beAiTUEa$CYH}nD3IAV3#7vGslx&;9+K=Uk|tH%+j!LI~6vs%YnayCVKv_}pm-8OwWqYueZ0I|5=?q2{;G{A$Zu9EV z;|2p|Ro@N_E=^kLaeYed9%C7>28-hAo*P8>JR zZOH*4pvp&6S&ElJ9Lmj@h$?vOUnTLmv_sj%`^p2#!i1#cma%@sw-@f zj19;LQkCKkrMPjauQ%et6h@?Q6#Y4PfIIzWq%qQ47BWWK1Tf+S1?t z&q^gh9Y?fw*F!ox4S^s|am=d!6v};g&}zUIZAR_UQkD$aPr|HRsj9Xd_kF(yrCjbO z&2VIzwDKC)<6%$xgDiiIcLIAgLW=~^EXT-}riV^IWF2h}`hRu6ED~8`L2QEUBxh_x zqmyY-Pxd~D%Mz?`!1RL|9sz;GEw?40p>O6Wf4BTH#AtUzLl?m?dSI&2jY|Xz@G5bXaZ-6)LsorsWp@q*7kEO7J+}X7#=ts&8|J_{ZGenb|$} zC8-=foWhR`{Q2K3*-boZ-b1CJw?1(V;%*rNWShtHmN#pk$*e)M!#! zF?CDQ{8UjrKvO{KG7hm?J`l$1c@hOgy)zw4S;<$gWG}4LO?qM8t!Bw5Wu&+6I zKF-ra9;hDLFUom7`$|saq{X0cAu)+Hx1#Pde6k!eK8k zW#|Woz9+`pHZd8_wUI`8b+S@d`aXjvO1n@KW}ji)ZPRV+dWAx9bTD0Bdd_p{nN$G* zxOIz6xe$MpziS%H*>Zp46?SrS)AWwGEDpjRGYy?$C_}Q%Uqk;%8$y7CX`U^uu3;DS zsT}t0UHqt_+O*Y)s|Q7ZXWh_dr2LEK`b13jvtni?Pq0?sB;R73_W_@@@uv4Xm zH`uWC`H5>*j3hJC<+8o6>|I*HBM|sR(HXe6xCw#+z+}j;ll$K6FQ{zC9i$Ps+s_4_O&wziV?Xmm`HlOzK=SIh(t7~&usg{ufLUkaf zM>EpDb-~Hby>blF=Kp@e#fp)!v#xVI>y_o@tC=M&W1o@IUPxvz#Fby4!{5zk91oDQ z{@mf%MjUCzUJUcZ;|d9MMPgC0-JHtLs}akwJ4f3;2zaV+Z#u*hMfo5VVu?;6g28|4 zc7a%~n7lV9Wqnv^7f)&nsyr;Zj8D455=CRZaCK`A%LV#5oD5k3#{11i;HF$DCFqaL zG=ujFp_C=aDE(F&LI|lVgdD`&sSB13>Km-<6*-^(9Qzc4+M z77_vE0p^Ih+f+>25vO^!+qLmWf4 z4?MNDq%UIczs;ZnrGbYk} zV2+?H*P{6tpCob&$dO$_j>dnC$kDvO{c4dTEFU=%Ac_h%-+T!4fb#NQcpeHms{23> zOi9-eOAj%X?$QkvTo)hNbM8e$0(6n)B}2Nw5O$avY008E_m=zAK}ex~|2s!R!Thmr zgXJ6sBO^NN1ij(Q#G7GGjEJpLgK$Xu4T*lDIz;ZiB-v691R4*R`40|^|xF*hqDz~QjK7uC< zT%_9~3#v09h7EDA!>b}8z$Pdw7GYs#WpfE0K75+2+y_8=IofUH1e0+geYKA)1*G&6 zWEsj(`)H_~rjTL&jU?L1WPkPOh+ zL85I{-CtLZGvG9kPr&P~^qVxnhoalQcnXAG8gG>Q?@0N1IZ%IT2y};bs8sB3?xebc zVj9C);TQ+{M7}Hk8@^nc0HQ_|5RiQY{b7=nVJTi+=7*c)%rjN5~R3BHHxjv2vV9jD5cNE}gKGHsp?{jl%hn{q_mXesog@>f@lGiE37(K zMlP3Xj?5#j$`{;K6=R`E;j zA=E8ay+wb=&MIJRmiF{uTUnVNllsAltA2EF)z>6-DgC1s=KxGit#-kdnqOV1Vp@tO^{0t3*eJMcI0zBb*ZW}B`;=tw1 z0oenlNOl>XpMT zfCiECj!y=Rt>!PILQRLA(kpbC3Bn135vm#W_8AtXump=H@j}fsvhh5RTcCn zrgRwMK>S8JCD5un4K#IYXX}tmqRy{Lays1zGm{5)csNeoqrQh$BX)I>Tox216z=2e zfnkw)YGqV|3*g+v{)W8eF^DHHf+DM+J1c*g{@=4i(c}p=g)?c(vFAR>aj9dWI_~&k ztEAankuW4}^L4^RG^7=Q1&x4IT~niNhC%%|L*(DrHXr)>_?HaPPzh{_{sd!|#hq-w z6`%NBK}n8w(qe)%`Ba%;yy8-5w9@W$z{aCv{U8>pg-r)YabSB;RrDaWtBMcx;e>x& zERsz8HsEnGj_G&br8DogE<0pbq3AYVmT-Qj>(mtIG6+$g)GU|^my(aYG73HGMoW~GvI&Pbv``O zn^Ex)$Lf_wLmS$-t*4eLU2_z9~1^RO*E82-Q$gI7^Kn3cnET3*8X%Pmt~ zQK8DRa3m}k2>h@V0~$lNg^69~Rkz`8V8X(A)PBlN2B>g zX~NLdV#NYKD>H277ox+L0kK_#h$)^GpTu}w6^F#AH8I0PB!*kxN>#CT@Bvm4ew)Z` z+76!vN;wZ9l?tOd*C9m4P&^QRQbELsV08*doQKa15;Ar|3aOuu87hB7+Lt(<1Z74O zo4D>384jGJE+#T-IjBJsm5g%RV(Ue?bl~XD8qyvl=aR2c$gc1IKg_uZd;N?ib_h@b zE94UaQ>?Es5*RJqWq_!)5+R|#YD%lvhbbCpE847_&QI8nxpTnHki270g&NN(FeggQ z+Kkic)2JO^8`9;!!Ivg)_%^cT{vSTYyhm)Bje2-~7w;TwpGSPonB z!Yq~Zxi~2l)xR(Vbj7&b7E0YdgFG;n!N4wQYqg6`kc|aaOX$;1;h|Xmke^9u^Xo|v zd}NORRk(x7YYaU6O)Dl8i0p7Qlm!KC9D6$5YoICgY_HF#ML2(H5Tvo=Ko5&WXt5tm zb0sslXOw@|8z0Jms`ULI2fKeo+@MEtX@*))J@1V$2(0C>FEFko6k)X~vshJ~EkKC;QV`{QQYZmm zSx0fk=|$bNtbK;gy@1H1ddmIWqO~g7+Uf00!r0B7|lfW#-H@j83 zJ3={rMZgaOMj2Ze-X7tha+>VJB6X% z97inzR;aF)%Z%0@7a-Ui8Gc4!i^WVVhN`1W)nslf9qo?$-85aijiX5!Y+@kMV}5eK z7y5r(aF&7s7f}ED0@K^3#ROL_#Y=uNEiSq)v&jcIY&uZu-;AI0(4*2~5qMyTz8y}& zZ{4Po?P#M;cZwP8z!|+ecY51wjfdOwzMSJqH+eRs!_^Fzv6s;c1`{KH7O31V#wm^o zbeN<{!Kc4MJ_U+?f@0H3&Yp$%Z0yNi@X2=}B(5yv8F1T;vgx8$AcR z@mTi8gY$2EX!-l(mZT%>k=>tbS%08!<=fU^=3t%?AK>!7F)pkJ&5Oou@iCHSVTFG* zITj1G7EtdhJas1XYzof8Qf@H5bv|^OnJnb6@;)TeaQ@xb^$%hdjNCWB>uo|&W*Cu_ z@Q8SD>!Xs%;DZ>F^OKxoQqH!?LJbr}iBFUOTgAxI{{$YN^4TP(y~-hFLQq<5j~rxt z9_rx(eY`^kpLG~VZ9Q^xd>*QI=y!h)1l7m;ydZ>?aq}snts#XPlO$*;r@?>QoqjNwzB#aEj&MB_SZDcK=XPa>{g0JMpifc4|{D<}!a{bH>Zj zzasF+#THL-ch6XsX05V|W5du${)JLZXFlc=u;QIfM9*afNvk4FWj{?vSxwmU| ziSL6YP;73jiG^KjP~K?+8t}s#J~nqu`AYVakVa!4MWBEpP31+F<9Fi zICvpO(8TyFH*q!-p2B|_Ar2B2(HBX_5bjSP<&~ArFC}O?BP)dtwkzNlkO$~_V&$D_ zHmoEa`fuPhAPLaKixtgX4j6wUwLi$BJrHscWu5KjDkIA>v3%nJ%{=7`p?zR?lc8m_ zfIx;6)yVki4axQ= zLB%ug4*Ix=Ip|lkaa(qfR6ayy)WP>42es=*$)eNL;RNE@?)PK(z6V3(NF_7(xIoJD z=(7miJK!al8P#zZ9eV9T%tIl$AaHZpvUK&^5<$5k5m;R<;vi=i%5ty5z?_~ZdAX$9 z87r`)O9-@)!@_?=XhzhzDE6m4JruRjMfbs;)ES8%eeTFh0-HS&F*D{#>SZr|5#ff^g~1|m;NPwEJpU|$W_9D>?j)i zAR@cWJAC4ELo7mhpxcfMgvJ|IOmrbSz3%DELKq2^gJ0T1iOK7pYi*f#ow3u7J z{+S$whT?z9=HRBknwS};U^G1fEF{ljuPtjb(9Cxu4!|I07a=M-1F8m-5@U84GYWHB zF(EYUS=CkASI^m086!ngUq3y@f_brOYLKvA@NJ zQZrdgY#=?0B7;KoVUaDDikf}!08t+jt1K#G30i;rw_??`%u6NY??|zMaSmoEJ_Yb6 z;(M5|PTcH}v4PvoOqi43O@J%;b#+2<q2n@BL|g3+{7LiHk&T;u-XE;};1X%39!DzQ&RO&Q$WFv>P<8y!RpB7nbQ~F)l?ls{u@O^lZM;*RL5BZ5b9>HA?;7- zNNiR8>Zyj6=ec2y-Se<86vn|)y%;+um~>0}D5oed6x(Vt4H!sMNSR0HQTlSf93^6u z8uRo2QuZZ)Dy~W8KFk7t!UG>p{_fOL7W02YG2NW;E&-Na`1|C?*%|k1Ogj9*F_xo!}gk$N3#M9$*mnpuT-SrdK#sZV*L8ejp~S zw6uTR&5eB|&_7d%Y%&SNeK)X?bE-U>Vbm+kd}PxS^ps~?01j&6#n4MSfQ|S4YI1+? z!ao3$YQFoajW)C|v2ecE^uc%`iUGRW9vjpT^OM@$Kj`HWtusWOYs2`y(Hm~L2Ns?G zgT>ZqINSJ>i9=@GFLr+zebb@Xp?}p=#RX7_WqnLXSC~jujR_+&LAC3Ge zbK3qChAT#43%U$zRR$@-jtK^eUGH?$AH44Rir>(JxWkPZNJ~Kf;c+$B-Qom$!uNe!+XvY(arIK6fU~O<$J8a&$$nWY}g~S^WN;#jjF%6KqtF`Ec3 zns7?mR5>N4pK1|V{I`g*%x0BmFIubD;rVSg>KE?sb;~L35Oa$7$aB{-1(bT>X)?TS z4U?q+@X{W$cB-Nl^__oPcFU1OZv58Z#+8RCO<=R!|+A z`0q+b!YNXk1w=BLKTu)cKbXFO3q?RF!JGgkBg0@$v4t=v;|I(MEhgrKty}QxF(*)s ziIgUb_lKA@E>0tQT>Hz6Ze|9C>PR1a!5t1qRF&HwQX?Yx;URzI3N`8amU0_wly~Er zC|d;dqyUmh9~oeI13x2~m)2f?qsq&sQlM&`gib_3kW&J3>He=(E6ts~iz98o=l8eP zpyO2){}!3oD*Wastu{(a6giGk%=r5JDR>ZjtD$l$*Oo-?zhsV~{d%?CrBZ&yuFpI( z76!i_;7B`(F)xjlgm#jD#g&R;9+{ErG3P3UlC&yizc1XK&0|r23U?K|Tx` zA(5WQz5T5k5IjfDIyUJPd_B2lB$@&gXS6abE7H#*#atm;M#}FGD2yNSY3rqnm(!V6 z{}aFm%JP4f?>{H=`HP{ zixJ!^3ZNIU)x~(>_(Jr>41UH>w*3bWW#r5-`IaPkfso0>qljVGU(i!Z`h}yU4Rs83b=Qk z8gK@SoG!x6@*}!`LH2Pix&I+WwJ<&xUoQmo9rd5Z)iS%|DDabR1@LH?ivA%j&yujp zHa&IwGd)*`Zul^8$6ryAuDTrs<2XzU_7x^K*AToW6kgyfWL@=|sz=!6si5gAAU`v>5ir&GcJ zWL3mMb}!OkiQP_VP*N0+(nugAobEb#(R#Sp52h5wqKb|^%{M;NI zqUyc96<#7T`PE?{?28ntjs`Ni223_ZAwC8KD3VU&MLIwrS} z47c-^v+ZJv<(UIe=zMb|jm!UrRqc^VOoY(j)ul{fhnP{Wp69m=XI7ve>(GL}uV+o* zG;D19u}>|aITMUx0d?KpYX!9Mfz6R6@Q^*};2GcCxNMLVY$cPiT{z`Rkv2X_5+!tO zGbJ8bXPdp9^Vza*(qKh$PBnj7FRboq=NVK2h*rvd{L|@j#8JHqqGm8IW-YaqT!fG5S~(!Do<%| z><2&5Q_8159I=`)j=G*Kr$b-7$&4AzY<^)lOzh5|E`6nyBX|(_YDs@#SkZx+&5|w< zUSvr~Uo7dXByAEsslAd`YVYStTKK?NE2$_cNX{q^-Gig-E~(}8Q(e+xCV%ZIjW#yQ z?YW!!(ib&jdRHs0B~<~kXf;D<8n08Z$*)4fJ)Ol9%I<)# z2xlw|8`V*TyYnAUX)ekT5G%oga2;8QW$RIMpcpzb`G+j!h{M^${zBsmBk;)+he$~C z3sO27?pKdp+Q#{_LPk6(w3sU5a>sZ_CRBel3cV3a^C+P@hxvcI^4mE=QPZ?f9AfVS zF>Z~V`3Fv}U~DPsi4f&CoW?6bqh&UFON2Qy_zAJ~@P;!spD?+0r-V3H21MpWG<+xk zg4O}GQ3^n!c=LlBB6pJ&hzXFK`~w7E*t!&3fHwu4wei2&Tz->LC?;bp0n{go0=vB= zS``5=dKvWSF1ml5;LM74ln}0qx+E*WFKVvnp}r_BA&h*{yQa%~(Ot6KU@yAR!Jc)a z$m=R%7t3P2C|#yaz>6jgDy*P(U9`=HU6h22ESv12se51$0DnH3>m@Aj8SZx-V)L>o zd2d!6;T?d9;SKmSaBz{>3jQekXNX9|+_K=LQV^H{PMCj*7WBV0OoEy_Etoz@jE5R^ zq6KH$3(E`OY;5Nu(h|dMj5C<-@q$+as&gN(a)hRHSL(yN?@$7Mr&KG>^i%C=t4XR& zhW%PGY0Ix{^WR5P$x|81o*BTQOUd7D#y!l8Y-VOLlkg!(nbp@41W2Va?K zA1{PK*IW|m@mJ#VnkLd4ZKM{j#041bFhRqNqIQ3Cc9liMls`G&(46XlcZ?nu<9TvC;Ex z;l3&u?H1MYThzvIQ5$nyR6ff^9s6c6gq%jFR#QJ_KLg-lAU8#&cw$VUDdYXxgKLS6 zBocMB2=$vbeW)Hc7q{?Yg!C6$W*Y~zrNw^(3k>RIO2R+n9eM=wfIx-QqnV|#M>Kp( z?uT%I;w^ihCM|$OnLg#W=Lu^*&2H8NM0!9iyYWzKM7nUkCq>A&x6*qGw(M=}ubn{q z75MP_+tR~SUP#~!Bh-hZvdOq}#pooV(m;1!gBQ0XsNryxOl3mK>kl7PvlCxhX1ISK z^EsmcscKC?szbnhg=?5kW6Y=?pBc67agx1^E$SRkcmY`lC!-#63z!_UztWmB*Q$rzeKwTJk`Nc`B|JqfL<<&w%PfN?1@ zGYgRQ1Ogq7Te5EVeZIQSL~>n`t^6V2FU4~E6U2n2pbfCwkS;Db)Uh%Hn3jK~Xz2p% zt^az!nV}Hkjw9R$Lt5F-)#%t6UYZ|4tV6{S|M@bs>>Vy}nb=Qs;lQXDFJlhig)Y*v zMZ@;9G@|;4Kz(<`)-mw+!bI(TD2ZmZJ4iC3vT}o0ag&5w!mHkx41;ey;>NEHq@8&X zfGdeS7<4Y2yKErsMr>j5y!MsX4*tZFOS%w3smylfa?a)3_i zzUow_=M|7cF!i^oG8gOYqASHKc4tBWDfj+sK0M9;eee>r3;Kfg914H`?J%nH^E9zA z_!}n%0)BA-+pQy+qG$%`vci5@GexS%UOur3yvBW3bvO-YR(EE z23GMw6;Si+VZpDp;+-eoC0zHuO0m65c~u&{{sGNcV^~0rI^Yp<=!_sxaJ8; zZ*waeNJ6zQvXa%(C}vdFwkPP%lxj4FW|XR@CaCf1T#!pMhoXOT_((CxcgzqcSV*dH z^tpxnX;f62cN?ixOn-Do$3No3-ahb-^E36kqcHS7-;E56c%YZt!W3y+7>sM=ZOQ>U zq_w_)`w@YaDPj@bk+Yi|@HGIbz$GA+cYT|rJ|IlOuoEF$Obc_Svk zJxHPbNOTx_=CgnOq{~Ni)y$YB8K<@EQA8p!SBem>r{R(8si1QS!SDh)K2^4xnDwH9 zs}7LD2xczfsUVSy_>a%|oy~V@qpGEV|ipHr61T;xY z?=~a+p~Z(7M*Z|$iRBp)5L=Q#uf5aM10d zDuuw|Gjvs;!GS)4@V*E>HJ}jWKy^BNC;}4ze?{cjL6=&yrGRz>(?v%5QCA2D`9UEt}b~u5Ogr9NKH^*y6`mTNs$o8E8`xBCMdx%{vhOEgrJm zHuMjTj7!43^ghld&!D8Yyt6l!VSS*f1H3O}{KehA#Gib@Wz*T)Fy-iy=NKmhQ%9is z^SdI;51I1`?c{GnHtbRmD&l}8yplIo*5pLPph4C%Dl;mo4>d3dj=j5Ru%#a#JBn8} z!*c8c&%L34@PJQth&qE-$B>@Lwg4?gSZ*7OI4b3q4NOAtD=9aHhi1;BGHSR|ti^ z{#IHCR}96DRO=ec>beq32px`~xDE&aa^Oagam99jP;mk2Aecbo+4*5h32$x?U-*)gv*a7JMyT9+l8*Ev5jlUqe z5=eZ18~B1i=K9e@afe&HR3ie8PC%YrqPP`cdkU)|3ik9=A_&01(-O-d(ZF(~(3<+5 zB<3iVUPqP0(&)!%X;8NR!-|u$g6x!zjc|nF8;Zq;yfzhb;eLkF6q}kr>l-~}l%P=7;b90zX!AiKC?%i@VIGM9% zB_pkzX9sjPx|u-5`4A=y1ZCTo^DbC}xk#@JYB_SRGHWF=l6)TFH^X|#9xGET;QA*9 z4!d&ptXSUtt1Lh~y&u^qTbtOowU4=0lZUg- z^$`b|34s=P2NH5!0FC&=vIYQJkvMcvAZMv`5yKZF&Vz`nVyim^?X|&8DJ;~KWOMzU zijD->Q89iPhXw?DZ4`OII{d6LeH<7YbVX zbogS`QOnhq4&#gzvvY}3tlFc|(1iVo3N5cm(|s%Oh4E)<0Xfyga%uq30>VLmRz(0X z6W0?6Ll58UdJ;k-$Ky>F9KdlandynZzy=x{wLj#D;f}qHJEA}yY(qGr9O1-%_a61aEiSk_T<5a_RxPJArbbn1%7=Ay?f#G3m@ zF1Jxc!>6E#JJgKf;>{)0l1PYus#O|6h<3@4)Ktx0k6!`ERAuEnJ{-RF9kuk+%SsRk z_R+GcH*JX)MKdaDTEl}^f+WxaUfHh;DtjS5L z)$BUi1UaHaFz7<7HuI;bv0NW>w;9aUlOYO7MUN$7?n}9$iSL+Dt^=okVn8&YrF|UA zoi|0b(aFUnMYYD28xNAmeI2Zj1KQCNFSv7pIqwd&@_{N`<2kUl3e(*`P7lkqCzC*J zo-L7QX1FKklKCvFZ`1XIQFOtl`;`cLM(_bml#9= zPk*SB-&q4gAP`8v0AYfYX+Rc<#p5|feD;vN(bJc6pRnA&{9mKJhjpN#+n~=p?i_eB ze6YJcIZ(aG&11sO8T0rA$jIykx@NM|FUSTeFF{lVfB1~!@#raD5k<$xBctV4jFn{B ziJu^uRKGgb!*+f8T~xg5`xko2<5TtRiwt(RA9|h{VE)7Gf?oq!ufK5XXa+2}LVxQ7 z^NeQae*j-8J%V;ghwTE!2etRD9ZC${r_Xs@z4bk#y6kax>elZtNwt%62IayL3>x6v zZWqLmiy;NyYzXSEz$YcHRto+VdLv+=%h{2`I529{wVAoX+S_hGFf=cOH3P_Eosa<^ zEKrrHWip~#b>#-zV^k?qVPgMYjDMQbVN`@N zvSaX6j}zG?ml4bBGzZ_}Eq@Sy;AQ)#Ac=ow;HLZ~j!2qN$)DYMV~@1npZZ0ydL?QqWFQXpzocRd-=5D_}A)Lo?IgoXZzUG=I8|dkH^5 zaocDw)q9D%MrSsFYg`qdTD>%0uu{N5m&)f zt{29hr(-nJP@h>`Rv6TLidEdEki}CFvNm+)PHEfnEAOxctlcSIx@^tN7S6T>S5U>x z+~AJ(rsF6aVDDYtg3|H+Po-j53zoAnj($U+482h@&H7~8l7FFQ`%_mzT(K!pk%xBr zhj34HUo3cEMS5&Nceu8Kz$nc&ga&L`!$NjxW-u)wr7gUz0PFv_>t5u1-w2115x z-dH@{l>EnnC+Oj;CLD+|{-4;-=3HR7|I|K#O}n)Fb7GaSOjDY}MyLyst{U5Z_Yga@ zJP04M^&^pW_kWYjyHv?;qfG|d_s}~1Rm-hbdJWMpi>cVhcnxgC=IlbGS0!~ zDx}b>Gp58M8&f4J$Rl12?8 zj4P;vIOa_4*NbM|aImkb7zyEe`EbFZ)5o)SaCkv?Ef$WVz$dEHUMCg#I0RN4 z4VEA&403u30Ye@cwzw)}JYKNJ1-+yvX;%zdgnRwaB0_sd!+_)4kiFuuW_6+CCgL%d zaIdvcxtxJ#_&4;cr#-#Y*rF>-d=#LzMtX;Q{ePh*iVC*~k=ok&>HXeFN{j~hMhC6{ zF(Xn<9(jobrI9Yl9OZ+E#}v2&5{R9bH5Wv*0?dUym%q7Jf9;T^(;kie!@)a?FE1u9A?;) zGk?TuaXX0>1Y=33iyYO~a$n)4Q!DLFBtS&Nta#nUqQL}EyNS`1qWao`q`&oML%Xg9 ztoVqy%q~Af+2|!4LES&7peZ6iK~oG;#uO66=RS7jx`?=#o=_Zrrn-*YL0*p~5dAND z;Xv*%H=a+$ZwCtYLqk=KAFBEj{>iLMlYfBv5+LUVk|9XY!|r}S-kZp0Qcws(CHSr{ zlo3=mpznB=+yK?)fK~gGC2j+(I*G#nSb?R;AmV$Pg_BF@gWYpA`5c5D8iNFSgBOyi_V}Lxv#Z97(d<^?--tmkP%&Su5++b*4)0d@OQGbzn z&i=x5U7rgC07g93TqRn#ph@?ySEMcT=$OGN;#8mI7ez_;L&Lc2xh7X59Mf zafYA20dPcszETYFW(WoB#qHM3@k@=0*yzNR>GR!n&B6B z!;5Aab^l#k*%Au36F(q|LSGphYJW56N!&ceA-E?~Z^f~KZ8mGnVyaQMr0oIH1<^o7 zUg2L;I7wHzrW+w2tUcnN$6N3{b&9=jAx;$!2#JhIPAqjVeZtLDdi*a#9asf+=_5A5 zDQSffyEImc)$$U=U^qTOj2k_n2Klfgbam%(*pEp-WTbCk*y(q`t9(37BYzDOai;*LJ5O_Y9 zwJHTaE^(w1`|@AZXcs`iWp9VrAL4JL`8sh_x47B$U>s19+$Ln|YJvIbcPsn$@fE+W z9x|wkp?^)9Ai}WvxXSsn=(qGomV9XbQdLjCiWi!tkF0u!S0_x_sdL&VNQa#^($kW;9oH zZ~m$CzczeA<%GX2a;DsW>8j)*$ey0*p5mRiFdWuFXNPSN1ZV=D*|J~%ie4&h_cZZR z+s-9oJOk0Em+`+T;OdsMGOR82^;`$Wh)LV@ z)XOzzPf<_TMMUjhkIRz@nM3^ZrNkW)s_s%Siuzn6jp{^LtACw|tUX{vw76;kl3{>R zO%9m?R{mhd^hNO1pmEy^sUydhWhu4k!#t zE-kP$)tSly56wn`Fe;PWSFEOM7`jY<&TKA8Vw8$SyA?aSwA%>Vc)k)o(B^5PBvA09 zleRDjX;!Z>1mop{i z9bOCPmpr$=$t%Wzd;aTIlhvW9kdJDkjDvV&b}t@;=zl`UR`cZMI>HV3i=t8g=WM`j z#$8k9{>Zz~!mCcaq)-$?3SK6#lFER3ZU(&S?_|15f+rNm+5rz9SKGY22ql!0aME_i zTJnKiT(Y<%wCFTK!TksQDgTY|tIp`*diQ5B&iDk>FY%cD2OYOwf`QN{a@M5{a zjQt>^Ch^xI0P%umKy%$>`oj3fO}27o{!s#P@#VvIKe|?_%MqXy6$%(Zf)pQMSvmvF z56!A!R^4RPoikUg|C*2h7$iQ+#NQ52?NJIsE`MfgeyFi>l9`rHGLqU!_J(+p(QP(Y z*C7D#{WxuirH5F7i*ddy| zu76~73=h^^lC21QpVk`0K`vQtgxYP7N&Cri@i94yI+TP(NoE99)u4c>LxN*0Yr(NN ztU1eS-FmFIyx2JAcf|Pxgpa9R0-uPKiFO@(onFTR&>+>9K21xEtV2`Qo#3gHj=8dk ztM!%`f4>R*;Pg>_>ti8%Vwrn_O%=TUiGPuapHKq1bqwTL1O3i1d|t}Zc5K_8<=KRU zGX*2TgQojxa(R9SZ#LnM-NbPl7wQI?W#Fzj0&42!mMVt>%W zATQ@`E@Awz9eaeyGP402gM@`cAJPC3hS*FE_LK9$+B%WlU2Gd=$!bc{*qB#M4QAbG zu7XSGRL-&W#U@#4(+xb?a}oK`7Q3l?M7>$;v%r8-T%bBy!`C%E(+)I#q^9G3Jl5&_ zZ)My>*6(}(76z;%mL_tf|E1*TvVWm0Qo;IvC+1}8fL%QE)+f#gN7bSFhB&Ha;2A?P zA|eujfIm~-Oo!#yK&LaaRFk(*bO<`+AjIBZF!b>}9z60)|(h+UdO zlEazCD>4v@udP62T=x_@{C^3dNs=Y3V6j3cT)YWXytyXcC?ehq$D0Q9i$$<@DbtBX@wXF0JBX+%5`_j5lK`C|TNAiv zQVqXbbD+4ms}G~pQgEPf>_tF*hhRF!5&`HL#eiq5a8 zw7mjE0${KSsj5yiG|id$}#rGE&lEE^kT@Ri+#yjR7H zAo6(!1y~nRFB}m09II~YPsmTF2)@#gZ+@Zc&@%F2Ai0etlACxk$saCr%OlUcX4O9a z#1?V7bg8Pg`NJhjyfPA730q6hf42i7-vwe&JtE}Ss}CZ^jsX&OZEKQ!c&Q*kI>65@ zSoCu?nuuf zs3$;I&o9>ZJ1we;d!v|BJ(!o&osKyh`i zRu>fL!@FzSSr2*L!7j;Xm_yp~xx+bUitZ61pMxUVkoJINdS+<~uE_|Y<&7ko0`flR zp^vFoc7N*%-XEPAIh9sH%@uJ*)J4$%stPS%ec%`q;3q=;GM_$%U;!djZC5W^B>HlB zkEcb!j5`_v+zQ8R$TZ2*zZkf55Vz})1QbLYm!A19J^XyhdIEEA*u3bI^Q!~S1QhpN znBW-q<4?Aj4HI@={TY1%6ud|i_?yt-%(`@wOn;?={(ft7&$x>_<=iX&!~y!iO@cqo z78?~K0V0P2k$F@dh(o776|#xlS^@|rYUtc;g|lY*x9o&@7~14tUZ@)g+h-z;5oPt` z(4C0o#(#qB9{3rr>Nuz#Q1FDotiU6O8j%9L^YgE_@GhZ2&o2BcV&Ied zDjKm!>~kvX36#MK0Qs~)PU=`*uJZp9Qx}QH0$u2_iQUEv>QTQB!7B_Hcq=L|3)E=~ za^?sKPlMjW0)=AaHwTB@t-g{4x*OxVY2ulx80j-@PCK$IyQ=U4H(1Ap{Y z(v#-$7`XhCE2E{;?to2PqZ4`uP6L6BmNu9`U7`gL7kfs;MG;6``~VXdbw!L}crngT zFGh{%i&2cf7&SsLM(+q?R6`#MAw9i=EXZ-N1_QT zQ-4m&Jrk3j8Ya6qGL@f#gG@V%GJhbSZy-LtY%%WbWVW-&1DicV@BrGOBW9qnM!&gb z#9Inl>0Z-mxK}j+N1%dnk+)XZjoK~9$NYw9d#&vly zjv0kq2o(+9N!DB8?6w&D`t;#p&qwHVdMFo5Li`8_G>r-C%#2Ht{AI>s2!Hp=j0-wO zdNQN!ASWS&KR;$1^7IHs(m+ZxtZY(X61Ng$t9B9ueQYPP!r9x$UfcbdU>6Td9Rp@8 zQIS=#-mD#TB^Ys-9k+He1MY6|F*Vs>eyuA$^o%)=(@qc8|L})SR8M}SO??rGAVB`u znVj3fNoN1d4Spctfw=w(;(wq9B(;W(?B-GPaVWxq;3Yk0m5fCrpjJMMm2OZSf~@D% z9t`06vjt_(a3p0OjCKVJcZ?g&PwI{S@5vyy1GeIfk+PT%1dshW5BjjzI`k_>0v%Oi z{?%}#V7Pg$ivyD~|1BWFyuqi$HrbR=rX0U{;NvbN8^73&0h|Fed4B*BgbopaeL%B# z=7cAMNpHO6I)ifoi_*di*CgzY(laMDmghK(HVX5Cj8(B7h)(pb$WC6G#w15I_*1 z5z_;Qz?wIO87?VC*zZ0E+p&~07rrIeWf z*Hl_5_wyH2CtAfGubz_JjNAI%$Zh)iUgVxX;dfm!8c31HeTS%eF?iQ(*w%{vKczY&EK{W$ciWS$?EdkJ4x(IF{2=32k>o# z5O!Hv`g(WS+rdPfzV4+=3VoI0tIviNSw2Q3G#kXGC|IX;`$hnBj@bROnQoILQTjTj zrO(%Fvzl*ZUw>f6ZKxa6IfLyFFBZLRi_lf;;g72~x4jz!1_T%6)cOV6rU+WcFIKY- z+2HI($nsiwGwX7+eG#dJl6k!%^}oUr>R5X$wRtW8vvUy+B)TE_^bgawOEl zNP8tlVSnS_D24jber!R)TGgzW5?Qo2q7*KHj#Ruv`$dAx=9H#j1DVhseB6_R_T&y? z&_2O@I1*^%gC$K&^^2cfMoqw4eBEmH;t!Cux7g<_%7VnSZTK2;+aWy{Dv=}zo zrGFXx=Q_Yb_l^(+TJ}O2PnAXK_Ogd3C}G@~cvvw(z4JemVDO!M&+|k2o#{7Xg&_j# z7!Hfz&dQgTBH_pMa)ZpZiBnf%>Ffmjn^mm&K8r{9S)yd5fxl@;pCvvWq)p^zWx1Zq z&ToZ^_?akKw3EgFWkWm_%qKX22rf=9D1R!-w(;kaZD{6W3y&8&mnTo$;8BiGziwX< zNMV|*qLJAv{TKTgKEvwrcSD0NCS$B!)I=)=O71DxWgqF1Qu<)4eND+c3=897#SG6& zS9fUAv$hkjBhA-@EX3)v#^#CQRjZ0!s-Qpq=p`%yk$OoJrB=iJT>=!{OA-|oMSo;N zV|yf~ibgXgCYg9tZE3}|ON2eGylED-%A{6Bnp$O4t6$Yhk!)sH!zQWc3Xa6r3226a zhaBVF$vApA|0cHkvJ~?Adte(6#;THmXQxqm*c0A?+`rWOSF?s!X@ik1^53s3IDoui zGZ>Dt0&6Z7_NX7mEI@A%UY)$3rGK%kdRj4k%)62>pcvtEaCq->^(;pi_foJvbE=U> z-a@kunSJq3QqTD;z9?wJIs}QDrA0!{1cuvSO{R){ZQsrcp*~Z2u~!@mb@a2gZ;Lti zkPl*EIRE8+T_#J6f&gWsbserby?2yIaCu|o)Q&k2Nd4e^c<1l)lRR`q{C`5b+fm+S zf!zAne`)DEJh94s)etHfm_dM;`e+YWFlj9%T z*YIJ)f*QEP)VChP+s}v(roXBQcD*v;L0%s_C&bAGMkG$Y4dR%I9dy%}_ge-Xo(nrM z>Uwmtexn=Cj_7G~8elMCV}Bo6hcR_qKaob@$8T-{5Kt_}R)iPEc5I5$ps+Q1e*h8( zNh~gy?CO{7E6XO-u~N(YbjmOyHW66dHfPL^=+(a2MC;cESi3IKk)XWtyISr_CG(6Y zzx+Jm0`h4V&=l(_;Z^LJ!kWQYC>9O8`#d>L4e}}HaM2iZdJcdN41aFktkxvW+8&JK*;j%1L3Bzr>TndkI{M=%C=s>wq@3dj7nL^xAXag-kDI|jd8rWXYxt!g=b9;4@ zuC-Zd(*CB_C4sQSaDM`iCY2D<2(cl2JO*69^PlEi;)k z;X3@`YBYo|ts>2r@L4vVfZU2>1J5uMrkuR!`R%hme1(MVICJe z=pQcuS*)ZZS%0qa<}wM4DusBPjhvCufd{WV4`brM#jyH1F` zodA}vx4xeNI3yqvR+VU{6IuGiqXT|)ayD-iJ$*bT#`24o$$M-97RKPWO+%b}yb=&w zN$3&VLhJH#8Gi*tN!C&zW&v-CRGUH+n!;d{XOTfuV1GbY)w(HPzN!FgDrK5+!Hwh? z3}(e(pjWoW&@l@phgdMWYJII>5(Mppf^jIFzfCZ6CkVR)a3s)fcdJQBAH>Sh?)&w zW7GYRB=+GEEtRt{-eC>2j(0Ljhs@;R9rzmFnUxwfW!#P`_e+m>2T#$<0`vxhXdt3_ z;omL~UvG+G!jm-8+Mv$)2+k?uq-p>Sq~}3s1Ah>K^GUr0V8ZXxGi(IuejjP`*$HIU z3#0{}^B&|a0o9rNqN};Hot)nX)#y134=67N6XZ!*Z^OJ|r87jQ3-%@C4y)GTJ0L-R zIVU1zJw$fEcs%UC5D*(Sx{D%NZt-Z@5Ix2(Hl1%%4cuXwE$Tyb_F?2$y%06CBn7JQ z1b;#`yZl{5&dRb*3%GMqAyn7{8{m@I0^Nq!ja8M6ISLu0un(s)Lb;!CZHey=8kC42 zO_&4wGph68xe}9!My5yCXw0j|^991%gNW zE6~g_+eJ<6r22_ee4*eu5Aw)=Mi0Cvut$Z(;U$6)j}k>htihp@K7=#vcYs|hFuvg_ znlgmRbiza~;FqgGs8#mat6-=Bt5YjZAP0}25J=+EHVKZ0QS7PUEN~?nw~y2#w0~=i z0{`ie-tra5A2mS@;sp|{*0UT>HwpN(VfK)-v{V&+>Fqt9`$H8q9!esh< zC=i+@JOcxQd7gw?)Wx<2^aki{H-Ev5l^_opxiWYY7|kOEF0$GXhhlLJBV-3%pme#} zU1|ECpycl)-h)7D0vdp9&jDWfm+|EAA*MtF;X+>?p0&4(PQYOPe9%X2w}vewvHr{8 zjqG?`F_NVBmhj<6?l`>XddLSMXZ7aCAfr1q2*rcaV!~}wBv^+Q<9WIk6`u9 zDAW=3BYb``aZmT;SH>U@#7P@uy@=>oq0Xu1hYGC14g(&Wj0(hcy&~|xZ-q=*fOYO; z><8cA_s(#xs8V^vvBu<32Y+V&IsN_xl7ZbMI8$QKg2Y^=`U_4@o!=$&5qE(IF+{)- zd<*jDf7nq7kiZR4TZ2^0MSl(R1BB>6a4;^L zYZxr|guX$Cpmvu(r<6fNp)|c;gKk0&-4fj;9K@T1A(9}dKf=@*tB52`mzZtHIb0~2 zlokjo+OplI8%ivpV_z#)8gf>`*~r?T#TYW6baP9+tRf6ifC9tvDwOxpQ+{ z8u$}st=b>xe&@m-kAIu=4KCj;rGYg0;g}p%WXD{n-jiKYt{C58C`j|ka1?O>7CDBoZbc$`$*(V{VB`i6_23`yNH@Qe zy>Yx1c(}LwUQ)9;HbeII5xEhU==0r<3!Si5R#pJyO~|%^0Dli+{cUf0K*8Lgz)+%< z03#?k2bE$7xX+b9C@c4Vu6S&6bnOIwc*w*1L-;)!t4Ye3A7I1ZH}D{Ot{rVCeH2vz z|ME+qE++P$rV`n*>>BWC#*MUraTKX44AZ9DKyA4rY@Bk6BT!@Pq;buo)DFx47*USj zb@!yWJ{~#}J%8<$>9Blp9@Euic)0m}(wj>6+k;K=l#Mdy;j5#s*I}hC0Za9%F@_(u zp@D%-1vko3C5QR5M~#iI0Ckz(0Xo8pj61T^+R#76S;RQ`j2d}a_la6_IasEq_Od90&vhK>&e5AP@)$0>MoH!61M@AP_VH3vG))T>Nc?j9xIPU?6Ysy zW+6cAru{X9Eyo?c=h+CePNqm>o7~w*EnktZ3V&gQY}XCk3$kb1-#lwe89EWvK=EP;knKaNo-QW>$WmKC}YGTZyfakO^}Szs*l-k z3Z!Rk7uCi1cbf-(sOhzf(lqN&cGI!SH<`wePW%e73cS?tN=K7iRJUewQQf}jH_ba* zRDaLlw{gedq9PanAw!oHI1WO4I4r6kD9o9JA{hat!?nPjq-{%+z`Ic0CD3oMeyka;CD$C9;e-CeZarrLX_;mZLjUc1N6qjc(vv3 z;pY&AH)cb=PT+eq>Ftx$F@W-!H3dbAD}Velc@p;^)PtKuWo`Le=#eMA1vJo$`jyiU zX$|Pa;(9I0AK_KzG`z9FA0_pp7_&y$*M5XF%#g{a#OkQW>;6!CM<5v#PO>JDN><(j zhwBiKiST6i0!^AdyaSKsms-&Ud|pg1S~fUX7*HZ*sb(PdpSws9BG%C_{SccDv}rmNqT`{SJJKQX_YM&kOO3>krTVMn}Fw z)vL?g7fD9-x`L#1>J?oC5xg7Thkx)4*GxIOD*O_Uzzx0L%^`XrmjiBpZWC9PK0PBr zM{y_9VN8)8e?jGCVFw7L+Z#{M0tHP4qf|)I6c9&Q?bD$+ARbM2A0;_?nVOEg*UiG8$vhO+b0Il@Id(IrRPta}{hL z8qCnyP_Z@xJ=68R(4QAF4!lB#FVhEeR?RX7TEXf2@a%L!&s`GXfaO ziJrLhz|Ua^(aDgSOn@Tjg@|Z+iGRGRZ$!9!u&pA?>uZU3$lQA;YnD`r!|mbuo67TKz|13@CMm18C#Jt>%2K2 zhIP!jucBr*&s{*9(Ux?DDM#BpwZQ7?QT$USn>JVA;7|g%C<-_HNo+B#)^xdR4vN5r zp$$UO5~G^OuCfOpHAiFA5H-IzNN+pX^BB7x%@NVe%E4!~5z#y(fl%@#(3^k0(N7XM z0r6%IW`XXgaetfQHgTELg)|!?yxG_)^N@tl;>6HX%r0aOh-49#;u;fp%##<=Uv%ux zQ>B=D5l&q}%7>>^5%V7d4j||g%NRVsREteEkF$a~rGc5k$<7&e;Fl{zFpLtO0Kh4} zJcc3V-(MPC@A4A}O3}Xf$<(b=44}(b;bGiLj>Chv+0i!kQOaBT1*OHvmBOA*0iEBNj#PpYDo(zmIJ(8h~)-N$l;bE&2Lks@JrLVau5Z`|>nW{dY*3!4 zz#BqO`SAdqRy5C6op*9!AvOQ-A(kTQD4|+R-gt^WlgAp6QP-X1f3}^e9d@omHO9?D${+!j;c zc+(+LZr#(y@i%Jo+?2I#L#JlADRp?x>3{Di*YFnMnhV?6Gp-A1(UUMUyUjRgiS^?r zE1q8(lJQ35!XUwxMXF(~6ge@ijyTLYTxmm3 zx>}?zPO8G_B`RF!wKyI|>1AZ8A6gvhLK??DQAMc0SG@2~UFC|yR@<#OTbOmXwto`K zNGjfsSJ(*lR{VF0FB{Ct`E7l=Zu!&ZRiNGo#Wx3Za{w%#T5FuR*|wrEo47!VR}*&$ zo6sjNG99qQJ#h)UdDcq)qQn6VU@BdD8A&I?V#Zkh6vTOjWS%t~2w@~Ai5SJW%m9X_1b#;2AN)TZmVPPzMioI&gD-%0B zA!cDj0JsH5hp!f3VCmO>Y88IN8S#pmF(Ro@eT9dGQFrP=M46@GnFxgiKmaJ(Gko;` zu4UA9gbEnL37HhtVK~GTRTz%ffv`y$6)G1_k0ka%rhF|NHepmn4Ukpvc7MgJB9x*} zDqJby2{h9oK4pKx@io4{fFhDb#t9!{$i&EC73m|`6k&35+`b6RkuVm?GD+CW5MBvi zs*?k{{DN>33XvG$`t{6%V;P85K{T7#!EXvC3w3*}XlFJ!Dv((Y!mj~KgNGj24gi;A zg8+kD5b26plF9`a5w<3mAAhDa1^4#*4+ZDOKf&D+z>XZFZh$P&^diYHs(lNyIp@HM z5Ez7Ymaf^r{R}vVv9%jm6)kW;w4y*9tAc3?Je~#GR4NJto_|0X!%%q+xT*q_yHP!k zuaF2$iC-xb0atHvAmE+`6j6ogkT13Y90p5a1aOPq0Nih(^Hdlv27mo`;DLIIWl3uP zCMS^gDoa;Xz1rV~U#jxzZwFQL^tb5>Uhz-#%~vYO7$0DH3mezabibJqxGtXTL%ht> zZ%WK+mt3SO2mQ`lfZcu+ogI_EnlL0|}yso>;W z)gTy)Svinzf`VxQcr63$Z1`4)V;C`jn#Bg+xC|?tuB{5$$tD@;`hHyMQN0n0lG1vVX6I3>xA-%pcU-l!XaE zTCl;<8~Zqba`o~Zdf$G~YxC75Rm!TxeYY5J&7|aQ-gyy*Oh=^>w!C2VippL6Rtui`?y7X~kVTu~O&!J7*)QRrT^A9$l4 ztdw50N-CqiJAV-My;t?e0|Th&mA)YlJd2 zxxk>l-R`>tAxvpjXj0Mu6@8d$pov&z?P2St525!u2CVD;A$A8ixOxk=H&XCz zSOW8|-MW!M0FZ`<9lIYbf(P&^@0SkmTkJZ%H1^z#*wq1YbSrx2 z&XoWzz?m7K%$_NN1`Hq^p>E+u*#IkinY@?Ob^$Vfm4bvIAt(tH0t6=@(h$a*4wW?R zpaNg4myrQuWPhluzfDvnSl=Rdb8YN^EoL zY*76~-bJbpOl9rRT128hYhj^3Yq_z(Xz}tE7cz9%bG$3@Q+y2Hr&1|&Bwl?e6|VfB z&i#mgyCLY*wv!>~)qLzd9KHz$O$agXLE0QXc5B1CPY8LQdkz5%i&*kAn(LRavflEs z1lB$96bT7{Xn1(DeW&geH{B8ba)0S2ETH_v1~kErZLzqs2Lrh|)3v7juAypo;ou zNM{`(y9~!ma21G|8y6v$?0*RQaB_@%s>iZ@4t#zg!;yiTQ0C5{JuP{ z`ik&DBP-i!NR9bGT_1Hemd-H|9vE_nHwu)a z#e@TAp8FVwo2Lo0N!mVirpEQs%^W8INHRlOA9Xpbj|3OKv`UcxMh>D81wVvv7x)qS zsUeM0&-9cCK`4%UC<1>lA1)$)VwmKBw94k$HX55!5@ z;GaAh^B@VK4H%uv2mwH z1&$oH$Q~(LQ@>(!2i5fmMH`H29`wR})+0%Uio#l}26--VAm!8Jg@3MpUnhZc7kGT5 z&i0BnGfL4TuPeKXB2#pH9%?+{Z)ldhbsiHtLYu&lBt7$pFeaU{m`6Fu!8{VDhl*4f zu)Ot~IUQOf5U-WQB*3D7xYzNoCHHC3loP$9 z3#A?30V&DNI((=RJ&m43v=(Srpc!ksGs@jI4PzIzXe%Y5d>D^F+r~XGRxY+UN^9^l-zo<=E*A z{yC7+Bgq7m2Z`r@mqucN(t+TA=SWv#HH<|;HJzcka zEd9%Z9xE$6wcU41`Y#9e9I3mF8IGiZ1(@@rN}f3dv&xYuV=8TGJ}ID+qbR8W66CHl zOOD5%wI|5|V$S~RmT7+zE>(Cr916Ixhu}}Ro%x19;53+jVXlF39IK&>Vpbo)1Fptl zCAc_cGmhe=Q0XD9?k$do)Y{JA(Q8|Jkcy3ZqBx3gW@>?@i^7P*jx1bWY@l5Ia6D<< ztSERe9D@p(kpN@Ka2q)$$xt^Vw$Z{D9LQ<=3Jy?N`r7S9=+l&>4B3rj@jH-olyV^2 zLy&F!r3^KHc}N*~ZDeIWDfyUe(8W@=TYc+IdcCoPIjN*!nY=-CSS$+93vWP3ij*3R zw5B)hhCG@qD%ReS;kp4422F!cWGjb^R2%doylfEMRE8qn;zmJ+c>kegvlSGcN;_3h7@47HXJ5@$BKys&eF624mamU%tkWWxS5*o zwYx|-8wrt9eaS&=%!Y^rSu@$LKSrmW$$S9J6ULG=$0XE9h@1>!vjKsyK`k?!3oTy& zeGPnM8K(HvRZgWWW~P*qY7`8)s4+4v zY!x6?W}k+CL|J#NJjt@129u7^CJ08hw}1j}6*uRYh-qln5_;lE62j4fgarf*7NIq1 z%r#^z-SSKB6B0**R#W&CRiM;=kjpD%cZM(;BgyOtX-WwaYaI;JXCgF8 zNqNwSrX}^%CsF7g7?UXRXT$(cKSNt$oR(6ySnL`5sA4hMIdPlK)An9 zn}Mf*e`?0n!G`*19t!}qDwF&!^dvW2wBe4 z@Z{II?ee1(96$tAaCBhsNmX|1DRu|wjdV%7In279dz7ON7PAIK=_t|elfSIDmvvc{ zwv=v3<*S}(-PolQrla<|ruCt8)xbxZr;YmFR=1Lq0pypke*q+aFh^p<5GOwpKp+qZ z3W6{QLI4sV2*Mx;ngBr%ga9Fc2?#NSNW@@h-Z|(J@ge!1MLpV6%~)%`*kAILi*9yg9cn6d+-S&r+~fhL8Q8R*YCb~{dKkN z#rtE$>$)vlzMIN_hHG^oN?U?HyAA(#fzBYX^dLzO^?JjV_3>5~6lgm*lTrs7AH_Wq znLdlv3Yf(K;u{pKPP^yU&?Kv(qx+1g-zX|Xa=Grq>aoV%XDa$)Z2t>pmbLxN#n7XK z3tkc*g-Z5}`1JA@9mL8siMKFdLT&Q^&`Ls17_Ad9qCP@@m0fB7WFe+Q@YLkjHTRUR z+c?)E8I&$MJsg^7;~rGK**%EsNp+3j{3p@mmxD%hQe1F9n*Ylgo+IEFF%zC+PA|yO zK284oJ|lqt8g@wIB2`S6TVn>uks)0aJ#6anu}k;*V;>(^oUm7Mq5?B@44Hl0*z4$W zk&~yMigCApp-^}}8Q;R2!xxZmYM&Z_pS2U}C>Jf#GbOmf{xmf=a2&01^F@sBe>#*f zm;+(oK8Uc7K-`)$QI(r1%MT|(5K`Fo*r9fmLE(6o3AT$jvtzJPal%&V0q77pDZUPw zkl|$tnaI&?0muXi1Lw!YzqnH#6M~?>J0^4lHYdk_MAT`(JUAx!6om=1y=`9_6LrD2 z$e2i!L%tGYVhG|l@)QC*aJHBbQU(|+CK4q002LEi_4O9TgkmM`$D5vxUVhl45wmY= zfjeFdNPy(9H3U0MR65&Bz=jE?2Txq%4d0oKP2Bqa)yIBd?>7N#fmEH5(`8JE9Q@!25ZV+7_1&}SyRj;t z?=+P_jCY*x{=#A*1_sH7Z?|z*qT8dnjI6np;^z;`OO7~&t25nZ;))anwq!93VtPg5 zI5f6SCA;q=yU|;SC`yOy9);`%W9}cw?l%g5Mx;D=e#40O#Oc#-A;{MU>Uk0)(7nLQ z5rN-jE5|1L>%V0veeL3AAQRvd9O^o{+xp&TUls*(EhtEvA*=}G^wGT(jc&U3xZKm4 znnxs|r7Ry_g85!_BmTp}L-&ZAaJcIfycC#FE)^Q@H{z6hF$Ivm z<9`5J96Sgjk;juyDV*a;2UeoSu~18YW^eUzI8J5>1QS!R?&4|2Jw$>mf>~#3K}TxA zzqTQ4pN@>b#h7T6lE%wao6{!dZv`Yn6<3}6*NWbe1e1qB_Tyo}vyrftOJ3CHshu3C z(G!p9YMnu7TLu`*&5wv^EVR8JC^F(b8qszMO zawsPw3%<6-WP;v>J~f0a7WTJwX8q5n9c^;wN=35&V69Lewxm7d(WF1_yK$uQ3`Xe)bK_; zhEu09o*L%qXBJZ@FGTNGGciAZPJvnU6*ZLP_>nfm#r(P)Kyw~F9-syh4lL`_;7x%t zJq_aQS8b=^wiz0A8raJ)8Tr5iPvAF>Ns7!oDK-s3gQolqG<~2DF}zp&pnn>=@W=P= z>tHt9X06!9JK5b)R{ij{_j%G_S_h-)ssdDu00y?qZFa^X&E^h>1>)3yMn6t)p@kpV zsL5p60cdH^Mdld|(2K|S<*v5{#uhk5RjYO?H8>(f60)lS}3_>Nd6egLnl3c>waI> zOP9qYte!%@A>EW%dQZ6OVc)Q>oLYi=hDH3Q+yM%!?^+x(hXmBa7r%%a*F0Dsoj2oI zU>y=a@tFuLva+KP27*LPRKHJ)ClH78S6c;s{0G`$=`91d7834%v_^mH(6Rj$MOFfN zwk?MN*SL`Q8u!Ho^@aH1r zWsMBeL3$Gm;;{gKF6z=5Erx3k!(B&jy8AcS!yQai7PS-}8(3ohh0?Y}bgZ#}Y2yXy zB;HMM9)?F|CjXeHaB}*Oqc4v!Kbk%(+++k0Zw72%uMR#ijY2e0BsI#nK3HWm)F8w>lhN>hkRQ;KF#URvjbn^vJ0AuD z*Y&ns^Fy%BX|Y(q{7lpCLsKg-zNNkfMVA7Bsx{-L3ac+?>et8g+b?i+C=FC#(wWvW zMF{B23jcM7Eimc=BI||mZX(I_ie&osyzG(b14AYdhaT6^IE0Q-LxC`3y+r$x8$*F3 z9t;+S0x)TRe3UyKOA9;~r4;25%o*|T3x}a3_v~L#*k$kYR#4bB*Z{GNj$e&%KW2+8 zEi04KkVMKU>QGrnAQ>DyDT{9OFy(5>LqA4FJ)A6k53kJ8&!r5D^$UipG}CZde5SG( zqk3C(+`Y{6CX3+1<`E|o{oTS2Y{@mBtp1GtM*m2EcBLol-^dwkbE7?3FTTIc&?Dn^ z21Uqrfh4z(&okJ+>fY#xNkvfOk>M<-wWuO{cu8AK!?RF>6zak1zR31u?46(U>LFoH&IbI9Vp!8v^fWkG*T2lVVEHMO+@d+g`Uc zkpnbltG#8mZ;~I|1m0kKMRr3su?!{M~aiogxkC_$x*S$_&Za zLW-RLP<38E^~;PWW7MtxjTTNhr^7Xz*5dbnBReGLg;AECz;#@;(_igK9Ik4&Cc8s_ z<>K@ZD0I0~ZBe$Sr{3>D0!9y--j$S=7?$^K;ZVgymGL3L6JZ5nE3sqHFcC-Kn=Z>1 zYl8Jv1Bj8R^!`i$C_?}U1P~NZWKa@^gW)`kp^zU0pa6pdf(!!$4Fd!N5CIGj02l~= z2-pDtAOH{m7yxJqAOH{m0uKYi#d+**2R^M<8a%>M$(gs?(Q4_4V+vdb3FMTmXOdyq zyPS0FMldLtqui00s(rBjAm6a`1o0ft7s%~gwFCuF0~lkP=c-xy^$^cz?5Abks!5)R zF0~ktZqg1d&4L9vbkWy4adL0re!8B2^xqP@s)l}+2JFd_{<&ML+8P2~)TtWf9Z+*> zqR{}?1bonLoav)#P+K~Oqp4ZIyGhA}pdG5Mv+hd#Q^PV!YIIT!P+sMJ!VciWthgow z81END5EFt2Ao3p}gyIaEf^ZSW4e=og&QRS5$I{edRTz?;M+#zO5Cleu&@l*q&>LBO z6D*Yi9ek|I5dp|r#%P8145=U;L9Ktsm=#`>7qY8h}qe@Bbq~LQq zxIEefB)rd?4X|)iOqWEvmy^?3TtTFo18-hXQE(RJ!KLKzxEQ<&JAaBMYFdBf4sQN4 z#2Wwl^HRMz)|(tm&$nJhKFjmL#vC=``8Jp$0N(i$zg$l3d}Hx?(hW0z3@Ig|U{*yM zz7HF{%l?YlUi@GF1LHRtxUdT_q@6-+JKpo~^rQ%#<_ zC$&c@34L;>1=uDA1Ozn@uteaB#?y}zJ1RCS8P6xlskC47naQfIERFSLrfJPL0uEU7 zflVdDQ!_;#ovmPVaKGh$%T>(Ld_CG|VcIQ!0E3ULq^WzA~1^c_JD( zQAu3c9(S#RCuUBhJVdwdCYs^Qf_)&O{IK5i7U&5tgS0d(qO>%BT4GvS*O>do-zp8p zdK1HeNH}ccQ^2T^zS};T>EY(E4BXyk^pA;F`d>E4TFeLFXr0g&hI64f$ z4UZYpkpJE%A4dTF=K!g_ViOGY9~L?o8~v@g%aO@!P`9cn&;t=n4Hon#BL{7Bt$4qY z&aA9RsQ5}1#+Q*+6=DpBN+FpZISk1z#FWSjgE*0s3*eW3Amh80!AyJ!CP^|!5STvd zB|yCT&?R}i6+Hq(#X6ICfZgT{3XGcSReTae*I7<%rUMX>!clNg$sn=I{;5Ct5}b26 zCV$Bk(~F89?g1}t?&)XY_I)5p9R^|>AR*ROB{ zkEcze>{$hW79xmicJk-h2Q7O2J`s=(f3ElI1}f2@+6YXKH*ops`Sa6HfSIV#ufj_A zKoEMP?{$IspVa&4CIW-;2t=xDe<1#YI-Z#bG9KYOZ->RvLz_JS;L!T*gMX`$;3xTG zI{NSdXyh)37*UzfI9$-_nS??Bcsw@)KhJx2fMbRn`j7Tz`Z;Sio%;{bo?!wd)M<4e%Ac^2e)3} zZvY&B5N0!Rk>WRCT|a@6>+?|P8#tYBfPH7t8~NTq#(M+DR<3>6-T+H`11mky%Lji%@o_O8k`b+B7X-VvxLIpu9HEKaX z$WGiLKtrHy+63LsEwh3_mbmh&0oMYw)V!a6lFp-vz(4q~iVrG^L_y&|g;gb;qTmC; zzWCtX7ou?H@xi2QY<JM1r79>us0OX=RL?fdFP+pkwzcKm@EkrjB=7u!H= zaQ$|#YDdTyu&JxxmFonxuNNCFXhBYPe0Rh|)=7MpZ!aSuzyXqJ;-(86i0bRFIjE_B zA`7$>zC*bK!iI5F7$!;NE)?zT=D)SHsYDK(4f4`w83a4w%O{L!oJ~O=$ardk%+17S zEngMz!~7NH+3JV4`5ubwvFBMn5)91Vw17(eUWI2>2WvpetKeJU*x5!g*jBpExR^GS z$FMW59Co&QLF+Cbq0{rjqb@#0T z-;R?9aCc?z$OZWt;hZv444x@IYaX`3KA*Wlk!^U#^TXI#bKs}#3v4FZnM^geF9s>Z zHAV!qH30SiyTu9uT@TQEeXiQ%)@3Is4~xX6U3*A47W)=QNI*S>IA(=t%Qt@x2?95=O_Rn1pP*`N zA)l`p%rLDW;)lBos&Z}Z_9-5J>pkpS@8{aelZc?ZJX)lw5cq7bnk)V9L(zZ>ocOpm zNCL)tIh3osjU>$BFaFGD8rnIQ?BfHs&Gh>WHstt!9Q8Ci_aYB%rSrU;$h#21A@367 ztYaGEtW#V;3PQ$Nn?<>~bAjB6qGOwF14X%_aL|i0C#k&^83pZZi8IoFeKh{*uqW+R zvi`a?hxa^08_uwFGk5$_sM^oA!r2|dQltCAnHnA+?ouzE;PpJzm&ywz_k^p`DT*y- zqx$)YAJq1=fXx)Vz{2Gji6+nFuP0uRsW|7c3(<0K06>J*U<2({*4YSDz~jNf zs>l*{HPQhAHU}6>kH7-#xDhVB^0rri)tZA%74}sAK{)^dKY^BiGXS=utzXuT_ok&j zG0?{E;+Ri?Vq6HA<$#n?@114C?}ZHEYc9a(o^UAU_fk7rEQ(`X&kd+JW=ln)9kj=pj5+R!%Ae~yaGU#^2$g!c@Ht5VVuNxcH+hVW9KT=|5-4*~TTL7?Y z0SGGL4ATN&Lb<7b(u9ir$lbHb+zWA6HDW=)=H0n7e~V(M<;Ku2X)xcNdO*>D z^DTx4)LJ&;MszLi@c*vubjIOtBvJG>h#M@DvlRIMz^1f`hUfd%XlGFLCz#>5HQgzc z>WvRRw$kJ?Lknjs;WZY!Dt+inGF-}b=ra&dnvt+1?E$+)kQJL9j-y?GV?y);fkSe- zf4ypvd83ZZ%qn_80y^;bF_x&4LL!On4PTcN6@68UYWf3F4_T(r1N8(tmJ&cjA=tGsocpO^-%^6+h`pDq|0^ zwa-Eh<%5j-)s->^Kae#kNlk20AY{<&2=U$-RKM~jawb8^9q&8PCxA<5+3kSByg5;% z2`-T;z%HM=W?KGDAK;xl`QV-{rVY({c+lD~_?SN!KjmX8kN~=|cWq)PCXH>*h5w)h z(H~KN7!{f(yP+-*1rhtqySf4U@%hjP(o|Ua`*xXnXc9`|xB>EF1R2)$MWvx5l6ajI zA@XX`!-iEJ7RB)8hAJ2~*lz~5d~!jOSNj+`fDV07d}MN zgnN@Z0Hzo378|4n(~*g+)XYRi)zCzS6H3E>gbvY)7`YzDNl9p`V&tIMxq7)?k@`e; zo(l;0{eGhN`?*3cbcnzSLFq6Kg*!iKvKmWdKp@x!Y6EI;foaMdejxz67c_x`Tg|-cUKWjZ=((5h?;Ta{1uw^c zV4Ap>xtCEn+v0nB31H|FsMsvO#Cq&{k6ml~h9X>LT*-X~1!l&Q^kKR^*LNo`Uwi(r^FACi=AM1U8ZPOVJdBNOz9wC4(hUe z00?w&Pde1{EuRVhgrnOYQRR^-)5a_u)FfqVbtkis)TPF34hrRWHYO)MfvDW4X7NLC zqY;}q-LZ_W!^Yftd1o2MLUVx?EKFlW=CH~yE@mW%T$&JyK z`NFcyN-KiI^~D4fdDwQpd|z}KmKf+(ut}A)KK3n^&k*tWZ_9xNO$+3UdtF}f}(rraFYv8(E0S=lOoEI5*Cy_9pn zsz=9@rNibA>!yg40H90t3rg94 z9y9d}E@*40L$w760qI7LFF=H+!hdJ~X7kGjF-*GL$_2eARlOgX+E%FE` zB_pa=o{L0XP%^?yqem2f;!-P6=2`Q{W+s_e$n_+u5yh`$cjUtlMZvq^>8C%CQH_X< z4hBr)!^4FU6KVY~t(B+e%`Pp!)oc%G8Ej)ll6&G84*E|)9XL$3_Ghnb0l|WgzORJP zDBQxeMX9as1l5` zp45MXliKj`kKh&T#3!_MDAK5m*#N-mrLeIeXC6+0VST~GvCE7ehvx|R)Xi6T8h&m|FVVXq4&p(sXm_=+H^qP7li&J3pp`qP z{4W9ZBf!Jy-9cf(bh#4Y$|k=I!}!*M$5knBrn ze!}ct1I+5toubPLx3*lcy;)nekje{z4t6~4g#SBVy0<*ud=BI5@U2Mtbw+r^hRFX{ z9Muw07)uMqoya=?ex3B5?wN$2z&_YRn+EOv$%MvA()r1FYXV|8wHTE&oD07o<{jed zil{JPyw+NO!va;m>2UwzOU(Y`e(}K)bBOyIS?ivAu&2%)<<*!lSuD~E`Fj`gSNFd` z)Wj###N#yk)lB>j9M_Nu`O`~HTR6B(TxH}@bXY~Wt?(}ti}!DXFud;xu0p^jq}UOJ zrB&(&gCa}_ESwR^1V$0UxEOwXW(Ei1!q^8R_g(RSbRldo_yZL)m@XUeM`JMlIy@E| zzJmt;alz_e0#@d-MhuuU9O*9TJS1%koEYpXXvd%IHeDt+o3RxvpeHrwb7E>dBa$%! ztLA})T0Zi1fQev18CWRjQZYVJh~6sBb5~&6Y~-O1cOR2-x%*jvf&2%U(Qy1H0;3m9 z0_>81Nd)BI08>Ob(l!kWQN#b$Dqv!%5p@Ulsu-|ak%W|&sof{fBlH4wT=kqUV#!HU&8Sp|h?Ju&&qwFwO_hITRRr{RxJ|QSErQs;K=ghgXPC@<;NE{XPo)Vp2xpiOP{!r5@Ya={g1`Oh5Z78cF*Nnf7=RAlnVu&<(lyH(D@WcYOz0 z@A|rtSeoG;u-Q%y>zR)bd7hS-W?Vmi|GB-2v`yCYi@#Xi*qX=A0}N(jh^b(8uL`a`r^QKxt`lsCkDyV?H8)0dH77p7W;ny_CFB?6*g^V zI2!;2#sK`EK})i>`W6BBe#!dY3l9mDU>QZRn}!-Og*+LrYYC>UxZ12~FZvm*t{VIw z?RsQSK=c8HnIY_=RO?kYj$4p_)`{x6s~*{|daA_Vp?`tQZMo_JRT-yRDN(BIt-5W- zsnb-pD4nTe9~E9|)%R)D6$j7tL#rPCtU7|(%N|TteZNjL<#!YyQ96tTp@TBepv_;^ z*?raW&t5(Ph@Y>ys^0#Bd|()=EF$pN$J=)w2kA|whVGaeN+8`@C*`MqF5uj#5V$r; zS?x46q^_gJ^YUYSqlWz<%>v=Uifj9(mzW2mh6Wqw+>HVKg|-I=+CJ;ld^drj#@WjD zpU22ji5m2vPGVlD8AG^^uqrwuN*{ue;SU^Pc#Kd|ECp_cWzkM&IzpBIGn?Dm-}{-~ z)1+0$_015He}WH^q3M5rIZglT4LMVLJ7fHi*S42{Fv{f4Xzm1}hrf>TMAPm>tk%g{JLkEDsF>!^=L=Y;g1RAER;`~pw>5|Dio@G-W zd^$AwKoyP=Y%zRR9z#S&@IE5!_>xcL4I1l)qP&IWtYewuixGf-R{=L;)xA}6aQK z-mIChz~)TP{>n0dLzf&`#u%~;>0=q0j%C1&Wx$MOU@w+&E0$3wmQn3kAC@thUtQ;!B^40VT4qJd4C%VFBCMCz0A%8C}?lflfSnLQcqZoVaYGEDE2v1#j_ z=LA2nmpSG37(~?MkDCm0;+475TI{Lacl}=Kg+4~4_NRz{$=Do7^OG?)_#DDPZl;;j zh{1A8eXL|yJ{z+Y@05&@>Al`Sm4X!K0)*8|1jOu>+`0Keth<-Jq`Tw};$41`wn3kU!-izXo-zs;-!u9@!u-2wtH+go%=B zOSz2-mYzhQiTC#ySd66+T$uwJlcw%B7XSRa`a$4-u7EHQNj`o?UEH{_&CZaKSbH3S zv(&4#W5Y$q@5d@o9YzAiStbTu9}Q^eiE=3M3Kz^ke84}WtYAR=Cw}mX=@Jv5z-t50 zXnp&x%pB#nf(4>&E)ey47q9{jO)N!a9)Id5*=FThBI_Vr!OUHGg+O7$RcyilQiBT- zu(<($LM(;-@DeeB{IX)r;Q_XW2h<|u0st@&7yuvucmWUq zh5&#JL%{|F+2IDY2}09Dh&A|uz5WnSdfHTfU_xjKbHxP`MTJpu*s0ioprSm-6T3Ja z9XGK{XnHJFdPT+WEp{xo1L8qh?D(b_Tl3?wZT3}g!4Sid!Rhm$cK8rs6;a)g6z_Ab3?7=R7 zW5u+mmpt8^8h1qHk^!N(?X>Br*kgeWGAur~xfJ5G=W-=p_l@^`T{v2a%o^z^j+;~< znXA3fY`4Pzjl50^CYL(xbrn=jTJ)S|ukur>)pY~@T|y+so8olWGCVu>-&D!mS+rwa z;p98HEG~_%SWrz<^O!N=ZJUSCVR*-XF*gT5nq`nk!j`rMjKj&a+08;dnhgoFP6CtW zKN_p;Ta^#sNea#DAl83=Gt%(mLu*hC%NMO!~z6F{*X0Iz^dU7ra;8=Jk%jwsNw6eucw) zg>&@^m+%T_AH3Y!=?WL+3TNO7$G%(PBC!?DsuhkyD_k@yoF^+BFIKn!SmC@^;dt{! zt8h@di)Xx8M%VG` zW|Q)Nn%R07euz_h%h6;TLg8O4vnQKmC3bC8uVT4b=h3s1Y8@J zpWNrmf_Z{oMF9xBtcYQvDU)jaTw5()e)XcKHn7Eh0`$?MK@l}y?A1&)P)Gw zZ>4qC=F{Ay=?tRMbo5P6_?V6tK?l-V0=9xU^wxAm1LHjSunjc^h~7JDjm{XIag z>iD4BWibTYy+06rlLP30HZ`$TXky!DVzXpoBarIus^Bz5-a%+3w%rt@#3mFBaCEcL zopA;}TV`7KXQl)C6uiGicfOer5;sOioS|vaWOVJIfh&3youab) z$BQA6J!zhH`-bLv{osWne18KEOkO492@dGQ^w2FB0+Bj@PLZlgbgGhtjkV*e4eZR0*MAaZp$%OK-%>s*|Ly-pB?!Z_>0Q!q z)vv%|Ej{}$m1G)0OCeZ;Uud=iYXYIt9ArFGnVEVs%4p9sKu8jqNUZ9#AE0AgUmWNw zDeySKcU3Qc@QNLzl!CMlC}`zLwX>x}7QGrca^fpc9|~D}=(yM=W#XdB+e7fLk=$P+ zEmK`3WPM@r*tRDC&VeNj9ih=)nC1mk(UJq*JlFO$f|S=+;cY}ux;$q*2cabMRpv2M zAZYm}pR7p@ezcxO;}?WyZ^FYVh6K#wegbxYEr(-&a{{5kTj5BiaCTx_@xPFH?xj)@ z7sFpXc&_Q0v0P>GWX1_@c#g+>LDy@es(5g?{D%IE1Axwb9h5&D=0csR&$2kjI>B_b z*POweYlPN8D4n5t<;69Zt!G|2iizkBjxsPxnBi_jhDSHY=QZ2~+(9&meT3xQ^*fJi z%jC{~oY7PWWfL>^tX=qLq7At;r~9TeFJ$g~Itaq0E(QtF33G*lmG{eAPFt41FD`RU zjmev%Ac48XXxA$#c)r{L;$ayp2Rk#Mw0TL#dB9)n&Yf+WI}3qaEJ3!JsmvV1wZ`z4 zIq*B4#gyzGcKQ{}I2|@4N_mx3vBedH=H$5YS&oZF zZxN%@W75NsFP-Gfr-YG6 z$|Zn`)W7?yjL;>E0&pzfmqA0@g5k?HR8`w3sU3VA2Tr>04TkkjiWz7u21P#3)h}&vE8EZR358_s5T$Eqlj}7QW-S>i{4(;Q-(KT*s+g@~S9&VpS#w zH61H>hVqQ`sd-{^oMx=F@13vf`#z?(MVXOtcw`tXivfwSwZKvMbt3;I9J6ZrjMh-2 z^6HOBpLrFh0tSc>v#ErCF1>`SXu!#7TIfsDLcTtX{(YteEm$k2vs?aOjZRF{J>u^@ z?G8Nv&;iAE9X+v!Ra`^A1r)0@;S&&?QZrg4`>wwLYhr}8k?7!mmWv4knHcST%x=%1 zh`3LQ7M#aQCH#Wr1i91V+M+oUCH3)sUiFcsTKi;jiy_1flI=o&$QM`SLxjIMk_A1f zva(BLPT1Fh2~+!D_KrsufcA4O zcs6|QWtI8t=+5Uv&ZTPyB*u=7>dFkk8)!qt})jYNod@cx{8 zm}P%18s32Vb6}P|c6*JRb2TZ5@pEYh73QL?x>E4!y|in88Jpf~c0@@U=W;o5y)obLFa!yzFb*=&Mr6&q?9mcJVm{Kz6@j|u z|Nr-@0}s%DW3Bo1cKOZ`U8k-FSSw@qw;uy9|55A|Mmy>o453uhY(hYC!q}-35h!Pc z1_nSo6U0IqGiDzek^(-P4VyzEu?v-tI)`k#J7Qq>&WF|i3K7g4p^P^fYmx%a+5_fd zAL5H$6kE8O8!6)Dm>c*l8R)PlqI010{EU(PhP!2d?AKC-oXtnxnT$*XY@(6Q;qspl z;4IwAD)CZybIdlwQLe})T9B`XGamIURD!-{>Uc;Bu4+AYE^wX+IaAU2Y?i(_hKV+u zT5oYfN-n?ZXNWmjOz>u3D_=kJdjy zXOx?izM*KA5QyNO=P4bqq6x2T*c-zcxBlFJEO)AO*vaqJ(~5BrZo-=A;?xGcZ9c;B zf#0`859v0Yx5SBuExaX&ADcjBC8owiTR22EL{bI%kKj9AaU1-zXzr=P^l3iN<1bqx zY8iALJwtsp`C26?8Ag;mI!?zu!y_VUUpm8D0!7&A6h0MXwS*2Tr*%YU6^UwzdDo_Y zdsA-LQcE0xe1^?5`q5D?Oqv}*rnH34%vpgjsU#8a;5;}5jg$I$jQQ*}@yumP0p)XOH%MiCRJLFshJ5mrlorl~2`aE7svldT!j&$6nxe04Qx6$W#eOA~@ushiDJ^7WHJxDNhDxDcq|d4? zQFGGkm#=Cbmv{3338l^-`J(zlg};>$!lAE?(k>>VEHyqUjZ6(Da5{wOYdS;~1u5le zIC|`0!^B{%5paCx5paC`)x=zGA73^AEh_Ba*stv0&L<-B@j0>$-&QtiR&_IUKgtwF{#!SK`h$y=xfIkok!;(7aLXrGML}vyCCmP23&B; zH#VR2K%j|+_P_q@!Y`pBFA}zC{pgU?&i$T?eOXS6Wj!hD_4d#EMzwe0BIlQd$L+!E zk>2fRBuwq?l$I;i|A;Ps)l%}vqR^QJhL@HK80=vgFv6cq6-%@fHXE!aM)p&K>6vb0 zKIP6*iCygbU#4;hhaqvD;|j|JqKA#waP~K$iACg(7;KAJlDl~a08m;jL5s*QAt>W# z7k`4w%g=Ni<|+p!H=?XB1g1#zRa0+zv18XS0ZsWgX3a_}iMhUi?Kl%#0vkNZYr^@I zfUwDNkgp^V8^vcdUrsW4S4lYvMkq&log9@jIm#$GiWJFF!^lyd$Wg?_G&7K+;B%+u z;~>DO9j1(;XPA31I~y340Rx1WIz@@+)5{+2FZ58sat-LL7beF zI~}@sVDPMMQtS^?ZGTkVI3LMdA7bZT0>xXWwwo~O51Lr z`#=!0bIFsu9Fh3(l>0BGN5P|BjspQ)vTb%X#;s920BJIuhN0&gqr<3TjS?ptnGV7+ z)B}#xsFxCxs8L$h3@Tl4=`;%9G^&gjB?Bvc3~l5Jr>PBpvSw|hHKk0#s3xItsM|>Y z(5QOgjtRh1OpMiSHal7dO9-(Ff~Uh`0qRQ8k;{>)^!no8YlslH4-F;kfVV+eI9`sY zzxrdoC{lZ{qe?zM%rch~M*zjmhnId}7Z43`#7|R~=jw&hQ`6kRP$qE{;QyayRUxYD z-^(*N1dkzq0AYX30i{G>(K=c;!_Mi2iH#5BT4>$k65zHA!L#^3z`;oESk8KqtO_%c zCMB>yfh;ih|CdjmxqKdw`bL~9fu#?H$ztf8n>%@Ii36UMli9ni*X`>6nF2`2*2pRS z7vj}OfpQ!!y{w2IM{CO{=dZO1&q@WKNN5G5_y<0J*=fOq=mI_^`_2G9iJIo0{5|Py zJlXm^G3NJ_6Tc_xeNRmLp2F*Uvh+Q1pYJIs-;*iF_r$HczxT9zU5ZWPPoPJvKoSKX z3~p?=2Mun5ro#VtMz@tJeSRjo7;QIt5@$Tj13ph*ZfHcS5V4voZt6ioewwG`??S2j zGf#wn5~)l7?e%D-7(@UmPdtkn66NU<$`cXzuw})I_NCXnd{_$ib-%_nmVJbnJ=1?*IR zD#+kN2m_xk!(Krf{lSH!OvlrITTqs;3@7lfcAC%e7c&CZ?3U7`2B{?^r_P_d|AT)x zw2@cT-boHnxOY%D1rHU38aVtq4*{soPN`j}@L-+b3BGUcJU^^+( zVJ<^v8LMn`d(Y)Vra)5Z0;X2jz@8##)CxlAp4hJwhy>XvSY+f9joqm*n##62wgL!* zDcj-zwsVjc=Hf^_1*NU6D1L8$5huq5u~CqyShj>gn%@e@LtmTCg|d-MghZ!o)k9S^ z7xy7cwZy=Vl2mzeY|~#kHsb97eT;1=Va=%{8fyDG&cjv|^<;9XR9wf_BVfmN&j4?2 z#+Jga_nHf_HPr*swoifWIL&?M_OilXW+-%*p-|hV_b)OO9DEFgaO%f@i=m*1#83c< zIYeBTC9)HrsZc;sp`b|sY6_vCk=t@`luNS_G5M2AhA5Ul{}3mPWw--}rsKF0;lzJyBRM z65|v2OS4a`9(<1iioePrHUSA7{t5!o@aQhE32uIr0~GDAGNL^D|q9^?X|2Ktsv;l2_yUR$U6#-H>uO5%3&SE=05Lb2;@y9$J0yf) z&*dn9#oYAr0nHd{A@pEkZ{H=SWqoW%Zv^%_hR?BEm{Ogmo2{(DNXy^|~t(c#VOzjY)kiZ8OmC zTKHFHb^tJ54J?tjQgUv{Wt%Bjd+ybya#KQeX$U^Q7+hw$uyV7s$8nj9@<*M79u80j zfgu5)TG~w@hkp*Sy;SboP&)5JmLXnI<3*_FjcAebk7n}Mk0Dxp z=12p70FULYn3q~*TAag(@9cS?MOdbMQ2}sEyvB^bSy|%wVk*rFC#bp!6TTq$3o>2F zw}2!EO+(@UV=!|*i0XT;NgoC>R6@qr)a-P>$0H{NGRAkM1QW4FfM;6q7>#3$`Ucu8cwaK9Y`KzeNLf1DlQ<* zqQ}4k5A!H+9LVID;~oRYUw=qw+z_fZIwLFtWIMTdx`^$bcoNCo{^M0`Pho?#eH+Sp zstX+s9=4Up^sT^efWN9gb0|p!)~*jY?7aF;f7?9=x#=Sti}Z0HY;iJKB1k!;C2W-Y z$6>ATd8ihLauK^?`7|8T^0J*>xtxf6x!t`|z5)tA@)59anfLX{?tf~sI}EOvyVl%c z;ImKMVbqB$(!k<4JB;B%62=aL%x&*x4Wo9IZd#%;hrRhnm=pw zO>VDkdpsg<&jZcl#&x2Y)>*Di}t$^cKM|9MK&F zBruGLZIbTh%dZ*qL1hw$YAJ%0D09w5)6SvTOgf3ZP!5p2ut>P0Gnc(^dn`>SadOzk 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 delta 144004 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 OD-UYWAY4#+$Fv4~480ft diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index e8bfb39944d8ca27c96221cf83330af0882d446f..72fe29bf62f8c0255b548aa355b40c9596a9bbcc 100644 GIT binary patch delta 138171 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

)dMu|I$St6d2c^`MQ%7jHLQG0*1*wi@S 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ⅈ(S21m;dQ~ubF~|8#vehg1S&;9UF^XamFE8$ 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 z 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 FRszp-@E`yH delta 138274 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 EhFbxvqyPW_ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CyberAccessProgram.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CyberAccessProgram.ts index ccd4118a8320..0b87ec9cc6f9 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/CyberAccessProgram.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CyberAccessProgram.ts @@ -3,7 +3,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. /** - * Requested cyber treatment for a ChatGPT-authenticated Codex turn. + * Requested cyber treatment for an OpenAI model turn. * Authorization and model-tier restrictions remain server-owned. */ export type CyberAccessProgram = "standard" | "daybreakBlue" | "daybreakRed"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/turn.rs b/codex-rs/app-server-protocol/src/protocol/v2/turn.rs index f938d179e697..f7a7ec09eab8 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/turn.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/turn.rs @@ -118,7 +118,7 @@ pub struct AdditionalContextEntry { pub kind: AdditionalContextKind, } -/// Requested cyber treatment for a ChatGPT-authenticated Codex turn. +/// Requested cyber treatment for an OpenAI model turn. /// Authorization and model-tier restrictions remain server-owned. #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] @@ -271,7 +271,7 @@ pub struct TurnStartParams { #[ts(optional = nullable)] pub multi_agent_mode: Option, - /// EXPERIMENTAL - Request a workspace-authorized cyber program for this + /// EXPERIMENTAL - Request an authorized cyber program for this /// turn. Omission preserves automatic behavior. This does not grant access. #[experimental("turn/start.cyberAccessProgram")] #[ts(optional = nullable)] diff --git a/codex-rs/app-server/src/request_processors/config_processor.rs b/codex-rs/app-server/src/request_processors/config_processor.rs index 56a8b4b316ee..72ddca52f6e3 100644 --- a/codex-rs/app-server/src/request_processors/config_processor.rs +++ b/codex-rs/app-server/src/request_processors/config_processor.rs @@ -64,6 +64,7 @@ const BACKGROUND_PAGINATED_ROLLOUT_MIGRATION_FEATURE: &str = "background_paginated_rollout_migration"; const SUPPORTED_EXPERIMENTAL_FEATURE_ENABLEMENT: &[&str] = &[ + "api_key_cyber_access_programs", "api_key_model_discovery", "auth_elicitation", BACKGROUND_PAGINATED_ROLLOUT_MIGRATION_FEATURE, diff --git a/codex-rs/app-server/tests/suite/v2/cyber_access_program.rs b/codex-rs/app-server/tests/suite/v2/cyber_access_program.rs index fea72f3f56ac..6cfedf7706e1 100644 --- a/codex-rs/app-server/tests/suite/v2/cyber_access_program.rs +++ b/codex-rs/app-server/tests/suite/v2/cyber_access_program.rs @@ -3,7 +3,10 @@ use app_test_support::ChatGptAuthFixture; use app_test_support::TestAppServer; use app_test_support::write_chatgpt_auth; use codex_app_server_protocol::ClientRequest; +use codex_app_server_protocol::CodexErrorInfo; use codex_app_server_protocol::CyberAccessProgram; +use codex_app_server_protocol::ExperimentalFeatureEnablementSetParams; +use codex_app_server_protocol::ExperimentalFeatureEnablementSetResponse; use codex_app_server_protocol::JSONRPCErrorError; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ThreadForkParams; @@ -16,13 +19,18 @@ use codex_app_server_protocol::ThreadResumeParams; use codex_app_server_protocol::ThreadResumeResponse; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartedNotification; +use codex_app_server_protocol::TurnError; use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStatus; use codex_app_server_protocol::UserInput; use codex_login::AuthCredentialsStoreMode; +use codex_login::AuthKeyringBackendKind; +use codex_login::login_with_api_key; use core_test_support::responses; use pretty_assertions::assert_eq; use serde_json::json; +use std::collections::BTreeMap; +use std::collections::HashMap; use std::path::Path; use tempfile::TempDir; use wiremock::Mock; @@ -192,6 +200,167 @@ async fn turn_start_forwards_explicit_cyber_access_program() -> Result<()> { Ok(()) } +#[tokio::test] +async fn api_key_cyber_access_program_requires_both_features() -> Result<()> { + core_test_support::skip_if_no_network!(Ok(())); + let server = responses::start_mock_server().await; + let expected_programs = [ + None, + None, + None, + Some(json!({"cyber": "daybreak_blue"})), + Some(json!({"cyber": "daybreak_red"})), + Some(json!({"cyber": "standard"})), + ]; + let requests = responses::mount_sse_sequence( + &server, + expected_programs + .iter() + .enumerate() + .map(|(index, _)| responses::sse_completed(&format!("resp-{index}"))) + .collect(), + ) + .await; + let home = TempDir::new()?; + let mut app = start_api_key_app(home.path(), &server).await?; + set_api_key_cyber_access_programs(&mut app, /*enabled*/ true).await?; + for disabled_feature in [ + Some("api_key_cyber_access_programs"), + Some("api_key_model_discovery"), + None, + ] { + let thread = app + .start_thread(ThreadStartParams { + config: disabled_feature + .map(|feature| HashMap::from([(format!("features.{feature}"), json!(false))])), + ..Default::default() + }) + .await? + .thread; + for program in [ + None, + Some(CyberAccessProgram::DaybreakBlue), + Some(CyberAccessProgram::DaybreakRed), + Some(CyberAccessProgram::Standard), + ] { + let completed = app + .start_turn_and_wait_for_completion(TurnStartParams { + thread_id: thread.id.clone(), + cyber_access_program: program, + input: vec![UserInput::Text { + text: "hello".to_owned(), + text_elements: Vec::new(), + }], + ..Default::default() + }) + .await?; + if program.is_some() && disabled_feature.is_some() { + assert_eq!(completed.turn.status, TurnStatus::Failed); + assert_eq!( + completed.turn.error, + Some(TurnError { + message: "Cyber access programs are disabled for this API-key session." + .to_owned(), + codex_error_info: Some(CodexErrorInfo::Other), + additional_details: None, + misalignment: None, + }) + ); + } else { + assert_eq!(completed.turn.status, TurnStatus::Completed); + } + } + } + assert_eq!( + requests + .requests() + .iter() + .map(|request| ( + request.header("authorization"), + request.body_json()["model"].clone(), + request.body_json().get("access_programs").cloned(), + )) + .collect::>(), + expected_programs + .into_iter() + .map(|program| ( + Some("Bearer test-key".to_owned()), + json!("gpt-6-sol"), + program + )) + .collect::>() + ); + app.shutdown_gracefully().await?; + Ok(()) +} + +#[tokio::test] +async fn api_key_cyber_access_program_server_rejection_does_not_fall_back() -> Result<()> { + core_test_support::skip_if_no_network!(Ok(())); + let server = responses::start_mock_server().await; + Mock::given(method("POST")) + .and(path("/v1/responses")) + .respond_with(ResponseTemplate::new(/*s*/ 403).set_body_json(json!({ + "error": { + "type": "invalid_request_error", + "code": "access_program_not_enabled", + "message": "Daybreak Blue is not enabled for this project." + } + }))) + .expect(/*r*/ 1..) + .mount(&server) + .await; + let home = TempDir::new()?; + let mut app = start_api_key_app(home.path(), &server).await?; + set_api_key_cyber_access_programs(&mut app, /*enabled*/ true).await?; + let thread = app.start_thread(ThreadStartParams::default()).await?.thread; + let completed = app + .start_turn_and_wait_for_completion(TurnStartParams { + thread_id: thread.id, + cyber_access_program: Some(CyberAccessProgram::DaybreakBlue), + input: vec![UserInput::Text { + text: "hello".to_owned(), + text_elements: Vec::new(), + }], + ..Default::default() + }) + .await?; + assert_eq!(completed.turn.status, TurnStatus::Failed); + let error = completed.turn.error.expect("server rejection"); + assert_eq!( + error.codex_error_info, + Some(CodexErrorInfo::HttpConnectionFailed { + http_status_code: Some(403), + }) + ); + assert!( + error + .message + .contains("Daybreak Blue is not enabled for this project.") + ); + let requests = responses::received_responses_requests(&server).await; + assert!(!requests.is_empty()); + // Existing transport retries may repeat the request; none may drop the program. + assert_eq!( + requests + .iter() + .map(|request| ( + request.header("authorization"), + request.body_json()["access_programs"].clone() + )) + .collect::>(), + vec![ + ( + Some("Bearer test-key".to_owned()), + json!({"cyber": "daybreak_blue"}) + ); + requests.len() + ] + ); + app.shutdown_gracefully().await?; + Ok(()) +} + #[tokio::test] async fn daybreak_thread_metadata_persists_independently_across_restart_and_fork() -> Result<()> { core_test_support::skip_if_no_network!(Ok(())); @@ -461,3 +630,55 @@ async fn start_chatgpt_app(home: &Path, server: &MockServer) -> Result Result { + Mock::given(method("GET")) + .and(path("/v1/responses")) + .respond_with(ResponseTemplate::new(/*s*/ 426)) + .mount(server) + .await; + // The existing inference override disables remote catalog discovery. Forwarding + // must therefore work without granting programs through a test catalog. + std::fs::write( + home.join("config.toml"), + format!( + "model = \"gpt-6-sol\"\nopenai_base_url = \"{}/v1\"\n[features]\napi_key_model_discovery = true\n", + server.uri() + ), + )?; + login_with_api_key( + home, + "test-key", + AuthCredentialsStoreMode::File, + AuthKeyringBackendKind::default(), + )?; + TestAppServer::builder() + .with_codex_home(home) + .without_managed_config() + .with_env_overrides(&[ + ("OPENAI_API_KEY", None), + ("CODEX_API_KEY", None), + ("OPENAI_BASE_URL", None), + ]) + .build_initialized() + .await +} + +async fn set_api_key_cyber_access_programs(app: &mut TestAppServer, enabled: bool) -> Result<()> { + let enablement = BTreeMap::from([("api_key_cyber_access_programs".to_owned(), enabled)]); + let response: ExperimentalFeatureEnablementSetResponse = app + .request( + |request_id| ClientRequest::ExperimentalFeatureEnablementSet { + request_id, + params: ExperimentalFeatureEnablementSetParams { + enablement: enablement.clone(), + }, + }, + ) + .await?; + assert_eq!( + response, + ExperimentalFeatureEnablementSetResponse { enablement } + ); + Ok(()) +} diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index d5521009b99b..6aaa39c3f540 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -713,6 +713,9 @@ "analytics_plan_history": { "type": "boolean" }, + "api_key_cyber_access_programs": { + "type": "boolean" + }, "api_key_model_discovery": { "type": "boolean" }, @@ -6740,6 +6743,9 @@ "analytics_plan_history": { "type": "boolean" }, + "api_key_cyber_access_programs": { + "type": "boolean" + }, "api_key_model_discovery": { "type": "boolean" }, diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index f5ac495337fd..e852b632d8fe 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -272,6 +272,8 @@ pub struct ModelClient { restored_history: bool, request_contributors: Vec>, executed_tool_calls: Option, + // Resolved once when the session is created, like other session feature flags. + api_key_cyber_access_programs: cyber_access_program::ApiKeyCyberAccessPrograms, } /// A turn-scoped streaming session created from a [`ModelClient`]. @@ -541,6 +543,8 @@ impl ModelClient { restored_history: false, request_contributors, executed_tool_calls: None, + api_key_cyber_access_programs: + cyber_access_program::ApiKeyCyberAccessPrograms::UnsupportedProvider, } } @@ -565,10 +569,12 @@ impl ModelClient { prompt_cache_key_override: Option, event_sender: Sender, codex_responses_headers: Option>, + api_key_cyber_access_programs: cyber_access_program::ApiKeyCyberAccessPrograms, ) -> Self { self.prompt_cache_key_override = prompt_cache_key_override; self.event_sender = Some(event_sender); self.codex_responses_headers = codex_responses_headers; + self.api_key_cyber_access_programs = api_key_cyber_access_programs; self } @@ -1730,7 +1736,8 @@ impl ModelClientSession { request.access_programs = cyber_access_program::for_auth( client_setup.auth.as_ref(), prompt.cyber_access_program, - ); + self.client.api_key_cyber_access_programs, + )?; self.client .prepare_response_items_for_request(&mut request.input); if crate::guardian::is_basic_session_source(&self.client.state.session_source) { @@ -1889,7 +1896,8 @@ impl ModelClientSession { request.access_programs = cyber_access_program::for_auth( client_setup.auth.as_ref(), prompt.cyber_access_program, - ); + self.client.api_key_cyber_access_programs, + )?; let mut websocket_metadata = responses_metadata.clone(); websocket_metadata.routing_hint = self.client.build_routing_hint_header( client_setup.auth.as_ref(), diff --git a/codex-rs/core/src/cyber_access_program.rs b/codex-rs/core/src/cyber_access_program.rs index 4a508715fd47..7f75798562df 100644 --- a/codex-rs/core/src/cyber_access_program.rs +++ b/codex-rs/core/src/cyber_access_program.rs @@ -1,12 +1,55 @@ +//! Forward explicit programs while leaving entitlement and model policy to the server. + use codex_api::AccessPrograms; +use codex_features::Feature; use codex_login::CodexAuth; +use codex_protocol::error::CodexErr; +use codex_protocol::error::Result; use codex_protocol::turn_input::CyberAccessProgram; +#[derive(Clone, Copy, Debug)] +pub(crate) enum ApiKeyCyberAccessPrograms { + UnsupportedProvider, + Disabled, + Enabled, +} + +impl ApiKeyCyberAccessPrograms { + pub(crate) fn from_config(config: &crate::config::Config) -> Self { + if config.model_provider_id != codex_model_provider_info::OPENAI_PROVIDER_ID { + Self::UnsupportedProvider + } else if config.features.enabled(Feature::ApiKeyCyberAccessPrograms) + && config.features.enabled(Feature::ApiKeyModelDiscovery) + { + Self::Enabled + } else { + Self::Disabled + } + } +} + pub(crate) fn for_auth( auth: Option<&CodexAuth>, program: Option, -) -> Option { - program - .filter(|_| auth.is_some_and(CodexAuth::is_chatgpt_auth)) - .map(AccessPrograms::from) + policy: ApiKeyCyberAccessPrograms, +) -> Result> { + let Some(program) = program else { + return Ok(None); + }; + let Some(auth) = auth else { + return Ok(None); + }; + if auth.is_chatgpt_auth() { + return Ok(Some(program.into())); + } + if !auth.is_api_key_auth() { + return Ok(None); + } + match policy { + ApiKeyCyberAccessPrograms::UnsupportedProvider => Ok(None), + ApiKeyCyberAccessPrograms::Disabled => Err(CodexErr::InvalidRequest( + "Cyber access programs are disabled for this API-key session.".to_owned(), + )), + ApiKeyCyberAccessPrograms::Enabled => Ok(Some(program.into())), + } } diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 824cf589cd4e..e92aaeef8ae5 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -1763,6 +1763,7 @@ impl Session { .or(fork_cache_key), tx_event.clone(), codex_responses_headers, + crate::cyber_access_program::ApiKeyCyberAccessPrograms::from_config(&config), ), executed_tool_calls: executed_tool_calls.clone(), code_mode_service: crate::tools::code_mode::CodeModeService::new( diff --git a/codex-rs/core/src/tools/executed_tool_calls/request_metadata_tests.rs b/codex-rs/core/src/tools/executed_tool_calls/request_metadata_tests.rs index efa05e24e66e..9b75f4d4ac0f 100644 --- a/codex-rs/core/src/tools/executed_tool_calls/request_metadata_tests.rs +++ b/codex-rs/core/src/tools/executed_tool_calls/request_metadata_tests.rs @@ -534,6 +534,8 @@ async fn recorder_refreshes_without_changing_execution_features_or_claiming_miss crate::ConfigRefreshOutcome::Published ); let mut replay = output(&call.call_id); + // Replay the same history item across the configuration refresh. + replay.set_id(direct_output.id().cloned()); calls.attach_direct_call_to_output(&mut replay, prepared); assert_eq!(replay, direct_output); } diff --git a/codex-rs/core/tests/suite/compact_program_tests.rs b/codex-rs/core/tests/suite/compact_program_tests.rs index d65c36660d3d..3a357df4104c 100644 --- a/codex-rs/core/tests/suite/compact_program_tests.rs +++ b/codex-rs/core/tests/suite/compact_program_tests.rs @@ -254,6 +254,30 @@ async fn model_switch_program_pair( Arc::clone(&resumed.codex) } }; + if matches!(history, History::ApiKeyResume) { + // Resuming a ChatGPT turn does not enable the API-key rollout features. + let error = submit_pair(&thread, next_model, Some(CyberAccessProgram::DaybreakRed)) + .await + .expect_err("API-key selections require the rollout features"); + assert!( + error + .to_string() + .contains("Cyber access programs are disabled for this API-key session.") + ); + thread.shutdown_and_wait().await?; + assert_eq!( + server + .received_requests() + .await + .expect("recorded fixture requests") + .iter() + .filter(|request| request.method == "POST" && request.url.path() == "/v1/responses") + .count(), + 1, + "only the original ChatGPT turn should reach inference" + ); + return Ok(()); + } let summary = if local { ev_assistant_message("summary", "summary of surviving history") } else { @@ -277,17 +301,7 @@ async fn model_switch_program_pair( submit_pair(&thread, next_model, Some(CyberAccessProgram::DaybreakRed)).await?; thread.shutdown_and_wait().await?; let requests = requests.requests(); - let authorized = !matches!(history, History::ApiKeyResume); - let old_program = if authorized { - expected_program - } else { - Value::Null - }; - let new_program = if authorized { - json!({"cyber": "daybreak_red"}) - } else { - Value::Null - }; + let new_program = json!({"cyber": "daybreak_red"}); let actual = requests .iter() .map(|request| { @@ -302,7 +316,7 @@ async fn model_switch_program_pair( } following.push(json!([next_model, new_program])); assert_eq!(&actual[1..], following.as_slice()); - assert_eq!(actual[0], json!([previous_model, old_program])); + assert_eq!(actual[0], json!([previous_model, expected_program])); if !local { assert_eq!(requests[0].inputs_of_type("compaction_trigger").len(), 1); } diff --git a/codex-rs/core/tests/suite/cyber_access_program.rs b/codex-rs/core/tests/suite/cyber_access_program.rs index e5021c0f0317..1d7ee9adaecf 100644 --- a/codex-rs/core/tests/suite/cyber_access_program.rs +++ b/codex-rs/core/tests/suite/cyber_access_program.rs @@ -159,10 +159,10 @@ async fn recover_turn_restores_cyber_access_program_without_making_it_sticky() - } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn cyber_access_program_omits_api_key_and_spoofed_custom_provider() -> Result<()> { +async fn cyber_access_program_omits_spoofed_custom_providers() -> Result<()> { core_test_support::skip_if_no_network!(Ok(())); for (auth, provider_id) in [ - (CodexAuth::from_api_key("test-key"), "openai"), + (CodexAuth::from_api_key("test-key"), "custom"), (CodexAuth::create_dummy_chatgpt_auth_for_testing(), "custom"), ] { let server = responses::start_mock_server().await; diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index 2b6d26acb330..7f61ef3aee9a 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -96,6 +96,8 @@ pub enum Feature { AnalyticsPlanHistory, /// Discover model catalogs for OpenAI API-key authentication. ApiKeyModelDiscovery, + /// Forward explicit programs with builtin OpenAI API keys. + ApiKeyCyberAccessPrograms, /// Deprecated no-op; use `tui.fullscreen_transcript` instead. TranscriptV2, // Stable. @@ -1300,6 +1302,12 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::UnderDevelopment, default_enabled: false, }, + FeatureSpec { + id: Feature::ApiKeyCyberAccessPrograms, + key: "api_key_cyber_access_programs", + stage: Stage::Stable, + default_enabled: false, + }, FeatureSpec { id: Feature::EnableRequestCompression, key: "enable_request_compression", diff --git a/codex-rs/protocol/src/turn_input.rs b/codex-rs/protocol/src/turn_input.rs index e7c17ead2fcd..cb27cd4f9639 100644 --- a/codex-rs/protocol/src/turn_input.rs +++ b/codex-rs/protocol/src/turn_input.rs @@ -142,7 +142,7 @@ pub enum TurnInputMode { Steer { expected_turn_id: String }, } -/// Requested cyber treatment for a ChatGPT-authenticated Codex turn. +/// Requested cyber treatment for an OpenAI model turn. /// Authorization and model-tier restrictions remain server-owned. #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "snake_case")] From 67352b5b4adc3d3e8865ac8bf343b952b8c040e7 Mon Sep 17 00:00:00 2001 From: Vivian Fang Date: Wed, 30 Sep 2026 00:00:10 +0000 Subject: [PATCH 15/24] Recover exec-server sessions after environment info timeouts (#49407) ## Why A stuck transport can fill the outbound queue, leaving an `environment/info` request blocked while sending. The timeout must cover sending as well as waiting for a response. ## What changed Wrap the live metadata RPC in a 30-second timeout. On timeout, close the probed connection and request session recovery, returning the timeout error without retrying the failed request. Document the experimental `environment/info` endpoint and its timeout behavior. ## Testing Extend the WebSocket session recovery test to cover an unanswered `environment/info` request, checking that it times out, closes the connection, and resumes the same session while retaining the client instance. GitOrigin-RevId: 9035d2edab3626e780d8bd40ebcdf0ca327bfddb --- codex-rs/app-server/README.md | 9 ++++++ codex-rs/exec-server/src/client.rs | 50 ++++++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 268eee9022a5..64aef0e2deed 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -111,6 +111,15 @@ after a client tries to archive or delete it. After the owner releases the worker, its saved conversation can be archived or deleted normally. Ordinary client-controlled threads keep their existing behavior. +## Environment information (experimental) + +`environment/info` connects to a configured environment by `environmentId` and +returns its detected `shell` plus its default `cwd` as a canonical +environment-native `file:` URI. Connection failures are returned as request +errors. After connecting, the live metadata request has a 30-second timeout. A +timeout closes the probed connection and starts normal session recovery without +retrying the failed request. + ## User verification (experimental) Codex app-server advertises `openai/elicitation.userVerification` to the diff --git a/codex-rs/exec-server/src/client.rs b/codex-rs/exec-server/src/client.rs index 538bb214fc19..1192a5b8f366 100644 --- a/codex-rs/exec-server/src/client.rs +++ b/codex-rs/exec-server/src/client.rs @@ -840,11 +840,25 @@ impl ExecServerClient { // TODO: Remove after app-server migrates off this call. pub async fn force_environment_info(&self) -> Result { let rpc_client = self.rpc_client().await?; - self.map_rpc_call_result( - rpc_client - .call_with_timeout(ENVIRONMENT_INFO_METHOD, &(), ENVIRONMENT_INFO_TIMEOUT) - .await, + // Bound sending as well as receiving: a stuck transport can fill the outbound queue. + let result = timeout( + ENVIRONMENT_INFO_TIMEOUT, + rpc_client.call(ENVIRONMENT_INFO_METHOD, &()), ) + .await; + match result { + Ok(result) => self.map_rpc_call_result(result), + Err(_) => { + let error = ExecServerError::from(RpcCallError::TimedOut { + method: ENVIRONMENT_INFO_METHOD.to_string(), + timeout: ENVIRONMENT_INFO_TIMEOUT, + }); + // Retire only the connection we probed; recovery ignores a stale client. + rpc_client.close_transport().await; + self.inner.request_recovery(rpc_client, error.to_string()); + Err(error) + } + } } pub async fn read_environment_config( @@ -2661,8 +2675,10 @@ mod tests { Ok(()) } + #[test_case::test_case(false; "socket_closed")] + #[test_case::test_case(true; "health_check_timed_out")] #[tokio::test] - async fn remote_websocket_client_resumes_session() { + async fn remote_websocket_client_resumes_session(health_check_timeout: bool) { let listener = TcpListener::bind("127.0.0.1:0") .await .expect("listener should bind"); @@ -2680,7 +2696,22 @@ mod tests { /*expected_resume_session_id*/ None, ) .await; - first.close(None).await.expect("websocket should close"); + if health_check_timeout { + let JSONRPCMessage::Request(request) = read_jsonrpc_websocket(&mut first).await + else { + panic!("expected environment info request"); + }; + assert_eq!(request.method, "environment/info"); + // Keep the socket open without answering. The client must close it itself. + while let Some(Ok(message)) = first.next().await { + assert!(matches!(message, Message::Ping(_) | Message::Close(_))); + if matches!(message, Message::Close(_)) { + break; + } + } + } else { + first.close(None).await.expect("websocket should close"); + } let mut resumed = accept_websocket(&listener).await; complete_websocket_initialize( @@ -2703,6 +2734,13 @@ mod tests { HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault), ); let stable_client = client.get().await.expect("client should connect"); + if health_check_timeout { + let error = stable_client + .force_environment_info() + .await + .expect_err("unanswered health check should time out"); + assert!(error.to_string().contains("timed out")); + } timeout(Duration::from_secs(1), resumed_rx) .await .expect("session resume should not time out") From bd5185820df893016f51e34a7e7e19ebc8e7240b Mon Sep 17 00:00:00 2001 From: Ruslan Nigmatullin Date: Wed, 30 Sep 2026 00:04:34 +0000 Subject: [PATCH 16/24] Compare tool call metadata in the recorder refresh test (#49408) ## What changed Update the replay assertion in the recorder refresh test to compare `executed_tool_call_metadata()` instead of entire response items. This keeps the assertion focused on preserving recorded tool call metadata across a configuration refresh. GitOrigin-RevId: 763173a918f4e0bc79c3c04466453cafb9629775 --- .../src/tools/executed_tool_calls/request_metadata_tests.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/codex-rs/core/src/tools/executed_tool_calls/request_metadata_tests.rs b/codex-rs/core/src/tools/executed_tool_calls/request_metadata_tests.rs index 9b75f4d4ac0f..b4ffbc8276af 100644 --- a/codex-rs/core/src/tools/executed_tool_calls/request_metadata_tests.rs +++ b/codex-rs/core/src/tools/executed_tool_calls/request_metadata_tests.rs @@ -537,7 +537,10 @@ async fn recorder_refreshes_without_changing_execution_features_or_claiming_miss // Replay the same history item across the configuration refresh. replay.set_id(direct_output.id().cloned()); calls.attach_direct_call_to_output(&mut replay, prepared); - assert_eq!(replay, direct_output); + assert_eq!( + replay.executed_tool_call_metadata(), + direct_output.executed_tool_call_metadata() + ); } let cell = CellId::new(format!("cell-{index}")); From eefe0ce1a8e39788a4de7ef5bafd47e51339610b Mon Sep 17 00:00:00 2001 From: Ruslan Nigmatullin Date: Wed, 30 Sep 2026 00:26:47 +0000 Subject: [PATCH 17/24] Bind the app-server time provider to a local variable (#49411) GitOrigin-RevId: c0725029a8e58d384d17ff50d99030cc7d1c9e96 --- codex-rs/app-server/src/message_processor.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 3e8f1fbdd0a5..f5a9a65425ae 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -365,10 +365,11 @@ impl MessageProcessor { outgoing.clone(), thread_state_manager.clone(), )), - Some(app_server_time_provider( - outgoing.clone(), - thread_state_manager.clone(), - )), + Some({ + let time_provider = + app_server_time_provider(outgoing.clone(), thread_state_manager.clone()); + time_provider + }), ); match code_mode_session_provider { Some(provider) => manager.with_code_mode_session_provider(provider), From 16a7c0f0ebc6e366b3ecee183d21e02b5a100b7b Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Wed, 30 Sep 2026 00:37:33 +0000 Subject: [PATCH 18/24] Filter graceful shutdown guard and trigger traces from SQLite logs (#49414) ## What changed Set the default SQLite log filter to `DEBUG` for `tokio_graceful::guard` and `tokio_graceful::trigger`, dropping their `TRACE` events while retaining debug and higher-level diagnostics. ## Testing Extend the SQLite sink filter test to verify that guard and trigger traces are dropped, their debug events are retained, and `tokio_graceful::shutdown` trace events remain available. GitOrigin-RevId: c98495c834db35f3d8799ab4f0a1f7a82a53c8ef --- codex-rs/state/src/log_db.rs | 2 ++ codex-rs/state/src/log_db_filter_tests.rs | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/codex-rs/state/src/log_db.rs b/codex-rs/state/src/log_db.rs index 0361312cabba..6b4cc42c4392 100644 --- a/codex-rs/state/src/log_db.rs +++ b/codex-rs/state/src/log_db.rs @@ -74,6 +74,8 @@ pub fn default_filter() -> Targets { .with_target("codex_otel.log_only", LevelFilter::OFF) .with_target("codex_otel.trace_safe", LevelFilter::OFF) .with_target("rmcp", LevelFilter::INFO) + .with_target("tokio_graceful::guard", LevelFilter::DEBUG) + .with_target("tokio_graceful::trigger", LevelFilter::DEBUG) .with_target("codex_api::responses_websocket_timing", LevelFilter::OFF) .with_target("codex_core::post_sampling_token_estimate", LevelFilter::OFF) // Full model request bodies and streamed response payloads overwhelm the diff --git a/codex-rs/state/src/log_db_filter_tests.rs b/codex-rs/state/src/log_db_filter_tests.rs index d628dd834a27..3f04e1049cdf 100644 --- a/codex-rs/state/src/log_db_filter_tests.rs +++ b/codex-rs/state/src/log_db_filter_tests.rs @@ -49,6 +49,11 @@ async fn sqlite_sink_filters_noisy_targets_without_dropping_useful_diagnostics() tracing::warn!(target: "h2::proto::streams", "retained-http2-warning"); tracing::debug!(target: "rmcp::transport", "dropped-rmcp-debug"); tracing::info!(target: "rmcp::transport", "retained-rmcp-info"); + tracing::trace!(target: "tokio_graceful::guard", "dropped-guard-trace"); + tracing::debug!(target: "tokio_graceful::guard", "retained-guard-debug"); + tracing::trace!(target: "tokio_graceful::trigger", "dropped-trigger-trace"); + tracing::debug!(target: "tokio_graceful::trigger", "retained-trigger-debug"); + tracing::trace!(target: "tokio_graceful::shutdown", "retained-shutdown-trace"); tracing::debug!( target: "codex_rmcp_client::oauth", "dropped-codex-rmcp-client-debug" @@ -118,6 +123,21 @@ async fn sqlite_sink_filters_noisy_targets_without_dropping_useful_diagnostics() ), ("WARN", "h2::proto::streams", Some("retained-http2-warning")), ("INFO", "rmcp::transport", Some("retained-rmcp-info")), + ( + "DEBUG", + "tokio_graceful::guard", + Some("retained-guard-debug") + ), + ( + "DEBUG", + "tokio_graceful::trigger", + Some("retained-trigger-debug") + ), + ( + "TRACE", + "tokio_graceful::shutdown", + Some("retained-shutdown-trace") + ), ( "INFO", "codex_rmcp_client::oauth", From d940fb21f35ef32a736016a4c196150c6380bdb3 Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Wed, 30 Sep 2026 00:37:33 +0000 Subject: [PATCH 19/24] Truncate input text in protocol debug output (#49415) ## What changed Use `derive_more::Debug` for `ContentItem` and `UserInput` to limit debug formatting of `ContentItem::InputText` and `UserInput::Text` to a prefix of at most 512 bytes, ending at a UTF-8 character boundary. Append the full text length in bytes to each preview. GitOrigin-RevId: ca711dde8f0e3ba7047103415dbc1836f6401e61 --- codex-rs/Cargo.lock | 1 + codex-rs/protocol/Cargo.toml | 1 + codex-rs/protocol/src/models.rs | 3 ++- codex-rs/protocol/src/user_input.rs | 3 ++- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 56a74c464dca..9fd17b06a614 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -4268,6 +4268,7 @@ dependencies = [ "codex-utils-path-uri", "codex-utils-redacted-string", "codex-utils-string", + "derive_more 2.1.1", "encoding_rs", "gix-url", "globset", diff --git a/codex-rs/protocol/Cargo.toml b/codex-rs/protocol/Cargo.toml index 0ca5117c8caf..914ed5260c95 100644 --- a/codex-rs/protocol/Cargo.toml +++ b/codex-rs/protocol/Cargo.toml @@ -25,6 +25,7 @@ codex-utils-image = { workspace = true } codex-utils-path-uri = { workspace = true } codex-utils-redacted-string = { workspace = true } codex-utils-string = { workspace = true } +derive_more = { workspace = true, features = ["debug"] } encoding_rs = { workspace = true } gix-url = { workspace = true } globset = { workspace = true } diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index e31b88fdbbb0..bb29f84985f5 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -873,10 +873,11 @@ pub enum ResponseInputItem { }, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)] +#[derive(derive_more::Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ContentItem { InputText { + #[debug("{:?} <{} bytes>", &text[..text.floor_char_boundary(/*index*/ 512)], text.len())] text: String, }, InputImage { diff --git a/codex-rs/protocol/src/user_input.rs b/codex-rs/protocol/src/user_input.rs index 8f5f87c00a79..9e28c5ec3d9b 100644 --- a/codex-rs/protocol/src/user_input.rs +++ b/codex-rs/protocol/src/user_input.rs @@ -11,10 +11,11 @@ pub const MAX_USER_INPUT_TEXT_CHARS: usize = 1 << 20; /// User input #[non_exhaustive] -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS, JsonSchema)] +#[derive(derive_more::Debug, Clone, Deserialize, Serialize, PartialEq, TS, JsonSchema)] #[serde(tag = "type", rename_all = "snake_case")] pub enum UserInput { Text { + #[debug("{:?} <{} bytes>", &text[..text.floor_char_boundary(/*index*/ 512)], text.len())] text: String, /// UI-defined spans within `text` that should be treated as special elements. /// These are byte ranges into the UTF-8 `text` buffer and are used to render From ab84d71f5767e4a565ce81c2c426287cb48c7918 Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Wed, 30 Sep 2026 00:37:34 +0000 Subject: [PATCH 20/24] Omit payloads from multiline ANSI warnings (#49416) ## Why `ansi_escape_line` logged the rendered contents of every line when given multiline input, allowing large payloads to inflate warning logs. ## What changed Replace line contents in the warning with structured `input_bytes` and `line_count` fields. Preserve the existing behavior of returning the styled first line. ## Testing Enable unit tests for `codex-ansi-escape` and add coverage with small and 128 KiB tails, asserting the exact warning fields and preservation of first-line styling. GitOrigin-RevId: 4be985b4442eaa12f7fdee8a583d6e9714c618d8 --- codex-rs/Cargo.lock | 2 + codex-rs/ansi-escape/Cargo.toml | 5 +- codex-rs/ansi-escape/src/ansi_escape_tests.rs | 76 +++++++++++++++++++ codex-rs/ansi-escape/src/lib.rs | 10 ++- 4 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 codex-rs/ansi-escape/src/ansi_escape_tests.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 9fd17b06a614..a26f2fd61e6f 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2021,8 +2021,10 @@ name = "codex-ansi-escape" version = "0.0.0" dependencies = [ "ansi-to-tui", + "pretty_assertions", "ratatui", "tracing", + "tracing-subscriber", ] [[package]] diff --git a/codex-rs/ansi-escape/Cargo.toml b/codex-rs/ansi-escape/Cargo.toml index edb5b6e514cc..60b1aedf29c7 100644 --- a/codex-rs/ansi-escape/Cargo.toml +++ b/codex-rs/ansi-escape/Cargo.toml @@ -7,7 +7,6 @@ license.workspace = true [lib] name = "codex_ansi_escape" path = "src/lib.rs" -test = false doctest = false [lints] @@ -17,3 +16,7 @@ workspace = true ansi-to-tui = { workspace = true } ratatui = { workspace = true } tracing = { workspace = true, features = ["log"] } + +[dev-dependencies] +pretty_assertions = { workspace = true } +tracing-subscriber = { workspace = true } diff --git a/codex-rs/ansi-escape/src/ansi_escape_tests.rs b/codex-rs/ansi-escape/src/ansi_escape_tests.rs new file mode 100644 index 000000000000..d51deb73aa66 --- /dev/null +++ b/codex-rs/ansi-escape/src/ansi_escape_tests.rs @@ -0,0 +1,76 @@ +//! Check that multiline warnings omit payloads and preserve first-line rendering. + +use std::collections::BTreeMap; +use std::sync::Arc; +use std::sync::Mutex; + +use pretty_assertions::assert_eq; +use ratatui::style::Stylize; +use ratatui::text::Line; +use tracing::Event; +use tracing::Level; +use tracing::Subscriber; +use tracing::field::Field; +use tracing::field::Visit; +use tracing_subscriber::Layer; +use tracing_subscriber::layer::Context; +use tracing_subscriber::layer::SubscriberExt; + +use super::ansi_escape_line; + +#[derive(Debug, PartialEq, Eq)] +struct CapturedEvent { + level: Level, + fields: BTreeMap, +} + +impl Visit for CapturedEvent { + fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) { + self.fields + .insert(field.name().to_string(), format!("{value:?}")); + } +} + +struct CaptureLayer(Arc>>); + +impl Layer for CaptureLayer { + fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { + let mut captured = CapturedEvent { + level: *event.metadata().level(), + fields: BTreeMap::new(), + }; + event.record(&mut captured); + self.0.lock().expect("capture events").push(captured); + } +} + +#[test] +fn multiline_warning_contains_counts_and_preserves_the_styled_first_line() { + let events = Arc::new(Mutex::new(Vec::new())); + let subscriber = tracing_subscriber::registry().with(CaptureLayer(Arc::clone(&events))); + let mut expected_events = Vec::new(); + + tracing::subscriber::with_default(subscriber, || { + for tail_bytes in [8, 128 * 1024] { + let tail = "x".repeat(tail_bytes); + let input = format!("\x1b[31mfirst\x1b[0m\n{tail}\nlast"); + assert_eq!(ansi_escape_line(&input), Line::from("first".red())); + expected_events.push(CapturedEvent { + level: Level::WARN, + fields: BTreeMap::from([ + ("input_bytes".to_string(), input.len().to_string()), + ("line_count".to_string(), "3".to_string()), + ( + "message".to_string(), + "ansi_escape_line: expected a single line".to_string(), + ), + ]), + }); + } + }); + + assert_eq!( + *events.lock().expect("read captured events"), + expected_events + ); +} diff --git a/codex-rs/ansi-escape/src/lib.rs b/codex-rs/ansi-escape/src/lib.rs index b47cf14f8ea6..caaabf2a7aef 100644 --- a/codex-rs/ansi-escape/src/lib.rs +++ b/codex-rs/ansi-escape/src/lib.rs @@ -31,7 +31,11 @@ pub fn ansi_escape_line(s: &str) -> Line<'static> { [] => "".into(), [only] => only.clone(), [first, rest @ ..] => { - tracing::warn!("ansi_escape_line: expected a single line, got {first:?} and {rest:?}"); + tracing::warn!( + input_bytes = s.len(), + line_count = rest.len() + 1, + "ansi_escape_line: expected a single line" + ); first.clone() } } @@ -56,3 +60,7 @@ pub fn ansi_escape(s: &str) -> Text<'static> { }, } } + +#[cfg(test)] +#[path = "ansi_escape_tests.rs"] +mod tests; From d1d0e89558578f622b93702686963e67948fd84c Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Wed, 30 Sep 2026 01:14:51 +0000 Subject: [PATCH 21/24] Infer Windows UNC paths with forward and mixed slashes (#49424) ## Why `LegacyAppPathString` treated `//server/share/project` as POSIX, losing its UNC interpretation when inferring a `PathUri`. ## What changed Infer Windows syntax whenever an API path starts with two separators, including forward and mixed slashes, independently of the host. For example, `//server/share/project` now converts to `file://server/share/project`. Double-slash POSIX paths require an explicit `PathConvention::Posix`; inferred paths still undergo validation. ## Testing Add regression tests for UNC and namespace paths, mixed separators, URI serialization round trips, explicit POSIX conversion, convention mismatches, and malformed UNC paths. GitOrigin-RevId: 720893de517addb7a938e6784d1ed0f05df3ec0d --- .../utils/path-uri/src/api_path_string.rs | 15 +++-- .../path-uri/src/api_path_string_tests.rs | 61 ++++++++++++++++++- 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/codex-rs/utils/path-uri/src/api_path_string.rs b/codex-rs/utils/path-uri/src/api_path_string.rs index 3b9def4ba40d..b2ecdf0aed55 100644 --- a/codex-rs/utils/path-uri/src/api_path_string.rs +++ b/codex-rs/utils/path-uri/src/api_path_string.rs @@ -162,9 +162,11 @@ impl LegacyAppPathString { /// Infers the path convention of an absolute API path from its spelling. /// - /// Relative paths and ambiguous spellings return `None`. In particular, - /// slash-prefixed paths are treated as POSIX even when they could also be - /// interpreted as slash-delimited Windows UNC paths. + /// Two leading separators select Windows UNC or namespace syntax, including + /// forward and mixed slashes, independently of the current host. This favors + /// UNC paths over ambiguous double-slash POSIX paths. Call [`Self::to_path_uri`] + /// with an explicit POSIX convention to preserve that interpretation. + /// Relative paths return `None`; inferred prefixes still require validation. pub fn infer_absolute_path_convention(&self) -> Option { let bytes = self.0.as_bytes(); let has_windows_drive_root = matches!( @@ -172,7 +174,12 @@ impl LegacyAppPathString { [drive, b':', separator, ..] if drive.is_ascii_alphabetic() && is_windows_separator_byte(*separator) ); - if has_windows_drive_root || self.0.starts_with(r"\\") { + let has_windows_unc_root = matches!( + bytes, + [first, second, ..] + if is_windows_separator_byte(*first) && is_windows_separator_byte(*second) + ); + if has_windows_drive_root || has_windows_unc_root { Some(PathConvention::Windows) } else if self.0.starts_with('/') { Some(PathConvention::Posix) diff --git a/codex-rs/utils/path-uri/src/api_path_string_tests.rs b/codex-rs/utils/path-uri/src/api_path_string_tests.rs index d5a621d29079..b115a9006b73 100644 --- a/codex-rs/utils/path-uri/src/api_path_string_tests.rs +++ b/codex-rs/utils/path-uri/src/api_path_string_tests.rs @@ -418,7 +418,7 @@ fn infers_absolute_path_conventions_from_api_text() { (r"\\.\COM1", Some(PathConvention::Windows)), ("/workspace/file.rs", Some(PathConvention::Posix)), ("/C:/workspace/file.rs", Some(PathConvention::Posix)), - ("//server/share/file.rs", Some(PathConvention::Posix)), + ("//server/share/file.rs", Some(PathConvention::Windows)), ("", None), (".", None), ("subdir/file.rs", None), @@ -657,3 +657,62 @@ fn serializes_and_deserializes_as_a_string() { rendered ); } + +#[test] +fn inferred_unc_paths_accept_forward_and_mixed_slashes_on_every_host() { + for raw in [ + "//server/share/project", + r"//server/share\project", + r"/\server/share/project", + r"\/server/share/project", + r"\\server\share\project", + "//wsl.localhost/Ubuntu/home/user/project", + "//127.0.0.1/c$/project", + "//LOCALHOST/c$/project", + "//?/C:/project", + ] { + let path = LegacyAppPathString::from_string(raw); + let expected = path.to_path_uri(PathConvention::Windows).expect(raw); + let inferred = path.to_inferred_path_uri().expect(raw); + assert_eq!( + (inferred.clone(), inferred.infer_path_convention()), + (expected, Some(PathConvention::Windows)), + "{raw}" + ); + let serialized = serde_json::to_string(&inferred).expect("serialize URI"); + assert_eq!( + serde_json::from_str::(&serialized).expect("deserialize URI"), + inferred, + "{raw}" + ); + } + let slash = LegacyAppPathString::from_string("//server/share/project") + .to_inferred_path_uri() + .unwrap(); + assert_eq!( + slash, + PathUri::parse("file://server/share/project").unwrap() + ); +} + +#[test] +fn double_slash_posix_paths_require_an_explicit_convention() { + let path = LegacyAppPathString::from_string("//server/share/project"); + assert_eq!( + path.to_path_uri(PathConvention::Posix).unwrap(), + PathUri::parse("file:///server/share/project").unwrap() + ); + let cwd = PathUri::parse("file:///workspace").unwrap(); + assert!(matches!( + path.resolve_against(&cwd, /*user_home_dir*/ None), + Err(LegacyAppPathStringError::MismatchedConvention { .. }) + )); + for raw in ["//", "//server", "//server/", "///server/share"] { + assert!( + LegacyAppPathString::from_string(raw) + .to_inferred_path_uri() + .is_none(), + "{raw}" + ); + } +} From 8ea2c0e0d4bc1261258eb532b5a9d616147b6571 Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Wed, 30 Sep 2026 01:19:40 +0000 Subject: [PATCH 22/24] Prune diagnostic logs periodically by age and database size (#49425) ## Why Startup-only cleanup leaves expired logs in long-running sessions, and age-based retention alone does not limit database usage. ## What changed Run cleanup in the background immediately after initialization and every 30 minutes. Start with the existing 10-day retention window, then halve it until occupied SQLite pages fit within a 64 MiB budget or the window reaches one second. Preserve logs within that final second even when they exceed the budget. Exclude free pages from the budget so SQLite can reuse them, and retain the passive checkpoint after the first sweep. Stop the task when the runtime is dropped or the log pool is closed. ## Testing Add tests for the 10-day boundary, repeated retention-window halving, free-page accounting, the one-second retention floor, immediate startup cleanup, and periodic cleanup without new writes or a restart. Update existing log tests to use current timestamps so background retention does not remove their fixtures. GitOrigin-RevId: c6eeb5ec13006d4a03354e08322827429be53ce8 --- codex-rs/state/src/runtime.rs | 8 +- codex-rs/state/src/runtime/logs.rs | 189 +++++++++--------- .../state/src/runtime/logs_maintenance.rs | 84 ++++++++ .../src/runtime/logs_maintenance_tests.rs | 166 +++++++++++++++ 4 files changed, 350 insertions(+), 97 deletions(-) create mode 100644 codex-rs/state/src/runtime/logs_maintenance.rs create mode 100644 codex-rs/state/src/runtime/logs_maintenance_tests.rs diff --git a/codex-rs/state/src/runtime.rs b/codex-rs/state/src/runtime.rs index 2adc20c5b491..3cf16222264d 100644 --- a/codex-rs/state/src/runtime.rs +++ b/codex-rs/state/src/runtime.rs @@ -43,6 +43,7 @@ mod backfill; mod external_agent_config_imports; mod goals; mod logs; +mod logs_maintenance; mod memories; mod memory_versions; mod projects; @@ -276,12 +277,7 @@ impl StateRuntime { runtime.close().await; return Err(err); } - if let Err(err) = runtime.run_logs_startup_maintenance().await { - warn!( - "failed to run startup maintenance for logs db at {}: {err}", - logs_path.display(), - ); - } + runtime.start_periodic_logs_maintenance(std::time::Duration::from_secs(30 * 60)); Ok(runtime) } diff --git a/codex-rs/state/src/runtime/logs.rs b/codex-rs/state/src/runtime/logs.rs index 0cdc91124c56..9b331b491abe 100644 --- a/codex-rs/state/src/runtime/logs.rs +++ b/codex-rs/state/src/runtime/logs.rs @@ -1,7 +1,5 @@ use super::*; -const LOG_RETENTION_DAYS: i64 = 10; - impl StateRuntime { pub async fn insert_log(&self, entry: &LogEntry) -> anyhow::Result<()> { self.insert_logs(std::slice::from_ref(entry)).await @@ -280,30 +278,6 @@ WHERE id IN ( Ok(()) } - pub(crate) async fn delete_logs_before(&self, cutoff_ts: i64) -> anyhow::Result { - let result = sqlx::query("DELETE FROM logs WHERE ts < ?") - .bind(cutoff_ts) - .execute(self.logs_pool.as_ref()) - .await?; - Ok(result.rows_affected()) - } - - pub(crate) async fn run_logs_startup_maintenance(&self) -> anyhow::Result<()> { - let Some(cutoff) = - Utc::now().checked_sub_signed(chrono::Duration::days(LOG_RETENTION_DAYS)) - else { - return Ok(()); - }; - self.delete_logs_before(cutoff.timestamp()).await?; - // Startup cleanup should not wait behind or block foreground work. - // PASSIVE checkpoints copy whatever is immediately available and skip - // frames that would require waiting on active readers or writers. - sqlx::query("PRAGMA wal_checkpoint(PASSIVE)") - .execute(self.logs_pool.as_ref()) - .await?; - Ok(()) - } - /// Query logs with optional filters. pub async fn query_logs(&self, query: &LogQuery) -> anyhow::Result> { let mut builder = QueryBuilder::::new( @@ -568,6 +542,7 @@ mod tests { #[tokio::test] async fn insert_logs_use_dedicated_log_database() { + let now = chrono::Utc::now().timestamp(); let codex_home = unique_temp_dir(); let runtime = StateRuntime::init( crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()), @@ -578,7 +553,7 @@ mod tests { runtime .insert_logs(&[LogEntry { - ts: 1, + ts: now + 1, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -753,6 +728,7 @@ mod tests { #[tokio::test] async fn query_logs_with_search_matches_rendered_body_substring() { + let now = chrono::Utc::now().timestamp(); let codex_home = unique_temp_dir(); let runtime = StateRuntime::init( crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()), @@ -764,7 +740,7 @@ mod tests { runtime .insert_logs(&[ LogEntry { - ts: 1_700_000_001, + ts: now + 1, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -777,7 +753,7 @@ mod tests { module_path: None, }, LogEntry { - ts: 1_700_000_002, + ts: now + 2, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -809,6 +785,7 @@ mod tests { #[tokio::test] async fn query_logs_filters_level_set_without_rewriting_stored_level() { + let now = chrono::Utc::now().timestamp(); let codex_home = unique_temp_dir(); let runtime = StateRuntime::init( crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()), @@ -820,7 +797,7 @@ mod tests { runtime .insert_logs(&[ LogEntry { - ts: 1, + ts: now + 1, ts_nanos: 0, level: "TRACE".to_string(), target: "cli".to_string(), @@ -833,7 +810,7 @@ mod tests { module_path: None, }, LogEntry { - ts: 2, + ts: now + 2, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -846,7 +823,7 @@ mod tests { module_path: None, }, LogEntry { - ts: 3, + ts: now + 3, ts_nanos: 0, level: "warn".to_string(), target: "cli".to_string(), @@ -859,7 +836,7 @@ mod tests { module_path: None, }, LogEntry { - ts: 4, + ts: now + 4, ts_nanos: 0, level: "ERROR".to_string(), target: "cli".to_string(), @@ -897,6 +874,7 @@ mod tests { #[tokio::test] async fn insert_logs_prunes_old_rows_when_thread_exceeds_size_limit() { + let now = chrono::Utc::now().timestamp(); let codex_home = unique_temp_dir(); let runtime = StateRuntime::init( crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()), @@ -909,7 +887,7 @@ mod tests { runtime .insert_logs(&[ LogEntry { - ts: 1, + ts: now + 1, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -922,7 +900,7 @@ mod tests { module_path: Some("mod".to_string()), }, LogEntry { - ts: 2, + ts: now + 2, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -947,13 +925,14 @@ mod tests { .expect("query thread logs"); assert_eq!(rows.len(), 1); - assert_eq!(rows[0].ts, 2); + assert_eq!(rows[0].ts, now + 2); let _ = tokio::fs::remove_dir_all(codex_home).await; } #[tokio::test] async fn insert_logs_prunes_single_thread_row_when_it_exceeds_size_limit() { + let now = chrono::Utc::now().timestamp(); let codex_home = unique_temp_dir(); let runtime = StateRuntime::init( crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()), @@ -965,7 +944,7 @@ mod tests { let eleven_mebibytes = "d".repeat(11 * 1024 * 1024); runtime .insert_logs(&[LogEntry { - ts: 1, + ts: now + 1, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -995,6 +974,7 @@ mod tests { #[tokio::test] async fn insert_logs_prunes_threadless_rows_per_process_uuid_only() { + let now = chrono::Utc::now().timestamp(); let codex_home = unique_temp_dir(); let runtime = StateRuntime::init( crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()), @@ -1007,7 +987,7 @@ mod tests { runtime .insert_logs(&[ LogEntry { - ts: 1, + ts: now + 1, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1020,7 +1000,7 @@ mod tests { module_path: Some("mod".to_string()), }, LogEntry { - ts: 2, + ts: now + 2, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1033,7 +1013,7 @@ mod tests { module_path: Some("mod".to_string()), }, LogEntry { - ts: 3, + ts: now + 3, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1060,13 +1040,14 @@ mod tests { let mut timestamps: Vec = rows.into_iter().map(|row| row.ts).collect(); timestamps.sort_unstable(); - assert_eq!(timestamps, vec![2, 3]); + assert_eq!(timestamps, vec![now + 2, now + 3]); let _ = tokio::fs::remove_dir_all(codex_home).await; } #[tokio::test] async fn insert_logs_prunes_single_threadless_process_row_when_it_exceeds_size_limit() { + let now = chrono::Utc::now().timestamp(); let codex_home = unique_temp_dir(); let runtime = StateRuntime::init( crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()), @@ -1078,7 +1059,7 @@ mod tests { let eleven_mebibytes = "e".repeat(11 * 1024 * 1024); runtime .insert_logs(&[LogEntry { - ts: 1, + ts: now + 1, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1108,6 +1089,7 @@ mod tests { #[tokio::test] async fn insert_logs_prunes_threadless_rows_with_null_process_uuid() { + let now = chrono::Utc::now().timestamp(); let codex_home = unique_temp_dir(); let runtime = StateRuntime::init( crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()), @@ -1120,7 +1102,7 @@ mod tests { runtime .insert_logs(&[ LogEntry { - ts: 1, + ts: now + 1, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1133,7 +1115,7 @@ mod tests { module_path: Some("mod".to_string()), }, LogEntry { - ts: 2, + ts: now + 2, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1146,7 +1128,7 @@ mod tests { module_path: Some("mod".to_string()), }, LogEntry { - ts: 3, + ts: now + 3, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1172,13 +1154,14 @@ mod tests { let mut timestamps: Vec = rows.into_iter().map(|row| row.ts).collect(); timestamps.sort_unstable(); - assert_eq!(timestamps, vec![2, 3]); + assert_eq!(timestamps, vec![now + 2, now + 3]); let _ = tokio::fs::remove_dir_all(codex_home).await; } #[tokio::test] async fn insert_logs_prunes_single_threadless_null_process_row_when_it_exceeds_limit() { + let now = chrono::Utc::now().timestamp(); let codex_home = unique_temp_dir(); let runtime = StateRuntime::init( crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()), @@ -1190,7 +1173,7 @@ mod tests { let eleven_mebibytes = "f".repeat(11 * 1024 * 1024); runtime .insert_logs(&[LogEntry { - ts: 1, + ts: now + 1, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1220,6 +1203,7 @@ mod tests { #[tokio::test] async fn insert_logs_prunes_old_rows_when_thread_exceeds_row_limit() { + let now = chrono::Utc::now().timestamp(); let codex_home = unique_temp_dir(); let runtime = StateRuntime::init( crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()), @@ -1230,7 +1214,7 @@ mod tests { let entries: Vec = (1..=1_001) .map(|ts| LogEntry { - ts, + ts: now + ts, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1258,14 +1242,15 @@ mod tests { let timestamps: Vec = rows.into_iter().map(|row| row.ts).collect(); assert_eq!(timestamps.len(), 1_000); - assert_eq!(timestamps.first().copied(), Some(2)); - assert_eq!(timestamps.last().copied(), Some(1_001)); + assert_eq!(timestamps.first().copied(), Some(now + 2)); + assert_eq!(timestamps.last().copied(), Some(now + 1_001)); let _ = tokio::fs::remove_dir_all(codex_home).await; } #[tokio::test] async fn insert_logs_prunes_old_threadless_rows_when_process_exceeds_row_limit() { + let now = chrono::Utc::now().timestamp(); let codex_home = unique_temp_dir(); let runtime = StateRuntime::init( crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()), @@ -1276,7 +1261,7 @@ mod tests { let entries: Vec = (1..=1_001) .map(|ts| LogEntry { - ts, + ts: now + ts, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1308,14 +1293,15 @@ mod tests { .map(|row| row.ts) .collect(); assert_eq!(timestamps.len(), 1_000); - assert_eq!(timestamps.first().copied(), Some(2)); - assert_eq!(timestamps.last().copied(), Some(1_001)); + assert_eq!(timestamps.first().copied(), Some(now + 2)); + assert_eq!(timestamps.last().copied(), Some(now + 1_001)); let _ = tokio::fs::remove_dir_all(codex_home).await; } #[tokio::test] async fn insert_logs_prunes_old_threadless_null_process_rows_when_row_limit_exceeded() { + let now = chrono::Utc::now().timestamp(); let codex_home = unique_temp_dir(); let runtime = StateRuntime::init( crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()), @@ -1326,7 +1312,7 @@ mod tests { let entries: Vec = (1..=1_001) .map(|ts| LogEntry { - ts, + ts: now + ts, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1358,14 +1344,15 @@ mod tests { .map(|row| row.ts) .collect(); assert_eq!(timestamps.len(), 1_000); - assert_eq!(timestamps.first().copied(), Some(2)); - assert_eq!(timestamps.last().copied(), Some(1_001)); + assert_eq!(timestamps.first().copied(), Some(now + 2)); + assert_eq!(timestamps.last().copied(), Some(now + 1_001)); let _ = tokio::fs::remove_dir_all(codex_home).await; } #[tokio::test] async fn query_feedback_logs_returns_newest_lines_within_limit_in_order() { + let now = chrono::Utc::now().timestamp(); let codex_home = unique_temp_dir(); let runtime = StateRuntime::init( crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()), @@ -1377,7 +1364,7 @@ mod tests { runtime .insert_logs(&[ LogEntry { - ts: 1, + ts: now + 1, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1390,7 +1377,7 @@ mod tests { module_path: None, }, LogEntry { - ts: 2, + ts: now + 2, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1403,7 +1390,7 @@ mod tests { module_path: None, }, LogEntry { - ts: 3, + ts: now + 3, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1427,9 +1414,14 @@ mod tests { assert_eq!( String::from_utf8(bytes).expect("valid utf-8"), [ - format_feedback_log_line(/*ts*/ 1, /*ts_nanos*/ 0, "INFO", "alpha"), - format_feedback_log_line(/*ts*/ 2, /*ts_nanos*/ 0, "INFO", "bravo"), - format_feedback_log_line(/*ts*/ 3, /*ts_nanos*/ 0, "INFO", "charlie"), + format_feedback_log_line(/*ts*/ now + 1, /*ts_nanos*/ 0, "INFO", "alpha"), + format_feedback_log_line(/*ts*/ now + 2, /*ts_nanos*/ 0, "INFO", "bravo"), + format_feedback_log_line( + /*ts*/ now + 3, + /*ts_nanos*/ 0, + "INFO", + "charlie" + ), ] .concat() ); @@ -1439,6 +1431,7 @@ mod tests { #[tokio::test] async fn query_feedback_logs_excludes_oversized_newest_row() { + let now = chrono::Utc::now().timestamp(); let codex_home = unique_temp_dir(); let runtime = StateRuntime::init( crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()), @@ -1451,7 +1444,7 @@ mod tests { runtime .insert_logs(&[ LogEntry { - ts: 1, + ts: now + 1, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1464,7 +1457,7 @@ mod tests { module_path: None, }, LogEntry { - ts: 2, + ts: now + 2, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1492,6 +1485,7 @@ mod tests { #[tokio::test] async fn query_feedback_logs_includes_threadless_rows_from_same_process() { + let now = chrono::Utc::now().timestamp(); let codex_home = unique_temp_dir(); let runtime = StateRuntime::init( crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()), @@ -1503,7 +1497,7 @@ mod tests { runtime .insert_logs(&[ LogEntry { - ts: 1, + ts: now + 1, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1516,7 +1510,7 @@ mod tests { module_path: None, }, LogEntry { - ts: 2, + ts: now + 2, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1529,7 +1523,7 @@ mod tests { module_path: None, }, LogEntry { - ts: 3, + ts: now + 3, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1542,7 +1536,7 @@ mod tests { module_path: None, }, LogEntry { - ts: 4, + ts: now + 4, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1567,19 +1561,19 @@ mod tests { String::from_utf8(bytes).expect("valid utf-8"), [ format_feedback_log_line( - /*ts*/ 1, + /*ts*/ now + 1, /*ts_nanos*/ 0, "INFO", "threadless-before" ), format_feedback_log_line( - /*ts*/ 2, + /*ts*/ now + 2, /*ts_nanos*/ 0, "INFO", "thread-scoped" ), format_feedback_log_line( - /*ts*/ 3, + /*ts*/ now + 3, /*ts_nanos*/ 0, "INFO", "threadless-after" @@ -1593,6 +1587,7 @@ mod tests { #[tokio::test] async fn query_feedback_logs_excludes_threadless_rows_from_prior_processes() { + let now = chrono::Utc::now().timestamp(); let codex_home = unique_temp_dir(); let runtime = StateRuntime::init( crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()), @@ -1604,7 +1599,7 @@ mod tests { runtime .insert_logs(&[ LogEntry { - ts: 1, + ts: now + 1, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1617,7 +1612,7 @@ mod tests { module_path: None, }, LogEntry { - ts: 2, + ts: now + 2, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1630,7 +1625,7 @@ mod tests { module_path: None, }, LogEntry { - ts: 3, + ts: now + 3, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1643,7 +1638,7 @@ mod tests { module_path: None, }, LogEntry { - ts: 4, + ts: now + 4, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1668,19 +1663,19 @@ mod tests { String::from_utf8(bytes).expect("valid utf-8"), [ format_feedback_log_line( - /*ts*/ 2, + /*ts*/ now + 2, /*ts_nanos*/ 0, "INFO", "old-process-thread" ), format_feedback_log_line( - /*ts*/ 3, + /*ts*/ now + 3, /*ts_nanos*/ 0, "INFO", "new-process-thread" ), format_feedback_log_line( - /*ts*/ 4, + /*ts*/ now + 4, /*ts_nanos*/ 0, "INFO", "new-process-threadless" @@ -1694,6 +1689,7 @@ mod tests { #[tokio::test] async fn query_feedback_logs_keeps_newest_suffix_across_thread_and_threadless_logs() { + let now = chrono::Utc::now().timestamp(); let codex_home = unique_temp_dir(); let runtime = StateRuntime::init( crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()), @@ -1714,7 +1710,7 @@ mod tests { runtime .insert_logs(&[ LogEntry { - ts: 1, + ts: now + 1, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1727,7 +1723,7 @@ mod tests { module_path: None, }, LogEntry { - ts: 2, + ts: now + 2, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1740,7 +1736,7 @@ mod tests { module_path: None, }, LogEntry { - ts: 3, + ts: now + 3, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1772,6 +1768,7 @@ mod tests { #[tokio::test] async fn query_feedback_logs_for_threads_merges_requested_threads_and_threadless_rows() { + let now = chrono::Utc::now().timestamp(); let codex_home = unique_temp_dir(); let runtime = StateRuntime::init( crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()), @@ -1783,7 +1780,7 @@ mod tests { runtime .insert_logs(&[ LogEntry { - ts: 1, + ts: now + 1, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1796,7 +1793,7 @@ mod tests { module_path: None, }, LogEntry { - ts: 2, + ts: now + 2, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1809,7 +1806,7 @@ mod tests { module_path: None, }, LogEntry { - ts: 3, + ts: now + 3, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1822,7 +1819,7 @@ mod tests { module_path: None, }, LogEntry { - ts: 4, + ts: now + 4, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1835,7 +1832,7 @@ mod tests { module_path: None, }, LogEntry { - ts: 5, + ts: now + 5, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1848,7 +1845,7 @@ mod tests { module_path: None, }, LogEntry { - ts: 6, + ts: now + 6, ts_nanos: 0, level: "INFO".to_string(), target: "cli".to_string(), @@ -1872,16 +1869,26 @@ mod tests { assert_eq!( String::from_utf8(bytes).expect("valid utf-8"), [ - format_feedback_log_line(/*ts*/ 1, /*ts_nanos*/ 0, "INFO", "thread-1"), - format_feedback_log_line(/*ts*/ 2, /*ts_nanos*/ 0, "INFO", "thread-2"), format_feedback_log_line( - /*ts*/ 3, + /*ts*/ now + 1, + /*ts_nanos*/ 0, + "INFO", + "thread-1" + ), + format_feedback_log_line( + /*ts*/ now + 2, + /*ts_nanos*/ 0, + "INFO", + "thread-2" + ), + format_feedback_log_line( + /*ts*/ now + 3, /*ts_nanos*/ 0, "INFO", "threadless-proc-1" ), format_feedback_log_line( - /*ts*/ 4, + /*ts*/ now + 4, /*ts_nanos*/ 0, "INFO", "threadless-proc-2" diff --git a/codex-rs/state/src/runtime/logs_maintenance.rs b/codex-rs/state/src/runtime/logs_maintenance.rs new file mode 100644 index 000000000000..7d7879730c5c --- /dev/null +++ b/codex-rs/state/src/runtime/logs_maintenance.rs @@ -0,0 +1,84 @@ +//! Prune diagnostic logs in the background immediately, then on a timer. +//! SQLite keeps freed pages for reuse. + +use super::StateRuntime; +use chrono::Utc; +use sqlx::SqlitePool; +use std::sync::Arc; +use std::time::Duration; + +const LOG_RETENTION_SECONDS: i64 = 10 * 24 * 60 * 60; // 10 days +const LOG_DATABASE_BUDGET_BYTES: i64 = 64 * 1024 * 1024; // 64 MiB + +impl StateRuntime { + pub(super) fn start_periodic_logs_maintenance(self: &Arc, period: Duration) { + let runtime = Arc::downgrade(self); + tokio::spawn(async move { + let mut interval = tokio::time::interval_at( + /*start*/ tokio::time::Instant::now(), + /*period*/ period, + ); + interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + let mut first_sweep = true; + loop { + // Hold no strong runtime reference while waiting. + interval.tick().await; + let Some(runtime) = runtime.upgrade() else { + break; + }; + if runtime.logs_pool.is_closed() { + break; + } + if let Err(err) = prune_by_age_and_size( + &runtime.logs_pool, + Utc::now().timestamp(), + LOG_DATABASE_BUDGET_BYTES, + ) + .await + { + tracing::warn!("failed to prune diagnostic logs: {err}"); + } + if first_sweep { + first_sweep = false; + // Preserve the startup checkpoint without waiting for readers or writers. + if let Err(err) = sqlx::query("PRAGMA wal_checkpoint(PASSIVE)") + .execute(runtime.logs_pool.as_ref()) + .await + { + tracing::warn!("failed to checkpoint diagnostic logs: {err}"); + } + } + } + }); + } +} + +async fn prune_by_age_and_size( + pool: &SqlitePool, + now: i64, + budget_bytes: i64, +) -> anyhow::Result<()> { + let mut retention_seconds = LOG_RETENTION_SECONDS; + loop { + sqlx::query("DELETE FROM logs WHERE ts < ?") + .bind(now.saturating_sub(retention_seconds)) + .execute(pool) + .await?; + // Exclude free pages: SQLite reuses them without shrinking the file. + let occupied_bytes: i64 = sqlx::query_scalar( + "SELECT (page_count - freelist_count) * page_size \ + FROM pragma_page_count(), pragma_freelist_count(), pragma_page_size()", + ) + .fetch_one(pool) + .await?; + if occupied_bytes <= budget_bytes || retention_seconds == 1 { + return Ok(()); + } + // Keep the newest second even if it exceeds the budget. + retention_seconds = (retention_seconds / 2).max(1); + } +} + +#[cfg(test)] +#[path = "logs_maintenance_tests.rs"] +mod tests; diff --git a/codex-rs/state/src/runtime/logs_maintenance_tests.rs b/codex-rs/state/src/runtime/logs_maintenance_tests.rs new file mode 100644 index 000000000000..1ca3939b1ab2 --- /dev/null +++ b/codex-rs/state/src/runtime/logs_maintenance_tests.rs @@ -0,0 +1,166 @@ +//! Exercise age/size pruning and automatic cleanup of an idle runtime. + +use super::LOG_RETENTION_SECONDS; +use super::StateRuntime; +use super::prune_by_age_and_size; +use crate::SqliteConfig; +use crate::migrations::LOGS_MIGRATOR; +use chrono::Utc; +use codex_utils_absolute_path::test_support::PathExt; +use pretty_assertions::assert_eq; +use sqlx::SqlitePool; +use std::path::PathBuf; +use std::time::Duration; + +const NOW: i64 = 2_000_000_000; +const DAY: i64 = 24 * 60 * 60; + +async fn database_with_logs(ages: &[i64]) -> (SqlitePool, PathBuf) { + let directory = super::super::test_support::unique_temp_dir(); + tokio::fs::create_dir_all(&directory).await.unwrap(); + let pool = SqliteConfig::new_for_testing(directory.as_path().abs()) + .open_read_write_pool(&directory.join("logs.sqlite")) + .await + .unwrap(); + LOGS_MIGRATOR.run(&pool).await.unwrap(); + for age in ages { + sqlx::query( + "INSERT INTO logs (ts, ts_nanos, level, target, feedback_log_body) \ + VALUES (?, 0, 'INFO', 'retention-test', ?)", + ) + .bind(NOW - age) + .bind("x".repeat(128 * 1024)) + .execute(&pool) + .await + .unwrap(); + } + (pool, directory) +} + +async fn retained_ages(pool: &SqlitePool) -> Vec { + sqlx::query_scalar("SELECT ? - ts FROM logs ORDER BY ts DESC") + .bind(NOW) + .fetch_all(pool) + .await + .unwrap() +} + +#[tokio::test] +async fn keeps_ten_day_boundary_when_under_budget() { + let (pool, directory) = + database_with_logs(&[0, LOG_RETENTION_SECONDS, LOG_RETENTION_SECONDS + 1]).await; + prune_by_age_and_size(&pool, NOW, /*budget_bytes*/ 1024 * 1024) + .await + .unwrap(); + assert_eq!(retained_ages(&pool).await, vec![0, LOG_RETENTION_SECONDS]); + pool.close().await; + tokio::fs::remove_dir_all(directory).await.unwrap(); +} + +#[tokio::test] +async fn halves_again_when_first_halving_deletes_nothing() { + let (pool, directory) = database_with_logs(&[DAY, 2 * DAY, 4 * DAY]).await; + // Three payloads exceed this budget; two fit, including table/index pages. + prune_by_age_and_size(&pool, NOW, /*budget_bytes*/ 350 * 1024) + .await + .unwrap(); + assert_eq!(retained_ages(&pool).await, vec![DAY, 2 * DAY]); + // A second cleanup must ignore free pages left in the file. + let allocated: i64 = sqlx::query_scalar( + "SELECT page_count * page_size FROM pragma_page_count(), pragma_page_size()", + ) + .fetch_one(&pool) + .await + .unwrap(); + assert!(allocated > 350 * 1024); + prune_by_age_and_size(&pool, NOW, /*budget_bytes*/ 350 * 1024) + .await + .unwrap(); + assert_eq!(retained_ages(&pool).await, vec![DAY, 2 * DAY]); + pool.close().await; + tokio::fs::remove_dir_all(directory).await.unwrap(); +} + +#[tokio::test] +async fn stops_at_one_second_when_recent_rows_exceed_budget() { + let (pool, directory) = database_with_logs(&[-1, 0, 1, 2, DAY]).await; + prune_by_age_and_size(&pool, NOW, /*budget_bytes*/ 1) + .await + .unwrap(); + assert_eq!(retained_ages(&pool).await, vec![-1, 0, 1]); + pool.close().await; + tokio::fs::remove_dir_all(directory).await.unwrap(); +} + +#[tokio::test] +async fn periodic_cleanup_prunes_without_new_log_writes_or_restart() { + let directory = super::super::test_support::unique_temp_dir(); + let runtime = StateRuntime::init( + SqliteConfig::new_for_testing(directory.as_path().abs()), + "test-provider".to_string(), + ) + .await + .unwrap(); + let now = Utc::now().timestamp(); + for timestamp in [now - 11 * DAY, now] { + sqlx::query("INSERT INTO logs (ts, ts_nanos, level, target) VALUES (?, 0, 'INFO', 'test')") + .bind(timestamp) + .execute(runtime.logs_pool.as_ref()) + .await + .unwrap(); + } + runtime.start_periodic_logs_maintenance(Duration::from_millis(10)); + tokio::time::timeout(Duration::from_secs(5), async { + loop { + let timestamps: Vec = sqlx::query_scalar("SELECT ts FROM logs ORDER BY ts") + .fetch_all(runtime.logs_pool.as_ref()) + .await + .unwrap(); + if timestamps == vec![now] { + break; + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + }) + .await + .expect("periodic cleanup should remove expired logs while idle"); + runtime.close().await; + tokio::fs::remove_dir_all(directory).await.unwrap(); +} + +#[tokio::test] +async fn startup_cleanup_runs_without_waiting_for_the_period() { + let directory = super::super::test_support::unique_temp_dir(); + tokio::fs::create_dir_all(&directory).await.unwrap(); + let sqlite = SqliteConfig::new_for_testing(directory.as_path().abs()); + let pool = sqlite + .open_read_write_pool(&sqlite.logs_db_path()) + .await + .unwrap(); + LOGS_MIGRATOR.run(&pool).await.unwrap(); + sqlx::query("INSERT INTO logs (ts, ts_nanos, level, target) VALUES (0, 0, 'INFO', 'test')") + .execute(&pool) + .await + .unwrap(); + pool.close().await; + + let runtime = StateRuntime::init(sqlite, "test-provider".to_string()) + .await + .unwrap(); + tokio::time::timeout(Duration::from_secs(5), async { + loop { + let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM logs") + .fetch_one(runtime.logs_pool.as_ref()) + .await + .unwrap(); + if count == 0 { + break; + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + }) + .await + .expect("startup cleanup should run before the 30-minute interval"); + runtime.close().await; + tokio::fs::remove_dir_all(directory).await.unwrap(); +} From 2cc65cdd4c7c167f0c7252fcb5165d649c16aca0 Mon Sep 17 00:00:00 2001 From: Benjamin Carlsson Date: Wed, 30 Sep 2026 01:21:05 +0000 Subject: [PATCH 23/24] Enable analytics by default for daemon-launched app servers (#49426) ## Why Daemon-launched app servers should use the same analytics default as first-party clients while honoring explicit user opt-outs. ## What changed Pass `--analytics-default-enabled` when the PID backend launches an app server, with or without remote control. Users can still disable analytics with `analytics.enabled = false` in `config.toml`. ## Testing Update argument assertions for both remote-control modes and the daemon start/restart test to expect the analytics flag alongside launch feature overrides. GitOrigin-RevId: 8a222a00fc56737f5fe2b0385abf59466d1a6671 --- codex-rs/app-server-daemon/src/backend/pid.rs | 2 ++ .../app-server-daemon/src/backend/pid_tests.rs | 15 +++++++++++++-- .../app-server-daemon/src/update_loop_tests.rs | 4 ++-- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/codex-rs/app-server-daemon/src/backend/pid.rs b/codex-rs/app-server-daemon/src/backend/pid.rs index 22e34d9095d2..0e69a3f0e90c 100644 --- a/codex-rs/app-server-daemon/src/backend/pid.rs +++ b/codex-rs/app-server-daemon/src/backend/pid.rs @@ -394,6 +394,8 @@ impl PidBackend { } }; if matches!(self.command_kind, PidCommandKind::AppServer { .. }) { + // Match first-party clients' default while preserving explicit analytics opt-outs. + args.push("--analytics-default-enabled".into()); for (name, enabled) in &self.feature_overrides { args.extend(["-c".into(), format!("features.{name}={enabled}").into()]); } diff --git a/codex-rs/app-server-daemon/src/backend/pid_tests.rs b/codex-rs/app-server-daemon/src/backend/pid_tests.rs index 19d262c09706..dbd6a4075ac6 100644 --- a/codex-rs/app-server-daemon/src/backend/pid_tests.rs +++ b/codex-rs/app-server-daemon/src/backend/pid_tests.rs @@ -602,7 +602,13 @@ fn app_server_remote_control_uses_runtime_flag() { assert_eq!( backend.command_args(), - vec!["app-server", "--remote-control", "--listen", "unix://"] + vec![ + "app-server", + "--remote-control", + "--listen", + "unix://", + "--analytics-default-enabled" + ] ); } @@ -616,7 +622,12 @@ fn app_server_disabled_remote_control_uses_compatible_args_and_runtime_env() { assert_eq!( backend.command_args(), - vec!["app-server", "--listen", "unix://"] + vec![ + "app-server", + "--listen", + "unix://", + "--analytics-default-enabled" + ] ); assert_eq!( backend.command_env(), diff --git a/codex-rs/app-server-daemon/src/update_loop_tests.rs b/codex-rs/app-server-daemon/src/update_loop_tests.rs index d932742371bd..0847655a30cf 100644 --- a/codex-rs/app-server-daemon/src/update_loop_tests.rs +++ b/codex-rs/app-server-daemon/src/update_loop_tests.rs @@ -584,9 +584,9 @@ async fn daemon_start_and_restart_preserve_launch_features() { features ); let expected = if features.is_empty() { - "app-server\n--listen\nunix://\n--managed-daemon\n" + "app-server\n--listen\nunix://\n--analytics-default-enabled\n--managed-daemon\n" } else { - "app-server\n--listen\nunix://\n-c\nfeatures.api_key_model_discovery=true\n-c\nfeatures.code_mode_host=false\n--managed-daemon\n" + "app-server\n--listen\nunix://\n--analytics-default-enabled\n-c\nfeatures.api_key_model_discovery=true\n-c\nfeatures.code_mode_host=false\n--managed-daemon\n" }; assert_eq!(std::fs::read_to_string(&args_path).unwrap(), expected); let reused = daemon From d8f69ea8bc998a33c19a08176cfc22fd4352c592 Mon Sep 17 00:00:00 2001 From: cooper-oai Date: Wed, 30 Sep 2026 01:26:55 +0000 Subject: [PATCH 24/24] Preserve bootstrap discovery across authentication changes (#49432) ## Why Embedded app-server configuration discovery must remain available after login and workspace changes, while content access granted to the previous account must be revoked. ## What changed Give `AuthRouteConfig` a separate application network policy and bind embedded bootstrap authentication to the effective application policy. Use that policy for account-owned content clients and invalidate it when the authenticated owner changes, preserving the local, endpoint-restricted bootstrap policy. ## Testing Add a regression test covering bootstrap discovery across login, workspace changes, and sign-out; revocation of previous account clients and permits; and continued enforcement of bootstrap endpoint restrictions and local requirements. GitOrigin-RevId: 083933e69508224f8232349ba3750c02329af06a --- .../app-server/src/in_process_bootstrap.rs | 3 +- .../src/in_process_bootstrap_tests.rs | 132 +++++++++++++++++- codex-rs/login/src/auth/manager.rs | 8 +- codex-rs/login/src/outbound_proxy.rs | 14 ++ 4 files changed, 149 insertions(+), 8 deletions(-) diff --git a/codex-rs/app-server/src/in_process_bootstrap.rs b/codex-rs/app-server/src/in_process_bootstrap.rs index b39ca0605dd1..a21c3b0b96d0 100644 --- a/codex-rs/app-server/src/in_process_bootstrap.rs +++ b/codex-rs/app-server/src/in_process_bootstrap.rs @@ -92,7 +92,8 @@ impl EmbeddedNetworkPolicy { .restrict_to_endpoints(endpoint.parse().into_iter().collect()), ), ) - .with_local_bootstrap_factory(factory); + .with_local_bootstrap_factory(factory) + .with_application_network_policy(self.effective.policy()); auth } } diff --git a/codex-rs/app-server/src/in_process_bootstrap_tests.rs b/codex-rs/app-server/src/in_process_bootstrap_tests.rs index bbc034797119..c6aa41d29709 100644 --- a/codex-rs/app-server/src/in_process_bootstrap_tests.rs +++ b/codex-rs/app-server/src/in_process_bootstrap_tests.rs @@ -1,7 +1,10 @@ -//! Embedded startup must keep the project selected by the caller. +//! Embedded startup preserves caller configuration and bootstrap access across authentication. +use super::EmbeddedNetworkPolicy; use super::configure; use crate::config_manager::ConfigManager; +use base64::Engine; +use base64::engine::general_purpose::URL_SAFE_NO_PAD; use codex_arg0::Arg0DispatchPaths; use codex_config::CloudConfigBundleLoader; use codex_config::LoaderOverrides; @@ -9,10 +12,137 @@ use codex_config::NoopThreadConfigLoader; use codex_core::config::ConfigBuilder; use codex_core::config::ConfigOverrides; use codex_core::config::set_project_trust_level; +use codex_http_client::DestinationPolicy; +use codex_http_client::HttpClientFactory; +use codex_http_client::NetworkPermit; +use codex_http_client::NetworkPolicyDenied; +use codex_login::AuthCredentialsStoreMode; +use codex_login::AuthKeyringBackendKind; +use codex_login::AuthManager; +use codex_login::CodexAuth; +use codex_login::ExternalAuth; +use codex_login::ExternalAuthFuture; +use codex_login::ExternalAuthRefreshContext; use codex_protocol::config_types::TrustLevel; use pretty_assertions::assert_eq; use std::sync::Arc; +struct BootstrapExternalAuth(CodexAuth); + +impl ExternalAuth for BootstrapExternalAuth { + fn resolve(&self) -> ExternalAuthFuture<'_, CodexAuth> { + Box::pin(async { Ok(self.0.clone()) }) + } + + fn refresh(&self, _context: ExternalAuthRefreshContext) -> ExternalAuthFuture<'_, CodexAuth> { + self.resolve() + } +} + +#[tokio::test] +async fn bootstrap_discovery_survives_identity_installation_but_account_access_is_revoked() +-> anyhow::Result<()> { + let home = tempfile::tempdir()?; + let overrides = LoaderOverrides::without_managed_config_for_tests(); + let config = ConfigBuilder::default() + .codex_home(home.path().to_path_buf()) + .loader_overrides(overrides.clone()) + .build() + .await?; + let policy = EmbeddedNetworkPolicy::load(&overrides).await; + let auth_config = policy.bind_bootstrap_auth(config.auth_config()); + let bootstrap = auth_config.auth_route_config.http_client_factory().clone(); + let bundle_url = + codex_backend_client::Client::new(config.chatgpt_base_url.clone(), bootstrap.clone()) + .config_bundle_url() + .parse()?; + let content_url = "https://chatgpt.com/backend-api/codex/responses".parse()?; + let manager = AuthManager::new( + home.path().to_path_buf(), + /*enable_codex_api_key_env*/ false, + AuthCredentialsStoreMode::Ephemeral, + /*forced_chatgpt_workspace_id*/ None, + Some(config.chatgpt_base_url.clone()), + AuthKeyringBackendKind::default(), + auth_config.auth_route_config, + ) + .await; + assert!(manager.auth_cached().is_none()); + + bootstrap.network_policy().acquire(&bundle_url)?; + let mut previous_access: Option<(HttpClientFactory, NetworkPermit)> = None; + for identity in ["bootstrap-account-one", "bootstrap-account-two"] { + let token = format!( + "e30.{}.signature", + URL_SAFE_NO_PAD.encode(serde_json::to_vec(&serde_json::json!({ + "sub": "bootstrap-user", + "https://api.openai.com/auth": { + "chatgpt_user_id": "bootstrap-user", + "chatgpt_account_id": identity, + }, + }))?), + ); + let auth = CodexAuth::from_external_chatgpt_tokens(&token, identity, Some("enterprise"))?; + manager + .set_external_auth(Arc::new(BootstrapExternalAuth(auth))) + .await?; + + if let Some((client, permit)) = previous_access.take() { + assert_eq!(permit.check(), Err(NetworkPolicyDenied::Revoked)); + assert_eq!( + client.network_policy().acquire(&content_url).err(), + Some(NetworkPolicyDenied::Revoked) + ); + } + + // Discovery must remain possible after both initial login and a workspace change. + bootstrap.network_policy().acquire(&bundle_url)?; + assert_eq!( + bootstrap.network_policy().acquire(&content_url).err(), + Some(NetworkPolicyDenied::Destination) + ); + assert_eq!( + manager + .application_network_policy() + .acquire(&content_url) + .err(), + Some(NetworkPolicyDenied::Unavailable) + ); + + assert!(policy.effective.publish( + policy.effective.policy().revision(), + DestinationPolicy::Unrestricted, + )); + let client = manager.http_client_factory(); + let permit = client.network_policy().acquire(&content_url)?; + previous_access = Some((client, permit)); + } + + let (client, permit) = previous_access.expect("the last identity has active account access"); + manager.clear_external_auth(); + assert_eq!(permit.check(), Err(NetworkPolicyDenied::Revoked)); + assert_eq!( + client.network_policy().acquire(&content_url).err(), + Some(NetworkPolicyDenied::Revoked) + ); + assert_eq!( + manager + .application_network_policy() + .acquire(&content_url) + .err(), + Some(NetworkPolicyDenied::Unavailable) + ); + bootstrap.network_policy().acquire(&bundle_url)?; + + // Losing local requirements must still close the bootstrap route. + policy.local.unavailable(policy.local.policy().revision()); + assert_eq!( + bootstrap.network_policy().acquire(&bundle_url).err(), + Some(NetworkPolicyDenied::Unavailable) + ); + Ok(()) +} + #[tokio::test] async fn startup_reloads_the_callers_selected_project() -> anyhow::Result<()> { let temp = tempfile::tempdir()?; diff --git a/codex-rs/login/src/auth/manager.rs b/codex-rs/login/src/auth/manager.rs index 66c20a656fb9..cdc9c8d35603 100644 --- a/codex-rs/login/src/auth/manager.rs +++ b/codex-rs/login/src/auth/manager.rs @@ -2136,10 +2136,7 @@ fn default_agent_identity_authapi_base_url() -> Option { impl AuthManager { /// Returns the application policy associated with this account owner. pub fn application_network_policy(&self) -> codex_http_client::NetworkPolicy { - self.auth_route_config - .http_client_factory() - .network_policy() - .clone() + self.auth_route_config.application_network_policy().clone() } /// Creates content clients bound to the account currently owned by this manager. @@ -2637,8 +2634,7 @@ impl AuthManager { auth_changed_for_refresh && !same_owner(previous, new_auth.as_ref()); if owner_changed { self.auth_route_config - .http_client_factory() - .network_policy() + .application_network_policy() .invalidate(); } if auth_changed_for_refresh { diff --git a/codex-rs/login/src/outbound_proxy.rs b/codex-rs/login/src/outbound_proxy.rs index 430b99d453fa..89fca57e089c 100644 --- a/codex-rs/login/src/outbound_proxy.rs +++ b/codex-rs/login/src/outbound_proxy.rs @@ -1,4 +1,5 @@ use codex_http_client::HttpClientFactory; +use codex_http_client::NetworkPolicy; /// Auth-layer adapter around client-owned proxy policy. /// @@ -7,6 +8,7 @@ use codex_http_client::HttpClientFactory; #[derive(Debug, Clone, PartialEq, Eq)] pub struct AuthRouteConfig { http_client_factory: HttpClientFactory, + application_network_policy: NetworkPolicy, local_bootstrap_factory: Option, } @@ -14,11 +16,19 @@ impl AuthRouteConfig { /// Adapts an application-resolved HTTP client factory for auth requests. pub fn from_http_client_factory(http_client_factory: HttpClientFactory) -> Self { Self { + application_network_policy: http_client_factory.network_policy().clone(), http_client_factory, local_bootstrap_factory: None, } } + /// Selects the account-owned policy revoked when the authenticated identity changes. + /// Bootstrap discovery can use separate local rules until this policy is loaded. + pub fn with_application_network_policy(mut self, policy: NetworkPolicy) -> Self { + self.application_network_policy = policy; + self + } + /// Installs the configuration owner's local-only policy for auth discovery. /// Only auth-owned endpoint constructors can access this factory. pub fn with_local_bootstrap_factory(mut self, factory: HttpClientFactory) -> Self { @@ -43,4 +53,8 @@ impl AuthRouteConfig { pub fn http_client_factory(&self) -> &HttpClientFactory { &self.http_client_factory } + + pub(crate) fn application_network_policy(&self) -> &NetworkPolicy { + &self.application_network_policy + } }