From 675cb1afbda5de4585345371019756b107a5812e Mon Sep 17 00:00:00 2001 From: Curtis 'Fjord' Hawthorne Date: Tue, 26 May 2026 14:17:43 -0700 Subject: [PATCH 01/87] Clarify view_image tool description (#23949) --- codex-rs/core/src/tools/handlers/view_image_spec.rs | 2 +- codex-rs/core/tests/suite/code_mode.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codex-rs/core/src/tools/handlers/view_image_spec.rs b/codex-rs/core/src/tools/handlers/view_image_spec.rs index 662cd639a6..ed0b1021a5 100644 --- a/codex-rs/core/src/tools/handlers/view_image_spec.rs +++ b/codex-rs/core/src/tools/handlers/view_image_spec.rs @@ -40,7 +40,7 @@ pub fn create_view_image_tool(options: ViewImageToolOptions) -> ToolSpec { ToolSpec::Function(ResponsesApiTool { name: VIEW_IMAGE_TOOL_NAME.to_string(), - description: "View a local image from the filesystem (only use if given a full filepath by the user, and the image isn't already attached to the thread context within tags)." + description: "View a local image file from the filesystem when visual inspection is needed. Use this for images already available on disk." .to_string(), strict: false, defer_loading: None, diff --git a/codex-rs/core/tests/suite/code_mode.rs b/codex-rs/core/tests/suite/code_mode.rs index fb9e7fb420..90daf21963 100644 --- a/codex-rs/core/tests/suite/code_mode.rs +++ b/codex-rs/core/tests/suite/code_mode.rs @@ -2861,7 +2861,7 @@ text(JSON.stringify(tool)); parsed, serde_json::json!({ "name": "view_image", - "description": "View a local image from the filesystem (only use if given a full filepath by the user, and the image isn't already attached to the thread context within tags).\n\nexec tool declaration:\n```ts\ndeclare const tools: { view_image(args: {\n // Local filesystem path to an image file\n path: string;\n}): Promise<{\n // Image detail hint returned by view_image. Returns `high` for default resized behavior or `original` when original resolution is preserved.\n detail: \"high\" | \"original\";\n // Data URL for the loaded image.\n image_url: string;\n}>; };\n```", + "description": "View a local image file from the filesystem when visual inspection is needed. Use this for images already available on disk.\n\nexec tool declaration:\n```ts\ndeclare const tools: { view_image(args: {\n // Local filesystem path to an image file\n path: string;\n}): Promise<{\n // Image detail hint returned by view_image. Returns `high` for default resized behavior or `original` when original resolution is preserved.\n detail: \"high\" | \"original\";\n // Data URL for the loaded image.\n image_url: string;\n}>; };\n```", }) ); From 22e45014a2820a8c5a7037d15dab903ccd3eb994 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Tue, 26 May 2026 14:34:02 -0700 Subject: [PATCH 02/87] TUI config cleanup: plugin mentions (#24266) ## Summary TUI plugin mention refresh still joined app-server plugin inventory with client-local plugin config, which can diverge once plugin state is owned by the app server. This changes the TUI to mirror the GUI client: `plugin/list` is the autocomplete source, and mention candidates are plugin-level entries filtered to installed, enabled, and not disabled by admin. The TUI no longer reads local plugin config or calls `plugin/read` while refreshing plugin mention candidates. ## API shape and limitations The current app-server API does not expose effective per-session plugin capability summaries for mention autocomplete. As in the GUI, autocomplete now trusts `plugin/list` metadata rather than proving which plugin capabilities are loaded in the active session. That avoids stale client-local reads and the cwd/remote detail gaps in `plugin/read`, but intentionally accepts the same list-level tradeoff as the app: if `plugin/list` reports a remote plugin before its local bundle is materialized, the plugin can still appear as a mention candidate. --- codex-rs/tui/src/app/background_requests.rs | 2 +- codex-rs/tui/src/app/plugin_mentions.rs | 192 ++++++++++---------- 2 files changed, 97 insertions(+), 97 deletions(-) diff --git a/codex-rs/tui/src/app/background_requests.rs b/codex-rs/tui/src/app/background_requests.rs index f81f8b071e..4b8b5f8920 100644 --- a/codex-rs/tui/src/app/background_requests.rs +++ b/codex-rs/tui/src/app/background_requests.rs @@ -409,7 +409,7 @@ impl App { } tokio::spawn(async move { - match fetch_plugin_mentions(request_handle, config).await { + match fetch_plugin_mentions(request_handle, config.cwd.to_path_buf()).await { Ok(plugins) => { app_event_tx.send(AppEvent::PluginMentionsLoaded { plugins: Some(plugins), diff --git a/codex-rs/tui/src/app/plugin_mentions.rs b/codex-rs/tui/src/app/plugin_mentions.rs index ef300896bc..5d94f51c08 100644 --- a/codex-rs/tui/src/app/plugin_mentions.rs +++ b/codex-rs/tui/src/app/plugin_mentions.rs @@ -1,125 +1,62 @@ //! Plugin mention capability enrichment for the TUI. //! -//! Mention inventory comes from app-server `plugin/list`, while mention eligibility still reuses -//! the older local bulk capability summaries. That keeps the feature app-server-shaped without -//! paying for a `plugin/read` per plugin. +//! Mention inventory comes from app-server `plugin/list`, matching the GUI +//! client. The current API exposes plugin-level mention metadata there, but not +//! effective per-session capability summaries. use super::background_requests::request_plugin_list; use super::*; +use codex_app_server_protocol::PluginAvailability; use codex_app_server_protocol::PluginListResponse; -use codex_app_server_protocol::PluginMarketplaceEntry; use codex_app_server_protocol::PluginSummary; -use codex_core_plugins::PluginsManager; use codex_plugin::PluginCapabilitySummary; -use std::collections::HashMap; - -#[derive(Debug, Clone)] -struct PluginMentionEntry { - config_name: String, - display_name: String, - description: Option, -} - -impl PluginMentionEntry { - fn capability_summary( - self, - capabilities_by_config_name: &HashMap, - ) -> Option { - let capabilities = capabilities_by_config_name.get(&self.config_name)?; - Some(PluginCapabilitySummary { - config_name: self.config_name, - display_name: self.display_name, - description: self.description, - has_skills: capabilities.has_skills, - mcp_server_names: capabilities.mcp_server_names.clone(), - app_connector_ids: capabilities.app_connector_ids.clone(), - }) - } -} pub(super) async fn fetch_plugin_mentions( request_handle: AppServerRequestHandle, - config: crate::legacy_core::config::Config, + cwd: PathBuf, ) -> Result> { - let response = request_plugin_list(request_handle, config.cwd.to_path_buf()).await?; - let mention_entries = plugin_mention_entries_from_list_response(response); - let capabilities_by_config_name = load_plugin_mention_capabilities(&config).await; - - Ok(mention_entries - .into_iter() - .filter_map(|entry| entry.capability_summary(&capabilities_by_config_name)) - .collect()) + let response = request_plugin_list(request_handle, cwd).await?; + Ok(plugin_mentions_from_list_response(response)) } -async fn load_plugin_mention_capabilities( - config: &crate::legacy_core::config::Config, -) -> HashMap { - let plugins_input = config.plugins_config_input(); - PluginsManager::new(config.codex_home.to_path_buf()) - .plugins_for_config(&plugins_input) - .await - .capability_summaries() - .iter() - .cloned() - .map(|summary| (summary.config_name.clone(), summary)) - .collect() -} - -fn plugin_mention_entries_from_list_response( +fn plugin_mentions_from_list_response( response: PluginListResponse, -) -> Vec { +) -> Vec { response .marketplaces .into_iter() - .flat_map(plugin_mention_entries_from_marketplace) + .flat_map(|marketplace| { + let marketplace_name = marketplace.name; + marketplace + .plugins + .into_iter() + .filter_map(move |plugin| plugin_mention_from_summary(&marketplace_name, plugin)) + }) .collect() } -fn plugin_mention_entries_from_marketplace( - marketplace: PluginMarketplaceEntry, -) -> Vec { - let marketplace_name = marketplace.name; - marketplace - .plugins - .into_iter() - .filter_map(|plugin| plugin_mention_entry(&marketplace_name, plugin)) - .collect() +fn plugin_is_eligible_for_mentions(plugin: &PluginSummary) -> bool { + plugin.installed && plugin.enabled && plugin.availability != PluginAvailability::DisabledByAdmin } -fn plugin_mention_entry( +fn plugin_mention_from_summary( marketplace_name: &str, plugin: PluginSummary, -) -> Option { +) -> Option { if !plugin_is_eligible_for_mentions(&plugin) { return None; } - let config_name = plugin_mention_config_name(marketplace_name, &plugin)?; - Some(PluginMentionEntry { - config_name, + Some(PluginCapabilitySummary { + config_name: plugin.id.clone(), display_name: plugin_mention_display_name(&plugin), - description: plugin_mention_description(&plugin), + description: plugin_mention_description(marketplace_name, &plugin), + has_skills: false, + mcp_server_names: Vec::new(), + app_connector_ids: Vec::new(), }) } -fn plugin_is_eligible_for_mentions(plugin: &PluginSummary) -> bool { - plugin.installed && plugin.enabled -} - -fn plugin_mention_config_name(marketplace_name: &str, plugin: &PluginSummary) -> Option { - codex_plugin::PluginId::new(plugin.name.clone(), marketplace_name.to_string()) - .map(|plugin_id| plugin_id.as_key()) - .map_err(|err| { - tracing::warn!( - plugin_name = plugin.name, - marketplace_name, - error = %err, - "skipping plugin mention with invalid identity" - ); - }) - .ok() -} - fn plugin_mention_display_name(plugin: &PluginSummary) -> String { plugin .interface @@ -131,17 +68,80 @@ fn plugin_mention_display_name(plugin: &PluginSummary) -> String { .unwrap_or_else(|| plugin.name.clone()) } -fn plugin_mention_description(plugin: &PluginSummary) -> Option { +fn plugin_mention_description(marketplace_name: &str, plugin: &PluginSummary) -> Option { plugin .interface .as_ref() - .and_then(|interface| { - interface - .short_description - .as_deref() - .or(interface.long_description.as_deref()) - }) + .and_then(|interface| interface.short_description.as_deref()) .map(str::trim) .filter(|description| !description.is_empty()) .map(str::to_string) + .or_else(|| { + let marketplace_name = marketplace_name.trim(); + (!marketplace_name.is_empty()).then(|| marketplace_name.to_string()) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use codex_app_server_protocol::PluginAuthPolicy; + use codex_app_server_protocol::PluginAvailability; + use codex_app_server_protocol::PluginInstallPolicy; + use codex_app_server_protocol::PluginListResponse; + use codex_app_server_protocol::PluginMarketplaceEntry; + use codex_app_server_protocol::PluginSource; + use pretty_assertions::assert_eq; + + #[test] + fn plugin_mentions_use_plugin_list_summaries_and_gui_eligibility() { + let active = plugin_summary("active"); + let mut disabled_by_admin = plugin_summary("disabled-by-admin"); + disabled_by_admin.availability = PluginAvailability::DisabledByAdmin; + let mut disabled = plugin_summary("disabled"); + disabled.enabled = false; + let mut uninstalled = plugin_summary("uninstalled"); + uninstalled.installed = false; + + let response = PluginListResponse { + marketplaces: vec![PluginMarketplaceEntry { + name: "server-marketplace".to_string(), + path: None, + interface: None, + plugins: vec![active, disabled_by_admin, disabled, uninstalled], + }], + marketplace_load_errors: Vec::new(), + featured_plugin_ids: Vec::new(), + }; + + assert_eq!( + plugin_mentions_from_list_response(response), + vec![PluginCapabilitySummary { + config_name: "active@server-marketplace".to_string(), + display_name: "active".to_string(), + description: Some("server-marketplace".to_string()), + has_skills: false, + mcp_server_names: Vec::new(), + app_connector_ids: Vec::new(), + }] + ); + } + + fn plugin_summary(name: &str) -> PluginSummary { + PluginSummary { + id: format!("{name}@server-marketplace"), + remote_plugin_id: Some(format!("plugins~{name}")), + local_version: None, + name: name.to_string(), + share_context: None, + source: PluginSource::Remote, + installed: true, + enabled: true, + install_policy: PluginInstallPolicy::Available, + auth_policy: PluginAuthPolicy::OnInstall, + availability: PluginAvailability::Available, + interface: None, + keywords: Vec::new(), + } + } } From 414561294c3955ed6e2020873758cb3262287fb9 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Tue, 26 May 2026 14:40:06 -0700 Subject: [PATCH 03/87] Avoid repeated marketplace upgrades for alternate layouts (#24320) Fixes #24249. ## Why Codex already supports discovering marketplaces under both `.agents/plugins/marketplace.json` and `.claude-plugin/marketplace.json`. The Git marketplace auto-upgrade no-op check only looked for the `.agents` layout. That meant an installed `.claude-plugin` marketplace with matching revision metadata still looked absent, so plugin list/startup upgrade work could stage and re-activate the same marketplace again. That matches the failure shape in #24249: the report called out repeated marketplace sync/cache refresh logs and a large recently-touched `.tmp/marketplaces/.staging` directory. This change makes the auto-upgrade path recognize the installed `.claude-plugin` marketplace as already current, which should remove that staging/activation feedback loop. ## What changed `codex-rs/core-plugins/src/marketplace_upgrade.rs` now uses the existing supported marketplace manifest discovery helper when deciding whether an installed Git marketplace is already current. Existing local plugin source validation is unchanged; `source: "./"` still remains invalid. ## Confidence Confidence is high that this fixes the repeated marketplace upgrade path: the old hardcoded layout check was definitely wrong for installed `.claude-plugin` marketplaces, and the reported staging churn points directly at that path. Confidence is not 100% because we do not have a CPU profile or a fully re-run reporter repro. A malformed marketplace entry can still be logged as invalid if another caller repeatedly lists plugins; this PR fixes the staging/upgrade feedback loop that likely made the failure pathological, not every possible source of repeated marketplace resolution. --- codex-rs/core-plugins/src/marketplace_upgrade.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/codex-rs/core-plugins/src/marketplace_upgrade.rs b/codex-rs/core-plugins/src/marketplace_upgrade.rs index 9dda8a9a3e..010694c8b3 100644 --- a/codex-rs/core-plugins/src/marketplace_upgrade.rs +++ b/codex-rs/core-plugins/src/marketplace_upgrade.rs @@ -6,6 +6,7 @@ use self::activation::installed_marketplace_metadata_matches; use self::activation::write_installed_marketplace_metadata; use self::git::clone_git_source; use self::git::git_remote_revision; +use crate::marketplace::find_marketplace_manifest_path; use crate::marketplace::validate_marketplace_root; use codex_config::CONFIG_TOML_FILE; use codex_config::ConfigLayerStack; @@ -176,9 +177,7 @@ fn upgrade_configured_git_marketplace( MARKETPLACE_UPGRADE_GIT_TIMEOUT, )?; let destination = install_root.join(&marketplace.name); - if destination - .join(".agents/plugins/marketplace.json") - .is_file() + if find_marketplace_manifest_path(&destination).is_some() && marketplace.last_revision.as_deref() == Some(remote_revision.as_str()) && installed_marketplace_metadata_matches(&destination, marketplace, &remote_revision) { From 0a6bc4e68759f0086434fd5b4d87c7f5a597d6d1 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 26 May 2026 14:56:27 -0700 Subject: [PATCH 04/87] windows-sandbox: remove SandboxPolicy runner plumbing (#23813) ## Why The Windows sandbox runner still carried the old `SandboxPolicy` compatibility path even though core now computes `PermissionProfile`. That meant Windows command-runner execution could only see the legacy projection, so profile-only filesystem rules such as deny globs were not part of the runner input. ## What Changed - Removed the Windows-local `SandboxPolicy` parser/export and deleted `windows-sandbox-rs/src/policy.rs`. - Changed restricted-token capture/session setup, elevated setup, world-writable audit, read-root grant, and command-runner session APIs to accept `PermissionProfile` plus the profile cwd. - Bumped the elevated command-runner IPC protocol to version 2 because `SpawnRequest` now carries `permission_profile` / `permission_profile_cwd` instead of the legacy `policy_json_or_preset` / `sandbox_policy_cwd` fields. - Updated core exec, unified exec, debug-sandbox, TUI setup/grant flows, and app-server setup to pass the actual effective `PermissionProfile`. - Left regression coverage asserting the old IPC policy fields are absent and the runner serializes tagged `PermissionProfile` JSON. ## Verification - `cargo test -p codex-windows-sandbox` - `cargo test -p codex-core windows_sandbox` - `cargo test -p codex-app-server request_processors::windows_sandbox_processor` - `just fix -p codex-windows-sandbox -p codex-core -p codex-app-server -p codex-cli -p codex-tui` - `just fix -p codex-cli -p codex-tui` - `just fix -p codex-windows-sandbox -p codex-tui` - `rg "\\bSandboxPolicy\\b" codex-rs/windows-sandbox-rs` returned no matches. Note: `cargo test -p codex-cli` was attempted but did not reach crate tests because local disk filled while compiling dependencies (`No space left on device`). The targeted clippy pass compiled the affected CLI/TUI surfaces afterward. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/23813). * #24108 * __->__ #23813 --- .../windows_sandbox_processor.rs | 6 +- codex-rs/cli/src/debug_sandbox.rs | 36 ++-- codex-rs/core/src/exec.rs | 12 +- .../core/src/unified_exec/process_manager.rs | 8 +- codex-rs/core/src/windows_sandbox.rs | 57 ++--- .../core/src/windows_sandbox_read_grants.rs | 10 +- .../src/windows_sandbox_read_grants_tests.rs | 12 +- codex-rs/tui/src/app/event_dispatch.rs | 57 ++--- codex-rs/tui/src/app/platform_actions.rs | 23 +- .../src/chatwidget/windows_sandbox_prompts.rs | 14 +- codex-rs/windows-sandbox-rs/src/allow.rs | 188 ++++++++++------- codex-rs/windows-sandbox-rs/src/audit.rs | 19 -- .../windows-sandbox-rs/src/elevated_impl.rs | 104 --------- codex-rs/windows-sandbox-rs/src/lib.rs | 158 +++++++------- codex-rs/windows-sandbox-rs/src/policy.rs | 61 ------ .../src/resolved_permissions.rs | 37 ---- codex-rs/windows-sandbox-rs/src/setup.rs | 197 +++++++++++------- codex-rs/windows-sandbox-rs/src/spawn_prep.rs | 194 ++++++++--------- .../src/unified_exec/backends/elevated.rs | 42 ---- .../src/unified_exec/backends/legacy.rs | 22 +- .../src/unified_exec/mod.rs | 46 +--- .../src/unified_exec/tests.rs | 22 +- 22 files changed, 537 insertions(+), 788 deletions(-) delete mode 100644 codex-rs/windows-sandbox-rs/src/policy.rs diff --git a/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs b/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs index 0b639b7d5c..c507bf8ddf 100644 --- a/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs +++ b/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs @@ -76,10 +76,8 @@ impl WindowsSandboxRequestProcessor { Ok(config) => { let setup_request = WindowsSandboxSetupRequest { mode, - policy: config - .permissions - .legacy_sandbox_policy(config.cwd.as_path()), - policy_cwd: config.cwd.to_path_buf(), + permission_profile: config.permissions.effective_permission_profile(), + permission_profile_cwd: config.cwd.to_path_buf(), command_cwd, env_map: std::env::vars().collect(), codex_home: config.codex_home.to_path_buf(), diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index 918d81aad2..36c5e683b2 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -209,9 +209,9 @@ async fn run_command_under_sandbox( // does not support `--cwd`, but let's use the config value for consistency. let cwd = config.cwd.clone(); // For now, we always use the same cwd for both the command and the - // sandbox policy. In the future, we could add a CLI option to set them + // permission profile. In the future, we could add a CLI option to set them // separately. - let sandbox_policy_cwd = cwd.clone(); + let permission_profile_cwd = cwd.clone(); let env = create_env( &config.permissions.shell_environment_policy, @@ -222,7 +222,8 @@ async fn run_command_under_sandbox( if let SandboxType::Windows = sandbox_type { #[cfg(target_os = "windows")] { - run_command_under_windows_session(&config, command, cwd, sandbox_policy_cwd, env).await; + run_command_under_windows_session(&config, command, cwd, permission_profile_cwd, env) + .await; } #[cfg(not(target_os = "windows"))] { @@ -265,7 +266,7 @@ async fn run_command_under_sandbox( command, file_system_sandbox_policy: &file_system_sandbox_policy, network_sandbox_policy, - sandbox_policy_cwd: sandbox_policy_cwd.as_path(), + sandbox_policy_cwd: permission_profile_cwd.as_path(), enforce_managed_network: false, network: network.as_ref(), extra_allow_unix_sockets: allow_unix_sockets, @@ -297,7 +298,7 @@ async fn run_command_under_sandbox( command, cwd.as_path(), &config.permissions.effective_permission_profile(), - sandbox_policy_cwd.as_path(), + permission_profile_cwd.as_path(), use_legacy_landlock, allow_network_for_proxy(managed_network_requirements_enabled), ); @@ -349,24 +350,15 @@ async fn run_command_under_windows_session( config: &Config, command: Vec, cwd: AbsolutePathBuf, - sandbox_policy_cwd: AbsolutePathBuf, + permission_profile_cwd: AbsolutePathBuf, env: std::collections::HashMap, ) -> ! { use codex_core::windows_sandbox::WindowsSandboxLevelExt; use codex_protocol::config_types::WindowsSandboxLevel; - use codex_windows_sandbox::spawn_windows_sandbox_session_elevated; + use codex_windows_sandbox::spawn_windows_sandbox_session_elevated_for_permission_profile; use codex_windows_sandbox::spawn_windows_sandbox_session_legacy; - let sandbox_policy = config - .permissions - .legacy_sandbox_policy(sandbox_policy_cwd.as_path()); - let policy_str = match serde_json::to_string(&sandbox_policy) { - Ok(policy_str) => policy_str, - Err(err) => { - eprintln!("windows sandbox failed to serialize policy: {err}"); - std::process::exit(1); - } - }; + let permission_profile = config.permissions.effective_permission_profile(); let use_elevated = matches!( WindowsSandboxLevel::from_config(config), @@ -374,9 +366,9 @@ async fn run_command_under_windows_session( ); let spawned = if use_elevated { - spawn_windows_sandbox_session_elevated( - policy_str.as_str(), - sandbox_policy_cwd.as_path(), + spawn_windows_sandbox_session_elevated_for_permission_profile( + &permission_profile, + permission_profile_cwd.as_path(), config.codex_home.as_path(), command, cwd.as_path(), @@ -394,8 +386,8 @@ async fn run_command_under_windows_session( .await } else { spawn_windows_sandbox_session_legacy( - policy_str.as_str(), - sandbox_policy_cwd.as_path(), + &permission_profile, + permission_profile_cwd.as_path(), config.codex_home.as_path(), command, cwd.as_path(), diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 84ebe337c6..5cec3b49bb 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -423,7 +423,6 @@ pub(crate) async fn execute_exec_request( stdout_stream: Option, after_spawn: Option>, ) -> Result { - let sandbox_policy = exec_request.compatibility_sandbox_policy(); let ExecRequest { command, cwd, @@ -464,7 +463,6 @@ pub(crate) async fn execute_exec_request( stdout_stream, after_spawn, sandbox, - &sandbox_policy, &permission_profile, &windows_sandbox_policy_cwd, windows_sandbox_filesystem_overrides.as_ref(), @@ -481,7 +479,6 @@ async fn get_raw_output_result( stdout_stream: Option, after_spawn: Option>, #[cfg_attr(not(windows), allow(unused_variables))] sandbox: SandboxType, - #[cfg_attr(not(windows), allow(unused_variables))] sandbox_policy: &SandboxPolicy, #[cfg_attr(not(windows), allow(unused_variables))] permission_profile: &PermissionProfile, #[cfg_attr(not(windows), allow(unused_variables))] windows_sandbox_policy_cwd: &AbsolutePathBuf, #[cfg_attr(not(windows), allow(unused_variables))] windows_sandbox_filesystem_overrides: Option< @@ -492,7 +489,6 @@ async fn get_raw_output_result( if sandbox == SandboxType::WindowsRestrictedToken { return exec_windows_sandbox( params, - sandbox_policy, permission_profile, windows_sandbox_policy_cwd, windows_sandbox_filesystem_overrides, @@ -572,7 +568,6 @@ fn record_windows_sandbox_spawn_failure( #[cfg(target_os = "windows")] async fn exec_windows_sandbox( params: ExecParams, - sandbox_policy: &SandboxPolicy, permission_profile: &PermissionProfile, windows_sandbox_policy_cwd: &AbsolutePathBuf, windows_sandbox_filesystem_overrides: Option<&WindowsSandboxFilesystemOverrides>, @@ -604,11 +599,6 @@ async fn exec_windows_sandbox( None }; - let policy_str = serde_json::to_string(sandbox_policy).map_err(|err| { - CodexErr::Io(io::Error::other(format!( - "failed to serialize Windows sandbox policy: {err}" - ))) - })?; let sandbox_cwd = windows_sandbox_policy_cwd.clone(); let permission_profile = permission_profile.clone(); let codex_home = find_codex_home().map_err(|err| { @@ -655,7 +645,7 @@ async fn exec_windows_sandbox( ) } else { run_windows_sandbox_capture_with_filesystem_overrides( - policy_str.as_str(), + &permission_profile, &sandbox_cwd, codex_home.as_ref(), command, diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index fb99500755..08cd92ba53 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -866,12 +866,6 @@ impl UnifiedExecProcessManager { #[cfg(target_os = "windows")] if request.sandbox == codex_sandboxing::SandboxType::WindowsRestrictedToken { - let sandbox_policy = request.compatibility_sandbox_policy(); - let policy_json = serde_json::to_string(&sandbox_policy).map_err(|err| { - UnifiedExecError::create_process(format!( - "failed to serialize Windows sandbox policy: {err}" - )) - })?; let codex_home = crate::config::find_codex_home().map_err(|err| { UnifiedExecError::create_process(format!( "windows sandbox: failed to resolve codex_home: {err}" @@ -923,7 +917,7 @@ impl UnifiedExecProcessManager { codex_protocol::config_types::WindowsSandboxLevel::RestrictedToken | codex_protocol::config_types::WindowsSandboxLevel::Disabled => { codex_windows_sandbox::spawn_windows_sandbox_session_legacy( - policy_json.as_str(), + &request.permission_profile, request.windows_sandbox_policy_cwd.as_path(), codex_home.as_ref(), request.command.clone(), diff --git a/codex-rs/core/src/windows_sandbox.rs b/codex-rs/core/src/windows_sandbox.rs index 1494ce262e..b2ac8bf1f0 100644 --- a/codex-rs/core/src/windows_sandbox.rs +++ b/codex-rs/core/src/windows_sandbox.rs @@ -8,7 +8,7 @@ use codex_features::FeaturesToml; use codex_login::default_client::originator; use codex_otel::sanitize_metric_tag_value; use codex_protocol::config_types::WindowsSandboxLevel; -use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::models::PermissionProfile; use std::collections::BTreeMap; use std::collections::HashMap; use std::path::Path; @@ -145,16 +145,17 @@ pub fn elevated_setup_failure_metric_name(_err: &anyhow::Error) -> &'static str #[cfg(target_os = "windows")] pub fn run_elevated_setup( - policy: &SandboxPolicy, - policy_cwd: &Path, + permission_profile: &PermissionProfile, + permission_profile_cwd: &Path, command_cwd: &Path, env_map: &HashMap, codex_home: &Path, ) -> anyhow::Result<()> { let permissions = - codex_windows_sandbox::ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd( - policy, policy_cwd, - ); + codex_windows_sandbox::ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_cwd( + permission_profile, + permission_profile_cwd, + )?; codex_windows_sandbox::run_elevated_setup( codex_windows_sandbox::SandboxSetupRequest { permissions: &permissions, @@ -169,8 +170,8 @@ pub fn run_elevated_setup( #[cfg(not(target_os = "windows"))] pub fn run_elevated_setup( - _policy: &SandboxPolicy, - _policy_cwd: &Path, + _permission_profile: &PermissionProfile, + _permission_profile_cwd: &Path, _command_cwd: &Path, _env_map: &HashMap, _codex_home: &Path, @@ -180,15 +181,15 @@ pub fn run_elevated_setup( #[cfg(target_os = "windows")] pub fn run_legacy_setup_preflight( - policy: &SandboxPolicy, - policy_cwd: &Path, + permission_profile: &PermissionProfile, + permission_profile_cwd: &Path, command_cwd: &Path, env_map: &HashMap, codex_home: &Path, ) -> anyhow::Result<()> { codex_windows_sandbox::run_windows_sandbox_legacy_preflight( - policy, - policy_cwd, + permission_profile, + permission_profile_cwd, codex_home, command_cwd, env_map, @@ -197,16 +198,16 @@ pub fn run_legacy_setup_preflight( #[cfg(target_os = "windows")] pub fn run_setup_refresh_with_extra_read_roots( - policy: &SandboxPolicy, - policy_cwd: &Path, + permission_profile: &PermissionProfile, + permission_profile_cwd: &Path, command_cwd: &Path, env_map: &HashMap, codex_home: &Path, extra_read_roots: Vec, ) -> anyhow::Result<()> { codex_windows_sandbox::run_setup_refresh_with_extra_read_roots( - policy, - policy_cwd, + permission_profile, + permission_profile_cwd, command_cwd, env_map, codex_home, @@ -217,8 +218,8 @@ pub fn run_setup_refresh_with_extra_read_roots( #[cfg(not(target_os = "windows"))] pub fn run_legacy_setup_preflight( - _policy: &SandboxPolicy, - _policy_cwd: &Path, + _permission_profile: &PermissionProfile, + _permission_profile_cwd: &Path, _command_cwd: &Path, _env_map: &HashMap, _codex_home: &Path, @@ -228,8 +229,8 @@ pub fn run_legacy_setup_preflight( #[cfg(not(target_os = "windows"))] pub fn run_setup_refresh_with_extra_read_roots( - _policy: &SandboxPolicy, - _policy_cwd: &Path, + _permission_profile: &PermissionProfile, + _permission_profile_cwd: &Path, _command_cwd: &Path, _env_map: &HashMap, _codex_home: &Path, @@ -247,8 +248,8 @@ pub enum WindowsSandboxSetupMode { #[derive(Debug, Clone)] pub struct WindowsSandboxSetupRequest { pub mode: WindowsSandboxSetupMode, - pub policy: SandboxPolicy, - pub policy_cwd: PathBuf, + pub permission_profile: PermissionProfile, + pub permission_profile_cwd: PathBuf, pub command_cwd: PathBuf, pub env_map: HashMap, pub codex_home: PathBuf, @@ -285,8 +286,8 @@ async fn run_windows_sandbox_setup_and_persist( request: WindowsSandboxSetupRequest, ) -> anyhow::Result<()> { let mode = request.mode; - let policy = request.policy; - let policy_cwd = request.policy_cwd; + let permission_profile = request.permission_profile; + let permission_profile_cwd = request.permission_profile_cwd; let command_cwd = request.command_cwd; let env_map = request.env_map; let codex_home = request.codex_home; @@ -297,8 +298,8 @@ async fn run_windows_sandbox_setup_and_persist( WindowsSandboxSetupMode::Elevated => { if !sandbox_setup_is_complete(setup_codex_home.as_path()) { run_elevated_setup( - &policy, - policy_cwd.as_path(), + &permission_profile, + permission_profile_cwd.as_path(), command_cwd.as_path(), &env_map, setup_codex_home.as_path(), @@ -307,8 +308,8 @@ async fn run_windows_sandbox_setup_and_persist( } WindowsSandboxSetupMode::Unelevated => { run_legacy_setup_preflight( - &policy, - policy_cwd.as_path(), + &permission_profile, + permission_profile_cwd.as_path(), command_cwd.as_path(), &env_map, setup_codex_home.as_path(), diff --git a/codex-rs/core/src/windows_sandbox_read_grants.rs b/codex-rs/core/src/windows_sandbox_read_grants.rs index b0891ec6a6..a9561372ce 100644 --- a/codex-rs/core/src/windows_sandbox_read_grants.rs +++ b/codex-rs/core/src/windows_sandbox_read_grants.rs @@ -1,13 +1,13 @@ use crate::windows_sandbox::run_setup_refresh_with_extra_read_roots; use anyhow::Result; -use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::models::PermissionProfile; use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; pub fn grant_read_root_non_elevated( - policy: &SandboxPolicy, - policy_cwd: &Path, + permission_profile: &PermissionProfile, + permission_profile_cwd: &Path, command_cwd: &Path, env_map: &HashMap, codex_home: &Path, @@ -25,8 +25,8 @@ pub fn grant_read_root_non_elevated( let canonical_root = dunce::canonicalize(read_root)?; run_setup_refresh_with_extra_read_roots( - policy, - policy_cwd, + permission_profile, + permission_profile_cwd, command_cwd, env_map, codex_home, diff --git a/codex-rs/core/src/windows_sandbox_read_grants_tests.rs b/codex-rs/core/src/windows_sandbox_read_grants_tests.rs index bffa840ede..93cbcddfe3 100644 --- a/codex-rs/core/src/windows_sandbox_read_grants_tests.rs +++ b/codex-rs/core/src/windows_sandbox_read_grants_tests.rs @@ -1,18 +1,18 @@ use super::grant_read_root_non_elevated; -use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::models::PermissionProfile; use std::collections::HashMap; use std::path::Path; use tempfile::TempDir; -fn policy() -> SandboxPolicy { - SandboxPolicy::new_workspace_write_policy() +fn permission_profile() -> PermissionProfile { + PermissionProfile::workspace_write() } #[test] fn rejects_relative_path() { let tmp = TempDir::new().expect("tempdir"); let err = grant_read_root_non_elevated( - &policy(), + &permission_profile(), tmp.path(), tmp.path(), &HashMap::new(), @@ -28,7 +28,7 @@ fn rejects_missing_path() { let tmp = TempDir::new().expect("tempdir"); let missing = tmp.path().join("does-not-exist"); let err = grant_read_root_non_elevated( - &policy(), + &permission_profile(), tmp.path(), tmp.path(), &HashMap::new(), @@ -45,7 +45,7 @@ fn rejects_file_path() { let file_path = tmp.path().join("file.txt"); std::fs::write(&file_path, "hello").expect("write file"); let err = grant_read_root_non_elevated( - &policy(), + &permission_profile(), tmp.path(), tmp.path(), &HashMap::new(), diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index f92806b162..18ded9bbbc 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -907,8 +907,8 @@ impl App { return Ok(AppRunControl::Continue); } }; - let policy_cwd = self.config.cwd.clone(); - let command_cwd = policy_cwd.clone(); + let permission_profile_cwd = self.config.cwd.clone(); + let command_cwd = permission_profile_cwd.clone(); let env_map: std::collections::HashMap = std::env::vars().collect(); let codex_home = self.config.codex_home.clone(); @@ -930,25 +930,10 @@ impl App { self.chat_widget.show_windows_sandbox_setup_status(); self.windows_sandbox.setup_started_at = Some(Instant::now()); let session_telemetry = self.session_telemetry.clone(); - let Ok(policy) = permission_profile - .to_legacy_sandbox_policy(policy_cwd.as_path()) - .inspect_err(|err| { - tracing::error!( - %err, - "approval preset permissions cannot be projected for elevated Windows sandbox setup" - ); - }) - else { - tx.send(AppEvent::OpenWindowsSandboxFallbackPrompt { - preset, - profile_selection, - }); - return Ok(AppRunControl::Continue); - }; tokio::task::spawn_blocking(move || { let result = crate::legacy_core::windows_sandbox::run_elevated_setup( - &policy, - policy_cwd.as_path(), + &permission_profile, + permission_profile_cwd.as_path(), command_cwd.as_path(), &env_map, codex_home.as_path(), @@ -1031,8 +1016,8 @@ impl App { return Ok(AppRunControl::Continue); } }; - let policy_cwd = self.config.cwd.clone(); - let command_cwd = policy_cwd.clone(); + let permission_profile_cwd = self.config.cwd.clone(); + let command_cwd = permission_profile_cwd.clone(); let env_map: std::collections::HashMap = std::env::vars().collect(); let codex_home = self.config.codex_home.clone(); @@ -1040,26 +1025,11 @@ impl App { let session_telemetry = self.session_telemetry.clone(); self.chat_widget.show_windows_sandbox_setup_status(); - let Ok(policy) = permission_profile - .to_legacy_sandbox_policy(policy_cwd.as_path()) - .inspect_err(|err| { - tracing::error!( - %err, - "approval preset permissions cannot be projected for legacy Windows sandbox setup" - ); - }) - else { - tx.send(AppEvent::OpenWindowsSandboxFallbackPrompt { - preset, - profile_selection, - }); - return Ok(AppRunControl::Continue); - }; tokio::task::spawn_blocking(move || { if let Err(err) = crate::legacy_core::windows_sandbox::run_legacy_setup_preflight( - &policy, - policy_cwd.as_path(), + &permission_profile, + permission_profile_cwd.as_path(), command_cwd.as_path(), &env_map, codex_home.as_path(), @@ -1096,11 +1066,8 @@ impl App { /*hint*/ None, )); - let policy = self - .config - .permissions - .legacy_sandbox_policy(self.config.cwd.as_path()); - let policy_cwd = self.config.cwd.clone(); + let permission_profile = self.config.permissions.effective_permission_profile(); + let permission_profile_cwd = self.config.cwd.clone(); let command_cwd = self.config.cwd.clone(); let env_map: std::collections::HashMap = std::env::vars().collect(); @@ -1110,8 +1077,8 @@ impl App { tokio::task::spawn_blocking(move || { let requested_path = PathBuf::from(path); let event = match crate::legacy_core::grant_read_root_non_elevated( - &policy, - policy_cwd.as_path(), + &permission_profile, + permission_profile_cwd.as_path(), command_cwd.as_path(), &env_map, codex_home.as_path(), diff --git a/codex-rs/tui/src/app/platform_actions.rs b/codex-rs/tui/src/app/platform_actions.rs index c87776f48b..d11e410884 100644 --- a/codex-rs/tui/src/app/platform_actions.rs +++ b/codex-rs/tui/src/app/platform_actions.rs @@ -21,20 +21,25 @@ impl App { permission_profile: PermissionProfile, tx: AppEventSender, ) { - let Ok(sandbox_policy) = permission_profile.to_legacy_sandbox_policy(cwd.as_path()) else { - send_world_writable_scan_failed(&tx); + let Ok(permissions) = + codex_windows_sandbox::ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_cwd( + &permission_profile, + cwd.as_path(), + ) + else { return; }; tokio::task::spawn_blocking(move || { let logs_base_dir_path = logs_base_dir.as_path(); - let result = codex_windows_sandbox::apply_world_writable_scan_and_denies( - logs_base_dir_path, - cwd.as_path(), - &env_map, - &sandbox_policy, - Some(logs_base_dir_path), - ); + let result = + codex_windows_sandbox::apply_world_writable_scan_and_denies_for_permissions( + logs_base_dir_path, + cwd.as_path(), + &env_map, + &permissions, + Some(logs_base_dir_path), + ); if result.is_err() { // Scan failed: warn without examples. send_world_writable_scan_failed(&tx); diff --git a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs index dd90af0557..3b8dfce200 100644 --- a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs +++ b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs @@ -15,12 +15,20 @@ impl ChatWidget { } let cwd = self.config.cwd.clone(); let env_map: std::collections::HashMap = std::env::vars().collect(); - let policy = self.config.legacy_sandbox_policy(); - match codex_windows_sandbox::apply_world_writable_scan_and_denies( + let permission_profile = self.config.permissions.effective_permission_profile(); + let Ok(permissions) = + codex_windows_sandbox::ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_cwd( + &permission_profile, + cwd.as_path(), + ) + else { + return None; + }; + match codex_windows_sandbox::apply_world_writable_scan_and_denies_for_permissions( self.config.codex_home.as_path(), cwd.as_path(), &env_map, - &policy, + &permissions, Some(self.config.codex_home.as_path()), ) { Ok(_) => None, diff --git a/codex-rs/windows-sandbox-rs/src/allow.rs b/codex-rs/windows-sandbox-rs/src/allow.rs index 541a86fc1c..59ed2468c6 100644 --- a/codex-rs/windows-sandbox-rs/src/allow.rs +++ b/codex-rs/windows-sandbox-rs/src/allow.rs @@ -44,19 +44,36 @@ pub(crate) fn compute_allow_paths_for_permissions( #[cfg(test)] mod tests { use super::*; - use codex_protocol::protocol::SandboxPolicy; + use codex_protocol::models::PermissionProfile; + use codex_protocol::permissions::NetworkSandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use std::fs; use tempfile::TempDir; + fn workspace_write_profile( + writable_roots: &[AbsolutePathBuf], + exclude_tmpdir_env_var: bool, + exclude_slash_tmp: bool, + ) -> PermissionProfile { + PermissionProfile::workspace_write_with( + writable_roots, + NetworkSandboxPolicy::Restricted, + exclude_tmpdir_env_var, + exclude_slash_tmp, + ) + } + fn compute_allow_paths( - policy: &SandboxPolicy, - policy_cwd: &Path, + permission_profile: &PermissionProfile, + permission_profile_cwd: &Path, command_cwd: &Path, env_map: &HashMap, ) -> AllowDenyPaths { - let permissions = - ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, policy_cwd); + let permissions = ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_cwd( + permission_profile, + permission_profile_cwd, + ) + .expect("managed permission profile"); compute_allow_paths_for_permissions(&permissions, command_cwd, env_map) } @@ -68,14 +85,19 @@ mod tests { let _ = fs::create_dir_all(&command_cwd); let _ = fs::create_dir_all(&extra_root); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![AbsolutePathBuf::try_from(extra_root.as_path()).unwrap()], - network_access: false, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - }; + let writable_roots = vec![AbsolutePathBuf::try_from(extra_root.as_path()).unwrap()]; + let permission_profile = workspace_write_profile( + &writable_roots, + /*exclude_tmpdir_env_var*/ false, + /*exclude_slash_tmp*/ false, + ); - let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &HashMap::new()); + let paths = compute_allow_paths( + &permission_profile, + &command_cwd, + &command_cwd, + &HashMap::new(), + ); assert!( paths @@ -91,25 +113,29 @@ mod tests { } #[test] - fn uses_policy_cwd_for_legacy_workspace_root() { + fn uses_profile_cwd_for_workspace_root() { let tmp = TempDir::new().expect("tempdir"); - let policy_cwd = tmp.path().join("workspace"); - let command_cwd = policy_cwd.join("subdir"); + let permission_profile_cwd = tmp.path().join("workspace"); + let command_cwd = permission_profile_cwd.join("subdir"); fs::create_dir_all(&command_cwd).expect("create command cwd"); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; + let permission_profile = workspace_write_profile( + &[], + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ true, + ); - let paths = compute_allow_paths(&policy, &policy_cwd, &command_cwd, &HashMap::new()); + let paths = compute_allow_paths( + &permission_profile, + &permission_profile_cwd, + &command_cwd, + &HashMap::new(), + ); assert!( paths .allow - .contains(&dunce::canonicalize(&policy_cwd).unwrap()) + .contains(&dunce::canonicalize(&permission_profile_cwd).unwrap()) ); assert!( !paths @@ -127,17 +153,16 @@ mod tests { let _ = fs::create_dir_all(&command_cwd); let _ = fs::create_dir_all(&temp_dir); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: false, - }; + let permission_profile = workspace_write_profile( + &[], + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ false, + ); let mut env_map = HashMap::new(); env_map.insert("TEMP".into(), temp_dir.to_string_lossy().to_string()); env_map.insert("TMP".into(), temp_dir.to_string_lossy().to_string()); - let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &env_map); + let paths = compute_allow_paths(&permission_profile, &command_cwd, &command_cwd, &env_map); assert!( paths @@ -160,17 +185,16 @@ mod tests { let _ = fs::create_dir_all(&command_cwd); let _ = fs::create_dir_all(&temp_dir); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: false, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - }; + let permission_profile = workspace_write_profile( + &[], + /*exclude_tmpdir_env_var*/ false, + /*exclude_slash_tmp*/ false, + ); let mut env_map = HashMap::new(); env_map.insert("TEMP".into(), temp_dir.to_string_lossy().to_string()); env_map.insert("TMP".into(), temp_dir.to_string_lossy().to_string()); - let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &env_map); + let paths = compute_allow_paths(&permission_profile, &command_cwd, &command_cwd, &env_map); let expected_allow: HashSet = [ dunce::canonicalize(&command_cwd).unwrap(), @@ -189,14 +213,18 @@ mod tests { let command_cwd = tmp.path().join("workspace"); let _ = fs::create_dir_all(&command_cwd); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: false, - }; + let permission_profile = workspace_write_profile( + &[], + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ false, + ); - let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &HashMap::new()); + let paths = compute_allow_paths( + &permission_profile, + &command_cwd, + &command_cwd, + &HashMap::new(), + ); let expected_allow: HashSet = [dunce::canonicalize(&command_cwd).unwrap()] .into_iter() .collect(); @@ -212,14 +240,18 @@ mod tests { let git_dir = command_cwd.join(".git"); let _ = fs::create_dir_all(&git_dir); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: false, - }; + let permission_profile = workspace_write_profile( + &[], + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ false, + ); - let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &HashMap::new()); + let paths = compute_allow_paths( + &permission_profile, + &command_cwd, + &command_cwd, + &HashMap::new(), + ); let expected_allow: HashSet = [dunce::canonicalize(&command_cwd).unwrap()] .into_iter() .collect(); @@ -239,14 +271,18 @@ mod tests { let _ = fs::create_dir_all(&command_cwd); let _ = fs::write(&git_file, "gitdir: .git/worktrees/example"); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: false, - }; + let permission_profile = workspace_write_profile( + &[], + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ false, + ); - let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &HashMap::new()); + let paths = compute_allow_paths( + &permission_profile, + &command_cwd, + &command_cwd, + &HashMap::new(), + ); let expected_allow: HashSet = [dunce::canonicalize(&command_cwd).unwrap()] .into_iter() .collect(); @@ -267,14 +303,18 @@ mod tests { let _ = fs::create_dir_all(&codex_dir); let _ = fs::create_dir_all(&agents_dir); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: false, - }; + let permission_profile = workspace_write_profile( + &[], + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ false, + ); - let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &HashMap::new()); + let paths = compute_allow_paths( + &permission_profile, + &command_cwd, + &command_cwd, + &HashMap::new(), + ); let expected_allow: HashSet = [dunce::canonicalize(&command_cwd).unwrap()] .into_iter() .collect(); @@ -295,14 +335,18 @@ mod tests { let command_cwd = tmp.path().join("workspace"); let _ = fs::create_dir_all(&command_cwd); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: false, - }; + let permission_profile = workspace_write_profile( + &[], + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ false, + ); - let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &HashMap::new()); + let paths = compute_allow_paths( + &permission_profile, + &command_cwd, + &command_cwd, + &HashMap::new(), + ); assert_eq!(paths.allow.len(), 1); assert!( paths.deny.is_empty(), diff --git a/codex-rs/windows-sandbox-rs/src/audit.rs b/codex-rs/windows-sandbox-rs/src/audit.rs index 0d6f4b5744..58dd15c2c8 100644 --- a/codex-rs/windows-sandbox-rs/src/audit.rs +++ b/codex-rs/windows-sandbox-rs/src/audit.rs @@ -7,7 +7,6 @@ use crate::cap::workspace_write_root_contains_path; use crate::logging::debug_log; use crate::logging::log_note; use crate::path_normalization::canonical_path_key; -use crate::policy::SandboxPolicy; use crate::resolved_permissions::ResolvedWindowsSandboxPermissions; use crate::setup::effective_write_roots_for_permissions; use crate::token::LocalSid; @@ -218,24 +217,6 @@ pub fn audit_everyone_writable( Ok(Vec::new()) } -pub fn apply_world_writable_scan_and_denies( - codex_home: &Path, - cwd: &Path, - env_map: &std::collections::HashMap, - sandbox_policy: &SandboxPolicy, - logs_base_dir: Option<&Path>, -) -> Result<()> { - let permissions = - ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(sandbox_policy, cwd); - apply_world_writable_scan_and_denies_for_permissions( - codex_home, - cwd, - env_map, - &permissions, - logs_base_dir, - ) -} - pub fn apply_world_writable_scan_and_denies_for_permissions( codex_home: &Path, cwd: &Path, diff --git a/codex-rs/windows-sandbox-rs/src/elevated_impl.rs b/codex-rs/windows-sandbox-rs/src/elevated_impl.rs index f8a059918a..6038a2413f 100644 --- a/codex-rs/windows-sandbox-rs/src/elevated_impl.rs +++ b/codex-rs/windows-sandbox-rs/src/elevated_impl.rs @@ -4,23 +4,6 @@ use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; -pub struct ElevatedSandboxCaptureRequest<'a> { - pub policy_json_or_preset: &'a str, - pub sandbox_policy_cwd: &'a Path, - pub codex_home: &'a Path, - pub command: Vec, - pub cwd: &'a Path, - pub env_map: HashMap, - pub timeout_ms: Option, - pub use_private_desktop: bool, - pub proxy_enforced: bool, - pub read_roots_override: Option<&'a [PathBuf]>, - pub read_roots_include_platform_defaults: bool, - pub write_roots_override: Option<&'a [PathBuf]>, - pub deny_read_paths_override: &'a [AbsolutePathBuf], - pub deny_write_paths_override: &'a [AbsolutePathBuf], -} - pub struct ElevatedSandboxProfileCaptureRequest<'a> { pub permission_profile: &'a PermissionProfile, pub permission_profile_cwd: &'a Path, @@ -39,7 +22,6 @@ pub struct ElevatedSandboxProfileCaptureRequest<'a> { } mod windows_impl { - use super::ElevatedSandboxCaptureRequest; use super::ElevatedSandboxProfileCaptureRequest; use crate::acl::allow_null_device; use crate::cap::load_or_create_cap_sids; @@ -56,7 +38,6 @@ mod windows_impl { use crate::logging::log_failure; use crate::logging::log_start; use crate::logging::log_success; - use crate::policy::parse_policy; use crate::resolved_permissions::ResolvedWindowsSandboxPermissions; use crate::runner_client::spawn_runner_transport; use crate::sandbox_utils::ensure_codex_home_exists; @@ -64,7 +45,6 @@ mod windows_impl { use crate::setup::effective_write_roots_for_permissions; use crate::token::LocalSid; use anyhow::Result; - use codex_protocol::models::PermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; use std::path::Path; @@ -218,87 +198,13 @@ mod windows_impl { }) })() } - - /// Legacy policy-string adapter for callers that have not moved to permission profiles yet. - #[allow(clippy::too_many_arguments)] - pub fn run_windows_sandbox_capture( - request: ElevatedSandboxCaptureRequest<'_>, - ) -> Result { - let ElevatedSandboxCaptureRequest { - policy_json_or_preset, - sandbox_policy_cwd, - codex_home, - command, - cwd, - env_map, - timeout_ms, - use_private_desktop, - proxy_enforced, - read_roots_override, - read_roots_include_platform_defaults, - write_roots_override, - deny_read_paths_override, - deny_write_paths_override, - } = request; - let policy = parse_policy(policy_json_or_preset)?; - let permission_profile = - PermissionProfile::from_legacy_sandbox_policy_for_cwd(&policy, sandbox_policy_cwd); - run_windows_sandbox_capture_for_permission_profile(ElevatedSandboxProfileCaptureRequest { - permission_profile: &permission_profile, - permission_profile_cwd: sandbox_policy_cwd, - codex_home, - command, - cwd, - env_map, - timeout_ms, - use_private_desktop, - proxy_enforced, - read_roots_override, - read_roots_include_platform_defaults, - write_roots_override, - deny_read_paths_override, - deny_write_paths_override, - }) - } - - #[cfg(test)] - mod tests { - use crate::policy::SandboxPolicy; - - fn workspace_policy(network_access: bool) -> SandboxPolicy { - SandboxPolicy::WorkspaceWrite { - writable_roots: Vec::new(), - network_access, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - } - } - - #[test] - fn applies_network_block_when_access_is_disabled() { - assert!(!workspace_policy(/*network_access*/ false).has_full_network_access()); - } - - #[test] - fn skips_network_block_when_access_is_allowed() { - assert!(workspace_policy(/*network_access*/ true).has_full_network_access()); - } - - #[test] - fn applies_network_block_for_read_only() { - assert!(!SandboxPolicy::new_read_only_policy().has_full_network_access()); - } - } } -#[cfg(target_os = "windows")] -pub use windows_impl::run_windows_sandbox_capture; #[cfg(target_os = "windows")] pub use windows_impl::run_windows_sandbox_capture_for_permission_profile; #[cfg(not(target_os = "windows"))] mod stub { - use super::ElevatedSandboxCaptureRequest; use super::ElevatedSandboxProfileCaptureRequest; use anyhow::Result; use anyhow::bail; @@ -311,14 +217,6 @@ mod stub { pub timed_out: bool, } - /// Stub implementation for non-Windows targets; sandboxing only works on Windows. - #[allow(clippy::too_many_arguments)] - pub fn run_windows_sandbox_capture( - _request: ElevatedSandboxCaptureRequest<'_>, - ) -> Result { - bail!("Windows sandbox is only available on Windows") - } - /// Stub implementation for non-Windows targets; sandboxing only works on Windows. #[allow(clippy::too_many_arguments)] pub fn run_windows_sandbox_capture_for_permission_profile( @@ -328,7 +226,5 @@ mod stub { } } -#[cfg(not(target_os = "windows"))] -pub use stub::run_windows_sandbox_capture; #[cfg(not(target_os = "windows"))] pub use stub::run_windows_sandbox_capture_for_permission_profile; diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index ca9d6c16f3..03d2393ed8 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -34,8 +34,6 @@ mod logging; #[cfg(target_os = "windows")] mod path_normalization; #[cfg(target_os = "windows")] -mod policy; -#[cfg(target_os = "windows")] mod process; #[cfg(target_os = "windows")] mod resolved_permissions; @@ -106,8 +104,6 @@ pub use acl::fetch_dacl_handle; #[cfg(target_os = "windows")] pub use acl::path_mask_allows; #[cfg(target_os = "windows")] -pub use audit::apply_world_writable_scan_and_denies; -#[cfg(target_os = "windows")] pub use audit::apply_world_writable_scan_and_denies_for_permissions; #[cfg(target_os = "windows")] pub use cap::load_or_create_cap_sids; @@ -137,12 +133,8 @@ pub use dpapi::protect as dpapi_protect; #[cfg(target_os = "windows")] pub use dpapi::unprotect as dpapi_unprotect; #[cfg(target_os = "windows")] -pub use elevated_impl::ElevatedSandboxCaptureRequest; -#[cfg(target_os = "windows")] pub use elevated_impl::ElevatedSandboxProfileCaptureRequest; #[cfg(target_os = "windows")] -pub use elevated_impl::run_windows_sandbox_capture as run_windows_sandbox_capture_elevated; -#[cfg(target_os = "windows")] pub use elevated_impl::run_windows_sandbox_capture_for_permission_profile as run_windows_sandbox_capture_for_permission_profile_elevated; #[cfg(target_os = "windows")] pub use helper_materialization::resolve_current_exe_for_launch; @@ -193,10 +185,6 @@ pub use logging::log_writer; #[cfg(target_os = "windows")] pub use path_normalization::canonicalize_path; #[cfg(target_os = "windows")] -pub use policy::SandboxPolicy; -#[cfg(target_os = "windows")] -pub use policy::parse_policy; -#[cfg(target_os = "windows")] pub use process::PipeSpawnHandles; #[cfg(target_os = "windows")] pub use process::StderrMode; @@ -264,8 +252,6 @@ pub use token::create_workspace_write_token_with_caps_from; #[cfg(target_os = "windows")] pub use token::get_current_token_for_restriction; #[cfg(target_os = "windows")] -pub use unified_exec::spawn_windows_sandbox_session_elevated; -#[cfg(target_os = "windows")] pub use unified_exec::spawn_windows_sandbox_session_elevated_for_permission_profile; #[cfg(target_os = "windows")] pub use unified_exec::spawn_windows_sandbox_session_legacy; @@ -293,8 +279,6 @@ pub use workspace_acl::is_command_cwd_root; #[cfg(not(target_os = "windows"))] pub use stub::CaptureResult; #[cfg(not(target_os = "windows"))] -pub use stub::apply_world_writable_scan_and_denies; -#[cfg(not(target_os = "windows"))] pub use stub::run_windows_sandbox_capture; #[cfg(not(target_os = "windows"))] pub use stub::run_windows_sandbox_legacy_preflight; @@ -303,9 +287,7 @@ pub use stub::run_windows_sandbox_legacy_preflight; mod windows_impl { use super::logging::log_failure; use super::logging::log_success; - use super::policy::SandboxPolicy; use super::process::create_process_as_user; - use super::resolved_permissions::ResolvedWindowsSandboxPermissions; use super::sandbox_utils::ensure_codex_home_exists; use super::spawn_prep::LegacyAclSids; use super::spawn_prep::SpawnPrepOptions; @@ -316,6 +298,7 @@ mod windows_impl { use super::spawn_prep::prepare_legacy_spawn_context; use super::spawn_prep::root_capability_sids; use anyhow::Result; + use codex_protocol::models::PermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashMap; use std::io; @@ -370,8 +353,8 @@ mod windows_impl { #[allow(clippy::too_many_arguments)] pub fn run_windows_sandbox_capture( - policy_json_or_preset: &str, - sandbox_policy_cwd: &Path, + permission_profile: &PermissionProfile, + permission_profile_cwd: &Path, codex_home: &Path, command: Vec, cwd: &Path, @@ -380,8 +363,8 @@ mod windows_impl { use_private_desktop: bool, ) -> Result { run_windows_sandbox_capture_with_filesystem_overrides( - policy_json_or_preset, - sandbox_policy_cwd, + permission_profile, + permission_profile_cwd, codex_home, command, cwd, @@ -395,8 +378,8 @@ mod windows_impl { #[allow(clippy::too_many_arguments)] pub fn run_windows_sandbox_capture_with_filesystem_overrides( - policy_json_or_preset: &str, - sandbox_policy_cwd: &Path, + permission_profile: &PermissionProfile, + permission_profile_cwd: &Path, codex_home: &Path, command: Vec, cwd: &Path, @@ -415,8 +398,8 @@ mod windows_impl { .map(AbsolutePathBuf::to_path_buf) .collect::>(); let common = prepare_legacy_spawn_context( - policy_json_or_preset, - sandbox_policy_cwd, + permission_profile, + permission_profile_cwd, codex_home, cwd, &mut env_map, @@ -426,12 +409,11 @@ mod windows_impl { add_git_safe_directory: false, }, )?; - let policy = common.policy; let permissions = common.permissions; let current_dir = common.current_dir; let logs_base_dir = common.logs_base_dir.as_deref(); let uses_write_capabilities = common.uses_write_capabilities; - if !policy.has_full_disk_read_access() { + if !permissions.has_full_disk_read_access() { anyhow::bail!( "Restricted read-only access requires the elevated Windows sandbox backend" ); @@ -441,14 +423,14 @@ mod windows_impl { if !additional_deny_read_paths.is_empty() { anyhow::bail!("deny-read overrides require the elevated Windows sandbox backend"); } - let capability_roots = legacy_session_capability_roots( - &policy, - sandbox_policy_cwd, - ¤t_dir, - &env_map, + let capability_roots = + legacy_session_capability_roots(&permissions, ¤t_dir, &env_map, codex_home); + let security = prepare_legacy_session_security( + uses_write_capabilities, codex_home, - ); - let security = prepare_legacy_session_security(&policy, codex_home, cwd, capability_roots)?; + cwd, + capability_roots, + )?; allow_null_device_for_workspace_write(uses_write_capabilities); apply_legacy_session_acl_rules( &permissions, @@ -595,32 +577,29 @@ mod windows_impl { } pub fn run_windows_sandbox_legacy_preflight( - sandbox_policy: &SandboxPolicy, - sandbox_policy_cwd: &Path, + permission_profile: &PermissionProfile, + permission_profile_cwd: &Path, codex_home: &Path, cwd: &Path, env_map: &HashMap, ) -> Result<()> { - let is_workspace_write = matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }); - if !is_workspace_write { + let Ok(permissions) = super::resolved_permissions::ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_cwd( + permission_profile, + permission_profile_cwd, + ) else { + return Ok(()); + }; + if !permissions.uses_write_capabilities_for_cwd(cwd, env_map) { return Ok(()); } ensure_codex_home_exists(codex_home)?; let current_dir = cwd.to_path_buf(); - let capability_roots = legacy_session_capability_roots( - sandbox_policy, - sandbox_policy_cwd, - ¤t_dir, - env_map, - codex_home, - ); + let capability_roots = + legacy_session_capability_roots(&permissions, ¤t_dir, env_map, codex_home); let write_root_sids = root_capability_sids(codex_home, cwd, capability_roots)?; apply_legacy_session_acl_rules( - &ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd( - sandbox_policy, - sandbox_policy_cwd, - ), + &permissions, codex_home, ¤t_dir, env_map, @@ -638,43 +617,66 @@ mod windows_impl { #[cfg(test)] mod tests { - use crate::policy::SandboxPolicy; use crate::resolved_permissions::ResolvedWindowsSandboxPermissions; + use codex_protocol::models::PermissionProfile; + use codex_protocol::permissions::NetworkSandboxPolicy; + use std::collections::HashMap; use std::path::Path; - fn workspace_policy(network_access: bool) -> SandboxPolicy { - SandboxPolicy::WorkspaceWrite { - writable_roots: Vec::new(), - network_access, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - } + fn workspace_profile(network_policy: NetworkSandboxPolicy) -> PermissionProfile { + PermissionProfile::workspace_write_with( + &[], + network_policy, + /*exclude_tmpdir_env_var*/ false, + /*exclude_slash_tmp*/ false, + ) } - fn should_apply_network_block(policy: &SandboxPolicy) -> bool { - ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, Path::new(".")) - .should_apply_network_block() + fn should_apply_network_block(permission_profile: &PermissionProfile) -> bool { + ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_cwd( + permission_profile, + Path::new("."), + ) + .expect("managed permissions") + .should_apply_network_block() } #[test] fn applies_network_block_when_access_is_disabled() { - assert!(should_apply_network_block(&workspace_policy( - /*network_access*/ false + assert!(should_apply_network_block(&workspace_profile( + NetworkSandboxPolicy::Restricted ))); } #[test] fn skips_network_block_when_access_is_allowed() { - assert!(!should_apply_network_block(&workspace_policy( - /*network_access*/ true + assert!(!should_apply_network_block(&workspace_profile( + NetworkSandboxPolicy::Enabled ))); } #[test] fn applies_network_block_for_read_only() { - assert!(should_apply_network_block( - &SandboxPolicy::new_read_only_policy() - )); + assert!(should_apply_network_block(&PermissionProfile::read_only())); + } + + #[test] + fn legacy_preflight_skips_profiles_without_managed_filesystem_permissions() { + for permission_profile in [ + PermissionProfile::Disabled, + PermissionProfile::External { + network: NetworkSandboxPolicy::Restricted, + }, + ] { + super::run_windows_sandbox_legacy_preflight( + &permission_profile, + Path::new("."), + Path::new("."), + Path::new("."), + &HashMap::new(), + ) + .expect("unsupported profiles do not need ACL preflight"); + } } } } @@ -683,7 +685,7 @@ mod windows_impl { mod stub { use anyhow::Result; use anyhow::bail; - use codex_protocol::protocol::SandboxPolicy; + use codex_protocol::models::PermissionProfile; use std::collections::HashMap; use std::path::Path; @@ -697,8 +699,8 @@ mod stub { #[allow(clippy::too_many_arguments)] pub fn run_windows_sandbox_capture( - _policy_json_or_preset: &str, - _sandbox_policy_cwd: &Path, + _permission_profile: &PermissionProfile, + _permission_profile_cwd: &Path, _codex_home: &Path, _command: Vec, _cwd: &Path, @@ -709,19 +711,9 @@ mod stub { bail!("Windows sandbox is only available on Windows") } - pub fn apply_world_writable_scan_and_denies( - _codex_home: &Path, - _cwd: &Path, - _env_map: &HashMap, - _sandbox_policy: &SandboxPolicy, - _logs_base_dir: Option<&Path>, - ) -> Result<()> { - bail!("Windows sandbox is only available on Windows") - } - pub fn run_windows_sandbox_legacy_preflight( - _sandbox_policy: &SandboxPolicy, - _sandbox_policy_cwd: &Path, + _permission_profile: &PermissionProfile, + _permission_profile_cwd: &Path, _codex_home: &Path, _cwd: &Path, _env_map: &HashMap, diff --git a/codex-rs/windows-sandbox-rs/src/policy.rs b/codex-rs/windows-sandbox-rs/src/policy.rs deleted file mode 100644 index cfaa9689ee..0000000000 --- a/codex-rs/windows-sandbox-rs/src/policy.rs +++ /dev/null @@ -1,61 +0,0 @@ -use anyhow::Result; -pub use codex_protocol::protocol::SandboxPolicy; - -pub fn parse_policy(value: &str) -> Result { - match value { - "read-only" => Ok(SandboxPolicy::new_read_only_policy()), - "workspace-write" => Ok(SandboxPolicy::new_workspace_write_policy()), - "danger-full-access" | "external-sandbox" => { - anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing") - } - other => { - let parsed: SandboxPolicy = serde_json::from_str(other)?; - if matches!( - parsed, - SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } - ) { - anyhow::bail!( - "DangerFullAccess and ExternalSandbox are not supported for sandboxing" - ); - } - Ok(parsed) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use pretty_assertions::assert_eq; - - #[test] - fn rejects_external_sandbox_preset() { - let err = parse_policy("external-sandbox").unwrap_err(); - assert!( - err.to_string() - .contains("DangerFullAccess and ExternalSandbox are not supported") - ); - } - - #[test] - fn rejects_external_sandbox_json() { - let payload = - serde_json::to_string(&codex_protocol::protocol::SandboxPolicy::ExternalSandbox { - network_access: codex_protocol::protocol::NetworkAccess::Enabled, - }) - .unwrap(); - let err = parse_policy(&payload).unwrap_err(); - assert!( - err.to_string() - .contains("DangerFullAccess and ExternalSandbox are not supported") - ); - } - - #[test] - fn parses_read_only_policy() { - assert_eq!( - parse_policy("read-only").unwrap(), - SandboxPolicy::new_read_only_policy() - ); - } -} diff --git a/codex-rs/windows-sandbox-rs/src/resolved_permissions.rs b/codex-rs/windows-sandbox-rs/src/resolved_permissions.rs index dbb08b2115..75c4c216a3 100644 --- a/codex-rs/windows-sandbox-rs/src/resolved_permissions.rs +++ b/codex-rs/windows-sandbox-rs/src/resolved_permissions.rs @@ -5,7 +5,6 @@ use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxKind; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; -use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashMap; use std::path::Path; @@ -58,14 +57,6 @@ pub fn token_mode_for_permission_profile( } impl ResolvedWindowsSandboxPermissions { - pub fn from_legacy_policy_for_cwd(policy: &SandboxPolicy, cwd: &Path) -> Self { - Self { - file_system: FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(policy, cwd) - .materialize_project_roots_with_cwd(cwd), - network: NetworkSandboxPolicy::from(policy), - } - } - pub fn try_from_permission_profile(permission_profile: &PermissionProfile) -> Result { if !matches!(permission_profile, PermissionProfile::Managed { .. }) { anyhow::bail!( @@ -246,34 +237,6 @@ mod tests { assert_eq!(expected_roots, roots); } - #[test] - fn legacy_workspace_root_stays_bound_to_policy_cwd() { - let tmp = TempDir::new().expect("tempdir"); - let policy_cwd = tmp.path().join("workspace"); - let command_cwd = policy_cwd.join("subdir"); - std::fs::create_dir_all(&command_cwd).expect("create command cwd"); - - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: Vec::new(), - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; - let permissions = - ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(&policy, &policy_cwd); - - let roots = permissions - .writable_roots_for_cwd(&command_cwd, &HashMap::new()) - .into_iter() - .map(|root| root.root) - .collect::>(); - - assert_eq!( - roots, - vec![dunce::canonicalize(&policy_cwd).expect("canonical policy cwd")] - ); - } - #[test] fn permission_profile_workspace_root_stays_bound_to_profile_cwd() { let tmp = TempDir::new().expect("tempdir"); diff --git a/codex-rs/windows-sandbox-rs/src/setup.rs b/codex-rs/windows-sandbox-rs/src/setup.rs index b77a874160..cc600b85ac 100644 --- a/codex-rs/windows-sandbox-rs/src/setup.rs +++ b/codex-rs/windows-sandbox-rs/src/setup.rs @@ -17,7 +17,6 @@ use crate::helper_materialization::helper_bin_dir; use crate::logging::log_note; use crate::path_normalization::canonical_path_key; use crate::path_normalization::canonicalize_path; -use crate::policy::SandboxPolicy; use crate::resolved_permissions::ResolvedWindowsSandboxPermissions; use crate::setup_error::SetupErrorCode; use crate::setup_error::SetupFailure; @@ -30,6 +29,7 @@ use anyhow::Result; use anyhow::anyhow; use base64::Engine; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; +use codex_protocol::models::PermissionProfile; use windows_sys::Win32::Foundation::CloseHandle; use windows_sys::Win32::Foundation::GetLastError; @@ -104,21 +104,19 @@ pub struct SetupRootOverrides { } pub fn run_setup_refresh( - policy: &SandboxPolicy, - policy_cwd: &Path, + permission_profile: &PermissionProfile, + permission_profile_cwd: &Path, command_cwd: &Path, env_map: &HashMap, codex_home: &Path, proxy_enforced: bool, ) -> Result<()> { - if matches!( - policy, - SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } - ) { + let Ok(permissions) = ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_cwd( + permission_profile, + permission_profile_cwd, + ) else { return Ok(()); - } - let permissions = - ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, policy_cwd); + }; run_setup_refresh_inner( SandboxSetupRequest { permissions: &permissions, @@ -139,22 +137,20 @@ pub fn run_setup_refresh_with_overrides( } pub fn run_setup_refresh_with_extra_read_roots( - policy: &SandboxPolicy, - policy_cwd: &Path, + permission_profile: &PermissionProfile, + permission_profile_cwd: &Path, command_cwd: &Path, env_map: &HashMap, codex_home: &Path, extra_read_roots: Vec, proxy_enforced: bool, ) -> Result<()> { - if matches!( - policy, - SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } - ) { + let Ok(permissions) = ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_cwd( + permission_profile, + permission_profile_cwd, + ) else { return Ok(()); - } - let permissions = - ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, policy_cwd); + }; let mut read_roots = gather_read_roots(command_cwd, &permissions, env_map, codex_home); read_roots.extend(extra_read_roots); run_setup_refresh_inner( @@ -1020,13 +1016,15 @@ mod tests { use crate::helper_materialization::BIN_DIRNAME; use crate::helper_materialization::RESOURCES_DIRNAME; use crate::helper_materialization::helper_bin_dir; - use crate::policy::SandboxPolicy; use crate::resolved_permissions::ResolvedWindowsSandboxPermissions; + use codex_protocol::models::PermissionProfile; + use codex_protocol::permissions::NetworkSandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use std::collections::HashMap; use std::collections::HashSet; use std::fs; + use std::path::Path; use std::path::PathBuf; use tempfile::TempDir; @@ -1038,10 +1036,63 @@ mod tests { } fn permissions_for( - policy: &SandboxPolicy, - policy_cwd: &std::path::Path, + permission_profile: &PermissionProfile, + permission_profile_cwd: &Path, ) -> ResolvedWindowsSandboxPermissions { - ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, policy_cwd) + ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_cwd( + permission_profile, + permission_profile_cwd, + ) + .expect("managed permission profile") + } + + fn workspace_write_profile( + writable_roots: &[AbsolutePathBuf], + exclude_tmpdir_env_var: bool, + exclude_slash_tmp: bool, + ) -> PermissionProfile { + PermissionProfile::workspace_write_with( + writable_roots, + NetworkSandboxPolicy::Restricted, + exclude_tmpdir_env_var, + exclude_slash_tmp, + ) + } + + #[test] + fn setup_refresh_skips_profiles_without_managed_filesystem_permissions() { + let tmp = TempDir::new().expect("tempdir"); + let command_cwd = tmp.path().join("workspace"); + let codex_home = tmp.path().join("codex-home"); + fs::create_dir_all(&command_cwd).expect("create workspace"); + + for permission_profile in [ + PermissionProfile::Disabled, + PermissionProfile::External { + network: NetworkSandboxPolicy::Restricted, + }, + ] { + super::run_setup_refresh( + &permission_profile, + command_cwd.as_path(), + command_cwd.as_path(), + &HashMap::new(), + codex_home.as_path(), + /*proxy_enforced*/ false, + ) + .expect("unsupported profiles do not need setup refresh"); + + super::run_setup_refresh_with_extra_read_roots( + &permission_profile, + command_cwd.as_path(), + command_cwd.as_path(), + &HashMap::new(), + codex_home.as_path(), + vec![command_cwd.clone()], + /*proxy_enforced*/ false, + ) + .expect("unsupported profiles do not need setup refresh"); + } } #[test] @@ -1361,8 +1412,8 @@ mod tests { let codex_home = tmp.path().join("codex-home"); let command_cwd = tmp.path().join("workspace"); fs::create_dir_all(&command_cwd).expect("create workspace"); - let policy = SandboxPolicy::new_read_only_policy(); - let permissions = permissions_for(&policy, &command_cwd); + let permission_profile = PermissionProfile::read_only(); + let permissions = permissions_for(&permission_profile, &command_cwd); let roots = gather_read_roots(&command_cwd, &permissions, &HashMap::new(), &codex_home); let expected = @@ -1379,16 +1430,15 @@ mod tests { let writable_root = tmp.path().join("extra-write-root"); fs::create_dir_all(&command_cwd).expect("create workspace"); fs::create_dir_all(&writable_root).expect("create writable root"); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![ - AbsolutePathBuf::from_absolute_path(&writable_root) - .expect("absolute writable root"), - ], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; - let permissions = permissions_for(&policy, &command_cwd); + let writable_roots = vec![ + AbsolutePathBuf::from_absolute_path(&writable_root).expect("absolute writable root"), + ]; + let permission_profile = workspace_write_profile( + &writable_roots, + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ true, + ); + let permissions = permissions_for(&permission_profile, &command_cwd); let roots = gather_read_roots(&command_cwd, &permissions, &HashMap::new(), &codex_home); let expected_writable = @@ -1401,16 +1451,14 @@ mod tests { fn build_payload_roots_preserves_helper_roots_when_read_override_is_provided() { let tmp = TempDir::new().expect("tempdir"); let codex_home = tmp.path().join("codex-home"); - let policy_cwd = tmp.path().join("policy-cwd"); + let permission_profile_cwd = tmp.path().join("permission-profile-cwd"); let command_cwd = tmp.path().join("workspace"); let readable_root = tmp.path().join("docs"); - fs::create_dir_all(&policy_cwd).expect("create policy cwd"); + fs::create_dir_all(&permission_profile_cwd).expect("create permission profile cwd"); fs::create_dir_all(&command_cwd).expect("create workspace"); fs::create_dir_all(&readable_root).expect("create readable root"); - let policy = SandboxPolicy::ReadOnly { - network_access: false, - }; - let permissions = permissions_for(&policy, &policy_cwd); + let permission_profile = PermissionProfile::read_only(); + let permissions = permissions_for(&permission_profile, &permission_profile_cwd); let (read_roots, write_roots) = build_payload_roots( &super::SandboxSetupRequest { @@ -1449,16 +1497,14 @@ mod tests { fn build_payload_roots_replaces_full_read_policy_when_read_override_is_provided() { let tmp = TempDir::new().expect("tempdir"); let codex_home = tmp.path().join("codex-home"); - let policy_cwd = tmp.path().join("policy-cwd"); + let permission_profile_cwd = tmp.path().join("permission-profile-cwd"); let command_cwd = tmp.path().join("workspace"); let readable_root = tmp.path().join("docs"); - fs::create_dir_all(&policy_cwd).expect("create policy cwd"); + fs::create_dir_all(&permission_profile_cwd).expect("create permission profile cwd"); fs::create_dir_all(&command_cwd).expect("create workspace"); fs::create_dir_all(&readable_root).expect("create readable root"); - let policy = SandboxPolicy::ReadOnly { - network_access: false, - }; - let permissions = permissions_for(&policy, &policy_cwd); + let permission_profile = PermissionProfile::read_only(); + let permissions = permissions_for(&permission_profile, &permission_profile_cwd); let (read_roots, write_roots) = build_payload_roots( &super::SandboxSetupRequest { @@ -1504,13 +1550,12 @@ mod tests { fs::create_dir_all(&command_cwd).expect("create workspace"); fs::create_dir_all(&extra_root).expect("create extra root"); fs::create_dir_all(&sandbox_root).expect("create sandbox root"); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; - let permissions = permissions_for(&policy, &command_cwd); + let permission_profile = workspace_write_profile( + &[], + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ true, + ); + let permissions = permissions_for(&permission_profile, &command_cwd); let override_roots = vec![ command_cwd.clone(), extra_root.clone(), @@ -1553,21 +1598,20 @@ mod tests { } #[test] - fn effective_write_roots_use_policy_cwd_for_legacy_workspace_root() { + fn effective_write_roots_use_profile_cwd_for_workspace_root() { let tmp = TempDir::new().expect("tempdir"); let codex_home = tmp.path().join("codex-home"); - let policy_cwd = tmp.path().join("workspace"); - let command_cwd = policy_cwd.join("subdir"); + let permission_profile_cwd = tmp.path().join("workspace"); + let command_cwd = permission_profile_cwd.join("subdir"); fs::create_dir_all(&codex_home).expect("create codex home"); fs::create_dir_all(&command_cwd).expect("create command cwd"); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; - let permissions = permissions_for(&policy, &policy_cwd); + let permission_profile = workspace_write_profile( + &[], + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ true, + ); + let permissions = permissions_for(&permission_profile, &permission_profile_cwd); let effective_write_roots = super::effective_write_roots_for_setup( &permissions, @@ -1579,7 +1623,7 @@ mod tests { assert_eq!( effective_write_roots, - vec![dunce::canonicalize(&policy_cwd).expect("canonical policy cwd")] + vec![dunce::canonicalize(&permission_profile_cwd).expect("canonical profile cwd")] ); } @@ -1594,16 +1638,15 @@ mod tests { let explicit_deny = tmp.path().join("explicit-deny"); fs::create_dir_all(&command_git).expect("create command .git"); fs::create_dir_all(&extra_codex).expect("create extra .codex"); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![ - AbsolutePathBuf::from_absolute_path(&extra_write_root) - .expect("absolute writable root"), - ], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; - let permissions = permissions_for(&policy, &command_cwd); + let writable_roots = vec![ + AbsolutePathBuf::from_absolute_path(&extra_write_root).expect("absolute writable root"), + ]; + let permission_profile = workspace_write_profile( + &writable_roots, + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ true, + ); + let permissions = permissions_for(&permission_profile, &command_cwd); let request = super::SandboxSetupRequest { permissions: &permissions, command_cwd: &command_cwd, @@ -1633,8 +1676,8 @@ mod tests { let codex_home = tmp.path().join("codex-home"); let command_cwd = tmp.path().join("workspace"); fs::create_dir_all(&command_cwd).expect("create workspace"); - let policy = SandboxPolicy::new_read_only_policy(); - let permissions = permissions_for(&policy, &command_cwd); + let permission_profile = PermissionProfile::read_only(); + let permissions = permissions_for(&permission_profile, &command_cwd); let roots = gather_full_read_roots_for_permissions( &command_cwd, diff --git a/codex-rs/windows-sandbox-rs/src/spawn_prep.rs b/codex-rs/windows-sandbox-rs/src/spawn_prep.rs index f16e964308..b68cc57a81 100644 --- a/codex-rs/windows-sandbox-rs/src/spawn_prep.rs +++ b/codex-rs/windows-sandbox-rs/src/spawn_prep.rs @@ -17,8 +17,6 @@ use crate::identity::SandboxCreds; use crate::identity::require_logon_sandbox_creds; use crate::logging::log_start; use crate::path_normalization::canonicalize_path; -use crate::policy::SandboxPolicy; -use crate::policy::parse_policy; use crate::resolved_permissions::ResolvedWindowsSandboxPermissions; use crate::sandbox_utils::ensure_codex_home_exists; use crate::sandbox_utils::inject_git_safe_directory; @@ -33,6 +31,7 @@ use crate::workspace_acl::protect_workspace_agents_dir; use crate::workspace_acl::protect_workspace_codex_dir; use anyhow::Context; use anyhow::Result; +use codex_protocol::models::PermissionProfile; use std::collections::HashMap; use std::ffi::c_void; use std::path::Path; @@ -41,7 +40,6 @@ use windows_sys::Win32::Foundation::CloseHandle; use windows_sys::Win32::Foundation::HANDLE; pub(crate) struct SpawnContext { - pub(crate) policy: SandboxPolicy, pub(crate) permissions: ResolvedWindowsSandboxPermissions, pub(crate) current_dir: PathBuf, pub(crate) logs_base_dir: Option, @@ -81,21 +79,18 @@ pub(crate) struct LegacyAclSids<'a> { } fn prepare_spawn_context_common( - policy_json_or_preset: &str, - policy_cwd: &Path, + permission_profile: &PermissionProfile, + permission_profile_cwd: &Path, codex_home: &Path, cwd: &Path, env_map: &mut HashMap, command: &[String], options: SpawnPrepOptions, ) -> Result { - let policy = parse_policy(policy_json_or_preset)?; - if matches!( - &policy, - SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } - ) { - anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing") - } + let permissions = ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_cwd( + permission_profile, + permission_profile_cwd, + )?; normalize_null_device_env(env_map); ensure_non_interactive_pager(env_map); @@ -112,12 +107,9 @@ fn prepare_spawn_context_common( let logs_base_dir = Some(sandbox_base); log_start(command, logs_base_dir.as_deref()); - let permissions = - ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(&policy, policy_cwd); let uses_write_capabilities = permissions.uses_write_capabilities_for_cwd(cwd, env_map); Ok(SpawnContext { - policy, permissions, current_dir: cwd.to_path_buf(), logs_base_dir, @@ -126,8 +118,8 @@ fn prepare_spawn_context_common( } pub(crate) fn prepare_legacy_spawn_context( - policy_json_or_preset: &str, - policy_cwd: &Path, + permission_profile: &PermissionProfile, + permission_profile_cwd: &Path, codex_home: &Path, cwd: &Path, env_map: &mut HashMap, @@ -135,8 +127,8 @@ pub(crate) fn prepare_legacy_spawn_context( options: SpawnPrepOptions, ) -> Result { let common = prepare_spawn_context_common( - policy_json_or_preset, - policy_cwd, + permission_profile, + permission_profile_cwd, codex_home, cwd, env_map, @@ -150,38 +142,31 @@ pub(crate) fn prepare_legacy_spawn_context( } pub(crate) fn prepare_legacy_session_security( - policy: &SandboxPolicy, + uses_write_capabilities: bool, codex_home: &Path, cwd: &Path, capability_roots: impl IntoIterator, ) -> Result { let caps = load_or_create_cap_sids(codex_home)?; let (h_token, readonly_sid, readonly_sid_str, write_root_sids) = unsafe { - match policy { - SandboxPolicy::ReadOnly { .. } => { - let psid = LocalSid::from_string(&caps.readonly)?; - let (h_token, _psid) = create_readonly_token_with_cap(psid.as_ptr())?; - (h_token, Some(psid), Some(caps.readonly), Vec::new()) - } - SandboxPolicy::WorkspaceWrite { .. } => { - let write_root_sids = root_capability_sids(codex_home, cwd, capability_roots)?; - if write_root_sids.is_empty() { - anyhow::bail!("workspace-write sandbox has no writable root capability SIDs"); - } - let base = get_current_token_for_restriction()?; - let cap_ptrs: Vec<*mut c_void> = write_root_sids - .iter() - .map(|root| root.sid.as_ptr()) - .collect(); - let h_token = - create_workspace_write_token_with_caps_from(base, cap_ptrs.as_slice()); - CloseHandle(base); - let h_token = h_token?; - (h_token, None, None, write_root_sids) - } - SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => { - unreachable!("dangerous policies rejected before legacy session prep") + if uses_write_capabilities { + let write_root_sids = root_capability_sids(codex_home, cwd, capability_roots)?; + if write_root_sids.is_empty() { + anyhow::bail!("workspace-write sandbox has no writable root capability SIDs"); } + let base = get_current_token_for_restriction()?; + let cap_ptrs: Vec<*mut c_void> = write_root_sids + .iter() + .map(|root| root.sid.as_ptr()) + .collect(); + let h_token = create_workspace_write_token_with_caps_from(base, cap_ptrs.as_slice()); + CloseHandle(base); + let h_token = h_token?; + (h_token, None, None, write_root_sids) + } else { + let psid = LocalSid::from_string(&caps.readonly)?; + let (h_token, _psid) = create_readonly_token_with_cap(psid.as_ptr())?; + (h_token, Some(psid), Some(caps.readonly), Vec::new()) } }; @@ -194,21 +179,18 @@ pub(crate) fn prepare_legacy_session_security( } pub(crate) fn legacy_session_capability_roots( - policy: &SandboxPolicy, - policy_cwd: &Path, + permissions: &ResolvedWindowsSandboxPermissions, current_dir: &Path, env_map: &HashMap, codex_home: &Path, ) -> Vec { - let permissions = - ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, policy_cwd); - let allow_paths = compute_allow_paths_for_permissions(&permissions, current_dir, env_map) + let allow_paths = compute_allow_paths_for_permissions(permissions, current_dir, env_map) .allow .into_iter() .collect::>(); if permissions.uses_write_capabilities_for_cwd(current_dir, env_map) { effective_write_roots_for_permissions( - &permissions, + permissions, current_dir, env_map, codex_home, @@ -459,7 +441,6 @@ pub(crate) fn prepare_elevated_spawn_context_for_permissions( #[cfg(test)] mod tests { - use super::SandboxPolicy; use super::SpawnPrepOptions; use super::deny_root_capabilities_for_path; use super::legacy_session_capability_roots; @@ -469,34 +450,52 @@ mod tests { use crate::cap::load_or_create_cap_sids; use crate::cap::workspace_write_cap_sid_for_root; use crate::resolved_permissions::ResolvedWindowsSandboxPermissions; + use codex_protocol::models::PermissionProfile; + use codex_protocol::permissions::NetworkSandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use std::collections::HashMap; use std::path::Path; use tempfile::TempDir; - fn should_apply_network_block(policy: &SandboxPolicy) -> bool { - ResolvedWindowsSandboxPermissions::from_legacy_policy_for_cwd(policy, Path::new(".")) - .should_apply_network_block() + fn workspace_profile( + network_policy: NetworkSandboxPolicy, + writable_roots: &[AbsolutePathBuf], + exclude_tmpdir_env_var: bool, + exclude_slash_tmp: bool, + ) -> PermissionProfile { + PermissionProfile::workspace_write_with( + writable_roots, + network_policy, + exclude_tmpdir_env_var, + exclude_slash_tmp, + ) + } + + fn should_apply_network_block(permission_profile: &PermissionProfile) -> bool { + ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_cwd( + permission_profile, + Path::new("."), + ) + .expect("managed permission profile") + .should_apply_network_block() } #[test] fn no_network_env_rewrite_applies_for_workspace_write() { assert!(should_apply_network_block( - &SandboxPolicy::new_workspace_write_policy(), + &PermissionProfile::workspace_write() )); } #[test] fn no_network_env_rewrite_skips_when_network_access_is_allowed() { - assert!(!should_apply_network_block( - &SandboxPolicy::WorkspaceWrite { - writable_roots: Vec::new(), - network_access: true, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - }, - )); + assert!(!should_apply_network_block(&workspace_profile( + NetworkSandboxPolicy::Enabled, + &[], + /*exclude_tmpdir_env_var*/ false, + /*exclude_slash_tmp*/ false, + ))); } #[test] @@ -506,7 +505,7 @@ mod tests { let mut env_map = HashMap::new(); let _context = prepare_legacy_spawn_context( - "workspace-write", + &PermissionProfile::workspace_write(), cwd.path(), codex_home.path(), cwd.path(), @@ -536,7 +535,7 @@ mod tests { )]); let context = prepare_spawn_context_common( - "workspace-write", + &PermissionProfile::workspace_write(), cwd.path(), codex_home.path(), cwd.path(), @@ -548,7 +547,7 @@ mod tests { }, ) .expect("preserve existing env prep"); - assert_eq!(context.policy, SandboxPolicy::new_workspace_write_policy()); + assert!(context.uses_write_capabilities); assert_eq!(env_map.get("SBX_NONET_ACTIVE"), None); assert_eq!( @@ -558,24 +557,28 @@ mod tests { } #[test] - fn legacy_session_capability_roots_use_policy_cwd_for_workspace_root() { + fn legacy_session_capability_roots_use_profile_cwd_for_workspace_root() { let tmp = TempDir::new().expect("tempdir"); let codex_home = tmp.path().join("codex-home"); - let policy_cwd = tmp.path().join("workspace"); - let command_cwd = policy_cwd.join("subdir"); + let permission_profile_cwd = tmp.path().join("workspace"); + let command_cwd = permission_profile_cwd.join("subdir"); std::fs::create_dir_all(&codex_home).expect("create codex home"); std::fs::create_dir_all(&command_cwd).expect("create command cwd"); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: Vec::new(), - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; + let permission_profile = workspace_profile( + NetworkSandboxPolicy::Restricted, + &[], + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ true, + ); + let permissions = ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_cwd( + &permission_profile, + &permission_profile_cwd, + ) + .expect("managed permission profile"); let roots = legacy_session_capability_roots( - &policy, - &policy_cwd, + &permissions, &command_cwd, &HashMap::new(), &codex_home, @@ -583,7 +586,7 @@ mod tests { assert_eq!( roots, - vec![dunce::canonicalize(&policy_cwd).expect("canonical policy cwd")] + vec![dunce::canonicalize(&permission_profile_cwd).expect("canonical profile cwd")] ); } @@ -672,24 +675,25 @@ mod tests { std::fs::create_dir_all(&active_root).expect("create active root"); std::fs::create_dir_all(&sandbox_root).expect("create sandbox root"); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![ - AbsolutePathBuf::try_from(active_root.as_path()).expect("active root"), - AbsolutePathBuf::try_from(codex_home.as_path()).expect("codex home"), - AbsolutePathBuf::try_from(sandbox_root.as_path()).expect("sandbox root"), - ], - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; - - let roots = legacy_session_capability_roots( - &policy, - &workspace, - &workspace, - &HashMap::new(), - &codex_home, + let writable_roots = vec![ + AbsolutePathBuf::try_from(active_root.as_path()).expect("active root"), + AbsolutePathBuf::try_from(codex_home.as_path()).expect("codex home"), + AbsolutePathBuf::try_from(sandbox_root.as_path()).expect("sandbox root"), + ]; + let permission_profile = workspace_profile( + NetworkSandboxPolicy::Restricted, + &writable_roots, + /*exclude_tmpdir_env_var*/ true, + /*exclude_slash_tmp*/ true, ); + let permissions = ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_cwd( + &permission_profile, + &workspace, + ) + .expect("managed permission profile"); + + let roots = + legacy_session_capability_roots(&permissions, &workspace, &HashMap::new(), &codex_home); assert!(roots.contains(&dunce::canonicalize(&workspace).expect("workspace"))); assert!(roots.contains(&dunce::canonicalize(&active_root).expect("active root"))); diff --git a/codex-rs/windows-sandbox-rs/src/unified_exec/backends/elevated.rs b/codex-rs/windows-sandbox-rs/src/unified_exec/backends/elevated.rs index 1e13e45cc0..09ac786238 100644 --- a/codex-rs/windows-sandbox-rs/src/unified_exec/backends/elevated.rs +++ b/codex-rs/windows-sandbox-rs/src/unified_exec/backends/elevated.rs @@ -8,7 +8,6 @@ use crate::ipc_framed::FramedMessage; use crate::ipc_framed::IPC_PROTOCOL_VERSION; use crate::ipc_framed::Message; use crate::ipc_framed::SpawnRequest; -use crate::policy::parse_policy; use crate::resolved_permissions::ResolvedWindowsSandboxPermissions; use crate::runner_client::spawn_runner_transport; use crate::spawn_prep::prepare_elevated_spawn_context_for_permissions; @@ -145,44 +144,3 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated_for_permission_profil stdin_open, )) } - -#[allow(clippy::too_many_arguments)] -pub(crate) async fn spawn_windows_sandbox_session_elevated( - policy_json_or_preset: &str, - sandbox_policy_cwd: &Path, - codex_home: &Path, - command: Vec, - cwd: &Path, - env_map: HashMap, - timeout_ms: Option, - read_roots_override: Option<&[PathBuf]>, - read_roots_include_platform_defaults: bool, - write_roots_override: Option<&[PathBuf]>, - deny_read_paths_override: &[AbsolutePathBuf], - deny_write_paths_override: &[AbsolutePathBuf], - tty: bool, - stdin_open: bool, - use_private_desktop: bool, -) -> Result { - let policy = parse_policy(policy_json_or_preset)?; - let permission_profile = - PermissionProfile::from_legacy_sandbox_policy_for_cwd(&policy, sandbox_policy_cwd); - spawn_windows_sandbox_session_elevated_for_permission_profile( - &permission_profile, - sandbox_policy_cwd, - codex_home, - command, - cwd, - env_map, - timeout_ms, - read_roots_override, - read_roots_include_platform_defaults, - write_roots_override, - deny_read_paths_override, - deny_write_paths_override, - tty, - stdin_open, - use_private_desktop, - ) - .await -} diff --git a/codex-rs/windows-sandbox-rs/src/unified_exec/backends/legacy.rs b/codex-rs/windows-sandbox-rs/src/unified_exec/backends/legacy.rs index 4db782a601..2fc8c12e00 100644 --- a/codex-rs/windows-sandbox-rs/src/unified_exec/backends/legacy.rs +++ b/codex-rs/windows-sandbox-rs/src/unified_exec/backends/legacy.rs @@ -17,6 +17,7 @@ use crate::spawn_prep::legacy_session_capability_roots; use crate::spawn_prep::prepare_legacy_session_security; use crate::spawn_prep::prepare_legacy_spawn_context; use anyhow::Result; +use codex_protocol::models::PermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_pty::ProcessDriver; use codex_utils_pty::SpawnedProcess; @@ -269,8 +270,8 @@ fn resize_conpty_handle(hpc: &Arc>>, size: TerminalSize) #[allow(clippy::too_many_arguments)] pub(crate) async fn spawn_windows_sandbox_session_legacy( - policy_json_or_preset: &str, - sandbox_policy_cwd: &Path, + permission_profile: &PermissionProfile, + permission_profile_cwd: &Path, codex_home: &Path, command: Vec, cwd: &Path, @@ -283,8 +284,8 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy( use_private_desktop: bool, ) -> Result { let common = prepare_legacy_spawn_context( - policy_json_or_preset, - sandbox_policy_cwd, + permission_profile, + permission_profile_cwd, codex_home, cwd, &mut env_map, @@ -294,7 +295,7 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy( add_git_safe_directory: false, }, )?; - if !common.policy.has_full_disk_read_access() { + if !common.permissions.has_full_disk_read_access() { anyhow::bail!("Restricted read-only access requires the elevated Windows sandbox backend"); } // WRITE_RESTRICTED tokens consult restricting SIDs only for writes, so this @@ -307,14 +308,17 @@ pub(crate) async fn spawn_windows_sandbox_session_legacy( .map(AbsolutePathBuf::to_path_buf) .collect::>(); let capability_roots = legacy_session_capability_roots( - &common.policy, - sandbox_policy_cwd, + &common.permissions, &common.current_dir, &env_map, codex_home, ); - let security = - prepare_legacy_session_security(&common.policy, codex_home, cwd, capability_roots)?; + let security = prepare_legacy_session_security( + common.uses_write_capabilities, + codex_home, + cwd, + capability_roots, + )?; allow_null_device_for_workspace_write(common.uses_write_capabilities); apply_legacy_session_acl_rules( diff --git a/codex-rs/windows-sandbox-rs/src/unified_exec/mod.rs b/codex-rs/windows-sandbox-rs/src/unified_exec/mod.rs index ceb4bd03b6..e327200426 100644 --- a/codex-rs/windows-sandbox-rs/src/unified_exec/mod.rs +++ b/codex-rs/windows-sandbox-rs/src/unified_exec/mod.rs @@ -19,8 +19,8 @@ use std::path::PathBuf; #[allow(clippy::too_many_arguments)] pub async fn spawn_windows_sandbox_session_legacy( - policy_json_or_preset: &str, - sandbox_policy_cwd: &Path, + permission_profile: &PermissionProfile, + permission_profile_cwd: &Path, codex_home: &Path, command: Vec, cwd: &Path, @@ -33,8 +33,8 @@ pub async fn spawn_windows_sandbox_session_legacy( use_private_desktop: bool, ) -> Result { backends::legacy::spawn_windows_sandbox_session_legacy( - policy_json_or_preset, - sandbox_policy_cwd, + permission_profile, + permission_profile_cwd, codex_home, command, cwd, @@ -87,44 +87,6 @@ pub async fn spawn_windows_sandbox_session_elevated_for_permission_profile( .await } -#[allow(clippy::too_many_arguments)] -pub async fn spawn_windows_sandbox_session_elevated( - policy_json_or_preset: &str, - sandbox_policy_cwd: &Path, - codex_home: &Path, - command: Vec, - cwd: &Path, - env_map: HashMap, - timeout_ms: Option, - read_roots_override: Option<&[PathBuf]>, - read_roots_include_platform_defaults: bool, - write_roots_override: Option<&[PathBuf]>, - deny_read_paths_override: &[AbsolutePathBuf], - deny_write_paths_override: &[AbsolutePathBuf], - tty: bool, - stdin_open: bool, - use_private_desktop: bool, -) -> Result { - backends::elevated::spawn_windows_sandbox_session_elevated( - policy_json_or_preset, - sandbox_policy_cwd, - codex_home, - command, - cwd, - env_map, - timeout_ms, - read_roots_override, - read_roots_include_platform_defaults, - write_roots_override, - deny_read_paths_override, - deny_write_paths_override, - tty, - stdin_open, - use_private_desktop, - ) - .await -} - #[cfg(test)] pub(crate) use backends::windows_common::finish_driver_spawn; #[cfg(test)] 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 e2830ad8a1..b08d7a6b2f 100644 --- a/codex-rs/windows-sandbox-rs/src/unified_exec/tests.rs +++ b/codex-rs/windows-sandbox-rs/src/unified_exec/tests.rs @@ -5,6 +5,7 @@ use crate::ipc_framed::Message; use crate::ipc_framed::decode_bytes; use crate::ipc_framed::read_frame; use crate::run_windows_sandbox_capture; +use codex_protocol::models::PermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_pty::ProcessDriver; use pretty_assertions::assert_eq; @@ -149,8 +150,9 @@ fn legacy_non_tty_cmd_emits_output() { let cwd = sandbox_cwd(); let codex_home = sandbox_home("legacy-non-tty-cmd"); println!("cmd codex_home={}", codex_home.path().display()); + let permission_profile = PermissionProfile::workspace_write(); let spawned = spawn_windows_sandbox_session_legacy( - "workspace-write", + &permission_profile, cwd.as_path(), codex_home.path(), vec![ @@ -189,8 +191,9 @@ fn legacy_non_tty_cmd_rejects_deny_read_overrides() { let secret_path = AbsolutePathBuf::from_absolute_path(cwd.join("legacy-non-tty-deny-read-secret.env")) .expect("absolute deny-read fixture path"); + let permission_profile = PermissionProfile::workspace_write(); let err = spawn_windows_sandbox_session_legacy( - "workspace-write", + &permission_profile, cwd.as_path(), codex_home.path(), vec![ @@ -228,8 +231,9 @@ fn legacy_non_tty_powershell_emits_output() { let cwd = sandbox_cwd(); let codex_home = sandbox_home("legacy-non-tty-pwsh"); println!("pwsh codex_home={}", codex_home.path().display()); + let permission_profile = PermissionProfile::workspace_write(); let spawned = spawn_windows_sandbox_session_legacy( - "workspace-write", + &permission_profile, cwd.as_path(), codex_home.path(), vec![ @@ -413,8 +417,9 @@ fn legacy_capture_powershell_emits_output() { let cwd = sandbox_cwd(); let codex_home = sandbox_home("legacy-capture-pwsh"); println!("capture pwsh codex_home={}", codex_home.path().display()); + let permission_profile = PermissionProfile::workspace_write(); let result = run_windows_sandbox_capture( - "workspace-write", + &permission_profile, cwd.as_path(), codex_home.path(), vec![ @@ -452,8 +457,9 @@ fn legacy_tty_powershell_emits_output_and_accepts_input() { let cwd = sandbox_cwd(); let codex_home = sandbox_home("legacy-tty-pwsh"); println!("tty pwsh codex_home={}", codex_home.path().display()); + let permission_profile = PermissionProfile::workspace_write(); let spawned = spawn_windows_sandbox_session_legacy( - "workspace-write", + &permission_profile, cwd.as_path(), codex_home.path(), vec![ @@ -505,8 +511,9 @@ fn legacy_tty_cmd_emits_output_and_accepts_input() { let cwd = sandbox_cwd(); let codex_home = sandbox_home("legacy-tty-cmd"); println!("tty cmd codex_home={}", codex_home.path().display()); + let permission_profile = PermissionProfile::workspace_write(); let spawned = spawn_windows_sandbox_session_legacy( - "workspace-write", + &permission_profile, cwd.as_path(), codex_home.path(), vec![ @@ -558,8 +565,9 @@ fn legacy_tty_cmd_default_desktop_emits_output_and_accepts_input() { "tty cmd default desktop codex_home={}", codex_home.path().display() ); + let permission_profile = PermissionProfile::workspace_write(); let spawned = spawn_windows_sandbox_session_legacy( - "workspace-write", + &permission_profile, cwd.as_path(), codex_home.path(), vec![ From 46391f7efa0aa06a8074f47bc01d2651d95889b0 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Tue, 26 May 2026 15:49:37 -0700 Subject: [PATCH 05/87] [codex] remove plain image wrapper spans (#24652) ## Why Remote image submissions currently wrap native `input_image` spans in literal `` and `` text spans. Those extra prompt tokens add structure without providing label or routing information. ## What Changed - Serialize `UserInput::Image` directly as an `input_image` content span. - Preserve named local-image framing and legacy wrapper parsing for labeled attachments and existing histories. - Update existing request-shape expectations for drag-and-drop images, model switching, and compaction. ## Validation - `just test -p codex-protocol` - Focused `codex-core` run covering `drag_drop_image_persists_rollout_request_shape`, `model_change_from_image_to_text_strips_prior_image_content`, and `snapshot_request_shape_pre_turn_compaction_including_incoming_user_message` ## Notes - A broader `just test -p codex-core` run was attempted; the affected tests passed, while the overall run failed in unrelated CLI, MCP, and tooling tests plus a `thread_manager` timeout. --- codex-rs/core/tests/suite/image_rollout.rs | 6 --- codex-rs/core/tests/suite/model_switching.rs | 13 ----- ..._compaction_including_incoming_shapes.snap | 8 ++- codex-rs/protocol/src/models.rs | 54 +++++-------------- 4 files changed, 16 insertions(+), 65 deletions(-) diff --git a/codex-rs/core/tests/suite/image_rollout.rs b/codex-rs/core/tests/suite/image_rollout.rs index 67a79d603f..87eb03079e 100644 --- a/codex-rs/core/tests/suite/image_rollout.rs +++ b/codex-rs/core/tests/suite/image_rollout.rs @@ -257,16 +257,10 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()> id: None, role: "user".to_string(), content: vec![ - ContentItem::InputText { - text: codex_protocol::models::image_open_tag_text(), - }, ContentItem::InputImage { image_url, detail: Some(DEFAULT_IMAGE_DETAIL), }, - ContentItem::InputText { - text: codex_protocol::models::image_close_tag_text(), - }, ContentItem::InputText { text: "dropped image".to_string(), }, diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index fd237a8d5c..3c4dd40b32 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -555,19 +555,6 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result< .any(|text| text == "image content omitted because you do not support image input"), "second request should include the image-omitted placeholder text" ); - assert!( - second_user_texts - .iter() - .any(|text| text == &codex_protocol::models::image_open_tag_text()), - "second request should preserve the image open tag text" - ); - assert!( - second_user_texts - .iter() - .any(|text| text == &codex_protocol::models::image_close_tag_text()), - "second request should preserve the image close tag text" - ); - Ok(()) } diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap index 2edd63d3d2..679081353e 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap @@ -19,8 +19,6 @@ Scenario: Pre-turn auto-compaction with a context override emits the context dif 02:message/user:\nPRE_TURN_SUMMARY 03:message/developer: 04:message/user: -05:message/user[4]: - [01] - [02] - [03] - [04] USER_THREE +05:message/user[2]: + [01] + [02] USER_THREE diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 6112ea8a6c..85c5a5d704 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -1238,18 +1238,10 @@ impl From> for ResponseInputItem { UserInput::Image { image_url, detail } => { image_index += 1; let detail = detail.unwrap_or(DEFAULT_IMAGE_DETAIL); - vec![ - ContentItem::InputText { - text: image_open_tag_text(), - }, - ContentItem::InputImage { - image_url, - detail: Some(detail), - }, - ContentItem::InputText { - text: image_close_tag_text(), - }, - ] + vec![ContentItem::InputImage { + image_url, + detail: Some(detail), + }] } UserInput::LocalImage { path, detail } => { image_index += 1; @@ -2622,7 +2614,7 @@ mod tests { } #[test] - fn wraps_image_user_input_with_tags() -> Result<()> { + fn serializes_image_user_input_without_tags() -> Result<()> { let image_url = "data:image/png;base64,abc".to_string(); let item = ResponseInputItem::from(vec![UserInput::Image { @@ -2632,18 +2624,10 @@ mod tests { match item { ResponseInputItem::Message { content, .. } => { - let expected = vec![ - ContentItem::InputText { - text: image_open_tag_text(), - }, - ContentItem::InputImage { - image_url, - detail: Some(DEFAULT_IMAGE_DETAIL), - }, - ContentItem::InputText { - text: image_close_tag_text(), - }, - ]; + let expected = vec![ContentItem::InputImage { + image_url, + detail: Some(DEFAULT_IMAGE_DETAIL), + }]; assert_eq!(content, expected); } other => panic!("expected message response but got {other:?}"), @@ -2664,7 +2648,7 @@ mod tests { match item { ResponseInputItem::Message { content, .. } => { assert_eq!( - content.get(1), + content.first(), Some(&ContentItem::InputImage { image_url, detail: Some(ImageDetail::Original), @@ -2862,35 +2846,23 @@ mod tests { ResponseInputItem::Message { content, .. } => { assert_eq!( content.first(), - Some(&ContentItem::InputText { - text: image_open_tag_text(), - }) - ); - assert_eq!( - content.get(1), Some(&ContentItem::InputImage { image_url, detail: Some(DEFAULT_IMAGE_DETAIL), }) ); assert_eq!( - content.get(2), - Some(&ContentItem::InputText { - text: image_close_tag_text(), - }) - ); - assert_eq!( - content.get(3), + content.get(1), Some(&ContentItem::InputText { text: local_image_open_tag_text(/*label_number*/ 2), }) ); assert!(matches!( - content.get(4), + content.get(2), Some(ContentItem::InputImage { .. }) )); assert_eq!( - content.get(5), + content.get(3), Some(&ContentItem::InputText { text: image_close_tag_text(), }) From 9826581e7b2c130bc389f4490cd746851eb79af0 Mon Sep 17 00:00:00 2001 From: iceweasel-oai Date: Tue, 26 May 2026 15:59:25 -0700 Subject: [PATCH 06/87] Attach Windows sandbox log to feedback reports (#24623) ## Why Windows sandbox diagnostics are currently hard to recover from `/feedback` even though they are often the most useful artifact when debugging sandbox behavior. Now that sandbox logging uses daily rolling files, feedback can safely include the current day's sandbox log without uploading the old ever-growing legacy `sandbox.log`. ## What changed - Add a `codex-windows-sandbox` helper that resolves the current daily sandbox log from `codex_home`. - When feedback is submitted with logs enabled on Windows, app-server attaches today's sandbox log if it exists. - Upload the attachment under the stable filename `windows-sandbox.log`, independent of the dated on-disk filename. - Keep existing raw `extra_log_files` behavior unchanged for rollout and desktop log attachments. ## Verification - `cargo fmt -p codex-app-server -p codex-windows-sandbox` - `cargo test -p codex-windows-sandbox current_log_file_path_for_codex_home_uses_sandbox_dir` - `cargo test -p codex-app-server windows_sandbox_log_attachment_uses_current_log` - Manual CLI/TUI `/feedback` test confirmed Sentry received `windows-sandbox.log`. --- codex-rs/Cargo.lock | 1 + codex-rs/app-server/Cargo.toml | 3 ++ .../request_processors/feedback_processor.rs | 51 +++++++++++++++++++ codex-rs/feedback/src/lib.rs | 2 + codex-rs/tui/src/bottom_pane/feedback_view.rs | 33 ++++++++++++ ...ck_upload_consent_lists_doctor_report.snap | 2 +- ...sts_windows_sandbox_log_when_included.snap | 13 +++++ codex-rs/tui/src/chatwidget.rs | 7 +++ ..._tests__feedback_upload_consent_popup.snap | 2 + .../chatwidget/tests/popups_and_settings.rs | 2 + codex-rs/windows-sandbox-rs/src/lib.rs | 2 + codex-rs/windows-sandbox-rs/src/logging.rs | 14 +++++ 12 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_view__tests__feedback_upload_consent_lists_windows_sandbox_log_when_included.snap diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 044146d9de..1b1bc0e59a 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1929,6 +1929,7 @@ dependencies = [ "codex-utils-json-to-toml", "codex-utils-pty", "codex-web-search-extension", + "codex-windows-sandbox", "core_test_support", "flate2", "futures", diff --git a/codex-rs/app-server/Cargo.toml b/codex-rs/app-server/Cargo.toml index 17ecd7dc3f..09e3197b77 100644 --- a/codex-rs/app-server/Cargo.toml +++ b/codex-rs/app-server/Cargo.toml @@ -94,6 +94,9 @@ tracing = { workspace = true, features = ["log"] } tracing-subscriber = { workspace = true, features = ["env-filter", "fmt", "json"] } uuid = { workspace = true, features = ["serde", "v7"] } +[target.'cfg(windows)'.dependencies] +codex-windows-sandbox = { workspace = true } + [dev-dependencies] app_test_support = { workspace = true } axum = { workspace = true, default-features = false, features = [ diff --git a/codex-rs/app-server/src/request_processors/feedback_processor.rs b/codex-rs/app-server/src/request_processors/feedback_processor.rs index 18f519d3c0..73da67f735 100644 --- a/codex-rs/app-server/src/request_processors/feedback_processor.rs +++ b/codex-rs/app-server/src/request_processors/feedback_processor.rs @@ -1,4 +1,6 @@ use super::*; +#[cfg(target_os = "windows")] +use codex_feedback::WINDOWS_SANDBOX_LOG_ATTACHMENT_FILENAME; #[derive(Clone)] pub(crate) struct FeedbackRequestProcessor { @@ -186,6 +188,12 @@ impl FeedbackRequestProcessor { )), }); } + if let Some(sandbox_log_attachment) = + windows_sandbox_log_attachment(&self.config.codex_home) + && seen_attachment_paths.insert(sandbox_log_attachment.path.clone()) + { + attachment_paths.push(sandbox_log_attachment); + } } if let Some(extra_log_files) = extra_log_files { for extra_log_file in extra_log_files { @@ -264,3 +272,46 @@ impl FeedbackRequestProcessor { fn auto_review_rollout_filename(thread_id: ThreadId) -> String { format!("auto-review-rollout-{thread_id}.jsonl") } + +#[cfg(target_os = "windows")] +fn windows_sandbox_log_attachment(codex_home: &Path) -> Option { + let sandbox_log_path = codex_windows_sandbox::current_log_file_path_for_codex_home(codex_home); + sandbox_log_path + .is_file() + .then_some(FeedbackAttachmentPath { + path: sandbox_log_path, + attachment_filename_override: Some(WINDOWS_SANDBOX_LOG_ATTACHMENT_FILENAME.to_string()), + }) +} + +#[cfg(not(target_os = "windows"))] +fn windows_sandbox_log_attachment(_codex_home: &Path) -> Option { + None +} + +#[cfg(all(test, target_os = "windows"))] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn windows_sandbox_log_attachment_uses_current_log() { + let codex_home = tempfile::tempdir().expect("create tempdir"); + let sandbox_dir = codex_windows_sandbox::sandbox_dir(codex_home.path()); + std::fs::create_dir_all(&sandbox_dir).expect("create sandbox dir"); + let sandbox_log_path = + codex_windows_sandbox::current_log_file_path_for_codex_home(codex_home.path()); + std::fs::write(&sandbox_log_path, "sandbox log").expect("write sandbox log"); + + let attachment = windows_sandbox_log_attachment(codex_home.path()) + .map(|attachment| (attachment.path, attachment.attachment_filename_override)); + + assert_eq!( + attachment, + Some(( + sandbox_log_path, + Some(WINDOWS_SANDBOX_LOG_ATTACHMENT_FILENAME.to_string()) + )) + ); + } +} diff --git a/codex-rs/feedback/src/lib.rs b/codex-rs/feedback/src/lib.rs index 9a8caf489a..7c27d2b3b0 100644 --- a/codex-rs/feedback/src/lib.rs +++ b/codex-rs/feedback/src/lib.rs @@ -29,6 +29,8 @@ pub use feedback_diagnostics::FeedbackDiagnostics; /// Filename used for the redacted `codex doctor --json` feedback attachment. pub const DOCTOR_REPORT_ATTACHMENT_FILENAME: &str = "codex-doctor-report.json"; +/// Filename used for the Windows sandbox log feedback attachment. +pub const WINDOWS_SANDBOX_LOG_ATTACHMENT_FILENAME: &str = "windows-sandbox.log"; const DEFAULT_MAX_BYTES: usize = 4 * 1024 * 1024; // 4 MiB const SENTRY_DSN: &str = "https://ae32ed50620d7a7792c1ce5df38b3e3e@o33249.ingest.us.sentry.io/4510195390611458"; diff --git a/codex-rs/tui/src/bottom_pane/feedback_view.rs b/codex-rs/tui/src/bottom_pane/feedback_view.rs index 04bcbd0b00..2770598fc4 100644 --- a/codex-rs/tui/src/bottom_pane/feedback_view.rs +++ b/codex-rs/tui/src/bottom_pane/feedback_view.rs @@ -1,6 +1,7 @@ use codex_feedback::DOCTOR_REPORT_ATTACHMENT_FILENAME; use codex_feedback::FEEDBACK_DIAGNOSTICS_ATTACHMENT_FILENAME; use codex_feedback::FeedbackDiagnostics; +use codex_feedback::WINDOWS_SANDBOX_LOG_ATTACHMENT_FILENAME; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyModifiers; @@ -472,6 +473,7 @@ pub(crate) fn feedback_upload_consent_params( category: FeedbackCategory, rollout_path: Option, auto_review_rollout_filename: Option, + include_windows_sandbox_log: bool, feedback_diagnostics: &FeedbackDiagnostics, ) -> super::SelectionViewParams { use super::popup_consts::standard_popup_hint_line; @@ -509,6 +511,15 @@ pub(crate) fn feedback_upload_consent_params( ]) .into(), ]; + if include_windows_sandbox_log { + header_lines.push( + Line::from(vec![ + " • ".into(), + WINDOWS_SANDBOX_LOG_ATTACHMENT_FILENAME.into(), + ]) + .into(), + ); + } if let Some(path) = rollout_path.as_deref() && let Some(name) = path.file_name().map(|s| s.to_string_lossy().to_string()) { @@ -696,6 +707,7 @@ mod tests { FeedbackCategory::Bug, Some(std::path::PathBuf::from("rollout.jsonl")), Some("auto-review-rollout.jsonl".to_string()), + /*include_windows_sandbox_log*/ false, &FeedbackDiagnostics::default(), ); @@ -704,6 +716,27 @@ mod tests { insta::assert_snapshot!("feedback_upload_consent_lists_doctor_report", rendered); } + #[test] + fn feedback_upload_consent_lists_windows_sandbox_log_when_included() { + let (tx_raw, _rx) = tokio::sync::mpsc::unbounded_channel::(); + let tx = AppEventSender::new(tx_raw); + let params = feedback_upload_consent_params( + tx, + FeedbackCategory::Bug, + Some(std::path::PathBuf::from("rollout.jsonl")), + Some("auto-review-rollout.jsonl".to_string()), + /*include_windows_sandbox_log*/ true, + &FeedbackDiagnostics::default(), + ); + + let rendered = render_renderable(params.header.as_ref(), /*width*/ 60); + + insta::assert_snapshot!( + "feedback_upload_consent_lists_windows_sandbox_log_when_included", + rendered + ); + } + #[test] fn submit_feedback_emits_submit_event_with_trimmed_note() { let (tx_raw, mut rx) = tokio::sync::mpsc::unbounded_channel::(); diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_view__tests__feedback_upload_consent_lists_doctor_report.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_view__tests__feedback_upload_consent_lists_doctor_report.snap index 6cca54542d..e91d736ecd 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_view__tests__feedback_upload_consent_lists_doctor_report.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_view__tests__feedback_upload_consent_lists_doctor_report.snap @@ -1,6 +1,6 @@ --- source: tui/src/bottom_pane/feedback_view.rs -assertion_line: 704 +assertion_line: 716 expression: rendered --- Upload logs? diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_view__tests__feedback_upload_consent_lists_windows_sandbox_log_when_included.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_view__tests__feedback_upload_consent_lists_windows_sandbox_log_when_included.snap new file mode 100644 index 0000000000..741505d5f5 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__feedback_view__tests__feedback_upload_consent_lists_windows_sandbox_log_when_included.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/feedback_view.rs +assertion_line: 734 +expression: rendered +--- +Upload logs? + +The following files will be sent: + • codex-logs.log + • codex-doctor-report.json + • windows-sandbox.log + • rollout.jsonl + • auto-review-rollout.jsonl diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index b9c5101187..16ac980f35 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -977,12 +977,19 @@ impl ChatWidget { pub(crate) fn open_feedback_consent(&mut self, category: crate::app_event::FeedbackCategory) { let snapshot = self.feedback.snapshot(self.thread_id); + #[cfg(target_os = "windows")] + let include_windows_sandbox_log = + codex_windows_sandbox::current_log_file_path_for_codex_home(&self.config.codex_home) + .is_file(); + #[cfg(not(target_os = "windows"))] + let include_windows_sandbox_log = false; let params = crate::bottom_pane::feedback_upload_consent_params( self.app_event_tx.clone(), category, self.current_rollout_path.clone(), self.thread_id .map(|thread_id| format!("auto-review-rollout-{thread_id}.jsonl")), + include_windows_sandbox_log, snapshot.feedback_diagnostics(), ); self.bottom_pane.show_selection_view(params); diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__feedback_upload_consent_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__feedback_upload_consent_popup.snap index 354e473a18..b860cf8fb3 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__feedback_upload_consent_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__feedback_upload_consent_popup.snap @@ -1,5 +1,6 @@ --- source: tui/src/chatwidget/tests/popups_and_settings.rs +assertion_line: 2562 expression: popup --- Upload logs? @@ -7,6 +8,7 @@ expression: popup The following files will be sent: • codex-logs.log • codex-doctor-report.json + • windows-sandbox.log • auto-review-rollout-thread-1.jsonl • codex-connectivity-diagnostics.txt diff --git a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs index 3a4bdd1329..55aadefb6d 100644 --- a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs +++ b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs @@ -2621,6 +2621,7 @@ async fn feedback_upload_consent_popup_snapshot() { crate::app_event::FeedbackCategory::Bug, chat.current_rollout_path.clone(), Some("auto-review-rollout-thread-1.jsonl".to_string()), + /*include_windows_sandbox_log*/ true, &codex_feedback::FeedbackDiagnostics::new(vec![codex_feedback::FeedbackDiagnostic { headline: "Proxy environment variables are set and may affect connectivity." .to_string(), @@ -2641,6 +2642,7 @@ async fn feedback_good_result_consent_popup_includes_connectivity_diagnostics_fi crate::app_event::FeedbackCategory::GoodResult, chat.current_rollout_path.clone(), Some("auto-review-rollout-thread-1.jsonl".to_string()), + /*include_windows_sandbox_log*/ false, &codex_feedback::FeedbackDiagnostics::new(vec![codex_feedback::FeedbackDiagnostic { headline: "Proxy environment variables are set and may affect connectivity." .to_string(), diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index 03d2393ed8..26e22dde07 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -177,6 +177,8 @@ pub use ipc_framed::write_frame; #[cfg(target_os = "windows")] pub use logging::current_log_file_path; #[cfg(target_os = "windows")] +pub use logging::current_log_file_path_for_codex_home; +#[cfg(target_os = "windows")] pub use logging::log_file_path_for_utc_date; #[cfg(target_os = "windows")] pub use logging::log_note; diff --git a/codex-rs/windows-sandbox-rs/src/logging.rs b/codex-rs/windows-sandbox-rs/src/logging.rs index f5e8399709..7108a1de36 100644 --- a/codex-rs/windows-sandbox-rs/src/logging.rs +++ b/codex-rs/windows-sandbox-rs/src/logging.rs @@ -43,6 +43,10 @@ pub fn current_log_file_path(base_dir: &Path) -> PathBuf { log_file_path_for_utc_date(base_dir, chrono::Utc::now().date_naive()) } +pub fn current_log_file_path_for_codex_home(codex_home: &Path) -> PathBuf { + current_log_file_path(&crate::sandbox_dir(codex_home)) +} + pub fn log_writer(base_dir: &Path) -> Option { if !base_dir.is_dir() { return None; @@ -142,4 +146,14 @@ mod tests { PathBuf::from("logs").join("sandbox.2026-05-21.log") ); } + + #[test] + fn current_log_file_path_for_codex_home_uses_sandbox_dir() { + let codex_home = Path::new("codex-home"); + + assert_eq!( + current_log_file_path_for_codex_home(codex_home), + current_log_file_path(&codex_home.join(".sandbox")) + ); + } } From dc4e54d06152b3bb91a4e9989299b094d20ad827 Mon Sep 17 00:00:00 2001 From: rhan-oai Date: Tue, 26 May 2026 16:24:33 -0700 Subject: [PATCH 07/87] Restore legacy image detail values (#24644) ## Why Older persisted rollouts can contain `input_image.detail` values of `auto` or `low` from before `ImageDetail` was narrowed to `high`/`original`. Current deserialization rejects those values, which can make resume skip later compacted checkpoints and reconstruct an oversized raw suffix before the next compaction attempt. Confirmed Sentry reports fixed by this compatibility path: - [CODEX-1H3F](https://openai.sentry.io/issues/7500642496/) - [CODEX-1H6N](https://openai.sentry.io/issues/7501025347/) - [CODEX-1JDP](https://openai.sentry.io/issues/7504549065/) - [CODEX-1HW6](https://openai.sentry.io/issues/7503407986/) ## Background [openai/codex#20693](https://github.com/openai/codex/pull/20693) added image-detail plumbing for app-server `UserInput` so input images could explicitly request `detail: original`. The Slack discussion behind that PR was about ScreenSpot / bridge evals where user input images were resized, while tool output images already had MCP/code-mode ways to request image detail. In review, the intended new API surface was narrowed to `high` and `original`: default to `high`, allow `original` when callers need unchanged image handling, and avoid encouraging new `auto` or `low` usage. That policy still makes sense for newly emitted values. The missing compatibility piece is persisted history. Older rollouts can already contain `auto` and `low`, and resume reconstructs typed history by deserializing those rollout records. Rejecting old values at that boundary causes valid compacted checkpoints to be skipped. This PR restores `auto` and `low` as real variants so old records deserialize and round-trip without being rewritten as `high`, while product paths can continue to default to `high` and avoid emitting `auto` for new behavior. ## What changed - Restored `ImageDetail::Auto` and `ImageDetail::Low` as first-class protocol values. - Preserved `auto`/`low` through rollout deserialization, MCP image metadata, code-mode image output, and schema/type generation. - Kept local image byte handling conservative: only `original` switches to original-resolution loading; `auto`/`low`/`high` continue through the resize-to-fit path while retaining their detail value. - Added regression coverage for enum round-tripping and code-mode `low` detail handling. ## Testing - `just write-app-server-schema` - `just test -p codex-protocol` - `just test -p codex-tools` - `just test -p codex-code-mode` - `just test -p codex-app-server-protocol` - `just test -p codex-core suite::rmcp_client::stdio_image_responses_preserve_original_detail_metadata` - `just test -p codex-core suite::code_mode::code_mode_can_use_mcp_image_result_with_image_helper` - Loaded broken rollouts on local fixed builds, and started/completed new turns. I also attempted `just test -p codex-core`; the local broad run did not finish green: 2559 tests run, 2467 passed, 55 flaky, 91 failed, 1 timed out. The failures were broad timeout/deadline failures across unrelated areas; targeted changed-path core tests above passed. --- .../schema/json/ClientRequest.json | 2 + .../schema/json/ServerNotification.json | 2 + .../codex_app_server_protocol.schemas.json | 2 + .../codex_app_server_protocol.v2.schemas.json | 2 + .../json/v2/ItemCompletedNotification.json | 2 + .../json/v2/ItemStartedNotification.json | 2 + .../RawResponseItemCompletedNotification.json | 2 + .../schema/json/v2/ReviewStartResponse.json | 2 + .../schema/json/v2/ThreadForkResponse.json | 2 + .../schema/json/v2/ThreadListResponse.json | 2 + .../json/v2/ThreadMetadataUpdateResponse.json | 2 + .../schema/json/v2/ThreadReadResponse.json | 2 + .../schema/json/v2/ThreadResumeParams.json | 2 + .../schema/json/v2/ThreadResumeResponse.json | 2 + .../json/v2/ThreadRollbackResponse.json | 2 + .../schema/json/v2/ThreadStartResponse.json | 2 + .../json/v2/ThreadStartedNotification.json | 2 + .../json/v2/ThreadUnarchiveResponse.json | 2 + .../json/v2/TurnCompletedNotification.json | 2 + .../schema/json/v2/TurnStartParams.json | 2 + .../schema/json/v2/TurnStartResponse.json | 2 + .../json/v2/TurnStartedNotification.json | 2 + .../schema/json/v2/TurnSteerParams.json | 2 + .../schema/typescript/ImageDetail.ts | 2 +- codex-rs/code-mode/src/description.rs | 2 +- codex-rs/code-mode/src/response.rs | 2 + codex-rs/code-mode/src/runtime/value.rs | 8 +++- codex-rs/code-mode/src/service.rs | 38 ++++++++++++++++++- .../src/tools/code_mode/response_adapter.rs | 2 + codex-rs/protocol/src/models.rs | 36 +++++++++++++++++- codex-rs/tools/src/image_detail.rs | 2 +- codex-rs/tools/src/image_detail_tests.rs | 12 +++++- 32 files changed, 140 insertions(+), 10 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 552defad1b..63e8fbde1a 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1085,6 +1085,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 90899cb152..2c290558da 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -2031,6 +2031,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], 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 ab19f3e770..28dc006b5e 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 @@ -10160,6 +10160,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], 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 b12b0eedd8..d1c59e9855 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 @@ -6640,6 +6640,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json index 87d36d2927..cea3dae7d2 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json @@ -287,6 +287,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json index d45fddf4ba..529667388e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json @@ -287,6 +287,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json index e69b342282..f82acbae64 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json @@ -165,6 +165,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json index 877eeb1278..df93fbbea5 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json @@ -424,6 +424,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 8c45f57d38..a9a2529fc5 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -529,6 +529,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json index a9a186228b..8ccceaaa7c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -450,6 +450,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json index a2a4834cb3..87af03ea62 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json @@ -450,6 +450,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json index 5d76f9b181..a6d86c3485 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -450,6 +450,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json index d4bdeda0ed..58b85d41a4 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -224,6 +224,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 8055ff58fc..b4fa181c88 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -529,6 +529,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json index d0e25a1eaa..4256d13ec4 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json @@ -450,6 +450,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index d456ccebc4..6ab655a8bc 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -529,6 +529,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json index dd7de34b82..91bc13a9c6 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -450,6 +450,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json index 5929510520..0b68bfa957 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -450,6 +450,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json index ddaab863f7..4856f73529 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json @@ -424,6 +424,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], 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 ffc130dcc9..17b69a698e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json @@ -123,6 +123,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json index a2836f7f03..c5e48ae6ce 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json @@ -424,6 +424,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json index 8c57890a3b..075699a34e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json @@ -424,6 +424,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnSteerParams.json b/codex-rs/app-server-protocol/schema/json/v2/TurnSteerParams.json index ef05b27673..6f5452de66 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnSteerParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnSteerParams.json @@ -44,6 +44,8 @@ }, "ImageDetail": { "enum": [ + "auto", + "low", "high", "original" ], diff --git a/codex-rs/app-server-protocol/schema/typescript/ImageDetail.ts b/codex-rs/app-server-protocol/schema/typescript/ImageDetail.ts index 5a62cc32f1..a48f07c088 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ImageDetail.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ImageDetail.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type ImageDetail = "high" | "original"; +export type ImageDetail = "auto" | "low" | "high" | "original"; diff --git a/codex-rs/code-mode/src/description.rs b/codex-rs/code-mode/src/description.rs index 18d3c4555c..0c2813e51a 100644 --- a/codex-rs/code-mode/src/description.rs +++ b/codex-rs/code-mode/src/description.rs @@ -24,7 +24,7 @@ const EXEC_DESCRIPTION_TEMPLATE: &str = r#"Run JavaScript code to orchestrate/co - Global helpers: - `exit()`: Immediately ends the current script successfully (like an early return from the top level). - `text(value: string | number | boolean | undefined | null)`: Appends a text item. Non-string values are stringified with `JSON.stringify(...)` when possible. -- `image(imageUrlOrItem: string | { image_url: string; detail?: "high" | "original" | null } | ImageContent, detail?: "high" | "original" | null)`: Appends an image item. `image_url` can be an HTTPS URL or a base64-encoded `data:` URL. To forward an MCP tool image, pass an individual `ImageContent` block from `result.content`, for example `image(result.content[0])`. MCP image blocks may request detail with `_meta: { "codex/imageDetail": "original" }`. When provided, the second `detail` argument overrides any detail embedded in the first argument. +- `image(imageUrlOrItem: string | { image_url: string; detail?: "auto" | "low" | "high" | "original" | null } | ImageContent, detail?: "auto" | "low" | "high" | "original" | null)`: Appends an image item. `image_url` can be an HTTPS URL or a base64-encoded `data:` URL. To forward an MCP tool image, pass an individual `ImageContent` block from `result.content`, for example `image(result.content[0])`. MCP image blocks may request detail with `_meta: { "codex/imageDetail": "original" }`. When provided, the second `detail` argument overrides any detail embedded in the first argument. - `store(key: string, value: any)`: stores a serializable value under a string key for later `exec` calls in the same session. - `load(key: string)`: returns the stored value for a string key, or `undefined` if it is missing. - `notify(value: string | number | boolean | undefined | null)`: immediately injects an extra `custom_tool_call_output` for the current `exec` call. Values are stringified like `text(...)`. diff --git a/codex-rs/code-mode/src/response.rs b/codex-rs/code-mode/src/response.rs index ae92639cc0..0ac3a03770 100644 --- a/codex-rs/code-mode/src/response.rs +++ b/codex-rs/code-mode/src/response.rs @@ -4,6 +4,8 @@ use serde::Serialize; #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum ImageDetail { + Auto, + Low, High, Original, } diff --git a/codex-rs/code-mode/src/runtime/value.rs b/codex-rs/code-mode/src/runtime/value.rs index 865b5a569d..8d76a832d3 100644 --- a/codex-rs/code-mode/src/runtime/value.rs +++ b/codex-rs/code-mode/src/runtime/value.rs @@ -71,10 +71,14 @@ pub(super) fn normalize_output_image( Some(detail) => { let normalized = detail.to_ascii_lowercase(); Some(match normalized.as_str() { + "auto" => ImageDetail::Auto, + "low" => ImageDetail::Low, "high" => ImageDetail::High, "original" => ImageDetail::Original, _ => { - return Err("image detail must be one of: high, original".to_string()); + return Err( + "image detail must be one of: auto, low, high, original".to_string() + ); } }) } @@ -156,7 +160,7 @@ fn parse_mcp_output_image( .and_then(JsonValue::as_object) .and_then(|meta| meta.get(CODEX_IMAGE_DETAIL_META_KEY)) .and_then(JsonValue::as_str) - .filter(|detail| matches!(*detail, "high" | "original")) + .filter(|detail| matches!(*detail, "auto" | "low" | "high" | "original")) .map(str::to_string); Ok((image_url, detail)) } diff --git a/codex-rs/code-mode/src/service.rs b/codex-rs/code-mode/src/service.rs index de4ed13e58..d153f1b416 100644 --- a/codex-rs/code-mode/src/service.rs +++ b/codex-rs/code-mode/src/service.rs @@ -1359,7 +1359,7 @@ image( } #[tokio::test] - async fn image_helper_rejects_unsupported_detail() { + async fn image_helper_accepts_low_detail() { let service = CodeModeService::new(); let response = service @@ -1369,6 +1369,38 @@ image({ image_url: "https://example.com/image.jpg", detail: "low", }); +"# + .to_string(), + yield_time_ms: None, + ..execute_request("") + }) + .await + .unwrap(); + + assert_eq!( + response, + RuntimeResponse::Result { + cell_id: "1".to_string(), + content_items: vec![FunctionCallOutputContentItem::InputImage { + image_url: "https://example.com/image.jpg".to_string(), + detail: Some(crate::ImageDetail::Low), + }], + error_text: None, + } + ); + } + + #[tokio::test] + async fn image_helper_rejects_unsupported_detail() { + let service = CodeModeService::new(); + + let response = service + .execute(ExecuteRequest { + source: r#" +image({ + image_url: "https://example.com/image.jpg", + detail: "medium", +}); "# .to_string(), yield_time_ms: None, @@ -1382,7 +1414,9 @@ image({ RuntimeResponse::Result { cell_id: "1".to_string(), content_items: Vec::new(), - error_text: Some("image detail must be one of: high, original".to_string()), + error_text: Some( + "image detail must be one of: auto, low, high, original".to_string() + ), } ); } diff --git a/codex-rs/core/src/tools/code_mode/response_adapter.rs b/codex-rs/core/src/tools/code_mode/response_adapter.rs index 133d6e2fc1..e20cf6a071 100644 --- a/codex-rs/core/src/tools/code_mode/response_adapter.rs +++ b/codex-rs/core/src/tools/code_mode/response_adapter.rs @@ -17,6 +17,8 @@ impl IntoProtocol for CodeModeImageDetail { fn into_protocol(self) -> ImageDetail { let value = self; match value { + CodeModeImageDetail::Auto => ImageDetail::Auto, + CodeModeImageDetail::Low => ImageDetail::Low, CodeModeImageDetail::High => ImageDetail::High, CodeModeImageDetail::Original => ImageDetail::Original, } diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 85c5a5d704..86603db1ef 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -725,6 +725,8 @@ pub enum ContentItem { #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "lowercase")] pub enum ImageDetail { + Auto, + Low, High, Original, } @@ -1077,7 +1079,7 @@ pub fn local_image_content_items_with_label_number( ) -> Vec { let mode = match detail { ImageDetail::Original => PromptImageMode::Original, - ImageDetail::High => PromptImageMode::ResizeToFit, + ImageDetail::Auto | ImageDetail::Low | ImageDetail::High => PromptImageMode::ResizeToFit, }; match load_for_prompt_bytes(path, file_bytes, mode) { @@ -1599,6 +1601,8 @@ fn convert_mcp_content_to_items( .and_then(|meta| meta.get(CODEX_IMAGE_DETAIL_META_KEY)) .and_then(serde_json::Value::as_str) .and_then(|detail| match detail { + "auto" => Some(ImageDetail::Auto), + "low" => Some(ImageDetail::Low), "high" => Some(ImageDetail::High), "original" => Some(ImageDetail::Original), _ => None, @@ -1674,6 +1678,36 @@ mod tests { ); } + #[test] + fn image_detail_roundtrips_all_wire_values() -> Result<()> { + assert_eq!( + serde_json::from_str::("\"auto\"")?, + ImageDetail::Auto + ); + assert_eq!( + serde_json::from_str::("\"low\"")?, + ImageDetail::Low + ); + assert_eq!(serde_json::to_string(&ImageDetail::Auto)?, "\"auto\""); + assert_eq!(serde_json::to_string(&ImageDetail::Low)?, "\"low\""); + + let content_item: ContentItem = serde_json::from_value(serde_json::json!({ + "type": "input_image", + "image_url": "data:image/png;base64,abc", + "detail": "auto", + }))?; + + assert_eq!( + content_item, + ContentItem::InputImage { + image_url: "data:image/png;base64,abc".to_string(), + detail: Some(ImageDetail::Auto), + } + ); + + Ok(()) + } + #[test] fn sandbox_permissions_helpers_match_documented_semantics() { let cases = [ diff --git a/codex-rs/tools/src/image_detail.rs b/codex-rs/tools/src/image_detail.rs index 145cda663c..37086f691d 100644 --- a/codex-rs/tools/src/image_detail.rs +++ b/codex-rs/tools/src/image_detail.rs @@ -16,7 +16,7 @@ pub fn normalize_output_image_detail( Some(ImageDetail::Original) } Some(ImageDetail::Original) | None => None, - Some(ImageDetail::High) => Some(ImageDetail::High), + Some(ImageDetail::Auto | ImageDetail::Low | ImageDetail::High) => detail, } } diff --git a/codex-rs/tools/src/image_detail_tests.rs b/codex-rs/tools/src/image_detail_tests.rs index 919537acf7..2e46b49d08 100644 --- a/codex-rs/tools/src/image_detail_tests.rs +++ b/codex-rs/tools/src/image_detail_tests.rs @@ -70,6 +70,14 @@ fn explicit_original_is_dropped_without_model_support() { fn explicit_non_original_detail_is_preserved() { let model_info = model_info(); + assert_eq!( + normalize_output_image_detail(&model_info, Some(ImageDetail::Auto)), + Some(ImageDetail::Auto) + ); + assert_eq!( + normalize_output_image_detail(&model_info, Some(ImageDetail::Low)), + Some(ImageDetail::Low) + ); assert_eq!( normalize_output_image_detail(&model_info, Some(ImageDetail::High)), Some(ImageDetail::High) @@ -88,7 +96,7 @@ fn sanitize_original_falls_back_to_high_without_support() { }, FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,BBB".to_string(), - detail: Some(ImageDetail::High), + detail: Some(ImageDetail::Low), }, ]; @@ -106,7 +114,7 @@ fn sanitize_original_falls_back_to_high_without_support() { }, FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,BBB".to_string(), - detail: Some(ImageDetail::High), + detail: Some(ImageDetail::Low), }, ] ); From 487521733bea0a0a37559cffb519a1f6fb6bb444 Mon Sep 17 00:00:00 2001 From: marksteinbrick-oai Date: Tue, 26 May 2026 16:38:46 -0700 Subject: [PATCH 08/87] [codex-analytics] add grouped session id to runtime events (#24655) ## Why - Runtime analytics events report `thread_id`, which identifies the individual thread emitting an event - They don't report `session_id`, which identifies the shared session for a root thread and its subagent threads - Emitting both identifiers allows analytics to group related activity ## What Changed - Adds `session_id` to relevant analytics events (thread_initalized, turn, turn_steer, compaction, guardian_review) - Tracks each thread's session ID in the analytics reducer so subsequent thread scoped events emit the same value - Carries the shared session ID through subagent initialization ## Verification - `just test -p codex-analytics` validates event payloads and subagent session grouping. - Focused `codex-app-server` tests validate session IDs for thread, turn, and steer events. - Focused `codex-core` tests validate root and subagent session ID propagation. --- .../analytics/src/analytics_client_tests.rs | 32 +++++++++++++++++++ codex-rs/analytics/src/events.rs | 8 +++++ codex-rs/analytics/src/facts.rs | 1 + codex-rs/analytics/src/reducer.rs | 15 +++++++-- .../app-server/tests/suite/v2/analytics.rs | 2 ++ .../app-server/tests/suite/v2/thread_fork.rs | 9 +++++- .../tests/suite/v2/thread_resume.rs | 9 +++++- .../app-server/tests/suite/v2/thread_start.rs | 9 +++++- .../app-server/tests/suite/v2/turn_start.rs | 1 + .../app-server/tests/suite/v2/turn_steer.rs | 1 + codex-rs/core/src/agent/control.rs | 1 + codex-rs/core/src/codex_delegate.rs | 1 + codex-rs/core/src/session/mod.rs | 3 ++ 13 files changed, 87 insertions(+), 5 deletions(-) diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index 26daadf5f5..fa64b46d7a 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -1213,6 +1213,7 @@ fn compaction_event_serializes_expected_shape() { completed_at: 106, duration_ms: Some(6543), }, + "session-thread-1".to_string(), sample_app_server_client_metadata(), sample_runtime_metadata(), Some(ThreadSource::User), @@ -1229,6 +1230,7 @@ fn compaction_event_serializes_expected_shape() { "event_type": "codex_compaction_event", "event_params": { "thread_id": "thread-1", + "session_id": "session-thread-1", "turn_id": "turn-1", "app_server_client": { "product_client_id": DEFAULT_ORIGINATOR, @@ -1307,6 +1309,7 @@ fn thread_initialized_event_serializes_expected_shape() { event_type: "codex_thread_initialized", event_params: ThreadInitializedEventParams { thread_id: "thread-0".to_string(), + session_id: "session-thread-0".to_string(), app_server_client: CodexAppServerClientMetadata { product_client_id: DEFAULT_ORIGINATOR.to_string(), client_name: Some("codex-tui".to_string()), @@ -1338,6 +1341,7 @@ fn thread_initialized_event_serializes_expected_shape() { "event_type": "codex_thread_initialized", "event_params": { "thread_id": "thread-0", + "session_id": "session-thread-0", "app_server_client": { "product_client_id": DEFAULT_ORIGINATOR, "client_name": "codex-tui", @@ -1605,6 +1609,7 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize let payload = serde_json::to_value(&events).expect("serialize events"); assert_eq!(payload.as_array().expect("events array").len(), 1); assert_eq!(payload[0]["event_type"], "codex_thread_initialized"); + assert_eq!(payload[0]["event_params"]["session_id"], "session-thread-1"); assert_eq!( payload[0]["event_params"]["app_server_client"]["product_client_id"], DEFAULT_ORIGINATOR @@ -1781,6 +1786,7 @@ async fn compaction_event_ingests_custom_fact() { let payload = serde_json::to_value(&events).expect("serialize events"); assert_eq!(payload.as_array().expect("events array").len(), 1); assert_eq!(payload[0]["event_type"], "codex_compaction_event"); + assert_eq!(payload[0]["event_params"]["session_id"], "session-thread-1"); assert_eq!(payload[0]["event_params"]["thread_id"], "thread-1"); assert_eq!(payload[0]["event_params"]["turn_id"], "turn-compact"); assert_eq!( @@ -1905,6 +1911,10 @@ async fn guardian_review_event_ingests_custom_fact_with_optional_target_item() { let payload = serde_json::to_value(&events).expect("serialize events"); assert_eq!(payload.as_array().expect("events array").len(), 1); assert_eq!(payload[0]["event_type"], "codex_guardian_review"); + assert_eq!( + payload[0]["event_params"]["session_id"], + "session-thread-guardian" + ); assert_eq!(payload[0]["event_params"]["thread_id"], "thread-guardian"); assert_eq!(payload[0]["event_params"]["turn_id"], "turn-guardian"); assert_eq!(payload[0]["event_params"]["review_id"], "review-guardian"); @@ -2397,6 +2407,7 @@ async fn item_review_summaries_do_not_cross_threads_with_reused_item_ids() { fn subagent_thread_started_review_serializes_expected_shape() { let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request( SubAgentThreadStartedInput { + session_id: "session-root".to_string(), thread_id: "thread-review".to_string(), parent_thread_id: None, product_client_id: "codex-tui".to_string(), @@ -2440,6 +2451,7 @@ fn subagent_thread_started_thread_spawn_serializes_parent_thread_id() { .expect("valid thread id"); let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request( SubAgentThreadStartedInput { + session_id: "session-root".to_string(), thread_id: "thread-spawn".to_string(), parent_thread_id: None, product_client_id: "codex-tui".to_string(), @@ -2459,18 +2471,21 @@ fn subagent_thread_started_thread_spawn_serializes_parent_thread_id() { )); let payload = serde_json::to_value(&event).expect("serialize thread spawn subagent event"); + assert_eq!(payload["event_params"]["thread_id"], "thread-spawn"); assert_eq!(payload["event_params"]["thread_source"], "subagent"); assert_eq!(payload["event_params"]["subagent_source"], "thread_spawn"); assert_eq!( payload["event_params"]["parent_thread_id"], "11111111-1111-1111-1111-111111111111" ); + assert_eq!(payload["event_params"]["session_id"], "session-root"); } #[test] fn subagent_thread_started_memory_consolidation_serializes_expected_shape() { let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request( SubAgentThreadStartedInput { + session_id: "session-root".to_string(), thread_id: "thread-memory".to_string(), parent_thread_id: None, product_client_id: "codex-tui".to_string(), @@ -2496,6 +2511,7 @@ fn subagent_thread_started_memory_consolidation_serializes_expected_shape() { fn subagent_thread_started_other_serializes_expected_shape() { let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request( SubAgentThreadStartedInput { + session_id: "session-root".to_string(), thread_id: "thread-guardian".to_string(), parent_thread_id: None, product_client_id: "codex-tui".to_string(), @@ -2517,6 +2533,7 @@ fn subagent_thread_started_other_serializes_expected_shape() { fn subagent_thread_started_other_serializes_explicit_parent_thread_id() { let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request( SubAgentThreadStartedInput { + session_id: "session-root".to_string(), thread_id: "thread-guardian".to_string(), parent_thread_id: Some("parent-thread-guardian".to_string()), product_client_id: "codex-tui".to_string(), @@ -2546,6 +2563,7 @@ async fn subagent_thread_started_publishes_without_initialize() { .ingest( AnalyticsFact::Custom(CustomAnalyticsFact::SubAgentThreadStarted( SubAgentThreadStartedInput { + session_id: "session-root".to_string(), thread_id: "thread-review".to_string(), parent_thread_id: None, product_client_id: "codex-tui".to_string(), @@ -2619,6 +2637,7 @@ async fn subagent_thread_started_inherits_parent_connection_for_new_thread() { .ingest( AnalyticsFact::Custom(CustomAnalyticsFact::SubAgentThreadStarted( SubAgentThreadStartedInput { + session_id: "session-root".to_string(), thread_id: "thread-review".to_string(), parent_thread_id: None, product_client_id: "parent-client".to_string(), @@ -2666,6 +2685,8 @@ async fn subagent_thread_started_inherits_parent_connection_for_new_thread() { .await; let payload = serde_json::to_value(&events).expect("serialize events"); + assert_eq!(payload[0]["event_params"]["session_id"], "session-root"); + assert_eq!(payload[0]["event_params"]["thread_id"], "thread-review"); assert_eq!( payload[0]["event_params"]["app_server_client"]["product_client_id"], "parent-client" @@ -2686,6 +2707,7 @@ async fn subagent_tool_items_inherit_parent_connection_metadata() { .ingest( AnalyticsFact::Custom(CustomAnalyticsFact::SubAgentThreadStarted( SubAgentThreadStartedInput { + session_id: "session-root".to_string(), thread_id: "thread-subagent".to_string(), parent_thread_id: Some("thread-1".to_string()), product_client_id: "codex-tui".to_string(), @@ -3191,6 +3213,7 @@ fn turn_event_serializes_expected_shape() { event_type: "codex_turn_event", event_params: crate::events::CodexTurnEventParams { thread_id: "thread-2".to_string(), + session_id: "session-thread-2".to_string(), turn_id: "turn-2".to_string(), app_server_client: sample_app_server_client_metadata(), runtime: sample_runtime_metadata(), @@ -3241,6 +3264,7 @@ fn turn_event_serializes_expected_shape() { "event_type": "codex_turn_event", "event_params": { "thread_id": "thread-2", + "session_id": "session-thread-2", "turn_id": "turn-2", "submission_type": null, "app_server_client": { @@ -3342,6 +3366,10 @@ async fn accepted_turn_steer_emits_expected_event() { let payload = serde_json::to_value(&out[0]).expect("serialize turn steer event"); assert_eq!(payload["event_type"], json!("codex_turn_steer_event")); assert_eq!(payload["event_params"]["thread_id"], json!("thread-2")); + assert_eq!( + payload["event_params"]["session_id"], + json!("session-thread-2") + ); assert_eq!(payload["event_params"]["expected_turn_id"], json!("turn-2")); assert_eq!(payload["event_params"]["accepted_turn_id"], json!("turn-2")); assert_eq!(payload["event_params"]["num_input_images"], json!(1)); @@ -3559,6 +3587,10 @@ async fn turn_lifecycle_emits_turn_event() { let payload = serde_json::to_value(&out[0]).expect("serialize turn event"); assert_eq!(payload["event_type"], json!("codex_turn_event")); assert_eq!(payload["event_params"]["thread_id"], json!("thread-2")); + assert_eq!( + payload["event_params"]["session_id"], + json!("session-thread-2") + ); assert_eq!(payload["event_params"]["turn_id"], json!("turn-2")); assert_eq!( payload["event_params"]["app_server_client"], diff --git a/codex-rs/analytics/src/events.rs b/codex-rs/analytics/src/events.rs index 87cb165ac2..8791e5767a 100644 --- a/codex-rs/analytics/src/events.rs +++ b/codex-rs/analytics/src/events.rs @@ -147,6 +147,7 @@ pub(crate) struct CodexRuntimeMetadata { #[derive(Serialize)] pub(crate) struct ThreadInitializedEventParams { pub(crate) thread_id: String, + pub(crate) session_id: String, pub(crate) app_server_client: CodexAppServerClientMetadata, pub(crate) runtime: CodexRuntimeMetadata, pub(crate) model: String, @@ -420,6 +421,7 @@ impl GuardianReviewAnalyticsResult { #[derive(Serialize)] pub(crate) struct GuardianReviewEventPayload { + pub(crate) session_id: String, pub(crate) app_server_client: CodexAppServerClientMetadata, pub(crate) runtime: CodexRuntimeMetadata, #[serde(flatten)] @@ -738,6 +740,7 @@ pub(crate) struct CodexHookRunEventRequest { #[derive(Serialize)] pub(crate) struct CodexCompactionEventParams { pub(crate) thread_id: String, + pub(crate) session_id: String, pub(crate) turn_id: String, pub(crate) app_server_client: CodexAppServerClientMetadata, pub(crate) runtime: CodexRuntimeMetadata, @@ -767,6 +770,7 @@ pub(crate) struct CodexCompactionEventRequest { #[derive(Serialize)] pub(crate) struct CodexTurnEventParams { pub(crate) thread_id: String, + pub(crate) session_id: String, pub(crate) turn_id: String, // TODO(rhan-oai): Populate once queued/default submission type is plumbed from // the turn/start callsites instead of always being reported as None. @@ -821,6 +825,7 @@ pub(crate) struct CodexTurnEventRequest { #[derive(Serialize)] pub(crate) struct CodexTurnSteerEventParams { pub(crate) thread_id: String, + pub(crate) session_id: String, pub(crate) expected_turn_id: Option, pub(crate) accepted_turn_id: Option, pub(crate) app_server_client: CodexAppServerClientMetadata, @@ -926,6 +931,7 @@ pub(crate) fn codex_plugin_metadata(plugin: PluginTelemetryMetadata) -> CodexPlu pub(crate) fn codex_compaction_event_params( input: CodexCompactionEvent, + session_id: String, app_server_client: CodexAppServerClientMetadata, runtime: CodexRuntimeMetadata, thread_source: Option, @@ -934,6 +940,7 @@ pub(crate) fn codex_compaction_event_params( ) -> CodexCompactionEventParams { CodexCompactionEventParams { thread_id: input.thread_id, + session_id, turn_id: input.turn_id, app_server_client, runtime, @@ -1026,6 +1033,7 @@ pub(crate) fn subagent_thread_started_event_request( ) -> ThreadInitializedEvent { let event_params = ThreadInitializedEventParams { thread_id: input.thread_id, + session_id: input.session_id, app_server_client: CodexAppServerClientMetadata { product_client_id: input.product_client_id, client_name: Some(input.client_name), diff --git a/codex-rs/analytics/src/facts.rs b/codex-rs/analytics/src/facts.rs index 56bd0a5d2c..9a85276712 100644 --- a/codex-rs/analytics/src/facts.rs +++ b/codex-rs/analytics/src/facts.rs @@ -199,6 +199,7 @@ pub struct AppInvocation { #[derive(Clone)] pub struct SubAgentThreadStartedInput { + pub session_id: String, pub thread_id: String, pub parent_thread_id: Option, pub product_client_id: String, diff --git a/codex-rs/analytics/src/reducer.rs b/codex-rs/analytics/src/reducer.rs index d072720d10..f2f036b8f4 100644 --- a/codex-rs/analytics/src/reducer.rs +++ b/codex-rs/analytics/src/reducer.rs @@ -255,6 +255,7 @@ struct ItemReviewSummary { #[derive(Clone)] struct ThreadMetadataState { + session_id: String, thread_source: Option, initialization_mode: ThreadInitializationMode, subagent_source: Option, @@ -263,6 +264,7 @@ struct ThreadMetadataState { impl ThreadMetadataState { fn from_thread_metadata( + session_id: String, session_source: &SessionSource, thread_source: Option, initialization_mode: ThreadInitializationMode, @@ -281,6 +283,7 @@ impl ThreadMetadataState { | SessionSource::Unknown => (None, None), }; Self { + session_id, thread_source, initialization_mode, subagent_source, @@ -525,6 +528,7 @@ impl AnalyticsReducer { thread_state .metadata .get_or_insert_with(|| ThreadMetadataState { + session_id: input.session_id.clone(), thread_source: Some(ThreadSource::Subagent), initialization_mode: ThreadInitializationMode::New, subagent_source: Some(subagent_source_name(&input.subagent_source)), @@ -543,8 +547,8 @@ impl AnalyticsReducer { input: GuardianReviewEventParams, out: &mut Vec, ) { - let Some(connection_state) = - self.thread_connection_or_warn(AnalyticsDropSite::guardian(&input)) + let Some((connection_state, thread_metadata)) = + self.thread_context_or_warn(AnalyticsDropSite::guardian(&input)) else { return; }; @@ -552,6 +556,7 @@ impl AnalyticsReducer { GuardianReviewEventRequest { event_type: "codex_guardian_review", event_params: GuardianReviewEventPayload { + session_id: thread_metadata.session_id.clone(), app_server_client: connection_state.app_server_client.clone(), runtime: connection_state.runtime.clone(), guardian_review: input, @@ -1231,11 +1236,13 @@ impl AnalyticsReducer { out: &mut Vec, ) { let session_source: SessionSource = thread.source.into(); + let session_id = thread.session_id; let thread_id = thread.id; let Some(connection_state) = self.connections.get(&connection_id) else { return; }; let thread_metadata = ThreadMetadataState::from_thread_metadata( + session_id.clone(), &session_source, thread.thread_source.map(Into::into), initialization_mode, @@ -1252,6 +1259,7 @@ impl AnalyticsReducer { event_type: "codex_thread_initialized", event_params: ThreadInitializedEventParams { thread_id, + session_id, app_server_client: connection_state.app_server_client.clone(), runtime: connection_state.runtime.clone(), model, @@ -1277,6 +1285,7 @@ impl AnalyticsReducer { event_type: "codex_compaction_event", event_params: codex_compaction_event_params( input, + thread_metadata.session_id.clone(), connection_state.app_server_client.clone(), connection_state.runtime.clone(), thread_metadata.thread_source, @@ -1379,6 +1388,7 @@ impl AnalyticsReducer { event_type: "codex_turn_steer_event", event_params: CodexTurnSteerEventParams { thread_id: pending_request.thread_id, + session_id: thread_metadata.session_id.clone(), expected_turn_id: Some(pending_request.expected_turn_id), accepted_turn_id, app_server_client: connection_state.app_server_client.clone(), @@ -2447,6 +2457,7 @@ fn codex_turn_event_params( let token_usage = turn_state.token_usage.clone(); CodexTurnEventParams { thread_id, + session_id: thread_metadata.session_id.clone(), turn_id, app_server_client, runtime, diff --git a/codex-rs/app-server/tests/suite/v2/analytics.rs b/codex-rs/app-server/tests/suite/v2/analytics.rs index c6f95af95d..8e8f2a7557 100644 --- a/codex-rs/app-server/tests/suite/v2/analytics.rs +++ b/codex-rs/app-server/tests/suite/v2/analytics.rs @@ -168,11 +168,13 @@ pub(crate) fn thread_initialized_event(payload: &Value) -> Result<&Value> { pub(crate) fn assert_basic_thread_initialized_event( event: &Value, thread_id: &str, + session_id: &str, expected_model: &str, initialization_mode: &str, expected_thread_source: &str, ) { assert_eq!(event["event_params"]["thread_id"], thread_id); + assert_eq!(event["event_params"]["session_id"], session_id); assert_eq!( event["event_params"]["app_server_client"]["product_client_id"], DEFAULT_CLIENT_NAME diff --git a/codex-rs/app-server/tests/suite/v2/thread_fork.rs b/codex-rs/app-server/tests/suite/v2/thread_fork.rs index cc89dd71cb..287afe1016 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_fork.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_fork.rs @@ -405,7 +405,14 @@ async fn thread_fork_tracks_thread_initialized_analytics() -> Result<()> { let payload = wait_for_analytics_payload(&server, DEFAULT_READ_TIMEOUT).await?; let event = thread_initialized_event(&payload)?; - assert_basic_thread_initialized_event(event, &thread.id, "mock-model", "forked", "user"); + assert_basic_thread_initialized_event( + event, + &thread.id, + &thread.session_id, + "mock-model", + "forked", + "user", + ); Ok(()) } diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index 8e60c90a01..bb2de2cfbb 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -422,7 +422,14 @@ async fn thread_resume_tracks_thread_initialized_analytics() -> Result<()> { let payload = wait_for_analytics_payload(&server, DEFAULT_READ_TIMEOUT).await?; let event = thread_initialized_event(&payload)?; - assert_basic_thread_initialized_event(event, &thread.id, "gpt-5.3-codex", "resumed", "user"); + assert_basic_thread_initialized_event( + event, + &thread.id, + &thread.session_id, + "gpt-5.3-codex", + "resumed", + "user", + ); assert_eq!(event["event_params"]["thread_source"], "user"); Ok(()) } diff --git a/codex-rs/app-server/tests/suite/v2/thread_start.rs b/codex-rs/app-server/tests/suite/v2/thread_start.rs index ecd8ad8e3d..6be6d838ba 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_start.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_start.rs @@ -439,7 +439,14 @@ async fn thread_start_tracks_thread_initialized_analytics() -> Result<()> { let payload = wait_for_analytics_payload(&server, DEFAULT_READ_TIMEOUT).await?; assert_eq!(payload["events"].as_array().expect("events array").len(), 1); let event = thread_initialized_event(&payload)?; - assert_basic_thread_initialized_event(event, &thread.id, "mock-model", "new", "user"); + assert_basic_thread_initialized_event( + event, + &thread.id, + &thread.session_id, + "mock-model", + "new", + "user", + ); Ok(()) } diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index 88596b4639..d0f1c13246 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -862,6 +862,7 @@ async fn turn_start_tracks_turn_event_analytics() -> Result<()> { let event = wait_for_analytics_event(&server, DEFAULT_READ_TIMEOUT, "codex_turn_event").await?; assert_eq!(event["event_params"]["thread_id"], thread.id); + assert_eq!(event["event_params"]["session_id"], thread.session_id); assert_eq!(event["event_params"]["turn_id"], turn.id); assert_eq!( event["event_params"]["app_server_client"]["product_client_id"], diff --git a/codex-rs/app-server/tests/suite/v2/turn_steer.rs b/codex-rs/app-server/tests/suite/v2/turn_steer.rs index 66ac33972b..0c4dcabe77 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_steer.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_steer.rs @@ -306,6 +306,7 @@ async fn turn_steer_returns_active_turn_id() -> Result<()> { let event = wait_for_analytics_event(&server, DEFAULT_READ_TIMEOUT, "codex_turn_steer_event").await?; assert_eq!(event["event_params"]["thread_id"], thread.id); + assert_eq!(event["event_params"]["session_id"], thread.session_id); assert_eq!(event["event_params"]["result"], "accepted"); assert_eq!(event["event_params"]["num_input_images"], 0); assert_eq!(event["event_params"]["expected_turn_id"], turn.id); diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index 4706a455ff..8f25074d76 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -317,6 +317,7 @@ impl AgentControl { .services .analytics_events_client, client_metadata, + new_thread.thread.codex.session.session_id(), new_thread.thread_id, /*parent_thread_id*/ None, thread_config, diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index dd88e9238f..2afbeb45c9 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -109,6 +109,7 @@ pub(crate) async fn run_codex_thread_interactive( emit_subagent_session_started( &parent_session.services.analytics_events_client, client_metadata, + codex.session.session_id(), codex.session.conversation_id, Some(parent_session.conversation_id), thread_config, diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 5ea30e5bb5..245047c925 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -72,6 +72,7 @@ use codex_network_proxy::normalize_host; use codex_otel::current_span_trace_id; use codex_otel::current_span_w3c_trace_context; use codex_otel::set_parent_from_w3c_trace_context; +use codex_protocol::SessionId; use codex_protocol::ThreadId; use codex_protocol::approvals::ElicitationRequestEvent; use codex_protocol::approvals::ExecPolicyAmendment; @@ -3302,6 +3303,7 @@ impl Session { pub(crate) fn emit_subagent_session_started( analytics_events_client: &AnalyticsEventsClient, client_metadata: AppServerClientMetadata, + session_id: SessionId, thread_id: ThreadId, parent_thread_id: Option, thread_config: ThreadConfigSnapshot, @@ -3320,6 +3322,7 @@ pub(crate) fn emit_subagent_session_started( .unwrap_or_default() .as_secs(); analytics_events_client.track_subagent_thread_started(SubAgentThreadStartedInput { + session_id: session_id.to_string(), thread_id: thread_id.to_string(), parent_thread_id: parent_thread_id.map(|thread_id| thread_id.to_string()), product_client_id: client_name.clone(), From 0d37db4b2b10b56991566b3512b0677754733835 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Tue, 26 May 2026 17:19:02 -0700 Subject: [PATCH 09/87] [codex] Remove obsolete goal continuation turn marker (#24658) ## Why `continuation_turn_id` was introduced to distinguish synthetic goal continuation turns for the no-tool continuation suppression heuristic. #20523 removed that heuristic, but left the marker behind. It is still written and cleared without affecting any runtime decision. ## What Changed - Remove `GoalRuntimeState::continuation_turn_id`. - Remove the marker setter/clearer and their now-no-op start, finish, and abort call sites. ## Testing - Not run yet (deferred at request). --- codex-rs/core/src/goals.rs | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/codex-rs/core/src/goals.rs b/codex-rs/core/src/goals.rs index 6df681d4b0..c8e76f7d41 100644 --- a/codex-rs/core/src/goals.rs +++ b/codex-rs/core/src/goals.rs @@ -172,7 +172,6 @@ pub(crate) struct GoalRuntimeState { pub(crate) budget_limit_reported_goal_id: Mutex>, accounting_lock: Semaphore, accounting: Mutex, - continuation_turn_id: Mutex>, pub(crate) continuation_lock: Semaphore, } @@ -188,7 +187,6 @@ impl GoalRuntimeState { budget_limit_reported_goal_id: Mutex::new(None), accounting_lock: Semaphore::new(/*permits*/ 1), accounting: Mutex::new(GoalAccountingSnapshot::new()), - continuation_turn_id: Mutex::new(None), continuation_lock: Semaphore::new(/*permits*/ 1), } } @@ -900,20 +898,6 @@ impl Session { } } - async fn mark_thread_goal_continuation_turn_started(&self, turn_id: String) { - *self.goal_runtime.continuation_turn_id.lock().await = Some(turn_id); - } - - async fn take_thread_goal_continuation_turn(&self, turn_id: &str) -> bool { - let mut continuation_turn_id = self.goal_runtime.continuation_turn_id.lock().await; - if continuation_turn_id.as_deref() == Some(turn_id) { - *continuation_turn_id = None; - true - } else { - false - } - } - async fn clear_reserved_goal_continuation_turn(&self, turn_state: &Arc>) { let mut active_turn_guard = self.active_turn.lock().await; if let Some(active_turn) = active_turn_guard.as_ref() @@ -941,8 +925,6 @@ impl Session { tracing::warn!("failed to account thread goal progress at turn end: {err}"); } - self.take_thread_goal_continuation_turn(&turn_context.sub_id) - .await; if turn_completed { let mut accounting = self.goal_runtime.accounting.lock().await; if accounting @@ -957,8 +939,6 @@ impl Session { async fn handle_thread_goal_task_abort(&self, turn_context: Option<&TurnContext>) { if let Some(turn_context) = turn_context { - self.take_thread_goal_continuation_turn(&turn_context.sub_id) - .await; if let Err(err) = self .account_thread_goal_progress( turn_context, @@ -1373,8 +1353,6 @@ impl Session { .await; return; } - self.mark_thread_goal_continuation_turn_started(turn_context.sub_id.clone()) - .await; self.start_task(turn_context, Vec::new(), RegularTask::new()) .await; } From 9fe55d68e6e1d850642cd2fc5039ae76d1213dad Mon Sep 17 00:00:00 2001 From: sayan-oai Date: Tue, 26 May 2026 18:05:19 -0700 Subject: [PATCH 10/87] fix: dont compact standalone websearch schema (#24660) add new `parse_tool_input_schema_without_compaction` to bypass the existing compaction/trimming of client-provided tool schemas that are over 4k bytes. we want this for standalone web search to keep field guidance/metadata on certain fields; this keeps us closer to parity with existing hosted tool schema (which didnt go through this 4k byte filter). --- .../app-server/tests/suite/v2/web_search.rs | 9 ++++-- codex-rs/ext/extension-api/src/lib.rs | 1 + codex-rs/ext/web-search/src/tool.rs | 5 ++-- codex-rs/tools/src/json_schema.rs | 18 +++++++++++- codex-rs/tools/src/json_schema_tests.rs | 29 ++++++++++++++++--- codex-rs/tools/src/lib.rs | 1 + 6 files changed, 53 insertions(+), 10 deletions(-) diff --git a/codex-rs/app-server/tests/suite/v2/web_search.rs b/codex-rs/app-server/tests/suite/v2/web_search.rs index 58b2ff91a8..927f14ca9d 100644 --- a/codex-rs/app-server/tests/suite/v2/web_search.rs +++ b/codex-rs/app-server/tests/suite/v2/web_search.rs @@ -112,9 +112,12 @@ async fn standalone_web_search_round_trips_encrypted_output() -> Result<()> { assert_eq!(requests.len(), 2); let first_response = requests[0].body_json(); - assert!( - requests[0].tool_by_name("web", "run").is_some(), - "web.run should be sent to the model" + let web_run = requests[0] + .tool_by_name("web", "run") + .context("web.run should be sent to the model")?; + assert_eq!( + web_run.pointer("/parameters/properties/time/description"), + Some(&json!("Get time for the given UTC offsets.")) ); assert!( !has_hosted_web_search(&first_response), diff --git a/codex-rs/ext/extension-api/src/lib.rs b/codex-rs/ext/extension-api/src/lib.rs index a7c5c87e52..f3c3dc526e 100644 --- a/codex-rs/ext/extension-api/src/lib.rs +++ b/codex-rs/ext/extension-api/src/lib.rs @@ -21,6 +21,7 @@ pub use codex_tools::ToolOutput; pub use codex_tools::ToolPayload; pub use codex_tools::ToolSpec; pub use codex_tools::parse_tool_input_schema; +pub use codex_tools::parse_tool_input_schema_without_compaction; pub use contributors::ApprovalReviewContributor; pub use contributors::ConfigContributor; pub use contributors::ContextContributor; diff --git a/codex-rs/ext/web-search/src/tool.rs b/codex-rs/ext/web-search/src/tool.rs index 1e1db2deca..875476db9a 100644 --- a/codex-rs/ext/web-search/src/tool.rs +++ b/codex-rs/ext/web-search/src/tool.rs @@ -10,7 +10,7 @@ use codex_extension_api::ToolExecutor; use codex_extension_api::ToolName; use codex_extension_api::ToolOutput; use codex_extension_api::ToolSpec; -use codex_extension_api::parse_tool_input_schema; +use codex_extension_api::parse_tool_input_schema_without_compaction; use codex_login::default_client::build_reqwest_client; use codex_model_provider::SharedModelProvider; use codex_tools::ResponsesApiNamespace; @@ -40,7 +40,8 @@ impl ToolExecutor for WebSearchTool { } fn spec(&self) -> ToolSpec { - let parameters = match parse_tool_input_schema(&commands_schema()) { + // parse schema without compaction that removes field metadata/descriptions to match hosted tool definition + let parameters = match parse_tool_input_schema_without_compaction(&commands_schema()) { Ok(parameters) => parameters, Err(err) => panic!("search command schema should parse: {err}"), }; diff --git a/codex-rs/tools/src/json_schema.rs b/codex-rs/tools/src/json_schema.rs index 02936d52d1..1b53ad9803 100644 --- a/codex-rs/tools/src/json_schema.rs +++ b/codex-rs/tools/src/json_schema.rs @@ -157,10 +157,26 @@ impl From for AdditionalProperties { /// Parse the tool `input_schema` or return an error for invalid schema. pub fn parse_tool_input_schema(input_schema: &JsonValue) -> Result { + let mut input_schema = prepare_tool_input_schema(input_schema); + compact_large_tool_schema(&mut input_schema); + deserialize_tool_input_schema(input_schema) +} + +/// Parse a trusted tool `input_schema` without running large-schema compaction. +pub fn parse_tool_input_schema_without_compaction( + input_schema: &JsonValue, +) -> Result { + deserialize_tool_input_schema(prepare_tool_input_schema(input_schema)) +} + +fn prepare_tool_input_schema(input_schema: &JsonValue) -> JsonValue { let mut input_schema = input_schema.clone(); sanitize_json_schema(&mut input_schema); prune_unreachable_definitions(&mut input_schema); - compact_large_tool_schema(&mut input_schema); + input_schema +} + +fn deserialize_tool_input_schema(input_schema: JsonValue) -> Result { let schema: JsonSchema = serde_json::from_value(input_schema)?; if matches!( schema.schema_type, diff --git a/codex-rs/tools/src/json_schema_tests.rs b/codex-rs/tools/src/json_schema_tests.rs index 52b30138c1..9315d43c00 100644 --- a/codex-rs/tools/src/json_schema_tests.rs +++ b/codex-rs/tools/src/json_schema_tests.rs @@ -3,6 +3,7 @@ use super::JsonSchema; use super::JsonSchemaPrimitiveType; use super::JsonSchemaType; use super::parse_tool_input_schema; +use super::parse_tool_input_schema_without_compaction; use pretty_assertions::assert_eq; use std::collections::BTreeMap; @@ -791,8 +792,8 @@ fn many_string_properties(count: usize) -> serde_json::Map Date: Tue, 26 May 2026 19:00:40 -0700 Subject: [PATCH 11/87] fix(core): instrument stalled tool-listing handoff (#24667) ## Why When a turn needs a follow-up request after tool output is recorded, Codex can still appear stuck in `Thinking` before the next `/responses` request is opened. The existing local trace showed the last completed response and the absence of a new backend request, but it did not show whether the stall was in tool-router preparation or later request setup. Issue: N/A (internal incident investigation) ## What Changed Added trace spans around the pre-stream tool-router handoff in `core/src/session/turn.rs`, including the `built_tools` phase and the MCP manager read lock. Added per-server MCP tool-listing spans and trace breadcrumbs in `codex-mcp/src/connection_manager.rs` with startup snapshot / startup-complete state so a pending MCP client is visible in feedback logs instead of looking like a silent hang. ## Verification - `just fmt` - `just test -p codex-mcp` - `just test -p codex-core` (prior full rerun fails in this workspace on unrelated integration tests: code-mode output length expectations, one shell timeout formatting assertion, and shell snapshot timeouts; latest review-fix rerun compiled and passed 1160 tests before I stopped the abnormally slow unrelated suite) --- codex-rs/codex-mcp/src/connection_manager.rs | 33 ++++++++++++++++++-- codex-rs/core/src/session/turn.rs | 15 ++++++++- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/codex-rs/codex-mcp/src/connection_manager.rs b/codex-rs/codex-mcp/src/connection_manager.rs index 91ed8b1b64..00d8adca2e 100644 --- a/codex-rs/codex-mcp/src/connection_manager.rs +++ b/codex-rs/codex-mcp/src/connection_manager.rs @@ -64,7 +64,10 @@ use rmcp::model::Resource; use rmcp::model::ResourceTemplate; use tokio::task::JoinSet; use tokio_util::sync::CancellationToken; +use tracing::Instrument; use tracing::instrument; +use tracing::trace; +use tracing::trace_span; use tracing::warn; /// A thin wrapper around a set of running [`RmcpClient`] instances. @@ -378,13 +381,37 @@ impl McpConnectionManager { } /// Returns all tools with model-visible names normalized. - #[instrument(level = "trace", skip_all)] + #[instrument(level = "trace", skip_all, fields(mcp_server_count = self.clients.len()))] pub async fn list_all_tools(&self) -> Vec { let mut tools = Vec::new(); - for managed_client in self.clients.values() { - let Some(server_tools) = managed_client.listed_tools().await else { + for (server_name, managed_client) in &self.clients { + let has_startup_snapshot = managed_client.startup_snapshot.is_some(); + let startup_complete = managed_client + .startup_complete + .load(std::sync::atomic::Ordering::Acquire); + trace!( + server_name = %server_name, + has_startup_snapshot, + startup_complete, + "waiting for MCP server tools while building tool list" + ); + let Some(server_tools) = managed_client + .listed_tools() + .instrument(trace_span!( + "list_tools_for_server", + server_name = %server_name, + has_startup_snapshot, + startup_complete + )) + .await + else { continue; }; + trace!( + server_name = %server_name, + tool_count = server_tools.len(), + "listed MCP server tools while building tool list" + ); tools.extend( server_tools .into_iter() diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index 82fa35f108..2b9fa02529 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -1006,12 +1006,25 @@ async fn run_sampling_request( clippy::await_holding_invalid_type, reason = "tool router construction reads through the session-owned manager guard" )] +#[instrument(level = "trace", + skip_all, + fields( + turn_id = %turn_context.sub_id, + model = %turn_context.model_info.slug, + apps_enabled = turn_context.apps_enabled() + ) +)] pub(crate) async fn built_tools( sess: &Session, turn_context: &TurnContext, cancellation_token: &CancellationToken, ) -> CodexResult> { - let mcp_connection_manager = sess.services.mcp_connection_manager.read().await; + let mcp_connection_manager = sess + .services + .mcp_connection_manager + .read() + .instrument(trace_span!("read_mcp_connection_manager")) + .await; let has_mcp_servers = mcp_connection_manager.has_servers(); let all_mcp_tools = mcp_connection_manager .list_all_tools() From cca1e0ba1d3ea925d297dcbaca2f77c658fcf1bc Mon Sep 17 00:00:00 2001 From: "Adam Perry @ OpenAI" Date: Tue, 26 May 2026 20:59:47 -0700 Subject: [PATCH 12/87] Uprev Rust toolchain pins to 1.95.0 (#24684) ## Summary - Bump the workspace Rust toolchain from `1.93.0` to `1.95.0` across Cargo, Bazel, CI, release workflows, devcontainers, and the Codex environment config. - Refresh `MODULE.bazel.lock` so the Bazel Rust toolchain artifacts match the new version. - Leave purpose-specific toolchains unchanged, including the `argument-comment-lint` nightly and the upstream `rusty_v8` `1.91.0` build pin. - Includes fixes for new lints from `just fix` and a few codex-authored fixes for lints without a suggestion. --- .codex/environments/environment.toml | 2 +- .devcontainer/Dockerfile.secure | 2 +- .devcontainer/devcontainer.secure.json | 2 +- .github/workflows/cargo-deny.yml | 4 +- .../rust-ci-full-nextest-platform.yml | 4 +- .github/workflows/rust-ci-full.yml | 8 +- .github/workflows/rust-ci.yml | 6 +- .../rust-release-argument-comment-lint.yml | 2 +- .github/workflows/rust-release-windows.yml | 2 +- .github/workflows/rust-release.yml | 4 +- .github/workflows/rusty-v8-release.yml | 4 +- .github/workflows/v8-canary.yml | 10 +- MODULE.bazel | 2 +- MODULE.bazel.lock | 212 +++++++++--------- codex-rs/.github/workflows/cargo-audit.yml | 2 +- codex-rs/app-server-protocol/src/export.rs | 24 +- .../src/schema_fixtures.rs | 2 +- .../src/transport/remote_control/websocket.rs | 13 +- .../src/transport/websocket.rs | 8 +- codex-rs/app-server/src/outgoing_message.rs | 4 +- .../suite/v2/connection_handling_websocket.rs | 13 +- codex-rs/apply-patch/src/lib.rs | 2 +- codex-rs/cli/src/doctor.rs | 6 +- codex-rs/cli/src/mcp_cmd.rs | 6 +- codex-rs/code-mode/src/description.rs | 2 +- codex-rs/codex-client/src/custom_ca.rs | 16 +- codex-rs/config/src/config_toml.rs | 2 +- codex-rs/config/src/loader/mod.rs | 2 +- codex-rs/config/src/mcp_edit.rs | 4 +- codex-rs/config/src/schema.rs | 2 +- codex-rs/core/src/config/edit.rs | 6 +- .../core/src/mcp_tool_approval_templates.rs | 2 +- codex-rs/core/src/test_support.rs | 2 +- .../src/tools/handlers/multi_agents_common.rs | 2 +- codex-rs/core/tests/suite/compact.rs | 6 +- codex-rs/core/tests/suite/compact_remote.rs | 2 +- .../core/tests/suite/compact_remote_parity.rs | 2 +- .../core/tests/suite/compact_resume_fork.rs | 6 +- .../core/tests/suite/realtime_conversation.rs | 6 +- codex-rs/debug-client/src/client.rs | 6 +- codex-rs/exec-server/src/client.rs | 2 +- codex-rs/exec-server/src/fs_sandbox.rs | 2 +- codex-rs/hooks/src/schema.rs | 2 +- codex-rs/memories/write/src/phase1.rs | 2 +- codex-rs/models-manager/src/manager.rs | 2 +- codex-rs/models-manager/src/test_support.rs | 2 +- codex-rs/rust-toolchain.toml | 2 +- codex-rs/scripts/setup-windows.ps1 | 2 +- codex-rs/state/src/runtime/memories.rs | 2 +- codex-rs/tui/src/app/thread_events.rs | 8 +- .../src/bottom_pane/request_user_input/mod.rs | 8 +- codex-rs/tui/src/chatwidget/protocol.rs | 6 +- codex-rs/tui/src/history_cell/mcp.rs | 6 +- codex-rs/tui/src/history_cell/session.rs | 2 +- codex-rs/tui/src/multi_agents.rs | 2 +- codex-rs/tui/src/resume_picker.rs | 20 +- codex-rs/tui/src/text_formatting.rs | 2 +- codex-rs/tui/src/theme_picker.rs | 4 +- codex-rs/utils/cli/src/format_env_display.rs | 2 +- 59 files changed, 230 insertions(+), 260 deletions(-) diff --git a/.codex/environments/environment.toml b/.codex/environments/environment.toml index f67f1983f2..4595a1fa65 100644 --- a/.codex/environments/environment.toml +++ b/.codex/environments/environment.toml @@ -8,4 +8,4 @@ script = "" [[actions]] name = "Run" icon = "run" -command = "cargo +1.93.0 run --manifest-path=codex-rs/Cargo.toml --bin codex -- -c mcp_oauth_credentials_store=file" +command = "cargo +1.95.0 run --manifest-path=codex-rs/Cargo.toml --bin codex -- -c mcp_oauth_credentials_store=file" diff --git a/.devcontainer/Dockerfile.secure b/.devcontainer/Dockerfile.secure index 6c1878eafa..83af317d8b 100644 --- a/.devcontainer/Dockerfile.secure +++ b/.devcontainer/Dockerfile.secure @@ -3,7 +3,7 @@ FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04 ARG TZ ARG DEBIAN_FRONTEND=noninteractive ARG NODE_MAJOR=22 -ARG RUST_TOOLCHAIN=1.92.0 +ARG RUST_TOOLCHAIN=1.95.0 # Keep this in sync with .devcontainer/codex-install/package.json and pnpm-lock.yaml. ARG CODEX_NPM_VERSION=0.121.0 diff --git a/.devcontainer/devcontainer.secure.json b/.devcontainer/devcontainer.secure.json index 5d5808e541..7b05fb9a6c 100644 --- a/.devcontainer/devcontainer.secure.json +++ b/.devcontainer/devcontainer.secure.json @@ -7,7 +7,7 @@ "args": { "TZ": "${localEnv:TZ:UTC}", "NODE_MAJOR": "22", - "RUST_TOOLCHAIN": "1.92.0", + "RUST_TOOLCHAIN": "1.95.0", "CODEX_NPM_VERSION": "0.121.0" } }, diff --git a/.github/workflows/cargo-deny.yml b/.github/workflows/cargo-deny.yml index f20d09e112..656c50ef28 100644 --- a/.github/workflows/cargo-deny.yml +++ b/.github/workflows/cargo-deny.yml @@ -20,10 +20,10 @@ jobs: persist-credentials: false - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 - name: Run cargo-deny uses: EmbarkStudios/cargo-deny-action@82eb9f621fbc699dd0918f3ea06864c14cc84246 # v2 with: - rust-version: 1.93.0 + rust-version: 1.95.0 manifest-path: ./codex-rs/Cargo.toml diff --git a/.github/workflows/rust-ci-full-nextest-platform.yml b/.github/workflows/rust-ci-full-nextest-platform.yml index 7dc39d33ed..3fdf7b51ee 100644 --- a/.github/workflows/rust-ci-full-nextest-platform.yml +++ b/.github/workflows/rust-ci-full-nextest-platform.yml @@ -94,7 +94,7 @@ jobs: - name: Install DotSlash uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2 - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 with: targets: ${{ inputs.target }} @@ -319,7 +319,7 @@ jobs: - name: Install DotSlash uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2 - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 with: targets: ${{ inputs.target }} diff --git a/.github/workflows/rust-ci-full.yml b/.github/workflows/rust-ci-full.yml index c4a9329c00..2111ed7b0a 100644 --- a/.github/workflows/rust-ci-full.yml +++ b/.github/workflows/rust-ci-full.yml @@ -25,7 +25,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 with: components: rustfmt - uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49 @@ -46,7 +46,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 - uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49 with: tool: cargo-shear@1.11.2 @@ -63,7 +63,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 with: toolchain: nightly-2025-09-18 components: llvm-tools-preview, rustc-dev, rust-src @@ -266,7 +266,7 @@ jobs: fi sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "${packages[@]}" fi - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 with: targets: ${{ matrix.target }} components: clippy diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 029b6f2c7e..d1d7c0df96 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -67,7 +67,7 @@ jobs: with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} persist-credentials: false - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 with: components: rustfmt - uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49 @@ -91,7 +91,7 @@ jobs: with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} persist-credentials: false - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 - uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49 with: tool: cargo-shear@1.11.2 @@ -111,7 +111,7 @@ jobs: with: ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} persist-credentials: false - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 - name: Install nightly argument-comment-lint toolchain shell: bash run: | diff --git a/.github/workflows/rust-release-argument-comment-lint.yml b/.github/workflows/rust-release-argument-comment-lint.yml index f654bd9dd7..94f15af7d4 100644 --- a/.github/workflows/rust-release-argument-comment-lint.yml +++ b/.github/workflows/rust-release-argument-comment-lint.yml @@ -60,7 +60,7 @@ jobs: with: persist-credentials: false - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 with: toolchain: nightly-2025-09-18 targets: ${{ matrix.target }} diff --git a/.github/workflows/rust-release-windows.yml b/.github/workflows/rust-release-windows.yml index 51412be0e0..088cb8a74c 100644 --- a/.github/workflows/rust-release-windows.yml +++ b/.github/workflows/rust-release-windows.yml @@ -100,7 +100,7 @@ jobs: Write-Host "Total RAM: $ramGiB GiB" Write-Host "Disk usage:" Get-PSDrive -PSProvider FileSystem | Format-Table -AutoSize Name, @{Name='Size(GB)';Expression={[math]::Round(($_.Used + $_.Free) / 1GB, 1)}}, @{Name='Free(GB)';Expression={[math]::Round($_.Free / 1GB, 1)}} - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 with: targets: ${{ matrix.target }} diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 2cd11e66fe..29d61d6a54 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -56,7 +56,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 - name: Validate tag matches Cargo.toml version shell: bash env: @@ -254,7 +254,7 @@ jobs: sudo apt-get update -y sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1 fi - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 with: targets: ${{ matrix.target }} diff --git a/.github/workflows/rusty-v8-release.yml b/.github/workflows/rusty-v8-release.yml index 3b56a1e93f..d98cdb2c43 100644 --- a/.github/workflows/rusty-v8-release.yml +++ b/.github/workflows/rusty-v8-release.yml @@ -152,9 +152,9 @@ jobs: python-version: "3.12" - name: Set up Rust toolchain for Cargo smoke - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 with: - toolchain: "1.93.0" + toolchain: "1.95.0" - name: Build Bazel V8 release pair env: diff --git a/.github/workflows/v8-canary.yml b/.github/workflows/v8-canary.yml index 979e991504..6341f218a6 100644 --- a/.github/workflows/v8-canary.yml +++ b/.github/workflows/v8-canary.yml @@ -166,9 +166,9 @@ jobs: python-version: "3.12" - name: Set up Rust toolchain for Cargo smoke - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 with: - toolchain: "1.93.0" + toolchain: "1.95.0" - name: Build Bazel V8 release pair env: @@ -310,9 +310,9 @@ jobs: architecture: x64 - name: Set up Codex Rust toolchain for Cargo smoke - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 with: - toolchain: "1.93.0" + toolchain: "1.95.0" targets: ${{ matrix.target }} - name: Install rusty_v8 Rust toolchain @@ -401,7 +401,7 @@ jobs: cd codex-rs RUSTY_V8_ARCHIVE="${GITHUB_WORKSPACE}/${archive}" \ RUSTY_V8_SRC_BINDING_PATH="${GITHUB_WORKSPACE}/${binding}" \ - cargo +1.93.0 test -p codex-v8-poc --target "${TARGET}" --features sandbox --no-run + cargo +1.95.0 test -p codex-v8-poc --target "${TARGET}" --features sandbox --no-run ) - name: Upload staged artifacts diff --git a/MODULE.bazel b/MODULE.bazel index 81f16b7b9e..abca889d64 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -163,7 +163,7 @@ use_repo(nightly_rust, "rust_toolchains") toolchains = use_extension("@rules_rs//rs/experimental/toolchains:module_extension.bzl", "toolchains") toolchains.toolchain( edition = "2024", - version = "1.93.0", + version = "1.95.0", ) use_repo(toolchains, "default_rust_toolchains") diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index ad56196dd9..8a3cdeb66e 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -1867,112 +1867,112 @@ "zvariant_utils_2.1.0": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.81\"},{\"name\":\"quote\",\"req\":\"^1.0.36\"},{\"features\":[\"extra-traits\",\"full\"],\"name\":\"syn\",\"req\":\"^2.0.64\"}],\"features\":{}}" }, "@@rules_rs+//rs/experimental/toolchains:module_extension.bzl%toolchains": { - "cargo-1.93.0-aarch64-apple-darwin.tar.xz": "6443909350322ad07f09bb5edfd9ff29268e6fe88c7d78bfba7a5e254248dc25", - "cargo-1.93.0-aarch64-pc-windows-gnullvm.tar.xz": "387832b989c8eb96c9ebd66402a87962167633bc6d91a49ffb8c7903c45f3476", - "cargo-1.93.0-aarch64-pc-windows-msvc.tar.xz": "155bff7a16aa7054e7ed7c3a82e362d4b302b3882d751b823e06ff63ae3f103d", - "cargo-1.93.0-aarch64-unknown-linux-gnu.tar.xz": "5998940b8b97286bb67facb1a85535eeb3d4d7a61e36a85e386e5c0c5cfe5266", - "cargo-1.93.0-x86_64-apple-darwin.tar.xz": "95a47c5ed797c35419908f04188d8b7de09946e71073c4b72632b16f5b10dfae", - "cargo-1.93.0-x86_64-pc-windows-gnullvm.tar.xz": "f19766837559f90476508140cb95cc708220012ec00a854fa9f99187b1f246b6", - "cargo-1.93.0-x86_64-pc-windows-msvc.tar.xz": "e59c5e2baa9ec17261f2cda6676ebf7b68b21a860e3f7451c4d964728951da75", - "cargo-1.93.0-x86_64-unknown-linux-gnu.tar.xz": "c23de3ae709ff33eed5e4ae59d1f9bcd75fa4dbaa9fb92f7b06bfb534b8db880", - "clippy-1.93.0-aarch64-apple-darwin.tar.xz": "0b6e943a8d12be0e68575acf59c9ea102daf795055fcbbf862b0bfd35ec40039", - "clippy-1.93.0-aarch64-pc-windows-gnullvm.tar.xz": "296949f49be2de77ce9d1c5023a5f0a58e28e329eec03642a3da0e175a67beeb", - "clippy-1.93.0-aarch64-pc-windows-msvc.tar.xz": "07bcf2edb88cdf5ead2f02e4a8493e9b0ef935a31253fac6f9f3378d8023f113", - "clippy-1.93.0-aarch64-unknown-linux-gnu.tar.xz": "872ae6d68d625946d281b91d928332e6b74f6ab269b6af842338df4338805a60", - "clippy-1.93.0-x86_64-apple-darwin.tar.xz": "e6d0b1afb9607c14a1172d09ee194a032bbb3e48af913d55c5a473e0559eddde", - "clippy-1.93.0-x86_64-pc-windows-gnullvm.tar.xz": "b6f1f7264ed6943c59dedfb9531fbadcc3c0fcf273c940a63d58898b14a1060f", - "clippy-1.93.0-x86_64-pc-windows-msvc.tar.xz": "25fb103390bf392980b4689ac09b2ec2ab4beefb7022a983215b613ad05eab57", - "clippy-1.93.0-x86_64-unknown-linux-gnu.tar.xz": "793108977514b15c0f45ade28ae35c58b05370cb0f22e89bd98fdfa61eabf55d", - "rust-analyzer-1.93.0-aarch64-apple-darwin.tar.xz": "8a09a46d45277678b2d112eef61736e03e78dfa6c506e187df176b904659e5a7", - "rust-analyzer-1.93.0-aarch64-pc-windows-gnullvm.tar.xz": "1fb48fadbaea6da36cfcd6b72122eeaa6ab025c9c82441777cf913108f73c115", - "rust-analyzer-1.93.0-aarch64-pc-windows-msvc.tar.xz": "558100ab62fe22d552be43c1bf166fef057ff2f3eb0d786cf5fe509a08701f37", - "rust-analyzer-1.93.0-aarch64-unknown-linux-gnu.tar.xz": "b2e7890cf5953f572eb05d7328c086fa60d4f16e5d301d35c52b3f58b88a8316", - "rust-analyzer-1.93.0-x86_64-apple-darwin.tar.xz": "3cb7975d4fd17840e2980c9d864755f801ee9e7594de886a5816c73b07594508", - "rust-analyzer-1.93.0-x86_64-pc-windows-gnullvm.tar.xz": "4471db6393b7380624899482aae99612fd1cf509ae36d0c68292077c2104127d", - "rust-analyzer-1.93.0-x86_64-pc-windows-msvc.tar.xz": "f4c1f8f120f48974cb1e0cea01a955ce52cdfa0f1db0355c5fb5a2deeca8188a", - "rust-analyzer-1.93.0-x86_64-unknown-linux-gnu.tar.xz": "33fcd377be3b5ffdd95977c3d0219f63725c18b6c8b53fb5be0418962a84738c", - "rust-src-1.93.0.tar.xz": "0e7b9acd5debfeffef3741dc8f6edf137d70426a0027d6b190cc8cfb0a1ac23c", - "rust-std-1.93.0-aarch64-apple-darwin.tar.xz": "8603c63715349636ed85b4fe716c4e827a727918c840e54aff5b243cedadf19b", - "rust-std-1.93.0-aarch64-apple-ios-macabi.tar.xz": "24d47e615ce101869ff452a572a6b77ed7cf70f2454d0b50892ac849e8c7ac4d", - "rust-std-1.93.0-aarch64-apple-ios-sim.tar.xz": "d1d5e2d1b79206f2cc9fb7f6a2958cfe0f4bbc9147fda8dbc3608aa4be5e6816", - "rust-std-1.93.0-aarch64-apple-ios.tar.xz": "49228e70387773a71cf144509baf39979ab2cdb604340fff64b483ab41f61617", - "rust-std-1.93.0-aarch64-linux-android.tar.xz": "59c16648d9a29c07f63a1749cae6b73078f20fef1206c5e0f81c784ae8796cdb", - "rust-std-1.93.0-aarch64-pc-windows-gnullvm.tar.xz": "9a270d50eaaacc7cb1925565a8b65ff831686aa1592b7034bb9848d7f2a9738d", - "rust-std-1.93.0-aarch64-pc-windows-msvc.tar.xz": "f7bd3d25baf3643c8769b8c4d2e6cde45bb25042fac698e0daf19fc9f58f8568", - "rust-std-1.93.0-aarch64-unknown-fuchsia.tar.xz": "d1e46c443a9607603c810942e99a95a1acfb105d1312426b468ff68febaabf77", - "rust-std-1.93.0-aarch64-unknown-linux-gnu.tar.xz": "84e82ff52c39c64dfd0e1c2d58fd3d5309d1d2502378131544c0d486b44af20a", - "rust-std-1.93.0-aarch64-unknown-linux-musl.tar.xz": "bab885a87da586040064064bd1c314d707164d8dc0fefee39d59be7f15ce6f7d", - "rust-std-1.93.0-aarch64-unknown-none-softfloat.tar.xz": "0f6305daf658a7d6c0efd075859cb60432c13b82e7ecee0d097074e4e1873391", - "rust-std-1.93.0-aarch64-unknown-none.tar.xz": "3cf1aa3309a8903e89bb20537113155ca4e88844c8cc9c34c43865d5ce5a6192", - "rust-std-1.93.0-aarch64-unknown-uefi.tar.xz": "317b0af124e0e124bd76b8e5a2fb0c600279177d0bed9c841a3202df2d0f7f8e", - "rust-std-1.93.0-arm-linux-androideabi.tar.xz": "d010b26fc88e28a93cc94ea6ca5d2c90efed7f9846fae1e40db7c183b50575e2", - "rust-std-1.93.0-arm-unknown-linux-gnueabi.tar.xz": "deedc54ffce099781986eed4aec0843866f1bf72904ab0f5cdb115b9c7af540e", - "rust-std-1.93.0-arm-unknown-linux-gnueabihf.tar.xz": "89e44e042bc1241b3999191c385fec8433d60a5a9fc373583cd3b2d9408d5074", - "rust-std-1.93.0-arm-unknown-linux-musleabi.tar.xz": "641a17acb5104637d4dc9c4be022a7927ae721eb08759fea96ecfaf5c60be4dc", - "rust-std-1.93.0-arm-unknown-linux-musleabihf.tar.xz": "94a92b454bf3b0aab046b257f555ccb08f16dc2dc281bea6a4ef17ea8f58cbdc", - "rust-std-1.93.0-armv7-linux-androideabi.tar.xz": "e295f26bb219a7a4ebb5c2e07fedfebb075be6830aaf910c742a57cd21018b6d", - "rust-std-1.93.0-armv7-unknown-linux-gnueabi.tar.xz": "8a7bd5227c78294864095edb07837ff32ff6c07cd1a4a418f9bcc3ebd7e79874", - "rust-std-1.93.0-armv7-unknown-linux-gnueabihf.tar.xz": "f015f9b2d588454a9dc62942ab2e800d82c602e4eab6f89f8213419491bcd203", - "rust-std-1.93.0-armv7-unknown-linux-musleabi.tar.xz": "d15d24c9fb7c15243e1341cea53590002df271060118914bd0efcda8ccbd0731", - "rust-std-1.93.0-armv7-unknown-linux-musleabihf.tar.xz": "a2e5ec22ed35fb51a503d1e10b37447b0fa7333f079585bc0b6a2eb599de43f3", - "rust-std-1.93.0-i686-linux-android.tar.xz": "68fd86f62dd63221717d1741210f0f5cf75da7a1e32eed5a46b1e67c9d9430e1", - "rust-std-1.93.0-i686-pc-windows-gnu.tar.xz": "cb613d5d1eb245e8a1f4c0b25f93c2997cd06c1cc3fc202155f2997aebf44d4d", - "rust-std-1.93.0-i686-pc-windows-gnullvm.tar.xz": "0f713dc252a6de706519fe6cdaab6d66aaf1b555133b536cc0ab28061aa4269c", - "rust-std-1.93.0-i686-pc-windows-msvc.tar.xz": "33dc1951e2dc21bd05361160d52f496eecf48e0b95df5083172698b1cd5b9a3f", - "rust-std-1.93.0-i686-unknown-freebsd.tar.xz": "67718aae1381879fdcca5699051eb87e0cda3d2fd0fe75e306ba0948b79df7db", - "rust-std-1.93.0-i686-unknown-linux-gnu.tar.xz": "b8b7020a61418b95c1ea26badaf8db6979778e28dbadddc81fb5010fe27c935b", - "rust-std-1.93.0-i686-unknown-linux-musl.tar.xz": "867e54b3e89dc0b6d2b7a538a80443add6c3990bb4bd2260dea2ed98a0dc9870", - "rust-std-1.93.0-i686-unknown-uefi.tar.xz": "929fd484b08d5b2077ff864f5f2d24b51a78f1b6e837b9eab9d7e8fb7f31adce", - "rust-std-1.93.0-powerpc-unknown-linux-gnu.tar.xz": "e851c0fa3e726ce3f7139c5803198a1aa9723594394734ac9e373c93d92e5ea3", - "rust-std-1.93.0-powerpc64-unknown-linux-gnu.tar.xz": "f729bb7d95705e12a92eb072e494b93d8822ca40aa4802ca780b0cf33b56d401", - "rust-std-1.93.0-powerpc64le-unknown-linux-gnu.tar.xz": "d209ac698a69ca9b9035adb97a0ed8e60a08db52960198c3e03b9ee714c1a46b", - "rust-std-1.93.0-powerpc64le-unknown-linux-musl.tar.xz": "34b98d5eca2fdbd6ba41b0faf14160ef1ebd038f6ecaa264d318ad33263e1cf1", - "rust-std-1.93.0-riscv32imc-unknown-none-elf.tar.xz": "71af84c81241cbc7811b267927990be025f30d7d3dc55df4b56da7ac250f7c78", - "rust-std-1.93.0-riscv64gc-unknown-linux-gnu.tar.xz": "b769fb6c9f3e0419a6bd0b7b79f9191bbd7a48a9f243b23eb7d135711aa6de1b", - "rust-std-1.93.0-riscv64gc-unknown-linux-musl.tar.xz": "a7ced602573d814d875d69022e026c1ccb520b4b2de9d430ddfd0966ec6c9643", - "rust-std-1.93.0-riscv64gc-unknown-none-elf.tar.xz": "842f72913f288a0c76601438e67ccd88c816dbf187587928e48bf8b9ce74cbf3", - "rust-std-1.93.0-s390x-unknown-linux-gnu.tar.xz": "41a65db45a288eb3eedb187b366f132d5b3615767de7ce994b123b342ac7a848", - "rust-std-1.93.0-thumbv6m-none-eabi.tar.xz": "be3f8aad5680dabb203300847dcbbabc15729170ba5c3a9c499efae4df410a9e", - "rust-std-1.93.0-thumbv7em-none-eabi.tar.xz": "8f93eefca39c0da417feddab64775f862c72bbe80da11914dcf47babef8f1644", - "rust-std-1.93.0-thumbv7em-none-eabihf.tar.xz": "a3b6914b966ac93dbe7531016d5f570b189445603c43614a60e0b9ea12674bd3", - "rust-std-1.93.0-thumbv7m-none-eabi.tar.xz": "cfa6227214f3ae58c06b36547c5bd6f0f6787764afa48cfa4ff3488264deab6c", - "rust-std-1.93.0-thumbv8m.main-none-eabi.tar.xz": "5f24df0aa8322561125575e365be7ad13a5bb26cf73c7fc9a3f4bcfa58e0febc", - "rust-std-1.93.0-thumbv8m.main-none-eabihf.tar.xz": "906b07580be2df277cced2b56bc03cb565b758c382bf3e82cbd8375b459815dd", - "rust-std-1.93.0-wasm32-unknown-emscripten.tar.xz": "63cdbb1ea7f353060539c00f7346f4f5fb0d6f09899cacddc1f172ef07c4af8b", - "rust-std-1.93.0-wasm32-unknown-unknown.tar.xz": "3100cb920ddac646943243f0eddd331128836b9161dd5f7b0a6c76375d39cc5e", - "rust-std-1.93.0-wasm32-wasip1-threads.tar.xz": "439c65dea31e855f0258632b6d19435ba8a80561297fa6dc6be48048c5cd1871", - "rust-std-1.93.0-wasm32-wasip1.tar.xz": "075de970ef865678dad258f1566d7cfe76a594698e9bf93dd69fa5cfdfcf1a6f", - "rust-std-1.93.0-wasm32-wasip2.tar.xz": "0ef01bb552036ab44456f5505015b13c88d3694629804d7af46452c8b0a48f8c", - "rust-std-1.93.0-x86_64-apple-darwin.tar.xz": "f112d41c8a31794f0f561d37fe77010ed0b405fa70284a2910891869d8c52418", - "rust-std-1.93.0-x86_64-apple-ios-macabi.tar.xz": "a543dd545747d372d973ace8b485a13603ce96c110c7ae734d605e45f6e162c5", - "rust-std-1.93.0-x86_64-apple-ios.tar.xz": "e151013b9bc5990e178285a33e62bae7700d8c48c06e97771abb1643aa907d75", - "rust-std-1.93.0-x86_64-linux-android.tar.xz": "dc05ca79d9fecc5ce3643adb9c6f89fd35c8e1d7146bf9b62e30bad41f9fb6a7", - "rust-std-1.93.0-x86_64-pc-windows-gnu.tar.xz": "a07c6ab596fad15ca7acd63ee7f2a5fea93fd421179252067e309c2aa0b2021b", - "rust-std-1.93.0-x86_64-pc-windows-gnullvm.tar.xz": "ef6cf0977bc5aa4bbd594afb9df4ba76fdd4f0fc5685cddbefff49ceed202a91", - "rust-std-1.93.0-x86_64-pc-windows-msvc.tar.xz": "2593e29af0b8def34ceb1185b8e85bd93a9e0d3b0c108d704c1b31370c50a48c", - "rust-std-1.93.0-x86_64-unknown-freebsd.tar.xz": "51b2feaff7c2d28633504ed92ab442a55d112e6a2bf09c91188f00dbaf03378a", - "rust-std-1.93.0-x86_64-unknown-fuchsia.tar.xz": "41f0f3eb96cedfc13ab5fd4f15065063f262d035c1f71d96c42587acdacdbabe", - "rust-std-1.93.0-x86_64-unknown-linux-gnu.tar.xz": "a849a418d0f27e69573e41763c395e924a0b98c16fcdc55599c1c79c27c1c777", - "rust-std-1.93.0-x86_64-unknown-linux-musl.tar.xz": "874658d2ced1ed2b9bf66c148b78a2e10cad475d0a4db32e68a08900905b89b8", - "rust-std-1.93.0-x86_64-unknown-netbsd.tar.xz": "aad63193af89772031f9a85f193afc0b15f8e6d4a9a4983c5f5d3802f69a89e8", - "rust-std-1.93.0-x86_64-unknown-none.tar.xz": "01dcca7ae4b7e82fbfa399adb5e160afaa13143e5a17e1e0737c38cf07365fb3", - "rust-std-1.93.0-x86_64-unknown-uefi.tar.xz": "ec4e439d9485ce752b56999e8e41ed82373fc833a005cf2531c6f7ef7e785392", - "rustc-1.93.0-aarch64-apple-darwin.tar.xz": "092be03c02b44c405dab1232541c84f32b2d9e8295747568c3d531dd137221dc", - "rustc-1.93.0-aarch64-pc-windows-gnullvm.tar.xz": "d3bc0cdaf157e20b1f23e510b5e3c4c6e9117d08f5284c04dee60aecff1bc851", - "rustc-1.93.0-aarch64-pc-windows-msvc.tar.xz": "a3ac1a8e411de8470f71b366f89d187718c431526912b181692ed0a18c56c7ad", - "rustc-1.93.0-aarch64-unknown-linux-gnu.tar.xz": "1a9045695892ec08d8e9751bf7cf7db71fe27a6202dd12ce13aca48d0602dbde", - "rustc-1.93.0-x86_64-apple-darwin.tar.xz": "594bb293f0a4f444656cf8dec2149fcb979c606260efee9e09bcf8c9c6ed6ae7", - "rustc-1.93.0-x86_64-pc-windows-gnullvm.tar.xz": "0cdaa8de66f5ce21d1ea73917efc5c64f408bda49f678ddde19465ced9d5ec63", - "rustc-1.93.0-x86_64-pc-windows-msvc.tar.xz": "fa17677eee0d83eb055b309953184bf87ba634923d8897f860cda65d55c6e350", - "rustc-1.93.0-x86_64-unknown-linux-gnu.tar.xz": "00c6e6740ea6a795e33568cd7514855d58408a1180cd820284a7bbf7c46af715", - "rustfmt-1.93.0-aarch64-apple-darwin.tar.xz": "0dd1faedf0768ef362f4aae4424b34e8266f2b9cf5e76ea4fcaf780220b363a0", - "rustfmt-1.93.0-aarch64-pc-windows-gnullvm.tar.xz": "5888827e7fbd7d59930870b4856fce8d6d8fca5e02f6535f8ae3d7ad0ccf2d4a", - "rustfmt-1.93.0-aarch64-pc-windows-msvc.tar.xz": "24eed108489567133bbfe40c8eacda1567be55fae4c526911b39eb33eb27a6cb", - "rustfmt-1.93.0-aarch64-unknown-linux-gnu.tar.xz": "92e1acb45ae642136258b4dabb39302af2d53c83e56ebd5858bc969f9e5c141a", - "rustfmt-1.93.0-x86_64-apple-darwin.tar.xz": "c8453b4c5758eb39423042ffa9c23ed6128cbed2b15b581e5e1192c9cc0b1d4e", - "rustfmt-1.93.0-x86_64-pc-windows-gnullvm.tar.xz": "47167e9e78db9be4503a060dee02f4df2cda252da32175dbf44331f965a747b9", - "rustfmt-1.93.0-x86_64-pc-windows-msvc.tar.xz": "5becc7c2dba4b9ab5199012cad30829235a7f7fb5d85a238697e8f0e44cbd9af", - "rustfmt-1.93.0-x86_64-unknown-linux-gnu.tar.xz": "7f81f6c17d11a7fda5b4e1b111942fb3b23d30dcec767e13e340ebfb762a5e33" + "cargo-1.95.0-aarch64-apple-darwin.tar.xz": "6c2ffed8e1ac9cf4dc9e80f282a869a6b237a153e7c55cca039d33de29d80aaf", + "cargo-1.95.0-aarch64-pc-windows-gnullvm.tar.xz": "14683a8e0b0ee8afdd7e2896fc8e91a7ff0ba55c2e20912e639eea498f1e1d10", + "cargo-1.95.0-aarch64-pc-windows-msvc.tar.xz": "e645b30fa035a18aa12d28b699052014c7efa9dd4a33dabd223f0d16b5fa28e8", + "cargo-1.95.0-aarch64-unknown-linux-gnu.tar.xz": "7c070aeba9bbf12073646995a03f36c346bb5f541d0078ba6d9dc2a7adaaf6af", + "cargo-1.95.0-x86_64-apple-darwin.tar.xz": "e2e1131ade2dddc0d779e0ab3a6a990085c7a654951235742823c3a1ce0f190f", + "cargo-1.95.0-x86_64-pc-windows-gnullvm.tar.xz": "239c098b9878ad01ad5e0feeee377e6ee3311bc1534b1fb5eb630489d463bab2", + "cargo-1.95.0-x86_64-pc-windows-msvc.tar.xz": "cab2606cb2d0aa31c55d50512fe07a9f15e893227566fbeb448306760cd0d2bf", + "cargo-1.95.0-x86_64-unknown-linux-gnu.tar.xz": "e74edd2cf7d0f1f1383b4f00eb90c843750bc489e2ccf7214e6476678a907425", + "clippy-1.95.0-aarch64-apple-darwin.tar.xz": "fd183baa023d0c4e0c5b8184226e2d4c85126adf156cb1f3a726ec593bba8d62", + "clippy-1.95.0-aarch64-pc-windows-gnullvm.tar.xz": "b81d0fe05c4ec514aefaffdf0649b175a2f82572163202c17531358f196b6168", + "clippy-1.95.0-aarch64-pc-windows-msvc.tar.xz": "44c1b7ada72aa8f3fcaceb37a3899665bc9b160c2fea77879c8ecb65a9e97eba", + "clippy-1.95.0-aarch64-unknown-linux-gnu.tar.xz": "fb021e0c0fc2238be9266d7614f4a26bc372544c4cba3528d729ab24ad229fc9", + "clippy-1.95.0-x86_64-apple-darwin.tar.xz": "e47367f6b1489d74cbba93b387310adcb82e27a51e44b2c6ff543eb4f199fe32", + "clippy-1.95.0-x86_64-pc-windows-gnullvm.tar.xz": "9358dfc3b831a5f4b3a3a0016da734b4ef5e78c84ca8f148f56a9c126515ebfa", + "clippy-1.95.0-x86_64-pc-windows-msvc.tar.xz": "ddc151d6f58c6658b7380292ecaef36e62d063bbdbf7f5802669810575bb5b75", + "clippy-1.95.0-x86_64-unknown-linux-gnu.tar.xz": "ac779bc9839dd47180806b133e4e2563c4a34716284cd5b8fede8ef289f452ca", + "rust-analyzer-1.95.0-aarch64-apple-darwin.tar.xz": "11231fc6574301b94bd379af4ef409caef7c65b877bcecf2b227dc0d74aa0ec7", + "rust-analyzer-1.95.0-aarch64-pc-windows-gnullvm.tar.xz": "6b2c0820957fdf0e3026f51fe85ad3cef94d5948e29cc83d2221f65dafc7a16b", + "rust-analyzer-1.95.0-aarch64-pc-windows-msvc.tar.xz": "92958624f23d4b0980748ac9e6d67f6f67a868f8224e8c6240e3f84145e2d805", + "rust-analyzer-1.95.0-aarch64-unknown-linux-gnu.tar.xz": "b37e5b9aad624e54228254f98a710ee19ad464fe7ada93ef12e20c87886a0047", + "rust-analyzer-1.95.0-x86_64-apple-darwin.tar.xz": "6cd111900e13fd19b188c5d8844b34136af3967066c0ea2914ce5c3508296c85", + "rust-analyzer-1.95.0-x86_64-pc-windows-gnullvm.tar.xz": "01bd05a6b990ad37907ff26e8c285c5ba8b7e674fabd0e264fc7f7aa04d963c5", + "rust-analyzer-1.95.0-x86_64-pc-windows-msvc.tar.xz": "ba58e349f5e8b0ef13735c48d4ad8d8c7664472f8403f3c9d97b291bd54a7638", + "rust-analyzer-1.95.0-x86_64-unknown-linux-gnu.tar.xz": "a9d71c6e7427c45afcd846a8b34a3e3301ae7a0e91a2bcf929326af77a7dc68e", + "rust-src-1.95.0.tar.xz": "67b09138c8db96afc4bbfc69ea771ac9a091fd777698acb43f6dfd9fb7dea363", + "rust-std-1.95.0-aarch64-apple-darwin.tar.xz": "9b30089b0f767cb91b2190ffec55a9beeb2a21a1405d8da0f664d7e09d08e6d8", + "rust-std-1.95.0-aarch64-apple-ios-macabi.tar.xz": "0e1760828f4e0fa1cde0061ba5680619dcc1cdcafec9242cc18dc4547c73b1cd", + "rust-std-1.95.0-aarch64-apple-ios-sim.tar.xz": "4bfe5b0c74c10d121a8ac60f1833c7714b963f9130f6256ca313d94405267deb", + "rust-std-1.95.0-aarch64-apple-ios.tar.xz": "6fcc42d8dbba4910a128ffa32d62a730339a7e3882a90341a881f2edf66ff55a", + "rust-std-1.95.0-aarch64-linux-android.tar.xz": "de5e8fa5d955809891eea77682811fc90be705f78883bd94071e98f5a738d05b", + "rust-std-1.95.0-aarch64-pc-windows-gnullvm.tar.xz": "93d810b8872771afe04f66aa30a4eee48736aca693186e4c7cc2766e1a82e340", + "rust-std-1.95.0-aarch64-pc-windows-msvc.tar.xz": "be21b5a8a71c49b4dcbc19956233b0de7bfda3ee3c8a199148299f867e95cb42", + "rust-std-1.95.0-aarch64-unknown-fuchsia.tar.xz": "b311a0523f75e031d683d983515edb9baaf22a843dbd44ce2a30a3204752f592", + "rust-std-1.95.0-aarch64-unknown-linux-gnu.tar.xz": "3a21b271b1ff973b94d69b25e7a39992f9fbcae1ab6d9475844a23e6ad3908ac", + "rust-std-1.95.0-aarch64-unknown-linux-musl.tar.xz": "f6710416ed9a7d5cf2a15efa761eb79a1deeb43f9961bbe05cc97bec4ef9064a", + "rust-std-1.95.0-aarch64-unknown-none-softfloat.tar.xz": "54d691468e25e7989b022a171337beadf78b5202877b312b75182b7f93efbb8b", + "rust-std-1.95.0-aarch64-unknown-none.tar.xz": "2b0c986dc9902866311f1fe2d44bc2bd84479d2ac84ed7ada76a5eb7ba37080f", + "rust-std-1.95.0-aarch64-unknown-uefi.tar.xz": "ca657564103024d345ca32e8e4ade7ebb395a51163d2897d9e5f8373c025e49e", + "rust-std-1.95.0-arm-linux-androideabi.tar.xz": "12a9c5fa24608159c2b2bd50abc0c6d0add407c0258cee894c2f61c07051a9c4", + "rust-std-1.95.0-arm-unknown-linux-gnueabi.tar.xz": "f6cd592dacdf41f724ee90a2f34db028e37ca2a7fb26fa86e93e8fd68e24d066", + "rust-std-1.95.0-arm-unknown-linux-gnueabihf.tar.xz": "fda8408ea17881c6529e27e58672d6c628f786cad557fac92856077e7a610239", + "rust-std-1.95.0-arm-unknown-linux-musleabi.tar.xz": "62f21fabc209fd0de53156764fc426da74c59525bb60cb4c1c3ffd1be0bbe00b", + "rust-std-1.95.0-arm-unknown-linux-musleabihf.tar.xz": "2842ce67f7a4c68c6e8b30ad3bb36484fb745edcc2694b2a36bf0609cf758044", + "rust-std-1.95.0-armv7-linux-androideabi.tar.xz": "61e95e144986c52ff9fa2fe3c249a68b2bf268adb2a2eeb81d80c180e43027f1", + "rust-std-1.95.0-armv7-unknown-linux-gnueabi.tar.xz": "e23454d6ca7fc3f5eb7cf9241572765176d86e9f45d4f394de31a5fd794e523f", + "rust-std-1.95.0-armv7-unknown-linux-gnueabihf.tar.xz": "bd319e18ca2dad0450f76277874d56356330da536be8cf271509f8e6f28ac6de", + "rust-std-1.95.0-armv7-unknown-linux-musleabi.tar.xz": "a49bc987e0531800f92825ce61402b5734ea747caa8970d3373506742ed7daa6", + "rust-std-1.95.0-armv7-unknown-linux-musleabihf.tar.xz": "77f9aaff4669c076edc96cdb99dc21749d2c692c862232b52176572c289fe671", + "rust-std-1.95.0-i686-linux-android.tar.xz": "e7b5e18d1d4119c7c1454ee8427933f27aeff9e81a22248c2829f5f299d3a937", + "rust-std-1.95.0-i686-pc-windows-gnu.tar.xz": "2e98fb94fc500690d7e72e071ce29dd98790ff518163963f5c82c32afba9231d", + "rust-std-1.95.0-i686-pc-windows-gnullvm.tar.xz": "b429a8c456d6a1815ece064102bd6a25f67d46db4f6d7b79dbe7d1b9d1f78e0f", + "rust-std-1.95.0-i686-pc-windows-msvc.tar.xz": "6206fd7e8bd119e9d1ba1c425ad25c282512eb0d5659f2dcb4be224b84715706", + "rust-std-1.95.0-i686-unknown-freebsd.tar.xz": "3b13b3ccecf482c0da3e94f19e0aa5e8e375622f4e09b30e6dae2b7638bee63f", + "rust-std-1.95.0-i686-unknown-linux-gnu.tar.xz": "527c5d5249a7f77b48d3c9da3ac512d27b47f43d08dbe3c6f82a3d5b35d8aa27", + "rust-std-1.95.0-i686-unknown-linux-musl.tar.xz": "af4d3e7aabb63d39a7a2ff5435cc993b65ff38a2d2e23f1967e519037a1b0455", + "rust-std-1.95.0-i686-unknown-uefi.tar.xz": "3233985273616ec36861f2d50b4a025c903b2bb8c45b171c0ae9e2de8342125b", + "rust-std-1.95.0-powerpc-unknown-linux-gnu.tar.xz": "59e0abbaa246502521e37c55b8d6cf88d5b8a697b0c70c61ec189937308f7246", + "rust-std-1.95.0-powerpc64-unknown-linux-gnu.tar.xz": "cc7fb9aa289ff1756502ae16a05e2885289165f01ed94a7c2db6576b3dae74a6", + "rust-std-1.95.0-powerpc64le-unknown-linux-gnu.tar.xz": "2370d9266051a0b23346d42e43a00f91b2daff22a963fb03e28ae50cb0b76c50", + "rust-std-1.95.0-powerpc64le-unknown-linux-musl.tar.xz": "0362ba4ecfe0bb508f0d2b064c6fbbfe72604d5ae1989d6a8a7b4fe5ff1889f1", + "rust-std-1.95.0-riscv32imc-unknown-none-elf.tar.xz": "04befebf3b15372dacb7e6c0fcdab842c17a63ea58bf2f4cdccd7182ae9195c6", + "rust-std-1.95.0-riscv64gc-unknown-linux-gnu.tar.xz": "50fe7869e166bb4c990a0e1664366b1ffdbe669664b7663cd03c079bd0efdcac", + "rust-std-1.95.0-riscv64gc-unknown-linux-musl.tar.xz": "e01bdbf5d6fa3e529671d49e87ba81dc9612101144f3ee5a0e1de3c48f27b47c", + "rust-std-1.95.0-riscv64gc-unknown-none-elf.tar.xz": "a4cb7a1527f3b56a39464e5ca2b174a27b708b26d78e604735eb5ffd9ee4d20b", + "rust-std-1.95.0-s390x-unknown-linux-gnu.tar.xz": "31978c1286afff9a0bb7f01c2ae4a39f40727b6100a82b6d934f146b06cde510", + "rust-std-1.95.0-thumbv6m-none-eabi.tar.xz": "602ec023c4615fc1c2d78b688554d42fa525e07e861c052f406fd7a607e5d5ee", + "rust-std-1.95.0-thumbv7em-none-eabi.tar.xz": "fb671966ba9aede333956ed43fcfe114ec890ca6e70369c9f3219871ee3ae8ae", + "rust-std-1.95.0-thumbv7em-none-eabihf.tar.xz": "fa3d189c09b64d818ad65a3fec1ce1c7d7b3908aea6fc4607a3fcc05067cad81", + "rust-std-1.95.0-thumbv7m-none-eabi.tar.xz": "e9e39f483ad4c1ce55fa4f508009e8d7d7ef25efe6592f7bfbabc159a24658ff", + "rust-std-1.95.0-thumbv8m.main-none-eabi.tar.xz": "3b64bffb193b37e83dc7b169add55f0ef3298220f8475468be4bb87276a68105", + "rust-std-1.95.0-thumbv8m.main-none-eabihf.tar.xz": "25ec92187d3a45fb1e397b0ba6000341d872523925a99a48b1978bdf1e6038cb", + "rust-std-1.95.0-wasm32-unknown-emscripten.tar.xz": "967b92a8682e8b8a1a459d776b36e41c906cdcac1008fef70e4800fec40d6864", + "rust-std-1.95.0-wasm32-unknown-unknown.tar.xz": "5587b89ff69623d09e476439d44a24453b4e4ea3d5e0b53a5c0a935151ff3fd1", + "rust-std-1.95.0-wasm32-wasip1-threads.tar.xz": "9079935a00a3c3aaf284957bbe82972983ce2708019687cec1f4988c30c1e0f3", + "rust-std-1.95.0-wasm32-wasip1.tar.xz": "86e5b6d98c7520bb9c3ad4f8cbbbf14beaf230b0f06b437db398e3c4f7dae43e", + "rust-std-1.95.0-wasm32-wasip2.tar.xz": "68146eb4c887431379966efa21d75dc957f18bd1166239c1deaa53fe38cb9ab4", + "rust-std-1.95.0-x86_64-apple-darwin.tar.xz": "2be13c14122b8d4d09b7f7c434fca9ae7215ec72049944189c88c4d9128ce504", + "rust-std-1.95.0-x86_64-apple-ios-macabi.tar.xz": "60b92e51e87f84046e0b19ccedc88c45b4b62c3ab10351f8473453f341c894f1", + "rust-std-1.95.0-x86_64-apple-ios.tar.xz": "11abb7b1c92b5a88b8c3ba21ee596a1be7dee5e817b336f26b4968d8ed5513ad", + "rust-std-1.95.0-x86_64-linux-android.tar.xz": "77b8e2be4a6e784a63cd77de944864c8044ddf4d5c7d56f663ada8a38a8319c4", + "rust-std-1.95.0-x86_64-pc-windows-gnu.tar.xz": "f57e045016a04130125fb43295d95f9ad2bebc296150eadb031dbf5167ad12bd", + "rust-std-1.95.0-x86_64-pc-windows-gnullvm.tar.xz": "3c52a0e34e0b4abe4439cabd77383408f5f9c80b6e249fbc0855df424b45008d", + "rust-std-1.95.0-x86_64-pc-windows-msvc.tar.xz": "7c659bdc88646e7e1befa370881bd311be87b26f006933a28b40dcab2f7cc832", + "rust-std-1.95.0-x86_64-unknown-freebsd.tar.xz": "dfe913c2f477172db10d3723d9f5c536d8b6f42776c979cc855820d8249adec3", + "rust-std-1.95.0-x86_64-unknown-fuchsia.tar.xz": "faa2bc09a3992f1d81b538121d64a3a396f4ec666e665c79d2ab47461c2d3206", + "rust-std-1.95.0-x86_64-unknown-linux-gnu.tar.xz": "047ea7098803d3500fa1072e9cee5392697e21525559e4458128a2bf874aa382", + "rust-std-1.95.0-x86_64-unknown-linux-musl.tar.xz": "aee540abf132920f791ef781489851a078d69dff493fb628d49c1d573f92bb3a", + "rust-std-1.95.0-x86_64-unknown-netbsd.tar.xz": "7a82b71c53f20cb147a340819fcab645da220b312a96194531e631ee99783a7d", + "rust-std-1.95.0-x86_64-unknown-none.tar.xz": "7c151c0e7bf3b0b4d7136774cd3686e5f691b761b648b17e83af58e7669d3e01", + "rust-std-1.95.0-x86_64-unknown-uefi.tar.xz": "4cc55629480aa8ab5b39eb6b7458433b48461d6626fdea0330fb88e23af818ea", + "rustc-1.95.0-aarch64-apple-darwin.tar.xz": "149e85a285b6eba58eb6c8bdf7deb1b93763890598e62cb635a712e3a8454f04", + "rustc-1.95.0-aarch64-pc-windows-gnullvm.tar.xz": "758e729faabd8dabfab584d2bead59ca4fbcf1125ffc3c43a69332d6d9f2316b", + "rustc-1.95.0-aarch64-pc-windows-msvc.tar.xz": "0dbec9739b93427ccdd3948c3b1f83cec42e4c9545d930a8d1e1464ff4092c5f", + "rustc-1.95.0-aarch64-unknown-linux-gnu.tar.xz": "0fe3689eeaed603e5ef24572d11597d3edadaefd2cb181674ad621260f2501d2", + "rustc-1.95.0-x86_64-apple-darwin.tar.xz": "33db457715446a69ed6f69f78f5fbb9ca8e17a16585d1d7a0060479bfe4c7afc", + "rustc-1.95.0-x86_64-pc-windows-gnullvm.tar.xz": "44ffbe057bb8f967087a1ad549a7139e9e5017d3aab396f42a6393f897e39531", + "rustc-1.95.0-x86_64-pc-windows-msvc.tar.xz": "4cb1f3b578adc6541cbe13a6f85f1fd8c0ce643d90b506a36dee24c680864c67", + "rustc-1.95.0-x86_64-unknown-linux-gnu.tar.xz": "8426a3d170a5879f5682f5fbdd024a1779b3951e7baba685af2d6dc32a6dfc15", + "rustfmt-1.95.0-aarch64-apple-darwin.tar.xz": "c54af79adfdc790d27fc56e24407e370c80be5a89ad537eef9fbd45d3c3e28e8", + "rustfmt-1.95.0-aarch64-pc-windows-gnullvm.tar.xz": "85b690b664ee4f03521c6bc3f1af8fd7e6efae5851a8a7ff18965594484688e2", + "rustfmt-1.95.0-aarch64-pc-windows-msvc.tar.xz": "a873c048743e6da29e09a8b55c774ee113f8f6ae4fd57d988d304a6453801b34", + "rustfmt-1.95.0-aarch64-unknown-linux-gnu.tar.xz": "64cce868f0f3d29f1524e11e9bab01ac9d538a31665fea1cd6b78af46a1c0a41", + "rustfmt-1.95.0-x86_64-apple-darwin.tar.xz": "5f7228f40a160e80d260e74e068d6fec8627aa02f1f5ae29d2019b9347076401", + "rustfmt-1.95.0-x86_64-pc-windows-gnullvm.tar.xz": "74eb0bb0227af0df81a5df9cbf8384e5e038c4cd575152ecc27dfa891ff3756a", + "rustfmt-1.95.0-x86_64-pc-windows-msvc.tar.xz": "8bcf91606e36b8a0164efafde50709cd7a3c02143a2edaa81dbf3dccd6ed8f4c", + "rustfmt-1.95.0-x86_64-unknown-linux-gnu.tar.xz": "f1b2a7301513ffdd95ebf22ebbdd932e4d17fc806f748d93924740d0297b1396" } } } diff --git a/codex-rs/.github/workflows/cargo-audit.yml b/codex-rs/.github/workflows/cargo-audit.yml index 0c41471b65..1d935d61e8 100644 --- a/codex-rs/.github/workflows/cargo-audit.yml +++ b/codex-rs/.github/workflows/cargo-audit.yml @@ -17,7 +17,7 @@ jobs: working-directory: codex-rs steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 + - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 - name: Install cargo-audit uses: taiki-e/install-action@v2 with: diff --git a/codex-rs/app-server-protocol/src/export.rs b/codex-rs/app-server-protocol/src/export.rs index fcaa8a1eb5..56cf7038f2 100644 --- a/codex-rs/app-server-protocol/src/export.rs +++ b/codex-rs/app-server-protocol/src/export.rs @@ -948,10 +948,8 @@ impl ScanState { '(' => self.depth.paren += 1, ')' => self.depth.paren = (self.depth.paren - 1).max(0), '<' => self.depth.angle += 1, - '>' => { - if self.depth.angle > 0 { - self.depth.angle -= 1; - } + '>' if self.depth.angle > 0 => { + self.depth.angle -= 1; } _ => {} } @@ -2212,20 +2210,14 @@ mod tests { continue; } match ch { - '\\' => { - if in_single || in_double { - escape = true; - } + '\\' if (in_single || in_double) => { + escape = true; } - '\'' => { - if !in_double { - in_single = !in_single; - } + '\'' if !in_double => { + in_single = !in_single; } - '"' => { - if !in_single { - in_double = !in_double; - } + '"' if !in_single => { + in_double = !in_double; } '{' if !in_single && !in_double => level_brace += 1, '}' if !in_single && !in_double => level_brace -= 1, diff --git a/codex-rs/app-server-protocol/src/schema_fixtures.rs b/codex-rs/app-server-protocol/src/schema_fixtures.rs index 447fcf8cfe..18ed557ddf 100644 --- a/codex-rs/app-server-protocol/src/schema_fixtures.rs +++ b/codex-rs/app-server-protocol/src/schema_fixtures.rs @@ -196,7 +196,7 @@ fn canonicalize_json(value: &Value) -> Value { } Value::Object(map) => { let mut entries: Vec<_> = map.iter().collect(); - entries.sort_by(|(left, _), (right, _)| left.cmp(right)); + entries.sort_by_key(|(key, _)| *key); let mut sorted = Map::with_capacity(map.len()); for (key, child) in entries { sorted.insert(key.clone(), canonicalize_json(child)); diff --git a/codex-rs/app-server-transport/src/transport/remote_control/websocket.rs b/codex-rs/app-server-transport/src/transport/remote_control/websocket.rs index 6358d592e4..95bc9d3c6f 100644 --- a/codex-rs/app-server-transport/src/transport/remote_control/websocket.rs +++ b/codex-rs/app-server-transport/src/transport/remote_control/websocket.rs @@ -1390,14 +1390,13 @@ pub(super) async fn connect_remote_control_websocket( status_publisher.publish_environment_id(/*environment_id*/ None); } tungstenite::Error::Http(response) - if matches!(response.status().as_u16(), 401 | 403) => + if matches!(response.status().as_u16(), 401 | 403) + && recover_remote_control_auth(auth_recovery, auth_change_rx).await => { - if recover_remote_control_auth(auth_recovery, auth_change_rx).await { - return Err(io::Error::other(format!( - "remote control websocket auth failed with HTTP {}; retrying after auth recovery", - response.status() - ))); - } + return Err(io::Error::other(format!( + "remote control websocket auth failed with HTTP {}; retrying after auth recovery", + response.status() + ))); } _ => {} } diff --git a/codex-rs/app-server-transport/src/transport/websocket.rs b/codex-rs/app-server-transport/src/transport/websocket.rs index 177e378906..5677faae67 100644 --- a/codex-rs/app-server-transport/src/transport/websocket.rs +++ b/codex-rs/app-server-transport/src/transport/websocket.rs @@ -347,7 +347,7 @@ async fn run_websocket_inbound_loop( incoming_message = websocket_reader.next() => { match incoming_message { Some(Ok(message)) => match message.into_incoming() { - Some(IncomingWebSocketMessage::Text(text)) => { + Some(IncomingWebSocketMessage::Text(text)) if !forward_incoming_message( &transport_event_tx, &writer_tx_for_reader, @@ -355,10 +355,10 @@ async fn run_websocket_inbound_loop( &text, ) .await - { - break; - } + => { + break; } + Some(IncomingWebSocketMessage::Text(_)) => {} Some(IncomingWebSocketMessage::Ping(payload)) => { match writer_control_tx.try_send(M::pong(payload)) { Ok(()) => {} diff --git a/codex-rs/app-server/src/outgoing_message.rs b/codex-rs/app-server/src/outgoing_message.rs index 6cab439bcc..e8fffd5361 100644 --- a/codex-rs/app-server/src/outgoing_message.rs +++ b/codex-rs/app-server/src/outgoing_message.rs @@ -456,8 +456,8 @@ impl OutgoingMessageSender { ) -> Vec { let request_id_to_callback = self.request_id_to_callback.lock().await; let mut requests = request_id_to_callback - .iter() - .filter_map(|(_, entry)| { + .values() + .filter_map(|entry| { (entry.thread_id == Some(thread_id)).then_some(entry.request.clone()) }) .collect::>(); 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 cf76b9d573..0e00bc6309 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 @@ -758,12 +758,15 @@ pub(super) async fn read_response_and_notification_for_method( JSONRPCMessage::Response(candidate) if candidate.id == target_id => { response = Some(candidate); } + JSONRPCMessage::Notification(candidate) + if candidate.method == method && notification.is_some() => + { + bail!( + "received duplicate notification for method `{method}` before completing paired read" + ); + } JSONRPCMessage::Notification(candidate) if candidate.method == method => { - if notification.replace(candidate).is_some() { - bail!( - "received duplicate notification for method `{method}` before completing paired read" - ); - } + notification = Some(candidate); } _ => {} } diff --git a/codex-rs/apply-patch/src/lib.rs b/codex-rs/apply-patch/src/lib.rs index aa2326ab28..4e79141aab 100644 --- a/codex-rs/apply-patch/src/lib.rs +++ b/codex-rs/apply-patch/src/lib.rs @@ -776,7 +776,7 @@ fn compute_replacements( } } - replacements.sort_by(|(lhs_idx, _, _), (rhs_idx, _, _)| lhs_idx.cmp(rhs_idx)); + replacements.sort_by_key(|(index, _, _)| *index); Ok(replacements) } diff --git a/codex-rs/cli/src/doctor.rs b/codex-rs/cli/src/doctor.rs index d2422d05de..2e53d5d51f 100644 --- a/codex-rs/cli/src/doctor.rs +++ b/codex-rs/cli/src/doctor.rs @@ -2176,11 +2176,7 @@ struct RolloutStats { impl RolloutStats { fn average_bytes(&self) -> u64 { - if self.files == 0 { - 0 - } else { - self.total_bytes / self.files - } + self.total_bytes.checked_div(self.files).unwrap_or(0) } } diff --git a/codex-rs/cli/src/mcp_cmd.rs b/codex-rs/cli/src/mcp_cmd.rs index a84d1c9019..0103782653 100644 --- a/codex-rs/cli/src/mcp_cmd.rs +++ b/codex-rs/cli/src/mcp_cmd.rs @@ -537,7 +537,7 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) -> let effective_mcp_servers = mcp_manager.effective_servers(&config, /*auth*/ None).await; let mut entries: Vec<_> = mcp_servers.iter().collect(); - entries.sort_by(|(a, _), (b, _)| a.cmp(b)); + entries.sort_by_key(|(name, _)| *name); let auth_statuses = compute_auth_statuses( effective_mcp_servers.iter(), config.mcp_oauth_credentials_store_mode, @@ -906,7 +906,7 @@ async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Re let headers_display = match http_headers { Some(map) if !map.is_empty() => { let mut pairs: Vec<_> = map.iter().collect(); - pairs.sort_by(|(a, _), (b, _)| a.cmp(b)); + pairs.sort_by_key(|(name, _)| *name); pairs .into_iter() .map(|(k, _)| format!("{k}=*****")) @@ -919,7 +919,7 @@ async fn run_get(config_overrides: &CliConfigOverrides, get_args: GetArgs) -> Re let env_headers_display = match env_http_headers { Some(map) if !map.is_empty() => { let mut pairs: Vec<_> = map.iter().collect(); - pairs.sort_by(|(a, _), (b, _)| a.cmp(b)); + pairs.sort_by_key(|(name, _)| *name); pairs .into_iter() .map(|(k, var)| format!("{k}={var}")) diff --git a/codex-rs/code-mode/src/description.rs b/codex-rs/code-mode/src/description.rs index 0c2813e51a..1362b51a4c 100644 --- a/codex-rs/code-mode/src/description.rs +++ b/codex-rs/code-mode/src/description.rs @@ -649,7 +649,7 @@ fn render_json_schema_object(map: &serde_json::Map) -> String .unwrap_or_default(); let mut sorted_properties = properties.iter().collect::>(); - sorted_properties.sort_unstable_by(|(name_a, _), (name_b, _)| name_a.cmp(name_b)); + sorted_properties.sort_unstable_by_key(|(name_a, _)| *name_a); if sorted_properties .iter() .any(|(_, value)| has_property_description(value)) diff --git a/codex-rs/codex-client/src/custom_ca.rs b/codex-rs/codex-client/src/custom_ca.rs index 7a8b2f27bd..1a211beedf 100644 --- a/codex-rs/codex-client/src/custom_ca.rs +++ b/codex-rs/codex-client/src/custom_ca.rs @@ -477,15 +477,13 @@ impl ConfiguredCaBundle { })?; certificates.push(CertificateDer::from(cert_der.to_vec())); } - SectionKind::Crl => { - if !logged_crl_presence { - info!( - source_env = self.source_env, - ca_path = %self.path.display(), - "ignoring X509 CRL entries found in custom CA bundle" - ); - logged_crl_presence = true; - } + SectionKind::Crl if !logged_crl_presence => { + info!( + source_env = self.source_env, + ca_path = %self.path.display(), + "ignoring X509 CRL entries found in custom CA bundle" + ); + logged_crl_presence = true; } _ => {} } diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index 20a0769d83..3dbf483c2d 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -861,7 +861,7 @@ fn project_config_for_lookup_key( .iter() .filter(|(key, _)| normalize_project_lookup_key((*key).clone()) == lookup_key) .collect(); - normalized_matches.sort_by(|(left, _), (right, _)| left.cmp(right)); + normalized_matches.sort_by_key(|(key, _)| *key); normalized_matches .first() .map(|(_, project_config)| (**project_config).clone()) diff --git a/codex-rs/config/src/loader/mod.rs b/codex-rs/config/src/loader/mod.rs index e8972ce5ad..3c1723e51b 100644 --- a/codex-rs/config/src/loader/mod.rs +++ b/codex-rs/config/src/loader/mod.rs @@ -1006,7 +1006,7 @@ fn project_trust_for_lookup_key( .iter() .filter(|(key, _)| normalize_project_trust_lookup_key((*key).clone()) == lookup_key) .collect(); - normalized_matches.sort_by(|(left, _), (right, _)| left.cmp(right)); + normalized_matches.sort_by_key(|(key, _)| *key); normalized_matches .first() .map(|(key, trust_level)| ((**key).clone(), **trust_level)) diff --git a/codex-rs/config/src/mcp_edit.rs b/codex-rs/config/src/mcp_edit.rs index 7e062e2998..9f007de113 100644 --- a/codex-rs/config/src/mcp_edit.rs +++ b/codex-rs/config/src/mcp_edit.rs @@ -230,7 +230,7 @@ fn serialize_mcp_server(config: &McpServerConfig) -> TomlItem { let mut tools = TomlTable::new(); tools.set_implicit(false); let mut tool_entries: Vec<_> = config.tools.iter().collect(); - tool_entries.sort_by(|(left, _), (right, _)| left.cmp(right)); + tool_entries.sort_by_key(|(name, _)| *name); for (name, tool_config) in tool_entries { let mut tool_entry = TomlTable::new(); tool_entry.set_implicit(false); @@ -280,7 +280,7 @@ where I: IntoIterator, { let mut entries: Vec<_> = pairs.into_iter().collect(); - entries.sort_by(|(left, _), (right, _)| left.cmp(right)); + entries.sort_by_key(|(key, _)| *key); let mut table = TomlTable::new(); table.set_implicit(false); for (key, value_str) in entries { diff --git a/codex-rs/config/src/schema.rs b/codex-rs/config/src/schema.rs index df0de3b1cd..e0d2c70671 100644 --- a/codex-rs/config/src/schema.rs +++ b/codex-rs/config/src/schema.rs @@ -99,7 +99,7 @@ pub fn canonicalize(value: &Value) -> Value { Value::Array(items) => Value::Array(items.iter().map(canonicalize).collect()), Value::Object(map) => { let mut entries: Vec<_> = map.iter().collect(); - entries.sort_by(|(left, _), (right, _)| left.cmp(right)); + entries.sort_by_key(|(key, _)| *key); let mut sorted = Map::with_capacity(map.len()); for (key, child) in entries { sorted.insert(key.clone(), canonicalize(child)); diff --git a/codex-rs/core/src/config/edit.rs b/codex-rs/core/src/config/edit.rs index ca54a7c3f5..290edf6fb8 100644 --- a/codex-rs/core/src/config/edit.rs +++ b/codex-rs/core/src/config/edit.rs @@ -179,7 +179,7 @@ pub fn keymap_binding_clear_edit(context: &str, action: &str) -> ConfigEdit { pub fn model_availability_nux_count_edits(shown_count: &HashMap) -> Vec { let mut shown_count_entries: Vec<_> = shown_count.iter().collect(); - shown_count_entries.sort_unstable_by(|(left, _), (right, _)| left.cmp(right)); + shown_count_entries.sort_unstable_by_key(|(left, _)| *left); let mut edits = vec![ConfigEdit::ClearPath { segments: vec!["tui".to_string(), "model_availability_nux".to_string()], @@ -354,7 +354,7 @@ mod document_helpers { if !config.tools.is_empty() { let mut tools = new_implicit_table(); let mut tool_entries: Vec<_> = config.tools.iter().collect(); - tool_entries.sort_by(|(left, _), (right, _)| left.cmp(right)); + tool_entries.sort_by_key(|(name, _)| *name); for (name, tool_config) in tool_entries { tools.insert(name, serialize_mcp_server_tool(tool_config)); } @@ -501,7 +501,7 @@ mod document_helpers { I: IntoIterator, { let mut entries: Vec<_> = pairs.into_iter().collect(); - entries.sort_by(|(a, _), (b, _)| a.cmp(b)); + entries.sort_by_key(|(key, _)| *key); let mut table = TomlTable::new(); table.set_implicit(false); for (key, val) in entries { diff --git a/codex-rs/core/src/mcp_tool_approval_templates.rs b/codex-rs/core/src/mcp_tool_approval_templates.rs index 66002ab5ab..1c905b4f27 100644 --- a/codex-rs/core/src/mcp_tool_approval_templates.rs +++ b/codex-rs/core/src/mcp_tool_approval_templates.rs @@ -170,7 +170,7 @@ fn render_tool_params( .iter() .filter(|(name, _)| !handled_names.contains(name.as_str())) .collect::>(); - remaining_params.sort_by(|(left_name, _), (right_name, _)| left_name.cmp(right_name)); + remaining_params.sort_by_key(|(name, _)| *name); for (name, value) in remaining_params { if handled_names.contains(name.as_str()) { diff --git a/codex-rs/core/src/test_support.rs b/codex-rs/core/src/test_support.rs index 48eec66c58..e491ca7413 100644 --- a/codex-rs/core/src/test_support.rs +++ b/codex-rs/core/src/test_support.rs @@ -30,7 +30,7 @@ use crate::unified_exec; static TEST_MODEL_PRESETS: Lazy> = Lazy::new(|| { let mut response = bundled_models_response() .unwrap_or_else(|err| panic!("bundled models.json should parse: {err}")); - response.models.sort_by(|a, b| a.priority.cmp(&b.priority)); + response.models.sort_by_key(|model| model.priority); let mut presets: Vec = response.models.into_iter().map(Into::into).collect(); ModelPreset::mark_default_by_picker_visibility(&mut presets); presets diff --git a/codex-rs/core/src/tools/handlers/multi_agents_common.rs b/codex-rs/core/src/tools/handlers/multi_agents_common.rs index 4f459a0d0e..f1e7bcd429 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_common.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_common.rs @@ -105,7 +105,7 @@ pub(crate) fn build_wait_agent_statuses( status: status.clone(), }) .collect::>(); - extras.sort_by(|left, right| left.thread_id.to_string().cmp(&right.thread_id.to_string())); + extras.sort_by_key(|entry| entry.thread_id.to_string()); entries.extend(extras); entries } diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index be6d30c6fa..20f4eae489 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -559,10 +559,8 @@ async fn summarize_context_three_requests_and_instructions() { RolloutItem::TurnContext(_) => { regular_turn_context_count += 1; } - RolloutItem::Compacted(ci) => { - if ci.message == expected_summary_message { - saw_compacted_summary = true; - } + RolloutItem::Compacted(ci) if ci.message == expected_summary_message => { + saw_compacted_summary = true; } _ => {} } diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index d8779bfb05..bd290cad41 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -110,7 +110,7 @@ fn canonical_json(value: &Value) -> Value { match value { Value::Object(map) => { let mut entries = map.iter().collect::>(); - entries.sort_by(|(left_key, _), (right_key, _)| left_key.cmp(right_key)); + entries.sort_by_key(|(left_key, _)| *left_key); Value::Object( entries .into_iter() diff --git a/codex-rs/core/tests/suite/compact_remote_parity.rs b/codex-rs/core/tests/suite/compact_remote_parity.rs index 8979b2cced..22678fc46f 100644 --- a/codex-rs/core/tests/suite/compact_remote_parity.rs +++ b/codex-rs/core/tests/suite/compact_remote_parity.rs @@ -1047,7 +1047,7 @@ fn canonical_json(value: &Value) -> Value { match value { Value::Object(map) => { let mut entries = map.iter().collect::>(); - entries.sort_by(|(left_key, _), (right_key, _)| left_key.cmp(right_key)); + entries.sort_by_key(|(left_key, _)| *left_key); Value::Object( entries .into_iter() diff --git a/codex-rs/core/tests/suite/compact_resume_fork.rs b/codex-rs/core/tests/suite/compact_resume_fork.rs index ed2f1a8e1b..92284b2a1e 100644 --- a/codex-rs/core/tests/suite/compact_resume_fork.rs +++ b/codex-rs/core/tests/suite/compact_resume_fork.rs @@ -638,10 +638,8 @@ async fn snapshot_rollback_followup_turn_trims_context_updates() -> Result<()> { fn normalize_line_endings(value: &mut Value) { match value { - Value::String(text) => { - if text.contains('\r') { - *text = text.replace("\r\n", "\n").replace('\r', "\n"); - } + Value::String(text) if text.contains('\r') => { + *text = text.replace("\r\n", "\n").replace('\r', "\n"); } Value::Array(items) => { for item in items { diff --git a/codex-rs/core/tests/suite/realtime_conversation.rs b/codex-rs/core/tests/suite/realtime_conversation.rs index 05428799ad..b99333e66c 100644 --- a/codex-rs/core/tests/suite/realtime_conversation.rs +++ b/codex-rs/core/tests/suite/realtime_conversation.rs @@ -1835,15 +1835,14 @@ async fn conversation_startup_context_current_thread_selects_many_turns_by_budge "head detail ".repeat(120), "tail detail ".repeat(170), ); - let mut user_turns = (1..=7) + let user_turns = (1..=7) .map(|index| { format!( "short-turn-{index}-start {} short-turn-{index}-end", "detail ".repeat(86) ) }) - .collect::>(); - user_turns.push(latest_long_user_turn.clone()); + .chain([latest_long_user_turn.clone()]); let mut builder = test_codex().with_config({ let realtime_base_url = realtime_server.uri().to_string(); @@ -1858,7 +1857,6 @@ async fn conversation_startup_context_current_thread_selects_many_turns_by_budge // end-to-end startup-context test without paying for a model turn per // fixture entry in platform CI. let history = user_turns - .into_iter() .enumerate() .flat_map(|(index, user_turn)| { let turn_number = index + 1; diff --git a/codex-rs/debug-client/src/client.rs b/codex-rs/debug-client/src/client.rs index 69eb474aaf..90bccf1daa 100644 --- a/codex-rs/debug-client/src/client.rs +++ b/codex-rs/debug-client/src/client.rs @@ -312,10 +312,8 @@ impl AppServerClient { }; match message { - JSONRPCMessage::Response(response) => { - if &response.id == request_id { - return Ok(response); - } + JSONRPCMessage::Response(response) if &response.id == request_id => { + return Ok(response); } JSONRPCMessage::Request(request) => { let _ = handle_server_request(request, &stdin); diff --git a/codex-rs/exec-server/src/client.rs b/codex-rs/exec-server/src/client.rs index aae415f047..e06a736c17 100644 --- a/codex-rs/exec-server/src/client.rs +++ b/codex-rs/exec-server/src/client.rs @@ -218,7 +218,7 @@ impl LazyRemoteExecServerClient { } let next_client = match self.cached_client() { - Some(client) + Some(_client) if matches!( &self.transport_params, ExecServerTransportParams::WebSocketUrl { .. } diff --git a/codex-rs/exec-server/src/fs_sandbox.rs b/codex-rs/exec-server/src/fs_sandbox.rs index 76b0f22b2b..3356791d44 100644 --- a/codex-rs/exec-server/src/fs_sandbox.rs +++ b/codex-rs/exec-server/src/fs_sandbox.rs @@ -137,7 +137,7 @@ fn sandbox_cwd(sandbox: &FileSystemSandboxContext) -> Result Vec { let mut roots = Vec::new(); for path in std::iter::once(runtime_paths.codex_self_exe.as_path()) - .chain(runtime_paths.codex_linux_sandbox_exe.as_deref().into_iter()) + .chain(runtime_paths.codex_linux_sandbox_exe.as_deref()) { if let Some(parent) = path.parent() && let Ok(root) = AbsolutePathBuf::from_absolute_path(parent) diff --git a/codex-rs/hooks/src/schema.rs b/codex-rs/hooks/src/schema.rs index 3f17986dec..bd2ac16dfb 100644 --- a/codex-rs/hooks/src/schema.rs +++ b/codex-rs/hooks/src/schema.rs @@ -707,7 +707,7 @@ fn canonicalize_json(value: &Value) -> Value { Value::Array(items) => Value::Array(items.iter().map(canonicalize_json).collect()), Value::Object(map) => { let mut entries: Vec<_> = map.iter().collect(); - entries.sort_by(|(left, _), (right, _)| left.cmp(right)); + entries.sort_by_key(|(key, _)| *key); let mut sorted = Map::with_capacity(map.len()); for (key, child) in entries { sorted.insert(key.clone(), canonicalize_json(child)); diff --git a/codex-rs/memories/write/src/phase1.rs b/codex-rs/memories/write/src/phase1.rs index 1844ab3b52..21b20759b1 100644 --- a/codex-rs/memories/write/src/phase1.rs +++ b/codex-rs/memories/write/src/phase1.rs @@ -206,7 +206,7 @@ async fn run_jobs( claimed_candidates: Vec, stage_one_context: StageOneRequestContext, ) -> Vec { - futures::stream::iter(claimed_candidates.into_iter()) + futures::stream::iter(claimed_candidates) .map(|claim| { let context = Arc::clone(&context); let config = Arc::clone(&config); diff --git a/codex-rs/models-manager/src/manager.rs b/codex-rs/models-manager/src/manager.rs index af510c8d73..ac4044ac20 100644 --- a/codex-rs/models-manager/src/manager.rs +++ b/codex-rs/models-manager/src/manager.rs @@ -107,7 +107,7 @@ pub trait ModelsManager: fmt::Debug + Send + Sync { /// Build picker-ready presets from the active catalog snapshot. fn build_available_models(&self, mut remote_models: Vec) -> Vec { - remote_models.sort_by(|a, b| a.priority.cmp(&b.priority)); + remote_models.sort_by_key(|model| model.priority); let mut presets: Vec = remote_models.into_iter().map(Into::into).collect(); let uses_codex_backend = self diff --git a/codex-rs/models-manager/src/test_support.rs b/codex-rs/models-manager/src/test_support.rs index aff2838907..7397231689 100644 --- a/codex-rs/models-manager/src/test_support.rs +++ b/codex-rs/models-manager/src/test_support.rs @@ -14,7 +14,7 @@ pub fn get_model_offline_for_tests(model: Option<&str>) -> String { return model.to_string(); } let mut response = bundled_models_response().unwrap_or_default(); - response.models.sort_by(|a, b| a.priority.cmp(&b.priority)); + response.models.sort_by_key(|model| model.priority); let presets: Vec = response.models.into_iter().map(Into::into).collect(); presets .iter() diff --git a/codex-rs/rust-toolchain.toml b/codex-rs/rust-toolchain.toml index 954b684895..910c65114e 100644 --- a/codex-rs/rust-toolchain.toml +++ b/codex-rs/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.93.0" +channel = "1.95.0" components = ["clippy", "rustfmt", "rust-src"] diff --git a/codex-rs/scripts/setup-windows.ps1 b/codex-rs/scripts/setup-windows.ps1 index df87731380..a8fe0c2f75 100644 --- a/codex-rs/scripts/setup-windows.ps1 +++ b/codex-rs/scripts/setup-windows.ps1 @@ -179,7 +179,7 @@ if (-not (Ensure-Command 'cargo')) { Write-Host "==> Configuring Rust toolchain per rust-toolchain.toml" -ForegroundColor Cyan # Pin to the workspace toolchain and install components -$toolchain = '1.93.0' +$toolchain = '1.95.0' & rustup toolchain install $toolchain --profile minimal | Out-Host & rustup default $toolchain | Out-Host & rustup component add clippy rustfmt rust-src --toolchain $toolchain | Out-Host diff --git a/codex-rs/state/src/runtime/memories.rs b/codex-rs/state/src/runtime/memories.rs index 350d22cc6d..3101500796 100644 --- a/codex-rs/state/src/runtime/memories.rs +++ b/codex-rs/state/src/runtime/memories.rs @@ -511,7 +511,7 @@ WHERE so.thread_id = ? AND so.source_updated_at = ? } } - selected.sort_by(|a, b| a.thread_id.to_string().cmp(&b.thread_id.to_string())); + selected.sort_by_key(|entry| entry.thread_id.to_string()); Ok(selected) } diff --git a/codex-rs/tui/src/app/thread_events.rs b/codex-rs/tui/src/app/thread_events.rs index b0c9e7e1be..a4e514a433 100644 --- a/codex-rs/tui/src/app/thread_events.rs +++ b/codex-rs/tui/src/app/thread_events.rs @@ -104,10 +104,10 @@ impl ThreadEventStore { ServerNotification::TurnStarted(turn) => { self.active_turn_id = Some(turn.turn.id.clone()); } - ServerNotification::TurnCompleted(turn) => { - if self.active_turn_id.as_deref() == Some(turn.turn.id.as_str()) { - self.active_turn_id = None; - } + ServerNotification::TurnCompleted(turn) + if self.active_turn_id.as_deref() == Some(turn.turn.id.as_str()) => + { + self.active_turn_id = None; } ServerNotification::ThreadClosed(_) => { self.active_turn_id = None; diff --git a/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs b/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs index 13cbc34daa..16baedc097 100644 --- a/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs +++ b/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs @@ -1188,11 +1188,9 @@ impl BottomPaneView for RequestUserInputOverlay { KeyCode::Backspace | KeyCode::Delete => { self.clear_selection(); } - KeyCode::Tab => { - if self.selected_option_index().is_some() { - self.focus = Focus::Notes; - self.ensure_selected_for_notes(); - } + KeyCode::Tab if self.selected_option_index().is_some() => { + self.focus = Focus::Notes; + self.ensure_selected_for_notes(); } KeyCode::Enter => { let has_selection = self.selected_option_index().is_some(); diff --git a/codex-rs/tui/src/chatwidget/protocol.rs b/codex-rs/tui/src/chatwidget/protocol.rs index b3b58cbc82..6d56330367 100644 --- a/codex-rs/tui/src/chatwidget/protocol.rs +++ b/codex-rs/tui/src/chatwidget/protocol.rs @@ -332,10 +332,8 @@ impl ChatWidget { reasoning_effort, agents_states, }), - ThreadItem::EnteredReviewMode { review, .. } => { - if !from_replay { - self.enter_review_mode_with_hint(review, /*from_replay*/ false); - } + ThreadItem::EnteredReviewMode { review, .. } if !from_replay => { + self.enter_review_mode_with_hint(review, /*from_replay*/ false); } _ => {} } diff --git a/codex-rs/tui/src/history_cell/mcp.rs b/codex-rs/tui/src/history_cell/mcp.rs index 5b761c1ef5..7b1e8eebc4 100644 --- a/codex-rs/tui/src/history_cell/mcp.rs +++ b/codex-rs/tui/src/history_cell/mcp.rs @@ -358,7 +358,7 @@ pub(crate) fn new_mcp_tools_output( let effective_servers = config.mcp_servers.get().clone(); let mut servers: Vec<_> = effective_servers.iter().collect(); - servers.sort_by(|(a, _), (b, _)| a.cmp(b)); + servers.sort_by_key(|(server, _)| *server); for (server, cfg) in servers { let prefix = qualified_mcp_tool_name_prefix(server); @@ -430,7 +430,7 @@ pub(crate) fn new_mcp_tools_output( && !headers.is_empty() { let mut pairs: Vec<_> = headers.iter().collect(); - pairs.sort_by(|(a, _), (b, _)| a.cmp(b)); + pairs.sort_by_key(|(name, _)| *name); let display = pairs .into_iter() .map(|(name, _)| format!("{name}=*****")) @@ -442,7 +442,7 @@ pub(crate) fn new_mcp_tools_output( && !headers.is_empty() { let mut pairs: Vec<_> = headers.iter().collect(); - pairs.sort_by(|(a, _), (b, _)| a.cmp(b)); + pairs.sort_by_key(|(name, _)| *name); let display = pairs .into_iter() .map(|(name, var)| format!("{name}={var}")) diff --git a/codex-rs/tui/src/history_cell/session.rs b/codex-rs/tui/src/history_cell/session.rs index 46bbb35eed..23e8836454 100644 --- a/codex-rs/tui/src/history_cell/session.rs +++ b/codex-rs/tui/src/history_cell/session.rs @@ -58,7 +58,7 @@ fn with_border_internal( let span_count = line.spans.len(); let mut spans: Vec> = Vec::with_capacity(span_count + 4); spans.push(Span::from("│ ").dim()); - spans.extend(line.into_iter()); + spans.extend(line); if used_width < content_width { spans.push(Span::from(" ".repeat(content_width - used_width)).dim()); } diff --git a/codex-rs/tui/src/multi_agents.rs b/codex-rs/tui/src/multi_agents.rs index 4fb5c8c265..3273c610ae 100644 --- a/codex-rs/tui/src/multi_agents.rs +++ b/codex-rs/tui/src/multi_agents.rs @@ -516,7 +516,7 @@ fn wait_complete_lines( .then(|| (parsed_thread_id, agent_metadata(parsed_thread_id), status)) }) .collect::>(); - extras.sort_by(|left, right| left.0.to_string().cmp(&right.0.to_string())); + extras.sort_by_key(|entry| entry.0.to_string()); entries.extend(extras); if entries.is_empty() { diff --git a/codex-rs/tui/src/resume_picker.rs b/codex-rs/tui/src/resume_picker.rs index 63f0c8773b..6e8b65decd 100644 --- a/codex-rs/tui/src/resume_picker.rs +++ b/codex-rs/tui/src/resume_picker.rs @@ -1159,23 +1159,21 @@ impl PickerState { self.request_frame(); } } - _ if allow_plain_char_navigation && self.list_keymap.jump_top.is_pressed(key) => { - if !self.filtered_rows.is_empty() { + _ if allow_plain_char_navigation && self.list_keymap.jump_top.is_pressed(key) + && !self.filtered_rows.is_empty() => { self.selected = 0; self.ensure_selected_visible(); self.request_frame(); } - } - _ if allow_plain_char_navigation && self.list_keymap.jump_bottom.is_pressed(key) => { - if !self.filtered_rows.is_empty() { + _ if allow_plain_char_navigation && self.list_keymap.jump_bottom.is_pressed(key) + && !self.filtered_rows.is_empty() => { self.selected = self.filtered_rows.len().saturating_sub(1); self.ensure_selected_visible(); self.maybe_load_more_for_scroll(); self.request_frame(); } - } - _ if allow_plain_char_navigation && self.list_keymap.page_down.is_pressed(key) => { - if !self.filtered_rows.is_empty() { + _ if allow_plain_char_navigation && self.list_keymap.page_down.is_pressed(key) + && !self.filtered_rows.is_empty() => { let step = self.view_rows.unwrap_or(10).max(1); let target = self.selected.saturating_add(step); let max_index = self.filtered_rows.len().saturating_sub(1); @@ -1189,7 +1187,6 @@ impl PickerState { } self.request_frame(); } - } KeyEvent { code: KeyCode::Tab, .. } => { @@ -1222,16 +1219,15 @@ impl PickerState { code: KeyCode::Char(c), modifiers, .. - } => { + } // basic text input for search if !modifiers.contains(KeyModifiers::CONTROL) && !modifiers.contains(KeyModifiers::ALT) - { + => { let mut new_query = self.query.clone(); new_query.push(c); self.set_query(new_query); } - } _ => {} } Ok(None) diff --git a/codex-rs/tui/src/text_formatting.rs b/codex-rs/tui/src/text_formatting.rs index ae93890bbb..a89f392ebb 100644 --- a/codex-rs/tui/src/text_formatting.rs +++ b/codex-rs/tui/src/text_formatting.rs @@ -284,7 +284,7 @@ pub(crate) fn center_truncate_path(path: &str, max_width: usize) -> String { } }; - for (left_count, right_count) in prioritized.into_iter().chain(fallback.into_iter()) { + for (left_count, right_count) in prioritized.into_iter().chain(fallback) { let mut segments: Vec> = raw_segments[..left_count] .iter() .map(|seg| Segment { diff --git a/codex-rs/tui/src/theme_picker.rs b/codex-rs/tui/src/theme_picker.rs index 90393ed0b1..1c3171045e 100644 --- a/codex-rs/tui/src/theme_picker.rs +++ b/codex-rs/tui/src/theme_picker.rs @@ -199,10 +199,9 @@ fn render_preview( 0 }; - let mut y = area.y.saturating_add(top_pad); let render_width = area.width.saturating_sub(left_pad); let style_context = current_diff_render_style_context(); - for (idx, row) in preview_rows.iter().enumerate() { + for (y, (idx, row)) in (area.y.saturating_add(top_pad)..).zip(preview_rows.iter().enumerate()) { if y >= area.y + area.height { break; } @@ -232,7 +231,6 @@ fn render_preview( Rect::new(area.x.saturating_add(left_pad), y, render_width, 1), buf, ); - y += 1; } } diff --git a/codex-rs/utils/cli/src/format_env_display.rs b/codex-rs/utils/cli/src/format_env_display.rs index cee63bb32b..714b44c312 100644 --- a/codex-rs/utils/cli/src/format_env_display.rs +++ b/codex-rs/utils/cli/src/format_env_display.rs @@ -8,7 +8,7 @@ pub fn format_env_display>( if let Some(map) = env { let mut pairs: Vec<_> = map.iter().collect(); - pairs.sort_by(|(a, _), (b, _)| a.cmp(b)); + pairs.sort_by_key(|(key, _)| *key); parts.extend(pairs.into_iter().map(|(key, _)| format!("{key}=*****"))); } From 8ed38fe38eb99dd51a0eef33366a45a392733b82 Mon Sep 17 00:00:00 2001 From: efrazer-oai Date: Tue, 26 May 2026 22:09:54 -0700 Subject: [PATCH 13/87] fix: add noninteractive install script mode (#21567) # Summary The Codex standalone installers can pause after installation to ask about an older managed install or launching Codex. That makes unattended bootstrap and update flows hard to complete reliably. This PR adds noninteractive installer control on macOS/Linux and Windows through `CODEX_NON_INTERACTIVE=1`. Noninteractive operation is environment-only, which gives automated callers one stable way to suppress prompts. When a noninteractive install leaves an older npm, bun, or brew-managed Codex installed, the standalone bin is configured ahead of that command on `PATH` so the newly installed Codex is the one future launches select. It also supports `CODEX_RELEASE` for callers that select a release through environment variables while retaining the existing explicit release inputs. Release selection accepts `latest`, stable `x.y.z` versions, and Codex prereleases written as `rust-v0.134.0-alpha.3`, `v0.134.0-alpha.3`, or `0.134.0-alpha.3`; it validates that shape before constructing release requests. # Stack 1. [#21567](https://github.com/openai/codex/pull/21567) - Adds release and noninteractive environment controls to the installers. (current) 2. [#24637](https://github.com/openai/codex/pull/24637) - Runs standalone updater installs with `CODEX_NON_INTERACTIVE=1`. 3. [#24639](https://github.com/openai/codex/pull/24639) - Removes explicit release argument inputs in favor of `CODEX_RELEASE`. # Evidence | Before | After | | --- | --- | | ![Interactive install prompts](https://github.com/user-attachments/assets/feecb45a-7087-4681-8775-ba57b07e97fa) | ![Noninteractive install completes without prompts](https://github.com/user-attachments/assets/53dcc791-383a-46e2-9a95-3b37b80ae053) | Environment-controlled macOS install with an existing npm-managed Codex on `PATH`: https://github.com/user-attachments/assets/442e0b5b-4a32-4bf5-996b-68784777380d # Design decisions Windows installs using the older standalone bin layout still require an interactive migration confirmation. Noninteractive mode does not auto-migrate that existing directory because replacing it is a destructive transition for an early, limited-use layout; unattended installs on that layout fail with an instruction to rerun interactively. # Testing Tests: installer syntax validation, release-selector acceptance and rejection coverage including PowerShell `Latest` compatibility, macOS live-terminal installer smoke testing with environment-controlled stable and prerelease installation and competing PATH precedence, shell rejection of the omitted noninteractive flag, and Windows ARM64 PowerShell smoke testing with environment-only noninteractive behavior, retained release input, and competing PATH precedence through Parallels. --- scripts/install/install.ps1 | 59 ++++++++++++++++++++++++++++++++++--- scripts/install/install.sh | 32 ++++++++++++++++++-- 2 files changed, 85 insertions(+), 6 deletions(-) diff --git a/scripts/install/install.ps1 b/scripts/install/install.ps1 index 8ebc4fbb82..6973d482e2 100644 --- a/scripts/install/install.ps1 +++ b/scripts/install/install.ps1 @@ -1,11 +1,18 @@ +[CmdletBinding()] param( - [string]$Release = "latest" + [string]$Release = $env:CODEX_RELEASE ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" +if ([string]::IsNullOrWhiteSpace($Release)) { + $Release = "latest" +} + +$NonInteractive = $env:CODEX_NON_INTERACTIVE -match "^(?i:1|true|yes)$" + function Write-Step { param( [string]$Message @@ -27,6 +34,10 @@ function Prompt-YesNo { [string]$Prompt ) + if ($NonInteractive) { + return $false + } + if ([Console]::IsInputRedirected -or [Console]::IsOutputRedirected) { return $false } @@ -55,6 +66,16 @@ function Normalize-Version { return $RawVersion } +function Assert-ValidReleaseVersion { + param( + [string]$Version + ) + + if ($Version -cne "latest" -and $Version -cnotmatch "^[0-9]+\.[0-9]+\.[0-9]+(?:-(?:alpha|beta)(?:\.[0-9]+)?)?$") { + throw "Invalid Codex release version: $Version. Expected latest or x.y.z[-alpha[.N]|-beta[.N]]." + } +} + function Find-ReleaseAssetMetadata { param( [string]$AssetName, @@ -141,6 +162,22 @@ function Path-Contains { return $false } +function Prepend-PathEntry { + param( + [string]$PathValue, + [string]$Entry + ) + + $needle = $Entry.TrimEnd("\") + $segments = @($Entry) + if (-not [string]::IsNullOrWhiteSpace($PathValue)) { + $segments += $PathValue.Split(";", [System.StringSplitOptions]::RemoveEmptyEntries) | + Where-Object { $_.TrimEnd("\") -ine $needle } + } + + return ($segments -join ";") +} + function Invoke-WithInstallLock { param( [string]$LockPath, @@ -181,6 +218,7 @@ function Remove-StaleInstallArtifacts { function Resolve-Version { $normalizedVersion = Normalize-Version -RawVersion $Release + Assert-ValidReleaseVersion -Version $normalizedVersion if ($normalizedVersion -ne "latest") { return $normalizedVersion } @@ -191,7 +229,9 @@ function Resolve-Version { exit 1 } - return (Normalize-Version -RawVersion $release.tag_name) + $resolvedVersion = Normalize-Version -RawVersion $release.tag_name + Assert-ValidReleaseVersion -Version $resolvedVersion + return $resolvedVersion } function Get-VersionFromBinary { @@ -839,7 +879,16 @@ try { Maybe-HandleConflictingInstall -Conflict $conflictingInstall $userPath = [Environment]::GetEnvironmentVariable("Path", "User") -if (-not (Path-Contains -PathValue $userPath -Entry $visibleBinDir)) { +$prioritizeVisibleBin = $null -ne $conflictingInstall +if ($prioritizeVisibleBin) { + $newUserPath = Prepend-PathEntry -PathValue $userPath -Entry $visibleBinDir + if ($newUserPath -cne $userPath) { + [Environment]::SetEnvironmentVariable("Path", $newUserPath, "User") + Write-Step "PATH updated for future PowerShell sessions." + } else { + Write-Step "$visibleBinDir is already first on PATH." + } +} elseif (-not (Path-Contains -PathValue $userPath -Entry $visibleBinDir)) { if ([string]::IsNullOrWhiteSpace($userPath)) { $newUserPath = $visibleBinDir } else { @@ -854,7 +903,9 @@ if (-not (Path-Contains -PathValue $userPath -Entry $visibleBinDir)) { Write-Step "PATH is already configured for future PowerShell sessions." } -if (-not (Path-Contains -PathValue $env:Path -Entry $visibleBinDir)) { +if ($prioritizeVisibleBin) { + $env:Path = Prepend-PathEntry -PathValue $env:Path -Entry $visibleBinDir +} elseif (-not (Path-Contains -PathValue $env:Path -Entry $visibleBinDir)) { if ([string]::IsNullOrWhiteSpace($env:Path)) { $env:Path = $visibleBinDir } else { diff --git a/scripts/install/install.sh b/scripts/install/install.sh index cc4fc91345..23980b0c5e 100755 --- a/scripts/install/install.sh +++ b/scripts/install/install.sh @@ -2,7 +2,8 @@ set -eu -RELEASE="latest" +RELEASE="${CODEX_RELEASE:-latest}" +NON_INTERACTIVE="${CODEX_NON_INTERACTIVE:-false}" BIN_DIR="${CODEX_INSTALL_DIR:-$HOME/.local/bin}" BIN_PATH="$BIN_DIR/codex" @@ -46,6 +47,19 @@ normalize_version() { esac } +validate_version() { + version="$1" + + if [ "$version" = "latest" ]; then + return + fi + + if ! printf '%s\n' "$version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?$'; then + echo "Invalid Codex release version: $version. Expected latest or x.y.z[-alpha[.N]|-beta[.N]]." >&2 + exit 1 + fi +} + parse_args() { while [ "$#" -gt 0 ]; do case "$1" in @@ -60,6 +74,10 @@ parse_args() { --help | -h) cat </dev/null; then printf '%s [y/N] ' "$prompt" >/dev/tty if ! IFS= read -r answer Date: Tue, 26 May 2026 22:22:34 -0700 Subject: [PATCH 14/87] Allow runtime enablement for remote plugins (#24707) experimentalFeature/enablement/set now accepts remote_plugin as a supported runtime feature key --- codex-rs/app-server/README.md | 2 +- .../src/request_processors/config_processor.rs | 1 + .../tests/suite/v2/experimental_feature_list.rs | 11 ++++++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index db76e363ec..88edc22f9e 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -192,7 +192,7 @@ Example with notification opt-out: - `modelProvider/capabilities/read` — read provider-level capabilities for the currently configured model provider. - `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. Pass `threadId` when showing feature state for an existing loaded thread so `enabled` is computed from that thread's refreshed config, including project-local config for the thread's cwd; if omitted, the server uses its default config resolution context. For non-beta flags, `displayName`/`description`/`announcement` are `null`. - `permissionProfile/list` — beta; list available permission profile ids with optional display `description` text, using cursor pagination. Pass `cwd` when the caller needs project-local `[permissions.]` entries to be included in the current catalog view. -- `experimentalFeature/enablement/set` — patch the in-memory process-wide runtime feature enablement for the currently supported feature keys (`apps`, `memories`, `plugins`, `tool_suggest`, `tool_call_mcp_elicitation`). For each feature, precedence is: cloud requirements > --enable > config.toml > experimentalFeature/enablement/set (new) > code default. +- `experimentalFeature/enablement/set` — patch the in-memory process-wide runtime feature enablement for currently supported feature keys. For each feature, precedence is: cloud requirements > --enable > config.toml > experimentalFeature/enablement/set (new) > code default. - `environment/add` — experimental; add or replace a named remote environment by `environmentId` and `execServerUrl` for later selection by `thread/start` or `turn/start`; returns `{}` and does not change the default environment. - `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). Built-in presets do not select a model; the Plan preset selects medium reasoning effort. This response omits built-in developer instructions; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly. - `skills/list` — list skills for one or more `cwd` values (optional `forceReload`). 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 72948270a6..39dbd17fe6 100644 --- a/codex-rs/app-server/src/request_processors/config_processor.rs +++ b/codex-rs/app-server/src/request_processors/config_processor.rs @@ -54,6 +54,7 @@ const SUPPORTED_EXPERIMENTAL_FEATURE_ENABLEMENT: &[&str] = &[ "mentions_v2", "plugins", "remote_control", + "remote_plugin", "tool_suggest", "tool_call_mcp_elicitation", ]; diff --git a/codex-rs/app-server/tests/suite/v2/experimental_feature_list.rs b/codex-rs/app-server/tests/suite/v2/experimental_feature_list.rs index d6101995fa..782dba3edf 100644 --- a/codex-rs/app-server/tests/suite/v2/experimental_feature_list.rs +++ b/codex-rs/app-server/tests/suite/v2/experimental_feature_list.rs @@ -340,6 +340,7 @@ async fn experimental_feature_enablement_set_only_updates_named_features() -> Re BTreeMap::from([ ("memories".to_string(), true), ("plugins".to_string(), true), + ("remote_plugin".to_string(), true), ("tool_suggest".to_string(), true), ("tool_call_mcp_elicitation".to_string(), false), ]), @@ -352,6 +353,7 @@ async fn experimental_feature_enablement_set_only_updates_named_features() -> Re enablement: BTreeMap::from([ ("memories".to_string(), true), ("plugins".to_string(), true), + ("remote_plugin".to_string(), true), ("tool_suggest".to_string(), true), ("tool_call_mcp_elicitation".to_string(), false), ]), @@ -381,6 +383,13 @@ async fn experimental_feature_enablement_set_only_updates_named_features() -> Re .and_then(|features| features.get("plugins")), Some(&json!(true)) ); + assert_eq!( + config + .additional + .get("features") + .and_then(|features| features.get("remote_plugin")), + Some(&json!(true)) + ); assert_eq!( config .additional @@ -474,7 +483,7 @@ async fn experimental_feature_enablement_set_rejects_non_allowlisted_feature() - ); assert!( error.message.contains( - "apps, memories, mentions_v2, plugins, remote_control, tool_suggest, tool_call_mcp_elicitation" + "apps, memories, mentions_v2, plugins, remote_control, remote_plugin, tool_suggest, tool_call_mcp_elicitation" ), "{}", error.message From e88626621bad4bb2655fa8b5fc7d2f73d68e168d Mon Sep 17 00:00:00 2001 From: Dylan Hurd Date: Wed, 27 May 2026 00:23:15 -0700 Subject: [PATCH 15/87] fix(auto-review) skip legacy notify for auto review threads (#24714) ## Summary Clear inherited legacy `notify` from Guardian review session config, since we should not be passing auto review threads into `notify` targets. Keeps legacy notify payload and hook runtime behavior unchanged for normal user turns. ## Testing - [x] add a Guardian config regression and dedicated Guardian integration test so review sessions cannot inherit parent notify hooks --- codex-rs/core/src/guardian/review_session.rs | 1 + codex-rs/core/src/guardian/tests.rs | 19 +++ codex-rs/core/tests/suite/guardian_review.rs | 169 +++++++++++++++++++ codex-rs/core/tests/suite/mod.rs | 2 + 4 files changed, 191 insertions(+) create mode 100644 codex-rs/core/tests/suite/guardian_review.rs diff --git a/codex-rs/core/src/guardian/review_session.rs b/codex-rs/core/src/guardian/review_session.rs index e56d5ce170..67f733e0ba 100644 --- a/codex-rs/core/src/guardian/review_session.rs +++ b/codex-rs/core/src/guardian/review_session.rs @@ -905,6 +905,7 @@ pub(crate) fn build_guardian_review_session_config( .map(guardian_policy_prompt_with_config) .unwrap_or_else(guardian_policy_prompt), ); + guardian_config.notify = None; guardian_config.developer_instructions = None; guardian_config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never); guardian_config diff --git a/codex-rs/core/src/guardian/tests.rs b/codex-rs/core/src/guardian/tests.rs index 0544b27770..00f0d9be38 100644 --- a/codex-rs/core/src/guardian/tests.rs +++ b/codex-rs/core/src/guardian/tests.rs @@ -2215,6 +2215,25 @@ async fn guardian_review_session_config_clears_parent_developer_instructions() { ); } +#[tokio::test] +async fn guardian_review_session_config_clears_legacy_notify() { + let mut parent_config = test_config().await; + parent_config.notify = Some(vec![ + "/path/to/notify".to_string(), + "turn-ended".to_string(), + ]); + + let guardian_config = build_guardian_review_session_config_for_test( + &parent_config, + /*live_network_config*/ None, + "active-model", + /*reasoning_effort*/ None, + ) + .expect("guardian config"); + + assert_eq!(guardian_config.notify, None); +} + #[tokio::test] async fn guardian_review_session_config_uses_live_network_proxy_state() { let mut parent_config = test_config().await; diff --git a/codex-rs/core/tests/suite/guardian_review.rs b/codex-rs/core/tests/suite/guardian_review.rs new file mode 100644 index 0000000000..5b76d09ae9 --- /dev/null +++ b/codex-rs/core/tests/suite/guardian_review.rs @@ -0,0 +1,169 @@ +#![cfg(not(target_os = "windows"))] + +use anyhow::Result; +use codex_core::config::Constrained; +use codex_core::sandboxing::SandboxPermissions; +use codex_protocol::config_types::ApprovalsReviewer; +use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::EventMsg; +use codex_protocol::protocol::Op; +use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::user_input::UserInput; +use core_test_support::fs_wait; +use core_test_support::responses::ev_assistant_message; +use core_test_support::responses::ev_completed; +use core_test_support::responses::ev_function_call; +use core_test_support::responses::ev_response_created; +use core_test_support::responses::mount_sse_sequence; +use core_test_support::responses::sse; +use core_test_support::responses::start_mock_server; +use core_test_support::skip_if_no_network; +use core_test_support::skip_if_sandbox; +use core_test_support::test_codex::test_codex; +use core_test_support::wait_for_event; +use pretty_assertions::assert_eq; +use serde_json::Value; +use serde_json::json; +use std::fs; +use std::os::unix::fs::PermissionsExt; +use std::time::Duration; +use tempfile::TempDir; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn guardian_review_session_does_not_inherit_legacy_notify() -> Result<()> { + skip_if_no_network!(Ok(())); + skip_if_sandbox!(Ok(())); + + let server = start_mock_server().await; + let approval_policy = AskForApproval::OnRequest; + let sandbox_policy = SandboxPolicy::WorkspaceWrite { + writable_roots: vec![], + network_access: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, + }; + + let notify_dir = TempDir::new()?; + let notify_script = notify_dir.path().join("notify.sh"); + fs::write( + ¬ify_script, + r#"#!/bin/bash +set -e +payload_path="$(dirname "${0}")/notify.jsonl" +printf '%s\n' "${@: -1}" >> "${payload_path}""#, + )?; + fs::set_permissions(¬ify_script, fs::Permissions::from_mode(0o755))?; + let notify_file = notify_dir.path().join("notify.jsonl"); + let notify_script_str = notify_script.to_str().unwrap().to_string(); + let sandbox_policy_for_config = sandbox_policy.clone(); + + let mut builder = test_codex().with_config(move |config| { + config.notify = Some(vec![notify_script_str]); + config.permissions.approval_policy = Constrained::allow_any(approval_policy); + config + .set_legacy_sandbox_policy(sandbox_policy_for_config) + .expect("set sandbox policy"); + }); + let test = builder.build(&server).await?; + + let output_file = test.cwd.path().join("guardian-review-notify.txt"); + let command = format!("printf guardian-approved > {}", output_file.display()); + let tool_args = json!({ + "cmd": command, + "yield_time_ms": 1_000_u64, + "sandbox_permissions": SandboxPermissions::RequireEscalated, + "justification": "Exercise Guardian approval routing.", + }); + let responses = mount_sse_sequence( + &server, + vec![ + sse(vec![ + ev_response_created("resp-parent-tool"), + ev_function_call( + "exec-call", + "exec_command", + &serde_json::to_string(&tool_args)?, + ), + ev_completed("resp-parent-tool"), + ]), + sse(vec![ + ev_response_created("resp-guardian-review"), + ev_assistant_message( + "msg-guardian-review", + &json!({ + "risk_level": "low", + "user_authorization": "high", + "outcome": "allow", + "rationale": "The command writes a marker file in the workspace.", + }) + .to_string(), + ), + ev_completed("resp-guardian-review"), + ]), + sse(vec![ + ev_response_created("resp-parent-done"), + ev_assistant_message("msg-parent-done", "done"), + ev_completed("resp-parent-done"), + ]), + ], + ) + .await; + + test.codex + .submit(Op::UserInput { + environments: None, + items: vec![UserInput::Text { + text: "run a command that requires Guardian review".into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + responsesapi_client_metadata: None, + thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { + cwd: Some(test.cwd.path().to_path_buf()), + approval_policy: Some(approval_policy), + approvals_reviewer: Some(ApprovalsReviewer::AutoReview), + sandbox_policy: Some(sandbox_policy), + ..Default::default() + }, + }) + .await?; + wait_for_event(&test.codex, |event| { + matches!(event, EventMsg::TurnComplete(_)) + }) + .await; + + let guardian_request = responses + .requests() + .into_iter() + .find(|request| request.body_contains_text("Exercise Guardian approval routing.")) + .expect("expected Guardian review request"); + assert!(guardian_request.body_contains_text(&command)); + + fs_wait::wait_for_path_exists(¬ify_file, Duration::from_secs(5)).await?; + tokio::time::sleep(Duration::from_millis(100)).await; + let notify_payload_raw = tokio::fs::read_to_string(¬ify_file).await?; + let payloads: Vec = notify_payload_raw + .lines() + .map(serde_json::from_str::) + .collect::>()?; + + assert_eq!( + payloads.len(), + 1, + "unexpected notify payloads: {payloads:?}" + ); + assert_eq!( + payloads[0]["input-messages"], + json!(["run a command that requires Guardian review"]) + ); + assert_eq!(payloads[0]["last-assistant-message"], json!("done")); + assert!( + !notify_payload_raw.contains( + "The following is the Codex agent history whose request action you are assessing." + ), + "Guardian review transcript leaked into legacy notify payload: {notify_payload_raw}" + ); + assert_eq!(fs::read_to_string(&output_file)?, "guardian-approved"); + + Ok(()) +} diff --git a/codex-rs/core/tests/suite/mod.rs b/codex-rs/core/tests/suite/mod.rs index f47de672bd..f4ab6c1d6d 100644 --- a/codex-rs/core/tests/suite/mod.rs +++ b/codex-rs/core/tests/suite/mod.rs @@ -50,6 +50,8 @@ mod deprecation_notice; mod exec; mod exec_policy; mod fork_thread; +#[cfg(not(target_os = "windows"))] +mod guardian_review; mod hierarchical_agents; #[cfg(not(target_os = "windows"))] mod hooks; From ca2e343f6564faaf740f50a0cf27b2092b6c40fd Mon Sep 17 00:00:00 2001 From: Celia Chen Date: Wed, 27 May 2026 03:48:40 -0700 Subject: [PATCH 16/87] Revert "Add Bedrock Mantle GovCloud region (#23860)" (#24690) This reverts commit 5381240f57fe326b13bc81325f3c61596592fc7a. Gov cloud should not be supported # External (non-OpenAI) Pull Request Requirements External code contributions are by invitation only. Please read the dedicated "Contributing" markdown file for details: https://github.com/openai/codex/blob/main/docs/contributing.md If your PR conforms to our contribution guidelines, replace this text with a detailed and high quality description of your changes. Include a link to a bug report or enhancement request. --- codex-rs/model-provider/src/amazon_bedrock/mantle.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/codex-rs/model-provider/src/amazon_bedrock/mantle.rs b/codex-rs/model-provider/src/amazon_bedrock/mantle.rs index e5b497d355..7881845e45 100644 --- a/codex-rs/model-provider/src/amazon_bedrock/mantle.rs +++ b/codex-rs/model-provider/src/amazon_bedrock/mantle.rs @@ -7,11 +7,10 @@ use super::auth::BedrockAuthMethod; use super::auth::resolve_auth_method; const BEDROCK_MANTLE_SERVICE_NAME: &str = "bedrock-mantle"; -const BEDROCK_MANTLE_SUPPORTED_REGIONS: [&str; 13] = [ +const BEDROCK_MANTLE_SUPPORTED_REGIONS: [&str; 12] = [ "us-east-2", "us-east-1", "us-west-2", - "us-gov-west-1", "ap-southeast-3", "ap-south-1", "ap-northeast-1", @@ -73,10 +72,6 @@ mod tests { base_url("ap-northeast-1").expect("supported region"), "https://bedrock-mantle.ap-northeast-1.api.aws/openai/v1" ); - assert_eq!( - base_url("us-gov-west-1").expect("supported region"), - "https://bedrock-mantle.us-gov-west-1.api.aws/openai/v1" - ); } #[test] From bc005029bdd88749f71be5fc9134b6a5b2aea941 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Wed, 27 May 2026 13:00:06 +0200 Subject: [PATCH 17/87] feat: handle goal usage limits in goal extension (#24628) ## Why The extracted goal runtime needs a host-callable path for turns that stop because the workspace usage limit is reached. In that case, any in-turn goal progress should be accounted before the goal becomes terminal, and active goal accounting must be cleared so later tool-finish or turn-stop handling does not keep charging usage to a stopped goal. ## What changed - Adds `GoalRuntimeHandle::usage_limit_active_goal_for_turn`, which accounts current active-goal progress, marks the active or budget-limited thread goal as `UsageLimited`, records terminal metrics when the status changes, clears active goal accounting, and emits the updated goal event. - Covers both active and budget-limited goals in `ext/goal/tests/goal_extension_backend.rs`, including the invariant that later token/tool events do not add usage after the goal has been usage-limited. ## Testing - Added `usage_limit_active_goal_accounts_progress_and_clears_accounting`. - Added `usage_limit_budget_limited_goal_accounts_remaining_progress`. --- codex-rs/ext/goal/src/accounting.rs | 11 + codex-rs/ext/goal/src/runtime.rs | 48 ++++ .../ext/goal/tests/goal_extension_backend.rs | 245 +++++++++++++++++- 3 files changed, 303 insertions(+), 1 deletion(-) diff --git a/codex-rs/ext/goal/src/accounting.rs b/codex-rs/ext/goal/src/accounting.rs index 2a679837cc..e0a84f5f8b 100644 --- a/codex-rs/ext/goal/src/accounting.rs +++ b/codex-rs/ext/goal/src/accounting.rs @@ -83,6 +83,17 @@ impl GoalAccountingState { self.inner().current_turn_id.clone() } + pub(crate) fn turn_is_current_active_goal(&self, turn_id: &str) -> bool { + let inner = self.inner(); + if inner.current_turn_id.as_deref() != Some(turn_id) { + return false; + } + let Some(turn) = inner.turns.get(turn_id) else { + return false; + }; + turn.account_tokens && turn.active_goal_id.is_some() + } + pub(crate) fn record_token_usage( &self, turn_id: impl Into, diff --git a/codex-rs/ext/goal/src/runtime.rs b/codex-rs/ext/goal/src/runtime.rs index 0139cc2c52..16a4ff6a2f 100644 --- a/codex-rs/ext/goal/src/runtime.rs +++ b/codex-rs/ext/goal/src/runtime.rs @@ -192,6 +192,54 @@ impl GoalRuntimeHandle { Ok(()) } + pub async fn usage_limit_active_goal_for_turn(&self, turn_id: &str) -> Result<(), String> { + if !self.is_enabled() { + return Ok(()); + } + + if !self + .inner + .accounting_state + .turn_is_current_active_goal(turn_id) + { + return Ok(()); + } + + let progress_event_id = format!("{turn_id}:usage-limit-progress"); + self.account_active_goal_progress( + turn_id, + progress_event_id.as_str(), + codex_state::GoalAccountingMode::ActiveOnly, + BudgetLimitedGoalDisposition::ClearActive, + ) + .await?; + + let previous_status = self + .current_goal_status_for_metrics(/*expected_goal_id*/ None) + .await?; + let Some(goal) = self + .inner + .state_dbs + .thread_goals() + .usage_limit_active_thread_goal(self.thread_id()) + .await + .map_err(|err| err.to_string())? + else { + return Ok(()); + }; + self.inner + .metrics + .record_terminal_if_status_changed(previous_status, &goal); + self.inner.accounting_state.clear_active_goal(); + let goal = protocol_goal_from_state(goal); + self.inner.event_emitter.thread_goal_updated( + format!("{turn_id}:usage-limit"), + Some(turn_id.to_string()), + goal, + ); + Ok(()) + } + pub async fn restore_after_resume(&self) -> Result<(), String> { if !self.is_enabled() { return Ok(()); diff --git a/codex-rs/ext/goal/tests/goal_extension_backend.rs b/codex-rs/ext/goal/tests/goal_extension_backend.rs index 0a36439b25..584f7362b7 100644 --- a/codex-rs/ext/goal/tests/goal_extension_backend.rs +++ b/codex-rs/ext/goal/tests/goal_extension_backend.rs @@ -363,6 +363,243 @@ async fn budget_limited_goal_keeps_accounting_after_later_tool_finish() -> anyho Ok(()) } +#[tokio::test] +async fn usage_limit_active_goal_accounts_progress_and_clears_accounting() -> anyhow::Result<()> { + let runtime = test_runtime().await?; + let thread_id = test_thread_id()?; + seed_thread_metadata(runtime.as_ref(), thread_id).await?; + let harness = GoalExtensionHarness::new(runtime.clone(), thread_id).await?; + harness.start_turn("turn-1", &TokenUsage::default()).await; + + let tools = harness.tools(); + let create_tool = tool_by_name(&tools, "create_goal"); + create_tool + .handle(tool_call( + "create_goal", + "call-create-goal", + json!({ "objective": "ship goal extension backend" }), + )) + .await?; + harness.sink.clear(); + + harness + .record_token_usage( + "turn-1", + &token_usage( + /*input_tokens*/ 20, /*cached_input_tokens*/ 5, /*output_tokens*/ 8, + /*reasoning_output_tokens*/ 2, /*total_tokens*/ 30, + ), + ) + .await; + harness + .runtime_handle() + .usage_limit_active_goal_for_turn("turn-1") + .await + .map_err(anyhow::Error::msg)?; + + let goal = runtime + .thread_goals() + .get_thread_goal(thread_id) + .await? + .ok_or_else(|| anyhow::anyhow!("goal should exist"))?; + assert_eq!(23, goal.tokens_used); + assert_eq!(codex_state::ThreadGoalStatus::UsageLimited, goal.status); + assert_eq!( + vec![ + CapturedGoalEvent { + event_id: "turn-1:usage-limit-progress".to_string(), + turn_id: Some("turn-1".to_string()), + status: ThreadGoalStatus::Active, + tokens_used: 23, + }, + CapturedGoalEvent { + event_id: "turn-1:usage-limit".to_string(), + turn_id: Some("turn-1".to_string()), + status: ThreadGoalStatus::UsageLimited, + tokens_used: 23, + }, + ], + harness.sink.goal_events() + ); + + harness + .record_token_usage( + "turn-1", + &token_usage( + /*input_tokens*/ 50, /*cached_input_tokens*/ 5, + /*output_tokens*/ 20, /*reasoning_output_tokens*/ 0, + /*total_tokens*/ 70, + ), + ) + .await; + harness + .notify_tool_finish("turn-1", "call-shell-after-usage-limit", "shell") + .await; + harness.stop_turn("turn-1").await; + + let goal = runtime + .thread_goals() + .get_thread_goal(thread_id) + .await? + .ok_or_else(|| anyhow::anyhow!("goal should exist"))?; + assert_eq!(23, goal.tokens_used); + assert_eq!(codex_state::ThreadGoalStatus::UsageLimited, goal.status); + Ok(()) +} + +#[tokio::test] +async fn usage_limit_budget_limited_goal_accounts_remaining_progress() -> anyhow::Result<()> { + let runtime = test_runtime().await?; + let thread_id = test_thread_id()?; + seed_thread_metadata(runtime.as_ref(), thread_id).await?; + let harness = GoalExtensionHarness::new(runtime.clone(), thread_id).await?; + harness.start_turn("turn-1", &TokenUsage::default()).await; + + let tools = harness.tools(); + let create_tool = tool_by_name(&tools, "create_goal"); + create_tool + .handle(tool_call( + "create_goal", + "call-create-goal", + json!({ + "objective": "ship goal extension backend", + "token_budget": 25, + }), + )) + .await?; + + harness + .record_token_usage( + "turn-1", + &token_usage( + /*input_tokens*/ 20, /*cached_input_tokens*/ 5, + /*output_tokens*/ 10, /*reasoning_output_tokens*/ 0, + /*total_tokens*/ 30, + ), + ) + .await; + harness + .notify_tool_finish("turn-1", "call-shell", "shell") + .await; + harness.sink.clear(); + + harness + .record_token_usage( + "turn-1", + &token_usage( + /*input_tokens*/ 24, /*cached_input_tokens*/ 5, + /*output_tokens*/ 16, /*reasoning_output_tokens*/ 0, + /*total_tokens*/ 40, + ), + ) + .await; + harness + .runtime_handle() + .usage_limit_active_goal_for_turn("turn-1") + .await + .map_err(anyhow::Error::msg)?; + + let goal = runtime + .thread_goals() + .get_thread_goal(thread_id) + .await? + .ok_or_else(|| anyhow::anyhow!("goal should exist"))?; + assert_eq!(35, goal.tokens_used); + assert_eq!(codex_state::ThreadGoalStatus::UsageLimited, goal.status); + assert_eq!( + vec![ + CapturedGoalEvent { + event_id: "turn-1:usage-limit-progress".to_string(), + turn_id: Some("turn-1".to_string()), + status: ThreadGoalStatus::BudgetLimited, + tokens_used: 35, + }, + CapturedGoalEvent { + event_id: "turn-1:usage-limit".to_string(), + turn_id: Some("turn-1".to_string()), + status: ThreadGoalStatus::UsageLimited, + tokens_used: 35, + }, + ], + harness.sink.goal_events() + ); + Ok(()) +} + +#[tokio::test] +async fn usage_limit_plan_turn_does_not_stop_goal() -> anyhow::Result<()> { + let runtime = test_runtime().await?; + let thread_id = test_thread_id()?; + seed_thread_metadata(runtime.as_ref(), thread_id).await?; + let harness = GoalExtensionHarness::new(runtime.clone(), thread_id).await?; + + let tools = harness.tools(); + let create_tool = tool_by_name(&tools, "create_goal"); + create_tool + .handle(tool_call( + "create_goal", + "call-create-goal", + json!({ "objective": "ship goal extension backend" }), + )) + .await?; + + harness + .start_turn_with_mode("turn-plan", ModeKind::Plan, &TokenUsage::default()) + .await; + harness.sink.clear(); + harness + .runtime_handle() + .usage_limit_active_goal_for_turn("turn-plan") + .await + .map_err(anyhow::Error::msg)?; + + let goal = runtime + .thread_goals() + .get_thread_goal(thread_id) + .await? + .ok_or_else(|| anyhow::anyhow!("goal should exist"))?; + assert_eq!(codex_state::ThreadGoalStatus::Active, goal.status); + assert_eq!(Vec::::new(), harness.sink.goal_events()); + Ok(()) +} + +#[tokio::test] +async fn usage_limit_stale_turn_does_not_stop_current_goal() -> anyhow::Result<()> { + let runtime = test_runtime().await?; + let thread_id = test_thread_id()?; + seed_thread_metadata(runtime.as_ref(), thread_id).await?; + let harness = GoalExtensionHarness::new(runtime.clone(), thread_id).await?; + harness.start_turn("turn-1", &TokenUsage::default()).await; + + let tools = harness.tools(); + let create_tool = tool_by_name(&tools, "create_goal"); + create_tool + .handle(tool_call( + "create_goal", + "call-create-goal", + json!({ "objective": "ship goal extension backend" }), + )) + .await?; + harness.stop_turn("turn-1").await; + harness.start_turn("turn-2", &TokenUsage::default()).await; + harness.sink.clear(); + + harness + .runtime_handle() + .usage_limit_active_goal_for_turn("turn-1") + .await + .map_err(anyhow::Error::msg)?; + + let goal = runtime + .thread_goals() + .get_thread_goal(thread_id) + .await? + .ok_or_else(|| anyhow::anyhow!("goal should exist"))?; + assert_eq!(codex_state::ThreadGoalStatus::Active, goal.status); + assert_eq!(Vec::::new(), harness.sink.goal_events()); + Ok(()) +} + #[tokio::test] async fn update_goal_can_block_and_accounts_final_progress() -> anyhow::Result<()> { let runtime = test_runtime().await?; @@ -719,8 +956,14 @@ impl GoalExtensionHarness { } async fn start_turn(&self, turn_id: &str, usage: &TokenUsage) { + self.start_turn_with_mode(turn_id, ModeKind::Default, usage) + .await; + } + + async fn start_turn_with_mode(&self, turn_id: &str, mode: ModeKind, usage: &TokenUsage) { let turn_store = ExtensionData::new(turn_id); - let collaboration_mode = default_collaboration_mode(); + let mut collaboration_mode = default_collaboration_mode(); + collaboration_mode.mode = mode; for contributor in self.registry.turn_lifecycle_contributors() { contributor .on_turn_start(TurnStartInput { From 7df8431bbdcdd6bdbac7d2dacd5b59aca0fb7732 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Wed, 27 May 2026 14:14:30 +0200 Subject: [PATCH 18/87] Fix guardian review test user input (#24746) ## Summary - Add the missing additional_context field to the guardian review Op::UserInput test initializer. ## Test plan - just fmt - just test -p codex-core guardian_review - just test -p codex-core (compiles, then fails on local environment issues: sandbox-exec Operation not permitted, missing test_stdio_server helper binary, and unrelated timeouts) --- codex-rs/core/tests/suite/guardian_review.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/codex-rs/core/tests/suite/guardian_review.rs b/codex-rs/core/tests/suite/guardian_review.rs index 5b76d09ae9..df38fb4353 100644 --- a/codex-rs/core/tests/suite/guardian_review.rs +++ b/codex-rs/core/tests/suite/guardian_review.rs @@ -118,6 +118,7 @@ printf '%s\n' "${@: -1}" >> "${payload_path}""#, }], final_output_json_schema: None, responsesapi_client_metadata: None, + additional_context: Default::default(), thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { cwd: Some(test.cwd.path().to_path_buf()), approval_policy: Some(approval_policy), From d2ebb8d8caf6bc487466ee50f61eaa2dc53fa091 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Wed, 27 May 2026 15:17:23 +0200 Subject: [PATCH 19/87] feat: add thread idle lifecycle hook (#24744) ## Why Extensions can currently observe thread start, resume, and stop, but they do not have a lifecycle point for the host to say that immediately pending thread work has drained. That makes idle follow-up behavior harder to express as extension-owned logic instead of host-specific plumbing. This adds an explicit idle lifecycle hook so an extension can react when a thread becomes idle while the host keeps ownership of whether any submitted follow-up input starts a turn, is queued, or is ignored. ## What changed - Added `ThreadIdleInput` with access to the session-scoped and thread-scoped extension stores. - Added a default `on_thread_idle` method to `ThreadLifecycleContributor`. - Re-exported `ThreadIdleInput` from the extension API surface. ## Testing Not run; this only extends the extension API trait surface with a default hook and exported input type. --- codex-rs/ext/extension-api/src/contributors.rs | 8 ++++++++ .../extension-api/src/contributors/thread_lifecycle.rs | 8 ++++++++ codex-rs/ext/extension-api/src/lib.rs | 1 + 3 files changed, 17 insertions(+) diff --git a/codex-rs/ext/extension-api/src/contributors.rs b/codex-rs/ext/extension-api/src/contributors.rs index 2113fdef8e..fda295b7f7 100644 --- a/codex-rs/ext/extension-api/src/contributors.rs +++ b/codex-rs/ext/extension-api/src/contributors.rs @@ -16,6 +16,7 @@ mod turn_lifecycle; pub use prompt::PromptFragment; pub use prompt::PromptSlot; +pub use thread_lifecycle::ThreadIdleInput; pub use thread_lifecycle::ThreadResumeInput; pub use thread_lifecycle::ThreadStartInput; pub use thread_lifecycle::ThreadStopInput; @@ -51,6 +52,13 @@ pub trait ThreadLifecycleContributor: Send + Sync { /// Called after the host constructs a runtime from persisted history. async fn on_thread_resume(&self, _input: ThreadResumeInput<'_>) {} + /// Called after the host has drained immediately pending thread work. + /// + /// Implementations may use host capabilities captured by the extension to + /// submit follow-up input. The host remains responsible for deciding + /// whether that input starts a turn, is queued, or is ignored. + async fn on_thread_idle(&self, _input: ThreadIdleInput<'_>) {} + /// Called before the host drops the thread runtime and thread-scoped store. async fn on_thread_stop(&self, _input: ThreadStopInput<'_>) {} } diff --git a/codex-rs/ext/extension-api/src/contributors/thread_lifecycle.rs b/codex-rs/ext/extension-api/src/contributors/thread_lifecycle.rs index d50f5a1e2f..07c7e7fa81 100644 --- a/codex-rs/ext/extension-api/src/contributors/thread_lifecycle.rs +++ b/codex-rs/ext/extension-api/src/contributors/thread_lifecycle.rs @@ -18,6 +18,14 @@ pub struct ThreadResumeInput<'a> { pub thread_store: &'a ExtensionData, } +/// Input supplied when the host has no immediately pending thread work. +pub struct ThreadIdleInput<'a> { + /// Store scoped to the host session runtime. + pub session_store: &'a ExtensionData, + /// Store scoped to this thread runtime. + pub thread_store: &'a ExtensionData, +} + /// Input supplied when the host stops a thread runtime. pub struct ThreadStopInput<'a> { /// Store scoped to the host session runtime. diff --git a/codex-rs/ext/extension-api/src/lib.rs b/codex-rs/ext/extension-api/src/lib.rs index f3c3dc526e..80ca67e290 100644 --- a/codex-rs/ext/extension-api/src/lib.rs +++ b/codex-rs/ext/extension-api/src/lib.rs @@ -27,6 +27,7 @@ pub use contributors::ConfigContributor; pub use contributors::ContextContributor; pub use contributors::PromptFragment; pub use contributors::PromptSlot; +pub use contributors::ThreadIdleInput; pub use contributors::ThreadLifecycleContributor; pub use contributors::ThreadResumeInput; pub use contributors::ThreadStartInput; From 61cbf3574eca870df6fa7f49648ec7e001901b5a Mon Sep 17 00:00:00 2001 From: jif-oai Date: Wed, 27 May 2026 15:49:08 +0200 Subject: [PATCH 20/87] Drop startup context when truncating forked rollouts (#24751) ## Summary - Change last-`n` fork truncation to start at the first fork-turn boundary instead of returning the full rollout when the fork history is shorter than the requested window. - Add coverage for the startup-prefix case in both rollout truncation tests and agent control spawn behavior. - Ensure bounded forked children still rebuild context after the cached prefix is truncated. ## Testing - Added unit coverage for truncation behavior when the parent history is under the requested fork-turn limit. - Added an agent control test covering bounded fork spawn behavior with startup context present. - Not run (not requested). --- codex-rs/core/src/agent/control_tests.rs | 102 ++++++++++++++++++ .../core/src/thread_rollout_truncation.rs | 16 +-- .../src/thread_rollout_truncation_tests.rs | 28 +++++ 3 files changed, 140 insertions(+), 6 deletions(-) diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index 8209272f10..b5ba938ef7 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -1113,6 +1113,108 @@ async fn spawn_agent_fork_last_n_turns_keeps_only_recent_turns() { .expect("parent shutdown should submit"); } +#[tokio::test] +async fn spawn_agent_fork_last_n_turns_drops_parent_startup_prefix_when_under_limit() { + let harness = AgentControlHarness::new().await; + let (parent_thread_id, parent_thread) = harness.start_thread().await; + let startup_turn_context = parent_thread.codex.session.new_default_turn().await; + parent_thread + .codex + .session + .record_conversation_items( + startup_turn_context.as_ref(), + &[ResponseItem::Message { + id: None, + role: "developer".to_string(), + content: vec![ContentItem::InputText { + text: "parent startup developer context".to_string(), + }], + phase: None, + }], + ) + .await; + parent_thread + .inject_user_message_without_turn("current parent task".to_string()) + .await; + let spawn_turn_context = parent_thread.codex.session.new_default_turn().await; + let parent_spawn_call_id = "spawn-call-last-n-under-limit".to_string(); + parent_thread + .codex + .session + .record_conversation_items( + spawn_turn_context.as_ref(), + &[spawn_agent_call(&parent_spawn_call_id)], + ) + .await; + parent_thread + .codex + .session + .ensure_rollout_materialized() + .await; + parent_thread + .codex + .session + .flush_rollout() + .await + .expect("parent rollout should flush"); + + let child_thread_id = harness + .control + .spawn_agent_with_metadata( + harness.config.clone(), + text_input("child task"), + Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id, + depth: 1, + agent_path: None, + agent_nickname: None, + agent_role: None, + })), + SpawnAgentOptions { + fork_parent_spawn_call_id: Some(parent_spawn_call_id), + fork_mode: Some(SpawnAgentForkMode::LastNTurns(2)), + ..Default::default() + }, + ) + .await + .expect("bounded forked spawn should drop startup prefix") + .thread_id; + + let child_thread = harness + .manager + .get_thread(child_thread_id) + .await + .expect("child thread should be registered"); + let history = child_thread.codex.session.clone_history().await; + assert!( + history_contains_text(history.raw_items(), "current parent task"), + "bounded fork should retain the requested recent parent turn" + ); + assert!( + !history_contains_text(history.raw_items(), "parent startup developer context"), + "bounded fork should drop parent startup context even when fewer turns exist than requested" + ); + assert!( + child_thread + .codex + .session + .reference_context_item() + .await + .is_none(), + "bounded forked child should still rebuild context after truncating the cached prefix" + ); + + let _ = harness + .control + .shutdown_live_agent(child_thread_id) + .await + .expect("child shutdown should submit"); + let _ = parent_thread + .submit(Op::Shutdown {}) + .await + .expect("parent shutdown should submit"); +} + #[tokio::test] async fn spawn_agent_fork_last_n_turns_strips_parent_usage_hints() { let harness = AgentControlHarness::new().await; diff --git a/codex-rs/core/src/thread_rollout_truncation.rs b/codex-rs/core/src/thread_rollout_truncation.rs index e20ee53d47..69e615c4fb 100644 --- a/codex-rs/core/src/thread_rollout_truncation.rs +++ b/codex-rs/core/src/thread_rollout_truncation.rs @@ -130,7 +130,8 @@ pub(crate) fn truncate_rollout_before_nth_user_message_from_start( /// Return a suffix of `items` that keeps the last `n_from_end` fork turns. /// -/// If fewer than or equal to `n_from_end` fork turns exist, this returns the full rollout. +/// If fewer than or equal to `n_from_end` fork turns exist, this keeps from the first fork-turn +/// boundary and still drops pre-turn startup context. pub(crate) fn truncate_rollout_to_last_n_fork_turns( items: &[RolloutItem], n_from_end: usize, @@ -140,11 +141,14 @@ pub(crate) fn truncate_rollout_to_last_n_fork_turns( } let fork_turn_positions = fork_turn_positions_in_rollout(items); - if fork_turn_positions.len() <= n_from_end { - return items.to_vec(); - } - - let keep_idx = fork_turn_positions[fork_turn_positions.len() - n_from_end]; + let Some(keep_idx) = fork_turn_positions + .len() + .checked_sub(n_from_end) + .map(|position| fork_turn_positions[position]) + .or_else(|| fork_turn_positions.first().copied()) + else { + return Vec::new(); + }; items[keep_idx..].to_vec() } diff --git a/codex-rs/core/src/thread_rollout_truncation_tests.rs b/codex-rs/core/src/thread_rollout_truncation_tests.rs index df370a0546..e0a8837165 100644 --- a/codex-rs/core/src/thread_rollout_truncation_tests.rs +++ b/codex-rs/core/src/thread_rollout_truncation_tests.rs @@ -29,6 +29,17 @@ fn assistant_msg(text: &str) -> ResponseItem { } } +fn developer_msg(text: &str) -> ResponseItem { + ResponseItem::Message { + id: None, + role: "developer".to_string(), + content: vec![ContentItem::InputText { + text: text.to_string(), + }], + phase: None, + } +} + fn inter_agent_msg(text: &str, trigger_turn: bool) -> ResponseItem { let communication = InterAgentCommunication::new( AgentPath::root(), @@ -197,6 +208,23 @@ fn truncates_rollout_to_last_n_fork_turns_counts_trigger_turn_messages() { ); } +#[test] +fn truncates_rollout_to_last_n_fork_turns_drops_startup_prefix_even_when_under_limit() { + let rollout = vec![ + RolloutItem::ResponseItem(developer_msg("startup developer context")), + RolloutItem::ResponseItem(user_msg("current task")), + RolloutItem::ResponseItem(assistant_msg("answer")), + ]; + + let truncated = truncate_rollout_to_last_n_fork_turns(&rollout, /*n_from_end*/ 2); + let expected = rollout[1..].to_vec(); + + assert_eq!( + serde_json::to_value(&truncated).unwrap(), + serde_json::to_value(&expected).unwrap() + ); +} + #[test] fn truncates_rollout_to_last_n_fork_turns_applies_thread_rollback_markers() { let rollout = vec![ From f20904c4d60c9c4d3349b2cb357ef83c6e1a4a2a Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Wed, 27 May 2026 07:22:30 -0700 Subject: [PATCH 21/87] TUI config cleanup: plugin marketplace (#24257) ## Why Plugin and marketplace mutations are applied by the app server, but several TUI follow-up paths still refreshed state from the TUI host config. In remote workspace mode, that can leave plugin UI state tied to stale client-local `config.toml` after the server has already applied the mutation. ## What - Stop reloading the TUI host config after app-server-owned plugin, marketplace, skill, and app mutations. - Use the same app-server-owned refresh path for local and remote sessions: ask the app server to reload user config where the running session needs it, then refetch plugin list/detail state from the app server. - Build plugin mention candidates from existing app-server `plugin/list` and `plugin/read` data in both local and remote sessions instead of TUI-host plugin config. - Avoid the duplicate local config reload after `ReloadUserConfig` asks the app server to reload config. ## Verification Manually launched a local WebSocket app-server with a temp server `CODEX_HOME`, launched the TUI with a separate temp host `CODEX_HOME` and `--remote`, installed a sample plugin from a temp local marketplace through `/plugins`, and confirmed the TUI refreshed to installed state while only the server config gained `[plugins."sample@debug"]`. Trace logs showed the TUI using app-server `plugin/list` and `plugin/read` for the refresh path. --- codex-rs/tui/src/app/background_requests.rs | 6 +-- codex-rs/tui/src/app/event_dispatch.rs | 60 ++++----------------- codex-rs/tui/src/app/thread_routing.rs | 1 - 3 files changed, 13 insertions(+), 54 deletions(-) diff --git a/codex-rs/tui/src/app/background_requests.rs b/codex-rs/tui/src/app/background_requests.rs index 4b8b5f8920..b3e1edb901 100644 --- a/codex-rs/tui/src/app/background_requests.rs +++ b/codex-rs/tui/src/app/background_requests.rs @@ -400,16 +400,16 @@ impl App { } pub(super) fn refresh_plugin_mentions(&mut self, app_server: &AppServerSession) { - let config = self.config.clone(); + let cwd = self.config.cwd.to_path_buf(); let request_handle = app_server.request_handle(); let app_event_tx = self.app_event_tx.clone(); - if !config.features.enabled(Feature::Plugins) { + if !self.config.features.enabled(Feature::Plugins) { app_event_tx.send(AppEvent::PluginMentionsLoaded { plugins: None }); return; } tokio::spawn(async move { - match fetch_plugin_mentions(request_handle, config.cwd.to_path_buf()).await { + match fetch_plugin_mentions(request_handle, cwd).await { Ok(plugins) => { app_event_tx.send(AppEvent::PluginMentionsLoaded { plugins: Some(plugins), diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index 18ded9bbbc..5af2c49233 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -506,9 +506,6 @@ impl App { self.chat_widget .on_marketplace_add_loaded(cwd.clone(), source, result); if add_succeeded && self.chat_widget.config_ref().cwd.as_path() == cwd.as_path() { - if let Err(err) = self.refresh_in_memory_config_from_disk().await { - tracing::warn!(error = %err, "failed to refresh config after marketplace add"); - } self.fetch_plugins_list(app_server, cwd); } } @@ -516,14 +513,7 @@ impl App { let marketplace_contents_changed = matches!(&result, Ok(response) if !response.upgraded_roots.is_empty()); if marketplace_contents_changed { - if let Err(err) = self.refresh_in_memory_config_from_disk().await { - tracing::warn!( - error = %err, - "failed to refresh config after marketplace upgrade" - ); - } - self.chat_widget.refresh_plugin_mentions(); - self.chat_widget.submit_op(AppCommand::reload_user_config()); + self.refresh_plugin_mentions_after_config_write(); } self.chat_widget .on_marketplace_upgrade_loaded(cwd.clone(), result); @@ -558,11 +548,7 @@ impl App { ); if remove_succeeded && self.chat_widget.config_ref().cwd.as_path() == cwd.as_path() { - if let Err(err) = self.refresh_in_memory_config_from_disk().await { - tracing::warn!(error = %err, "failed to refresh config after marketplace remove"); - } - self.chat_widget.refresh_plugin_mentions(); - self.chat_widget.submit_op(AppCommand::reload_user_config()); + self.refresh_plugin_mentions_after_config_write(); self.fetch_plugins_list(app_server, cwd); } } @@ -609,11 +595,7 @@ impl App { } => { let install_succeeded = result.is_ok(); if install_succeeded { - if let Err(err) = self.refresh_in_memory_config_from_disk().await { - tracing::warn!(error = %err, "failed to refresh config after plugin install"); - } - self.chat_widget.refresh_plugin_mentions(); - self.chat_widget.submit_op(AppCommand::reload_user_config()); + self.refresh_plugin_mentions_after_config_write(); } let should_refresh_plugin_detail = self.chat_widget.on_plugin_install_loaded( cwd.clone(), @@ -665,14 +647,7 @@ impl App { self.pending_plugin_enabled_writes.remove(&plugin_id); let update_succeeded = result.is_ok(); if update_succeeded { - if let Err(err) = self.refresh_in_memory_config_from_disk().await { - tracing::warn!( - error = %err, - "failed to refresh config after plugin toggle" - ); - } - self.chat_widget.refresh_plugin_mentions(); - self.chat_widget.submit_op(AppCommand::reload_user_config()); + self.refresh_plugin_mentions_after_config_write(); } self.chat_widget .on_plugin_enabled_set(cwd, plugin_id, enabled, result); @@ -1303,14 +1278,7 @@ impl App { } => { let uninstall_succeeded = result.is_ok(); if uninstall_succeeded { - if let Err(err) = self.refresh_in_memory_config_from_disk().await { - tracing::warn!( - error = %err, - "failed to refresh config after plugin uninstall" - ); - } - self.chat_widget.refresh_plugin_mentions(); - self.chat_widget.submit_op(AppCommand::reload_user_config()); + self.refresh_plugin_mentions_after_config_write(); } self.chat_widget.on_plugin_uninstall_loaded( cwd.clone(), @@ -1705,14 +1673,6 @@ impl App { { Ok(()) => { self.chat_widget.update_skill_enabled(path, enabled); - if !app_server.uses_remote_workspace() - && let Err(err) = self.refresh_in_memory_config_from_disk().await - { - tracing::warn!( - error = %err, - "failed to refresh config after skill toggle" - ); - } } Err(err) => { let path_display = path.display(); @@ -1749,11 +1709,6 @@ impl App { { Ok(_) => { self.chat_widget.update_connector_enabled(&id, enabled); - if !app_server.uses_remote_workspace() - && let Err(err) = self.refresh_in_memory_config_from_disk().await - { - tracing::warn!(error = %err, "failed to refresh config after app toggle"); - } } Err(err) => { self.chat_widget.add_error_message(format!( @@ -2098,6 +2053,11 @@ impl App { } } + fn refresh_plugin_mentions_after_config_write(&mut self) { + self.chat_widget.refresh_plugin_mentions(); + self.chat_widget.submit_op(AppCommand::reload_user_config()); + } + async fn apply_keymap_clear(&mut self, context: String, action: String) { let keymap_config = match crate::keymap_setup::keymap_without_custom_binding( &self.config.tui_keymap, diff --git a/codex-rs/tui/src/app/thread_routing.rs b/codex-rs/tui/src/app/thread_routing.rs index 8f15aed79a..a403dc232c 100644 --- a/codex-rs/tui/src/app/thread_routing.rs +++ b/codex-rs/tui/src/app/thread_routing.rs @@ -684,7 +684,6 @@ impl App { } AppCommand::ReloadUserConfig => { app_server.reload_user_config().await?; - self.refresh_in_memory_config_from_disk().await?; Ok(true) } AppCommand::OverrideTurnContext { .. } => { From aa184548b1ee0a559fba7be658240053e270e16f Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Wed, 27 May 2026 11:36:52 -0300 Subject: [PATCH 22/87] fix(tui): complete vim word-end and line-end behavior (#24380) ## Why The TUI Vim composer currently diverges from normal Vim editing in two common workflows: pressing `e` repeatedly can remain stuck at an existing word end, and normal mode does not support `C` for changing through the end of the line. The existing `D` behavior also removes the newline when the cursor is already at the line boundary, which makes the new `C` action and existing deletion action surprising in multiline prompts. Closes #23926. Closes #24238. ## What Changed - Make normal-mode `e` advance from the current word end to the next word end, including for operator motions such as `de`. - Add configurable Vim normal-mode `change_to_line_end` behavior, bound to `C` by default, which deletes to the end of the current line and enters Insert mode. - Keep the newline intact when `D` or `C` is pressed at the end-of-line boundary. - Add regression coverage for repeated `e`, `de`, `C`, and the multiline `C`/`D` boundary behavior. - Regenerate the config schema and update the keymap picker snapshots for the new Vim action. ## How to Test 1. Run Codex with Vim composer mode enabled: ```bash cd codex-rs cargo run --bin codex -- -c tui.vim_mode_default=true ``` 2. Enter `alpha beta gamma`, press `Esc`, `0`, then press `e` repeatedly. Confirm the cursor advances through the ends of `alpha`, `beta`, and `gamma`. 3. Enter `hello world`, press `Esc`, `0`, `w`, then `C`. Confirm `world` is deleted and the composer enters Insert mode. 4. Enter a multiline prompt with `hello` above `world`, press `Esc`, `k`, `$`, and then `D`. Confirm the newline is preserved and the two lines do not join. 5. At the same boundary, press `C` and type `!`. Confirm the composer enters Insert mode and yields `hello!` above `world`, preserving the newline. Targeted automated verification: - `just fix -p codex-tui` - `just argument-comment-lint-from-source -p codex-tui -p codex-config` - `cargo insta pending-snapshots` reports no pending snapshots. - `just test -p codex-tui` validates the new Vim and keymap snapshot coverage, but the command remains red due to two reproducible unrelated failures in `app::tests::update_feature_flags_disabling_guardian_*`. ## Validation Note The workspace-wide `just argument-comment-lint` form is currently blocked during Bazel analysis by the existing LLVM `compiler-rt` missing `include/sanitizer/*.h` failure; package-scoped source linting for the changed Rust crates passed. --- codex-rs/config/src/tui_keymap.rs | 2 + codex-rs/core/config.schema.json | 10 ++ ...ts__vim_e_advances_from_each_word_end.snap | 12 ++ codex-rs/tui/src/bottom_pane/textarea.rs | 154 +++++++++++++++++- codex-rs/tui/src/keymap.rs | 10 ++ codex-rs/tui/src/keymap_setup/actions.rs | 3 + ...ap_setup__tests__keymap_picker_custom.snap | 2 +- ...ests__keymap_picker_fast_mode_enabled.snap | 2 +- ...p__tests__keymap_picker_first_actions.snap | 4 +- ...ap_setup__tests__keymap_picker_narrow.snap | 2 +- ...ymap_setup__tests__keymap_picker_wide.snap | 2 +- 11 files changed, 190 insertions(+), 13 deletions(-) create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__textarea__tests__vim_e_advances_from_each_word_end.snap diff --git a/codex-rs/config/src/tui_keymap.rs b/codex-rs/config/src/tui_keymap.rs index 5b1e7d4cc8..5848d1f444 100644 --- a/codex-rs/config/src/tui_keymap.rs +++ b/codex-rs/config/src/tui_keymap.rs @@ -223,6 +223,8 @@ pub struct TuiVimNormalKeymap { pub delete_char: Option, /// Delete from cursor to end of line (`D`). pub delete_to_line_end: Option, + /// Change from cursor to end of line and enter insert mode (`C`). + pub change_to_line_end: Option, /// Yank the entire line (`Y`). pub yank_line: Option, /// Paste after cursor (`p`). diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 6af30b4071..06b7c56d5b 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -2834,6 +2834,7 @@ "append_after_cursor": null, "append_line_end": null, "cancel_operator": null, + "change_to_line_end": null, "delete_char": null, "delete_to_line_end": null, "enter_insert": null, @@ -3500,6 +3501,7 @@ "append_after_cursor": null, "append_line_end": null, "cancel_operator": null, + "change_to_line_end": null, "delete_char": null, "delete_to_line_end": null, "enter_insert": null, @@ -3765,6 +3767,14 @@ ], "description": "Cancel a pending operator and return to normal mode." }, + "change_to_line_end": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Change from cursor to end of line and enter insert mode (`C`)." + }, "delete_char": { "allOf": [ { diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__textarea__tests__vim_e_advances_from_each_word_end.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__textarea__tests__vim_e_advances_from_each_word_end.snap new file mode 100644 index 0000000000..0a41c2d80f --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__textarea__tests__vim_e_advances_from_each_word_end.snap @@ -0,0 +1,12 @@ +--- +source: tui/src/bottom_pane/textarea.rs +expression: "states.join(\"\\n\\n\")" +--- +alpha beta gamma + ^ + +alpha beta gamma + ^ + +alpha beta gamma + ^ diff --git a/codex-rs/tui/src/bottom_pane/textarea.rs b/codex-rs/tui/src/bottom_pane/textarea.rs index 96f6cf8cbb..d931b14b2c 100644 --- a/codex-rs/tui/src/bottom_pane/textarea.rs +++ b/codex-rs/tui/src/bottom_pane/textarea.rs @@ -755,7 +755,12 @@ impl TextArea { return; } if self.vim_normal_keymap.delete_to_line_end.is_pressed(event) { - self.kill_to_end_of_line(); + self.vim_kill_to_end_of_line(); + return; + } + if self.vim_normal_keymap.change_to_line_end.is_pressed(event) { + self.vim_kill_to_end_of_line(); + self.vim_mode = VimMode::Insert; return; } if self.vim_normal_keymap.yank_line.is_pressed(event) { @@ -914,7 +919,7 @@ impl TextArea { VimMotion::Down => self.move_cursor_down(), VimMotion::WordForward => self.set_cursor(self.beginning_of_next_word()), VimMotion::WordBackward => self.set_cursor(self.beginning_of_previous_word()), - VimMotion::WordEnd => self.set_cursor(self.end_of_next_word()), + VimMotion::WordEnd => self.set_cursor(self.vim_word_end_exclusive()), VimMotion::LineStart => self.set_cursor(self.beginning_of_current_line()), VimMotion::LineEnd => self.set_cursor(self.end_of_current_line()), } @@ -1007,6 +1012,13 @@ impl TextArea { } } + fn vim_kill_to_end_of_line(&mut self) { + let eol = self.end_of_current_line(); + if self.cursor_pos < eol { + self.kill_range(self.cursor_pos..eol); + } + } + pub fn kill_to_beginning_of_line(&mut self) { let bol = self.beginning_of_current_line(); let range = if self.cursor_pos == bol { @@ -1734,7 +1746,11 @@ impl TextArea { } pub(crate) fn end_of_next_word(&self) -> usize { - let suffix = &self.text[self.cursor_pos..]; + self.end_of_next_word_from(self.cursor_pos) + } + + fn end_of_next_word_from(&self, cursor_pos: usize) -> usize { + let suffix = &self.text[cursor_pos..]; let Some(first_non_ws) = suffix.find(|ch: char| !ch.is_whitespace()) else { return self.text.len(); }; @@ -1742,16 +1758,16 @@ impl TextArea { let run = &run[..run.find(char::is_whitespace).unwrap_or(run.len())]; let mut pieces = split_word_pieces(run).into_iter().peekable(); let Some((start, piece)) = pieces.next() else { - return self.cursor_pos + first_non_ws; + return cursor_pos + first_non_ws; }; - let word_start = self.cursor_pos + first_non_ws + start; + let word_start = cursor_pos + first_non_ws + start; let mut end = word_start + piece.len(); if piece.chars().all(is_word_separator) { while let Some((idx, piece)) = pieces.peek() { if !piece.chars().all(is_word_separator) { break; } - end = self.cursor_pos + first_non_ws + *idx + piece.len(); + end = cursor_pos + first_non_ws + *idx + piece.len(); pieces.next(); } } @@ -1759,8 +1775,22 @@ impl TextArea { self.adjust_pos_out_of_elements(end, /*prefer_start*/ false) } - fn vim_word_end_cursor(&self) -> usize { + fn vim_word_end_exclusive(&self) -> usize { let end = self.end_of_next_word(); + let target = if end > self.cursor_pos { + self.prev_atomic_boundary(end) + } else { + end + }; + if target == self.cursor_pos && end < self.text.len() { + self.end_of_next_word_from(end) + } else { + end + } + } + + fn vim_word_end_cursor(&self) -> usize { + let end = self.vim_word_end_exclusive(); if end > self.cursor_pos { self.prev_atomic_boundary(end) } else { @@ -2275,6 +2305,60 @@ mod tests { assert_eq!(t.cursor(), 11); } + #[test] + fn vim_shift_c_changes_to_line_end_and_enters_insert_mode() { + let mut t = ta_with("hello world\nnext line"); + t.set_cursor(/*pos*/ 6); + t.set_vim_enabled(/*enabled*/ true); + + t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::SHIFT)); + + assert_eq!(t.text(), "hello \nnext line"); + assert_eq!(t.vim_mode_label(), Some("Insert")); + assert_eq!(t.cursor(), 6); + assert_eq!(t.kill_buffer, "world"); + } + + #[test] + fn vim_uppercase_c_changes_to_line_end() { + let mut t = ta_with("hello world\nnext line"); + t.set_cursor(/*pos*/ 6); + t.set_vim_enabled(/*enabled*/ true); + + t.input(KeyEvent::new(KeyCode::Char('C'), KeyModifiers::NONE)); + + assert_eq!(t.text(), "hello \nnext line"); + assert_eq!(t.vim_mode_label(), Some("Insert")); + assert_eq!(t.cursor(), 6); + } + + #[test] + fn vim_d_at_line_end_does_not_remove_newline() { + let mut t = ta_with("hello\nworld"); + t.set_cursor(/*pos*/ "hello".len()); + t.set_vim_enabled(/*enabled*/ true); + + t.input(KeyEvent::new(KeyCode::Char('D'), KeyModifiers::NONE)); + + assert_eq!(t.text(), "hello\nworld"); + assert_eq!(t.vim_mode_label(), Some("Normal")); + assert_eq!(t.kill_buffer, ""); + } + + #[test] + fn vim_c_at_line_end_enters_insert_without_removing_newline() { + let mut t = ta_with("hello\nworld"); + t.set_cursor(/*pos*/ "hello".len()); + t.set_vim_enabled(/*enabled*/ true); + + t.input(KeyEvent::new(KeyCode::Char('C'), KeyModifiers::NONE)); + + assert_eq!(t.text(), "hello\nworld"); + assert_eq!(t.vim_mode_label(), Some("Insert")); + assert_eq!(t.cursor(), "hello".len()); + assert_eq!(t.kill_buffer, ""); + } + #[test] fn vim_shift_o_opens_line_above_with_shift_only_binding() { let mut t = ta_with("hello\nworld"); @@ -2348,6 +2432,62 @@ mod tests { assert_eq!(t.kill_buffer, "c"); } + #[test] + fn vim_e_advances_from_each_word_end() { + let mut t = ta_with("alpha beta gamma"); + t.set_cursor("alph".len()); // codespell:ignore alph + t.set_vim_enabled(/*enabled*/ true); + let mut states = Vec::new(); + + for _ in 0..3 { + t.input(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::NONE)); + states.push(format!("{}\n{}^", t.text(), " ".repeat(t.cursor()))); + } + + insta::assert_snapshot!("vim_e_advances_from_each_word_end", states.join("\n\n")); + } + + #[test] + fn vim_delete_to_word_end_advances_from_existing_word_end() { + let mut t = ta_with("alpha beta gamma"); + t.set_cursor("alph".len()); // codespell:ignore alph + t.set_vim_enabled(/*enabled*/ true); + + t.input(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::NONE)); + + assert_eq!(t.text(), "alph gamma"); // codespell:ignore alph + assert_eq!(t.kill_buffer, "a beta"); + } + + #[test] + fn vim_e_from_word_end_can_land_on_trailing_space() { + let mut t = ta_with("alpha "); + t.set_cursor("alph".len()); // codespell:ignore alph + t.set_vim_enabled(/*enabled*/ true); + + t.input(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::NONE)); + + assert_eq!(t.cursor(), "alpha ".len()); + } + + #[test] + fn vim_e_advances_across_atomic_element_word_ends() { + let mut t = TextArea::new(); + t.insert_str("alpha "); + t.insert_element(""); + t.insert_str(" gamma"); + let element_start = t.elements[0].range.start; + t.set_cursor("alph".len()); // codespell:ignore alph + t.set_vim_enabled(/*enabled*/ true); + + t.input(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::NONE)); + assert_eq!(t.cursor(), element_start); + + t.input(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::NONE)); + assert_eq!(t.cursor(), "alpha gamm".len()); + } + #[test] fn vim_dollar_lands_on_line_end_character() { let mut t = ta_with("abc\n123"); diff --git a/codex-rs/tui/src/keymap.rs b/codex-rs/tui/src/keymap.rs index 533e28f3a2..bc54224c40 100644 --- a/codex-rs/tui/src/keymap.rs +++ b/codex-rs/tui/src/keymap.rs @@ -156,6 +156,7 @@ pub(crate) struct VimNormalKeymap { pub(crate) move_line_end: Vec, pub(crate) delete_char: Vec, pub(crate) delete_to_line_end: Vec, + pub(crate) change_to_line_end: Vec, pub(crate) yank_line: Vec, pub(crate) paste_after: Vec, pub(crate) start_delete_operator: Vec, @@ -469,6 +470,7 @@ impl RuntimeKeymap { move_line_end: resolve_local!(keymap, defaults, vim_normal, move_line_end), delete_char: resolve_local!(keymap, defaults, vim_normal, delete_char), delete_to_line_end: resolve_local!(keymap, defaults, vim_normal, delete_to_line_end), + change_to_line_end: resolve_local!(keymap, defaults, vim_normal, change_to_line_end), yank_line: resolve_local!(keymap, defaults, vim_normal, yank_line), paste_after: resolve_local!(keymap, defaults, vim_normal, paste_after), start_delete_operator: resolve_local!( @@ -786,6 +788,10 @@ impl RuntimeKeymap { shift(KeyCode::Char('d')), plain(KeyCode::Char('D')) ], + change_to_line_end: default_bindings![ + shift(KeyCode::Char('c')), + plain(KeyCode::Char('C')) + ], yank_line: default_bindings![shift(KeyCode::Char('y')), plain(KeyCode::Char('Y'))], paste_after: default_bindings![plain(KeyCode::Char('p'))], start_delete_operator: default_bindings![plain(KeyCode::Char('d'))], @@ -1176,6 +1182,10 @@ impl RuntimeKeymap { "delete_to_line_end", self.vim_normal.delete_to_line_end.as_slice(), ), + ( + "change_to_line_end", + self.vim_normal.change_to_line_end.as_slice(), + ), ("yank_line", self.vim_normal.yank_line.as_slice()), ("paste_after", self.vim_normal.paste_after.as_slice()), ( diff --git a/codex-rs/tui/src/keymap_setup/actions.rs b/codex-rs/tui/src/keymap_setup/actions.rs index 9ce827b8a9..fb3ecd9f2c 100644 --- a/codex-rs/tui/src/keymap_setup/actions.rs +++ b/codex-rs/tui/src/keymap_setup/actions.rs @@ -135,6 +135,7 @@ pub(super) const KEYMAP_ACTIONS: &[KeymapActionDescriptor] = &[ action("vim_normal", "Vim normal", "move_line_end", "Move to the end of the line."), action("vim_normal", "Vim normal", "delete_char", "Delete the character under the cursor."), action("vim_normal", "Vim normal", "delete_to_line_end", "Delete from cursor to end of line."), + action("vim_normal", "Vim normal", "change_to_line_end", "Change from cursor to end of line and enter insert mode."), action("vim_normal", "Vim normal", "yank_line", "Yank the entire line."), action("vim_normal", "Vim normal", "paste_after", "Paste after the cursor."), action("vim_normal", "Vim normal", "start_delete_operator", "Begin a delete operator and wait for a motion."), @@ -263,6 +264,7 @@ pub(super) fn binding_slot<'a>( ("vim_normal", "move_line_end") => Some(&mut keymap.vim_normal.move_line_end), ("vim_normal", "delete_char") => Some(&mut keymap.vim_normal.delete_char), ("vim_normal", "delete_to_line_end") => Some(&mut keymap.vim_normal.delete_to_line_end), + ("vim_normal", "change_to_line_end") => Some(&mut keymap.vim_normal.change_to_line_end), ("vim_normal", "yank_line") => Some(&mut keymap.vim_normal.yank_line), ("vim_normal", "paste_after") => Some(&mut keymap.vim_normal.paste_after), ("vim_normal", "start_delete_operator") => Some(&mut keymap.vim_normal.start_delete_operator), @@ -373,6 +375,7 @@ pub(super) fn bindings_for_action<'a>( ("vim_normal", "move_line_end") => Some(runtime_keymap.vim_normal.move_line_end.as_slice()), ("vim_normal", "delete_char") => Some(runtime_keymap.vim_normal.delete_char.as_slice()), ("vim_normal", "delete_to_line_end") => Some(runtime_keymap.vim_normal.delete_to_line_end.as_slice()), + ("vim_normal", "change_to_line_end") => Some(runtime_keymap.vim_normal.change_to_line_end.as_slice()), ("vim_normal", "yank_line") => Some(runtime_keymap.vim_normal.yank_line.as_slice()), ("vim_normal", "paste_after") => Some(runtime_keymap.vim_normal.paste_after.as_slice()), ("vim_normal", "start_delete_operator") => Some(runtime_keymap.vim_normal.start_delete_operator.as_slice()), diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap index 898108c755..bc41e50b24 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap @@ -5,7 +5,7 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 93 actions, 1 customized, 2 unbound. + 94 actions, 1 customized, 2 unbound. [All] Common Customized (1) Unbound (2) App Composer Editor Vim Navigation Approval Debug diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_fast_mode_enabled.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_fast_mode_enabled.snap index 4d9440cb4e..e0c1aa7ffe 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_fast_mode_enabled.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_fast_mode_enabled.snap @@ -5,7 +5,7 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 94 actions, 0 customized, 3 unbound. + 95 actions, 0 customized, 3 unbound. [All] Common Customized (0) Unbound (3) App Composer Editor Vim Navigation Approval Debug diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap index 58b1b80803..856ac05d1e 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap @@ -2,14 +2,14 @@ source: tui/src/keymap_setup.rs expression: snapshot --- -tab: All (93 selectable) +tab: All (94 selectable) tab: Common (19 selectable) tab: Customized (0) (0 selectable) tab: Unbound (2) (2 selectable) tab: App (9 selectable) tab: Composer (5 selectable) tab: Editor (17 selectable) -tab: Vim (34 selectable) +tab: Vim (35 selectable) tab: Navigation (20 selectable) tab: Approval (8 selectable) tab: Debug (1 selectable) diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap index 6a98da9238..57b0131002 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap @@ -5,7 +5,7 @@ expression: "render_picker(params, 78)" Keymap All configurable shortcuts. - 93 actions, 0 customized, 2 unbound. + 94 actions, 0 customized, 2 unbound. [All] Common Customized (0) Unbound (2) App Composer Editor Vim Navigation Approval Debug diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap index 9d651ee4f0..4d10013649 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap @@ -5,7 +5,7 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 93 actions, 0 customized, 2 unbound. + 94 actions, 0 customized, 2 unbound. [All] Common Customized (0) Unbound (2) App Composer Editor Vim Navigation Approval Debug From 379511dceac290c273afc0e805e606f8ba438f2e Mon Sep 17 00:00:00 2001 From: jif-oai Date: Wed, 27 May 2026 18:44:07 +0200 Subject: [PATCH 23/87] Bump SQLx to pick up newer bundled SQLite (#24728) ## Why Codex stores thread, log, goal, and memory state in bundled SQLite databases through SQLx. We have a suspected SQLite WAL-reset corruption issue under heavy concurrent writer load, especially when multiple subagents are active. The existing `sqlx 0.8.6` dependency kept us on an older `libsqlite3-sys` / bundled SQLite, so this PR moves the SQLx stack far enough forward to pick up the newer bundled SQLite library. ## What changed - Bump the workspace `sqlx` dependency to `0.9.0`. - Use the SQLx 0.9 feature names explicitly: `runtime-tokio`, `tls-rustls`, and `sqlite-bundled`. - Update `Cargo.lock` so `sqlx-sqlite` resolves through `libsqlite3-sys 0.37.0`. - Refresh `MODULE.bazel.lock` for the dependency changes. - Adapt `codex-state` to SQLx 0.9: - build dynamic state queries with `QueryBuilder` instead of passing dynamic `String`s to `sqlx::query`; - remove the old `QueryBuilder` lifetime parameter from helper signatures; - preserve SQLx's new `Migrator` fields when constructing runtime migrators. ## Verification - `just test -p codex-state` - `just bazel-lock-check` - `cargo check -p codex-state --tests` --- MODULE.bazel.lock | 55 ++- codex-rs/Cargo.lock | 673 +++++++++++++++++--------- codex-rs/Cargo.toml | 7 +- codex-rs/state/src/migrations.rs | 2 + codex-rs/state/src/runtime/goals.rs | 87 ++-- codex-rs/state/src/runtime/logs.rs | 43 +- codex-rs/state/src/runtime/threads.rs | 66 +-- 7 files changed, 591 insertions(+), 342 deletions(-) diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 8a3cdeb66e..d5ef0b241c 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -704,6 +704,7 @@ "bitflags_2.11.0": "{\"dependencies\":[{\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"arbitrary\",\"req\":\"^1.0\"},{\"name\":\"bytemuck\",\"optional\":true,\"req\":\"^1.12\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"bytemuck\",\"req\":\"^1.12.2\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.228\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde_lib\",\"package\":\"serde\",\"req\":\"^1.0.103\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0.19\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.18\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"zerocopy\",\"req\":\"^0.8\"}],\"features\":{\"example_generated\":[],\"serde\":[\"serde_core\"],\"std\":[]}}", "blake2_0.10.6": "{\"dependencies\":[{\"features\":[\"mac\"],\"name\":\"digest\",\"req\":\"^0.10.3\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.10.3\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.2.2\"}],\"features\":{\"default\":[\"std\"],\"reset\":[],\"simd\":[],\"simd_asm\":[\"simd_opt\"],\"simd_opt\":[\"simd\"],\"size_opt\":[],\"std\":[\"digest/std\"]}}", "block-buffer_0.10.4": "{\"dependencies\":[{\"name\":\"generic-array\",\"req\":\"^0.14\"}],\"features\":{}}", + "block-buffer_0.12.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^1\"},{\"name\":\"hybrid-array\",\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.4\"}],\"features\":{}}", "block-padding_0.3.3": "{\"dependencies\":[{\"name\":\"generic-array\",\"req\":\"^0.14\"}],\"features\":{\"std\":[]}}", "block2_0.6.2": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"std\"],\"name\":\"objc2\",\"req\":\">=0.6.2, <0.8.0\"}],\"features\":{\"alloc\":[],\"compiler-rt\":[\"objc2/unstable-compiler-rt\"],\"default\":[\"std\"],\"gnustep-1-7\":[\"objc2/gnustep-1-7\"],\"gnustep-1-8\":[\"gnustep-1-7\",\"objc2/gnustep-1-8\"],\"gnustep-1-9\":[\"gnustep-1-8\",\"objc2/gnustep-1-9\"],\"gnustep-2-0\":[\"gnustep-1-9\",\"objc2/gnustep-2-0\"],\"gnustep-2-1\":[\"gnustep-2-0\",\"objc2/gnustep-2-1\"],\"std\":[\"alloc\"],\"unstable-coerce-pointee\":[],\"unstable-objfw\":[],\"unstable-private\":[],\"unstable-winobjc\":[\"gnustep-1-8\"]}}", "blocking_1.6.2": "{\"dependencies\":[{\"name\":\"async-channel\",\"req\":\"^2.0.0\"},{\"name\":\"async-task\",\"req\":\"^4.4.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-io\",\"req\":\"^0.3.28\"},{\"default_features\":false,\"name\":\"futures-lite\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"futures-lite\",\"req\":\"^2.0.0\"},{\"name\":\"piper\",\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.37\"}],\"features\":{}}", @@ -738,6 +739,7 @@ "cfg-if_1.0.4": "{\"dependencies\":[{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"}],\"features\":{\"rustc-dep-of-std\":[\"core\"]}}", "cfg_aliases_0.1.1": "{\"dependencies\":[],\"features\":{}}", "cfg_aliases_0.2.1": "{\"dependencies\":[],\"features\":{}}", + "chacha20_0.10.0": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"features\":[\"stream-wrapper\"],\"name\":\"cipher\",\"optional\":true,\"req\":\"^0.5\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"cipher\",\"req\":\"^0.5\"},{\"name\":\"cpufeatures\",\"req\":\"^0.3\",\"target\":\"cfg(any(target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.8.1\"}],\"features\":{\"default\":[\"cipher\"],\"legacy\":[\"cipher\"],\"rng\":[\"dep:rand_core\"],\"xchacha\":[\"cipher\"]}}", "chacha20_0.9.1": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"cipher\",\"req\":\"^0.4.4\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"cipher\",\"req\":\"^0.4.4\"},{\"name\":\"cpufeatures\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3.3\"}],\"features\":{\"std\":[\"cipher/std\"],\"zeroize\":[\"cipher/zeroize\"]}}", "chacha20poly1305_0.10.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aead\",\"req\":\"^0.5\"},{\"default_features\":false,\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"aead\",\"req\":\"^0.5\"},{\"features\":[\"zeroize\"],\"name\":\"chacha20\",\"req\":\"^0.9\"},{\"name\":\"cipher\",\"req\":\"^0.4\"},{\"name\":\"poly1305\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"zeroize\",\"req\":\"^1.5\"}],\"features\":{\"alloc\":[\"aead/alloc\"],\"default\":[\"alloc\",\"getrandom\"],\"getrandom\":[\"aead/getrandom\",\"rand_core\"],\"heapless\":[\"aead/heapless\"],\"rand_core\":[\"aead/rand_core\"],\"reduced-round\":[],\"std\":[\"aead/std\",\"alloc\"],\"stream\":[\"aead/stream\"]}}", "chardetng_0.1.17": "{\"dependencies\":[{\"name\":\"arrayvec\",\"optional\":true,\"req\":\"^0.5.1\"},{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"detone\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"name\":\"encoding_rs\",\"req\":\"^0.8.29\"},{\"default_features\":false,\"name\":\"memchr\",\"req\":\"^2.2.0\"},{\"name\":\"rayon\",\"optional\":true,\"req\":\"^1.3.0\"}],\"features\":{\"multithreading\":[\"rayon\",\"arrayvec\"],\"testing-only-no-semver-guarantees-do-not-use\":[]}}", @@ -757,6 +759,7 @@ "clipboard-win_5.4.1": "{\"dependencies\":[{\"name\":\"error-code\",\"req\":\"^3\",\"target\":\"cfg(windows)\"},{\"name\":\"windows-win\",\"optional\":true,\"req\":\"^3\",\"target\":\"cfg(windows)\"}],\"features\":{\"monitor\":[\"windows-win\"],\"std\":[\"error-code/std\"]}}", "clru_0.6.3": "{\"dependencies\":[{\"name\":\"hashbrown\",\"req\":\"^0.16\"}],\"features\":{}}", "cmake_0.1.57": "{\"dependencies\":[{\"name\":\"cc\",\"req\":\"^1.2.46\"}],\"features\":{}}", + "cmov_0.5.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.9\",\"target\":\"cfg(any(unix, windows))\"}],\"features\":{}}", "cmp_any_0.8.1": "{\"dependencies\":[],\"features\":{}}", "codespan-reporting_0.13.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.6.3\"},{\"kind\":\"dev\",\"name\":\"peg\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"pico-args\",\"req\":\"^0.5.0\"},{\"kind\":\"dev\",\"name\":\"rustyline\",\"req\":\"^6\"},{\"default_features\":false,\"features\":[\"derive\",\"alloc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"termcolor\",\"optional\":true,\"req\":\"^1.0.4\"},{\"name\":\"unicode-width\",\"req\":\">=0.1, <0.3\"},{\"kind\":\"dev\",\"name\":\"unindent\",\"req\":\"^0.1\"}],\"features\":{\"ascii-only\":[],\"default\":[\"std\",\"termcolor\"],\"serialization\":[\"serde\"],\"std\":[\"serde?/std\"],\"termcolor\":[\"std\",\"dep:termcolor\"]}}", "color-eyre_0.6.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"ansi-parser\",\"req\":\"^0.8.0\"},{\"name\":\"backtrace\",\"req\":\"^0.3.59\"},{\"name\":\"color-spantrace\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"eyre\",\"req\":\"^0.6\"},{\"name\":\"indenter\",\"req\":\"^0.3.0\"},{\"name\":\"once_cell\",\"req\":\"^1.18.0\"},{\"name\":\"owo-colors\",\"req\":\"^4.0\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"thiserror\",\"req\":\"^1.0.19\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1.13\"},{\"name\":\"tracing-error\",\"optional\":true,\"req\":\"^0.2.0\"},{\"features\":[\"env-filter\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3.0\"},{\"name\":\"url\",\"optional\":true,\"req\":\"^2.1.1\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.15\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"}],\"features\":{\"capture-spantrace\":[\"tracing-error\",\"color-spantrace\"],\"default\":[\"track-caller\",\"capture-spantrace\"],\"issue-url\":[\"url\"],\"track-caller\":[]}}", @@ -789,6 +792,7 @@ "coreaudio-sys_0.2.17": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"runtime\"],\"kind\":\"build\",\"name\":\"bindgen\",\"req\":\"^0.72\"}],\"features\":{\"audio_server_plugin\":[],\"audio_toolbox\":[],\"audio_unit\":[],\"core_audio\":[],\"core_midi\":[],\"default\":[\"audio_toolbox\",\"audio_unit\",\"core_audio\",\"audio_server_plugin\",\"open_al\",\"core_midi\"],\"io_kit_audio\":[],\"open_al\":[]}}", "cpal_0.15.3": "{\"dependencies\":[{\"name\":\"alsa\",\"req\":\"^0.9\",\"target\":\"cfg(any(target_os = \\\"linux\\\", target_os = \\\"dragonfly\\\", target_os = \\\"freebsd\\\", target_os = \\\"netbsd\\\"))\"},{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"asio-sys\",\"optional\":true,\"req\":\"^0.2\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.0\"},{\"name\":\"core-foundation-sys\",\"req\":\"^0.8.2\",\"target\":\"cfg(any(target_os = \\\"macos\\\", target_os = \\\"ios\\\"))\"},{\"default_features\":false,\"features\":[\"audio_unit\",\"core_audio\",\"audio_toolbox\"],\"name\":\"coreaudio-rs\",\"req\":\"^0.11\",\"target\":\"cfg(target_os = \\\"ios\\\")\"},{\"default_features\":false,\"features\":[\"audio_unit\",\"core_audio\"],\"name\":\"coreaudio-rs\",\"req\":\"^0.11\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"name\":\"dasp_sample\",\"req\":\"^0.11\"},{\"kind\":\"dev\",\"name\":\"hound\",\"req\":\"^3.5\"},{\"name\":\"jack\",\"optional\":true,\"req\":\"^0.11\",\"target\":\"cfg(any(target_os = \\\"linux\\\", target_os = \\\"dragonfly\\\", target_os = \\\"freebsd\\\", target_os = \\\"netbsd\\\"))\"},{\"name\":\"jni\",\"req\":\"^0.21\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"js-sys\",\"req\":\"^0.3.35\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"js-sys\",\"req\":\"^0.3.35\",\"target\":\"cfg(target_os = \\\"emscripten\\\")\"},{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_os = \\\"linux\\\", target_os = \\\"dragonfly\\\", target_os = \\\"freebsd\\\", target_os = \\\"netbsd\\\"))\"},{\"name\":\"mach2\",\"req\":\"^0.4\",\"target\":\"cfg(any(target_os = \\\"macos\\\", target_os = \\\"ios\\\"))\"},{\"default_features\":false,\"name\":\"ndk\",\"req\":\"^0.8\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"ndk-context\",\"req\":\"^0.1\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"kind\":\"dev\",\"name\":\"ndk-glue\",\"req\":\"^0.7\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"num-traits\",\"optional\":true,\"req\":\"^0.2.6\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"features\":[\"java-interface\"],\"name\":\"oboe\",\"req\":\"^0.6\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"kind\":\"dev\",\"name\":\"ringbuf\",\"req\":\"^0.3\"},{\"name\":\"wasm-bindgen\",\"optional\":true,\"req\":\"^0.2.58\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"wasm-bindgen\",\"req\":\"^0.2.89\",\"target\":\"cfg(target_os = \\\"emscripten\\\")\"},{\"name\":\"wasm-bindgen-futures\",\"req\":\"^0.4.33\",\"target\":\"cfg(target_os = \\\"emscripten\\\")\"},{\"features\":[\"AudioContext\",\"AudioContextOptions\",\"AudioBuffer\",\"AudioBufferSourceNode\",\"AudioNode\",\"AudioDestinationNode\",\"Window\",\"AudioContextState\"],\"name\":\"web-sys\",\"req\":\"^0.3.35\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"unknown\\\"))\"},{\"features\":[\"AudioContext\",\"AudioContextOptions\",\"AudioBuffer\",\"AudioBufferSourceNode\",\"AudioNode\",\"AudioDestinationNode\",\"Window\",\"AudioContextState\"],\"name\":\"web-sys\",\"req\":\"^0.3.35\",\"target\":\"cfg(target_os = \\\"emscripten\\\")\"},{\"features\":[\"Win32_Media_Audio\",\"Win32_Foundation\",\"Win32_Devices_Properties\",\"Win32_Media_KernelStreaming\",\"Win32_System_Com_StructuredStorage\",\"Win32_System_Threading\",\"Win32_Security\",\"Win32_System_SystemServices\",\"Win32_System_Variant\",\"Win32_Media_Multimedia\",\"Win32_UI_Shell_PropertiesSystem\"],\"name\":\"windows\",\"req\":\"^0.54.0\",\"target\":\"cfg(target_os = \\\"windows\\\")\"}],\"features\":{\"asio\":[\"asio-sys\",\"num-traits\"],\"oboe-shared-stdcxx\":[\"oboe/shared-stdcxx\"]}}", "cpufeatures_0.2.17": "{\"dependencies\":[{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.155\",\"target\":\"aarch64-linux-android\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.155\",\"target\":\"cfg(all(target_arch = \\\"aarch64\\\", target_os = \\\"linux\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.155\",\"target\":\"cfg(all(target_arch = \\\"aarch64\\\", target_vendor = \\\"apple\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.155\",\"target\":\"cfg(all(target_arch = \\\"loongarch64\\\", target_os = \\\"linux\\\"))\"}],\"features\":{}}", + "cpufeatures_0.3.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.155\",\"target\":\"cfg(all(target_arch = \\\"aarch64\\\", target_os = \\\"android\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.155\",\"target\":\"cfg(all(target_arch = \\\"aarch64\\\", target_os = \\\"linux\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.155\",\"target\":\"cfg(all(target_arch = \\\"aarch64\\\", target_vendor = \\\"apple\\\"))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.155\",\"target\":\"cfg(all(target_arch = \\\"loongarch64\\\", target_os = \\\"linux\\\"))\"}],\"features\":{}}", "crc-catalog_2.4.0": "{\"dependencies\":[],\"features\":{}}", "crc32fast_1.5.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1\"},{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"}],\"features\":{\"default\":[\"std\"],\"nightly\":[],\"std\":[]}}", "crc_3.4.0": "{\"dependencies\":[{\"name\":\"crc-catalog\",\"req\":\"^2.4.0\"}],\"features\":{}}", @@ -802,6 +806,7 @@ "crunchy_0.2.4": "{\"dependencies\":[],\"features\":{\"default\":[\"limit_128\"],\"limit_1024\":[],\"limit_128\":[],\"limit_2048\":[],\"limit_256\":[],\"limit_512\":[],\"limit_64\":[],\"std\":[]}}", "crypto-bigint_0.5.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1\"},{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"der\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"generic-array\",\"optional\":true,\"req\":\"^0.14\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"num-bigint\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"num-integer\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"num-traits\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rand_chacha\",\"req\":\"^0.3\"},{\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6.4\"},{\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"default_features\":false,\"name\":\"rlp\",\"optional\":true,\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"serdect\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2.4\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"serdect?/alloc\"],\"default\":[\"rand\"],\"extra-sizes\":[],\"rand\":[\"rand_core/std\"],\"serde\":[\"dep:serdect\"]}}", "crypto-common_0.1.7": "{\"dependencies\":[{\"features\":[\"more_lengths\"],\"name\":\"generic-array\",\"req\":\"=0.14.7\"},{\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"typenum\",\"req\":\"^1.14\"}],\"features\":{\"getrandom\":[\"rand_core/getrandom\"],\"std\":[]}}", + "crypto-common_0.2.2": "{\"dependencies\":[{\"features\":[\"sys_rng\"],\"name\":\"getrandom\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"hybrid-array\",\"req\":\"^0.4.7\"},{\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.10\"}],\"features\":{\"getrandom\":[\"rand_core\",\"dep:getrandom\"],\"rand_core\":[\"dep:rand_core\"],\"zeroize\":[\"hybrid-array/zeroize\"]}}", "crypto_box_0.9.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aead\",\"req\":\"^0.5.2\"},{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"blake2\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"chacha20\",\"optional\":true,\"req\":\"^0.9\"},{\"default_features\":false,\"name\":\"crypto_secretbox\",\"req\":\"^0.1.1\"},{\"default_features\":false,\"features\":[\"zeroize\"],\"name\":\"curve25519-dalek\",\"req\":\"^4\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rmp-serde\",\"req\":\"^1\"},{\"name\":\"salsa20\",\"optional\":true,\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"serdect\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"zeroize\",\"req\":\"^1\"}],\"features\":{\"alloc\":[\"aead/alloc\"],\"chacha20\":[\"dep:chacha20\",\"crypto_secretbox/chacha20\"],\"default\":[\"alloc\",\"getrandom\",\"salsa20\"],\"getrandom\":[\"aead/getrandom\",\"rand_core\"],\"heapless\":[\"aead/heapless\"],\"rand_core\":[\"aead/rand_core\"],\"salsa20\":[\"dep:salsa20\",\"crypto_secretbox/salsa20\"],\"seal\":[\"dep:blake2\",\"alloc\"],\"serde\":[\"dep:serdect\"],\"std\":[\"aead/std\"]}}", "crypto_secretbox_0.1.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aead\",\"req\":\"^0.5\"},{\"features\":[\"zeroize\"],\"name\":\"chacha20\",\"optional\":true,\"req\":\"^0.9\"},{\"default_features\":false,\"name\":\"cipher\",\"req\":\"^0.4\"},{\"default_features\":false,\"features\":[\"zeroize\"],\"name\":\"generic-array\",\"req\":\"^0.14.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"name\":\"poly1305\",\"req\":\"^0.8\"},{\"features\":[\"zeroize\"],\"name\":\"salsa20\",\"optional\":true,\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"zeroize\",\"req\":\"^1\"}],\"features\":{\"alloc\":[\"aead/alloc\"],\"default\":[\"alloc\",\"getrandom\",\"salsa20\"],\"getrandom\":[\"aead/getrandom\",\"rand_core\"],\"heapless\":[\"aead/heapless\"],\"rand_core\":[\"aead/rand_core\"],\"std\":[\"aead/std\",\"alloc\"],\"stream\":[\"aead/stream\"]}}", "csv-core_0.1.13": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"arrayvec\",\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"memchr\",\"req\":\"^2\"}],\"features\":{\"default\":[],\"libc\":[\"memchr/libc\"]}}", @@ -809,6 +814,7 @@ "ctor-proc-macro_0.0.7": "{\"dependencies\":[],\"features\":{\"default\":[]}}", "ctor_0.1.26": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"libc-print\",\"req\":\"^0.1.20\"},{\"name\":\"quote\",\"req\":\"^1.0.20\"},{\"default_features\":false,\"features\":[\"full\",\"parsing\",\"printing\",\"proc-macro\"],\"name\":\"syn\",\"req\":\"^1.0.98\"}],\"features\":{}}", "ctor_0.6.3": "{\"dependencies\":[{\"name\":\"ctor-proc-macro\",\"optional\":true,\"req\":\"=0.0.7\"},{\"default_features\":false,\"name\":\"dtor\",\"optional\":true,\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"libc-print\",\"req\":\"^0.1.20\"}],\"features\":{\"__no_warn_on_missing_unsafe\":[\"dtor?/__no_warn_on_missing_unsafe\"],\"default\":[\"dtor\",\"proc_macro\",\"__no_warn_on_missing_unsafe\"],\"dtor\":[\"dep:dtor\"],\"proc_macro\":[\"dep:ctor-proc-macro\",\"dtor?/proc_macro\"],\"used_linker\":[\"dtor?/used_linker\"]}}", + "ctutils_0.4.2": "{\"dependencies\":[{\"name\":\"cmov\",\"req\":\"^0.5.3\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.11\"},{\"default_features\":false,\"name\":\"subtle\",\"optional\":true,\"req\":\"^2\"}],\"features\":{\"alloc\":[],\"subtle\":[\"dep:subtle\"]}}", "curve25519-dalek-derive_0.1.1": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.66\"},{\"name\":\"quote\",\"req\":\"^1.0.31\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0.27\"}],\"features\":{}}", "curve25519-dalek_4.1.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1\"},{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"cpufeatures\",\"req\":\"^0.2.6\",\"target\":\"cfg(target_arch = \\\"x86_64\\\")\"},{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"curve25519-dalek-derive\",\"req\":\"^0.1\",\"target\":\"cfg(all(not(curve25519_dalek_backend = \\\"fiat\\\"), not(curve25519_dalek_backend = \\\"serial\\\"), target_arch = \\\"x86_64\\\"))\"},{\"default_features\":false,\"name\":\"digest\",\"optional\":true,\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"ff\",\"optional\":true,\"req\":\"^0.13\"},{\"default_features\":false,\"name\":\"fiat-crypto\",\"req\":\"^0.2.1\",\"target\":\"cfg(curve25519_dalek_backend = \\\"fiat\\\")\"},{\"default_features\":false,\"name\":\"group\",\"optional\":true,\"req\":\"^0.13\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.2\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6.4\"},{\"default_features\":false,\"features\":[\"getrandom\"],\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"kind\":\"build\",\"name\":\"rustc_version\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2.3.0\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"zeroize?/alloc\"],\"default\":[\"alloc\",\"precomputed-tables\",\"zeroize\"],\"group\":[\"dep:group\",\"rand_core\"],\"group-bits\":[\"group\",\"ff/bits\"],\"legacy_compatibility\":[],\"precomputed-tables\":[]}}", "cxx-build_1.0.194": "{\"dependencies\":[{\"name\":\"cc\",\"req\":\"^1.0.101\"},{\"name\":\"codespan-reporting\",\"req\":\"^0.13.1\"},{\"kind\":\"dev\",\"name\":\"cxx\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"cxx-gen\",\"req\":\"^0.7\"},{\"name\":\"indexmap\",\"req\":\"^2.9.0\"},{\"kind\":\"dev\",\"name\":\"pkg-config\",\"req\":\"^0.3.27\"},{\"default_features\":false,\"features\":[\"span-locations\"],\"name\":\"proc-macro2\",\"req\":\"^1.0.74\"},{\"default_features\":false,\"name\":\"quote\",\"req\":\"^1.0.35\"},{\"name\":\"scratch\",\"req\":\"^1.0.5\"},{\"default_features\":false,\"features\":[\"clone-impls\",\"full\",\"parsing\",\"printing\"],\"name\":\"syn\",\"req\":\"^2.0.46\"}],\"features\":{\"parallel\":[\"cc/parallel\"]}}", @@ -850,6 +856,7 @@ "difflib_0.4.0": "{\"dependencies\":[],\"features\":{}}", "diffy_0.4.2": "{\"dependencies\":[{\"name\":\"nu-ansi-term\",\"req\":\"^0.50\"}],\"features\":{}}", "digest_0.10.7": "{\"dependencies\":[{\"name\":\"blobby\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"block-buffer\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"const-oid\",\"optional\":true,\"req\":\"^0.9\"},{\"name\":\"crypto-common\",\"req\":\"^0.1.3\"},{\"default_features\":false,\"name\":\"subtle\",\"optional\":true,\"req\":\"^2.4\"}],\"features\":{\"alloc\":[],\"core-api\":[\"block-buffer\"],\"default\":[\"core-api\"],\"dev\":[\"blobby\"],\"mac\":[\"subtle\"],\"oid\":[\"const-oid\"],\"rand_core\":[\"crypto-common/rand_core\"],\"std\":[\"alloc\",\"crypto-common/std\"]}}", + "digest_0.11.3": "{\"dependencies\":[{\"name\":\"blobby\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"block-buffer\",\"optional\":true,\"req\":\"^0.12\"},{\"name\":\"common\",\"package\":\"crypto-common\",\"req\":\"^0.2\"},{\"name\":\"const-oid\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"ctutils\",\"optional\":true,\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.11\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.7\"}],\"features\":{\"alloc\":[],\"block-api\":[\"dep:block-buffer\"],\"default\":[\"block-api\"],\"dev\":[\"blobby\"],\"getrandom\":[\"common/getrandom\",\"rand_core\"],\"mac\":[\"dep:ctutils\"],\"oid\":[\"dep:const-oid\"],\"rand_core\":[\"common/rand_core\"],\"zeroize\":[\"dep:zeroize\",\"block-buffer?/zeroize\"]}}", "diplomat-runtime_0.15.1": "{\"dependencies\":[{\"name\":\"jni\",\"optional\":true,\"req\":\"^0.22\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4\"}],\"features\":{\"jvm-callback-support\":[\"dep:jni\"],\"log\":[\"dep:log\"]}}", "diplomat_0.15.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"diplomat_core\",\"req\":\"^0.15.0\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.7.1\"},{\"kind\":\"dev\",\"name\":\"prettyplease\",\"req\":\"^0.2.30\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.27\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"features\":[\"full\",\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.2.0\"}],\"features\":{}}", "diplomat_core_0.15.0": "{\"dependencies\":[{\"name\":\"displaydoc\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"either\",\"optional\":true,\"req\":\"^1.9.0\"},{\"features\":[\"yaml\"],\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.7.1\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.27\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"derive\",\"alloc\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"smallvec\",\"req\":\"^1.9.0\"},{\"features\":[\"ident\"],\"name\":\"strck\",\"req\":\"^1.0\"},{\"features\":[\"full\",\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{\"hir\":[\"either\"]}}", @@ -899,7 +906,7 @@ "erased-serde_0.3.31": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.13\"},{\"default_features\":false,\"name\":\"serde\",\"req\":\"^1.0.166\"},{\"kind\":\"dev\",\"name\":\"serde_cbor\",\"req\":\"^0.11.2\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.166\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.99\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.83\"}],\"features\":{\"alloc\":[\"serde/alloc\"],\"default\":[\"std\"],\"std\":[\"serde/std\"],\"unstable-debug\":[]}}", "errno_0.3.14": "{\"dependencies\":[{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(target_os=\\\"hermit\\\")\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(target_os=\\\"wasi\\\")\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Diagnostics_Debug\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <0.62\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"libc/std\"]}}", "error-code_3.3.2": "{\"dependencies\":[],\"features\":{\"std\":[]}}", - "etcetera_0.8.0": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"home\",\"req\":\"^0.5\"},{\"features\":[\"Win32_Foundation\",\"Win32_UI_Shell\"],\"name\":\"windows-sys\",\"req\":\"^0.48\",\"target\":\"cfg(windows)\"}],\"features\":{}}", + "etcetera_0.11.0": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Com\",\"Win32_UI_Shell\"],\"name\":\"windows-sys\",\"req\":\"^0.61\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "event-listener-strategy_0.5.4": "{\"dependencies\":[{\"default_features\":false,\"name\":\"event-listener\",\"req\":\"^5.0.0\"},{\"kind\":\"dev\",\"name\":\"futures-lite\",\"req\":\"^2.0.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.12\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.37\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"}],\"features\":{\"default\":[\"std\"],\"loom\":[\"event-listener/loom\"],\"portable-atomic\":[\"event-listener/portable-atomic\"],\"std\":[\"event-listener/std\"]}}", "event-listener_5.4.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"concurrent-queue\",\"req\":\"^2.4.0\"},{\"default_features\":false,\"features\":[\"cargo_bench_support\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7\"},{\"default_features\":false,\"name\":\"critical-section\",\"optional\":true,\"req\":\"^1.2.0\"},{\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"critical-section\",\"req\":\"^1.2.0\"},{\"kind\":\"dev\",\"name\":\"futures-lite\",\"req\":\"^2.0.0\"},{\"name\":\"loom\",\"optional\":true,\"req\":\"^0.7\",\"target\":\"cfg(loom)\"},{\"name\":\"parking\",\"optional\":true,\"req\":\"^2.0.0\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.12\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"portable-atomic-util\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"portable_atomic_crate\",\"optional\":true,\"package\":\"portable-atomic\",\"req\":\"^1.2.0\"},{\"kind\":\"dev\",\"name\":\"try-lock\",\"req\":\"^0.2.5\"},{\"kind\":\"dev\",\"name\":\"waker-fn\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"}],\"features\":{\"default\":[\"std\"],\"loom\":[\"concurrent-queue/loom\",\"parking?/loom\",\"dep:loom\"],\"portable-atomic\":[\"portable-atomic-util\",\"portable_atomic_crate\",\"concurrent-queue/portable-atomic\"],\"std\":[\"concurrent-queue/std\",\"parking\"]}}", "eventsource-stream_0.2.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"http\",\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"nom\",\"req\":\"^7.1\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.8\"},{\"features\":[\"stream\"],\"kind\":\"dev\",\"name\":\"reqwest\",\"req\":\"^0.11\"},{\"features\":[\"macros\",\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"url\",\"req\":\"^2.2\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"futures-core/std\",\"nom/std\"]}}", @@ -926,7 +933,6 @@ "fluent-langneg_0.13.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"unic-langid\",\"req\":\"^0.9\"},{\"features\":[\"macros\"],\"kind\":\"dev\",\"name\":\"unic-langid\",\"req\":\"^0.9\"},{\"features\":[\"macros\"],\"kind\":\"dev\",\"name\":\"unic-locale\",\"req\":\"^0.9\"}],\"features\":{\"cldr\":[\"unic-langid/likelysubtags\"],\"default\":[]}}", "fluent-syntax_0.11.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"iai\",\"req\":\"^0.1\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"thiserror\",\"req\":\"^1.0\"}],\"features\":{\"all-benchmarks\":[],\"default\":[],\"json\":[\"serde\",\"serde_json\"]}}", "fluent_0.16.1": "{\"dependencies\":[{\"name\":\"fluent-bundle\",\"req\":\"^0.15.3\"},{\"name\":\"fluent-pseudo\",\"optional\":true,\"req\":\"^0.3.2\"},{\"name\":\"unic-langid\",\"req\":\"^0.9\"}],\"features\":{}}", - "flume_0.11.1": "{\"dependencies\":[{\"features\":[\"attributes\",\"unstable\"],\"kind\":\"dev\",\"name\":\"async-std\",\"req\":\"^1.13.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"},{\"kind\":\"dev\",\"name\":\"crossbeam-channel\",\"req\":\"^0.5.5\"},{\"kind\":\"dev\",\"name\":\"crossbeam-utils\",\"req\":\"^0.8.10\"},{\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-core\",\"optional\":true,\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-sink\",\"optional\":true,\"req\":\"^0.3\"},{\"features\":[\"js\"],\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.2.15\"},{\"features\":[\"getrandom\"],\"name\":\"nanorand\",\"optional\":true,\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.3\"},{\"features\":[\"mutex\"],\"name\":\"spin1\",\"package\":\"spin\",\"req\":\"^0.9.8\"},{\"features\":[\"rt\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.16.1\"},{\"kind\":\"dev\",\"name\":\"waker-fn\",\"req\":\"^1.1.0\"}],\"features\":{\"async\":[\"futures-sink\",\"futures-core\"],\"default\":[\"async\",\"select\",\"eventual-fairness\"],\"eventual-fairness\":[\"select\",\"nanorand\"],\"select\":[],\"spin\":[]}}", "flume_0.12.0": "{\"dependencies\":[{\"features\":[\"attributes\",\"unstable\"],\"kind\":\"dev\",\"name\":\"async-std\",\"req\":\"^1.13.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"},{\"kind\":\"dev\",\"name\":\"crossbeam-channel\",\"req\":\"^0.5.5\"},{\"kind\":\"dev\",\"name\":\"crossbeam-utils\",\"req\":\"^0.8.10\"},{\"features\":[\"std\",\"js\"],\"name\":\"fastrand\",\"optional\":true,\"req\":\"^2.3\"},{\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-core\",\"optional\":true,\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-sink\",\"optional\":true,\"req\":\"^0.3\"},{\"features\":[\"js\"],\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.2.15\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.3\"},{\"features\":[\"mutex\"],\"name\":\"spin1\",\"package\":\"spin\",\"req\":\"^0.9.8\"},{\"features\":[\"rt\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.16.1\"},{\"kind\":\"dev\",\"name\":\"waker-fn\",\"req\":\"^1.1.0\"}],\"features\":{\"async\":[\"futures-sink\",\"futures-core\"],\"default\":[\"async\",\"select\",\"eventual-fairness\"],\"eventual-fairness\":[\"select\",\"fastrand\"],\"select\":[],\"spin\":[]}}", "fnv_1.0.7": "{\"dependencies\":[],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "foldhash_0.1.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"ahash\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"chrono\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"fxhash\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"hashbrown\",\"req\":\"^0.14\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"uuid\",\"req\":\"^1.8\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", @@ -938,20 +944,15 @@ "fs_extra_1.3.0": "{\"dependencies\":[],\"features\":{}}", "fsevent-sys_4.1.0": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.68\"}],\"features\":{}}", "fslock_0.2.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.66\",\"target\":\"cfg(unix)\"},{\"features\":[\"minwindef\",\"minwinbase\",\"winbase\",\"errhandlingapi\",\"winerror\",\"winnt\",\"synchapi\",\"handleapi\",\"fileapi\",\"processthreadsapi\"],\"name\":\"winapi\",\"req\":\"^0.3.8\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", - "futures-channel_0.3.31": "{\"dependencies\":[{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-sink\",\"optional\":true,\"req\":\"^0.3.31\"}],\"features\":{\"alloc\":[\"futures-core/alloc\"],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"sink\":[\"futures-sink\"],\"std\":[\"alloc\",\"futures-core/std\"],\"unstable\":[]}}", "futures-channel_0.3.32": "{\"dependencies\":[{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-sink\",\"optional\":true,\"req\":\"^0.3.32\"}],\"features\":{\"alloc\":[\"futures-core/alloc\"],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"sink\":[\"futures-sink\"],\"std\":[\"alloc\",\"futures-core/std\"],\"unstable\":[]}}", - "futures-core_0.3.31": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"require-cas\"],\"name\":\"portable-atomic\",\"optional\":true,\"req\":\"^1.3\"}],\"features\":{\"alloc\":[],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"std\":[\"alloc\"],\"unstable\":[]}}", "futures-core_0.3.32": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"require-cas\"],\"name\":\"portable-atomic\",\"optional\":true,\"req\":\"^1.3\"}],\"features\":{\"alloc\":[],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"std\":[\"alloc\"],\"unstable\":[]}}", - "futures-executor_0.3.31": "{\"dependencies\":[{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-task\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-util\",\"req\":\"^0.3.31\"},{\"name\":\"num_cpus\",\"optional\":true,\"req\":\"^1.8.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"futures-core/std\",\"futures-task/std\",\"futures-util/std\"],\"thread-pool\":[\"std\",\"num_cpus\"]}}", + "futures-executor_0.3.32": "{\"dependencies\":[{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-task\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-util\",\"req\":\"^0.3.32\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"futures-core/std\",\"futures-task/std\",\"futures-util/std\"],\"thread-pool\":[\"std\"]}}", "futures-intrusive_0.5.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-std\",\"req\":\"^1.4\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"crossbeam\",\"req\":\"^0.7\"},{\"features\":[\"async-await\"],\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.0\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"futures-test\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.4.0\"},{\"name\":\"lock_api\",\"req\":\"^0.4.1\"},{\"name\":\"parking_lot\",\"optional\":true,\"req\":\"^0.12.0\"},{\"kind\":\"dev\",\"name\":\"pin-utils\",\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"signal-hook\",\"req\":\"^0.1.11\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.14\"}],\"features\":{\"alloc\":[\"futures-core/alloc\"],\"default\":[\"std\"],\"std\":[\"alloc\",\"parking_lot\"]}}", - "futures-io_0.3.31": "{\"dependencies\":[],\"features\":{\"default\":[\"std\"],\"std\":[],\"unstable\":[]}}", + "futures-io_0.3.32": "{\"dependencies\":[],\"features\":{\"default\":[\"std\"],\"std\":[],\"unstable\":[]}}", "futures-lite_2.6.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"fastrand\",\"optional\":true,\"req\":\"^2.0.0\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.5\"},{\"name\":\"futures-io\",\"optional\":true,\"req\":\"^0.3.5\"},{\"name\":\"memchr\",\"optional\":true,\"req\":\"^2.3.3\"},{\"name\":\"parking\",\"optional\":true,\"req\":\"^2.2.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.0\"},{\"kind\":\"dev\",\"name\":\"spin_on\",\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"waker-fn\",\"req\":\"^1.0.0\"}],\"features\":{\"alloc\":[],\"default\":[\"race\",\"std\"],\"race\":[\"fastrand\"],\"std\":[\"alloc\",\"fastrand/std\",\"futures-io\",\"parking\"]}}", - "futures-macro_0.3.31": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0.52\"}],\"features\":{}}", - "futures-sink_0.3.31": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", + "futures-macro_0.3.32": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0.52\"}],\"features\":{}}", "futures-sink_0.3.32": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", - "futures-task_0.3.31": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"std\":[\"alloc\"],\"unstable\":[]}}", "futures-task_0.3.32": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"std\":[\"alloc\"],\"unstable\":[]}}", - "futures-util_0.3.31": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-channel\",\"optional\":true,\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-io\",\"optional\":true,\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-macro\",\"optional\":true,\"req\":\"=0.3.31\"},{\"default_features\":false,\"name\":\"futures-sink\",\"optional\":true,\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-task\",\"req\":\"^0.3.31\"},{\"name\":\"futures_01\",\"optional\":true,\"package\":\"futures\",\"req\":\"^0.1.25\"},{\"name\":\"memchr\",\"optional\":true,\"req\":\"^2.2\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.6\"},{\"name\":\"pin-utils\",\"req\":\"^0.1.0\"},{\"name\":\"slab\",\"optional\":true,\"req\":\"^0.4.2\"},{\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^0.1.11\"},{\"name\":\"tokio-io\",\"optional\":true,\"req\":\"^0.1.9\"}],\"features\":{\"alloc\":[\"futures-core/alloc\",\"futures-task/alloc\"],\"async-await\":[],\"async-await-macro\":[\"async-await\",\"futures-macro\"],\"bilock\":[],\"cfg-target-has-atomic\":[],\"channel\":[\"std\",\"futures-channel\"],\"compat\":[\"std\",\"futures_01\"],\"default\":[\"std\",\"async-await\",\"async-await-macro\"],\"io\":[\"std\",\"futures-io\",\"memchr\"],\"io-compat\":[\"io\",\"compat\",\"tokio-io\"],\"portable-atomic\":[\"futures-core/portable-atomic\"],\"sink\":[\"futures-sink\"],\"std\":[\"alloc\",\"futures-core/std\",\"futures-task/std\",\"slab\"],\"unstable\":[\"futures-core/unstable\",\"futures-task/unstable\"],\"write-all-vectored\":[\"io\"]}}", "futures-util_0.3.32": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-channel\",\"optional\":true,\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-io\",\"optional\":true,\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-macro\",\"optional\":true,\"req\":\"=0.3.32\"},{\"default_features\":false,\"name\":\"futures-sink\",\"optional\":true,\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-task\",\"req\":\"^0.3.32\"},{\"name\":\"futures_01\",\"optional\":true,\"package\":\"futures\",\"req\":\"^0.1.25\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.26\"},{\"name\":\"memchr\",\"optional\":true,\"req\":\"^2.2\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.6\"},{\"default_features\":false,\"name\":\"slab\",\"optional\":true,\"req\":\"^0.4.7\"},{\"name\":\"spin\",\"optional\":true,\"req\":\"^0.10.0\"},{\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^0.1.11\"},{\"name\":\"tokio-io\",\"optional\":true,\"req\":\"^0.1.9\"}],\"features\":{\"alloc\":[\"futures-core/alloc\",\"futures-task/alloc\",\"slab\"],\"async-await\":[],\"async-await-macro\":[\"async-await\",\"futures-macro\"],\"bilock\":[],\"cfg-target-has-atomic\":[],\"channel\":[\"std\",\"futures-channel\"],\"compat\":[\"std\",\"futures_01\",\"libc\"],\"default\":[\"std\",\"async-await\",\"async-await-macro\"],\"io\":[\"std\",\"futures-io\",\"memchr\"],\"io-compat\":[\"io\",\"compat\",\"tokio-io\",\"libc\"],\"portable-atomic\":[\"futures-core/portable-atomic\"],\"sink\":[\"futures-sink\"],\"std\":[\"alloc\",\"futures-core/std\",\"futures-task/std\",\"slab/std\"],\"unstable\":[\"futures-core/unstable\",\"futures-task/unstable\"],\"write-all-vectored\":[\"io\"]}}", "futures_0.3.31": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"assert_matches\",\"req\":\"^1.3.0\"},{\"default_features\":false,\"features\":[\"sink\"],\"name\":\"futures-channel\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-executor\",\"optional\":true,\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-io\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-sink\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-task\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"features\":[\"sink\"],\"name\":\"futures-util\",\"req\":\"^0.3.31\"},{\"kind\":\"dev\",\"name\":\"pin-project\",\"req\":\"^1.0.11\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^0.1.11\"}],\"features\":{\"alloc\":[\"futures-core/alloc\",\"futures-task/alloc\",\"futures-sink/alloc\",\"futures-channel/alloc\",\"futures-util/alloc\"],\"async-await\":[\"futures-util/async-await\",\"futures-util/async-await-macro\"],\"bilock\":[\"futures-util/bilock\"],\"cfg-target-has-atomic\":[],\"compat\":[\"std\",\"futures-util/compat\"],\"default\":[\"std\",\"async-await\",\"executor\"],\"executor\":[\"std\",\"futures-executor/std\"],\"io-compat\":[\"compat\",\"futures-util/io-compat\"],\"std\":[\"alloc\",\"futures-core/std\",\"futures-task/std\",\"futures-io/std\",\"futures-sink/std\",\"futures-util/std\",\"futures-util/io\",\"futures-util/channel\"],\"thread-pool\":[\"executor\",\"futures-executor/thread-pool\"],\"unstable\":[\"futures-core/unstable\",\"futures-task/unstable\",\"futures-channel/unstable\",\"futures-io/unstable\",\"futures-util/unstable\"],\"write-all-vectored\":[\"futures-util/write-all-vectored\"]}}", "fxhash_0.2.1": "{\"dependencies\":[{\"name\":\"byteorder\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"fnv\",\"req\":\"^1.0.5\"},{\"kind\":\"dev\",\"name\":\"seahash\",\"req\":\"^3.0.5\"}],\"features\":{}}", @@ -1048,7 +1049,7 @@ "hashbrown_0.14.5": "{\"dependencies\":[{\"default_features\":false,\"name\":\"ahash\",\"optional\":true,\"req\":\"^0.8.7\"},{\"name\":\"alloc\",\"optional\":true,\"package\":\"rustc-std-workspace-alloc\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"allocator-api2\",\"optional\":true,\"req\":\"^0.2.9\"},{\"features\":[\"allocator-api2\"],\"kind\":\"dev\",\"name\":\"bumpalo\",\"req\":\"^3.13.0\"},{\"name\":\"compiler_builtins\",\"optional\":true,\"req\":\"^0.1.2\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3.1\"},{\"default_features\":false,\"name\":\"equivalent\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"fnv\",\"req\":\"^1.0.7\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.4\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.3\"},{\"name\":\"rayon\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"rkyv\",\"optional\":true,\"req\":\"^0.7.42\"},{\"features\":[\"validation\"],\"kind\":\"dev\",\"name\":\"rkyv\",\"req\":\"^0.7.42\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.25\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"ahash\",\"inline-more\",\"allocator-api2\"],\"inline-more\":[],\"nightly\":[\"allocator-api2?/nightly\",\"bumpalo/allocator_api\"],\"raw\":[],\"rustc-dep-of-std\":[\"nightly\",\"core\",\"compiler_builtins\",\"alloc\",\"rustc-internal-api\"],\"rustc-internal-api\":[]}}", "hashbrown_0.15.5": "{\"dependencies\":[{\"name\":\"alloc\",\"optional\":true,\"package\":\"rustc-std-workspace-alloc\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"allocator-api2\",\"optional\":true,\"req\":\"^0.2.9\"},{\"features\":[\"allocator-api2\"],\"kind\":\"dev\",\"name\":\"bumpalo\",\"req\":\"^3.13.0\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3.1\"},{\"default_features\":false,\"name\":\"equivalent\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"fnv\",\"req\":\"^1.0.7\"},{\"default_features\":false,\"name\":\"foldhash\",\"optional\":true,\"req\":\"^0.1.2\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.4\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9.0\"},{\"name\":\"rayon\",\"optional\":true,\"req\":\"^1.2\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.2\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.25\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"default-hasher\",\"inline-more\",\"allocator-api2\",\"equivalent\",\"raw-entry\"],\"default-hasher\":[\"dep:foldhash\"],\"inline-more\":[],\"nightly\":[\"bumpalo/allocator_api\"],\"raw-entry\":[],\"rustc-dep-of-std\":[\"nightly\",\"core\",\"alloc\",\"rustc-internal-api\"],\"rustc-internal-api\":[]}}", "hashbrown_0.16.1": "{\"dependencies\":[{\"name\":\"alloc\",\"optional\":true,\"package\":\"rustc-std-workspace-alloc\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"allocator-api2\",\"optional\":true,\"req\":\"^0.2.9\"},{\"features\":[\"allocator-api2\"],\"kind\":\"dev\",\"name\":\"bumpalo\",\"req\":\"^3.13.0\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"name\":\"equivalent\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"fnv\",\"req\":\"^1.0.7\"},{\"default_features\":false,\"name\":\"foldhash\",\"optional\":true,\"req\":\"^0.2.0\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.4\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.155\",\"target\":\"cfg(unix)\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9.0\"},{\"name\":\"rayon\",\"optional\":true,\"req\":\"^1.9.0\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.2\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.220\",\"target\":\"cfg(any())\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.221\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"default-hasher\",\"inline-more\",\"allocator-api2\",\"equivalent\",\"raw-entry\"],\"default-hasher\":[\"dep:foldhash\"],\"inline-more\":[],\"nightly\":[\"foldhash?/nightly\",\"bumpalo/allocator_api\"],\"raw-entry\":[],\"rustc-dep-of-std\":[\"nightly\",\"core\",\"alloc\",\"rustc-internal-api\"],\"rustc-internal-api\":[],\"serde\":[\"dep:serde_core\",\"dep:serde\"]}}", - "hashlink_0.10.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"default-hasher\",\"inline-more\"],\"name\":\"hashbrown\",\"req\":\"^0.15\"},{\"kind\":\"dev\",\"name\":\"rustc-hash\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"}],\"features\":{\"serde_impl\":[\"serde\"]}}", + "hashlink_0.11.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"default-hasher\"],\"name\":\"hashbrown\",\"req\":\"^0.16\"},{\"kind\":\"dev\",\"name\":\"rustc-hash\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"}],\"features\":{\"serde_impl\":[\"serde\"]}}", "headers-core_0.3.0": "{\"dependencies\":[{\"name\":\"http\",\"req\":\"^1.0.0\"}],\"features\":{}}", "headers_0.4.1": "{\"dependencies\":[{\"name\":\"base64\",\"req\":\"^0.22\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"headers-core\",\"req\":\"^0.3\"},{\"name\":\"http\",\"req\":\"^1.0.0\"},{\"name\":\"httpdate\",\"req\":\"^1\"},{\"name\":\"mime\",\"req\":\"^0.3.14\"},{\"name\":\"sha1\",\"req\":\"^0.10\"}],\"features\":{\"nightly\":[]}}", "heapless_0.8.0": "{\"dependencies\":[{\"name\":\"defmt\",\"optional\":true,\"req\":\">=0.2.0, <0.4\"},{\"name\":\"hash32\",\"req\":\"^0.3.0\"},{\"name\":\"portable-atomic\",\"optional\":true,\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"name\":\"stable_deref_trait\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"ufmt\",\"req\":\"^0.2\"},{\"name\":\"ufmt-write\",\"optional\":true,\"req\":\"^0.1\"}],\"features\":{\"defmt-03\":[\"dep:defmt\"],\"mpmc_large\":[],\"portable-atomic\":[\"dep:portable-atomic\"],\"portable-atomic-critical-section\":[\"dep:portable-atomic\",\"portable-atomic\",\"portable-atomic?/critical-section\"],\"portable-atomic-unsafe-assume-single-core\":[\"dep:portable-atomic\",\"portable-atomic\",\"portable-atomic?/unsafe-assume-single-core\"],\"serde\":[\"dep:serde\"],\"ufmt\":[\"dep:ufmt-write\"]}}", @@ -1059,7 +1060,9 @@ "hickory-proto_0.25.2": "{\"dependencies\":[{\"name\":\"async-trait\",\"req\":\"^0.1.43\"},{\"default_features\":false,\"features\":[\"prebuilt-nasm\"],\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.12.3\"},{\"name\":\"backtrace\",\"optional\":true,\"req\":\"^0.3.50\"},{\"name\":\"bitflags\",\"optional\":true,\"req\":\"^2.4.1\"},{\"name\":\"bytes\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"critical-section\",\"optional\":true,\"req\":\"^1.1.1\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"data-encoding\",\"req\":\"^2.2.0\"},{\"name\":\"enum-as-inner\",\"req\":\"^0.6\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"futures-channel\",\"req\":\"^0.3.5\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"futures-executor\",\"req\":\"^0.3.5\"},{\"default_features\":false,\"name\":\"futures-io\",\"optional\":true,\"req\":\"^0.3.5\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"futures-util\",\"req\":\"^0.3.5\"},{\"features\":[\"stream\"],\"name\":\"h2\",\"optional\":true,\"req\":\"^0.4.0\"},{\"name\":\"h3\",\"optional\":true,\"req\":\"^0.0.7\"},{\"name\":\"h3-quinn\",\"optional\":true,\"req\":\"^0.0.9\"},{\"name\":\"http\",\"optional\":true,\"req\":\"^1.1\"},{\"default_features\":false,\"features\":[\"alloc\",\"compiled_data\"],\"name\":\"idna\",\"req\":\"^1.0.3\"},{\"default_features\":false,\"name\":\"ipnet\",\"req\":\"^2.3.0\"},{\"name\":\"js-sys\",\"optional\":true,\"req\":\"^0.3.44\"},{\"default_features\":false,\"features\":[\"critical-section\"],\"name\":\"once_cell\",\"req\":\"^1.20.0\"},{\"name\":\"pin-project-lite\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"log\",\"runtime-tokio\"],\"name\":\"quinn\",\"optional\":true,\"req\":\"^0.11.2\"},{\"default_features\":false,\"features\":[\"alloc\",\"std_rng\"],\"name\":\"rand\",\"req\":\"^0.9\"},{\"features\":[\"std\"],\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17\"},{\"default_features\":false,\"features\":[\"logging\",\"std\",\"tls12\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.23\"},{\"name\":\"rustls-pki-types\",\"optional\":true,\"req\":\"^1.10\"},{\"name\":\"rustls-platform-verifier\",\"optional\":true,\"req\":\"^0.5\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"socket2\",\"optional\":true,\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"thiserror\",\"req\":\"^2\"},{\"name\":\"time\",\"optional\":true,\"req\":\"^0.3\"},{\"features\":[\"alloc\"],\"name\":\"tinyvec\",\"req\":\"^1.1.1\"},{\"features\":[\"io-util\",\"macros\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.21\"},{\"features\":[\"rt\",\"time\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.21\"},{\"default_features\":false,\"features\":[\"early-data\"],\"name\":\"tokio-rustls\",\"optional\":true,\"req\":\"^0.26\"},{\"default_features\":false,\"name\":\"tracing\",\"req\":\"^0.1.30\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"url\",\"req\":\"^2.5.4\"},{\"name\":\"wasm-bindgen-crate\",\"optional\":true,\"package\":\"wasm-bindgen\",\"req\":\"^0.2.58\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^0.26\"}],\"features\":{\"__dnssec\":[\"dep:bitflags\",\"dep:rustls-pki-types\",\"dep:time\",\"std\"],\"__h3\":[\"dep:h3\",\"dep:h3-quinn\",\"dep:http\",\"std\"],\"__https\":[\"dep:bytes\",\"dep:h2\",\"dep:http\",\"std\"],\"__quic\":[\"dep:bytes\",\"dep:pin-project-lite\",\"dep:quinn\",\"std\"],\"__tls\":[\"dep:bytes\",\"dep:rustls\",\"dep:tokio-rustls\",\"std\",\"tokio\"],\"backtrace\":[\"dep:backtrace\",\"std\"],\"default\":[\"std\",\"tokio\"],\"dnssec-aws-lc-rs\":[\"dep:aws-lc-rs\",\"aws-lc-rs/aws-lc-sys\",\"aws-lc-rs/ring-io\",\"__dnssec\"],\"dnssec-ring\":[\"dep:ring\",\"__dnssec\"],\"h3-aws-lc-rs\":[\"quic-aws-lc-rs\",\"__h3\"],\"h3-ring\":[\"quic-ring\",\"__h3\"],\"https-aws-lc-rs\":[\"tls-aws-lc-rs\",\"__https\"],\"https-ring\":[\"tls-ring\",\"__https\"],\"mdns\":[\"socket2/all\",\"std\"],\"no-std-rand\":[\"once_cell/critical-section\",\"dep:critical-section\"],\"quic-aws-lc-rs\":[\"quinn/rustls-aws-lc-rs\",\"tls-aws-lc-rs\",\"__quic\"],\"quic-ring\":[\"quinn/rustls-ring\",\"tls-ring\",\"__quic\"],\"rustls-platform-verifier\":[\"dep:rustls-platform-verifier\",\"std\"],\"serde\":[\"dep:serde\",\"std\",\"url/serde\"],\"std\":[\"data-encoding/std\",\"futures-channel/std\",\"futures-io/std\",\"futures-util/std\",\"ipnet/std\",\"rand/std\",\"rand/thread_rng\",\"ring?/std\",\"thiserror/std\",\"tracing-subscriber/env-filter\",\"tracing-subscriber/fmt\",\"tracing-subscriber/std\",\"tracing/std\",\"url/std\"],\"testing\":[\"std\"],\"text-parsing\":[\"std\"],\"tls-aws-lc-rs\":[\"tokio-rustls/aws-lc-rs\",\"__tls\"],\"tls-ring\":[\"tokio-rustls/ring\",\"__tls\"],\"tokio\":[\"dep:tokio\",\"std\",\"tokio/net\",\"tokio/rt\",\"tokio/time\",\"tokio/rt-multi-thread\"],\"wasm-bindgen\":[\"dep:wasm-bindgen-crate\",\"dep:js-sys\"]}}", "hickory-resolver_0.25.2": "{\"dependencies\":[{\"name\":\"backtrace\",\"optional\":true,\"req\":\"^0.3.50\"},{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"futures-executor\",\"req\":\"^0.3.5\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-util\",\"req\":\"^0.3.5\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"hickory-proto\",\"req\":\"^0.25\"},{\"name\":\"ipconfig\",\"optional\":true,\"req\":\"^0.3.0\",\"target\":\"cfg(windows)\"},{\"features\":[\"sync\"],\"name\":\"moka\",\"req\":\"^0.12\"},{\"default_features\":false,\"features\":[\"critical-section\"],\"name\":\"once_cell\",\"req\":\"^1.20.0\"},{\"name\":\"parking_lot\",\"req\":\"^0.12\"},{\"default_features\":false,\"features\":[\"log\",\"runtime-tokio\"],\"name\":\"quinn\",\"optional\":true,\"req\":\"^0.11.2\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"rand\",\"req\":\"^0.9\"},{\"features\":[\"system\"],\"name\":\"resolv-conf\",\"optional\":true,\"req\":\"^0.7.0\"},{\"default_features\":false,\"features\":[\"logging\",\"std\",\"tls12\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.23\"},{\"features\":[\"derive\",\"rc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"name\":\"smallvec\",\"req\":\"^1.6\"},{\"default_features\":false,\"name\":\"thiserror\",\"req\":\"^2\"},{\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.21\"},{\"features\":[\"macros\",\"test-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.21\"},{\"default_features\":false,\"name\":\"tokio-rustls\",\"optional\":true,\"req\":\"^0.26\"},{\"kind\":\"dev\",\"name\":\"toml\",\"req\":\"^0.8.14\"},{\"default_features\":false,\"name\":\"tracing\",\"req\":\"^0.1.30\"},{\"default_features\":false,\"features\":[\"env-filter\",\"fmt\",\"std\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^0.26\"}],\"features\":{\"__dnssec\":[],\"__h3\":[\"__quic\"],\"__https\":[\"__tls\"],\"__quic\":[\"dep:quinn\",\"__tls\"],\"__tls\":[\"dep:rustls\",\"dep:tokio-rustls\",\"tokio\"],\"backtrace\":[\"dep:backtrace\",\"hickory-proto/backtrace\"],\"default\":[\"system-config\",\"tokio\"],\"dnssec-aws-lc-rs\":[\"hickory-proto/dnssec-aws-lc-rs\",\"__dnssec\"],\"dnssec-ring\":[\"hickory-proto/dnssec-ring\",\"__dnssec\"],\"h3-aws-lc-rs\":[\"hickory-proto/h3-aws-lc-rs\",\"__h3\"],\"h3-ring\":[\"hickory-proto/h3-ring\",\"__h3\"],\"https-aws-lc-rs\":[\"hickory-proto/https-aws-lc-rs\",\"__https\"],\"https-ring\":[\"hickory-proto/https-ring\",\"__https\"],\"quic-aws-lc-rs\":[\"hickory-proto/quic-aws-lc-rs\",\"__quic\",\"quinn/rustls-aws-lc-rs\"],\"quic-ring\":[\"hickory-proto/quic-ring\",\"__quic\",\"quinn/rustls-ring\"],\"rustls-platform-verifier\":[\"hickory-proto/rustls-platform-verifier\"],\"serde\":[\"dep:serde\",\"hickory-proto/serde\"],\"system-config\":[\"dep:ipconfig\",\"dep:resolv-conf\"],\"tls-aws-lc-rs\":[\"hickory-proto/tls-aws-lc-rs\",\"__tls\"],\"tls-ring\":[\"hickory-proto/tls-ring\",\"__tls\"],\"tokio\":[\"dep:tokio\",\"tokio/rt\",\"hickory-proto/tokio\"],\"webpki-roots\":[\"dep:webpki-roots\",\"hickory-proto/webpki-roots\"]}}", "hkdf_0.12.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"blobby\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.2.2\"},{\"name\":\"hmac\",\"req\":\"^0.12.1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"sha1\",\"req\":\"^0.10\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.10\"}],\"features\":{\"std\":[\"hmac/std\"]}}", + "hkdf_0.13.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"blobby\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^1\"},{\"name\":\"hmac\",\"req\":\"^0.13\"},{\"name\":\"kdf\",\"optional\":true,\"req\":\"^0.1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"sha1\",\"req\":\"^0.11\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.11\"}],\"features\":{}}", "hmac_0.12.1": "{\"dependencies\":[{\"features\":[\"mac\"],\"name\":\"digest\",\"req\":\"^0.10.3\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.2.2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"md-5\",\"req\":\"^0.10\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"sha-1\",\"req\":\"^0.10\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.10\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"streebog\",\"req\":\"^0.10\"}],\"features\":{\"reset\":[],\"std\":[\"digest/std\"]}}", + "hmac_0.13.0": "{\"dependencies\":[{\"features\":[\"mac\"],\"name\":\"digest\",\"req\":\"^0.11.2\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.11.2\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"md-5\",\"req\":\"^0.11\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"sha1\",\"req\":\"^0.11\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.11\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"streebog\",\"req\":\"^0.11\"}],\"features\":{\"zeroize\":[\"digest/zeroize\"]}}", "home_0.5.12": "{\"dependencies\":[{\"features\":[\"Win32_Foundation\",\"Win32_UI_Shell\",\"Win32_System_Com\"],\"name\":\"windows-sys\",\"req\":\"^0.61\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "home_0.5.9": "{\"dependencies\":[{\"features\":[\"Win32_Foundation\",\"Win32_UI_Shell\",\"Win32_System_Com\"],\"name\":\"windows-sys\",\"req\":\"^0.52\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "hostname_0.4.2": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(any(unix, target_os = \\\"redox\\\"))\"},{\"kind\":\"dev\",\"name\":\"similar-asserts\",\"req\":\"^1.6.1\"},{\"kind\":\"dev\",\"name\":\"version-sync\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"windows-bindgen\",\"req\":\"^0.65\"},{\"name\":\"windows-link\",\"req\":\"^0.2\",\"target\":\"cfg(target_os = \\\"windows\\\")\"}],\"features\":{\"default\":[],\"set\":[]}}", @@ -1071,6 +1074,7 @@ "http_1.4.0": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"name\":\"itoa\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.0\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "httparse_1.10.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.5\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "httpdate_1.0.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"}],\"features\":{}}", + "hybrid-array_0.4.12": "{\"dependencies\":[{\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"name\":\"bytemuck\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"ctutils\",\"optional\":true,\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"const-generics\"],\"name\":\"subtle\",\"optional\":true,\"req\":\"^2\"},{\"features\":[\"const-generics\"],\"name\":\"typenum\",\"req\":\"^1.20\"},{\"features\":[\"derive\"],\"name\":\"zerocopy\",\"optional\":true,\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.8\"}],\"features\":{\"alloc\":[],\"extra-sizes\":[]}}", "hyper-rustls_0.27.7": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"http\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"hyper\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"client-legacy\",\"tokio\"],\"name\":\"hyper-util\",\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"server-auto\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.4\"},{\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"rustls\",\"req\":\"^0.23\"},{\"default_features\":false,\"features\":[\"tls12\"],\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"name\":\"rustls-native-certs\",\"optional\":true,\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rustls-pemfile\",\"req\":\"^2\"},{\"name\":\"rustls-platform-verifier\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"tokio\",\"req\":\"^1.0\"},{\"features\":[\"io-std\",\"macros\",\"net\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"tokio-rustls\",\"req\":\"^0.26\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"aws-lc-rs\":[\"rustls/aws_lc_rs\"],\"default\":[\"native-tokio\",\"http1\",\"tls12\",\"logging\",\"aws-lc-rs\"],\"fips\":[\"aws-lc-rs\",\"rustls/fips\"],\"http1\":[\"hyper-util/http1\"],\"http2\":[\"hyper-util/http2\"],\"logging\":[\"log\",\"tokio-rustls/logging\",\"rustls/logging\"],\"native-tokio\":[\"rustls-native-certs\"],\"ring\":[\"rustls/ring\"],\"tls12\":[\"tokio-rustls/tls12\",\"rustls/tls12\"],\"webpki-tokio\":[\"webpki-roots\"]}}", "hyper-timeout_0.5.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"name\":\"hyper\",\"req\":\"^1.1\"},{\"features\":[\"http1\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.1\"},{\"kind\":\"dev\",\"name\":\"hyper-tls\",\"req\":\"^0.6\"},{\"features\":[\"client-legacy\",\"http1\"],\"name\":\"hyper-util\",\"req\":\"^0.1.10\"},{\"features\":[\"client-legacy\",\"http1\",\"server\",\"server-graceful\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1.10\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"name\":\"tokio\",\"req\":\"^1.35\"},{\"features\":[\"io-std\",\"io-util\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.35\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"}],\"features\":{}}", "hyper-tls_0.6.0": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"name\":\"hyper\",\"req\":\"^1\"},{\"features\":[\"client-legacy\",\"tokio\"],\"name\":\"hyper-util\",\"req\":\"^0.1.0\"},{\"features\":[\"http1\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1.0\"},{\"name\":\"native-tls\",\"req\":\"^0.2.1\"},{\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"io-std\",\"macros\",\"io-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0.0\"},{\"name\":\"tokio-native-tls\",\"req\":\"^0.3\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"}],\"features\":{\"alpn\":[\"native-tls/alpn\"],\"vendored\":[\"native-tls/vendored\"]}}", @@ -1173,7 +1177,7 @@ "libm_0.2.16": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"no-panic\",\"req\":\"^0.1.35\"}],\"features\":{\"arch\":[],\"default\":[\"arch\"],\"force-soft-floats\":[],\"unstable\":[\"unstable-intrinsics\",\"unstable-float\"],\"unstable-float\":[],\"unstable-intrinsics\":[],\"unstable-public-internals\":[]}}", "libredox_0.1.12": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2\"},{\"name\":\"ioslice\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"name\":\"redox_syscall\",\"optional\":true,\"req\":\"^0.7\"}],\"features\":{\"call\":[],\"default\":[\"call\",\"std\",\"redox_syscall\"],\"mkns\":[\"ioslice\"],\"std\":[]}}", "libredox_0.1.14": "{\"dependencies\":[{\"name\":\"bitflags\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"ioslice\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"plain\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"redox_syscall\",\"optional\":true,\"req\":\"^0.7\"}],\"features\":{\"base\":[\"libc\"],\"call\":[\"base\"],\"default\":[\"base\",\"call\",\"std\",\"redox_syscall\",\"protocol\"],\"mkns\":[\"ioslice\"],\"protocol\":[\"plain\",\"bitflags\",\"redox_syscall\"],\"std\":[\"base\"]}}", - "libsqlite3-sys_0.30.1": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"runtime\"],\"kind\":\"build\",\"name\":\"bindgen\",\"optional\":true,\"req\":\"^0.69\"},{\"kind\":\"build\",\"name\":\"cc\",\"optional\":true,\"req\":\"^1.1.6\"},{\"name\":\"openssl-sys\",\"optional\":true,\"req\":\"^0.9.103\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"optional\":true,\"req\":\"^0.3.19\"},{\"kind\":\"build\",\"name\":\"prettyplease\",\"optional\":true,\"req\":\"^0.2.20\"},{\"default_features\":false,\"kind\":\"build\",\"name\":\"quote\",\"optional\":true,\"req\":\"^1.0.36\"},{\"features\":[\"full\",\"extra-traits\",\"visit-mut\"],\"kind\":\"build\",\"name\":\"syn\",\"optional\":true,\"req\":\"^2.0.72\"},{\"kind\":\"build\",\"name\":\"vcpkg\",\"optional\":true,\"req\":\"^0.2.15\"}],\"features\":{\"buildtime_bindgen\":[\"bindgen\",\"pkg-config\",\"vcpkg\"],\"bundled\":[\"cc\",\"bundled_bindings\"],\"bundled-sqlcipher\":[\"bundled\"],\"bundled-sqlcipher-vendored-openssl\":[\"bundled-sqlcipher\",\"openssl-sys/vendored\"],\"bundled-windows\":[\"cc\",\"bundled_bindings\"],\"bundled_bindings\":[],\"default\":[\"min_sqlite_version_3_14_0\"],\"in_gecko\":[],\"loadable_extension\":[\"prettyplease\",\"quote\",\"syn\"],\"min_sqlite_version_3_14_0\":[\"pkg-config\",\"vcpkg\"],\"preupdate_hook\":[\"buildtime_bindgen\"],\"session\":[\"preupdate_hook\",\"buildtime_bindgen\"],\"sqlcipher\":[],\"unlock_notify\":[],\"wasm32-wasi-vfs\":[],\"with-asan\":[]}}", + "libsqlite3-sys_0.37.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"runtime\"],\"kind\":\"build\",\"name\":\"bindgen\",\"optional\":true,\"req\":\"^0.72\"},{\"kind\":\"build\",\"name\":\"cc\",\"optional\":true,\"req\":\"^1.2.27\"},{\"name\":\"openssl-sys\",\"optional\":true,\"req\":\"^0.9.103\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"optional\":true,\"req\":\"^0.3.19\"},{\"kind\":\"build\",\"name\":\"prettyplease\",\"optional\":true,\"req\":\"^0.2.20\"},{\"default_features\":false,\"kind\":\"build\",\"name\":\"quote\",\"optional\":true,\"req\":\"^1.0.36\"},{\"features\":[\"full\",\"extra-traits\",\"visit-mut\"],\"kind\":\"build\",\"name\":\"syn\",\"optional\":true,\"req\":\"^2.0.89\"},{\"kind\":\"build\",\"name\":\"vcpkg\",\"optional\":true,\"req\":\"^0.2.15\"}],\"features\":{\"buildtime_bindgen\":[\"bindgen\",\"pkg-config\",\"vcpkg\"],\"bundled\":[\"cc\",\"bundled_bindings\"],\"bundled-sqlcipher\":[\"bundled\"],\"bundled-sqlcipher-vendored-openssl\":[\"bundled-sqlcipher\",\"openssl-sys/vendored\"],\"bundled-windows\":[\"cc\",\"bundled_bindings\"],\"bundled_bindings\":[],\"column_metadata\":[],\"default\":[\"min_sqlite_version_3_34_1\"],\"in_gecko\":[],\"loadable_extension\":[\"prettyplease\",\"quote\",\"syn\"],\"min_sqlite_version_3_34_1\":[\"pkg-config\",\"vcpkg\"],\"preupdate_hook\":[\"buildtime_bindgen\"],\"session\":[\"preupdate_hook\",\"buildtime_bindgen\"],\"sqlcipher\":[],\"unlock_notify\":[],\"wasm32-wasi-vfs\":[],\"with-asan\":[]}}", "libssh2-sys_0.3.1": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0.25\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"libc\"],\"name\":\"libz-sys\",\"req\":\"^1.1.0\"},{\"name\":\"openssl-sys\",\"req\":\"^0.9.35\",\"target\":\"cfg(unix)\"},{\"name\":\"openssl-sys\",\"optional\":true,\"req\":\"^0.9.35\",\"target\":\"cfg(windows)\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.11\"},{\"kind\":\"build\",\"name\":\"vcpkg\",\"req\":\"^0.2\",\"target\":\"cfg(target_env = \\\"msvc\\\")\"}],\"features\":{\"openssl-on-win32\":[\"openssl-sys\"],\"vendored-openssl\":[\"openssl-sys/vendored\"],\"zlib-ng-compat\":[\"libz-sys/zlib-ng\"]}}", "libz-sys_1.1.23": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0.98\"},{\"kind\":\"build\",\"name\":\"cmake\",\"optional\":true,\"req\":\"^0.1.50\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.43\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.9\"},{\"kind\":\"build\",\"name\":\"vcpkg\",\"req\":\"^0.2.11\"}],\"features\":{\"asm\":[],\"default\":[\"libc\",\"stock-zlib\"],\"static\":[],\"stock-zlib\":[],\"zlib-ng\":[\"libc\",\"cmake\"],\"zlib-ng-no-cmake-experimental-community-maintained\":[\"libc\"]}}", "libz-sys_1.1.25": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0.98\"},{\"kind\":\"build\",\"name\":\"cmake\",\"optional\":true,\"req\":\"^0.1.50\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.43\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.9\"},{\"kind\":\"build\",\"name\":\"vcpkg\",\"req\":\"^0.2.11\"}],\"features\":{\"asm\":[],\"default\":[\"libc\",\"stock-zlib\"],\"static\":[],\"stock-zlib\":[],\"zlib-ng\":[\"libc\",\"cmake\"],\"zlib-ng-no-cmake-experimental-community-maintained\":[\"libc\"]}}", @@ -1203,7 +1207,7 @@ "matchit_0.8.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"actix-router\",\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.4\"},{\"kind\":\"dev\",\"name\":\"gonzales\",\"req\":\"^0.0.3-beta\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^0.14\"},{\"kind\":\"dev\",\"name\":\"path-tree\",\"req\":\"^0.2.2\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.5.4\"},{\"kind\":\"dev\",\"name\":\"route-recognizer\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"routefinder\",\"req\":\"^0.5.2\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"make\",\"util\"],\"kind\":\"dev\",\"name\":\"tower\",\"req\":\"^0.4\"}],\"features\":{\"__test_helpers\":[],\"default\":[]}}", "matchit_0.9.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"actix-router\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"gonzales\",\"req\":\"^0.0.3-beta\"},{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"features\":[\"http1\",\"server\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1\"},{\"features\":[\"tokio\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"path-tree\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"route-recognizer\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"routefinder\",\"req\":\"^0.5\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"make\",\"util\"],\"kind\":\"dev\",\"name\":\"tower\",\"req\":\"^0.5.2\"},{\"kind\":\"dev\",\"name\":\"wayfind\",\"req\":\"^0.8\"}],\"features\":{\"__test_helpers\":[],\"default\":[]}}", "maybe-async_0.2.10": "{\"dependencies\":[{\"features\":[\"attributes\"],\"kind\":\"dev\",\"name\":\"async-std\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"async-trait\",\"req\":\"^0.1\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"features\":[\"visit-mut\",\"full\"],\"name\":\"syn\",\"req\":\"^2.0\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1\"}],\"features\":{\"default\":[],\"is_sync\":[]}}", - "md-5_0.10.6": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.2.2\"},{\"name\":\"md5-asm\",\"optional\":true,\"req\":\"^0.5\",\"target\":\"cfg(any(target_arch = \\\"x86\\\", target_arch = \\\"x86_64\\\"))\"}],\"features\":{\"asm\":[\"md5-asm\"],\"default\":[\"std\"],\"force-soft\":[],\"loongarch64_asm\":[],\"oid\":[\"digest/oid\"],\"std\":[\"digest/std\"]}}", + "md-5_0.11.0": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"digest\",\"req\":\"^0.11\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.11\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^1\"}],\"features\":{\"alloc\":[\"digest/alloc\"],\"default\":[\"alloc\",\"oid\"],\"oid\":[\"digest/oid\"],\"zeroize\":[\"digest/zeroize\"]}}", "md5_0.8.0": "{\"dependencies\":[],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "memchr_2.7.6": "{\"dependencies\":[{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.20\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"libc\":[],\"logging\":[\"dep:log\"],\"rustc-dep-of-std\":[\"core\"],\"std\":[\"alloc\"],\"use_std\":[\"std\"]}}", "memchr_2.8.0": "{\"dependencies\":[{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.20\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"libc\":[],\"logging\":[\"dep:log\"],\"rustc-dep-of-std\":[\"core\"],\"std\":[\"alloc\"],\"use_std\":[\"std\"]}}", @@ -1235,7 +1239,6 @@ "notify-types_2.1.0": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.34.0\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.26.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.89\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.39\"},{\"name\":\"web-time\",\"optional\":true,\"req\":\"^1.1.0\"}],\"features\":{\"serde\":[\"dep:serde\",\"bitflags/serde\"],\"serialization-compat-6\":[]}}", "notify_8.2.0": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2.7.0\",\"target\":\"cfg(target_os=\\\"macos\\\")\"},{\"name\":\"crossbeam-channel\",\"optional\":true,\"req\":\"^0.5.0\"},{\"name\":\"flume\",\"optional\":true,\"req\":\"^0.11.1\"},{\"name\":\"fsevent-sys\",\"optional\":true,\"req\":\"^4.0.0\",\"target\":\"cfg(target_os=\\\"macos\\\")\"},{\"default_features\":false,\"name\":\"inotify\",\"req\":\"^0.11.0\",\"target\":\"cfg(any(target_os=\\\"linux\\\", target_os=\\\"android\\\"))\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.34.0\"},{\"name\":\"kqueue\",\"req\":\"^1.1.1\",\"target\":\"cfg(any(target_os=\\\"freebsd\\\", target_os=\\\"openbsd\\\", target_os = \\\"netbsd\\\", target_os = \\\"dragonflybsd\\\", target_os = \\\"ios\\\"))\"},{\"name\":\"kqueue\",\"optional\":true,\"req\":\"^1.1.1\",\"target\":\"cfg(target_os=\\\"macos\\\")\"},{\"name\":\"libc\",\"req\":\"^0.2.4\"},{\"name\":\"log\",\"req\":\"^0.4.17\"},{\"features\":[\"os-ext\"],\"name\":\"mio\",\"req\":\"^1.0\",\"target\":\"cfg(any(target_os=\\\"freebsd\\\", target_os=\\\"openbsd\\\", target_os = \\\"netbsd\\\", target_os = \\\"dragonflybsd\\\", target_os = \\\"ios\\\"))\"},{\"features\":[\"os-ext\"],\"name\":\"mio\",\"req\":\"^1.0\",\"target\":\"cfg(any(target_os=\\\"linux\\\", target_os=\\\"android\\\"))\"},{\"features\":[\"os-ext\"],\"name\":\"mio\",\"optional\":true,\"req\":\"^1.0\",\"target\":\"cfg(target_os=\\\"macos\\\")\"},{\"kind\":\"dev\",\"name\":\"nix\",\"req\":\"^0.29.0\"},{\"name\":\"notify-types\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.39\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.10.0\"},{\"kind\":\"dev\",\"name\":\"trash\",\"req\":\"^5.2.2\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"name\":\"walkdir\",\"req\":\"^2.4.0\"},{\"features\":[\"Win32_System_Threading\",\"Win32_Foundation\",\"Win32_Storage_FileSystem\",\"Win32_Security\",\"Win32_System_WindowsProgramming\",\"Win32_System_IO\"],\"name\":\"windows-sys\",\"req\":\"^0.60.1\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"macos_fsevent\"],\"macos_fsevent\":[\"fsevent-sys\"],\"macos_kqueue\":[\"kqueue\",\"mio\"],\"serde\":[\"notify-types/serde\"],\"serialization-compat-6\":[\"notify-types/serialization-compat-6\"]}}", "nu-ansi-term_0.50.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3.3\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.152\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.94\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Console\",\"Win32_Storage_FileSystem\",\"Win32_Security\"],\"name\":\"windows\",\"package\":\"windows-sys\",\"req\":\">=0.59, <=0.61\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"std\"],\"derive_serde_style\":[\"serde\"],\"gnu_legacy\":[],\"std\":[]}}", - "num-bigint-dig_0.8.6": "{\"dependencies\":[{\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.1.0\"},{\"default_features\":false,\"features\":[\"spin_no_std\"],\"name\":\"lazy_static\",\"req\":\"^1.2.0\"},{\"name\":\"libm\",\"req\":\"^0.2.1\"},{\"default_features\":false,\"features\":[\"i128\"],\"name\":\"num-integer\",\"req\":\"^0.1.39\"},{\"default_features\":false,\"name\":\"num-iter\",\"req\":\"^0.1.37\"},{\"default_features\":false,\"features\":[\"i128\"],\"name\":\"num-traits\",\"req\":\"^0.2.4\"},{\"default_features\":false,\"name\":\"rand\",\"optional\":true,\"req\":\"^0.8.3\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rand_chacha\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"rand_isaac\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"rand_xorshift\",\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"smallvec\",\"req\":\"^1.10.0\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.5\"}],\"features\":{\"default\":[\"std\",\"u64_digit\"],\"fuzz\":[\"arbitrary\",\"smallvec/arbitrary\"],\"i128\":[],\"nightly\":[],\"prime\":[\"rand/std_rng\"],\"std\":[\"num-integer/std\",\"num-traits/std\",\"smallvec/write\",\"rand/std\",\"serde/std\"],\"u64_digit\":[]}}", "num-bigint_0.4.6": "{\"dependencies\":[{\"default_features\":false,\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"i128\"],\"name\":\"num-integer\",\"req\":\"^0.1.46\"},{\"default_features\":false,\"features\":[\"i128\"],\"name\":\"num-traits\",\"req\":\"^0.2.18\"},{\"default_features\":false,\"name\":\"quickcheck\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"name\":\"rand\",\"optional\":true,\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"arbitrary\":[\"dep:arbitrary\"],\"default\":[\"std\"],\"quickcheck\":[\"dep:quickcheck\"],\"rand\":[\"dep:rand\"],\"serde\":[\"dep:serde\"],\"std\":[\"num-integer/std\",\"num-traits/std\"]}}", "num-complex_0.4.6": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bytecheck\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"bytemuck\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"i128\"],\"name\":\"num-traits\",\"req\":\"^0.2.18\"},{\"default_features\":false,\"name\":\"rand\",\"optional\":true,\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"rkyv\",\"optional\":true,\"req\":\"^0.7\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"bytecheck\":[\"dep:bytecheck\"],\"bytemuck\":[\"dep:bytemuck\"],\"default\":[\"std\"],\"libm\":[\"num-traits/libm\"],\"rand\":[\"dep:rand\"],\"rkyv\":[\"dep:rkyv\"],\"serde\":[\"dep:serde\"],\"std\":[\"num-traits/std\"]}}", "num-conv_0.2.0": "{\"dependencies\":[],\"features\":{}}", @@ -1324,7 +1327,6 @@ "pin-project_1.1.11": "{\"dependencies\":[{\"name\":\"pin-project-internal\",\"req\":\"=1.1.11\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1\"}],\"features\":{}}", "pin-utils_0.1.0": "{\"dependencies\":[],\"features\":{}}", "piper_0.2.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-channel\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"async-executor\",\"req\":\"^1.5.1\"},{\"kind\":\"dev\",\"name\":\"async-io\",\"req\":\"^2.0.0\"},{\"name\":\"atomic-waker\",\"req\":\"^1.1.0\"},{\"default_features\":false,\"features\":[\"cargo_bench_support\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"kind\":\"dev\",\"name\":\"easy-parallel\",\"req\":\"^3.2.0\"},{\"default_features\":false,\"name\":\"fastrand\",\"req\":\"^2.0.0\"},{\"name\":\"futures-io\",\"optional\":true,\"req\":\"^0.3.28\"},{\"kind\":\"dev\",\"name\":\"futures-lite\",\"req\":\"^2.0.0\"},{\"features\":[\"alloc\"],\"name\":\"portable-atomic-util\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"portable_atomic_crate\",\"optional\":true,\"package\":\"portable-atomic\",\"req\":\"^1.2.0\"}],\"features\":{\"default\":[\"std\"],\"portable-atomic\":[\"atomic-waker/portable-atomic\",\"portable_atomic_crate\",\"portable-atomic-util\"],\"std\":[\"fastrand/std\",\"futures-io\"]}}", - "pkcs1_0.7.5": "{\"dependencies\":[{\"features\":[\"db\"],\"kind\":\"dev\",\"name\":\"const-oid\",\"req\":\"^0.9\"},{\"features\":[\"oid\"],\"name\":\"der\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"pkcs8\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"spki\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"alloc\":[\"der/alloc\",\"zeroize\",\"pkcs8?/alloc\"],\"pem\":[\"alloc\",\"der/pem\",\"pkcs8?/pem\"],\"std\":[\"der/std\",\"alloc\"],\"zeroize\":[\"der/zeroize\"]}}", "pkcs8_0.10.2": "{\"dependencies\":[{\"features\":[\"oid\"],\"name\":\"der\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"},{\"name\":\"pkcs5\",\"optional\":true,\"req\":\"^0.7\"},{\"default_features\":false,\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"spki\",\"req\":\"^0.7.1\"},{\"default_features\":false,\"name\":\"subtle\",\"optional\":true,\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"3des\":[\"encryption\",\"pkcs5/3des\"],\"alloc\":[\"der/alloc\",\"der/zeroize\",\"spki/alloc\"],\"des-insecure\":[\"encryption\",\"pkcs5/des-insecure\"],\"encryption\":[\"alloc\",\"pkcs5/alloc\",\"pkcs5/pbes2\",\"rand_core\"],\"getrandom\":[\"rand_core/getrandom\"],\"pem\":[\"alloc\",\"der/pem\",\"spki/pem\"],\"sha1-insecure\":[\"encryption\",\"pkcs5/sha1-insecure\"],\"std\":[\"alloc\",\"der/std\",\"spki/std\"]}}", "pkg-config_0.3.32": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"}],\"features\":{}}", "plain_0.2.3": "{\"dependencies\":[],\"features\":{}}", @@ -1400,10 +1402,12 @@ "rama-udp_0.3.0-alpha.4": "{\"dependencies\":[{\"name\":\"rama-core\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-net\",\"req\":\"^0.3.0-alpha.4\"},{\"features\":[\"macros\",\"net\"],\"name\":\"tokio\",\"req\":\"^1.48\"},{\"features\":[\"net\"],\"name\":\"tokio-util\",\"req\":\"^0.7\"}],\"features\":{\"default\":[]}}", "rama-unix_0.3.0-alpha.4": "{\"dependencies\":[{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"name\":\"rama-core\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"rama-net\",\"req\":\"^0.3.0-alpha.4\"},{\"features\":[\"macros\",\"net\"],\"name\":\"tokio\",\"req\":\"^1.48\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.48\"}],\"features\":{\"default\":[]}}", "rama-utils_0.3.0-alpha.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"ahash\",\"req\":\"^0.8\"},{\"name\":\"const_format\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"loom\",\"req\":\"^0.7\",\"target\":\"cfg(loom)\"},{\"name\":\"parking_lot\",\"req\":\"^0.12\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"name\":\"rama-macros\",\"req\":\"^0.3.0-alpha.4\"},{\"name\":\"regex\",\"req\":\"^1.12\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1\"},{\"features\":[\"write\",\"serde\",\"const_generics\",\"const_new\"],\"name\":\"smallvec\",\"req\":\"^1.15\"},{\"name\":\"smol_str\",\"req\":\"^0.3\"},{\"features\":[\"time\",\"macros\"],\"name\":\"tokio\",\"req\":\"^1.48\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0\"},{\"name\":\"wildcard\",\"req\":\"^0.3\"}],\"features\":{}}", + "rand_0.10.1": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"rng\"],\"name\":\"chacha20\",\"optional\":true,\"req\":\"^0.10.0\"},{\"name\":\"getrandom\",\"optional\":true,\"req\":\"^0.4.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"postcard\",\"req\":\"^1.1.3\"},{\"default_features\":false,\"name\":\"rand_core\",\"req\":\"^0.10.0\"},{\"kind\":\"dev\",\"name\":\"rand_pcg\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.7\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.103\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.140\"}],\"features\":{\"alloc\":[],\"chacha\":[\"dep:chacha20\"],\"default\":[\"std\",\"std_rng\",\"sys_rng\",\"thread_rng\"],\"log\":[],\"serde\":[\"dep:serde\"],\"simd_support\":[],\"std\":[\"alloc\",\"getrandom?/std\"],\"std_rng\":[\"dep:chacha20\"],\"sys_rng\":[\"dep:getrandom\",\"getrandom/sys_rng\"],\"thread_rng\":[\"std\",\"std_rng\",\"sys_rng\"],\"unbiased\":[]}}", "rand_0.8.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.2.1\"},{\"default_features\":false,\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.22\",\"target\":\"cfg(unix)\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.4\"},{\"features\":[\"into_bits\"],\"name\":\"packed_simd\",\"optional\":true,\"package\":\"packed_simd_2\",\"req\":\"^0.3.7\"},{\"default_features\":false,\"name\":\"rand_chacha\",\"optional\":true,\"req\":\"^0.3.0\"},{\"name\":\"rand_core\",\"req\":\"^0.6.0\"},{\"kind\":\"dev\",\"name\":\"rand_pcg\",\"req\":\"^0.3.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.103\"}],\"features\":{\"alloc\":[\"rand_core/alloc\"],\"default\":[\"std\",\"std_rng\"],\"getrandom\":[\"rand_core/getrandom\"],\"min_const_gen\":[],\"nightly\":[],\"serde1\":[\"serde\",\"rand_core/serde1\"],\"simd_support\":[\"packed_simd\"],\"small_rng\":[],\"std\":[\"rand_core/std\",\"rand_chacha/std\",\"alloc\",\"getrandom\",\"libc\"],\"std_rng\":[\"rand_chacha\"]}}", "rand_0.9.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.2.1\"},{\"default_features\":false,\"name\":\"rand_chacha\",\"optional\":true,\"req\":\"^0.9.0\"},{\"default_features\":false,\"name\":\"rand_core\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"rand_pcg\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.7\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.103\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.140\"}],\"features\":{\"alloc\":[],\"default\":[\"std\",\"std_rng\",\"os_rng\",\"small_rng\",\"thread_rng\"],\"log\":[],\"nightly\":[],\"os_rng\":[\"rand_core/os_rng\"],\"serde\":[\"dep:serde\",\"rand_core/serde\"],\"simd_support\":[],\"small_rng\":[],\"std\":[\"rand_core/std\",\"rand_chacha?/std\",\"alloc\"],\"std_rng\":[\"dep:rand_chacha\"],\"thread_rng\":[\"std\",\"std_rng\",\"os_rng\"],\"unbiased\":[]}}", "rand_chacha_0.3.1": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"simd\"],\"name\":\"ppv-lite86\",\"req\":\"^0.2.8\"},{\"name\":\"rand_core\",\"req\":\"^0.6.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"serde1\":[\"serde\"],\"simd\":[],\"std\":[\"ppv-lite86/std\"]}}", "rand_chacha_0.9.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"simd\"],\"name\":\"ppv-lite86\",\"req\":\"^0.2.14\"},{\"name\":\"rand_core\",\"req\":\"^0.9.0\"},{\"features\":[\"os_rng\"],\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.9.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"os_rng\":[\"rand_core/os_rng\"],\"serde\":[\"dep:serde\"],\"std\":[\"ppv-lite86/std\",\"rand_core/std\"]}}", + "rand_core_0.10.1": "{\"dependencies\":[],\"features\":{}}", "rand_core_0.6.4": "{\"dependencies\":[{\"name\":\"getrandom\",\"optional\":true,\"req\":\"^0.2\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[],\"serde1\":[\"serde\"],\"std\":[\"alloc\",\"getrandom\",\"getrandom/std\"]}}", "rand_core_0.9.5": "{\"dependencies\":[{\"name\":\"getrandom\",\"optional\":true,\"req\":\"^0.3.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"os_rng\":[\"dep:getrandom\"],\"serde\":[\"dep:serde\"],\"std\":[\"getrandom?/std\"]}}", "rand_xorshift_0.4.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1\"},{\"name\":\"rand_core\",\"req\":\"^0.9.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.118\"}],\"features\":{\"serde\":[\"dep:serde\"]}}", @@ -1432,7 +1436,6 @@ "ring_0.17.14": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.2.8\"},{\"default_features\":false,\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"name\":\"getrandom\",\"req\":\"^0.2.10\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.148\",\"target\":\"cfg(all(any(all(target_arch = \\\"aarch64\\\", target_endian = \\\"little\\\"), all(target_arch = \\\"arm\\\", target_endian = \\\"little\\\")), any(target_os = \\\"android\\\", target_os = \\\"linux\\\")))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.155\",\"target\":\"cfg(all(all(target_arch = \\\"aarch64\\\", target_endian = \\\"little\\\"), target_vendor = \\\"apple\\\", any(target_os = \\\"ios\\\", target_os = \\\"macos\\\", target_os = \\\"tvos\\\", target_os = \\\"visionos\\\", target_os = \\\"watchos\\\")))\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.148\",\"target\":\"cfg(any(unix, windows, target_os = \\\"wasi\\\"))\"},{\"name\":\"untrusted\",\"req\":\"^0.9\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.37\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"unknown\\\"))\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Threading\"],\"name\":\"windows-sys\",\"req\":\"^0.52\",\"target\":\"cfg(all(all(target_arch = \\\"aarch64\\\", target_endian = \\\"little\\\"), target_os = \\\"windows\\\"))\"}],\"features\":{\"alloc\":[],\"default\":[\"alloc\",\"dev_urandom_fallback\"],\"dev_urandom_fallback\":[],\"less-safe-getrandom-custom-or-rdrand\":[],\"less-safe-getrandom-espidf\":[],\"slow_tests\":[],\"std\":[\"alloc\"],\"test_logging\":[],\"unstable-testing-arm-no-hw\":[],\"unstable-testing-arm-no-neon\":[],\"wasm32_unknown_unknown_js\":[\"getrandom/js\"]}}", "rmcp-macros_0.15.0": "{\"dependencies\":[{\"name\":\"darling\",\"req\":\"^0.23\"},{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{}}", "rmcp_0.15.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"async-trait\",\"req\":\"^0.1.89\"},{\"kind\":\"dev\",\"name\":\"async-trait\",\"req\":\"^0.1\"},{\"name\":\"axum\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22\"},{\"name\":\"bytes\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"serde\",\"clock\",\"std\",\"oldtime\"],\"name\":\"chrono\",\"req\":\"^0.4.38\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"features\":[\"serde\"],\"name\":\"chrono\",\"req\":\"^0.4.38\",\"target\":\"cfg(not(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\")))\"},{\"name\":\"futures\",\"req\":\"^0.3\"},{\"name\":\"http\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"http-body\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"http-body-util\",\"optional\":true,\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"reqwest\"],\"name\":\"oauth2\",\"optional\":true,\"req\":\"^5.0\"},{\"name\":\"pastey\",\"optional\":true,\"req\":\"^0.2.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"features\":[\"tokio1\"],\"name\":\"process-wrap\",\"optional\":true,\"req\":\"^9.0\"},{\"name\":\"rand\",\"optional\":true,\"req\":\"^0.9\"},{\"default_features\":false,\"features\":[\"json\",\"stream\"],\"name\":\"reqwest\",\"optional\":true,\"req\":\"^0.12\"},{\"name\":\"rmcp-macros\",\"optional\":true,\"req\":\"^0.15.0\"},{\"features\":[\"chrono04\"],\"name\":\"schemars\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"chrono04\"],\"kind\":\"dev\",\"name\":\"schemars\",\"req\":\"^1.1.0\"},{\"features\":[\"derive\",\"rc\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"sse-stream\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"thiserror\",\"req\":\"^2\"},{\"features\":[\"sync\",\"macros\",\"rt\",\"time\"],\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"name\":\"tokio-stream\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"tokio-util\",\"req\":\"^0.7\"},{\"name\":\"tower-service\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"env-filter\",\"std\",\"fmt\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"name\":\"url\",\"optional\":true,\"req\":\"^2.4\"},{\"features\":[\"v4\"],\"name\":\"uuid\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"__reqwest\":[\"dep:reqwest\"],\"auth\":[\"dep:oauth2\",\"__reqwest\",\"dep:url\"],\"client\":[\"dep:tokio-stream\"],\"client-side-sse\":[\"dep:sse-stream\",\"dep:http\"],\"default\":[\"base64\",\"macros\",\"server\"],\"elicitation\":[\"dep:url\"],\"macros\":[\"dep:rmcp-macros\",\"dep:pastey\"],\"reqwest\":[\"__reqwest\",\"reqwest?/rustls-tls\"],\"reqwest-native-tls\":[\"__reqwest\",\"reqwest?/native-tls\"],\"reqwest-tls-no-provider\":[\"__reqwest\",\"reqwest?/rustls-tls-no-provider\"],\"schemars\":[\"dep:schemars\"],\"server\":[\"transport-async-rw\",\"dep:schemars\",\"dep:pastey\"],\"server-side-http\":[\"uuid\",\"dep:rand\",\"dep:tokio-stream\",\"dep:http\",\"dep:http-body\",\"dep:http-body-util\",\"dep:bytes\",\"dep:sse-stream\",\"dep:axum\",\"tower\"],\"tower\":[\"dep:tower-service\"],\"transport-async-rw\":[\"tokio/io-util\",\"tokio-util/codec\"],\"transport-child-process\":[\"transport-async-rw\",\"tokio/process\",\"dep:process-wrap\"],\"transport-io\":[\"transport-async-rw\",\"tokio/io-std\"],\"transport-streamable-http-client\":[\"client-side-sse\",\"transport-worker\"],\"transport-streamable-http-client-reqwest\":[\"transport-streamable-http-client\",\"__reqwest\"],\"transport-streamable-http-server\":[\"transport-streamable-http-server-session\",\"server-side-http\",\"transport-worker\"],\"transport-streamable-http-server-session\":[\"transport-async-rw\",\"dep:tokio-stream\"],\"transport-worker\":[\"dep:tokio-stream\"]}}", - "rsa_0.9.10": "{\"dependencies\":[{\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"base64ct\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"const-oid\",\"req\":\"^0.9\"},{\"default_features\":false,\"features\":[\"alloc\",\"oid\"],\"name\":\"digest\",\"req\":\"^0.10.5\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4.1\"},{\"default_features\":false,\"features\":[\"i128\",\"prime\",\"zeroize\"],\"name\":\"num-bigint\",\"package\":\"num-bigint-dig\",\"req\":\"^0.8.6\"},{\"default_features\":false,\"name\":\"num-integer\",\"req\":\"^0.1.39\"},{\"default_features\":false,\"features\":[\"libm\"],\"name\":\"num-traits\",\"req\":\"^0.2.9\"},{\"default_features\":false,\"features\":[\"alloc\",\"pkcs8\"],\"name\":\"pkcs1\",\"req\":\"^0.7.5\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"pkcs8\",\"req\":\"^0.10.2\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rand_chacha\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"rand_core\",\"req\":\"^0.6.4\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"rand_xorshift\",\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.184\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0.89\"},{\"default_features\":false,\"features\":[\"oid\"],\"name\":\"sha1\",\"optional\":true,\"req\":\"^0.10.5\"},{\"default_features\":false,\"features\":[\"oid\"],\"kind\":\"dev\",\"name\":\"sha1\",\"req\":\"^0.10.5\"},{\"default_features\":false,\"features\":[\"oid\"],\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10.6\"},{\"default_features\":false,\"features\":[\"oid\"],\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.10.6\"},{\"default_features\":false,\"features\":[\"oid\"],\"kind\":\"dev\",\"name\":\"sha3\",\"req\":\"^0.10.7\"},{\"default_features\":false,\"features\":[\"alloc\",\"digest\",\"rand_core\"],\"name\":\"signature\",\"req\":\">2.0, <2.3\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"spki\",\"req\":\"^0.7.3\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2.1.1\"},{\"features\":[\"alloc\"],\"name\":\"zeroize\",\"req\":\"^1.5\"}],\"features\":{\"default\":[\"std\",\"pem\",\"u64_digit\"],\"getrandom\":[\"rand_core/getrandom\"],\"hazmat\":[],\"nightly\":[\"num-bigint/nightly\"],\"pem\":[\"pkcs1/pem\",\"pkcs8/pem\"],\"pkcs5\":[\"pkcs8/encryption\"],\"serde\":[\"dep:serde\",\"num-bigint/serde\"],\"std\":[\"digest/std\",\"pkcs1/std\",\"pkcs8/std\",\"rand_core/std\",\"signature/std\"],\"u64_digit\":[\"num-bigint/u64_digit\"]}}", "rtrb_0.3.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"crossbeam-utils\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.10\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "rust-embed-impl_8.11.0": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"name\":\"rust-embed-utils\",\"req\":\"^8.11.0\"},{\"name\":\"shellexpand\",\"optional\":true,\"req\":\"^3\"},{\"default_features\":false,\"features\":[\"derive\",\"parsing\",\"proc-macro\",\"printing\"],\"name\":\"syn\",\"req\":\"^2\"},{\"name\":\"walkdir\",\"req\":\"^2.3.1\"}],\"features\":{\"compression\":[],\"debug-embed\":[],\"deterministic-timestamps\":[],\"include-exclude\":[\"rust-embed-utils/include-exclude\"],\"interpolate-folder-path\":[\"shellexpand\"],\"mime-guess\":[\"rust-embed-utils/mime-guess\"]}}", "rust-embed-utils_8.11.0": "{\"dependencies\":[{\"name\":\"globset\",\"optional\":true,\"req\":\"^0.4.8\"},{\"name\":\"mime_guess\",\"optional\":true,\"req\":\"^2.0.4\"},{\"name\":\"sha2\",\"req\":\"^0.10.5\"},{\"name\":\"walkdir\",\"req\":\"^2.3.1\"}],\"features\":{\"debug-embed\":[],\"include-exclude\":[\"globset\"],\"mime-guess\":[\"mime_guess\"]}}", @@ -1509,8 +1512,10 @@ "serial_test_derive_3.3.1": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\">=0.6.1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"prettyplease\",\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"proc-macro\"],\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"default_features\":false,\"name\":\"quote\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"full\",\"printing\",\"parsing\",\"clone-impls\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{\"async\":[],\"default\":[],\"file_locks\":[],\"test_logging\":[]}}", "sha1-checked_0.10.0": "{\"dependencies\":[{\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"default_features\":false,\"features\":[\"compress\"],\"name\":\"sha1\",\"req\":\"^0.10.6\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.7\"}],\"features\":{\"default\":[\"oid\",\"std\"],\"oid\":[\"digest/oid\",\"sha1/oid\"],\"std\":[\"digest/std\",\"sha1/std\"],\"zeroize\":[\"dep:zeroize\"]}}", "sha1_0.10.6": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"name\":\"cpufeatures\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_arch = \\\"aarch64\\\", target_arch = \\\"x86\\\", target_arch = \\\"x86_64\\\"))\"},{\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.2.2\"},{\"name\":\"sha1-asm\",\"optional\":true,\"req\":\"^0.5\",\"target\":\"cfg(any(target_arch = \\\"aarch64\\\", target_arch = \\\"x86\\\", target_arch = \\\"x86_64\\\"))\"}],\"features\":{\"asm\":[\"sha1-asm\"],\"compress\":[],\"default\":[\"std\"],\"force-soft\":[],\"loongarch64_asm\":[],\"oid\":[\"digest/oid\"],\"std\":[\"digest/std\"]}}", + "sha1_0.11.0": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"name\":\"cpufeatures\",\"req\":\"^0.3\",\"target\":\"cfg(any(target_arch = \\\"aarch64\\\", target_arch = \\\"x86\\\", target_arch = \\\"x86_64\\\"))\"},{\"name\":\"digest\",\"req\":\"^0.11\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.11\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^1\"}],\"features\":{\"alloc\":[\"digest/alloc\"],\"default\":[\"alloc\",\"oid\"],\"oid\":[\"digest/oid\"],\"zeroize\":[\"digest/zeroize\"]}}", "sha1_smol_1.0.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"openssl\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.4\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"alloc\":[],\"std\":[\"alloc\"]}}", "sha2_0.10.9": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"name\":\"cpufeatures\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_arch = \\\"aarch64\\\", target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"},{\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.2.2\"},{\"name\":\"sha2-asm\",\"optional\":true,\"req\":\"^0.6.1\",\"target\":\"cfg(any(target_arch = \\\"aarch64\\\", target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"}],\"features\":{\"asm\":[\"sha2-asm\"],\"asm-aarch64\":[\"asm\"],\"compress\":[],\"default\":[\"std\"],\"force-soft\":[],\"force-soft-compact\":[],\"loongarch64_asm\":[],\"oid\":[\"digest/oid\"],\"std\":[\"digest/std\"]}}", + "sha2_0.11.0": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"cpufeatures\",\"req\":\"^0.3\",\"target\":\"cfg(any(target_arch = \\\"aarch64\\\", target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"},{\"name\":\"digest\",\"req\":\"^0.11\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.11\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^1\"}],\"features\":{\"alloc\":[\"digest/alloc\"],\"default\":[\"alloc\",\"oid\"],\"oid\":[\"digest/oid\"],\"zeroize\":[\"digest/zeroize\"]}}", "sharded-slab_0.1.7": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"indexmap\",\"req\":\"^1\"},{\"name\":\"lazy_static\",\"req\":\"^1\"},{\"features\":[\"checkpoint\"],\"name\":\"loom\",\"optional\":true,\"req\":\"^0.5\",\"target\":\"cfg(loom)\"},{\"features\":[\"checkpoint\"],\"kind\":\"dev\",\"name\":\"loom\",\"req\":\"^0.5\",\"target\":\"cfg(loom)\"},{\"kind\":\"dev\",\"name\":\"memory-stats\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"slab\",\"req\":\"^0.4.2\"}],\"features\":{}}", "shared_library_0.1.9": "{\"dependencies\":[{\"name\":\"lazy_static\",\"req\":\"^1\"},{\"name\":\"libc\",\"req\":\"^0.2\"}],\"features\":{}}", "shell-words_1.1.1": "{\"dependencies\":[],\"features\":{\"default\":[\"std\"],\"std\":[]}}", @@ -1533,13 +1538,13 @@ "socket2_0.6.3": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.172\",\"target\":\"cfg(any(unix, target_os = \\\"wasi\\\"))\"},{\"features\":[\"Win32_Foundation\",\"Win32_Networking_WinSock\",\"Win32_System_IO\",\"Win32_System_Threading\",\"Win32_System_WindowsProgramming\"],\"name\":\"windows-sys\",\"req\":\">=0.60, <0.62\",\"target\":\"cfg(windows)\"}],\"features\":{\"all\":[]}}", "spin_0.9.8": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4\"},{\"name\":\"lock_api_crate\",\"optional\":true,\"package\":\"lock_api\",\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"portable-atomic\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"barrier\":[\"mutex\"],\"default\":[\"lock_api\",\"mutex\",\"spin_mutex\",\"rwlock\",\"once\",\"lazy\",\"barrier\"],\"fair_mutex\":[\"mutex\"],\"lazy\":[\"once\"],\"lock_api\":[\"lock_api_crate\"],\"mutex\":[],\"once\":[],\"portable_atomic\":[\"portable-atomic\"],\"rwlock\":[],\"spin_mutex\":[\"mutex\"],\"std\":[],\"ticket_mutex\":[\"mutex\"],\"use_ticket_mutex\":[\"mutex\",\"ticket_mutex\"]}}", "spki_0.7.3": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.2\"},{\"default_features\":false,\"name\":\"base64ct\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"oid\"],\"name\":\"der\",\"req\":\"^0.7.2\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"alloc\":[\"base64ct?/alloc\",\"der/alloc\"],\"arbitrary\":[\"std\",\"dep:arbitrary\",\"der/arbitrary\"],\"base64\":[\"dep:base64ct\"],\"fingerprint\":[\"sha2\"],\"pem\":[\"alloc\",\"der/pem\"],\"std\":[\"der/std\",\"alloc\"]}}", - "sqlx-core_0.8.6": "{\"dependencies\":[{\"name\":\"async-io\",\"optional\":true,\"req\":\"^1.9.0\"},{\"name\":\"async-std\",\"optional\":true,\"req\":\"^1.12\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"base64\",\"req\":\"^0.22.0\"},{\"name\":\"bigdecimal\",\"optional\":true,\"req\":\"^0.4.0\"},{\"name\":\"bit-vec\",\"optional\":true,\"req\":\"^0.6.3\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"bstr\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"bytes\",\"req\":\"^1.1.0\"},{\"default_features\":false,\"features\":[\"clock\"],\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4.34\"},{\"name\":\"crc\",\"optional\":true,\"req\":\"^3\"},{\"name\":\"crossbeam-queue\",\"req\":\"^0.3.2\"},{\"name\":\"either\",\"req\":\"^1.6.1\"},{\"name\":\"event-listener\",\"req\":\"^5.2.0\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.19\"},{\"name\":\"futures-intrusive\",\"req\":\"^0.5.0\"},{\"name\":\"futures-io\",\"req\":\"^0.3.24\"},{\"default_features\":false,\"features\":[\"alloc\",\"sink\",\"io\"],\"name\":\"futures-util\",\"req\":\"^0.3.19\"},{\"name\":\"hashbrown\",\"req\":\"^0.15.0\"},{\"name\":\"hashlink\",\"req\":\"^0.10.0\"},{\"name\":\"indexmap\",\"req\":\"^2.0\"},{\"name\":\"ipnet\",\"optional\":true,\"req\":\"^2.3.0\"},{\"name\":\"ipnetwork\",\"optional\":true,\"req\":\"^0.20.0\"},{\"default_features\":false,\"name\":\"log\",\"req\":\"^0.4.18\"},{\"name\":\"mac_address\",\"optional\":true,\"req\":\"^1.1.5\"},{\"default_features\":false,\"name\":\"memchr\",\"req\":\"^2.4.1\"},{\"name\":\"native-tls\",\"optional\":true,\"req\":\"^0.2.10\"},{\"name\":\"once_cell\",\"req\":\"^1.9.0\"},{\"name\":\"percent-encoding\",\"req\":\"^2.1.0\"},{\"name\":\"regex\",\"optional\":true,\"req\":\"^1.5.5\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rust_decimal\",\"optional\":true,\"req\":\"^1.26.1\"},{\"default_features\":false,\"features\":[\"std\",\"tls12\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.15\"},{\"name\":\"rustls-native-certs\",\"optional\":true,\"req\":\"^0.8.0\"},{\"features\":[\"derive\",\"rc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.132\"},{\"features\":[\"raw_value\"],\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.73\"},{\"default_features\":false,\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10.0\"},{\"name\":\"smallvec\",\"req\":\"^1.7.0\"},{\"default_features\":false,\"features\":[\"postgres\",\"sqlite\",\"mysql\",\"migrate\",\"macros\",\"time\",\"uuid\"],\"kind\":\"dev\",\"name\":\"sqlx\",\"req\":\"=0.8.6\"},{\"name\":\"thiserror\",\"req\":\"^2.0.0\"},{\"features\":[\"formatting\",\"parsing\",\"macros\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.36\"},{\"default_features\":false,\"features\":[\"time\",\"net\",\"sync\",\"fs\",\"io-util\",\"rt\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"fs\"],\"name\":\"tokio-stream\",\"optional\":true,\"req\":\"^0.1.8\"},{\"features\":[\"log\"],\"name\":\"tracing\",\"req\":\"^0.1.37\"},{\"name\":\"url\",\"req\":\"^2.2.2\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^1.1.2\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^0.26\"}],\"features\":{\"_rt-async-std\":[\"async-std\",\"async-io\"],\"_rt-tokio\":[\"tokio\",\"tokio-stream\"],\"_tls-native-tls\":[\"native-tls\"],\"_tls-none\":[],\"_tls-rustls\":[\"rustls\"],\"_tls-rustls-aws-lc-rs\":[\"_tls-rustls\",\"rustls/aws-lc-rs\",\"webpki-roots\"],\"_tls-rustls-ring-native-roots\":[\"_tls-rustls\",\"rustls/ring\",\"rustls-native-certs\"],\"_tls-rustls-ring-webpki\":[\"_tls-rustls\",\"rustls/ring\",\"webpki-roots\"],\"any\":[],\"default\":[],\"json\":[\"serde\",\"serde_json\"],\"migrate\":[\"sha2\",\"crc\"],\"offline\":[\"serde\",\"either/serde\"]}}", - "sqlx-macros-core_0.8.6": "{\"dependencies\":[{\"name\":\"async-std\",\"optional\":true,\"req\":\"^1.12\"},{\"default_features\":false,\"name\":\"dotenvy\",\"req\":\"^0.15.7\"},{\"name\":\"either\",\"req\":\"^1.6.1\"},{\"name\":\"heck\",\"req\":\"^0.5\"},{\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"once_cell\",\"req\":\"^1.9.0\"},{\"default_features\":false,\"name\":\"proc-macro2\",\"req\":\"^1.0.79\"},{\"default_features\":false,\"name\":\"quote\",\"req\":\"^1.0.26\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0.132\"},{\"name\":\"serde_json\",\"req\":\"^1.0.73\"},{\"name\":\"sha2\",\"req\":\"^0.10.0\"},{\"features\":[\"offline\"],\"name\":\"sqlx-core\",\"req\":\"=0.8.6\"},{\"features\":[\"offline\",\"migrate\"],\"name\":\"sqlx-mysql\",\"optional\":true,\"req\":\"=0.8.6\"},{\"features\":[\"offline\",\"migrate\"],\"name\":\"sqlx-postgres\",\"optional\":true,\"req\":\"=0.8.6\"},{\"features\":[\"offline\",\"migrate\"],\"name\":\"sqlx-sqlite\",\"optional\":true,\"req\":\"=0.8.6\"},{\"default_features\":false,\"features\":[\"full\",\"derive\",\"parsing\",\"printing\",\"clone-impls\"],\"name\":\"syn\",\"req\":\"^2.0.52\"},{\"default_features\":false,\"features\":[\"time\",\"net\",\"sync\",\"fs\",\"io-util\",\"rt\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"url\",\"req\":\"^2.2.2\"}],\"features\":{\"_rt-async-std\":[\"async-std\",\"sqlx-core/_rt-async-std\"],\"_rt-tokio\":[\"tokio\",\"sqlx-core/_rt-tokio\"],\"_sqlite\":[],\"_tls-native-tls\":[\"sqlx-core/_tls-native-tls\"],\"_tls-rustls-aws-lc-rs\":[\"sqlx-core/_tls-rustls-aws-lc-rs\"],\"_tls-rustls-ring-native-roots\":[\"sqlx-core/_tls-rustls-ring-native-roots\"],\"_tls-rustls-ring-webpki\":[\"sqlx-core/_tls-rustls-ring-webpki\"],\"bigdecimal\":[\"sqlx-core/bigdecimal\",\"sqlx-mysql?/bigdecimal\",\"sqlx-postgres?/bigdecimal\"],\"bit-vec\":[\"sqlx-core/bit-vec\",\"sqlx-postgres?/bit-vec\"],\"chrono\":[\"sqlx-core/chrono\",\"sqlx-mysql?/chrono\",\"sqlx-postgres?/chrono\",\"sqlx-sqlite?/chrono\"],\"default\":[],\"derive\":[],\"ipnet\":[\"sqlx-core/ipnet\",\"sqlx-postgres?/ipnet\"],\"ipnetwork\":[\"sqlx-core/ipnetwork\",\"sqlx-postgres?/ipnetwork\"],\"json\":[\"sqlx-core/json\",\"sqlx-mysql?/json\",\"sqlx-postgres?/json\",\"sqlx-sqlite?/json\"],\"mac_address\":[\"sqlx-core/mac_address\",\"sqlx-postgres?/mac_address\"],\"macros\":[],\"migrate\":[\"sqlx-core/migrate\"],\"mysql\":[\"sqlx-mysql\"],\"postgres\":[\"sqlx-postgres\"],\"rust_decimal\":[\"sqlx-core/rust_decimal\",\"sqlx-mysql?/rust_decimal\",\"sqlx-postgres?/rust_decimal\"],\"sqlite\":[\"_sqlite\",\"sqlx-sqlite/bundled\"],\"sqlite-unbundled\":[\"_sqlite\",\"sqlx-sqlite/unbundled\"],\"time\":[\"sqlx-core/time\",\"sqlx-mysql?/time\",\"sqlx-postgres?/time\",\"sqlx-sqlite?/time\"],\"uuid\":[\"sqlx-core/uuid\",\"sqlx-mysql?/uuid\",\"sqlx-postgres?/uuid\",\"sqlx-sqlite?/uuid\"]}}", - "sqlx-macros_0.8.6": "{\"dependencies\":[{\"default_features\":false,\"name\":\"proc-macro2\",\"req\":\"^1.0.36\"},{\"default_features\":false,\"name\":\"quote\",\"req\":\"^1.0.26\"},{\"features\":[\"any\"],\"name\":\"sqlx-core\",\"req\":\"=0.8.6\"},{\"name\":\"sqlx-macros-core\",\"req\":\"=0.8.6\"},{\"default_features\":false,\"features\":[\"parsing\",\"proc-macro\"],\"name\":\"syn\",\"req\":\"^2.0.52\"}],\"features\":{\"_rt-async-std\":[\"sqlx-macros-core/_rt-async-std\"],\"_rt-tokio\":[\"sqlx-macros-core/_rt-tokio\"],\"_tls-native-tls\":[\"sqlx-macros-core/_tls-native-tls\"],\"_tls-rustls-aws-lc-rs\":[\"sqlx-macros-core/_tls-rustls-aws-lc-rs\"],\"_tls-rustls-ring-native-roots\":[\"sqlx-macros-core/_tls-rustls-ring-native-roots\"],\"_tls-rustls-ring-webpki\":[\"sqlx-macros-core/_tls-rustls-ring-webpki\"],\"bigdecimal\":[\"sqlx-macros-core/bigdecimal\"],\"bit-vec\":[\"sqlx-macros-core/bit-vec\"],\"chrono\":[\"sqlx-macros-core/chrono\"],\"default\":[],\"derive\":[\"sqlx-macros-core/derive\"],\"ipnet\":[\"sqlx-macros-core/ipnet\"],\"ipnetwork\":[\"sqlx-macros-core/ipnetwork\"],\"json\":[\"sqlx-macros-core/json\"],\"mac_address\":[\"sqlx-macros-core/mac_address\"],\"macros\":[\"sqlx-macros-core/macros\"],\"migrate\":[\"sqlx-macros-core/migrate\"],\"mysql\":[\"sqlx-macros-core/mysql\"],\"postgres\":[\"sqlx-macros-core/postgres\"],\"rust_decimal\":[\"sqlx-macros-core/rust_decimal\"],\"sqlite\":[\"sqlx-macros-core/sqlite\"],\"sqlite-unbundled\":[\"sqlx-macros-core/sqlite-unbundled\"],\"time\":[\"sqlx-macros-core/time\"],\"uuid\":[\"sqlx-macros-core/uuid\"]}}", - "sqlx-mysql_0.8.6": "{\"dependencies\":[{\"name\":\"atoi\",\"req\":\"^2.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"base64\",\"req\":\"^0.22.0\"},{\"name\":\"bigdecimal\",\"optional\":true,\"req\":\"^0.4.0\"},{\"default_features\":false,\"features\":[\"serde\"],\"name\":\"bitflags\",\"req\":\"^2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"byteorder\",\"req\":\"^1.4.3\"},{\"name\":\"bytes\",\"req\":\"^1.1.0\"},{\"default_features\":false,\"features\":[\"std\",\"clock\"],\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4.34\"},{\"name\":\"crc\",\"req\":\"^3.0.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"digest\",\"req\":\"^0.10.0\"},{\"name\":\"dotenvy\",\"req\":\"^0.15.5\"},{\"name\":\"either\",\"req\":\"^1.6.1\"},{\"default_features\":false,\"features\":[\"sink\",\"alloc\",\"std\"],\"name\":\"futures-channel\",\"req\":\"^0.3.19\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.19\"},{\"name\":\"futures-io\",\"req\":\"^0.3.24\"},{\"default_features\":false,\"features\":[\"alloc\",\"sink\",\"io\"],\"name\":\"futures-util\",\"req\":\"^0.3.19\"},{\"default_features\":false,\"name\":\"generic-array\",\"req\":\"^0.14.4\"},{\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"hkdf\",\"req\":\"^0.12.0\"},{\"default_features\":false,\"name\":\"hmac\",\"req\":\"^0.12.0\"},{\"name\":\"itoa\",\"req\":\"^1.0.1\"},{\"name\":\"log\",\"req\":\"^0.4.18\"},{\"default_features\":false,\"name\":\"md-5\",\"req\":\"^0.10.0\"},{\"default_features\":false,\"name\":\"memchr\",\"req\":\"^2.4.1\"},{\"name\":\"once_cell\",\"req\":\"^1.9.0\"},{\"name\":\"percent-encoding\",\"req\":\"^2.1.0\"},{\"default_features\":false,\"features\":[\"std\",\"std_rng\"],\"name\":\"rand\",\"req\":\"^0.8.4\"},{\"name\":\"rsa\",\"req\":\"^0.9\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rust_decimal\",\"optional\":true,\"req\":\"^1.26.1\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.144\"},{\"default_features\":false,\"name\":\"sha1\",\"req\":\"^0.10.1\"},{\"default_features\":false,\"name\":\"sha2\",\"req\":\"^0.10.0\"},{\"name\":\"smallvec\",\"req\":\"^1.7.0\"},{\"default_features\":false,\"features\":[\"mysql\"],\"kind\":\"dev\",\"name\":\"sqlx\",\"req\":\"=0.8.6\"},{\"name\":\"sqlx-core\",\"req\":\"=0.8.6\"},{\"name\":\"stringprep\",\"req\":\"^0.1.2\"},{\"name\":\"thiserror\",\"req\":\"^2.0.0\"},{\"features\":[\"formatting\",\"parsing\",\"macros\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.36\"},{\"features\":[\"log\"],\"name\":\"tracing\",\"req\":\"^0.1.37\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^1.1.2\"},{\"default_features\":false,\"name\":\"whoami\",\"req\":\"^1.2.1\"}],\"features\":{\"any\":[\"sqlx-core/any\"],\"bigdecimal\":[\"dep:bigdecimal\",\"sqlx-core/bigdecimal\"],\"chrono\":[\"dep:chrono\",\"sqlx-core/chrono\"],\"json\":[\"sqlx-core/json\",\"serde\"],\"migrate\":[\"sqlx-core/migrate\"],\"offline\":[\"sqlx-core/offline\",\"serde/derive\"],\"rust_decimal\":[\"dep:rust_decimal\",\"rust_decimal/maths\",\"sqlx-core/rust_decimal\"],\"time\":[\"dep:time\",\"sqlx-core/time\"],\"uuid\":[\"dep:uuid\",\"sqlx-core/uuid\"]}}", - "sqlx-postgres_0.8.6": "{\"dependencies\":[{\"name\":\"atoi\",\"req\":\"^2.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"base64\",\"req\":\"^0.22.0\"},{\"name\":\"bigdecimal\",\"optional\":true,\"req\":\"^0.4.0\"},{\"name\":\"bit-vec\",\"optional\":true,\"req\":\"^0.6.3\"},{\"default_features\":false,\"name\":\"bitflags\",\"req\":\"^2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"byteorder\",\"req\":\"^1.4.3\"},{\"default_features\":false,\"features\":[\"std\",\"clock\"],\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4.34\"},{\"name\":\"crc\",\"req\":\"^3.0.0\"},{\"default_features\":false,\"name\":\"dotenvy\",\"req\":\"^0.15.7\"},{\"name\":\"etcetera\",\"req\":\"^0.8.0\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"default_features\":false,\"features\":[\"sink\",\"alloc\",\"std\"],\"name\":\"futures-channel\",\"req\":\"^0.3.19\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.19\"},{\"default_features\":false,\"features\":[\"alloc\",\"sink\",\"io\"],\"name\":\"futures-util\",\"req\":\"^0.3.19\"},{\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"hkdf\",\"req\":\"^0.12.0\"},{\"default_features\":false,\"features\":[\"reset\"],\"name\":\"hmac\",\"req\":\"^0.12.0\"},{\"name\":\"home\",\"req\":\"^0.5.5\"},{\"name\":\"ipnet\",\"optional\":true,\"req\":\"^2.3.0\"},{\"name\":\"ipnetwork\",\"optional\":true,\"req\":\"^0.20.0\"},{\"name\":\"itoa\",\"req\":\"^1.0.1\"},{\"name\":\"log\",\"req\":\"^0.4.18\"},{\"name\":\"mac_address\",\"optional\":true,\"req\":\"^1.1.5\"},{\"default_features\":false,\"name\":\"md-5\",\"req\":\"^0.10.0\"},{\"default_features\":false,\"name\":\"memchr\",\"req\":\"^2.4.1\"},{\"name\":\"num-bigint\",\"optional\":true,\"req\":\"^0.4.3\"},{\"name\":\"once_cell\",\"req\":\"^1.9.0\"},{\"default_features\":false,\"features\":[\"std\",\"std_rng\"],\"name\":\"rand\",\"req\":\"^0.8.4\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rust_decimal\",\"optional\":true,\"req\":\"^1.26.1\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0.144\"},{\"features\":[\"raw_value\"],\"name\":\"serde_json\",\"req\":\"^1.0.85\"},{\"default_features\":false,\"name\":\"sha2\",\"req\":\"^0.10.0\"},{\"features\":[\"serde\"],\"name\":\"smallvec\",\"req\":\"^1.7.0\"},{\"default_features\":false,\"features\":[\"postgres\",\"derive\"],\"kind\":\"dev\",\"name\":\"sqlx\",\"req\":\"=0.8.6\"},{\"features\":[\"json\"],\"name\":\"sqlx-core\",\"req\":\"=0.8.6\"},{\"name\":\"stringprep\",\"req\":\"^0.1.2\"},{\"name\":\"thiserror\",\"req\":\"^2.0.0\"},{\"features\":[\"formatting\",\"parsing\",\"macros\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.36\"},{\"features\":[\"log\"],\"name\":\"tracing\",\"req\":\"^0.1.37\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^1.1.2\"},{\"default_features\":false,\"name\":\"whoami\",\"req\":\"^1.2.1\"}],\"features\":{\"any\":[\"sqlx-core/any\"],\"bigdecimal\":[\"dep:bigdecimal\",\"dep:num-bigint\",\"sqlx-core/bigdecimal\"],\"bit-vec\":[\"dep:bit-vec\",\"sqlx-core/bit-vec\"],\"chrono\":[\"dep:chrono\",\"sqlx-core/chrono\"],\"ipnet\":[\"dep:ipnet\",\"sqlx-core/ipnet\"],\"ipnetwork\":[\"dep:ipnetwork\",\"sqlx-core/ipnetwork\"],\"json\":[\"sqlx-core/json\"],\"mac_address\":[\"dep:mac_address\",\"sqlx-core/mac_address\"],\"migrate\":[\"sqlx-core/migrate\"],\"offline\":[\"sqlx-core/offline\"],\"rust_decimal\":[\"dep:rust_decimal\",\"rust_decimal/maths\",\"sqlx-core/rust_decimal\"],\"time\":[\"dep:time\",\"sqlx-core/time\"],\"uuid\":[\"dep:uuid\",\"sqlx-core/uuid\"]}}", - "sqlx-sqlite_0.8.6": "{\"dependencies\":[{\"name\":\"atoi\",\"req\":\"^2.0\"},{\"default_features\":false,\"features\":[\"std\",\"clock\"],\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4.34\"},{\"default_features\":false,\"features\":[\"async\"],\"name\":\"flume\",\"req\":\"^0.11.0\"},{\"default_features\":false,\"features\":[\"sink\",\"alloc\",\"std\"],\"name\":\"futures-channel\",\"req\":\"^0.3.19\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.19\"},{\"name\":\"futures-executor\",\"req\":\"^0.3.19\"},{\"name\":\"futures-intrusive\",\"req\":\"^0.5.0\"},{\"default_features\":false,\"features\":[\"alloc\",\"sink\"],\"name\":\"futures-util\",\"req\":\"^0.3.19\"},{\"default_features\":false,\"features\":[\"pkg-config\",\"vcpkg\",\"unlock_notify\"],\"name\":\"libsqlite3-sys\",\"req\":\"^0.30.1\"},{\"name\":\"log\",\"req\":\"^0.4.18\"},{\"name\":\"percent-encoding\",\"req\":\"^2.1.0\"},{\"name\":\"regex\",\"optional\":true,\"req\":\"^1.5.5\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.145\"},{\"name\":\"serde_urlencoded\",\"req\":\"^0.7\"},{\"default_features\":false,\"features\":[\"macros\",\"runtime-tokio\",\"tls-none\",\"sqlite\"],\"kind\":\"dev\",\"name\":\"sqlx\",\"req\":\"=0.8.6\"},{\"name\":\"sqlx-core\",\"req\":\"=0.8.6\"},{\"name\":\"thiserror\",\"req\":\"^2.0.0\"},{\"features\":[\"formatting\",\"parsing\",\"macros\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.36\"},{\"features\":[\"log\"],\"name\":\"tracing\",\"req\":\"^0.1.37\"},{\"name\":\"url\",\"req\":\"^2.2.2\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^1.1.2\"}],\"features\":{\"any\":[\"sqlx-core/any\"],\"bundled\":[\"libsqlite3-sys/bundled\"],\"chrono\":[\"dep:chrono\",\"sqlx-core/chrono\"],\"json\":[\"sqlx-core/json\",\"serde\"],\"migrate\":[\"sqlx-core/migrate\"],\"offline\":[\"sqlx-core/offline\",\"serde\"],\"preupdate-hook\":[\"libsqlite3-sys/preupdate_hook\"],\"regexp\":[\"dep:regex\"],\"time\":[\"dep:time\",\"sqlx-core/time\"],\"unbundled\":[\"libsqlite3-sys/buildtime_bindgen\"],\"uuid\":[\"dep:uuid\",\"sqlx-core/uuid\"]}}", - "sqlx_0.8.6": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.52\"},{\"features\":[\"attributes\"],\"kind\":\"dev\",\"name\":\"async-std\",\"req\":\"^1.12\"},{\"features\":[\"async_tokio\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"},{\"kind\":\"dev\",\"name\":\"dotenvy\",\"req\":\"^0.15.0\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.19\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"libsqlite3-sys\",\"req\":\"^0.30.1\"},{\"features\":[\"bundled-sqlcipher\"],\"kind\":\"dev\",\"name\":\"libsqlite3-sys\",\"req\":\"^0.30.1\",\"target\":\"cfg(sqlite_test_sqlcipher)\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1.0.6\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.4\"},{\"kind\":\"dev\",\"name\":\"rand_xoshiro\",\"req\":\"^0.6.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.132\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.73\"},{\"features\":[\"offline\",\"migrate\"],\"name\":\"sqlx-core\",\"req\":\"=0.8.6\"},{\"name\":\"sqlx-macros\",\"optional\":true,\"req\":\"=0.8.6\"},{\"name\":\"sqlx-mysql\",\"optional\":true,\"req\":\"=0.8.6\"},{\"name\":\"sqlx-postgres\",\"optional\":true,\"req\":\"=0.8.6\"},{\"name\":\"sqlx-sqlite\",\"optional\":true,\"req\":\"=0.8.6\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.10.1\"},{\"kind\":\"dev\",\"name\":\"time_\",\"package\":\"time\",\"req\":\"^0.3.2\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.15.0\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.53\"},{\"kind\":\"dev\",\"name\":\"url\",\"req\":\"^2.2.2\"}],\"features\":{\"_rt-async-std\":[],\"_rt-tokio\":[],\"_sqlite\":[],\"_unstable-all-types\":[\"bigdecimal\",\"rust_decimal\",\"json\",\"time\",\"chrono\",\"ipnet\",\"ipnetwork\",\"mac_address\",\"uuid\",\"bit-vec\",\"bstr\"],\"all-databases\":[\"mysql\",\"sqlite\",\"postgres\",\"any\"],\"any\":[\"sqlx-core/any\",\"sqlx-mysql?/any\",\"sqlx-postgres?/any\",\"sqlx-sqlite?/any\"],\"bigdecimal\":[\"sqlx-core/bigdecimal\",\"sqlx-macros?/bigdecimal\",\"sqlx-mysql?/bigdecimal\",\"sqlx-postgres?/bigdecimal\"],\"bit-vec\":[\"sqlx-core/bit-vec\",\"sqlx-macros?/bit-vec\",\"sqlx-postgres?/bit-vec\"],\"bstr\":[\"sqlx-core/bstr\"],\"chrono\":[\"sqlx-core/chrono\",\"sqlx-macros?/chrono\",\"sqlx-mysql?/chrono\",\"sqlx-postgres?/chrono\",\"sqlx-sqlite?/chrono\"],\"default\":[\"any\",\"macros\",\"migrate\",\"json\"],\"derive\":[\"sqlx-macros/derive\"],\"ipnet\":[\"sqlx-core/ipnet\",\"sqlx-macros?/ipnet\",\"sqlx-postgres?/ipnet\"],\"ipnetwork\":[\"sqlx-core/ipnetwork\",\"sqlx-macros?/ipnetwork\",\"sqlx-postgres?/ipnetwork\"],\"json\":[\"sqlx-core/json\",\"sqlx-macros?/json\",\"sqlx-mysql?/json\",\"sqlx-postgres?/json\",\"sqlx-sqlite?/json\"],\"mac_address\":[\"sqlx-core/mac_address\",\"sqlx-macros?/mac_address\",\"sqlx-postgres?/mac_address\"],\"macros\":[\"derive\",\"sqlx-macros/macros\"],\"migrate\":[\"sqlx-core/migrate\",\"sqlx-macros?/migrate\",\"sqlx-mysql?/migrate\",\"sqlx-postgres?/migrate\",\"sqlx-sqlite?/migrate\"],\"mysql\":[\"sqlx-mysql\",\"sqlx-macros?/mysql\"],\"postgres\":[\"sqlx-postgres\",\"sqlx-macros?/postgres\"],\"regexp\":[\"sqlx-sqlite?/regexp\"],\"runtime-async-std\":[\"_rt-async-std\",\"sqlx-core/_rt-async-std\",\"sqlx-macros?/_rt-async-std\"],\"runtime-async-std-native-tls\":[\"runtime-async-std\",\"tls-native-tls\"],\"runtime-async-std-rustls\":[\"runtime-async-std\",\"tls-rustls-ring\"],\"runtime-tokio\":[\"_rt-tokio\",\"sqlx-core/_rt-tokio\",\"sqlx-macros?/_rt-tokio\"],\"runtime-tokio-native-tls\":[\"runtime-tokio\",\"tls-native-tls\"],\"runtime-tokio-rustls\":[\"runtime-tokio\",\"tls-rustls-ring\"],\"rust_decimal\":[\"sqlx-core/rust_decimal\",\"sqlx-macros?/rust_decimal\",\"sqlx-mysql?/rust_decimal\",\"sqlx-postgres?/rust_decimal\"],\"sqlite\":[\"_sqlite\",\"sqlx-sqlite/bundled\",\"sqlx-macros?/sqlite\"],\"sqlite-preupdate-hook\":[\"sqlx-sqlite/preupdate-hook\"],\"sqlite-unbundled\":[\"_sqlite\",\"sqlx-sqlite/unbundled\",\"sqlx-macros?/sqlite-unbundled\"],\"time\":[\"sqlx-core/time\",\"sqlx-macros?/time\",\"sqlx-mysql?/time\",\"sqlx-postgres?/time\",\"sqlx-sqlite?/time\"],\"tls-native-tls\":[\"sqlx-core/_tls-native-tls\",\"sqlx-macros?/_tls-native-tls\"],\"tls-none\":[],\"tls-rustls\":[\"tls-rustls-ring\"],\"tls-rustls-aws-lc-rs\":[\"sqlx-core/_tls-rustls-aws-lc-rs\",\"sqlx-macros?/_tls-rustls-aws-lc-rs\"],\"tls-rustls-ring\":[\"tls-rustls-ring-webpki\"],\"tls-rustls-ring-native-roots\":[\"sqlx-core/_tls-rustls-ring-native-roots\",\"sqlx-macros?/_tls-rustls-ring-native-roots\"],\"tls-rustls-ring-webpki\":[\"sqlx-core/_tls-rustls-ring-webpki\",\"sqlx-macros?/_tls-rustls-ring-webpki\"],\"uuid\":[\"sqlx-core/uuid\",\"sqlx-macros?/uuid\",\"sqlx-mysql?/uuid\",\"sqlx-postgres?/uuid\",\"sqlx-sqlite?/uuid\"]}}", + "sqlx-core_0.9.0": "{\"dependencies\":[{\"name\":\"async-fs\",\"optional\":true,\"req\":\"^2.1\"},{\"default_features\":false,\"features\":[\"async-io\"],\"name\":\"async-global-executor\",\"optional\":true,\"req\":\"^3.1\"},{\"name\":\"async-io\",\"optional\":true,\"req\":\"^2.4.1\"},{\"name\":\"async-std\",\"optional\":true,\"req\":\"^1.13\"},{\"name\":\"async-task\",\"optional\":true,\"req\":\"^4.7.1\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"base64\",\"req\":\"^0.22.1\"},{\"name\":\"bigdecimal\",\"optional\":true,\"req\":\"^0.4.0\"},{\"name\":\"bit-vec\",\"optional\":true,\"req\":\"^0.8\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"bstr\",\"optional\":true,\"req\":\"^1.0.1\"},{\"name\":\"bytes\",\"req\":\"^1.2.0\"},{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"features\":[\"std\",\"clock\"],\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4.34\"},{\"name\":\"crc\",\"optional\":true,\"req\":\"^3\"},{\"name\":\"crossbeam-queue\",\"req\":\"^0.3.2\"},{\"name\":\"either\",\"req\":\"^1.6.1\"},{\"name\":\"event-listener\",\"req\":\"^5.2.0\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.32\"},{\"name\":\"futures-intrusive\",\"req\":\"^0.5.0\"},{\"name\":\"futures-io\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"features\":[\"alloc\",\"sink\",\"io\"],\"name\":\"futures-util\",\"req\":\"^0.3.32\"},{\"name\":\"hashbrown\",\"req\":\"^0.16.0\"},{\"name\":\"hashlink\",\"req\":\"^0.11.0\"},{\"name\":\"indexmap\",\"req\":\"^2.0\"},{\"name\":\"ipnet\",\"optional\":true,\"req\":\"^2.3.0\"},{\"name\":\"ipnetwork\",\"optional\":true,\"req\":\"^0.21.1\"},{\"default_features\":false,\"name\":\"log\",\"req\":\"^0.4.18\"},{\"name\":\"mac_address\",\"optional\":true,\"req\":\"^1.1.5\"},{\"default_features\":false,\"name\":\"memchr\",\"req\":\"^2.5.0\"},{\"name\":\"native-tls\",\"optional\":true,\"req\":\"^0.2.10\"},{\"name\":\"percent-encoding\",\"req\":\"^2.3.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rust_decimal\",\"optional\":true,\"req\":\"^1.36.0\"},{\"default_features\":false,\"features\":[\"std\",\"tls12\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.24\"},{\"name\":\"rustls-native-certs\",\"optional\":true,\"req\":\"^0.8.0\"},{\"features\":[\"derive\",\"rc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.219\"},{\"features\":[\"raw_value\"],\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.142\"},{\"default_features\":false,\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10.0\"},{\"name\":\"smallvec\",\"req\":\"^1.13.1\"},{\"default_features\":false,\"name\":\"smol\",\"optional\":true,\"req\":\"^2.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"thiserror\",\"req\":\"^2.0.18\"},{\"features\":[\"formatting\",\"parsing\",\"macros\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.47\"},{\"default_features\":false,\"features\":[\"time\",\"net\",\"sync\",\"fs\",\"io-util\",\"rt\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.25.0\"},{\"features\":[\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.25.0\"},{\"features\":[\"fs\"],\"name\":\"tokio-stream\",\"optional\":true,\"req\":\"^0.1.8\"},{\"name\":\"toml\",\"optional\":true,\"req\":\"^0.8.16\"},{\"features\":[\"log\"],\"name\":\"tracing\",\"req\":\"^0.1.37\"},{\"name\":\"url\",\"req\":\"^2.2.2\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^1.12.1\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"_rt-async-global-executor\":[\"async-global-executor\",\"_rt-async-io\",\"_rt-async-task\"],\"_rt-async-io\":[\"async-io\",\"async-fs\"],\"_rt-async-std\":[\"async-std\",\"_rt-async-io\"],\"_rt-async-task\":[\"async-task\"],\"_rt-smol\":[\"smol\",\"_rt-async-io\",\"_rt-async-task\"],\"_rt-tokio\":[\"tokio\",\"tokio-stream\"],\"_tls-native-tls\":[\"native-tls\"],\"_tls-none\":[],\"_tls-rustls\":[\"rustls\"],\"_tls-rustls-aws-lc-rs\":[\"_tls-rustls\",\"rustls/aws-lc-rs\",\"webpki-roots\"],\"_tls-rustls-ring-native-roots\":[\"_tls-rustls\",\"rustls/ring\",\"rustls-native-certs\"],\"_tls-rustls-ring-webpki\":[\"_tls-rustls\",\"rustls/ring\",\"webpki-roots\"],\"_unstable-doc\":[\"sqlx-toml\"],\"any\":[],\"default\":[],\"json\":[\"serde\",\"serde_json\"],\"migrate\":[\"sha2\",\"crc\"],\"offline\":[\"serde\",\"either/serde\"],\"sqlx-toml\":[\"serde\",\"toml/parse\"]}}", + "sqlx-macros-core_0.9.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"async-io\"],\"name\":\"async-global-executor\",\"optional\":true,\"req\":\"^3.1\"},{\"name\":\"async-std\",\"optional\":true,\"req\":\"^1.13\"},{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"name\":\"dotenvy\",\"req\":\"^0.15.7\"},{\"name\":\"either\",\"req\":\"^1.6.1\"},{\"name\":\"heck\",\"req\":\"^0.5\"},{\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"default_features\":false,\"name\":\"proc-macro2\",\"req\":\"^1.0.83\"},{\"default_features\":false,\"name\":\"quote\",\"req\":\"^1.0.35\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0.219\"},{\"name\":\"serde_json\",\"req\":\"^1.0.142\"},{\"name\":\"sha2\",\"req\":\"^0.10.0\"},{\"default_features\":false,\"name\":\"smol\",\"optional\":true,\"req\":\"^2.0\"},{\"features\":[\"offline\"],\"name\":\"sqlx-core\",\"req\":\"=0.9.0\"},{\"default_features\":false,\"features\":[\"offline\",\"migrate\"],\"name\":\"sqlx-mysql\",\"optional\":true,\"req\":\"=0.9.0\"},{\"features\":[\"offline\",\"migrate\"],\"name\":\"sqlx-postgres\",\"optional\":true,\"req\":\"=0.9.0\"},{\"features\":[\"offline\",\"migrate\"],\"name\":\"sqlx-sqlite\",\"optional\":true,\"req\":\"=0.9.0\"},{\"default_features\":false,\"features\":[\"full\",\"derive\",\"parsing\",\"printing\",\"clone-impls\"],\"name\":\"syn\",\"req\":\"^2.0.87\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"thiserror\",\"optional\":true,\"req\":\"^2.0.18\"},{\"default_features\":false,\"features\":[\"time\",\"net\",\"sync\",\"fs\",\"io-util\",\"rt\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.25.0\"},{\"name\":\"url\",\"req\":\"^2.2.2\"}],\"features\":{\"_rt-async-global-executor\":[\"async-global-executor\",\"sqlx-core/_rt-async-global-executor\"],\"_rt-async-std\":[\"async-std\",\"sqlx-core/_rt-async-std\"],\"_rt-smol\":[\"smol\",\"sqlx-core/_rt-smol\"],\"_rt-tokio\":[\"tokio\",\"sqlx-core/_rt-tokio\"],\"_sqlite\":[],\"_tls-native-tls\":[\"sqlx-core/_tls-native-tls\"],\"_tls-rustls-aws-lc-rs\":[\"sqlx-core/_tls-rustls-aws-lc-rs\"],\"_tls-rustls-ring-native-roots\":[\"sqlx-core/_tls-rustls-ring-native-roots\"],\"_tls-rustls-ring-webpki\":[\"sqlx-core/_tls-rustls-ring-webpki\"],\"bigdecimal\":[\"sqlx-core/bigdecimal\",\"sqlx-mysql?/bigdecimal\",\"sqlx-postgres?/bigdecimal\"],\"bit-vec\":[\"sqlx-core/bit-vec\",\"sqlx-postgres?/bit-vec\"],\"chrono\":[\"sqlx-core/chrono\",\"sqlx-mysql?/chrono\",\"sqlx-postgres?/chrono\",\"sqlx-sqlite?/chrono\"],\"default\":[],\"derive\":[],\"ipnet\":[\"sqlx-core/ipnet\",\"sqlx-postgres?/ipnet\"],\"ipnetwork\":[\"sqlx-core/ipnetwork\",\"sqlx-postgres?/ipnetwork\"],\"json\":[\"sqlx-core/json\",\"sqlx-mysql?/json\",\"sqlx-postgres?/json\",\"sqlx-sqlite?/json\"],\"mac_address\":[\"sqlx-core/mac_address\",\"sqlx-postgres?/mac_address\"],\"macros\":[\"thiserror\"],\"migrate\":[\"sqlx-core/migrate\"],\"mysql\":[\"sqlx-mysql\"],\"mysql-rsa\":[\"mysql\",\"sqlx-mysql/rsa\"],\"postgres\":[\"sqlx-postgres\"],\"rust_decimal\":[\"sqlx-core/rust_decimal\",\"sqlx-mysql?/rust_decimal\",\"sqlx-postgres?/rust_decimal\"],\"sqlite\":[\"_sqlite\",\"sqlx-sqlite/bundled\"],\"sqlite-load-extension\":[\"sqlx-sqlite/load-extension\"],\"sqlite-unbundled\":[\"_sqlite\",\"sqlx-sqlite/unbundled\"],\"sqlx-toml\":[\"sqlx-core/sqlx-toml\",\"sqlx-sqlite?/sqlx-toml\"],\"time\":[\"sqlx-core/time\",\"sqlx-mysql?/time\",\"sqlx-postgres?/time\",\"sqlx-sqlite?/time\"],\"uuid\":[\"sqlx-core/uuid\",\"sqlx-mysql?/uuid\",\"sqlx-postgres?/uuid\",\"sqlx-sqlite?/uuid\"]}}", + "sqlx-macros_0.9.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"proc-macro2\",\"req\":\"^1.0.83\"},{\"default_features\":false,\"name\":\"quote\",\"req\":\"^1.0.35\"},{\"features\":[\"any\"],\"name\":\"sqlx-core\",\"req\":\"=0.9.0\"},{\"name\":\"sqlx-macros-core\",\"req\":\"=0.9.0\"},{\"default_features\":false,\"features\":[\"parsing\",\"proc-macro\"],\"name\":\"syn\",\"req\":\"^2.0.87\"}],\"features\":{\"_rt-async-global-executor\":[\"sqlx-macros-core/_rt-async-global-executor\"],\"_rt-async-std\":[\"sqlx-macros-core/_rt-async-std\"],\"_rt-smol\":[\"sqlx-macros-core/_rt-smol\"],\"_rt-tokio\":[\"sqlx-macros-core/_rt-tokio\"],\"_tls-native-tls\":[\"sqlx-macros-core/_tls-native-tls\"],\"_tls-rustls-aws-lc-rs\":[\"sqlx-macros-core/_tls-rustls-aws-lc-rs\"],\"_tls-rustls-ring-native-roots\":[\"sqlx-macros-core/_tls-rustls-ring-native-roots\"],\"_tls-rustls-ring-webpki\":[\"sqlx-macros-core/_tls-rustls-ring-webpki\"],\"bigdecimal\":[\"sqlx-macros-core/bigdecimal\"],\"bit-vec\":[\"sqlx-macros-core/bit-vec\"],\"chrono\":[\"sqlx-macros-core/chrono\"],\"default\":[],\"derive\":[\"sqlx-macros-core/derive\"],\"ipnet\":[\"sqlx-macros-core/ipnet\"],\"ipnetwork\":[\"sqlx-macros-core/ipnetwork\"],\"json\":[\"sqlx-macros-core/json\"],\"mac_address\":[\"sqlx-macros-core/mac_address\"],\"macros\":[\"sqlx-macros-core/macros\"],\"migrate\":[\"sqlx-macros-core/migrate\"],\"mysql\":[\"sqlx-macros-core/mysql\"],\"mysql-rsa\":[\"sqlx-macros-core/mysql-rsa\"],\"postgres\":[\"sqlx-macros-core/postgres\"],\"rust_decimal\":[\"sqlx-macros-core/rust_decimal\"],\"sqlite\":[\"sqlx-macros-core/sqlite\"],\"sqlite-load-extension\":[\"sqlx-macros-core/sqlite-load-extension\"],\"sqlite-unbundled\":[\"sqlx-macros-core/sqlite-unbundled\"],\"sqlx-toml\":[\"sqlx-macros-core/sqlx-toml\"],\"time\":[\"sqlx-macros-core/time\"],\"uuid\":[\"sqlx-macros-core/uuid\"]}}", + "sqlx-mysql_0.9.0": "{\"dependencies\":[{\"name\":\"bigdecimal\",\"optional\":true,\"req\":\"^0.4.0\"},{\"default_features\":false,\"name\":\"bitflags\",\"req\":\"^2.4\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"byteorder\",\"req\":\"^1.4.3\"},{\"name\":\"bytes\",\"req\":\"^1.2.0\"},{\"default_features\":false,\"features\":[\"std\",\"clock\"],\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4.34\"},{\"name\":\"crc\",\"optional\":true,\"req\":\"^3.0.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"digest\",\"req\":\"^0.11.2\"},{\"default_features\":false,\"name\":\"dotenvy\",\"req\":\"^0.15.7\"},{\"name\":\"either\",\"req\":\"^1.6.1\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"features\":[\"alloc\",\"sink\",\"io\"],\"name\":\"futures-util\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"generic-array\",\"req\":\"^0.14.4\"},{\"name\":\"log\",\"req\":\"^0.4.18\"},{\"name\":\"percent-encoding\",\"req\":\"^2.3.0\"},{\"default_features\":false,\"features\":[\"thread_rng\"],\"name\":\"rand\",\"optional\":true,\"req\":\"^0.10.1\"},{\"name\":\"rsa\",\"optional\":true,\"req\":\"^0.10.0-rc.18\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rust_decimal\",\"optional\":true,\"req\":\"^1.36.0\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.219\"},{\"default_features\":false,\"name\":\"sha1\",\"req\":\"^0.11.0\"},{\"default_features\":false,\"name\":\"sha2\",\"req\":\"^0.11.0\"},{\"name\":\"sqlx-core\",\"req\":\"=0.9.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"thiserror\",\"req\":\"^2.0.18\"},{\"features\":[\"formatting\",\"parsing\",\"macros\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.47\"},{\"features\":[\"log\"],\"name\":\"tracing\",\"req\":\"^0.1.37\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^1.12.1\"}],\"features\":{\"any\":[\"sqlx-core/any\"],\"bigdecimal\":[\"dep:bigdecimal\",\"sqlx-core/bigdecimal\"],\"chrono\":[\"dep:chrono\",\"sqlx-core/chrono\"],\"default\":[],\"json\":[\"sqlx-core/json\",\"serde\"],\"migrate\":[\"sqlx-core/migrate\",\"dep:crc\"],\"offline\":[\"sqlx-core/offline\",\"serde/derive\",\"bitflags/serde\"],\"rsa\":[\"dep:rand\",\"dep:rsa\"],\"rust_decimal\":[\"dep:rust_decimal\",\"rust_decimal/maths\",\"sqlx-core/rust_decimal\"],\"time\":[\"dep:time\",\"sqlx-core/time\"],\"uuid\":[\"dep:uuid\",\"sqlx-core/uuid\"]}}", + "sqlx-postgres_0.9.0": "{\"dependencies\":[{\"name\":\"atoi\",\"req\":\"^2.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"base64\",\"req\":\"^0.22.1\"},{\"name\":\"bigdecimal\",\"optional\":true,\"req\":\"^0.4.0\"},{\"name\":\"bit-vec\",\"optional\":true,\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"bitflags\",\"req\":\"^2.4\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"byteorder\",\"req\":\"^1.4.3\"},{\"default_features\":false,\"features\":[\"std\",\"clock\"],\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4.34\"},{\"name\":\"crc\",\"optional\":true,\"req\":\"^3.0.0\"},{\"default_features\":false,\"name\":\"dotenvy\",\"req\":\"^0.15.7\"},{\"name\":\"etcetera\",\"req\":\"^0.11.0\",\"target\":\"cfg(target_os = \\\"windows\\\")\"},{\"default_features\":false,\"features\":[\"sink\",\"alloc\",\"std\"],\"name\":\"futures-channel\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"features\":[\"alloc\",\"sink\",\"io\"],\"name\":\"futures-util\",\"req\":\"^0.3.32\"},{\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"hkdf\",\"req\":\"^0.13.0\"},{\"default_features\":false,\"name\":\"hmac\",\"req\":\"^0.13.0\"},{\"name\":\"ipnet\",\"optional\":true,\"req\":\"^2.3.0\"},{\"name\":\"ipnetwork\",\"optional\":true,\"req\":\"^0.21.1\"},{\"name\":\"itoa\",\"req\":\"^1.0.5\"},{\"name\":\"log\",\"req\":\"^0.4.18\"},{\"name\":\"mac_address\",\"optional\":true,\"req\":\"^1.1.5\"},{\"default_features\":false,\"name\":\"md-5\",\"req\":\"^0.11.0\"},{\"default_features\":false,\"name\":\"memchr\",\"req\":\"^2.5.0\"},{\"name\":\"num-bigint\",\"optional\":true,\"req\":\"^0.4.3\"},{\"default_features\":false,\"features\":[\"thread_rng\"],\"name\":\"rand\",\"req\":\"^0.10.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rust_decimal\",\"optional\":true,\"req\":\"^1.36.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.219\"},{\"features\":[\"raw_value\"],\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.142\"},{\"default_features\":false,\"name\":\"sha2\",\"req\":\"^0.11.0\"},{\"name\":\"smallvec\",\"req\":\"^1.13.1\"},{\"name\":\"sqlx-core\",\"req\":\"=0.9.0\"},{\"name\":\"stringprep\",\"req\":\"^0.1.2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"thiserror\",\"req\":\"^2.0.18\"},{\"features\":[\"formatting\",\"parsing\",\"macros\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.47\"},{\"features\":[\"log\"],\"name\":\"tracing\",\"req\":\"^0.1.37\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^1.12.1\"},{\"default_features\":false,\"name\":\"whoami\",\"req\":\"^2.0.2\"}],\"features\":{\"any\":[\"sqlx-core/any\"],\"bigdecimal\":[\"dep:bigdecimal\",\"dep:num-bigint\",\"sqlx-core/bigdecimal\"],\"bit-vec\":[\"dep:bit-vec\",\"sqlx-core/bit-vec\"],\"chrono\":[\"dep:chrono\",\"sqlx-core/chrono\"],\"ipnet\":[\"dep:ipnet\",\"sqlx-core/ipnet\"],\"ipnetwork\":[\"dep:ipnetwork\",\"sqlx-core/ipnetwork\"],\"json\":[\"dep:serde\",\"dep:serde_json\",\"sqlx-core/json\"],\"mac_address\":[\"dep:mac_address\",\"sqlx-core/mac_address\"],\"migrate\":[\"sqlx-core/migrate\",\"dep:crc\"],\"offline\":[\"json\",\"sqlx-core/offline\",\"smallvec/serde\"],\"rust_decimal\":[\"dep:rust_decimal\",\"rust_decimal/maths\",\"sqlx-core/rust_decimal\"],\"time\":[\"dep:time\",\"sqlx-core/time\"],\"uuid\":[\"dep:uuid\",\"sqlx-core/uuid\"]}}", + "sqlx-sqlite_0.9.0": "{\"dependencies\":[{\"name\":\"atoi\",\"req\":\"^2.0\"},{\"default_features\":false,\"features\":[\"std\",\"clock\"],\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4.34\"},{\"default_features\":false,\"features\":[\"async\"],\"name\":\"flume\",\"req\":\"^0.12.0\"},{\"name\":\"form_urlencoded\",\"req\":\"^1.2.2\"},{\"default_features\":false,\"features\":[\"sink\",\"alloc\",\"std\"],\"name\":\"futures-channel\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.32\"},{\"name\":\"futures-executor\",\"req\":\"^0.3.32\"},{\"name\":\"futures-intrusive\",\"req\":\"^0.5.0\"},{\"default_features\":false,\"features\":[\"alloc\",\"sink\"],\"name\":\"futures-util\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"features\":[\"pkg-config\",\"vcpkg\"],\"name\":\"libsqlite3-sys\",\"req\":\">=0.30.1, <0.38.0\"},{\"name\":\"log\",\"req\":\"^0.4.18\"},{\"name\":\"percent-encoding\",\"req\":\"^2.3.0\"},{\"name\":\"regex\",\"optional\":true,\"req\":\"^1.6.0\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.219\"},{\"name\":\"sqlx-core\",\"req\":\"=0.9.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"thiserror\",\"req\":\"^2.0.18\"},{\"features\":[\"formatting\",\"parsing\",\"macros\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.47\"},{\"features\":[\"log\"],\"name\":\"tracing\",\"req\":\"^0.1.37\"},{\"name\":\"url\",\"req\":\"^2.2.2\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^1.12.1\"}],\"features\":{\"_unstable-all-sqlite-features\":[\"deserialize\",\"load-extension\",\"preupdate-hook\",\"unlock-notify\"],\"_unstable-all-types\":[\"json\",\"chrono\",\"time\",\"uuid\"],\"_unstable-docs\":[\"bundled\",\"any\",\"_unstable-all-types\",\"_unstable-all-sqlite-features\"],\"any\":[\"sqlx-core/any\"],\"bigdecimal\":[],\"bundled\":[\"libsqlite3-sys/bundled\"],\"chrono\":[\"dep:chrono\",\"sqlx-core/chrono\"],\"deserialize\":[],\"json\":[\"sqlx-core/json\",\"serde\"],\"load-extension\":[],\"migrate\":[\"sqlx-core/migrate\"],\"offline\":[\"sqlx-core/offline\",\"serde\"],\"preupdate-hook\":[\"libsqlite3-sys/preupdate_hook\"],\"regexp\":[\"dep:regex\"],\"rust_decimal\":[],\"sqlx-toml\":[\"sqlx-core/sqlx-toml\"],\"time\":[\"dep:time\",\"sqlx-core/time\"],\"unbundled\":[\"libsqlite3-sys/buildtime_bindgen\"],\"unlock-notify\":[\"libsqlite3-sys/unlock_notify\"],\"uuid\":[\"dep:uuid\",\"sqlx-core/uuid\"]}}", + "sqlx_0.9.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0.58\"},{\"features\":[\"attributes\"],\"kind\":\"dev\",\"name\":\"async-std\",\"req\":\"^1.13\"},{\"features\":[\"async_tokio\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"},{\"kind\":\"dev\",\"name\":\"dotenvy\",\"req\":\"^0.15.7\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"default_features\":false,\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.32\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"libsqlite3-sys\",\"req\":\"^0.37.0\"},{\"features\":[\"bundled-sqlcipher\"],\"kind\":\"dev\",\"name\":\"libsqlite3-sys\",\"req\":\"^0.37.0\",\"target\":\"cfg(sqlite_test_sqlcipher)\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1.0.6\"},{\"default_features\":false,\"features\":[\"thread_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.10.1\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.219\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.142\"},{\"features\":[\"migrate\"],\"name\":\"sqlx-core\",\"req\":\"=0.9.0\"},{\"name\":\"sqlx-macros\",\"optional\":true,\"req\":\"=0.9.0\"},{\"default_features\":false,\"name\":\"sqlx-mysql\",\"optional\":true,\"req\":\"=0.9.0\"},{\"name\":\"sqlx-postgres\",\"optional\":true,\"req\":\"=0.9.0\"},{\"name\":\"sqlx-sqlite\",\"optional\":true,\"req\":\"=0.9.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.10.1\"},{\"kind\":\"dev\",\"name\":\"time_\",\"package\":\"time\",\"req\":\"^0.3.47\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.25.0\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.53\"},{\"kind\":\"dev\",\"name\":\"url\",\"req\":\"^2.2.2\"}],\"features\":{\"_rt-async-global-executor\":[],\"_rt-async-std\":[],\"_rt-smol\":[],\"_rt-tokio\":[],\"_sqlite\":[],\"_unstable-all-types\":[\"bigdecimal\",\"rust_decimal\",\"json\",\"time\",\"chrono\",\"ipnet\",\"ipnetwork\",\"mac_address\",\"uuid\",\"bit-vec\",\"bstr\"],\"_unstable-docs\":[\"all-databases\",\"_unstable-all-types\",\"sqlx-sqlite/_unstable-docs\"],\"all-databases\":[\"mysql\",\"sqlite\",\"postgres\",\"any\"],\"any\":[\"sqlx-core/any\",\"sqlx-mysql?/any\",\"sqlx-postgres?/any\",\"sqlx-sqlite?/any\"],\"bigdecimal\":[\"sqlx-core/bigdecimal\",\"sqlx-macros?/bigdecimal\",\"sqlx-mysql?/bigdecimal\",\"sqlx-postgres?/bigdecimal\"],\"bit-vec\":[\"sqlx-core/bit-vec\",\"sqlx-macros?/bit-vec\",\"sqlx-postgres?/bit-vec\"],\"bstr\":[\"sqlx-core/bstr\"],\"chrono\":[\"sqlx-core/chrono\",\"sqlx-macros?/chrono\",\"sqlx-mysql?/chrono\",\"sqlx-postgres?/chrono\",\"sqlx-sqlite?/chrono\"],\"default\":[\"any\",\"macros\",\"migrate\",\"json\"],\"derive\":[\"sqlx-macros/derive\"],\"ipnet\":[\"sqlx-core/ipnet\",\"sqlx-macros?/ipnet\",\"sqlx-postgres?/ipnet\"],\"ipnetwork\":[\"sqlx-core/ipnetwork\",\"sqlx-macros?/ipnetwork\",\"sqlx-postgres?/ipnetwork\"],\"json\":[\"sqlx-core/json\",\"sqlx-macros?/json\",\"sqlx-mysql?/json\",\"sqlx-postgres?/json\",\"sqlx-sqlite?/json\"],\"mac_address\":[\"sqlx-core/mac_address\",\"sqlx-macros?/mac_address\",\"sqlx-postgres?/mac_address\"],\"macros\":[\"derive\",\"sqlx-macros/macros\",\"sqlx-core/offline\",\"sqlx-mysql?/offline\",\"sqlx-postgres?/offline\",\"sqlx-sqlite?/offline\"],\"migrate\":[\"sqlx-core/migrate\",\"sqlx-macros?/migrate\",\"sqlx-mysql?/migrate\",\"sqlx-postgres?/migrate\",\"sqlx-sqlite?/migrate\"],\"mysql\":[\"sqlx-mysql\",\"sqlx-macros?/mysql\"],\"mysql-rsa\":[\"mysql\",\"sqlx-mysql/rsa\",\"sqlx-macros?/mysql-rsa\"],\"postgres\":[\"sqlx-postgres\",\"sqlx-macros?/postgres\"],\"regexp\":[\"sqlx-sqlite?/regexp\"],\"runtime-async-global-executor\":[\"_rt-async-global-executor\",\"sqlx-core/_rt-async-global-executor\",\"sqlx-macros?/_rt-async-global-executor\"],\"runtime-async-std\":[\"_rt-async-std\",\"sqlx-core/_rt-async-std\",\"sqlx-macros?/_rt-async-std\"],\"runtime-smol\":[\"_rt-smol\",\"sqlx-core/_rt-smol\",\"sqlx-macros?/_rt-smol\"],\"runtime-tokio\":[\"_rt-tokio\",\"sqlx-core/_rt-tokio\",\"sqlx-macros?/_rt-tokio\"],\"rust_decimal\":[\"sqlx-core/rust_decimal\",\"sqlx-macros?/rust_decimal\",\"sqlx-mysql?/rust_decimal\",\"sqlx-postgres?/rust_decimal\"],\"sqlite\":[\"sqlite-bundled\",\"sqlite-deserialize\",\"sqlite-load-extension\",\"sqlite-unlock-notify\"],\"sqlite-bundled\":[\"_sqlite\",\"sqlx-sqlite/bundled\",\"sqlx-macros?/sqlite\"],\"sqlite-deserialize\":[\"sqlx-sqlite/deserialize\"],\"sqlite-load-extension\":[\"sqlx-sqlite/load-extension\",\"sqlx-macros?/sqlite-load-extension\"],\"sqlite-preupdate-hook\":[\"sqlx-sqlite/preupdate-hook\"],\"sqlite-unbundled\":[\"_sqlite\",\"sqlx-sqlite/unbundled\",\"sqlx-macros?/sqlite-unbundled\"],\"sqlite-unlock-notify\":[\"sqlx-sqlite/unlock-notify\"],\"sqlx-toml\":[\"sqlx-core/sqlx-toml\",\"sqlx-macros?/sqlx-toml\",\"sqlx-sqlite?/sqlx-toml\"],\"time\":[\"sqlx-core/time\",\"sqlx-macros?/time\",\"sqlx-mysql?/time\",\"sqlx-postgres?/time\",\"sqlx-sqlite?/time\"],\"tls-native-tls\":[\"sqlx-core/_tls-native-tls\",\"sqlx-macros?/_tls-native-tls\"],\"tls-none\":[],\"tls-rustls\":[\"tls-rustls-ring\"],\"tls-rustls-aws-lc-rs\":[\"sqlx-core/_tls-rustls-aws-lc-rs\",\"sqlx-macros?/_tls-rustls-aws-lc-rs\"],\"tls-rustls-ring\":[\"tls-rustls-ring-webpki\"],\"tls-rustls-ring-native-roots\":[\"sqlx-core/_tls-rustls-ring-native-roots\",\"sqlx-macros?/_tls-rustls-ring-native-roots\"],\"tls-rustls-ring-webpki\":[\"sqlx-core/_tls-rustls-ring-webpki\",\"sqlx-macros?/_tls-rustls-ring-webpki\"],\"uuid\":[\"sqlx-core/uuid\",\"sqlx-macros?/uuid\",\"sqlx-mysql?/uuid\",\"sqlx-postgres?/uuid\",\"sqlx-sqlite?/uuid\"]}}", "sse-stream_0.2.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1\"},{\"features\":[\"tracing\"],\"kind\":\"dev\",\"name\":\"axum\",\"req\":\"^0.8\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"http-body\",\"req\":\"^1\"},{\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"features\":[\"client\",\"http1\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1\"},{\"features\":[\"tokio\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"features\":[\"stream\"],\"kind\":\"dev\",\"name\":\"reqwest\",\"req\":\"^0.12\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"io\"],\"kind\":\"dev\",\"name\":\"tokio-util\",\"req\":\"^0.7\"},{\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"env-filter\",\"std\",\"fmt\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"}],\"features\":{\"default\":[],\"tracing\":[\"dep:tracing\"]}}", "stable_deref_trait_1.2.1": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", "starlark_0.13.0": "{\"dependencies\":[{\"features\":[\"bumpalo\",\"num-bigint\"],\"name\":\"allocative\",\"req\":\"^0.3.4\"},{\"name\":\"anyhow\",\"req\":\"^1.0.65\"},{\"name\":\"bumpalo\",\"req\":\"^3.8\"},{\"name\":\"cmp_any\",\"req\":\"^0.8.1\"},{\"name\":\"debugserver-types\",\"req\":\"^0.5.0\"},{\"name\":\"derivative\",\"req\":\"^2.2\"},{\"features\":[\"full\"],\"name\":\"derive_more\",\"req\":\"^1.0.0\"},{\"name\":\"display_container\",\"req\":\"^0.9.0\"},{\"name\":\"dupe\",\"req\":\"^0.9.0\"},{\"name\":\"either\",\"req\":\"^1.8\"},{\"name\":\"erased-serde\",\"req\":\"^0.3.12\"},{\"features\":[\"raw\"],\"name\":\"hashbrown\",\"req\":\"^0.14.3\"},{\"name\":\"inventory\",\"req\":\"^0.3.8\"},{\"name\":\"itertools\",\"req\":\"^0.13.0\"},{\"name\":\"maplit\",\"req\":\"^1.0.2\"},{\"name\":\"memoffset\",\"req\":\"^0.6.4\"},{\"name\":\"num-bigint\",\"req\":\"^0.4.3\"},{\"name\":\"num-traits\",\"req\":\"^0.2\"},{\"name\":\"once_cell\",\"req\":\"^1.8\"},{\"name\":\"paste\",\"req\":\"^1.0\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.4\"},{\"name\":\"ref-cast\",\"req\":\"^1.0.18\"},{\"name\":\"regex\",\"req\":\"^1.5.4\"},{\"name\":\"rustyline\",\"req\":\"^14.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"starlark_derive\",\"req\":\"^0.13.0\"},{\"name\":\"starlark_map\",\"req\":\"^0.13.0\"},{\"name\":\"starlark_syntax\",\"req\":\"^0.13.0\"},{\"name\":\"static_assertions\",\"req\":\"^1.1.0\"},{\"name\":\"strsim\",\"req\":\"^0.10.0\"},{\"name\":\"textwrap\",\"req\":\"^0.11\"},{\"name\":\"thiserror\",\"req\":\"^1.0.36\"}],\"features\":{}}", @@ -1659,7 +1664,7 @@ "ts-rs_11.1.0": "{\"dependencies\":[{\"features\":[\"serde\"],\"name\":\"bigdecimal\",\"optional\":true,\"req\":\">=0.0.13, <0.5\"},{\"name\":\"bson\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"bytes\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4\"},{\"features\":[\"serde\"],\"kind\":\"dev\",\"name\":\"chrono\",\"req\":\"^0.4\"},{\"name\":\"dprint-plugin-typescript\",\"optional\":true,\"req\":\"=0.95\"},{\"name\":\"heapless\",\"optional\":true,\"req\":\">=0.7, <0.9\"},{\"name\":\"indexmap\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"ordered-float\",\"optional\":true,\"req\":\">=3, <6\"},{\"name\":\"semver\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"name\":\"smol_str\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"thiserror\",\"req\":\"^2\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"sync\",\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.40\"},{\"name\":\"ts-rs-macros\",\"req\":\"=11.1.0\"},{\"name\":\"url\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"bigdecimal-impl\":[\"bigdecimal\"],\"bson-uuid-impl\":[\"bson\"],\"bytes-impl\":[\"bytes\"],\"chrono-impl\":[\"chrono\"],\"default\":[\"serde-compat\"],\"format\":[\"dprint-plugin-typescript\"],\"heapless-impl\":[\"heapless\"],\"import-esm\":[],\"indexmap-impl\":[\"indexmap\"],\"no-serde-warnings\":[\"ts-rs-macros/no-serde-warnings\"],\"ordered-float-impl\":[\"ordered-float\"],\"semver-impl\":[\"semver\"],\"serde-compat\":[\"ts-rs-macros/serde-compat\"],\"serde-json-impl\":[\"serde_json\"],\"smol_str-impl\":[\"smol_str\"],\"tokio-impl\":[\"tokio\"],\"url-impl\":[\"url\"],\"uuid-impl\":[\"uuid\"]}}", "two-face_0.5.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"cargo-lock\",\"req\":\"^10.1.0\"},{\"kind\":\"dev\",\"name\":\"insta\",\"req\":\"^1.44.3\"},{\"default_features\":false,\"features\":[\"read\"],\"kind\":\"dev\",\"name\":\"object\",\"req\":\"^0.36.7\"},{\"name\":\"serde\",\"req\":\"^1.0.228\"},{\"name\":\"serde_derive\",\"req\":\"^1.0.228\"},{\"kind\":\"dev\",\"name\":\"similar\",\"req\":\"^2.7.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"strum\",\"req\":\"^0.26.3\"},{\"default_features\":false,\"features\":[\"dump-load\",\"parsing\"],\"name\":\"syntect\",\"req\":\"^5.3.0\"},{\"default_features\":false,\"features\":[\"html\"],\"kind\":\"dev\",\"name\":\"syntect\",\"req\":\"^5.3.0\"},{\"kind\":\"dev\",\"name\":\"toml\",\"req\":\"^0.8.23\"},{\"default_features\":false,\"features\":[\"std\",\"xxhash64\"],\"kind\":\"dev\",\"name\":\"twox-hash\",\"req\":\"^2.1.2\"}],\"features\":{\"default\":[\"syntect-onig\"],\"syntect-default-fancy\":[\"syntect-fancy\",\"syntect/default-fancy\"],\"syntect-default-onig\":[\"syntect-onig\",\"syntect/default-onig\"],\"syntect-fancy\":[\"syntect/regex-fancy\"],\"syntect-onig\":[\"syntect/regex-onig\"]}}", "type-map_0.5.1": "{\"dependencies\":[{\"name\":\"rustc-hash\",\"req\":\"^2\"}],\"features\":{}}", - "typenum_1.19.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"scale-info\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"const-generics\":[],\"force_unix_path_separator\":[],\"i128\":[],\"no_std\":[],\"scale_info\":[\"scale-info/derive\"],\"strict\":[]}}", + "typenum_1.20.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"scale-info\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"const-generics\":[],\"i128\":[],\"scale_info\":[\"scale-info/derive\"],\"strict\":[]}}", "uds_windows_1.1.0": "{\"dependencies\":[{\"name\":\"memoffset\",\"req\":\"^0.9.0\"},{\"name\":\"tempfile\",\"req\":\"^3\",\"target\":\"cfg(windows)\"},{\"features\":[\"winsock2\",\"ws2def\",\"minwinbase\",\"ntdef\",\"processthreadsapi\",\"handleapi\",\"ws2tcpip\",\"winbase\"],\"name\":\"winapi\",\"req\":\"^0.3.9\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "uname_0.1.1": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2\"}],\"features\":{}}", "unarray_0.1.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"test-strategy\",\"req\":\"^0.2\"}],\"features\":{}}", @@ -1725,12 +1730,12 @@ "web-time_1.1.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"futures-channel\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_feature = \\\"atomics\\\"))\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_feature = \\\"atomics\\\"))\"},{\"features\":[\"js\"],\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.2\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"name\":\"js-sys\",\"req\":\"^0.3.20\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"features\":[\"macro\"],\"kind\":\"dev\",\"name\":\"pollster\",\"req\":\"^0.3\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"wasm-bindgen\",\"req\":\"^0.2.70\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-futures\",\"req\":\"^0.4\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"features\":[\"WorkerGlobalScope\"],\"kind\":\"dev\",\"name\":\"web-sys\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_feature = \\\"atomics\\\"))\"},{\"features\":[\"CssStyleDeclaration\",\"Document\",\"Element\",\"HtmlTableElement\",\"HtmlTableRowElement\",\"Performance\",\"Window\"],\"kind\":\"dev\",\"name\":\"web-sys\",\"req\":\"^0.3\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"}],\"features\":{\"serde\":[\"dep:serde\"]}}", "webbrowser_1.0.6": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"actix-files\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"actix-web\",\"req\":\"^4\"},{\"name\":\"core-foundation\",\"req\":\"^0.10\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"kind\":\"dev\",\"name\":\"crossbeam-channel\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.9.0\"},{\"name\":\"jni\",\"req\":\"^0.21\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"ndk-context\",\"req\":\"^0.1\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"kind\":\"dev\",\"name\":\"ndk-glue\",\"req\":\">=0.3, <=0.7\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"objc2\",\"req\":\"^0.6\",\"target\":\"cfg(any(target_os = \\\"ios\\\", target_os = \\\"tvos\\\", target_os = \\\"visionos\\\"))\"},{\"default_features\":false,\"features\":[\"std\",\"NSDictionary\",\"NSString\",\"NSURL\"],\"name\":\"objc2-foundation\",\"req\":\"^0.3\",\"target\":\"cfg(any(target_os = \\\"ios\\\", target_os = \\\"tvos\\\", target_os = \\\"visionos\\\"))\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^0.10\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"name\":\"url\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"urlencoding\",\"req\":\"^2.1\"},{\"features\":[\"Window\"],\"name\":\"web-sys\",\"req\":\"^0.3\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"}],\"features\":{\"disable-wsl\":[],\"hardened\":[],\"wasm-console\":[\"web-sys/console\"]}}", "webpki-root-certs_1.0.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"percent-encoding\",\"req\":\"^2.3\"},{\"default_features\":false,\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.8\"},{\"kind\":\"dev\",\"name\":\"ring\",\"req\":\"^0.17.0\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.17.0\"}],\"features\":{}}", - "webpki-roots_0.26.11": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"parent\",\"package\":\"webpki-roots\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"percent-encoding\",\"req\":\"^2.3\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.8\"},{\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.13\"},{\"kind\":\"dev\",\"name\":\"ring\",\"req\":\"^0.17.0\"},{\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.102\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.17.0\"},{\"kind\":\"dev\",\"name\":\"yasna\",\"req\":\"^0.5.2\"}],\"features\":{}}", "webpki-roots_1.0.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"percent-encoding\",\"req\":\"^2.3\"},{\"default_features\":false,\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.8\"},{\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14.3\"},{\"kind\":\"dev\",\"name\":\"ring\",\"req\":\"^0.17.0\"},{\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.17.0\"},{\"kind\":\"dev\",\"name\":\"yasna\",\"req\":\"^0.5.2\"}],\"features\":{}}", "weezl_0.1.12": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures\",\"optional\":true,\"req\":\"^0.3.12\"},{\"default_features\":false,\"features\":[\"macros\",\"io-util\",\"net\",\"rt\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"compat\"],\"kind\":\"dev\",\"name\":\"tokio-util\",\"req\":\"^0.6.2\"}],\"features\":{\"alloc\":[],\"async\":[\"futures\",\"std\"],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", "which_6.0.3": "{\"dependencies\":[{\"name\":\"either\",\"req\":\"^1.9.0\"},{\"name\":\"home\",\"req\":\"^0.5.9\",\"target\":\"cfg(any(windows, unix, target_os = \\\"redox\\\"))\"},{\"name\":\"regex\",\"optional\":true,\"req\":\"^1.10.2\"},{\"default_features\":false,\"features\":[\"fs\",\"std\"],\"name\":\"rustix\",\"req\":\"^0.38.30\",\"target\":\"cfg(any(unix, target_os = \\\"wasi\\\", target_os = \\\"redox\\\"))\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.9.0\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.40\"},{\"features\":[\"kernel\"],\"name\":\"winsafe\",\"req\":\"^0.0.19\",\"target\":\"cfg(windows)\"}],\"features\":{\"regex\":[\"dep:regex\"],\"tracing\":[\"dep:tracing\"]}}", "which_8.0.0": "{\"dependencies\":[{\"name\":\"env_home\",\"optional\":true,\"req\":\"^0.1.0\",\"target\":\"cfg(any(windows, unix, target_os = \\\"redox\\\"))\"},{\"name\":\"regex\",\"optional\":true,\"req\":\"^1.10.2\"},{\"default_features\":false,\"features\":[\"fs\",\"std\"],\"name\":\"rustix\",\"optional\":true,\"req\":\"^1.0.5\",\"target\":\"cfg(any(unix, target_os = \\\"wasi\\\", target_os = \\\"redox\\\"))\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.9.0\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.40\"},{\"features\":[\"kernel\"],\"name\":\"winsafe\",\"optional\":true,\"req\":\"^0.0.19\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"real-sys\"],\"real-sys\":[\"dep:env_home\",\"dep:rustix\",\"dep:winsafe\"],\"regex\":[\"dep:regex\"],\"tracing\":[\"dep:tracing\"]}}", "whoami_1.6.1": "{\"dependencies\":[{\"name\":\"libredox\",\"req\":\"^0.1.1\",\"target\":\"cfg(all(target_os = \\\"redox\\\", not(target_arch = \\\"wasm32\\\")))\"},{\"name\":\"wasite\",\"req\":\"^0.1\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"wasi\\\"))\"},{\"features\":[\"Navigator\",\"Document\",\"Window\",\"Location\"],\"name\":\"web-sys\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", not(target_os = \\\"wasi\\\"), not(daku)))\"}],\"features\":{\"default\":[\"web\"],\"web\":[\"web-sys\"]}}", + "whoami_2.1.2": "{\"dependencies\":[{\"default_features\":false,\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.186\",\"target\":\"cfg(all(any(target_vendor = \\\"apple\\\", target_os = \\\"linux\\\", target_os = \\\"dragonfly\\\", target_os = \\\"freebsd\\\", target_os = \\\"netbsd\\\", target_os = \\\"openbsd\\\", target_os = \\\"illumos\\\", target_os = \\\"hurd\\\"), not(target_arch = \\\"wasm32\\\")))\"},{\"default_features\":false,\"features\":[\"call\"],\"name\":\"libredox\",\"optional\":true,\"req\":\"^0.1.16\",\"target\":\"cfg(all(target_os = \\\"redox\\\", not(target_arch = \\\"wasm32\\\")))\"},{\"default_features\":false,\"features\":[\"SCDynamicStore\",\"SCDynamicStoreCopySpecific\"],\"name\":\"objc2-system-configuration\",\"optional\":true,\"req\":\"^0.3.2\",\"target\":\"cfg(target_vendor = \\\"apple\\\")\"},{\"default_features\":false,\"name\":\"wasite\",\"optional\":true,\"req\":\"^1.0.2\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"wasi\\\"))\"},{\"default_features\":false,\"features\":[\"Navigator\",\"Document\",\"Window\",\"Location\"],\"name\":\"web-sys\",\"optional\":true,\"req\":\"^0.3.85\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", not(target_os = \\\"wasi\\\"), not(daku)))\"}],\"features\":{\"default\":[\"std\",\"wasi-wasite\",\"wasm-web\"],\"force-stub\":[],\"std\":[\"dep:libc\",\"dep:libredox\",\"dep:objc2-system-configuration\",\"libc?/std\",\"web-sys?/std\"],\"wasi-wasite\":[\"dep:wasite\"],\"wasm-web\":[\"dep:web-sys\"]}}", "widestring_1.2.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"debugger_test\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"debugger_test_parser\",\"req\":\"^0.1\"},{\"features\":[\"Win32_System_Diagnostics_Debug\"],\"kind\":\"dev\",\"name\":\"windows-sys\",\"req\":\"^0.59\"}],\"features\":{\"alloc\":[],\"debugger_visualizer\":[\"alloc\"],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", "wildcard_0.3.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.10.5\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.203\"},{\"default_features\":false,\"name\":\"thiserror\",\"req\":\"^2.0.3\"},{\"kind\":\"dev\",\"name\":\"toml\",\"req\":\"^0.8.14\"},{\"kind\":\"dev\",\"name\":\"wildmatch\",\"req\":\"^2.3.4\"}],\"features\":{\"fatal-warnings\":[]}}", "wildmatch_2.6.1": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3.1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"ntest\",\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.10.2\"},{\"kind\":\"dev\",\"name\":\"regex-lite\",\"req\":\"^0.1.5\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"serde\":[\"dep:serde\"]}}", diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 1b1bc0e59a..33e81b0f44 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -181,7 +181,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "generic-array", ] @@ -193,7 +193,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -207,7 +207,7 @@ dependencies = [ "bech32", "chacha20poly1305", "cookie-factory", - "hmac", + "hmac 0.12.1", "i18n-embed", "i18n-embed-fl", "lazy_static", @@ -216,7 +216,7 @@ dependencies = [ "rand 0.8.5", "rust-embed", "scrypt", - "sha2", + "sha2 0.10.9", "subtle", "x25519-dalek", "zeroize", @@ -231,12 +231,12 @@ dependencies = [ "base64 0.21.7", "chacha20poly1305", "cookie-factory", - "hkdf", + "hkdf 0.12.4", "io_tee", "nom 7.1.3", "rand 0.8.5", "secrecy", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -776,7 +776,7 @@ dependencies = [ "p256", "rand 0.8.5", "ring", - "sha2", + "sha2 0.10.9", "time", "tokio", "tracing", @@ -946,11 +946,11 @@ dependencies = [ "bytes", "form_urlencoded", "hex", - "hmac", + "hmac 0.12.1", "http 0.2.12", "http 1.4.0", "percent-encoding", - "sha2", + "sha2 0.10.9", "time", "tracing", ] @@ -1164,7 +1164,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sha1", + "sha1 0.10.6", "sync_wrapper", "tokio", "tokio-tungstenite", @@ -1328,7 +1328,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -1340,6 +1340,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block-padding" version = "0.3.3" @@ -1613,7 +1622,18 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", ] [[package]] @@ -1623,7 +1643,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", - "chacha20", + "chacha20 0.9.1", "cipher", "poly1305", "zeroize", @@ -1666,7 +1686,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "inout", "zeroize", ] @@ -1759,6 +1779,12 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" + [[package]] name = "cmp_any" version = "0.8.1" @@ -1807,7 +1833,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -1825,7 +1851,7 @@ dependencies = [ "pretty_assertions", "serde", "serde_json", - "sha1", + "sha1 0.10.6", "tokio", "tracing", ] @@ -1933,7 +1959,7 @@ dependencies = [ "core_test_support", "flate2", "futures", - "hmac", + "hmac 0.12.1", "opentelemetry", "opentelemetry_sdk", "pretty_assertions", @@ -1942,7 +1968,7 @@ dependencies = [ "serde", "serde_json", "serial_test", - "sha2", + "sha2 0.10.9", "shlex", "tar", "tempfile", @@ -2003,7 +2029,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tempfile", "tokio", "tokio-tungstenite", @@ -2079,13 +2105,13 @@ dependencies = [ "constant_time_eq 0.3.1", "futures", "gethostname", - "hmac", + "hmac 0.12.1", "jsonwebtoken", "owo-colors", "pretty_assertions", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tempfile", "time", "tokio", @@ -2333,11 +2359,11 @@ dependencies = [ "codex-otel", "codex-protocol", "codex-utils-absolute-path", - "hmac", + "hmac 0.12.1", "pretty_assertions", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tempfile", "thiserror 2.0.18", "tokio", @@ -2455,7 +2481,7 @@ dependencies = [ "serde_ignored", "serde_json", "serde_path_to_error", - "sha2", + "sha2 0.10.9", "tempfile", "thiserror 2.0.18", "tokio", @@ -2480,7 +2506,7 @@ dependencies = [ "pretty_assertions", "serde", "serde_json", - "sha1", + "sha1 0.10.6", "tempfile", "tokio", "tracing", @@ -2581,7 +2607,7 @@ dependencies = [ "serde", "serde_json", "serial_test", - "sha1", + "sha1 0.10.6", "shlex", "similar", "tempfile", @@ -2601,7 +2627,7 @@ dependencies = [ "uuid", "walkdir", "which 8.0.0", - "whoami", + "whoami 1.6.1", "wiremock", "zstd 0.13.3", ] @@ -2871,7 +2897,7 @@ dependencies = [ "codex-utils-output-truncation", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tempfile", ] @@ -3052,7 +3078,7 @@ dependencies = [ "seccompiler", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tempfile", "tokio", "url", @@ -3102,7 +3128,7 @@ dependencies = [ "serde", "serde_json", "serial_test", - "sha2", + "sha2 0.10.9", "tempfile", "thiserror 2.0.18", "tiny_http", @@ -3137,7 +3163,7 @@ dependencies = [ "rmcp", "serde", "serde_json", - "sha1", + "sha1 0.10.6", "tempfile", "thiserror 2.0.18", "tokio", @@ -3523,7 +3549,7 @@ dependencies = [ "serde", "serde_json", "serial_test", - "sha2", + "sha2 0.10.9", "sse-stream", "tempfile", "thiserror 2.0.18", @@ -3614,7 +3640,7 @@ dependencies = [ "schemars 0.8.22", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tempfile", "tracing", ] @@ -3857,7 +3883,7 @@ dependencies = [ "serde", "serde_json", "serial_test", - "sha2", + "sha2 0.10.9", "shlex", "strum 0.27.2", "strum_macros 0.28.0", @@ -3925,7 +3951,7 @@ name = "codex-utils-cache" version = "0.0.0" dependencies = [ "lru 0.16.3", - "sha1", + "sha1 0.10.6", "tokio", ] @@ -4262,7 +4288,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "proptest", "serde_core", ] @@ -4490,6 +4516,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.4.0" @@ -4617,6 +4652,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "crypto_box" version = "0.9.1" @@ -4694,6 +4738,15 @@ version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -4701,9 +4754,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", - "digest", + "digest 0.10.7", "fiat-crypto", "rustc_version", "subtle", @@ -4935,10 +4988,10 @@ dependencies = [ "cbc", "dbus", "fastrand", - "hkdf", + "hkdf 0.12.4", "num", "once_cell", - "sha2", + "sha2 0.10.9", "zeroize", ] @@ -5128,12 +5181,23 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", "const-oid", - "crypto-common", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.0", + "crypto-common 0.2.2", + "ctutils", +] + [[package]] name = "diplomat" version = "0.15.0" @@ -5351,7 +5415,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", - "digest", + "digest 0.10.7", "elliptic-curve", "rfc6979", "signature", @@ -5377,7 +5441,7 @@ dependencies = [ "curve25519-dalek", "ed25519", "serde", - "sha2", + "sha2 0.10.9", "subtle", "zeroize", ] @@ -5399,7 +5463,7 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "digest", + "digest 0.10.7", "ff", "generic-array", "group", @@ -5548,13 +5612,12 @@ checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" [[package]] name = "etcetera" -version = "0.8.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" dependencies = [ "cfg-if", - "home", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -5810,17 +5873,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "flume" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" -dependencies = [ - "futures-core", - "futures-sink", - "spin", -] - [[package]] name = "flume" version = "0.12.0" @@ -5927,9 +5979,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -5937,15 +5989,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -5965,9 +6017,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -5984,9 +6036,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -5995,21 +6047,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -6019,7 +6071,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -6099,11 +6150,25 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasip2", + "wasip3", +] + [[package]] name = "gif" version = "0.14.1" @@ -7161,11 +7226,11 @@ dependencies = [ [[package]] name = "hashlink" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" dependencies = [ - "hashbrown 0.15.5", + "hashbrown 0.16.1", ] [[package]] @@ -7180,7 +7245,7 @@ dependencies = [ "http 1.4.0", "httpdate", "mime", - "sha1", + "sha1 0.10.6", ] [[package]] @@ -7278,7 +7343,16 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", +] + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac 0.13.0", ] [[package]] @@ -7287,7 +7361,16 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", ] [[package]] @@ -7383,6 +7466,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.8.1" @@ -7421,7 +7513,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.5", + "webpki-roots", ] [[package]] @@ -7738,6 +7830,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -8294,9 +8392,12 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" @@ -8342,9 +8443,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.30.1" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" dependencies = [ "cc", "pkg-config", @@ -8648,12 +8749,12 @@ dependencies = [ [[package]] name = "md-5" -version = "0.10.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" dependencies = [ "cfg-if", - "digest", + "digest 0.11.3", ] [[package]] @@ -8983,22 +9084,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-bigint-dig" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" -dependencies = [ - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand 0.8.5", - "smallvec", - "zeroize", -] - [[package]] name = "num-complex" version = "0.4.6" @@ -9063,7 +9148,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -9122,7 +9206,7 @@ dependencies = [ "serde", "serde_json", "serde_path_to_error", - "sha2", + "sha2 0.10.9", "thiserror 1.0.69", "url", ] @@ -9609,7 +9693,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -9731,10 +9815,10 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ - "digest", - "hmac", + "digest 0.10.7", + "hmac 0.12.1", "password-hash", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -9743,8 +9827,8 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "digest", - "hmac", + "digest 0.10.7", + "hmac 0.12.1", ] [[package]] @@ -9845,17 +9929,6 @@ dependencies = [ "futures-io", ] -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der", - "pkcs8", - "spki", -] - [[package]] name = "pkcs8" version = "0.10.2" @@ -9918,7 +9991,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -10155,7 +10228,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes", - "heck 0.5.0", + "heck 0.4.1", "itertools 0.11.0", "log", "multimap", @@ -10175,7 +10248,7 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" dependencies = [ - "heck 0.5.0", + "heck 0.4.1", "itertools 0.14.0", "log", "multimap", @@ -10371,6 +10444,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radix_trie" version = "0.2.1" @@ -10534,7 +10613,7 @@ dependencies = [ "rama-utils", "rand 0.9.3", "serde", - "sha1", + "sha1 0.10.6", ] [[package]] @@ -10587,7 +10666,7 @@ checksum = "b28ee9e1e5d39264414b71f5c33e7fbb66b382c3fac456fe0daad39cf5509933" dependencies = [ "ahash", "const_format", - "flume 0.12.0", + "flume", "hex", "ipnet", "itertools 0.14.0", @@ -10602,7 +10681,7 @@ dependencies = [ "rama-macros", "rama-utils", "serde", - "sha2", + "sha2 0.10.9", "socket2 0.6.2", "tokio", ] @@ -10655,7 +10734,7 @@ dependencies = [ "rustls-pki-types", "tokio", "tokio-rustls", - "webpki-roots 1.0.5", + "webpki-roots", "x509-parser", ] @@ -10722,6 +10801,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20 0.10.0", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -10760,6 +10850,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rand_xorshift" version = "0.4.0" @@ -10981,7 +11077,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.5", + "webpki-roots", ] [[package]] @@ -11006,7 +11102,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac", + "hmac 0.12.1", "subtle", ] @@ -11073,26 +11169,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "rsa" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" -dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core 0.6.4", - "signature", - "spki", - "subtle", - "zeroize", -] - [[package]] name = "rtrb" version = "0.3.3" @@ -11134,7 +11210,7 @@ version = "8.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" dependencies = [ - "sha2", + "sha2 0.10.9", "walkdir", ] @@ -11460,7 +11536,7 @@ checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" dependencies = [ "pbkdf2 0.12.2", "salsa20", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -11511,12 +11587,12 @@ dependencies = [ "cbc", "futures-util", "generic-array", - "hkdf", + "hkdf 0.12.4", "num", "once_cell", "rand 0.8.5", "serde", - "sha2", + "sha2 0.10.9", "zbus", ] @@ -11907,8 +11983,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -11917,8 +12004,8 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" dependencies = [ - "digest", - "sha1", + "digest 0.10.7", + "sha1 0.10.6", ] [[package]] @@ -11934,8 +12021,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -12006,7 +12104,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] @@ -12118,9 +12216,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" dependencies = [ "sqlx-core", "sqlx-macros", @@ -12131,12 +12229,13 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" dependencies = [ "base64 0.22.1", "bytes", + "cfg-if", "chrono", "crc", "crossbeam-queue", @@ -12146,17 +12245,16 @@ dependencies = [ "futures-intrusive", "futures-io", "futures-util", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "hashlink", "indexmap 2.13.0", "log", "memchr", - "once_cell", "percent-encoding", "rustls", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "thiserror 2.0.18", "time", @@ -12165,14 +12263,14 @@ dependencies = [ "tracing", "url", "uuid", - "webpki-roots 0.26.11", + "webpki-roots", ] [[package]] name = "sqlx-macros" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" dependencies = [ "proc-macro2", "quote", @@ -12183,79 +12281,64 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" dependencies = [ + "cfg-if", "dotenvy", "either", "heck 0.5.0", "hex", - "once_cell", "proc-macro2", "quote", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sqlx-core", "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", "syn 2.0.114", + "thiserror 2.0.18", "tokio", "url", ] [[package]] name = "sqlx-mysql" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" dependencies = [ - "atoi", - "base64 0.22.1", "bitflags 2.10.0", "byteorder", "bytes", "chrono", "crc", - "digest", + "digest 0.11.3", "dotenvy", "either", - "futures-channel", "futures-core", - "futures-io", "futures-util", "generic-array", - "hex", - "hkdf", - "hmac", - "itoa", "log", - "md-5", - "memchr", - "once_cell", "percent-encoding", - "rand 0.8.5", - "rsa", "serde", - "sha1", - "sha2", - "smallvec", + "sha1 0.11.0", + "sha2 0.11.0", "sqlx-core", - "stringprep", "thiserror 2.0.18", "time", "tracing", "uuid", - "whoami", ] [[package]] name = "sqlx-postgres" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" dependencies = [ "atoi", "base64 0.22.1", @@ -12269,18 +12352,16 @@ dependencies = [ "futures-core", "futures-util", "hex", - "hkdf", - "hmac", - "home", + "hkdf 0.13.0", + "hmac 0.13.0", "itoa", "log", "md-5", "memchr", - "once_cell", - "rand 0.8.5", + "rand 0.10.1", "serde", "serde_json", - "sha2", + "sha2 0.11.0", "smallvec", "sqlx-core", "stringprep", @@ -12288,18 +12369,19 @@ dependencies = [ "time", "tracing", "uuid", - "whoami", + "whoami 2.1.2", ] [[package]] name = "sqlx-sqlite" -version = "0.8.6" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" dependencies = [ "atoi", "chrono", - "flume 0.11.1", + "flume", + "form_urlencoded", "futures-channel", "futures-core", "futures-executor", @@ -12309,7 +12391,6 @@ dependencies = [ "log", "percent-encoding", "serde", - "serde_urlencoded", "sqlx-core", "thiserror 2.0.18", "time", @@ -12707,7 +12788,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix 1.1.4", "windows-sys 0.61.2", @@ -13540,7 +13621,7 @@ dependencies = [ "rand 0.9.3", "rustls", "rustls-pki-types", - "sha1", + "sha1 0.10.6", "thiserror 2.0.18", "utf-8", ] @@ -13567,9 +13648,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "uds_windows" @@ -13702,7 +13783,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "subtle", ] @@ -13914,6 +13995,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasite" version = "0.1.0" @@ -13979,6 +14069,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -13992,6 +14104,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.10.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + [[package]] name = "wayland-backend" version = "0.3.12" @@ -14107,15 +14231,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.5", -] - [[package]] name = "webpki-roots" version = "1.0.5" @@ -14193,6 +14308,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "whoami" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" + [[package]] name = "widestring" version = "1.2.1" @@ -14824,6 +14945,88 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.114", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.114", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.10.0", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "wl-clipboard-rs" @@ -14996,7 +15199,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_repr", - "sha1", + "sha1 0.10.6", "static_assertions", "tracing", "uds_windows", @@ -15140,9 +15343,9 @@ dependencies = [ "crc32fast", "crossbeam-utils", "flate2", - "hmac", + "hmac 0.12.1", "pbkdf2 0.11.0", - "sha1", + "sha1 0.10.6", "time", "zstd 0.11.2+zstd.1.5.2", ] @@ -15163,12 +15366,12 @@ dependencies = [ "displaydoc", "flate2", "getrandom 0.3.4", - "hmac", + "hmac 0.12.1", "indexmap 2.13.0", "lzma-rs", "memchr", "pbkdf2 0.12.2", - "sha1", + "sha1 0.10.6", "thiserror 2.0.18", "time", "xz2", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 6c9737e4ad..c85a55890c 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -366,13 +366,14 @@ sha2 = "0.10" shlex = "1.3.0" similar = "2.7.0" socket2 = "0.6.1" -sqlx = { version = "0.8.6", default-features = false, features = [ +sqlx = { version = "0.9.0", default-features = false, features = [ "chrono", "json", "macros", "migrate", - "runtime-tokio-rustls", - "sqlite", + "runtime-tokio", + "tls-rustls", + "sqlite-bundled", "time", "uuid", ] } diff --git a/codex-rs/state/src/migrations.rs b/codex-rs/state/src/migrations.rs index 641d584530..0e12f17c45 100644 --- a/codex-rs/state/src/migrations.rs +++ b/codex-rs/state/src/migrations.rs @@ -19,6 +19,8 @@ fn runtime_migrator(base: &'static Migrator) -> Migrator { ignore_missing: true, locking: base.locking, no_tx: base.no_tx, + table_name: base.table_name.clone(), + create_schemas: base.create_schemas.clone(), } } diff --git a/codex-rs/state/src/runtime/goals.rs b/codex-rs/state/src/runtime/goals.rs index 38a64d0845..aa0e90462b 100644 --- a/codex-rs/state/src/runtime/goals.rs +++ b/codex-rs/state/src/runtime/goals.rs @@ -399,44 +399,78 @@ WHERE thread_id = ? } let now_ms = datetime_to_epoch_millis(Utc::now()); + let active_or_stopped_status_filter = + "status IN ('active', 'paused', 'blocked', 'usage_limited', 'budget_limited')"; let status_filter = match mode { GoalAccountingMode::ActiveStatusOnly => "status = 'active'", GoalAccountingMode::ActiveOnly => "status IN ('active', 'budget_limited')", GoalAccountingMode::ActiveOrComplete => { "status IN ('active', 'budget_limited', 'complete')" } - GoalAccountingMode::ActiveOrStopped => { - "status IN ('active', 'paused', 'blocked', 'usage_limited', 'budget_limited')" - } + GoalAccountingMode::ActiveOrStopped => active_or_stopped_status_filter, }; let budget_limit_status_filter = match mode { GoalAccountingMode::ActiveStatusOnly | GoalAccountingMode::ActiveOnly | GoalAccountingMode::ActiveOrComplete => "status = 'active'", - GoalAccountingMode::ActiveOrStopped => { - "status IN ('active', 'paused', 'blocked', 'usage_limited', 'budget_limited')" - } + GoalAccountingMode::ActiveOrStopped => active_or_stopped_status_filter, }; - let goal_id_filter = if expected_goal_id.is_some() { - "goal_id = ?" - } else { - "1 = 1" - }; - let query = format!( + let mut builder = QueryBuilder::::new( r#" UPDATE thread_goals SET - time_used_seconds = time_used_seconds + ?, - tokens_used = tokens_used + ?, + time_used_seconds = time_used_seconds + + "#, + ); + builder.push_bind(time_delta_seconds); + builder.push( + r#", + tokens_used = tokens_used + + "#, + ); + builder.push_bind(token_delta); + builder.push( + r#", status = CASE - WHEN {budget_limit_status_filter} AND token_budget IS NOT NULL AND tokens_used + ? >= token_budget - THEN ? + WHEN + "#, + ); + builder.push(budget_limit_status_filter); + builder.push( + r#" + AND token_budget IS NOT NULL + AND tokens_used + + "#, + ); + builder.push_bind(token_delta); + builder.push( + r#" + >= token_budget + THEN + "#, + ); + builder.push_bind(crate::ThreadGoalStatus::BudgetLimited.as_str()); + builder.push( + r#" ELSE status END, - updated_at_ms = ? -WHERE thread_id = ? - AND {status_filter} - AND {goal_id_filter} + updated_at_ms = + "#, + ); + builder.push_bind(now_ms); + builder.push( + r#" +WHERE thread_id = + "#, + ); + builder.push_bind(thread_id.to_string()); + builder.push(" AND "); + builder.push(status_filter); + if let Some(expected_goal_id) = expected_goal_id { + builder.push(" AND goal_id = ").push_bind(expected_goal_id); + } + builder.push( + r#" RETURNING thread_id, goal_id, @@ -450,18 +484,7 @@ RETURNING "#, ); - let mut query = sqlx::query(&query) - .bind(time_delta_seconds) - .bind(token_delta) - .bind(token_delta) - .bind(crate::ThreadGoalStatus::BudgetLimited.as_str()) - .bind(now_ms) - .bind(thread_id.to_string()); - if let Some(expected_goal_id) = expected_goal_id { - query = query.bind(expected_goal_id); - } - - let row = query.fetch_optional(self.pool.as_ref()).await?; + let row = builder.build().fetch_optional(self.pool.as_ref()).await?; let Some(row) = row else { return Ok(GoalAccountingOutcome::Unchanged( diff --git a/codex-rs/state/src/runtime/logs.rs b/codex-rs/state/src/runtime/logs.rs index e21df25e90..75b9ce82b2 100644 --- a/codex-rs/state/src/runtime/logs.rs +++ b/codex-rs/state/src/runtime/logs.rs @@ -343,11 +343,23 @@ WHERE id IN ( let max_bytes = usize::try_from(LOG_PARTITION_SIZE_LIMIT_BYTES).unwrap_or(usize::MAX); // Bound the fetched rows in SQL first so over-retained partitions do not have to load // every row into memory, then apply the exact whole-line byte cap after formatting. - let requested_threads = vec!["(?)"; thread_ids.len()].join(", "); - let query = format!( + let mut builder = QueryBuilder::::new( r#" WITH requested_threads(thread_id) AS ( - VALUES {requested_threads} + VALUES + "#, + ); + { + let mut separated = builder.separated(", "); + for thread_id in thread_ids { + separated + .push("(") + .push_bind_unseparated(*thread_id) + .push_unseparated(")"); + } + } + builder.push( + r#" ), latest_processes AS ( SELECT ( @@ -388,16 +400,13 @@ bounded_feedback_logs AS ( ) SELECT ts, ts_nanos, level, feedback_log_body FROM bounded_feedback_logs -WHERE cumulative_estimated_bytes <= ? -ORDER BY ts DESC, ts_nanos DESC, id DESC -"# +WHERE cumulative_estimated_bytes <= +"#, ); - let mut sql = sqlx::query_as::<_, FeedbackLogRow>(query.as_str()); - for thread_id in thread_ids { - sql = sql.bind(thread_id); - } - let rows = sql - .bind(LOG_PARTITION_SIZE_LIMIT_BYTES) + builder.push_bind(LOG_PARTITION_SIZE_LIMIT_BYTES); + builder.push(" ORDER BY ts DESC, ts_nanos DESC, id DESC"); + let rows = builder + .build_query_as::() .fetch_all(self.logs_pool.as_ref()) .await?; @@ -463,7 +472,7 @@ fn format_feedback_log_line( line } -fn push_log_filters<'a>(builder: &mut QueryBuilder<'a, Sqlite>, query: &'a LogQuery) { +fn push_log_filters(builder: &mut QueryBuilder, query: &LogQuery) { if !query.levels_upper.is_empty() { builder.push(" AND UPPER(level) IN ("); { @@ -511,11 +520,7 @@ fn push_log_filters<'a>(builder: &mut QueryBuilder<'a, Sqlite>, query: &'a LogQu } } -fn push_like_filters<'a>( - builder: &mut QueryBuilder<'a, Sqlite>, - column: &str, - filters: &'a [String], -) { +fn push_like_filters(builder: &mut QueryBuilder, column: &str, filters: &[String]) { if filters.is_empty() { return; } @@ -613,6 +618,8 @@ mod tests { ignore_missing: false, locking: true, no_tx: false, + table_name: LOGS_MIGRATOR.table_name.clone(), + create_schemas: LOGS_MIGRATOR.create_schemas.clone(), }; let pool = SqlitePool::connect_with( SqliteConnectOptions::new() diff --git a/codex-rs/state/src/runtime/threads.rs b/codex-rs/state/src/runtime/threads.rs index 78180e7c7d..63bfe09f54 100644 --- a/codex-rs/state/src/runtime/threads.rs +++ b/codex-rs/state/src/runtime/threads.rs @@ -252,20 +252,16 @@ LIMIT 2 parent_thread_id: ThreadId, status: Option, ) -> anyhow::Result> { - let mut query = String::from( - "SELECT child_thread_id FROM thread_spawn_edges WHERE parent_thread_id = ?", + let mut builder = QueryBuilder::::new( + "SELECT child_thread_id FROM thread_spawn_edges WHERE parent_thread_id = ", ); - if status.is_some() { - query.push_str(" AND status = ?"); - } - query.push_str(" ORDER BY child_thread_id"); - - let mut sql = sqlx::query(query.as_str()).bind(parent_thread_id.to_string()); + builder.push_bind(parent_thread_id.to_string()); if let Some(status) = status { - sql = sql.bind(status.to_string()); + builder.push(" AND status = ").push_bind(status.to_string()); } + builder.push(" ORDER BY child_thread_id"); - let rows = sql.fetch_all(self.pool.as_ref()).await?; + let rows = builder.build().fetch_all(self.pool.as_ref()).await?; rows.into_iter() .map(|row| { ThreadId::try_from(row.try_get::("child_thread_id")?).map_err(Into::into) @@ -278,36 +274,48 @@ LIMIT 2 root_thread_id: ThreadId, status: Option, ) -> anyhow::Result> { - let status_filter = if status.is_some() { - " AND status = ?" - } else { - "" - }; - let query = format!( + let mut builder = QueryBuilder::::new( r#" WITH RECURSIVE subtree(child_thread_id, depth) AS ( SELECT child_thread_id, 1 FROM thread_spawn_edges - WHERE parent_thread_id = ?{status_filter} + WHERE parent_thread_id = + "#, + ); + builder.push_bind(root_thread_id.to_string()); + if let Some(status) = status { + let status = status.to_string(); + builder.push(" AND status = ").push_bind(status.clone()); + builder.push( + r#" UNION ALL SELECT edge.child_thread_id, subtree.depth + 1 FROM thread_spawn_edges AS edge JOIN subtree ON edge.parent_thread_id = subtree.child_thread_id - WHERE 1 = 1{status_filter} + WHERE status = + "#, + ); + builder.push_bind(status); + } else { + builder.push( + r#" + UNION ALL + SELECT edge.child_thread_id, subtree.depth + 1 + FROM thread_spawn_edges AS edge + JOIN subtree ON edge.parent_thread_id = subtree.child_thread_id + "#, + ); + } + builder.push( + r#" ) SELECT child_thread_id FROM subtree ORDER BY depth ASC, child_thread_id ASC - "# + "#, ); - let mut sql = sqlx::query(query.as_str()).bind(root_thread_id.to_string()); - if let Some(status) = status { - let status = status.to_string(); - sql = sql.bind(status.clone()).bind(status); - } - - let rows = sql.fetch_all(self.pool.as_ref()).await?; + let rows = builder.build().fetch_all(self.pool.as_ref()).await?; rows.into_iter() .map(|row| { ThreadId::try_from(row.try_get::("child_thread_id")?).map_err(Into::into) @@ -999,7 +1007,7 @@ fn one_thread_id_from_rows( } } -pub(super) fn push_thread_select_columns(builder: &mut QueryBuilder<'_, Sqlite>) { +pub(super) fn push_thread_select_columns(builder: &mut QueryBuilder) { builder.push( r#" SELECT @@ -1076,7 +1084,7 @@ pub struct ThreadFilterOptions<'a> { } pub(super) fn push_thread_filters<'a>( - builder: &mut QueryBuilder<'a, Sqlite>, + builder: &mut QueryBuilder, options: ThreadFilterOptions<'a>, ) { let ThreadFilterOptions { @@ -1156,7 +1164,7 @@ pub(super) fn push_thread_filters<'a>( } pub(super) fn push_thread_order_and_limit( - builder: &mut QueryBuilder<'_, Sqlite>, + builder: &mut QueryBuilder, sort_key: SortKey, sort_direction: SortDirection, limit: usize, From 5314f55097c2c56e4ca60e98ce6f4c9f755fc12f Mon Sep 17 00:00:00 2001 From: efrazer-oai Date: Wed, 27 May 2026 09:45:10 -0700 Subject: [PATCH 24/87] fix: run standalone updates noninteractively (#24637) # Summary The standalone update action currently downloads and runs the Codex installer as an interactive command. When an existing managed Codex install is present, accepting an update can therefore enter an installer prompt instead of completing the update. This change runs the standalone installer with `CODEX_NON_INTERACTIVE=1` on macOS/Linux and Windows. The installer environment-variable support is introduced by the parent PR; this PR wires that behavior into the Codex CLI update action. The rendered Windows command remains shell-safe, and long update commands wrap within the update-notice card. The standard test target snapshots the standalone notice for both platforms. # Stack 1. [#21567](https://github.com/openai/codex/pull/21567) - Adds environment-controlled release selection and noninteractive installer behavior. 2. [#24637](https://github.com/openai/codex/pull/24637) - Runs standalone updates with `CODEX_NON_INTERACTIVE=1`. (current) 3. [#24639](https://github.com/openai/codex/pull/24639) - Removes explicit release argument inputs in favor of `CODEX_RELEASE`. # Evidence Standalone updater-shaped macOS install with an existing npm-managed Codex on `PATH`: https://github.com/user-attachments/assets/a27fe9e9-db3a-4c39-a514-24bd3d1f01e8 # Testing Tests: targeted `codex-tui` update-action and update-notice snapshot tests, Rust formatting, benchmark smoke validation, macOS live-terminal standalone-update smoke testing, Windows ARM64 PowerShell standalone-update smoke testing through Parallels, and CI. --- codex-rs/tui/src/history_cell/notices.rs | 3 ++- ...update_available_history_cell_snapshot.snap | 11 +++++++++++ ...update_available_history_cell_snapshot.snap | 12 ++++++++++++ codex-rs/tui/src/history_cell/tests.rs | 18 ++++++++++++++++++ codex-rs/tui/src/update_action.rs | 18 ++++++++++++------ 5 files changed, 55 insertions(+), 7 deletions(-) create mode 100644 codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__tests__standalone_unix_update_available_history_cell_snapshot.snap create mode 100644 codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__tests__standalone_windows_update_available_history_cell_snapshot.snap diff --git a/codex-rs/tui/src/history_cell/notices.rs b/codex-rs/tui/src/history_cell/notices.rs index 3344dc45cd..7fb994442c 100644 --- a/codex-rs/tui/src/history_cell/notices.rs +++ b/codex-rs/tui/src/history_cell/notices.rs @@ -52,7 +52,8 @@ impl HistoryCell for UpdateAvailableHistoryCell { .width() .min(usize::from(width.saturating_sub(4))) .max(1); - with_border_with_inner_width(content.lines, inner_width) + let lines = adaptive_wrap_lines(content.lines, RtOptions::new(inner_width)); + with_border_with_inner_width(lines, inner_width) } fn raw_lines(&self) -> Vec> { diff --git a/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__tests__standalone_unix_update_available_history_cell_snapshot.snap b/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__tests__standalone_unix_update_available_history_cell_snapshot.snap new file mode 100644 index 0000000000..c8f352dbb4 --- /dev/null +++ b/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__tests__standalone_unix_update_available_history_cell_snapshot.snap @@ -0,0 +1,11 @@ +--- +source: tui/src/history_cell/tests.rs +expression: rendered +--- +╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ ✨ Update available! 0.0.0 -> 9.9.9 │ +│ Run sh -c 'curl -fsSL https://chatgpt.com/codex/install.sh | CODEX_NON_INTERACTIVE=1 sh' to update. │ +│ │ +│ See full release notes: │ +│ https://github.com/openai/codex/releases/latest │ +╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__tests__standalone_windows_update_available_history_cell_snapshot.snap b/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__tests__standalone_windows_update_available_history_cell_snapshot.snap new file mode 100644 index 0000000000..fb15a05775 --- /dev/null +++ b/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__tests__standalone_windows_update_available_history_cell_snapshot.snap @@ -0,0 +1,12 @@ +--- +source: tui/src/history_cell/tests.rs +expression: rendered +--- +╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ ✨ Update available! 0.0.0 -> 9.9.9 │ +│ Run powershell -ExecutionPolicy Bypass -c '$env:CODEX_NON_INTERACTIVE=1; irm │ +│ https://chatgpt.com/codex/install.ps1 | iex' to update. │ +│ │ +│ See full release notes: │ +│ https://github.com/openai/codex/releases/latest │ +╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ diff --git a/codex-rs/tui/src/history_cell/tests.rs b/codex-rs/tui/src/history_cell/tests.rs index 7f846d39fb..8c48ced86a 100644 --- a/codex-rs/tui/src/history_cell/tests.rs +++ b/codex-rs/tui/src/history_cell/tests.rs @@ -1027,6 +1027,24 @@ fn web_search_history_cell_snapshot() { insta::assert_snapshot!(rendered); } +#[test] +fn standalone_unix_update_available_history_cell_snapshot() { + let cell = + UpdateAvailableHistoryCell::new("9.9.9".to_string(), Some(UpdateAction::StandaloneUnix)); + let rendered = render_lines(&cell.display_lines(/*width*/ 110)).join("\n"); + + insta::assert_snapshot!(rendered); +} + +#[test] +fn standalone_windows_update_available_history_cell_snapshot() { + let cell = + UpdateAvailableHistoryCell::new("9.9.9".to_string(), Some(UpdateAction::StandaloneWindows)); + let rendered = render_lines(&cell.display_lines(/*width*/ 110)).join("\n"); + + insta::assert_snapshot!(rendered); +} + #[test] fn web_search_history_cell_wraps_with_indented_continuation() { let query = "example search query with several generic words to exercise wrapping".to_string(); diff --git a/codex-rs/tui/src/update_action.rs b/codex-rs/tui/src/update_action.rs index 0dbb145390..420562f13b 100644 --- a/codex-rs/tui/src/update_action.rs +++ b/codex-rs/tui/src/update_action.rs @@ -14,9 +14,9 @@ pub enum UpdateAction { BunGlobalLatest, /// Update via `brew upgrade codex`. BrewUpgrade, - /// Update via `curl -fsSL https://chatgpt.com/codex/install.sh | sh`. + /// Update via `curl -fsSL https://chatgpt.com/codex/install.sh | CODEX_NON_INTERACTIVE=1 sh`. StandaloneUnix, - /// Update via `irm https://chatgpt.com/codex/install.ps1|iex`. + /// Update via `$env:CODEX_NON_INTERACTIVE=1; irm https://chatgpt.com/codex/install.ps1 | iex`. StandaloneWindows, } @@ -43,7 +43,10 @@ impl UpdateAction { UpdateAction::BrewUpgrade => ("brew", &["upgrade", "--cask", "codex"]), UpdateAction::StandaloneUnix => ( "sh", - &["-c", "curl -fsSL https://chatgpt.com/codex/install.sh | sh"], + &[ + "-c", + "curl -fsSL https://chatgpt.com/codex/install.sh | CODEX_NON_INTERACTIVE=1 sh", + ], ), UpdateAction::StandaloneWindows => ( "powershell", @@ -51,7 +54,7 @@ impl UpdateAction { "-ExecutionPolicy", "Bypass", "-c", - "irm https://chatgpt.com/codex/install.ps1 | iex", + "$env:CODEX_NON_INTERACTIVE=1; irm https://chatgpt.com/codex/install.ps1 | iex", ], ), } @@ -140,7 +143,10 @@ mod tests { UpdateAction::StandaloneUnix.command_args(), ( "sh", - &["-c", "curl -fsSL https://chatgpt.com/codex/install.sh | sh"][..], + &[ + "-c", + "curl -fsSL https://chatgpt.com/codex/install.sh | CODEX_NON_INTERACTIVE=1 sh" + ][..], ) ); assert_eq!( @@ -151,7 +157,7 @@ mod tests { "-ExecutionPolicy", "Bypass", "-c", - "irm https://chatgpt.com/codex/install.ps1 | iex" + "$env:CODEX_NON_INTERACTIVE=1; irm https://chatgpt.com/codex/install.ps1 | iex" ][..], ) ); From 155905cf725d6c393cd29b1cd2ab9482fe651a32 Mon Sep 17 00:00:00 2001 From: sayan-oai Date: Wed, 27 May 2026 10:00:05 -0700 Subject: [PATCH 25/87] make vercel webhook url an env secret (#24778) move `DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL` to a repo environment secret. to keep scope of use of that env secret small, move the vercel website redeploy to its own post-release job. --- .github/workflows/rust-release.yml | 43 +++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 29d61d6a54..eaf886cd01 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -1282,19 +1282,6 @@ jobs: tag: ${{ github.ref_name }} config: .github/dotslash-argument-comment-lint-config.json - - name: Trigger developers.openai.com deploy - # Only trigger the deploy if the release is not a pre-release. - # The deploy is used to update the developers.openai.com website with the new config schema json file. - if: ${{ env.SIGN_MACOS == 'true' && !contains(steps.release_name.outputs.name, '-') }} - continue-on-error: true - env: - DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL: ${{ secrets.DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL }} - run: | - if ! curl -sS -f -o /dev/null -X POST "$DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL"; then - echo "::warning title=developers.openai.com deploy hook failed::Vercel deploy hook POST failed for ${GITHUB_REF_NAME}" - exit 1 - fi - # Publish to npm using OIDC authentication. # July 31, 2025: https://github.blog/changelog/2025-07-31-npm-trusted-publishing-with-oidc-is-generally-available/ # npm docs: https://docs.npmjs.com/trusted-publishers @@ -1507,6 +1494,36 @@ jobs: packages-dir: dist/python-runtime skip-existing: true + deploy-dev-website: + name: Trigger developers.openai.com deploy + needs: release + # Only trigger the deploy for a stable signed release. + # The deploy updates developers.openai.com with the new config schema json file. + if: >- + ${{ + !cancelled() && + needs.release.result == 'success' && + needs.release.outputs.sign_macos == 'true' && + !contains(needs.release.outputs.version, '-') + }} + runs-on: ubuntu-latest + continue-on-error: true + permissions: {} + environment: + name: dev-website-vercel-deploy + deployment: false + + steps: + - name: Trigger developers.openai.com deploy + continue-on-error: true + env: + DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL: ${{ secrets.DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL }} + run: | + if ! curl -sS -f -o /dev/null -X POST "$DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL"; then + echo "::warning title=developers.openai.com deploy hook failed::Vercel deploy hook POST failed for ${GITHUB_REF_NAME}" + exit 1 + fi + winget: name: winget needs: release From 8fcf2ad931b90589dd29a571f367e3185d26bbe0 Mon Sep 17 00:00:00 2001 From: canvrno-oai Date: Wed, 27 May 2026 11:05:42 -0700 Subject: [PATCH 26/87] fix: Preserve draft text when completing argument-taking slash commands (#23950) This adds slash command completion behavior for argument-taking commands, where text after the partially typed command becomes inline arguments instead of being discarded. This addresses the workflow of drafting text first, moving to the start, and completing a slash command around that existing draft. Before this change, this workflow would remove all user-input text aside from the slash command, which can be frustrating if the user had just typed out a long and well thought out goal. - Preserves the draft tail for inline-argument slash commands like `/goal` and `/review` when completing with `Tab` or `Enter`. - Keeps popup filtering focused on the command fragment under the cursor rather than the full draft text. - Leaves slash commands that do not support inline arguments unchanged, so completion still replaces the existing draft tail for those commands. - Adds focused TUI tests under slash input covering preserved arguments, cursor edge cases, and the negative case for a command without inline args. Follow-up simplification and test relocation from #24683 folded into this PR. --------- Co-authored-by: Eric Traut --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 13 +- .../bottom_pane/chat_composer/slash_input.rs | 233 ++++++++++++++++-- 2 files changed, 226 insertions(+), 20 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 8fcda378fc..9fc03764da 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -3504,6 +3504,9 @@ impl ChatComposer { && self .slash_input() .is_editing_command_name(first_line, cursor); + let command_filter_text = caret_on_first_line + .then(|| slash_input::command_popup_filter_text(first_line, cursor)) + .flatten(); // If the cursor is currently positioned within an `@token`, prefer the // file-search popup over the slash popup so users can insert a file path @@ -3517,14 +3520,18 @@ impl ChatComposer { match &mut self.popups.active { ActivePopup::Command(popup) => { if is_editing_slash_command_name { - popup.on_composer_text_change(first_line.to_string()); + if let Some(command_filter_text) = command_filter_text.as_deref() { + popup.on_composer_text_change(command_filter_text.to_string()); + } } else { self.popups.active = ActivePopup::None; } } _ => { - if is_editing_slash_command_name { - let command_popup = self.slash_input().command_popup(first_line); + if is_editing_slash_command_name + && let Some(command_filter_text) = command_filter_text.as_deref() + { + let command_popup = self.slash_input().command_popup(command_filter_text); self.popups.active = ActivePopup::Command(command_popup); } } diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/slash_input.rs b/codex-rs/tui/src/bottom_pane/chat_composer/slash_input.rs index a7205d237a..bb9d2c6904 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer/slash_input.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer/slash_input.rs @@ -126,7 +126,11 @@ impl<'a> SlashInput<'a> { self.enabled && !text.starts_with(' ') && text.trim().starts_with('/') } - pub(super) fn command_element_range(&self, first_line: &str) -> Option> { + pub(super) fn command_element_range( + &self, + first_line: &str, + cursor: usize, + ) -> Option> { if self.is_bash_mode { return None; } @@ -135,6 +139,11 @@ impl<'a> SlashInput<'a> { return None; } let element_end = 1 + name.len(); + // A draft tail can make an in-progress prefix look complete ("/re" + "view"). + // Keep it editable until the cursor leaves the command name. + if cursor <= first_line.len() && (1..element_end).contains(&cursor) { + return None; + } let has_space_after = first_line .get(element_end..) .and_then(|tail| tail.chars().next()) @@ -159,7 +168,7 @@ impl<'a> SlashInput<'a> { has_slash_command_prefix(name, self.command_flags, self.service_tier_commands) } - pub(super) fn command_popup(&self, first_line: &str) -> CommandPopup { + pub(super) fn command_popup(&self, filter_text: &str) -> CommandPopup { let mut command_popup = CommandPopup::new( CommandPopupFlags { collaboration_modes_enabled: self.command_flags.collaboration_modes_enabled, @@ -175,7 +184,7 @@ impl<'a> SlashInput<'a> { }, self.service_tier_commands.to_vec(), ); - command_popup.on_composer_text_change(first_line.to_string()); + command_popup.on_composer_text_change(filter_text.to_string()); command_popup } @@ -255,8 +264,12 @@ impl ChatComposer { } => { // Ensure popup filtering/selection reflects the latest composer text // before applying completion. - let first_line = self.draft.textarea.text().lines().next().unwrap_or(""); - popup.on_composer_text_change(first_line.to_string()); + let text = self.draft.textarea.text(); + let first_line = text.lines().next().unwrap_or("").to_owned(); + let cursor = self.draft.textarea.cursor(); + let filter_text = command_popup_filter_text(&first_line, cursor) + .unwrap_or_else(|| first_line.clone()); + popup.on_composer_text_change(filter_text); if let Some(selected_cmd) = popup.selected_item() { if selected_command_dispatches_immediately_on_tab(&selected_cmd) && let CommandItem::Builtin(cmd) = &selected_cmd @@ -267,8 +280,16 @@ impl ChatComposer { return (InputResult::Command(*cmd), true); } + if self + .complete_selected_slash_command_preserving_existing_draft_tail_as_inline_args( + &selected_cmd, + ) + { + return (InputResult::None, true); + } + if let Some(completed_text) = - selected_command_completion(first_line, &selected_cmd) + selected_command_completion(&first_line, &selected_cmd) { self.draft .textarea @@ -293,11 +314,23 @@ impl ChatComposer { } => { // Treat "/" as accepting the highlighted command as text completion // while the slash-command popup is active. - let first_line = self.draft.textarea.text().lines().next().unwrap_or(""); - popup.on_composer_text_change(first_line.to_string()); + let text = self.draft.textarea.text(); + let first_line = text.lines().next().unwrap_or("").to_owned(); + let cursor = self.draft.textarea.cursor(); + let filter_text = command_popup_filter_text(&first_line, cursor) + .unwrap_or_else(|| first_line.clone()); + popup.on_composer_text_change(filter_text); if let Some(selected_cmd) = popup.selected_item() { + if self + .complete_selected_slash_command_preserving_existing_draft_tail_as_inline_args( + &selected_cmd, + ) + { + return (InputResult::None, true); + } + if let Some(completed_text) = - selected_command_completion(first_line, &selected_cmd) + selected_command_completion(&first_line, &selected_cmd) { self.draft .textarea @@ -318,6 +351,15 @@ impl ChatComposer { .. } => { if let Some(sel) = popup.selected_item() { + if self + .complete_selected_slash_command_preserving_existing_draft_tail_as_inline_args( + &sel, + ) + && let Some(result) = self.try_dispatch_slash_command_with_args() + { + return (result, true); + } + self.stage_selected_slash_command_history(&sel); self.draft.textarea.set_text_clearing_elements(""); self.draft.is_bash_mode = false; @@ -338,6 +380,71 @@ impl ChatComposer { } } + fn complete_selected_slash_command_preserving_existing_draft_tail_as_inline_args( + &mut self, + selected_cmd: &CommandItem, + ) -> bool { + let CommandItem::Builtin(cmd) = selected_cmd else { + return false; + }; + if !cmd.supports_inline_args() { + return false; + } + + let text = self.draft.textarea.text(); + let first_line_end = text.find('\n').unwrap_or(text.len()); + let cursor = self.draft.textarea.cursor(); + if cursor > first_line_end || !text.starts_with('/') || !text.is_char_boundary(cursor) { + return false; + } + + let command_token_end = text[1..first_line_end] + .find(char::is_whitespace) + .map(|idx| 1 + idx) + .unwrap_or(first_line_end); + let typed_command_name = &text[1..command_token_end]; + let rest_after_token_is_empty = text[command_token_end..].trim().is_empty(); + if rest_after_token_is_empty && (cursor <= 1 || cursor >= command_token_end) { + return false; + } + let replace_end = + if cursor <= 1 || (typed_command_name == cmd.command() && rest_after_token_is_empty) { + command_token_end + } else { + cursor + }; + let tail = &text[replace_end..]; + let tail_starts_with_whitespace = tail.chars().next().is_some_and(char::is_whitespace); + let selected_command_text = format!("/{}", cmd.command()); + let replacement = if tail_starts_with_whitespace { + selected_command_text + } else { + format!("{selected_command_text} ") + }; + + let ranges_to_unmark = self + .draft + .textarea + .text_elements() + .into_iter() + .filter_map(|element| { + let range = element.byte_range.start..element.byte_range.end; + (range.start < replace_end && replace_end < range.end).then_some(range) + }) + .collect::>(); + for range in ranges_to_unmark { + self.draft.textarea.remove_element_range(range); + } + self.draft + .textarea + .replace_range(0..replace_end, &replacement); + self.draft.is_bash_mode = false; + self.draft + .textarea + .set_cursor(self.draft.textarea.text().len()); + true + } + /// Keep slash command elements aligned with the current first line. pub(super) fn sync_slash_command_elements(&mut self) { if !self.slash_commands_enabled() { @@ -346,7 +453,8 @@ impl ChatComposer { let text = self.draft.textarea.text(); let first_line_end = text.find('\n').unwrap_or(text.len()); let first_line = &text[..first_line_end]; - let desired_range = self.slash_input().command_element_range(first_line); + let cursor = self.draft.textarea.cursor(); + let desired_range = self.slash_input().command_element_range(first_line, cursor); // Slash commands are only valid at byte 0 of the first line. // Any slash-shaped element not matching the current desired prefix is stale. let mut has_desired = false; @@ -424,12 +532,20 @@ pub(super) fn args_elements( .collect() } +pub(super) fn command_popup_filter_text(first_line: &str, cursor: usize) -> Option { + let (name, _rest) = command_under_cursor(first_line, cursor)?; + Some(format!("/{name}")) +} + /// If the cursor is currently within a slash command on the first line, -/// extract the command name and the rest of the line after it. +/// extract the command fragment before the cursor and the rest of the line after it. fn command_under_cursor(first_line: &str, cursor: usize) -> Option<(&str, &str)> { if !first_line.starts_with('/') { return None; } + if cursor > first_line.len() || !first_line.is_char_boundary(cursor) { + return None; + } let name_start = 1usize; let name_end = first_line[name_start..] @@ -437,16 +553,99 @@ fn command_under_cursor(first_line: &str, cursor: usize) -> Option<(&str, &str)> .map(|idx| name_start + idx) .unwrap_or_else(|| first_line.len()); + let cursor = if cursor <= name_start { + name_end + } else { + cursor + }; if cursor > name_end { return None; } - let name = &first_line[name_start..name_end]; - let rest_start = first_line[name_end..] - .find(|c: char| !c.is_whitespace()) - .map(|idx| name_end + idx) - .unwrap_or(name_end); - let rest = &first_line[rest_start..]; + let name = &first_line[name_start..cursor]; + let rest = &first_line[cursor..]; Some((name, rest)) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::app_event::AppEvent; + use crate::bottom_pane::AppEventSender; + use pretty_assertions::assert_eq; + use tokio::sync::mpsc::unbounded_channel; + + fn test_composer() -> ChatComposer { + let (tx, _rx) = unbounded_channel::(); + ChatComposer::new( + /*has_input_focus*/ true, + AppEventSender::new(tx), + /*enhanced_keys_supported*/ false, + "Ask Codex to do anything".to_string(), + /*disable_paste_burst*/ false, + ) + } + + fn press(composer: &mut ChatComposer, code: KeyCode) -> InputResult { + composer + .handle_key_event(KeyEvent::new(code, KeyModifiers::NONE)) + .0 + } + + fn composer_with_text_at_cursor(text: &str, cursor: usize) -> ChatComposer { + let mut composer = test_composer(); + composer.draft.textarea.set_text_clearing_elements(text); + composer.draft.textarea.set_cursor(cursor); + composer.sync_popups(); + composer + } + + fn composer_with_draft_tail(prefix: &str, draft: &str) -> ChatComposer { + composer_with_text_at_cursor(&format!("{prefix}{draft}"), prefix.len()) + } + + #[test] + fn slash_completion_preserves_existing_draft_tail_for_inline_arg_commands() { + let draft = "view the diff"; + let expected_text = "/review view the diff"; + + let mut composer = composer_with_draft_tail("/re", draft); + assert_eq!(press(&mut composer, KeyCode::Tab), InputResult::None); + assert_eq!(composer.draft.textarea.text(), expected_text); + assert_eq!(composer.draft.textarea.cursor(), expected_text.len()); + + let mut composer = composer_with_draft_tail("/re", draft); + assert_eq!( + press(&mut composer, KeyCode::Enter), + InputResult::CommandWithArgs(SlashCommand::Review, draft.to_string(), Vec::new()) + ); + assert_eq!(composer.draft.textarea.text(), expected_text); + } + + #[test] + fn slash_completion_does_not_preserve_existing_draft_tail_for_other_commands() { + let mut composer = composer_with_draft_tail( + "/mo", + "preserve this draft only for opted-in slash commands", + ); + + assert_eq!(press(&mut composer, KeyCode::Tab), InputResult::None); + assert_eq!(composer.draft.textarea.text(), "/model "); + assert_eq!(composer.draft.textarea.cursor(), "/model ".len()); + } + + #[test] + fn slash_completion_does_not_turn_command_suffix_into_args() { + let mut composer = composer_with_text_at_cursor("/review", "/re".len()); + assert_eq!(press(&mut composer, KeyCode::Tab), InputResult::None); + assert_eq!(composer.draft.textarea.text(), "/review "); + + let mut composer = composer_with_text_at_cursor("/review", "/re".len()); + assert_eq!( + press(&mut composer, KeyCode::Enter), + InputResult::Command(SlashCommand::Review) + ); + assert!(composer.draft.textarea.is_empty()); + } +} From f0f483e8b2a2630bf8dfa5f8451e81eba20def6c Mon Sep 17 00:00:00 2001 From: canvrno-oai Date: Wed, 27 May 2026 11:08:16 -0700 Subject: [PATCH 27/87] [codex] Remove stale composer narrative doc references (#24641) ## Context `docs/tui-chat-composer.md` was removed by #20896 as part of removing local-only docs/specs from the repository. I checked the #20896 file list and the merge commit: the composer doc was deleted, not moved or copied, and current `main` does not contain a replacement composer narrative doc. Current guidance should keep contributors and agents focused on the docs that still exist: the module docs in `chat_composer.rs` and `paste_burst.rs`. ## Summary - Removes the scoped TUI bottom-pane AGENTS.md requirement to update `docs/tui-chat-composer.md`. - Removes stale module-doc references to that deleted narrative doc from `chat_composer.rs` and `paste_burst.rs`. ## Validation - Checked #20896 and the merge commit with rename/copy detection to confirm `docs/tui-chat-composer.md` was deleted rather than moved. - Searched current `main` for a replacement composer narrative doc. - Not run; documentation-only change. --- codex-rs/tui/src/bottom_pane/AGENTS.md | 2 -- codex-rs/tui/src/bottom_pane/chat_composer.rs | 1 - codex-rs/tui/src/bottom_pane/paste_burst.rs | 3 --- 3 files changed, 6 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/AGENTS.md b/codex-rs/tui/src/bottom_pane/AGENTS.md index b5328217db..b07fa2eca8 100644 --- a/codex-rs/tui/src/bottom_pane/AGENTS.md +++ b/codex-rs/tui/src/bottom_pane/AGENTS.md @@ -4,8 +4,6 @@ When changing the paste-burst or chat-composer state machines in this folder, ke - Update the relevant module docs (`chat_composer.rs` and/or `paste_burst.rs`) so they remain a readable, top-down explanation of the current behavior. -- Update the narrative doc `docs/tui-chat-composer.md` whenever behavior/assumptions change (Enter - handling, retro-capture, flush/clear rules, `disable_paste_burst`, non-ASCII/IME handling). - Keep implementations/docstrings aligned unless a divergence is intentional and documented. Practical check: diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 9fc03764da..e62cab9e86 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -111,7 +111,6 @@ //! composer flushes/clears any in-flight burst state so it cannot leak into subsequent input. //! //! For the detailed burst state machine, see `codex-rs/tui/src/bottom_pane/paste_burst.rs`. -//! For a narrative overview of the combined state machine, see `docs/tui-chat-composer.md`. //! //! # PasteBurst Integration Points //! diff --git a/codex-rs/tui/src/bottom_pane/paste_burst.rs b/codex-rs/tui/src/bottom_pane/paste_burst.rs index 44e3898db6..4ea8310c35 100644 --- a/codex-rs/tui/src/bottom_pane/paste_burst.rs +++ b/codex-rs/tui/src/bottom_pane/paste_burst.rs @@ -16,9 +16,6 @@ //! - buffer a burst as a single pasted string, or //! - let input flow through as normal typing. //! -//! For the higher-level view of how `PasteBurst` integrates with `ChatComposer`, see -//! `docs/tui-chat-composer.md`. -//! //! # Call Pattern //! //! `PasteBurst` is a pure state machine: it never mutates the textarea directly. The caller feeds From bee78806a9b8fad5f250f7822da08008b2c1dd01 Mon Sep 17 00:00:00 2001 From: ningyi-oai Date: Wed, 27 May 2026 11:09:33 -0700 Subject: [PATCH 28/87] [codex] add compaction metadata to turn headers (#24368) ## Summary - Add `request_kind` values for foreground turn, startup prewarm, compaction, and detached memory model requests. - Attach compaction dispatch metadata to local Responses, legacy `/v1/responses/compact`, and remote v2 compact requests. - Add the existing logical context-window identifier as `window_id` on turn-owned model request metadata. - Keep identity fields optional for detached memory requests, while still emitting `request_kind="memory"` in non-git/no-sandbox workspaces. ## Root Cause `x-codex-turn-metadata` has more than one producer. Foreground turns and compaction requests own a real turn and should carry that turn identity. Detached memory stage-one requests do not own a foreground turn, so absent identity fields are valid rather than missing data. Startup websocket prewarm is also a model request, but it has `generate=false` and must not be counted as a foreground turn. `thread_source` or session source identifies where a thread came from (for example review, guardian, or another subagent). `request_kind` identifies what the current outbound model request is doing (`turn`, `prewarm`, `compaction`, or `memory`). A review or guardian thread can issue either a normal turn request or a compaction request, so source cannot replace request kind. ## Behavior / Impact - Ordinary foreground requests send `request_kind="turn"`, their real identity fields, and `window_id=":"`. - Startup websocket warmup requests send `request_kind="prewarm"` so they are not counted as foreground turns. - Compaction requests send `request_kind="compaction"`, their real owning turn identity, the existing `window_id`, and `compaction.{trigger,reason,implementation,phase,strategy}`. - Detached memory stage-one requests send `request_kind="memory"` without `session_id`, `thread_id`, `turn_id`, or `window_id`; when no workspace metadata exists, the kind-only header is still emitted. - `session_id`, `thread_id`, `turn_id`, and `window_id` remain optional in the header schema because detached memory requests do not own a foreground turn or context window. - `window_id` is not a new ID system: it is copied from the already-sent `x-codex-window-id` / WS client metadata value at model-request dispatch time. - Existing `x-codex-window-id` HTTP/WS emission, value format, generation advancement, resume behavior, and fork reset behavior are unchanged. - `request_kind`, `window_id`, and upstream turn-owned identity fields remain schema-owned; input `responsesapi_client_metadata` cannot replace their canonical values. - No table, DAG, export, app-server API, or MCP `_meta` schema changes are included. A compaction attempt stopped by a pre-compact hook issues no model request and therefore has no request header; its outcome remains in analytics events. Status, error, duration, and token deltas also remain analytics fields rather than request-header fields. Future detached-memory attribution using a real initiating turn ID as `trigger_turn_id` is intentionally not part of this PR. ## Sync With Main - Final pushed head `716342e79` is rebased onto `origin/main@0d37db4b2`. - The metadata conflict came from upstream `#24160`, which added `forked_from_thread_id` on the same `turn_metadata` surface. Resolution preserves that field and its protection from client metadata override alongside this PR's request-kind, compaction, and window-id fields. - While resolving the overlapping commits, I removed an accidental recursive model-request overlay and a duplicate detached-memory header builder before completing the rebase. ## Latency / User Experience Boundary - Foreground turns perform no new filesystem, git, or network work. New fields are inserted into metadata already serialized for outgoing requests. - Compaction issues the same model/HTTP requests with the same prompt, model, service tier, and sampling settings; only metadata bytes change. - Startup prewarm already sent metadata; it is now correctly classified as `prewarm`. - Non-git detached memory now sends a small kind-only metadata header rather than no header. - This client diff adds no user-visible latency mechanism beyond negligible serialization and header bytes on already-existing requests. ## Validation On conflict-resolved head `1d35c2cfb` based on `origin/main@487521733`: - `just fmt` (passed) - `just fix -p codex-core` (passed) - `git diff --check origin/main...HEAD` (passed) - `just test -p codex-core -E 'test(turn_metadata) | test(websocket_first_turn_uses_startup_prewarm_and_create) | test(responses_stream_includes_turn_metadata_header_for_git_workspace_e2e) | test(responses_websocket_forwards_turn_metadata_on_initial_and_incremental_create) | test(remote_compact_v2_retries_failures_with_stream_retry_budget) | test(window_id_advances_after_compact_persists_on_resume_and_resets_on_fork)'` (`23 passed`; `bench-smoke` passed) - `just test -p codex-app-server -E 'test(turn_start_forwards_client_metadata_to_responses_request_v2) | test(turn_start_forwards_client_metadata_to_responses_websocket_request_body_v2) | test(auto_compaction_remote_emits_started_and_completed_items)'` (`3 passed`; `bench-smoke` passed) - `just test -p codex-memories-write` (`29 passed`; `bench-smoke` passed) --- .../tests/suite/v2/client_metadata.rs | 8 + .../app-server/tests/suite/v2/compaction.rs | 56 +++++ codex-rs/core/src/client.rs | 5 +- codex-rs/core/src/compact.rs | 10 +- codex-rs/core/src/compact_remote.rs | 22 +- codex-rs/core/src/compact_remote_v2.rs | 14 +- codex-rs/core/src/session/turn.rs | 5 +- codex-rs/core/src/session_startup_prewarm.rs | 3 +- codex-rs/core/src/turn_metadata.rs | 216 ++++++++++++++---- codex-rs/core/src/turn_metadata_tests.rs | 98 +++++++- codex-rs/core/tests/suite/agent_websocket.rs | 16 ++ codex-rs/core/tests/suite/compact.rs | 47 ++++ codex-rs/core/tests/suite/compact_remote.rs | 149 +++++++++++- codex-rs/memories/write/src/runtime.rs | 2 +- codex-rs/memories/write/src/startup_tests.rs | 33 ++- 15 files changed, 628 insertions(+), 56 deletions(-) diff --git a/codex-rs/app-server/tests/suite/v2/client_metadata.rs b/codex-rs/app-server/tests/suite/v2/client_metadata.rs index 3a00a433d1..9daa8d34f1 100644 --- a/codex-rs/app-server/tests/suite/v2/client_metadata.rs +++ b/codex-rs/app-server/tests/suite/v2/client_metadata.rs @@ -105,6 +105,10 @@ async fn turn_start_forwards_client_metadata_to_responses_request_v2() -> Result assert_eq!(metadata["thread_source"].as_str(), Some("client-supplied")); assert_eq!(metadata["turn_id"].as_str(), Some(turn.id.as_str())); assert!(metadata.get("session_id").is_some()); + assert_eq!( + metadata["window_id"].as_str(), + request.header("x-codex-window-id").as_deref() + ); Ok(()) } @@ -497,6 +501,10 @@ async fn turn_start_forwards_client_metadata_to_responses_websocket_request_body assert_eq!(metadata["origin"].as_str(), Some("gaas")); assert_eq!(metadata["turn_id"].as_str(), Some(turn.id.as_str())); assert!(metadata.get("session_id").is_some()); + assert_eq!( + metadata["window_id"].as_str(), + request["client_metadata"]["x-codex-window-id"].as_str() + ); websocket_server.shutdown().await; Ok(()) diff --git a/codex-rs/app-server/tests/suite/v2/compaction.rs b/codex-rs/app-server/tests/suite/v2/compaction.rs index 6db031b278..2cf9f6b36c 100644 --- a/codex-rs/app-server/tests/suite/v2/compaction.rs +++ b/codex-rs/app-server/tests/suite/v2/compaction.rs @@ -190,6 +190,58 @@ async fn auto_compaction_remote_emits_started_and_completed_items() -> Result<() let response_requests = responses_log.requests(); assert_eq!(response_requests.len(), 3); + let turn_metadata = response_requests + .iter() + .map(|request| { + request + .header("x-codex-turn-metadata") + .as_deref() + .map(parse_json_header) + .unwrap_or_else(|| panic!("turn request should include turn metadata")) + }) + .collect::>(); + for (request, metadata) in response_requests.iter().zip(&turn_metadata) { + assert_eq!(metadata["request_kind"].as_str(), Some("turn")); + assert!( + metadata["turn_id"] + .as_str() + .is_some_and(|turn_id| !turn_id.is_empty()), + "turn request should carry a non-empty turn id" + ); + assert_eq!( + metadata["window_id"].as_str(), + request.header("x-codex-window-id").as_deref() + ); + assert!(metadata.get("compaction").is_none()); + } + + let compact_metadata = compact_requests[0] + .header("x-codex-turn-metadata") + .as_deref() + .map(parse_json_header) + .unwrap_or_else(|| panic!("compact request should include turn metadata")); + assert_eq!( + compact_metadata["request_kind"].as_str(), + Some("compaction") + ); + assert_eq!( + compact_metadata["compaction"], + serde_json::json!({ + "trigger": "auto", + "reason": "context_limit", + "implementation": "responses_compact", + "phase": "pre_turn", + "strategy": "memento", + }) + ); + assert_eq!( + compact_metadata["turn_id"], turn_metadata[2]["turn_id"], + "pre-turn compaction should carry the current turn id" + ); + assert_eq!( + compact_metadata["window_id"].as_str(), + compact_requests[0].header("x-codex-window-id").as_deref() + ); Ok(()) } @@ -407,3 +459,7 @@ async fn wait_for_context_compaction_completed( } } } + +fn parse_json_header(value: &str) -> serde_json::Value { + serde_json::from_str(value).unwrap_or_else(|err| panic!("turn metadata should be json: {err}")) +} diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index d84b32a8b4..6c87400edd 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -383,7 +383,7 @@ impl ModelClient { self.store_cached_websocket_session(WebsocketSession::default()); } - fn current_window_id(&self) -> String { + pub(crate) fn current_window_id(&self) -> String { let thread_id = self.state.thread_id; let window_generation = self.state.window_generation.load(Ordering::Relaxed); format!("{thread_id}:{window_generation}") @@ -441,6 +441,7 @@ impl ModelClient { settings: CompactConversationRequestSettings, session_telemetry: &SessionTelemetry, compaction_trace: &CompactionTraceContext, + turn_metadata_header: Option<&str>, ) -> Result> { if prompt.input.is_empty() { return Ok(Vec::new()); @@ -496,7 +497,7 @@ impl ModelClient { extra_headers.extend(build_responses_headers( self.state.beta_features_header.as_deref(), /*turn_state*/ None, - /*turn_metadata_header*/ None, + parse_turn_metadata_header(turn_metadata_header).as_ref(), )); extra_headers.extend(self.build_responses_identity_headers()); extra_headers.extend(build_session_headers( diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index f2bded8d06..1a6256ad76 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -13,6 +13,7 @@ use crate::session::PreviousTurnSettings; use crate::session::session::Session; use crate::session::turn::get_last_assistant_message_from_turn; use crate::session::turn_context::TurnContext; +use crate::turn_metadata::CompactionTurnMetadata; use crate::util::backoff; use codex_analytics::CodexCompactionEvent; use codex_analytics::CompactionImplementation; @@ -128,6 +129,8 @@ async fn run_compact_task_inner( reason: CompactionReason, phase: CompactionPhase, ) -> CodexResult<()> { + let compaction_metadata = + CompactionTurnMetadata::new(trigger, reason, CompactionImplementation::Responses, phase); let attempt = CompactionAnalyticsAttempt::begin( sess.as_ref(), turn_context.as_ref(), @@ -153,6 +156,7 @@ async fn run_compact_task_inner( Arc::clone(&turn_context), input, initial_context_injection, + compaction_metadata, ) .await; let status = compaction_status_from_result(&result); @@ -173,6 +177,7 @@ async fn run_compact_task_inner_impl( turn_context: Arc, input: Vec, initial_context_injection: InitialContextInjection, + compaction_metadata: CompactionTurnMetadata, ) -> CodexResult { let compaction_item = TurnItem::ContextCompaction(ContextCompactionItem::new()); sess.emit_turn_item_started(&turn_context, &compaction_item) @@ -204,7 +209,10 @@ async fn run_compact_task_inner_impl( personality: turn_context.personality, ..Default::default() }; - let turn_metadata_header = turn_context.turn_metadata_state.current_header_value(); + let window_id = sess.services.model_client.current_window_id(); + let turn_metadata_header = turn_context + .turn_metadata_state + .current_header_value_for_compaction(&window_id, compaction_metadata); let attempt_result = drain_to_completed( &sess, turn_context.as_ref(), diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs index 969da47544..b4456dc717 100644 --- a/codex-rs/core/src/compact_remote.rs +++ b/codex-rs/core/src/compact_remote.rs @@ -17,6 +17,7 @@ use crate::hook_runtime::run_pre_compact_hooks; use crate::session::session::Session; use crate::session::turn::built_tools; use crate::session::turn_context::TurnContext; +use crate::turn_metadata::CompactionTurnMetadata; use codex_analytics::CompactionImplementation; use codex_analytics::CompactionPhase; use codex_analytics::CompactionReason; @@ -89,6 +90,12 @@ async fn run_remote_compact_task_inner( reason: CompactionReason, phase: CompactionPhase, ) -> CodexResult<()> { + let compaction_metadata = CompactionTurnMetadata::new( + trigger, + reason, + CompactionImplementation::ResponsesCompact, + phase, + ); let attempt = CompactionAnalyticsAttempt::begin( sess.as_ref(), turn_context.as_ref(), @@ -113,8 +120,13 @@ async fn run_remote_compact_task_inner( return Err(CodexErr::TurnAborted); } } - let result = - run_remote_compact_task_inner_impl(sess, turn_context, initial_context_injection).await; + let result = run_remote_compact_task_inner_impl( + sess, + turn_context, + initial_context_injection, + compaction_metadata, + ) + .await; let status = compaction_status_from_result(&result); let error = result.as_ref().err().map(ToString::to_string); if result.is_ok() { @@ -139,6 +151,7 @@ async fn run_remote_compact_task_inner_impl( sess: &Arc, turn_context: &Arc, initial_context_injection: InitialContextInjection, + compaction_metadata: CompactionTurnMetadata, ) -> CodexResult<()> { let context_compaction_item = ContextCompactionItem::new(); // Use the UI compaction item ID as the trace compaction ID so protocol lifecycle events, @@ -186,6 +199,10 @@ async fn run_remote_compact_task_inner_impl( output_schema: None, output_schema_strict: true, }; + let window_id = sess.services.model_client.current_window_id(); + let turn_metadata_header = turn_context + .turn_metadata_state + .current_header_value_for_compaction(&window_id, compaction_metadata); let mut new_history = sess .services .model_client @@ -203,6 +220,7 @@ async fn run_remote_compact_task_inner_impl( }, &turn_context.session_telemetry, &compaction_trace, + turn_metadata_header.as_deref(), ) .or_else(|err| async { let total_usage_breakdown = sess.get_total_token_usage_breakdown().await; diff --git a/codex-rs/core/src/compact_remote_v2.rs b/codex-rs/core/src/compact_remote_v2.rs index dcf241f990..0da3017f7d 100644 --- a/codex-rs/core/src/compact_remote_v2.rs +++ b/codex-rs/core/src/compact_remote_v2.rs @@ -21,6 +21,7 @@ use crate::responses_retry::handle_retryable_response_stream_error; use crate::session::session::Session; use crate::session::turn::built_tools; use crate::session::turn_context::TurnContext; +use crate::turn_metadata::CompactionTurnMetadata; use codex_analytics::CompactionImplementation; use codex_analytics::CompactionPhase; use codex_analytics::CompactionReason; @@ -105,6 +106,12 @@ async fn run_remote_compact_task_inner( reason: CompactionReason, phase: CompactionPhase, ) -> CodexResult<()> { + let compaction_metadata = CompactionTurnMetadata::new( + trigger, + reason, + CompactionImplementation::ResponsesCompactionV2, + phase, + ); let attempt = CompactionAnalyticsAttempt::begin( sess.as_ref(), turn_context.as_ref(), @@ -134,6 +141,7 @@ async fn run_remote_compact_task_inner( turn_context, client_session, initial_context_injection, + compaction_metadata, ) .await; let status = compaction_status_from_result(&result); @@ -161,6 +169,7 @@ async fn run_remote_compact_task_inner_impl( turn_context: &Arc, client_session: Option<&mut ModelClientSession>, initial_context_injection: InitialContextInjection, + compaction_metadata: CompactionTurnMetadata, ) -> CodexResult<()> { let context_compaction_item = ContextCompactionItem::new(); let compaction_trace = sess.services.rollout_thread_trace.compaction_trace_context( @@ -208,7 +217,10 @@ async fn run_remote_compact_task_inner_impl( output_schema_strict: true, }; - let turn_metadata_header = turn_context.turn_metadata_state.current_header_value(); + let window_id = sess.services.model_client.current_window_id(); + let turn_metadata_header = turn_context + .turn_metadata_state + .current_header_value_for_compaction(&window_id, compaction_metadata); let trace_attempt = compaction_trace.start_attempt(&serde_json::json!({ "model": turn_context.model_info.slug.as_str(), "instructions": prompt.base_instructions.text.as_str(), diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index 2b9fa02529..fe20b76e4d 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -234,7 +234,10 @@ pub(crate) async fn run_turn( .for_prompt(&turn_context.model_info.input_modalities) }; - let turn_metadata_header = turn_context.turn_metadata_state.current_header_value(); + let window_id = sess.services.model_client.current_window_id(); + let turn_metadata_header = turn_context + .turn_metadata_state + .current_header_value_for_model_request(&window_id); match run_sampling_request( Arc::clone(&sess), Arc::clone(&turn_context), diff --git a/codex-rs/core/src/session_startup_prewarm.rs b/codex-rs/core/src/session_startup_prewarm.rs index ca21e3ea72..0669e54469 100644 --- a/codex-rs/core/src/session_startup_prewarm.rs +++ b/codex-rs/core/src/session_startup_prewarm.rs @@ -256,9 +256,10 @@ async fn schedule_startup_prewarm_inner( build_prompt_started_at.elapsed(), /*status*/ None, ); + let window_id = session.services.model_client.current_window_id(); let startup_turn_metadata_header = startup_turn_context .turn_metadata_state - .current_header_value(); + .current_header_value_for_prewarm(&window_id); let mut client_session = session.services.model_client.new_session(); let websocket_warmup_started_at = Instant::now(); client_session diff --git a/codex-rs/core/src/turn_metadata.rs b/codex-rs/core/src/turn_metadata.rs index 52a714960a..4f9406d4b0 100644 --- a/codex-rs/core/src/turn_metadata.rs +++ b/codex-rs/core/src/turn_metadata.rs @@ -6,6 +6,11 @@ use std::sync::RwLock; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; +use codex_analytics::CompactionImplementation; +use codex_analytics::CompactionPhase; +use codex_analytics::CompactionReason; +use codex_analytics::CompactionStrategy; +use codex_analytics::CompactionTrigger; use codex_utils_string::to_ascii_json_string; use serde::Serialize; use serde_json::Value; @@ -27,12 +32,54 @@ const MODEL_KEY: &str = "model"; const REASONING_EFFORT_KEY: &str = "reasoning_effort"; const TURN_STARTED_AT_UNIX_MS_KEY: &str = "turn_started_at_unix_ms"; const USER_INPUT_REQUESTED_DURING_TURN_KEY: &str = "user_input_requested_during_turn"; +const REQUEST_KIND_KEY: &str = "request_kind"; +const COMPACTION_KEY: &str = "compaction"; +const WINDOW_ID_KEY: &str = "window_id"; pub(crate) struct McpTurnMetadataContext<'a> { pub(crate) model: &'a str, pub(crate) reasoning_effort: Option, } +/// Metadata present only on outbound model requests that perform compaction. +/// +/// These fields describe the operation at dispatch time. Post-response outcomes such as status, +/// error, duration, and token deltas remain in compaction analytics events. +#[derive(Clone, Copy, Debug, Serialize)] +pub(crate) struct CompactionTurnMetadata { + trigger: CompactionTrigger, + reason: CompactionReason, + implementation: CompactionImplementation, + phase: CompactionPhase, + strategy: CompactionStrategy, +} + +impl CompactionTurnMetadata { + pub(crate) fn new( + trigger: CompactionTrigger, + reason: CompactionReason, + implementation: CompactionImplementation, + phase: CompactionPhase, + ) -> Self { + Self { + trigger, + reason, + implementation, + phase, + strategy: CompactionStrategy::Memento, + } + } +} + +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +enum TurnMetadataRequestKind { + Turn, + Prewarm, + Compaction, + Memory, +} + #[derive(Clone, Debug, Default)] struct WorkspaceGitMetadata { associated_remote_urls: Option>, @@ -68,8 +115,15 @@ impl From for TurnMetadataWorkspace { } } -#[derive(Clone, Debug, Serialize, Default)] +/// Base payload for the outbound model request `x-codex-turn-metadata` header. +/// +/// Turn-owned state populates identity fields, including optional fork lineage. A concrete +/// request kind is added at outbound model dispatch so turns, startup prewarm, and compaction +/// remain distinguishable. Detached memory requests are constructed as `memory` directly. +#[derive(Clone, Debug, Serialize)] pub(crate) struct TurnMetadataBag { + #[serde(default, skip_serializing_if = "Option::is_none")] + request_kind: Option, #[serde(default, skip_serializing_if = "Option::is_none")] session_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -87,6 +141,41 @@ pub(crate) struct TurnMetadataBag { } impl TurnMetadataBag { + fn new( + request_kind: Option, + session_id: Option, + thread_id: Option, + forked_from_thread_id: Option, + thread_source: Option, + turn_id: Option, + sandbox: Option, + ) -> Self { + Self { + request_kind, + session_id, + thread_id, + forked_from_thread_id, + thread_source, + turn_id, + workspaces: BTreeMap::new(), + sandbox, + } + } + + fn with_workspace_git_metadata( + mut self, + repo_root: Option, + workspace_git_metadata: Option, + ) -> Self { + if let (Some(repo_root), Some(workspace_git_metadata)) = (repo_root, workspace_git_metadata) + && !workspace_git_metadata.is_empty() + { + self.workspaces + .insert(repo_root, workspace_git_metadata.into()); + } + self + } + fn to_header_value(&self) -> Option { to_ascii_json_string(self).ok() } @@ -117,6 +206,9 @@ fn merge_turn_metadata( | "turn_id" | TURN_STARTED_AT_UNIX_MS_KEY | "forked_from_thread_id" + | REQUEST_KIND_KEY + | COMPACTION_KEY + | WINDOW_ID_KEY ) { continue; } @@ -140,32 +232,24 @@ pub async fn build_turn_metadata_header( get_has_changes(cwd), ); let latest_git_commit_hash = head_commit_hash.map(|sha| sha.0); - if latest_git_commit_hash.is_none() - && associated_remote_urls.is_none() - && has_changes.is_none() - && sandbox.is_none() - { - return None; - } - - let workspace_git_metadata = WorkspaceGitMetadata { - associated_remote_urls, - latest_git_commit_hash, - has_changes, - }; - let mut metadata = TurnMetadataBag { - sandbox: sandbox.map(ToString::to_string), - ..Default::default() - }; - if let Some(repo_root) = repo_root - && !workspace_git_metadata.is_empty() - { - metadata - .workspaces - .insert(repo_root, workspace_git_metadata.into()); - } - - metadata.to_header_value() + TurnMetadataBag::new( + Some(TurnMetadataRequestKind::Memory), + /*session_id*/ None, + /*thread_id*/ None, + /*forked_from_thread_id*/ None, + /*thread_source*/ None, + /*turn_id*/ None, + sandbox.map(ToString::to_string), + ) + .with_workspace_git_metadata( + repo_root, + Some(WorkspaceGitMetadata { + associated_remote_urls, + latest_git_commit_hash, + has_changes, + }), + ) + .to_header_value() } #[derive(Clone, Debug)] @@ -173,7 +257,7 @@ pub(crate) struct TurnMetadataState { cwd: AbsolutePathBuf, repo_root: Option, base_metadata: TurnMetadataBag, - base_header: String, + base_header: Option, enriched_header: Arc>>, turn_started_at_unix_ms: Arc>>, responsesapi_client_metadata: Arc>>>, @@ -203,18 +287,16 @@ impl TurnMetadataState { ) .to_string(), ); - let base_metadata = TurnMetadataBag { - session_id: Some(session_id), - thread_id: Some(thread_id), + let base_metadata = TurnMetadataBag::new( + /*request_kind*/ None, + Some(session_id), + Some(thread_id), forked_from_thread_id, thread_source, - turn_id: Some(turn_id), + Some(turn_id), sandbox, - ..Default::default() - }; - let base_header = base_metadata - .to_header_value() - .unwrap_or_else(|| "{}".to_string()); + ); + let base_header = base_metadata.to_header_value(); Self { cwd, @@ -239,7 +321,7 @@ impl TurnMetadataState { { header } else { - self.base_header.clone() + self.base_header.clone()? }; let turn_started_at_unix_ms = *self .turn_started_at_unix_ms @@ -264,6 +346,7 @@ impl TurnMetadataState { ) -> Option { let header = self.current_header_value()?; let mut metadata = serde_json::from_str::>(&header).ok()?; + metadata.remove(REQUEST_KIND_KEY); metadata.insert( MODEL_KEY.to_string(), Value::String(context.model.to_string()), @@ -293,6 +376,52 @@ impl TurnMetadataState { Some(Value::Object(metadata)) } + fn current_header_value_for_model_request_kind( + &self, + window_id: &str, + request_kind: TurnMetadataRequestKind, + ) -> Option { + let header = self.current_header_value()?; + let mut metadata = serde_json::from_str::>(&header).ok()?; + metadata.insert( + REQUEST_KIND_KEY.to_string(), + serde_json::to_value(request_kind).ok()?, + ); + metadata.insert( + WINDOW_ID_KEY.to_string(), + Value::String(window_id.to_string()), + ); + to_ascii_json_string(&metadata).ok() + } + + pub(crate) fn current_header_value_for_model_request(&self, window_id: &str) -> Option { + self.current_header_value_for_model_request_kind(window_id, TurnMetadataRequestKind::Turn) + } + + pub(crate) fn current_header_value_for_prewarm(&self, window_id: &str) -> Option { + self.current_header_value_for_model_request_kind( + window_id, + TurnMetadataRequestKind::Prewarm, + ) + } + + pub(crate) fn current_header_value_for_compaction( + &self, + window_id: &str, + compaction: CompactionTurnMetadata, + ) -> Option { + let header = self.current_header_value_for_model_request_kind( + window_id, + TurnMetadataRequestKind::Compaction, + )?; + let mut metadata = serde_json::from_str::>(&header).ok()?; + metadata.insert( + COMPACTION_KEY.to_string(), + serde_json::to_value(compaction).ok()?, + ); + to_ascii_json_string(&metadata).ok() + } + pub(crate) fn mark_user_input_requested_during_turn(&self) { self.user_input_requested_during_turn .store(true, Ordering::Relaxed); @@ -335,13 +464,14 @@ impl TurnMetadataState { let Some(repo_root) = state.repo_root.clone() else { return; }; - if workspace_git_metadata.is_empty() { + + let enriched_metadata = state + .base_metadata + .clone() + .with_workspace_git_metadata(Some(repo_root), Some(workspace_git_metadata)); + if enriched_metadata.workspaces.is_empty() { return; } - let mut enriched_metadata = state.base_metadata.clone(); - enriched_metadata - .workspaces - .insert(repo_root, workspace_git_metadata.into()); if let Some(header_value) = enriched_metadata.to_header_value() { *state diff --git a/codex-rs/core/src/turn_metadata_tests.rs b/codex-rs/core/src/turn_metadata_tests.rs index a874b0db9b..a482392726 100644 --- a/codex-rs/core/src/turn_metadata_tests.rs +++ b/codex-rs/core/src/turn_metadata_tests.rs @@ -20,7 +20,7 @@ fn test_mcp_turn_metadata_context() -> McpTurnMetadataContext<'static> { } #[tokio::test] -async fn build_turn_metadata_header_includes_has_changes_for_clean_repo() { +async fn build_turn_metadata_header_marks_detached_memory_without_turn_identity() { let temp_dir = TempDir::new().expect("temp dir"); let repo_path = temp_dir.path().join("repo-東京").abs(); std::fs::create_dir_all(&repo_path).expect("create repo"); @@ -64,6 +64,13 @@ async fn build_turn_metadata_header_includes_has_changes_for_clean_repo() { assert!(header.is_ascii()); assert!(!header.contains("東京")); let parsed: Value = serde_json::from_str(&header).expect("valid json"); + assert_eq!(parsed["request_kind"].as_str(), Some("memory")); + assert!(parsed.get("session_id").is_none()); + assert!(parsed.get("thread_id").is_none()); + assert!(parsed.get("forked_from_thread_id").is_none()); + assert!(parsed.get("turn_id").is_none()); + assert!(parsed.get(WINDOW_ID_KEY).is_none()); + let expected_repo_path = repo_path.to_string_lossy().into_owned(); let actual_repo_path = parsed .get("workspaces") @@ -77,13 +84,25 @@ async fn build_turn_metadata_header_includes_has_changes_for_clean_repo() { .and_then(|workspaces| workspaces.values().next()) .cloned() .expect("workspace"); - assert_eq!( workspace.get("has_changes").and_then(Value::as_bool), Some(false) ); } +#[tokio::test] +async fn build_turn_metadata_header_marks_memory_without_workspace_metadata() { + let temp_dir = TempDir::new().expect("temp dir"); + let cwd = temp_dir.path().abs(); + + let header = build_turn_metadata_header(&cwd, /*sandbox*/ None) + .await + .expect("detached memory should emit its request kind"); + let parsed: Value = serde_json::from_str(&header).expect("valid json"); + + assert_eq!(parsed, serde_json::json!({"request_kind": "memory"})); +} + #[test] fn turn_metadata_state_uses_platform_sandbox_tag() { let temp_dir = TempDir::new().expect("temp dir"); @@ -109,6 +128,7 @@ fn turn_metadata_state_uses_platform_sandbox_tag() { let thread_id = json.get("thread_id").and_then(Value::as_str); let thread_source = json.get("thread_source").and_then(Value::as_str); + assert!(json.get("request_kind").is_none()); let expected_sandbox = permission_profile_sandbox_tag( &permission_profile, WindowsSandboxLevel::Disabled, @@ -228,6 +248,7 @@ fn turn_metadata_state_includes_model_and_reasoning_effort_only_in_request_meta( let meta = state .current_meta_value_for_mcp_request(test_mcp_turn_metadata_context()) .expect("turn metadata should be present"); + assert!(meta.get("request_kind").is_none()); assert_eq!(meta["model"].as_str(), Some("gpt-5.4")); assert_eq!(meta["reasoning_effort"].as_str(), Some("high")); @@ -367,7 +388,10 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields( "forked_from_thread_id".to_string(), "client-supplied".to_string(), ), + ("turn_id".to_string(), "client-supplied".to_string()), + (WINDOW_ID_KEY.to_string(), "client-supplied".to_string()), ("thread_source".to_string(), "client-supplied".to_string()), + ("request_kind".to_string(), "client-supplied".to_string()), ( "turn_started_at_unix_ms".to_string(), "client-supplied".to_string(), @@ -392,14 +416,84 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields( ); assert_eq!(json["thread_source"].as_str(), Some("user")); assert_eq!(json["turn_id"].as_str(), Some("turn-a")); + assert!(json.get("request_kind").is_none()); + assert!(json.get(WINDOW_ID_KEY).is_none()); assert_eq!( json["turn_started_at_unix_ms"].as_i64(), Some(1_700_000_000_123) ); + let model_request_header = state + .current_header_value_for_model_request("thread-a:1") + .expect("model request header"); + let model_request_json: Value = + serde_json::from_str(&model_request_header).expect("model request json"); + assert_eq!(model_request_json["request_kind"].as_str(), Some("turn")); + assert_eq!( + model_request_json[WINDOW_ID_KEY].as_str(), + Some("thread-a:1") + ); + let meta = state .current_meta_value_for_mcp_request(test_mcp_turn_metadata_context()) .expect("turn metadata should be present"); assert_eq!(meta["model"].as_str(), Some("gpt-5.4")); assert_eq!(meta["reasoning_effort"].as_str(), Some("high")); + assert!(meta.get(WINDOW_ID_KEY).is_none()); +} + +#[test] +fn turn_metadata_state_overlays_compaction_only_on_compaction_requests() { + let temp_dir = TempDir::new().expect("temp dir"); + let cwd = temp_dir.path().abs(); + let permission_profile = PermissionProfile::read_only(); + let state = TurnMetadataState::new( + "session-a".to_string(), + "thread-a".to_string(), + /*forked_from_thread_id*/ None, + Some(ThreadSource::User), + "turn-a".to_string(), + cwd, + &permission_profile, + WindowsSandboxLevel::Disabled, + /*enforce_managed_network*/ false, + ); + state.set_responsesapi_client_metadata(HashMap::from([( + "compaction".to_string(), + "client-supplied".to_string(), + )])); + + let compact_header = state + .current_header_value_for_compaction( + "thread-a:2", + CompactionTurnMetadata::new( + CompactionTrigger::Auto, + CompactionReason::ContextLimit, + CompactionImplementation::ResponsesCompactionV2, + CompactionPhase::MidTurn, + ), + ) + .expect("compact header"); + let compact_json: Value = serde_json::from_str(&compact_header).expect("json"); + assert_eq!(compact_json["request_kind"].as_str(), Some("compaction")); + assert_eq!(compact_json["turn_id"].as_str(), Some("turn-a")); + assert_eq!(compact_json[WINDOW_ID_KEY].as_str(), Some("thread-a:2")); + assert_eq!( + compact_json["compaction"], + serde_json::json!({ + "trigger": "auto", + "reason": "context_limit", + "implementation": "responses_compaction_v2", + "phase": "mid_turn", + "strategy": "memento", + }) + ); + + let regular_header = state + .current_header_value_for_model_request("thread-a:3") + .expect("regular header"); + let regular_json: Value = serde_json::from_str(®ular_header).expect("json"); + assert_eq!(regular_json["request_kind"].as_str(), Some("turn")); + assert_eq!(regular_json[WINDOW_ID_KEY].as_str(), Some("thread-a:3")); + assert!(regular_json.get("compaction").is_none()); } diff --git a/codex-rs/core/tests/suite/agent_websocket.rs b/codex-rs/core/tests/suite/agent_websocket.rs index 6fe697d2b1..c405e35995 100644 --- a/codex-rs/core/tests/suite/agent_websocket.rs +++ b/codex-rs/core/tests/suite/agent_websocket.rs @@ -95,6 +95,16 @@ async fn websocket_first_turn_uses_startup_prewarm_and_create() -> Result<()> { let turn = connection.get(1).expect("missing turn request").body_json(); assert_eq!(warmup["type"].as_str(), Some("response.create")); assert_eq!(warmup["generate"].as_bool(), Some(false)); + let warmup_metadata: Value = serde_json::from_str( + warmup["client_metadata"]["x-codex-turn-metadata"] + .as_str() + .expect("warmup turn metadata"), + )?; + assert_eq!(warmup_metadata["request_kind"].as_str(), Some("prewarm")); + assert_eq!( + warmup_metadata["window_id"].as_str(), + warmup["client_metadata"]["x-codex-window-id"].as_str() + ); assert!( turn["tools"] .as_array() @@ -102,6 +112,12 @@ async fn websocket_first_turn_uses_startup_prewarm_and_create() -> Result<()> { "expected request tools to be populated" ); assert_eq!(turn["type"].as_str(), Some("response.create")); + let turn_metadata: Value = serde_json::from_str( + turn["client_metadata"]["x-codex-turn-metadata"] + .as_str() + .expect("turn metadata"), + )?; + assert_eq!(turn_metadata["request_kind"].as_str(), Some("turn")); server.shutdown().await; Ok(()) diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index 20f4eae489..0e2c02de91 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -2788,11 +2788,58 @@ async fn manual_compact_twice_preserves_latest_user_messages() { contains_user_text(&requests[1], first_user_message), "first compact request should include history before compaction" ); + let compact_metadata: Value = serde_json::from_str( + &requests[1] + .header("x-codex-turn-metadata") + .expect("local compact request should include turn metadata"), + ) + .expect("local compact turn metadata should be valid json"); + assert_eq!( + compact_metadata["request_kind"].as_str(), + Some("compaction") + ); + assert_eq!( + compact_metadata["window_id"].as_str(), + requests[1].header("x-codex-window-id").as_deref() + ); + assert_eq!( + compact_metadata["compaction"], + json!({ + "trigger": "manual", + "reason": "user_requested", + "implementation": "responses", + "phase": "standalone_turn", + "strategy": "memento", + }) + ); assert!( contains_user_text(&requests[2], second_user_message), "second turn request missing second user message" ); + let next_turn_metadata: Value = serde_json::from_str( + &requests[2] + .header("x-codex-turn-metadata") + .expect("next regular request should include turn metadata"), + ) + .expect("next regular turn metadata should be valid json"); + assert_eq!( + next_turn_metadata["request_kind"].as_str(), + Some("turn"), + "regular requests after compaction should remain turn requests" + ); + assert_eq!( + next_turn_metadata["window_id"].as_str(), + requests[2].header("x-codex-window-id").as_deref() + ); + assert_ne!( + compact_metadata["window_id"], next_turn_metadata["window_id"], + "the next request should use the new compacted context window" + ); + assert!( + next_turn_metadata.get("compaction").is_none(), + "regular requests after compaction should not be marked as compact requests" + ); assert!( contains_user_text(&requests[2], first_user_message), "second turn request should include the compacted user history" diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index bd290cad41..1c5b80f35f 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -368,6 +368,36 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> { compact_request.header("thread-id").as_deref(), Some(thread_id.as_str()) ); + let compact_metadata: Value = serde_json::from_str( + &compact_request + .header("x-codex-turn-metadata") + .expect("remote compact request should include turn metadata"), + ) + .expect("remote compact turn metadata should be valid json"); + assert!( + compact_metadata["turn_id"] + .as_str() + .is_some_and(|id| !id.is_empty()), + "remote compact turn metadata should include its turn id" + ); + assert_eq!( + compact_metadata["request_kind"].as_str(), + Some("compaction") + ); + assert_eq!( + compact_metadata["window_id"].as_str(), + compact_request.header("x-codex-window-id").as_deref() + ); + assert_eq!( + compact_metadata["compaction"], + json!({ + "trigger": "manual", + "reason": "user_requested", + "implementation": "responses_compact", + "phase": "standalone_turn", + "strategy": "memento", + }) + ); let compact_body = compact_request.body_json(); assert_eq!( compact_body.get("model").and_then(|v| v.as_str()), @@ -375,6 +405,16 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> { ); let response_requests = responses_mock.requests(); let first_response_request = response_requests.first().expect("initial request missing"); + let first_response_metadata: Value = serde_json::from_str( + &first_response_request + .header("x-codex-turn-metadata") + .expect("initial request should include turn metadata"), + ) + .expect("initial turn metadata should be valid json"); + assert_ne!( + first_response_metadata["turn_id"], compact_metadata["turn_id"], + "manual compaction should use its own turn id" + ); assert_eq!( compact_body["tools"], first_response_request.body_json()["tools"], @@ -407,6 +447,33 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> { let response_requests = responses_mock.requests(); let follow_up_request = response_requests.last().expect("follow-up request missing"); + let follow_up_metadata: Value = serde_json::from_str( + &follow_up_request + .header("x-codex-turn-metadata") + .expect("follow-up request should include turn metadata"), + ) + .expect("follow-up turn metadata should be valid json"); + assert_eq!( + follow_up_metadata["request_kind"].as_str(), + Some("turn"), + "regular requests after compaction should remain turn requests" + ); + assert!( + follow_up_metadata.get("compaction").is_none(), + "regular requests after compaction should not be marked as compact requests" + ); + assert_ne!( + follow_up_metadata["turn_id"], compact_metadata["turn_id"], + "the following user turn should not reuse a manual compact turn id" + ); + assert_eq!( + follow_up_metadata["window_id"].as_str(), + follow_up_request.header("x-codex-window-id").as_deref() + ); + assert_ne!( + follow_up_metadata["window_id"], compact_metadata["window_id"], + "the following user turn should use the new compacted context window" + ); let follow_up_body = follow_up_request.body_json().to_string(); assert!( follow_up_body.contains("\"type\":\"compaction\""), @@ -797,6 +864,30 @@ async fn remote_compact_v2_reuses_compaction_trigger_for_followups() -> Result<( "expected compact request to advertise the remote_compaction_v2 beta feature" ); assert_eq!(compact_request.path(), "/v1/responses"); + let compact_metadata: Value = serde_json::from_str( + &compact_request + .header("x-codex-turn-metadata") + .expect("v2 compact request should include turn metadata"), + ) + .expect("v2 compact turn metadata should be valid json"); + assert_eq!( + compact_metadata["request_kind"].as_str(), + Some("compaction") + ); + assert_eq!( + compact_metadata["window_id"].as_str(), + compact_request.header("x-codex-window-id").as_deref() + ); + assert_eq!( + compact_metadata["compaction"], + json!({ + "trigger": "manual", + "reason": "user_requested", + "implementation": "responses_compaction_v2", + "phase": "standalone_turn", + "strategy": "memento", + }) + ); let compact_body = compact_request.body_json().to_string(); assert!( compact_body.contains("\"type\":\"compaction_trigger\""), @@ -1139,7 +1230,7 @@ async fn remote_compact_runs_automatically() -> Result<()> { let session_id = harness.test().session_configured.session_id.to_string(); let thread_id = harness.test().session_configured.thread_id.to_string(); - mount_sse_once( + let initial_request = mount_sse_once( harness.server(), sse(vec![ responses::ev_shell_command_call("m1", "echo 'hi'"), @@ -1195,7 +1286,63 @@ async fn remote_compact_runs_automatically() -> Result<()> { compact_mock.single_request().header("thread-id").as_deref(), Some(thread_id.as_str()) ); + let compact_metadata: Value = serde_json::from_str( + &compact_mock + .single_request() + .header("x-codex-turn-metadata") + .expect("auto remote compact request should include turn metadata"), + ) + .expect("auto remote compact turn metadata should be valid json"); + assert_eq!( + compact_metadata["request_kind"].as_str(), + Some("compaction") + ); + assert_eq!( + compact_metadata["compaction"], + json!({ + "trigger": "auto", + "reason": "context_limit", + "implementation": "responses_compact", + "phase": "mid_turn", + "strategy": "memento", + }) + ); + let initial_metadata: Value = serde_json::from_str( + &initial_request + .single_request() + .header("x-codex-turn-metadata") + .expect("initial request should include turn metadata"), + ) + .expect("initial turn metadata should be valid json"); + assert_eq!( + initial_metadata["turn_id"], compact_metadata["turn_id"], + "automatic mid-turn compaction should keep the current turn id" + ); + assert_eq!( + initial_metadata["window_id"], compact_metadata["window_id"], + "automatic mid-turn compaction summarizes the current context window" + ); let follow_up_request = responses_mock.single_request(); + let follow_up_metadata: Value = serde_json::from_str( + &follow_up_request + .header("x-codex-turn-metadata") + .expect("post-compaction continuation should include turn metadata"), + ) + .expect("post-compaction turn metadata should be valid json"); + assert_eq!( + follow_up_metadata["request_kind"].as_str(), + Some("turn"), + "post-compaction continuation should be a regular request" + ); + assert!(follow_up_metadata.get("compaction").is_none()); + assert_eq!( + follow_up_metadata["turn_id"], compact_metadata["turn_id"], + "automatic mid-turn continuation should keep the current turn id" + ); + assert_ne!( + follow_up_metadata["window_id"], compact_metadata["window_id"], + "post-compaction continuation should use the next context window" + ); let follow_up_body = follow_up_request.body_json().to_string(); assert!(follow_up_body.contains("REMOTE_COMPACTED_SUMMARY")); diff --git a/codex-rs/memories/write/src/runtime.rs b/codex-rs/memories/write/src/runtime.rs index 7bf2fe159d..2aeda32a55 100644 --- a/codex-rs/memories/write/src/runtime.rs +++ b/codex-rs/memories/write/src/runtime.rs @@ -153,7 +153,6 @@ impl MemoryStartupContext { StageOneRequestContext { model_info, - turn_metadata_header, session_telemetry: self .session_telemetry .clone() @@ -161,6 +160,7 @@ impl MemoryStartupContext { reasoning_effort: Some(reasoning_effort), reasoning_summary, service_tier: config_snapshot.service_tier, + turn_metadata_header, } } diff --git a/codex-rs/memories/write/src/startup_tests.rs b/codex-rs/memories/write/src/startup_tests.rs index 4af8560e67..1a735b3668 100644 --- a/codex-rs/memories/write/src/startup_tests.rs +++ b/codex-rs/memories/write/src/startup_tests.rs @@ -237,11 +237,13 @@ async fn memories_startup_phase2_prunes_old_extension_resources_without_stage1_i } #[tokio::test] -async fn memories_startup_phase1_uses_live_thread_service_tier() -> anyhow::Result<()> { +async fn memories_startup_phase1_uses_live_thread_service_tier_and_detached_metadata() +-> anyhow::Result<()> { let server = start_mock_server().await; let home = Arc::new(TempDir::new()?); let test = build_test_codex(&server, home).await?; assert_eq!(test.config.service_tier, None); + reset_git_repository(&test.config.cwd).await?; core_test_support::submit_thread_settings( &test.codex, @@ -279,6 +281,35 @@ async fn memories_startup_phase1_uses_live_thread_service_tier() -> anyhow::Resu Some(ServiceTier::Fast.request_value().to_string()) ); + let stage_one = mount_sse_once( + &server, + sse(vec![ + ev_response_created("resp-phase1"), + ev_assistant_message("msg-phase1", "phase1 complete"), + ev_completed("resp-phase1"), + ]), + ) + .await; + context + .stream_stage_one_prompt( + &test.config, + &codex_core::Prompt::default(), + &request_context, + ) + .await?; + let request = wait_for_single_request(&stage_one).await; + let metadata_header = request + .header("x-codex-turn-metadata") + .expect("detached memory request should include workspace metadata"); + let metadata: serde_json::Value = + serde_json::from_str(&metadata_header).expect("turn metadata json"); + assert_eq!(metadata["request_kind"].as_str(), Some("memory")); + assert!(metadata.get("session_id").is_none()); + assert!(metadata.get("thread_id").is_none()); + assert!(metadata.get("turn_id").is_none()); + assert!(metadata.get("window_id").is_none()); + assert!(metadata.get("workspaces").is_some()); + shutdown_test_codex(&test).await?; Ok(()) } From b1cbf622ad4b669bea2b45216284377b71131279 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Wed, 27 May 2026 11:11:04 -0700 Subject: [PATCH 29/87] [codex] Add friendly Python SDK sandbox presets (#24772) ## Why The Python SDK currently exposes sandbox selection differently depending on where it is used: thread lifecycle methods accept `SandboxMode`, while turns accept the lower-level `SandboxPolicy` shape. For the common case of choosing an access level, that leaks app-server wire details into otherwise straightforward SDK usage. This makes the common path explicit and discoverable: callers choose a named sandbox preset once, using the same keyword on threads and turns. The preset name `workspace_write` also makes the granted capability clear at the callsite. ## What changed - Added a root-level `Sandbox` enum with documented presets: - `Sandbox.read_only`: read files without allowing writes. - `Sandbox.workspace_write`: the normal default for projects with a recorded trust decision; read files and write inside the workspace and configured writable roots. - `Sandbox.full_access`: run without filesystem access restrictions. - Documented that omitting `sandbox=` delegates to app-server's configured default, while explicit turn overrides remain sticky for subsequent turns. - Updated sync and async thread lifecycle and turn APIs to consistently accept `sandbox=Sandbox...`, translating to the existing app-server thread and turn representations internally. - Updated the public API artifact generator so regenerated SDK wrappers retain the friendly enum shape. - Replaced low-level policy construction in Python docs, examples, and the walkthrough notebook with the preset API. - Added focused coverage for root exports, method signatures, preset-to-wire mapping, and rejection of raw string sandbox inputs. ## API impact High-level turn calls now use `sandbox=` instead of `sandbox_policy=`: ```python from openai_codex import Codex, Sandbox with Codex() as codex: thread = codex.thread_start(sandbox=Sandbox.workspace_write) result = thread.run("Review the diff only.", sandbox=Sandbox.read_only) ``` `thread_start(...)` already defaults to `ApprovalMode.auto_review`, so normal writable usage is concise: ```python with Codex() as codex: thread = codex.thread_start(sandbox=Sandbox.workspace_write) thread.run("Update the files in this workspace.") ``` With that combination, edits inside `cwd` and configured writable roots run within the workspace-write sandbox. Operations that require approval, such as edits outside those roots, are routed through auto review. When `sandbox=` is omitted, app-server resolves its configured default. A sandbox supplied to `run(...)` or `turn(...)` applies to that turn and subsequent turns. ## Test coverage - `sdk/python/tests/test_public_api_signatures.py` covers the public export and parameter names, including the default approval mode. - `sdk/python/tests/test_public_api_runtime_behavior.py` covers preset mappings to the existing wire types and raw string rejection. --- sdk/python/README.md | 27 ++++++- sdk/python/docs/api-reference.md | 42 +++++++--- sdk/python/docs/faq.md | 21 ++++- sdk/python/docs/getting-started.md | 40 ++++++++-- .../13_model_select_and_turn_params/async.py | 11 +-- .../13_model_select_and_turn_params/sync.py | 12 +-- sdk/python/notebooks/sdk_walkthrough.ipynb | 14 ++-- sdk/python/scripts/update_sdk_artifacts.py | 40 +++++++++- sdk/python/src/openai_codex/__init__.py | 2 + sdk/python/src/openai_codex/_sandbox.py | 78 +++++++++++++++++++ sdk/python/src/openai_codex/api.py | 43 +++++----- .../tests/test_public_api_runtime_behavior.py | 35 +++++++++ .../tests/test_public_api_signatures.py | 19 ++++- 13 files changed, 310 insertions(+), 74 deletions(-) create mode 100644 sdk/python/src/openai_codex/_sandbox.py diff --git a/sdk/python/README.md b/sdk/python/README.md index 5b8e6b8178..72290f38e0 100644 --- a/sdk/python/README.md +++ b/sdk/python/README.md @@ -23,12 +23,12 @@ when you intentionally want to run against a specific local app-server binary. ## Quickstart ```python -from openai_codex import Codex +from openai_codex import Codex, Sandbox with Codex() as codex: # Call login_api_key(...) first when this app-server session is not # already authenticated. - thread = codex.thread_start(model="gpt-5") + thread = codex.thread_start(model="gpt-5", sandbox=Sandbox.workspace_write) result = thread.run("Say hello in one sentence.") print(result.final_response) print(len(result.items)) @@ -38,6 +38,29 @@ with Codex() as codex: `final_response` is `None` when the turn completes without a final-answer or phase-less assistant message item. +## Sandbox + +Use the same enum when creating a thread or changing its sandbox for a turn: + +```python +from openai_codex import Codex, Sandbox + +with Codex() as codex: + thread = codex.thread_start(sandbox=Sandbox.workspace_write) + thread.run("Make the requested change.") + review = thread.run("Review the diff only.", sandbox=Sandbox.read_only) +``` + +Available presets: + +- `Sandbox.read_only`: read files without allowing writes. +- `Sandbox.workspace_write`: the normal default for projects with a recorded trust decision; read files and write inside the workspace and configured writable roots. +- `Sandbox.full_access`: run without filesystem access restrictions. + +When `sandbox=` is omitted, app-server uses its configured default. A sandbox +passed to `run(...)` or `turn(...)` applies to that turn and subsequent turns +on the thread. + ## Login Use the auth helper that matches your app: diff --git a/sdk/python/docs/api-reference.md b/sdk/python/docs/api-reference.md index db78ff5f1d..a353d32fa2 100644 --- a/sdk/python/docs/api-reference.md +++ b/sdk/python/docs/api-reference.md @@ -12,6 +12,7 @@ from openai_codex import ( Codex, AsyncCodex, ApprovalMode, + Sandbox, ChatgptLoginHandle, DeviceCodeLoginHandle, AsyncChatgptLoginHandle, @@ -63,10 +64,10 @@ Properties/methods: - `login_chatgpt_device_code() -> DeviceCodeLoginHandle` - `account(*, refresh_token: bool = False) -> GetAccountResponse` - `logout() -> None` -- `thread_start(*, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, ephemeral=None, model=None, model_provider=None, personality=None, sandbox=None) -> Thread` +- `thread_start(*, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, ephemeral=None, model=None, model_provider=None, personality=None, sandbox: Sandbox | None = None) -> Thread` - `thread_list(*, archived=None, cursor=None, cwd=None, limit=None, model_providers=None, sort_key=None, source_kinds=None) -> ThreadListResponse` -- `thread_resume(thread_id: str, *, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, model=None, model_provider=None, personality=None, sandbox=None) -> Thread` -- `thread_fork(thread_id: str, *, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, model=None, model_provider=None, sandbox=None) -> Thread` +- `thread_resume(thread_id: str, *, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, model=None, model_provider=None, personality=None, sandbox: Sandbox | None = None) -> Thread` +- `thread_fork(thread_id: str, *, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, model=None, model_provider=None, sandbox: Sandbox | None = None) -> Thread` - `thread_archive(thread_id: str) -> ThreadArchiveResponse` - `thread_unarchive(thread_id: str) -> Thread` - `models(*, include_hidden: bool = False) -> ModelListResponse` @@ -103,10 +104,10 @@ Properties/methods: - `login_chatgpt_device_code() -> Awaitable[AsyncDeviceCodeLoginHandle]` - `account(*, refresh_token: bool = False) -> Awaitable[GetAccountResponse]` - `logout() -> Awaitable[None]` -- `thread_start(*, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, ephemeral=None, model=None, model_provider=None, personality=None, sandbox=None) -> Awaitable[AsyncThread]` +- `thread_start(*, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, ephemeral=None, model=None, model_provider=None, personality=None, sandbox: Sandbox | None = None) -> Awaitable[AsyncThread]` - `thread_list(*, archived=None, cursor=None, cwd=None, limit=None, model_providers=None, sort_key=None, source_kinds=None) -> Awaitable[ThreadListResponse]` -- `thread_resume(thread_id: str, *, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, model=None, model_provider=None, personality=None, sandbox=None) -> Awaitable[AsyncThread]` -- `thread_fork(thread_id: str, *, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, ephemeral=None, model=None, model_provider=None, sandbox=None) -> Awaitable[AsyncThread]` +- `thread_resume(thread_id: str, *, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, model=None, model_provider=None, personality=None, sandbox: Sandbox | None = None) -> Awaitable[AsyncThread]` +- `thread_fork(thread_id: str, *, approval_mode=ApprovalMode.auto_review, base_instructions=None, config=None, cwd=None, developer_instructions=None, ephemeral=None, model=None, model_provider=None, sandbox: Sandbox | None = None) -> Awaitable[AsyncThread]` - `thread_archive(thread_id: str) -> Awaitable[ThreadArchiveResponse]` - `thread_unarchive(thread_id: str) -> Awaitable[AsyncThread]` - `models(*, include_hidden: bool = False) -> Awaitable[ModelListResponse]` @@ -148,16 +149,16 @@ attempt. API-key login completes synchronously and does not return a handle. ### Thread -- `run(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox_policy=None, service_tier=None, summary=None) -> TurnResult` -- `turn(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox_policy=None, service_tier=None, summary=None) -> TurnHandle` +- `run(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox: Sandbox | None = None, service_tier=None, summary=None) -> TurnResult` +- `turn(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox: Sandbox | None = None, service_tier=None, summary=None) -> TurnHandle` - `read(*, include_turns: bool = False) -> ThreadReadResponse` - `set_name(name: str) -> ThreadSetNameResponse` - `compact() -> ThreadCompactStartResponse` ### AsyncThread -- `run(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox_policy=None, service_tier=None, summary=None) -> Awaitable[TurnResult]` -- `turn(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox_policy=None, service_tier=None, summary=None) -> Awaitable[AsyncTurnHandle]` +- `run(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox: Sandbox | None = None, service_tier=None, summary=None) -> Awaitable[TurnResult]` +- `turn(input: str | Input, *, approval_mode=None, cwd=None, effort=None, model=None, output_schema=None, personality=None, sandbox: Sandbox | None = None, service_tier=None, summary=None) -> Awaitable[AsyncTurnHandle]` - `read(*, include_turns: bool = False) -> Awaitable[ThreadReadResponse]` - `set_name(name: str) -> Awaitable[ThreadSetNameResponse]` - `compact() -> Awaitable[ThreadCompactStartResponse]` @@ -182,6 +183,27 @@ phase-less assistant message item. Use `turn(...)` when you need low-level turn control (`stream()`, `steer()`, `interrupt()`) before collecting the turn result. +## Sandbox + +Use `sandbox=` consistently on thread lifecycle methods and turns: + +```python +from openai_codex import Codex, Sandbox + +with Codex() as codex: + thread = codex.thread_start(sandbox=Sandbox.workspace_write) + result = thread.run("Review the diff only.", sandbox=Sandbox.read_only) +``` + +Presets: + +- `Sandbox.read_only`: read files without allowing writes. +- `Sandbox.workspace_write`: the normal default for projects with a recorded trust decision; read files and write inside the workspace and configured writable roots. +- `Sandbox.full_access`: run without filesystem access restrictions. + +When `sandbox=` is omitted, app-server uses its configured default. A sandbox +passed to `run(...)` or `turn(...)` applies to that turn and subsequent turns. + ## TurnHandle / AsyncTurnHandle ### TurnHandle diff --git a/sdk/python/docs/faq.md b/sdk/python/docs/faq.md index 5f2ed87ffd..42f2cb1f6a 100644 --- a/sdk/python/docs/faq.md +++ b/sdk/python/docs/faq.md @@ -48,7 +48,26 @@ If you are migrating older code, update these names: - `sortKey` -> `sort_key` - `sourceKinds` -> `source_kinds` - `outputSchema` -> `output_schema` -- `sandboxPolicy` -> `sandbox_policy` + +## How do I choose sandbox access? + +Use the same `sandbox=` keyword for threads and turns: + +```python +from openai_codex import Sandbox + +thread = codex.thread_start(sandbox=Sandbox.workspace_write) +result = thread.run("Review only.", sandbox=Sandbox.read_only) +``` + +The presets are: + +- `Sandbox.read_only`: read files without allowing writes. +- `Sandbox.workspace_write`: the normal default for projects with a recorded trust decision; read files and write inside the workspace and configured writable roots. +- `Sandbox.full_access`: run without filesystem access restrictions. + +When `sandbox=` is omitted, app-server uses its configured default. A turn +sandbox override applies to that turn and subsequent turns. ## Why only `thread_start(...)` and `thread_resume(...)`? diff --git a/sdk/python/docs/getting-started.md b/sdk/python/docs/getting-started.md index b21db2a207..6113b9050a 100644 --- a/sdk/python/docs/getting-started.md +++ b/sdk/python/docs/getting-started.md @@ -26,7 +26,7 @@ Existing Codex auth state is reused automatically. To authenticate from the SDK, use the flow that fits your app: ```python -from openai_codex import Codex +from openai_codex import Codex, Sandbox with Codex() as codex: codex.login_api_key("sk-...") @@ -58,7 +58,11 @@ with Codex() as codex: server = codex.metadata.serverInfo print("Server:", None if server is None else server.name, None if server is None else server.version) - thread = codex.thread_start(model="gpt-5.4", config={"model_reasoning_effort": "high"}) + thread = codex.thread_start( + model="gpt-5.4", + config={"model_reasoning_effort": "high"}, + sandbox=Sandbox.workspace_write, + ) result = thread.run("Say hello in one sentence.") print("Thread:", thread.id) @@ -76,7 +80,29 @@ What happened: - use `thread.turn(...)` when you need a `TurnHandle` for streaming, steering, or interrupting before collecting `TurnResult` - one client can consume multiple active turns concurrently; turn streams are routed by turn ID -## 4) Continue the same thread (multi-turn) +## 4) Change sandbox access + +Use one enum for the initial sandbox and for later turn overrides: + +```python +from openai_codex import Codex, Sandbox + +with Codex() as codex: + thread = codex.thread_start(sandbox=Sandbox.workspace_write) + thread.run("Make the requested changes.") + review = thread.run("Review the diff only.", sandbox=Sandbox.read_only) +``` + +Available presets: + +- `Sandbox.read_only`: read files without allowing writes. +- `Sandbox.workspace_write`: the normal default for projects with a recorded trust decision; read files and write inside the workspace and configured writable roots. +- `Sandbox.full_access`: run without filesystem access restrictions. + +When `sandbox=` is omitted, app-server uses its configured default. A turn +override also becomes the sandbox for subsequent turns on that thread. + +## 5) Continue the same thread (multi-turn) ```python from openai_codex import Codex @@ -91,7 +117,7 @@ with Codex() as codex: print("second:", second.final_response) ``` -## 5) Async parity +## 6) Async parity Use `async with AsyncCodex()` as the normal async entrypoint. `AsyncCodex` initializes lazily, and context entry makes startup/shutdown explicit. @@ -111,7 +137,7 @@ async def main() -> None: asyncio.run(main()) ``` -## 6) Resume an existing thread +## 7) Resume an existing thread ```python from openai_codex import Codex @@ -124,7 +150,7 @@ with Codex() as codex: print(result.final_response) ``` -## 7) Public app-server types +## 8) Public app-server types The convenience wrappers live at the package root. Public app-server value and event types live under: @@ -133,7 +159,7 @@ event types live under: from openai_codex.types import ThreadReadResponse, Turn, TurnStatus ``` -## 8) Next stops +## 9) Next stops - API surface and signatures: `docs/api-reference.md` - Common decisions/pitfalls: `docs/faq.md` diff --git a/sdk/python/examples/13_model_select_and_turn_params/async.py b/sdk/python/examples/13_model_select_and_turn_params/async.py index 144169c46c..d9e706f240 100644 --- a/sdk/python/examples/13_model_select_and_turn_params/async.py +++ b/sdk/python/examples/13_model_select_and_turn_params/async.py @@ -13,12 +13,12 @@ import asyncio from openai_codex import ( AsyncCodex, + Sandbox, ) from openai_codex.types import ( Personality, ReasoningEffort, ReasoningSummary, - SandboxPolicy, ) REASONING_RANK = { @@ -67,13 +67,6 @@ OUTPUT_SCHEMA = { "additionalProperties": False, } -SANDBOX_POLICY = SandboxPolicy.model_validate( - { - "type": "readOnly", - "access": {"type": "fullAccess"}, - } -) - async def main() -> None: async with AsyncCodex(config=runtime_config()) as codex: @@ -106,7 +99,7 @@ async def main() -> None: model=selected_model.model, output_schema=OUTPUT_SCHEMA, personality=Personality.pragmatic, - sandbox_policy=SANDBOX_POLICY, + sandbox=Sandbox.read_only, summary=ReasoningSummary.model_validate("concise"), ) second = await second_turn.run() diff --git a/sdk/python/examples/13_model_select_and_turn_params/sync.py b/sdk/python/examples/13_model_select_and_turn_params/sync.py index 962665363f..4a9afa0980 100644 --- a/sdk/python/examples/13_model_select_and_turn_params/sync.py +++ b/sdk/python/examples/13_model_select_and_turn_params/sync.py @@ -11,12 +11,12 @@ ensure_local_sdk_src() from openai_codex import ( Codex, + Sandbox, ) from openai_codex.types import ( Personality, ReasoningEffort, ReasoningSummary, - SandboxPolicy, ) REASONING_RANK = { @@ -65,14 +65,6 @@ OUTPUT_SCHEMA = { "additionalProperties": False, } -SANDBOX_POLICY = SandboxPolicy.model_validate( - { - "type": "readOnly", - "access": {"type": "fullAccess"}, - } -) - - with Codex(config=runtime_config()) as codex: models = codex.models(include_hidden=True) selected_model = _pick_highest_model(models.data) @@ -102,7 +94,7 @@ with Codex(config=runtime_config()) as codex: model=selected_model.model, output_schema=OUTPUT_SCHEMA, personality=Personality.pragmatic, - sandbox_policy=SANDBOX_POLICY, + sandbox=Sandbox.read_only, summary=ReasoningSummary.model_validate("concise"), ).run() diff --git a/sdk/python/notebooks/sdk_walkthrough.ipynb b/sdk/python/notebooks/sdk_walkthrough.ipynb index bd313a02b1..1bb9bf731f 100644 --- a/sdk/python/notebooks/sdk_walkthrough.ipynb +++ b/sdk/python/notebooks/sdk_walkthrough.ipynb @@ -220,11 +220,11 @@ "source": [ "# Cell 5b: one turn with most optional turn params\n", "from pathlib import Path\n", - "from openai_codex import (\n", + "from openai_codex import Sandbox\n", + "from openai_codex.types import (\n", " Personality,\n", " ReasoningEffort,\n", " ReasoningSummary,\n", - " SandboxPolicy,\n", ")\n", "\n", "output_schema = {\n", @@ -237,7 +237,6 @@ " 'additionalProperties': False,\n", "}\n", "\n", - "sandbox_policy = SandboxPolicy.model_validate({'type': 'readOnly', 'access': {'type': 'fullAccess'}})\n", "summary = ReasoningSummary.model_validate('concise')\n", "\n", "with Codex() as codex:\n", @@ -249,7 +248,7 @@ " model='gpt-5.4',\n", " output_schema=output_schema,\n", " personality=Personality.pragmatic,\n", - " sandbox_policy=sandbox_policy,\n", + " sandbox=Sandbox.read_only,\n", " summary=summary,\n", " )\n", " result = turn.run()\n", @@ -266,11 +265,11 @@ "source": [ "# Cell 5c: choose highest model + highest supported reasoning, then run turns\n", "from pathlib import Path\n", - "from openai_codex import (\n", + "from openai_codex import Sandbox\n", + "from openai_codex.types import (\n", " Personality,\n", " ReasoningEffort,\n", " ReasoningSummary,\n", - " SandboxPolicy,\n", ")\n", "\n", "reasoning_rank = {\n", @@ -310,7 +309,6 @@ " 'required': ['summary', 'actions'],\n", " 'additionalProperties': False,\n", "}\n", - "sandbox_policy = SandboxPolicy.model_validate({'type': 'readOnly', 'access': {'type': 'fullAccess'}})\n", "\n", "with Codex() as codex:\n", " models = codex.models(include_hidden=True)\n", @@ -337,7 +335,7 @@ " model=selected_model.model,\n", " output_schema=output_schema,\n", " personality=Personality.pragmatic,\n", - " sandbox_policy=sandbox_policy,\n", + " sandbox=Sandbox.read_only,\n", " summary=ReasoningSummary.model_validate('concise'),\n", " ).run()\n", " print('agent.message.params:', second.final_response)\n", diff --git a/sdk/python/scripts/update_sdk_artifacts.py b/sdk/python/scripts/update_sdk_artifacts.py index e88c45521f..b20f814f66 100755 --- a/sdk/python/scripts/update_sdk_artifacts.py +++ b/sdk/python/scripts/update_sdk_artifacts.py @@ -875,7 +875,41 @@ def _approval_mode_model_arg_lines(*, indent: str = " ") -> list[str] def _model_arg_lines(fields: list[PublicFieldSpec], *, indent: str = " ") -> list[str]: - return [f"{indent}{field.wire_name}={field.py_name}," for field in fields] + lines: list[str] = [] + for field in fields: + arg = field.py_name + if field.wire_name == "sandbox": + arg = "_sandbox_mode(sandbox)" + elif field.wire_name == "sandbox_policy": + arg = "_sandbox_policy(sandbox)" + lines.append(f"{indent}{field.wire_name}={arg},") + return lines + + +def _replace_public_sandbox_field( + fields: list[PublicFieldSpec], *, wire_name: str +) -> list[PublicFieldSpec]: + """Expose stable wire sandbox settings through one public enum parameter.""" + public_fields: list[PublicFieldSpec] = [] + replaced = False + for field in fields: + if field.wire_name != wire_name: + public_fields.append(field) + continue + if replaced: + raise RuntimeError(f"Found more than one generated sandbox field named {wire_name}") + public_fields.append( + PublicFieldSpec( + wire_name=wire_name, + py_name="sandbox", + annotation="Sandbox | None", + required=False, + ) + ) + replaced = True + if not replaced: + raise RuntimeError(f"Could not find generated sandbox field named {wire_name}") + return public_fields def _replace_generated_block(source: str, block_name: str, body: str) -> str: @@ -1113,6 +1147,7 @@ def generate_public_api_flat_methods() -> None: "ThreadStartParams", exclude=approval_fields, ) + thread_start_fields = _replace_public_sandbox_field(thread_start_fields, wire_name="sandbox") thread_list_fields = _load_public_fields( "openai_codex.generated.v2_all", "ThreadListParams", @@ -1122,16 +1157,19 @@ def generate_public_api_flat_methods() -> None: "ThreadResumeParams", exclude={"thread_id", *approval_fields}, ) + thread_resume_fields = _replace_public_sandbox_field(thread_resume_fields, wire_name="sandbox") thread_fork_fields = _load_public_fields( "openai_codex.generated.v2_all", "ThreadForkParams", exclude={"thread_id", *approval_fields}, ) + thread_fork_fields = _replace_public_sandbox_field(thread_fork_fields, wire_name="sandbox") turn_start_fields = _load_public_fields( "openai_codex.generated.v2_all", "TurnStartParams", exclude={"thread_id", "input", *approval_fields}, ) + turn_start_fields = _replace_public_sandbox_field(turn_start_fields, wire_name="sandbox_policy") source = public_api_path.read_text() source = _replace_generated_block( diff --git a/sdk/python/src/openai_codex/__init__.py b/sdk/python/src/openai_codex/__init__.py index 3bc018a769..9f1ca770c5 100644 --- a/sdk/python/src/openai_codex/__init__.py +++ b/sdk/python/src/openai_codex/__init__.py @@ -15,6 +15,7 @@ from .api import ( LocalImageInput, MentionInput, RunInput, + Sandbox, SkillInput, TextInput, Thread, @@ -44,6 +45,7 @@ __all__ = [ "Codex", "AsyncCodex", "ApprovalMode", + "Sandbox", "ChatgptLoginHandle", "DeviceCodeLoginHandle", "AsyncChatgptLoginHandle", diff --git a/sdk/python/src/openai_codex/_sandbox.py b/sdk/python/src/openai_codex/_sandbox.py new file mode 100644 index 0000000000..24aeb57c85 --- /dev/null +++ b/sdk/python/src/openai_codex/_sandbox.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +from enum import Enum +from typing import NoReturn + +from .generated.v2_all import ( + DangerFullAccessSandboxPolicy, + ReadOnlySandboxPolicy, + SandboxMode, + SandboxPolicy, + WorkspaceWriteSandboxPolicy, +) + + +class Sandbox(str, Enum): + """Preset filesystem access levels for threads and turns. + + `read_only` allows file reads without writes. `workspace_write` is the + normal default for projects with a recorded trust decision and allows + writes inside the workspace and configured writable roots. `full_access` + removes filesystem access restrictions. + """ + + read_only = "read-only" + workspace_write = "workspace-write" + full_access = "full-access" + + +def _require_sandbox(sandbox: Sandbox) -> None: + if isinstance(sandbox, Sandbox): + return + options = ", ".join(f"Sandbox.{value.name}" for value in Sandbox) + raise ValueError(f"sandbox must be one of: {options}") + + +def _sandbox_mode(sandbox: Sandbox | None) -> SandboxMode | None: + """Translate a public preset to the thread lifecycle wire mode.""" + if sandbox is None: + return None + _require_sandbox(sandbox) + + match sandbox: + case Sandbox.read_only: + return SandboxMode.read_only + case Sandbox.workspace_write: + return SandboxMode.workspace_write + case Sandbox.full_access: + return SandboxMode.danger_full_access + case _: + return _assert_never_sandbox(sandbox) + + +def _sandbox_policy(sandbox: Sandbox | None) -> SandboxPolicy | None: + """Translate a public preset to the turn override wire policy.""" + if sandbox is None: + return None + _require_sandbox(sandbox) + + match sandbox: + case Sandbox.read_only: + return SandboxPolicy( + root=ReadOnlySandboxPolicy(type="readOnly"), + ) + case Sandbox.workspace_write: + return SandboxPolicy( + root=WorkspaceWriteSandboxPolicy(type="workspaceWrite"), + ) + case Sandbox.full_access: + return SandboxPolicy( + root=DangerFullAccessSandboxPolicy(type="dangerFullAccess"), + ) + case _: + return _assert_never_sandbox(sandbox) + + +def _assert_never_sandbox(sandbox: NoReturn) -> NoReturn: + """Make sandbox mapping exhaustive for static type checkers.""" + raise AssertionError(f"Unhandled sandbox: {sandbox!r}") diff --git a/sdk/python/src/openai_codex/api.py b/sdk/python/src/openai_codex/api.py index f14a311789..6b6ddf617e 100644 --- a/sdk/python/src/openai_codex/api.py +++ b/sdk/python/src/openai_codex/api.py @@ -37,6 +37,7 @@ from ._run import ( _collect_async_turn_result, _collect_turn_result, ) +from ._sandbox import Sandbox as Sandbox, _sandbox_mode, _sandbox_policy from .async_client import AsyncAppServerClient from .client import AppServerClient, AppServerConfig from .generated.v2_all import ( @@ -48,8 +49,6 @@ from .generated.v2_all import ( Personality, ReasoningEffort, ReasoningSummary, - SandboxMode, - SandboxPolicy, SortDirection, ThreadArchiveResponse, ThreadCompactStartResponse, @@ -138,7 +137,7 @@ class Codex: model: str | None = None, model_provider: str | None = None, personality: Personality | None = None, - sandbox: SandboxMode | None = None, + sandbox: Sandbox | None = None, service_name: str | None = None, service_tier: str | None = None, session_start_source: ThreadStartSource | None = None, @@ -156,7 +155,7 @@ class Codex: model=model, model_provider=model_provider, personality=personality, - sandbox=sandbox, + sandbox=_sandbox_mode(sandbox), service_name=service_name, service_tier=service_tier, session_start_source=session_start_source, @@ -205,7 +204,7 @@ class Codex: model: str | None = None, model_provider: str | None = None, personality: Personality | None = None, - sandbox: SandboxMode | None = None, + sandbox: Sandbox | None = None, service_tier: str | None = None, ) -> Thread: approval_policy, approvals_reviewer = _approval_mode_override_settings(approval_mode) @@ -220,7 +219,7 @@ class Codex: model=model, model_provider=model_provider, personality=personality, - sandbox=sandbox, + sandbox=_sandbox_mode(sandbox), service_tier=service_tier, ) resumed = self._client.thread_resume(thread_id, params) @@ -238,7 +237,7 @@ class Codex: ephemeral: bool | None = None, model: str | None = None, model_provider: str | None = None, - sandbox: SandboxMode | None = None, + sandbox: Sandbox | None = None, service_tier: str | None = None, thread_source: ThreadSource | None = None, ) -> Thread: @@ -254,7 +253,7 @@ class Codex: ephemeral=ephemeral, model=model, model_provider=model_provider, - sandbox=sandbox, + sandbox=_sandbox_mode(sandbox), service_tier=service_tier, thread_source=thread_source, ) @@ -371,7 +370,7 @@ class AsyncCodex: model: str | None = None, model_provider: str | None = None, personality: Personality | None = None, - sandbox: SandboxMode | None = None, + sandbox: Sandbox | None = None, service_name: str | None = None, service_tier: str | None = None, session_start_source: ThreadStartSource | None = None, @@ -390,7 +389,7 @@ class AsyncCodex: model=model, model_provider=model_provider, personality=personality, - sandbox=sandbox, + sandbox=_sandbox_mode(sandbox), service_name=service_name, service_tier=service_tier, session_start_source=session_start_source, @@ -440,7 +439,7 @@ class AsyncCodex: model: str | None = None, model_provider: str | None = None, personality: Personality | None = None, - sandbox: SandboxMode | None = None, + sandbox: Sandbox | None = None, service_tier: str | None = None, ) -> AsyncThread: await self._ensure_initialized() @@ -456,7 +455,7 @@ class AsyncCodex: model=model, model_provider=model_provider, personality=personality, - sandbox=sandbox, + sandbox=_sandbox_mode(sandbox), service_tier=service_tier, ) resumed = await self._client.thread_resume(thread_id, params) @@ -474,7 +473,7 @@ class AsyncCodex: ephemeral: bool | None = None, model: str | None = None, model_provider: str | None = None, - sandbox: SandboxMode | None = None, + sandbox: Sandbox | None = None, service_tier: str | None = None, thread_source: ThreadSource | None = None, ) -> AsyncThread: @@ -491,7 +490,7 @@ class AsyncCodex: ephemeral=ephemeral, model=model, model_provider=model_provider, - sandbox=sandbox, + sandbox=_sandbox_mode(sandbox), service_tier=service_tier, thread_source=thread_source, ) @@ -529,7 +528,7 @@ class Thread: model: str | None = None, output_schema: JsonObject | None = None, personality: Personality | None = None, - sandbox_policy: SandboxPolicy | None = None, + sandbox: Sandbox | None = None, service_tier: str | None = None, summary: ReasoningSummary | None = None, ) -> TurnResult: @@ -541,7 +540,7 @@ class Thread: model=model, output_schema=output_schema, personality=personality, - sandbox_policy=sandbox_policy, + sandbox=sandbox, service_tier=service_tier, summary=summary, ) @@ -562,7 +561,7 @@ class Thread: model: str | None = None, output_schema: JsonObject | None = None, personality: Personality | None = None, - sandbox_policy: SandboxPolicy | None = None, + sandbox: Sandbox | None = None, service_tier: str | None = None, summary: ReasoningSummary | None = None, ) -> TurnHandle: @@ -578,7 +577,7 @@ class Thread: model=model, output_schema=output_schema, personality=personality, - sandbox_policy=sandbox_policy, + sandbox_policy=_sandbox_policy(sandbox), service_tier=service_tier, summary=summary, ) @@ -612,7 +611,7 @@ class AsyncThread: model: str | None = None, output_schema: JsonObject | None = None, personality: Personality | None = None, - sandbox_policy: SandboxPolicy | None = None, + sandbox: Sandbox | None = None, service_tier: str | None = None, summary: ReasoningSummary | None = None, ) -> TurnResult: @@ -624,7 +623,7 @@ class AsyncThread: model=model, output_schema=output_schema, personality=personality, - sandbox_policy=sandbox_policy, + sandbox=sandbox, service_tier=service_tier, summary=summary, ) @@ -645,7 +644,7 @@ class AsyncThread: model: str | None = None, output_schema: JsonObject | None = None, personality: Personality | None = None, - sandbox_policy: SandboxPolicy | None = None, + sandbox: Sandbox | None = None, service_tier: str | None = None, summary: ReasoningSummary | None = None, ) -> AsyncTurnHandle: @@ -662,7 +661,7 @@ class AsyncThread: model=model, output_schema=output_schema, personality=personality, - sandbox_policy=sandbox_policy, + sandbox_policy=_sandbox_policy(sandbox), service_tier=service_tier, summary=summary, ) diff --git a/sdk/python/tests/test_public_api_runtime_behavior.py b/sdk/python/tests/test_public_api_runtime_behavior.py index 4e79cb6104..f6d83a965e 100644 --- a/sdk/python/tests/test_public_api_runtime_behavior.py +++ b/sdk/python/tests/test_public_api_runtime_behavior.py @@ -11,6 +11,7 @@ from openai_codex.api import ( ApprovalMode, AsyncCodex, Codex, + Sandbox, ) from openai_codex.generated.v2_all import TurnStartParams from openai_codex.models import InitializeResponse @@ -158,6 +159,40 @@ def test_unknown_approval_mode_is_rejected() -> None: public_api_module._approval_mode_settings("allow_all") # type: ignore[arg-type] +def test_sandbox_presets_serialize_for_threads_and_turns() -> None: + """One public sandbox enum should map to both stable wire representations.""" + assert { + sandbox.name: public_api_module._sandbox_mode(sandbox).value for sandbox in Sandbox + } == { + "read_only": "read-only", + "workspace_write": "workspace-write", + "full_access": "danger-full-access", + } + assert { + sandbox.name: public_api_module._sandbox_policy(sandbox).model_dump( + by_alias=True, + mode="json", + ) + for sandbox in Sandbox + } == { + "read_only": {"networkAccess": False, "type": "readOnly"}, + "workspace_write": { + "excludeSlashTmp": False, + "excludeTmpdirEnvVar": False, + "networkAccess": False, + "type": "workspaceWrite", + "writableRoots": [], + }, + "full_access": {"type": "dangerFullAccess"}, + } + + +def test_raw_sandbox_strings_are_rejected() -> None: + """Callers should use the discoverable enum rather than memorizing values.""" + with pytest.raises(ValueError, match="Sandbox\\.workspace_write"): + public_api_module._sandbox_mode("workspace") # type: ignore[arg-type] + + def test_retry_examples_compare_status_with_enum() -> None: for path in ( ROOT / "examples" / "10_error_handling_and_retry" / "sync.py", diff --git a/sdk/python/tests/test_public_api_signatures.py b/sdk/python/tests/test_public_api_signatures.py index 9a1212dfdf..b4dc49acf1 100644 --- a/sdk/python/tests/test_public_api_signatures.py +++ b/sdk/python/tests/test_public_api_signatures.py @@ -16,6 +16,7 @@ from openai_codex import ( AsyncThread, AsyncTurnHandle, Codex, + Sandbox, Thread, TurnHandle, TurnResult, @@ -29,6 +30,7 @@ EXPECTED_ROOT_EXPORTS = [ "Codex", "AsyncCodex", "ApprovalMode", + "Sandbox", "ChatgptLoginHandle", "DeviceCodeLoginHandle", "AsyncChatgptLoginHandle", @@ -191,6 +193,15 @@ def test_root_exports_approval_mode() -> None: ] +def test_root_exports_sandbox_presets() -> None: + """The friendly sandbox API should expose only obvious named presets.""" + assert [(sandbox.name, sandbox.value) for sandbox in Sandbox] == [ + ("read_only", "read-only"), + ("workspace_write", "workspace-write"), + ("full_access", "full-access"), + ] + + def test_package_and_default_client_versions_follow_project_version() -> None: """The importable package version should stay aligned with pyproject metadata.""" pyproject_path = Path(__file__).resolve().parents[1] / "pyproject.toml" @@ -341,7 +352,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None: "model", "output_schema", "personality", - "sandbox_policy", + "sandbox", "service_tier", "summary", ], @@ -352,7 +363,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None: "model", "output_schema", "personality", - "sandbox_policy", + "sandbox", "service_tier", "summary", ], @@ -416,7 +427,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None: "model", "output_schema", "personality", - "sandbox_policy", + "sandbox", "service_tier", "summary", ], @@ -427,7 +438,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None: "model", "output_schema", "personality", - "sandbox_policy", + "sandbox", "service_tier", "summary", ], From 8d398d3c5230fa69557b258264f3c6eb21fff9aa Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Wed, 27 May 2026 15:15:03 -0300 Subject: [PATCH 30/87] feat(tui): add vim text object bindings (#24382) ## Why Vim mode currently supports some normal-mode operators and motions, but common text-object combinations like `ciw`, `daw`, `di(`, and quote/bracket variants are still missing. That makes the composer feel incomplete for users who expect operator + text object editing to work inside prompts. Closes #21383. ## What Changed - Add Vim pending-state support for operator/text-object sequences. - Add `c` as a normal-mode operator for text objects, so combinations like `ciw` delete the object and enter insert mode. - Support word, WORD, delimiter, and quote text objects: - `iw`, `aw`, `iW`, `aW` - `i(`, `a(`, `i)`, `a)`, `ib`, `ab` - `i[`, `a[`, `i]`, `a]` - `i{`, `a{`, `i}`, `a}`, `iB`, `aB` - `i"`, `a"`, `i'`, `a'`, `i\``, `a\`` - Add configurable keymap entries and keymap picker coverage for the new Vim text-object context. - Regenerate the config schema and update keymap picker snapshots. ## How to Test Manual smoke test: 1. Start Codex with Vim composer mode enabled. 2. Type a draft such as: ```text alpha beta gamma call(foo[bar], {"x": "hello world"}) say "one \"two\" three" now ``` 3. Put the cursor on `beta`, press `ciw`, and confirm `beta` is removed and the composer enters insert mode. 4. Escape back to normal mode, put the cursor on `gamma`, press `daw`, and confirm `gamma` plus surrounding whitespace is removed. 5. Put the cursor inside `foo[bar]`, press `di[`, and confirm only `bar` is removed. 6. Put the cursor inside `call(...)`, press `da(`, and confirm the whole parenthesized section is removed. 7. Put the cursor inside the quoted text, press `ci"`, and confirm the quote contents are removed and insert mode starts. 8. Verify cancellation does not edit text: press `d` then `Esc`, and press `d` then `i` then `Esc`. Targeted tests: - `cargo test -p codex-tui --lib vim_` - `cargo nextest run -p codex-tui keymap_setup::tests` Additional local checks: - `just write-config-schema` - `just fmt` - `just fix -p codex-tui` - `git diff --check` - `cargo insta pending-snapshots --manifest-path tui/Cargo.toml` Local full-suite note: `just test -p codex-tui` ran to completion. The keymap snapshot failures were expected and accepted. Two unrelated guardian feature-flag tests still fail locally: - `app::tests::update_feature_flags_disabling_guardian_clears_review_policy_and_restores_default` - `app::tests::update_feature_flags_disabling_guardian_clears_manual_review_policy_without_history` `just argument-comment-lint` is currently blocked locally by Bazel analysis before the lint runs because `compiler-rt` has an empty `include/sanitizer/*.h` glob in the local Bazel cache. The touched Rust diff was manually inspected for opaque positional literals. --- codex-rs/config/src/tui_keymap.rs | 47 +++ codex-rs/core/config.schema.json | 138 +++++++ codex-rs/tui/src/bottom_pane/textarea.rs | 308 ++++++++++++--- codex-rs/tui/src/bottom_pane/textarea/vim.rs | 320 +++++++++++++++ codex-rs/tui/src/keymap.rs | 366 +++++++++++++++++- codex-rs/tui/src/keymap_setup/actions.rs | 36 ++ codex-rs/tui/src/keymap_setup/picker.rs | 2 +- ...ap_setup__tests__keymap_picker_custom.snap | 2 +- ...ests__keymap_picker_fast_mode_enabled.snap | 2 +- ...p__tests__keymap_picker_first_actions.snap | 4 +- ...ap_setup__tests__keymap_picker_narrow.snap | 2 +- ...ymap_setup__tests__keymap_picker_wide.snap | 2 +- 12 files changed, 1148 insertions(+), 81 deletions(-) create mode 100644 codex-rs/tui/src/bottom_pane/textarea/vim.rs diff --git a/codex-rs/config/src/tui_keymap.rs b/codex-rs/config/src/tui_keymap.rs index 5848d1f444..e41ec860ee 100644 --- a/codex-rs/config/src/tui_keymap.rs +++ b/codex-rs/config/src/tui_keymap.rs @@ -233,6 +233,8 @@ pub struct TuiVimNormalKeymap { pub start_delete_operator: Option, /// Begin yank operator; next key selects motion (`y`). pub start_yank_operator: Option, + /// Begin change operator; next keys select a text object. + pub start_change_operator: Option, /// Cancel a pending operator and return to normal mode. pub cancel_operator: Option, } @@ -268,10 +270,39 @@ pub struct TuiVimOperatorKeymap { pub motion_line_start: Option, /// Motion: to end of line (`$`). pub motion_line_end: Option, + /// Select an inner text object after an operator. + pub select_inner_text_object: Option, + /// Select an around text object after an operator. + pub select_around_text_object: Option, /// Cancel the pending operator and return to normal mode. pub cancel: Option, } +/// Vim text-object keybindings for modal editing inside text areas. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)] +#[serde(deny_unknown_fields)] +#[schemars(deny_unknown_fields)] +pub struct TuiVimTextObjectKeymap { + /// Text object: word. + pub word: Option, + /// Text object: whitespace-delimited WORD. + pub big_word: Option, + /// Text object: parentheses. + pub parentheses: Option, + /// Text object: brackets. + pub brackets: Option, + /// Text object: braces. + pub braces: Option, + /// Text object: double quotes. + pub double_quote: Option, + /// Text object: single quotes. + pub single_quote: Option, + /// Text object: backticks. + pub backtick: Option, + /// Cancel the pending text-object command. + pub cancel: Option, +} + /// Pager context keybindings for transcript and static overlays. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)] #[serde(deny_unknown_fields)] @@ -376,6 +407,8 @@ pub struct TuiKeymap { #[serde(default)] pub vim_operator: TuiVimOperatorKeymap, #[serde(default)] + pub vim_text_object: TuiVimTextObjectKeymap, + #[serde(default)] pub pager: TuiPagerKeymap, #[serde(default)] pub list: TuiListKeymap, @@ -562,6 +595,20 @@ mod tests { ); } + #[test] + fn misspelled_vim_text_object_action_is_rejected() { + let toml_input = r#" + [vim_text_object] + double_quotes = "shift-quote" + "#; + let err = toml::from_str::(toml_input) + .expect_err("expected unknown vim text object action"); + assert!( + err.to_string().contains("double_quotes"), + "expected error to mention misspelled field, got: {err}" + ); + } + #[test] fn removed_backtrack_actions_are_rejected() { for (context, action) in [ diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 06b7c56d5b..f8ffe9c832 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -2851,6 +2851,7 @@ "open_line_above": null, "open_line_below": null, "paste_after": null, + "start_change_operator": null, "start_delete_operator": null, "start_yank_operator": null, "yank_line": null @@ -2867,7 +2868,20 @@ "motion_word_backward": null, "motion_word_end": null, "motion_word_forward": null, + "select_around_text_object": null, + "select_inner_text_object": null, "yank_line": null + }, + "vim_text_object": { + "backtick": null, + "big_word": null, + "braces": null, + "brackets": null, + "cancel": null, + "double_quote": null, + "parentheses": null, + "single_quote": null, + "word": null } }, "description": "Keybinding overrides for the TUI.\n\nThis supports rebinding selected actions globally and by context. Context bindings take precedence over `global` bindings." @@ -3518,6 +3532,7 @@ "open_line_above": null, "open_line_below": null, "paste_after": null, + "start_change_operator": null, "start_delete_operator": null, "start_yank_operator": null, "yank_line": null @@ -3541,8 +3556,28 @@ "motion_word_backward": null, "motion_word_end": null, "motion_word_forward": null, + "select_around_text_object": null, + "select_inner_text_object": null, "yank_line": null } + }, + "vim_text_object": { + "allOf": [ + { + "$ref": "#/definitions/TuiVimTextObjectKeymap" + } + ], + "default": { + "backtick": null, + "big_word": null, + "braces": null, + "brackets": null, + "cancel": null, + "double_quote": null, + "parentheses": null, + "single_quote": null, + "word": null + } } }, "type": "object" @@ -3903,6 +3938,14 @@ ], "description": "Paste after cursor (`p`)." }, + "start_change_operator": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Begin change operator; next keys select a text object." + }, "start_delete_operator": { "allOf": [ { @@ -4022,6 +4065,22 @@ ], "description": "Motion: to start of next word (`w`)." }, + "select_around_text_object": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Select an around text object after an operator." + }, + "select_inner_text_object": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Select an inner text object after an operator." + }, "yank_line": { "allOf": [ { @@ -4033,6 +4092,85 @@ }, "type": "object" }, + "TuiVimTextObjectKeymap": { + "additionalProperties": false, + "description": "Vim text-object keybindings for modal editing inside text areas.", + "properties": { + "backtick": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Text object: backticks." + }, + "big_word": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Text object: whitespace-delimited WORD." + }, + "braces": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Text object: braces." + }, + "brackets": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Text object: brackets." + }, + "cancel": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Cancel the pending text-object command." + }, + "double_quote": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Text object: double quotes." + }, + "parentheses": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Text object: parentheses." + }, + "single_quote": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Text object: single quotes." + }, + "word": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Text object: word." + } + }, + "type": "object" + }, "UriBasedFileOpener": { "oneOf": [ { diff --git a/codex-rs/tui/src/bottom_pane/textarea.rs b/codex-rs/tui/src/bottom_pane/textarea.rs index d931b14b2c..39da5752c2 100644 --- a/codex-rs/tui/src/bottom_pane/textarea.rs +++ b/codex-rs/tui/src/bottom_pane/textarea.rs @@ -16,6 +16,7 @@ use crate::keymap::EditorKeymap; use crate::keymap::RuntimeKeymap; use crate::keymap::VimNormalKeymap; use crate::keymap::VimOperatorKeymap; +use crate::keymap::VimTextObjectKeymap; use codex_protocol::user_input::ByteRange; use codex_protocol::user_input::TextElement as UserTextElement; use crossterm::event::KeyCode; @@ -35,6 +36,13 @@ use textwrap::Options; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; +mod vim; +use self::vim::VimMode; +use self::vim::VimMotion; +use self::vim::VimOperator; +use self::vim::VimPending; +use self::vim::VimTextObjectScope; + const WORD_SEPARATORS: &str = "`~!@#$%^&*()-=+[{]}\\|;:'\",.<>/?"; fn is_word_separator(ch: char) -> bool { @@ -101,10 +109,11 @@ pub(crate) struct TextArea { kill_buffer_kind: KillBufferKind, vim_enabled: bool, vim_mode: VimMode, - vim_operator: Option, + vim_pending: VimPending, editor_keymap: EditorKeymap, vim_normal_keymap: VimNormalKeymap, vim_operator_keymap: VimOperatorKeymap, + vim_text_object_keymap: VimTextObjectKeymap, } #[derive(Debug, Clone)] @@ -119,14 +128,6 @@ pub(crate) struct TextAreaState { scroll: u16, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum VimMode { - /// Normal mode routes printable keys to movement, operators, and mode transitions. - Normal, - /// Insert mode routes input through the regular editor keymap until Escape is pressed. - Insert, -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum KillBufferKind { /// Characterwise kills and yanks paste at the cursor. @@ -135,36 +136,6 @@ enum KillBufferKind { Linewise, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum VimOperator { - /// Delete the range selected by the next motion or repeated operator key. - Delete, - /// Copy the range selected by the next motion or repeated operator key. - Yank, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum VimMotion { - /// Move one atomic boundary to the left. - Left, - /// Move one atomic boundary to the right. - Right, - /// Move one visual row up, preserving preferred display column. - Up, - /// Move one visual row down, preserving preferred display column. - Down, - /// Move to the start of the next word-like run. - WordForward, - /// Move to the start of the previous word-like run. - WordBackward, - /// Move to the end of the current or next word-like run. - WordEnd, - /// Move to the start of the current line. - LineStart, - /// Move to the end of the current line. - LineEnd, -} - impl TextArea { pub fn new() -> Self { let defaults = RuntimeKeymap::defaults(); @@ -179,10 +150,11 @@ impl TextArea { kill_buffer_kind: KillBufferKind::Characterwise, vim_enabled: false, vim_mode: VimMode::Insert, - vim_operator: None, + vim_pending: VimPending::None, editor_keymap: defaults.editor, vim_normal_keymap: defaults.vim_normal, vim_operator_keymap: defaults.vim_operator, + vim_text_object_keymap: defaults.vim_text_object, } } @@ -196,6 +168,7 @@ impl TextArea { self.editor_keymap = keymap.editor.clone(); self.vim_normal_keymap = keymap.vim_normal.clone(); self.vim_operator_keymap = keymap.vim_operator.clone(); + self.vim_text_object_keymap = keymap.vim_text_object.clone(); } /// Replace the visible textarea text and clear any existing text elements. @@ -257,7 +230,7 @@ impl TextArea { /// an old `d` or `y` command. pub(crate) fn set_vim_enabled(&mut self, enabled: bool) { self.vim_enabled = enabled; - self.vim_operator = None; + self.vim_pending = VimPending::None; self.vim_mode = if enabled { VimMode::Normal } else { @@ -293,7 +266,7 @@ impl TextArea { /// This is observable so the composer can avoid stealing the second key of /// `d{motion}` or `y{motion}` for higher-level shortcuts. pub(crate) fn is_vim_operator_pending(&self) -> bool { - self.vim_operator.is_some() + !matches!(self.vim_pending, VimPending::None) } /// Enter Vim insert mode if modal editing is enabled. @@ -304,7 +277,7 @@ impl TextArea { pub(crate) fn enter_vim_insert_mode(&mut self) { if self.vim_enabled { self.vim_mode = VimMode::Insert; - self.vim_operator = None; + self.vim_pending = VimPending::None; } } @@ -317,7 +290,7 @@ impl TextArea { pub(crate) fn enter_vim_normal_mode(&mut self) { if self.vim_enabled { self.vim_mode = VimMode::Normal; - self.vim_operator = None; + self.vim_pending = VimPending::None; self.preferred_col = None; } } @@ -669,9 +642,17 @@ impl TextArea { } fn handle_vim_normal(&mut self, event: KeyEvent) { - if let Some(op) = self.vim_operator.take() { - self.handle_vim_operator(op, event); - return; + let pending = std::mem::replace(&mut self.vim_pending, VimPending::None); + match pending { + VimPending::None => {} + VimPending::Operator(op) => { + self.handle_vim_operator(op, event); + return; + } + VimPending::TextObject { operator, scope } => { + self.handle_vim_text_object(operator, scope, event); + return; + } } if self.vim_normal_keymap.enter_insert.is_pressed(event) { @@ -776,15 +757,23 @@ impl TextArea { .start_delete_operator .is_pressed(event) { - self.vim_operator = Some(VimOperator::Delete); + self.vim_pending = VimPending::Operator(VimOperator::Delete); return; } if self.vim_normal_keymap.start_yank_operator.is_pressed(event) { - self.vim_operator = Some(VimOperator::Yank); + self.vim_pending = VimPending::Operator(VimOperator::Yank); + return; + } + if self + .vim_normal_keymap + .start_change_operator + .is_pressed(event) + { + self.vim_pending = VimPending::Operator(VimOperator::Change); return; } if self.vim_normal_keymap.cancel_operator.is_pressed(event) { - self.vim_operator = None; + self.vim_pending = VimPending::None; } } @@ -800,14 +789,41 @@ impl TextArea { if self.vim_operator_keymap.cancel.is_pressed(event) { return true; } + if let Some(scope) = self.vim_text_object_scope_for_event(event) { + self.vim_pending = VimPending::TextObject { + operator: op, + scope, + }; + return true; + } - if let Some(motion) = self.vim_motion_for_event(event) { + if op != VimOperator::Change + && let Some(motion) = self.vim_motion_for_event(event) + { self.apply_vim_operator(op, motion); return true; } false } + fn handle_vim_text_object( + &mut self, + op: VimOperator, + scope: VimTextObjectScope, + event: KeyEvent, + ) -> bool { + if self.vim_text_object_keymap.cancel.is_pressed(event) { + return true; + } + let Some(object) = self.vim_text_object_for_event(event) else { + return false; + }; + if let Some(range) = self.text_object_range(object, scope) { + self.apply_vim_operator_to_range(op, range); + } + true + } + fn vim_motion_for_event(&self, event: KeyEvent) -> Option { if self.vim_operator_keymap.motion_left.is_pressed(event) { return Some(VimMotion::Left); @@ -854,6 +870,18 @@ impl TextArea { match op { VimOperator::Delete => self.kill_range(range), VimOperator::Yank => self.yank_range(range), + VimOperator::Change => {} + } + } + + fn apply_vim_operator_to_range(&mut self, op: VimOperator, range: Range) { + match op { + VimOperator::Delete => self.kill_range(range), + VimOperator::Yank => self.yank_range(range), + VimOperator::Change => { + self.kill_range(range); + self.vim_mode = VimMode::Insert; + } } } @@ -2232,7 +2260,7 @@ mod tests { let mut t = TextArea::new(); t.set_vim_enabled(/*enabled*/ true); - t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('z'), KeyModifiers::NONE)); t.input(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); assert_eq!(t.vim_mode_label(), Some("Normal")); @@ -2399,6 +2427,180 @@ mod tests { assert_eq!(t.kill_buffer, "hello "); } + #[test] + fn vim_change_inner_word_deletes_word_and_enters_insert() { + let mut t = ta_with("hello world"); + t.set_cursor(/*pos*/ "hello ".len()); + t.set_vim_enabled(/*enabled*/ true); + + t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE)); + + assert_eq!(t.text(), "hello "); + assert_eq!(t.kill_buffer, "world"); + assert_eq!(t.cursor(), "hello ".len()); + assert_eq!(t.vim_mode_label(), Some("Insert")); + } + + #[test] + fn vim_word_text_objects_cover_delete_yank_and_big_word() { + let mut t = ta_with("hello world"); + t.set_cursor(/*pos*/ 1); + t.set_vim_enabled(/*enabled*/ true); + + t.input(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE)); + + assert_eq!(t.text(), "hello world"); + assert_eq!(t.kill_buffer, "hello "); + assert_eq!(t.vim_mode_label(), Some("Normal")); + + let mut t = ta_with("foo.bar/baz qux"); + t.set_cursor(/*pos*/ "foo.".len()); + t.set_vim_enabled(/*enabled*/ true); + + t.input(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('W'), KeyModifiers::NONE)); + + assert_eq!(t.text(), " qux"); + assert_eq!(t.kill_buffer, "foo.bar/baz"); + } + + #[test] + fn vim_word_text_objects_accept_cursor_at_word_end() { + let mut t = ta_with("hello world"); + t.set_cursor(/*pos*/ "hello".len()); + t.set_vim_enabled(/*enabled*/ true); + + t.input(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE)); + + assert_eq!(t.text(), "world"); + assert_eq!(t.kill_buffer, "hello "); + + let mut t = ta_with("foo bar"); + t.set_cursor(t.text().len()); + t.set_vim_enabled(/*enabled*/ true); + + t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('W'), KeyModifiers::NONE)); + + assert_eq!(t.text(), "foo "); + assert_eq!(t.kill_buffer, "bar"); + assert_eq!(t.cursor(), "foo ".len()); + assert_eq!(t.vim_mode_label(), Some("Insert")); + } + + #[test] + fn vim_delimiter_text_objects_select_innermost_pair_and_aliases() { + let mut t = ta_with("a(b(c)d)e"); + t.set_cursor(/*pos*/ "a(b(".len()); + t.set_vim_enabled(/*enabled*/ true); + + t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE)); + + assert_eq!(t.text(), "a(b()d)e"); + assert_eq!(t.kill_buffer, "c"); + assert_eq!(t.vim_mode_label(), Some("Insert")); + + let mut t = ta_with("a [b] c"); + t.set_cursor(/*pos*/ "a [".len()); + t.set_vim_enabled(/*enabled*/ true); + + t.input(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char(']'), KeyModifiers::NONE)); + + assert_eq!(t.text(), "a c"); + assert_eq!(t.kill_buffer, "[b]"); + } + + #[test] + fn vim_empty_inner_text_objects_are_valid_targets() { + let mut t = ta_with("call()"); + t.set_cursor(/*pos*/ "call(".len()); + t.set_vim_enabled(/*enabled*/ true); + + t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('('), KeyModifiers::NONE)); + + assert_eq!(t.text(), "call()"); + assert_eq!(t.kill_buffer, ""); + assert_eq!(t.cursor(), "call(".len()); + assert_eq!(t.vim_mode_label(), Some("Insert")); + + let mut t = ta_with(r#"say "" now"#); + t.set_cursor(/*pos*/ r#"say ""#.len()); + t.set_vim_enabled(/*enabled*/ true); + + t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE)); + + assert_eq!(t.text(), r#"say "" now"#); + assert_eq!(t.kill_buffer, ""); + assert_eq!(t.cursor(), r#"say ""#.len()); + assert_eq!(t.vim_mode_label(), Some("Insert")); + } + + #[test] + fn vim_quote_text_objects_are_line_local_and_handle_escapes() { + let mut t = ta_with(r#"say "a \"b\" c" now"#); + t.set_cursor(/*pos*/ r#"say "a \"#.len()); + t.set_vim_enabled(/*enabled*/ true); + + t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::SHIFT)); + + assert_eq!(t.text(), r#"say "" now"#); + assert_eq!(t.kill_buffer, r#"a \"b\" c"#); + assert_eq!(t.vim_mode_label(), Some("Insert")); + + let mut t = ta_with("one \"two\nthree\" four"); + t.set_cursor(/*pos*/ "one \"two\n".len()); + t.set_vim_enabled(/*enabled*/ true); + + t.input(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE)); + + assert_eq!(t.text(), "one \"two\nthree\" four"); + assert_eq!(t.kill_buffer, ""); + } + + #[test] + fn vim_text_object_cancellation_and_unsupported_change_motions_do_not_edit() { + let mut t = ta_with("hello world"); + t.set_cursor(/*pos*/ 1); + t.set_vim_enabled(/*enabled*/ true); + + t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('$'), KeyModifiers::NONE)); + + assert_eq!(t.text(), "hello world"); + assert_eq!(t.kill_buffer, ""); + assert_eq!(t.vim_mode_label(), Some("Normal")); + assert!(!t.is_vim_operator_pending()); + + t.input(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE)); + assert!(t.is_vim_operator_pending()); + t.input(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); + + assert_eq!(t.text(), "hello world"); + assert_eq!(t.kill_buffer, ""); + assert!(!t.is_vim_operator_pending()); + } + #[test] fn vim_operator_invalid_motion_is_consumed() { let mut t = ta_with("hello"); @@ -2408,7 +2610,7 @@ mod tests { t.input(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE)); assert!(t.is_vim_operator_pending()); - t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('z'), KeyModifiers::NONE)); assert_eq!(t.text(), "hello"); assert_eq!(t.vim_mode_label(), Some("Normal")); diff --git a/codex-rs/tui/src/bottom_pane/textarea/vim.rs b/codex-rs/tui/src/bottom_pane/textarea/vim.rs new file mode 100644 index 0000000000..2c1edc3fe7 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/textarea/vim.rs @@ -0,0 +1,320 @@ +use super::TextArea; +use super::split_word_pieces; +use crate::key_hint::KeyBindingListExt; +use crossterm::event::KeyEvent; +use std::ops::Range; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum VimMode { + /// Normal mode routes printable keys to movement, operators, and mode transitions. + Normal, + /// Insert mode routes input through the regular editor keymap until Escape is pressed. + Insert, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum VimOperator { + Delete, + Yank, + Change, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum VimPending { + None, + Operator(VimOperator), + TextObject { + operator: VimOperator, + scope: VimTextObjectScope, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum VimMotion { + Left, + Right, + Up, + Down, + WordForward, + WordBackward, + WordEnd, + LineStart, + LineEnd, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum VimTextObjectScope { + Inner, + Around, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum VimTextObject { + Word, + BigWord, + Parentheses, + Brackets, + Braces, + DoubleQuote, + SingleQuote, + Backtick, +} + +impl TextArea { + pub(super) fn vim_text_object_scope_for_event( + &self, + event: KeyEvent, + ) -> Option { + if self + .vim_operator_keymap + .select_inner_text_object + .is_pressed(event) + { + return Some(VimTextObjectScope::Inner); + } + if self + .vim_operator_keymap + .select_around_text_object + .is_pressed(event) + { + return Some(VimTextObjectScope::Around); + } + None + } + + pub(super) fn vim_text_object_for_event(&self, event: KeyEvent) -> Option { + if self.vim_text_object_keymap.word.is_pressed(event) { + return Some(VimTextObject::Word); + } + if self.vim_text_object_keymap.big_word.is_pressed(event) { + return Some(VimTextObject::BigWord); + } + if self.vim_text_object_keymap.parentheses.is_pressed(event) { + return Some(VimTextObject::Parentheses); + } + if self.vim_text_object_keymap.brackets.is_pressed(event) { + return Some(VimTextObject::Brackets); + } + if self.vim_text_object_keymap.braces.is_pressed(event) { + return Some(VimTextObject::Braces); + } + if self.vim_text_object_keymap.double_quote.is_pressed(event) { + return Some(VimTextObject::DoubleQuote); + } + if self.vim_text_object_keymap.single_quote.is_pressed(event) { + return Some(VimTextObject::SingleQuote); + } + if self.vim_text_object_keymap.backtick.is_pressed(event) { + return Some(VimTextObject::Backtick); + } + None + } + + pub(super) fn text_object_range( + &self, + object: VimTextObject, + scope: VimTextObjectScope, + ) -> Option> { + match object { + VimTextObject::Word => self.word_text_object_range(scope, /*big_word*/ false), + VimTextObject::BigWord => self.word_text_object_range(scope, /*big_word*/ true), + VimTextObject::Parentheses => self.paired_text_object_range(scope, '(', ')'), + VimTextObject::Brackets => self.paired_text_object_range(scope, '[', ']'), + VimTextObject::Braces => self.paired_text_object_range(scope, '{', '}'), + VimTextObject::DoubleQuote => self.quoted_text_object_range(scope, '"'), + VimTextObject::SingleQuote => self.quoted_text_object_range(scope, '\''), + VimTextObject::Backtick => self.quoted_text_object_range(scope, '`'), + } + } + + fn word_text_object_range( + &self, + scope: VimTextObjectScope, + big_word: bool, + ) -> Option> { + let inner = if big_word { + self.big_word_range_at_cursor()? + } else { + self.small_word_range_at_cursor()? + }; + Some(match scope { + VimTextObjectScope::Inner => inner, + VimTextObjectScope::Around => self.expand_word_around(inner), + }) + } + + fn big_word_range_at_cursor(&self) -> Option> { + self.non_ws_runs() + .into_iter() + .find(|range| self.cursor_overlaps_range(range) || self.cursor_is_at_range_end(range)) + } + + fn small_word_range_at_cursor(&self) -> Option> { + for run in self.non_ws_runs() { + if !self.cursor_overlaps_range(&run) && !self.cursor_is_at_range_end(&run) { + continue; + } + let mut last_piece = None; + for (piece_start, piece) in split_word_pieces(&self.text[run.clone()]) { + let piece = run.start + piece_start..run.start + piece_start + piece.len(); + if self.cursor_overlaps_range(&piece) { + return Some(piece); + } + last_piece = Some(piece); + } + if self.cursor_is_at_range_end(&run) { + return last_piece.or(Some(run)); + } + return Some(run); + } + None + } + + fn non_ws_runs(&self) -> Vec> { + let mut runs = Vec::new(); + let mut start = None; + for (idx, ch) in self.text.char_indices() { + if ch.is_whitespace() { + if let Some(run_start) = start.take() { + runs.push(run_start..idx); + } + } else if start.is_none() { + start = Some(idx); + } + } + if let Some(run_start) = start { + runs.push(run_start..self.text.len()); + } + runs + } + + fn cursor_overlaps_range(&self, range: &Range) -> bool { + range.start <= self.cursor_pos && self.cursor_pos < range.end + } + + fn cursor_is_at_range_end(&self, range: &Range) -> bool { + range.start < range.end && self.cursor_pos == range.end + } + + fn expand_word_around(&self, inner: Range) -> Range { + let following = self.following_whitespace_end(inner.end); + if following > inner.end { + return inner.start..following; + } + self.preceding_whitespace_start(inner.start)..inner.end + } + + fn following_whitespace_end(&self, start: usize) -> usize { + let mut end = start; + for (offset, ch) in self.text[start..].char_indices() { + if !ch.is_whitespace() { + break; + } + end = start + offset + ch.len_utf8(); + } + end + } + + fn preceding_whitespace_start(&self, end: usize) -> usize { + let mut start = end; + for (idx, ch) in self.text[..end].char_indices().rev() { + if !ch.is_whitespace() { + break; + } + start = idx; + } + start + } + + fn paired_text_object_range( + &self, + scope: VimTextObjectScope, + open: char, + close: char, + ) -> Option> { + let mut stack: Vec = Vec::new(); + let mut best: Option> = None; + for (idx, ch) in self.text.char_indices() { + if self.is_inside_element(idx) { + continue; + } + if ch == open { + stack.push(idx); + } else if ch == close { + let Some(open_idx) = stack.pop() else { + continue; + }; + let close_end = idx + ch.len_utf8(); + if open_idx <= self.cursor_pos && self.cursor_pos <= idx { + let candidate = match scope { + VimTextObjectScope::Inner => open_idx + open.len_utf8()..idx, + VimTextObjectScope::Around => open_idx..close_end, + }; + if candidate.start <= candidate.end + && best + .as_ref() + .is_none_or(|current| candidate.len() < current.len()) + { + best = Some(candidate); + } + } + } + } + best + } + + fn quoted_text_object_range( + &self, + scope: VimTextObjectScope, + quote: char, + ) -> Option> { + let line = self.beginning_of_current_line()..self.end_of_current_line(); + let mut open = None; + let mut best: Option> = None; + for (offset, ch) in self.text[line.clone()].char_indices() { + let idx = line.start + offset; + if self.is_inside_element(idx) || ch != quote || self.is_escaped(idx) { + continue; + } + if let Some(open_idx) = open.take() { + if open_idx <= self.cursor_pos && self.cursor_pos <= idx { + let candidate = match scope { + VimTextObjectScope::Inner => open_idx + quote.len_utf8()..idx, + VimTextObjectScope::Around => idx_range(open_idx, idx, quote), + }; + if candidate.start <= candidate.end + && best + .as_ref() + .is_none_or(|current| candidate.len() < current.len()) + { + best = Some(candidate); + } + } + } else { + open = Some(idx); + } + } + best + } + + fn is_inside_element(&self, pos: usize) -> bool { + self.elements + .iter() + .any(|element| pos >= element.range.start && pos < element.range.end) + } + + fn is_escaped(&self, pos: usize) -> bool { + let mut backslashes = 0; + for ch in self.text[..pos].chars().rev() { + if ch != '\\' { + break; + } + backslashes += 1; + } + backslashes % 2 == 1 + } +} + +fn idx_range(open_idx: usize, close_idx: usize, quote: char) -> Range { + open_idx..close_idx + quote.len_utf8() +} diff --git a/codex-rs/tui/src/keymap.rs b/codex-rs/tui/src/keymap.rs index bc54224c40..8e6d40c764 100644 --- a/codex-rs/tui/src/keymap.rs +++ b/codex-rs/tui/src/keymap.rs @@ -46,6 +46,7 @@ pub(crate) struct RuntimeKeymap { pub(crate) editor: EditorKeymap, pub(crate) vim_normal: VimNormalKeymap, pub(crate) vim_operator: VimOperatorKeymap, + pub(crate) vim_text_object: VimTextObjectKeymap, pub(crate) pager: PagerKeymap, pub(crate) list: ListKeymap, pub(crate) approval: ApprovalKeymap, @@ -161,6 +162,7 @@ pub(crate) struct VimNormalKeymap { pub(crate) paste_after: Vec, pub(crate) start_delete_operator: Vec, pub(crate) start_yank_operator: Vec, + pub(crate) start_change_operator: Vec, pub(crate) cancel_operator: Vec, } @@ -183,6 +185,22 @@ pub(crate) struct VimOperatorKeymap { pub(crate) motion_word_end: Vec, pub(crate) motion_line_start: Vec, pub(crate) motion_line_end: Vec, + pub(crate) select_inner_text_object: Vec, + pub(crate) select_around_text_object: Vec, + pub(crate) cancel: Vec, +} + +/// Vim text-object keybindings active after an operator plus inner/around prefix. +#[derive(Clone, Debug, Default)] +pub(crate) struct VimTextObjectKeymap { + pub(crate) word: Vec, + pub(crate) big_word: Vec, + pub(crate) parentheses: Vec, + pub(crate) brackets: Vec, + pub(crate) braces: Vec, + pub(crate) double_quote: Vec, + pub(crate) single_quote: Vec, + pub(crate) backtick: Vec, pub(crate) cancel: Vec, } @@ -452,7 +470,7 @@ impl RuntimeKeymap { yank: resolve_local!(keymap, defaults, editor, yank), }; - let vim_normal = VimNormalKeymap { + let mut vim_normal = VimNormalKeymap { enter_insert: resolve_local!(keymap, defaults, vim_normal, enter_insert), append_after_cursor: resolve_local!(keymap, defaults, vim_normal, append_after_cursor), append_line_end: resolve_local!(keymap, defaults, vim_normal, append_line_end), @@ -480,10 +498,113 @@ impl RuntimeKeymap { start_delete_operator ), start_yank_operator: resolve_local!(keymap, defaults, vim_normal, start_yank_operator), + start_change_operator: resolve_local!( + keymap, + defaults, + vim_normal, + start_change_operator + ), cancel_operator: resolve_local!(keymap, defaults, vim_normal, cancel_operator), }; - let vim_operator = VimOperatorKeymap { + let configured_vim_normal_bindings_to_preserve = configured_bindings_to_preserve([ + ( + keymap.vim_normal.enter_insert.as_ref(), + vim_normal.enter_insert.as_slice(), + ), + ( + keymap.vim_normal.append_after_cursor.as_ref(), + vim_normal.append_after_cursor.as_slice(), + ), + ( + keymap.vim_normal.append_line_end.as_ref(), + vim_normal.append_line_end.as_slice(), + ), + ( + keymap.vim_normal.insert_line_start.as_ref(), + vim_normal.insert_line_start.as_slice(), + ), + ( + keymap.vim_normal.open_line_below.as_ref(), + vim_normal.open_line_below.as_slice(), + ), + ( + keymap.vim_normal.open_line_above.as_ref(), + vim_normal.open_line_above.as_slice(), + ), + ( + keymap.vim_normal.move_left.as_ref(), + vim_normal.move_left.as_slice(), + ), + ( + keymap.vim_normal.move_right.as_ref(), + vim_normal.move_right.as_slice(), + ), + ( + keymap.vim_normal.move_up.as_ref(), + vim_normal.move_up.as_slice(), + ), + ( + keymap.vim_normal.move_down.as_ref(), + vim_normal.move_down.as_slice(), + ), + ( + keymap.vim_normal.move_word_forward.as_ref(), + vim_normal.move_word_forward.as_slice(), + ), + ( + keymap.vim_normal.move_word_backward.as_ref(), + vim_normal.move_word_backward.as_slice(), + ), + ( + keymap.vim_normal.move_word_end.as_ref(), + vim_normal.move_word_end.as_slice(), + ), + ( + keymap.vim_normal.move_line_start.as_ref(), + vim_normal.move_line_start.as_slice(), + ), + ( + keymap.vim_normal.move_line_end.as_ref(), + vim_normal.move_line_end.as_slice(), + ), + ( + keymap.vim_normal.delete_char.as_ref(), + vim_normal.delete_char.as_slice(), + ), + ( + keymap.vim_normal.delete_to_line_end.as_ref(), + vim_normal.delete_to_line_end.as_slice(), + ), + ( + keymap.vim_normal.yank_line.as_ref(), + vim_normal.yank_line.as_slice(), + ), + ( + keymap.vim_normal.paste_after.as_ref(), + vim_normal.paste_after.as_slice(), + ), + ( + keymap.vim_normal.start_delete_operator.as_ref(), + vim_normal.start_delete_operator.as_slice(), + ), + ( + keymap.vim_normal.start_yank_operator.as_ref(), + vim_normal.start_yank_operator.as_slice(), + ), + ( + keymap.vim_normal.cancel_operator.as_ref(), + vim_normal.cancel_operator.as_slice(), + ), + ]); + + if keymap.vim_normal.start_change_operator.is_none() { + vim_normal + .start_change_operator + .retain(|binding| !configured_vim_normal_bindings_to_preserve.contains(binding)); + } + + let mut vim_operator = VimOperatorKeymap { delete_line: resolve_local!(keymap, defaults, vim_operator, delete_line), yank_line: resolve_local!(keymap, defaults, vim_operator, yank_line), motion_left: resolve_local!(keymap, defaults, vim_operator, motion_left), @@ -505,9 +626,95 @@ impl RuntimeKeymap { motion_word_end: resolve_local!(keymap, defaults, vim_operator, motion_word_end), motion_line_start: resolve_local!(keymap, defaults, vim_operator, motion_line_start), motion_line_end: resolve_local!(keymap, defaults, vim_operator, motion_line_end), + select_inner_text_object: resolve_local!( + keymap, + defaults, + vim_operator, + select_inner_text_object + ), + select_around_text_object: resolve_local!( + keymap, + defaults, + vim_operator, + select_around_text_object + ), cancel: resolve_local!(keymap, defaults, vim_operator, cancel), }; + let configured_vim_operator_bindings_to_preserve = configured_bindings_to_preserve([ + ( + keymap.vim_operator.delete_line.as_ref(), + vim_operator.delete_line.as_slice(), + ), + ( + keymap.vim_operator.yank_line.as_ref(), + vim_operator.yank_line.as_slice(), + ), + ( + keymap.vim_operator.motion_left.as_ref(), + vim_operator.motion_left.as_slice(), + ), + ( + keymap.vim_operator.motion_right.as_ref(), + vim_operator.motion_right.as_slice(), + ), + ( + keymap.vim_operator.motion_up.as_ref(), + vim_operator.motion_up.as_slice(), + ), + ( + keymap.vim_operator.motion_down.as_ref(), + vim_operator.motion_down.as_slice(), + ), + ( + keymap.vim_operator.motion_word_forward.as_ref(), + vim_operator.motion_word_forward.as_slice(), + ), + ( + keymap.vim_operator.motion_word_backward.as_ref(), + vim_operator.motion_word_backward.as_slice(), + ), + ( + keymap.vim_operator.motion_word_end.as_ref(), + vim_operator.motion_word_end.as_slice(), + ), + ( + keymap.vim_operator.motion_line_start.as_ref(), + vim_operator.motion_line_start.as_slice(), + ), + ( + keymap.vim_operator.motion_line_end.as_ref(), + vim_operator.motion_line_end.as_slice(), + ), + ( + keymap.vim_operator.cancel.as_ref(), + vim_operator.cancel.as_slice(), + ), + ]); + + if keymap.vim_operator.select_inner_text_object.is_none() { + vim_operator + .select_inner_text_object + .retain(|binding| !configured_vim_operator_bindings_to_preserve.contains(binding)); + } + if keymap.vim_operator.select_around_text_object.is_none() { + vim_operator + .select_around_text_object + .retain(|binding| !configured_vim_operator_bindings_to_preserve.contains(binding)); + } + + let vim_text_object = VimTextObjectKeymap { + word: resolve_local!(keymap, defaults, vim_text_object, word), + big_word: resolve_local!(keymap, defaults, vim_text_object, big_word), + parentheses: resolve_local!(keymap, defaults, vim_text_object, parentheses), + brackets: resolve_local!(keymap, defaults, vim_text_object, brackets), + braces: resolve_local!(keymap, defaults, vim_text_object, braces), + double_quote: resolve_local!(keymap, defaults, vim_text_object, double_quote), + single_quote: resolve_local!(keymap, defaults, vim_text_object, single_quote), + backtick: resolve_local!(keymap, defaults, vim_text_object, backtick), + cancel: resolve_local!(keymap, defaults, vim_text_object, cancel), + }; + let pager = PagerKeymap { scroll_up: resolve_local!(keymap, defaults, pager, scroll_up), scroll_down: resolve_local!(keymap, defaults, pager, scroll_down), @@ -536,8 +743,7 @@ impl RuntimeKeymap { let list_move_down = resolve_local!(keymap, defaults, list, move_down); let list_accept = resolve_local!(keymap, defaults, list, accept); let list_cancel = resolve_local!(keymap, defaults, list, cancel); - let mut configured_bindings_to_preserve = Vec::new(); - for (configured, resolved) in [ + let configured_bindings_to_preserve = configured_bindings_to_preserve([ ( keymap.global.open_transcript.as_ref(), app.open_transcript.as_slice(), @@ -593,51 +799,42 @@ impl RuntimeKeymap { approval.decline.as_slice(), ), (keymap.approval.cancel.as_ref(), approval.cancel.as_slice()), - ] { - if configured.is_none() { - continue; - } - for binding in resolved { - if !configured_bindings_to_preserve.contains(binding) { - configured_bindings_to_preserve.push(*binding); - } - } - } + ]); let list = ListKeymap { move_up: list_move_up, move_down: list_move_down, - move_left: resolve_new_list_bindings( + move_left: resolve_new_default_bindings( keymap.list.move_left.as_ref(), &defaults.list.move_left, &configured_bindings_to_preserve, "tui.keymap.list.move_left", )?, - move_right: resolve_new_list_bindings( + move_right: resolve_new_default_bindings( keymap.list.move_right.as_ref(), &defaults.list.move_right, &configured_bindings_to_preserve, "tui.keymap.list.move_right", )?, - page_up: resolve_new_list_bindings( + page_up: resolve_new_default_bindings( keymap.list.page_up.as_ref(), &defaults.list.page_up, &configured_bindings_to_preserve, "tui.keymap.list.page_up", )?, - page_down: resolve_new_list_bindings( + page_down: resolve_new_default_bindings( keymap.list.page_down.as_ref(), &defaults.list.page_down, &configured_bindings_to_preserve, "tui.keymap.list.page_down", )?, - jump_top: resolve_new_list_bindings( + jump_top: resolve_new_default_bindings( keymap.list.jump_top.as_ref(), &defaults.list.jump_top, &configured_bindings_to_preserve, "tui.keymap.list.jump_top", )?, - jump_bottom: resolve_new_list_bindings( + jump_bottom: resolve_new_default_bindings( keymap.list.jump_bottom.as_ref(), &defaults.list.jump_bottom, &configured_bindings_to_preserve, @@ -654,6 +851,7 @@ impl RuntimeKeymap { editor, vim_normal, vim_operator, + vim_text_object, pager, list, approval, @@ -796,6 +994,7 @@ impl RuntimeKeymap { paste_after: default_bindings![plain(KeyCode::Char('p'))], start_delete_operator: default_bindings![plain(KeyCode::Char('d'))], start_yank_operator: default_bindings![plain(KeyCode::Char('y'))], + start_change_operator: default_bindings![plain(KeyCode::Char('c'))], cancel_operator: default_bindings![plain(KeyCode::Esc)], }, vim_operator: VimOperatorKeymap { @@ -813,6 +1012,35 @@ impl RuntimeKeymap { plain(KeyCode::Char('$')), shift(KeyCode::Char('$')) ], + select_inner_text_object: default_bindings![plain(KeyCode::Char('i'))], + select_around_text_object: default_bindings![plain(KeyCode::Char('a'))], + cancel: default_bindings![plain(KeyCode::Esc)], + }, + vim_text_object: VimTextObjectKeymap { + word: default_bindings![plain(KeyCode::Char('w'))], + big_word: default_bindings![shift(KeyCode::Char('w')), plain(KeyCode::Char('W'))], + parentheses: default_bindings![ + plain(KeyCode::Char('(')), + shift(KeyCode::Char('(')), + plain(KeyCode::Char(')')), + shift(KeyCode::Char(')')), + plain(KeyCode::Char('b')) + ], + brackets: default_bindings![plain(KeyCode::Char('[')), plain(KeyCode::Char(']'))], + braces: default_bindings![ + plain(KeyCode::Char('{')), + shift(KeyCode::Char('{')), + plain(KeyCode::Char('}')), + shift(KeyCode::Char('}')), + shift(KeyCode::Char('b')), + plain(KeyCode::Char('B')) + ], + double_quote: default_bindings![ + plain(KeyCode::Char('"')), + shift(KeyCode::Char('"')) + ], + single_quote: default_bindings![plain(KeyCode::Char('\''))], + backtick: default_bindings![plain(KeyCode::Char('`'))], cancel: default_bindings![plain(KeyCode::Esc)], }, pager: PagerKeymap { @@ -1196,6 +1424,10 @@ impl RuntimeKeymap { "start_yank_operator", self.vim_normal.start_yank_operator.as_slice(), ), + ( + "start_change_operator", + self.vim_normal.start_change_operator.as_slice(), + ), ( "cancel_operator", self.vim_normal.cancel_operator.as_slice(), @@ -1232,10 +1464,33 @@ impl RuntimeKeymap { "motion_line_end", self.vim_operator.motion_line_end.as_slice(), ), + ( + "select_inner_text_object", + self.vim_operator.select_inner_text_object.as_slice(), + ), + ( + "select_around_text_object", + self.vim_operator.select_around_text_object.as_slice(), + ), ("cancel", self.vim_operator.cancel.as_slice()), ], )?; + validate_unique( + "vim_text_object", + [ + ("word", self.vim_text_object.word.as_slice()), + ("big_word", self.vim_text_object.big_word.as_slice()), + ("parentheses", self.vim_text_object.parentheses.as_slice()), + ("brackets", self.vim_text_object.brackets.as_slice()), + ("braces", self.vim_text_object.braces.as_slice()), + ("double_quote", self.vim_text_object.double_quote.as_slice()), + ("single_quote", self.vim_text_object.single_quote.as_slice()), + ("backtick", self.vim_text_object.backtick.as_slice()), + ("cancel", self.vim_text_object.cancel.as_slice()), + ], + )?; + validate_unique( "pager", [ @@ -1524,7 +1779,24 @@ fn resolve_bindings( parse_bindings(spec, path) } -fn resolve_new_list_bindings( +fn configured_bindings_to_preserve( + pairs: [(Option<&KeybindingsSpec>, &[KeyBinding]); N], +) -> Vec { + let mut configured_bindings = Vec::new(); + for (configured, resolved) in pairs { + if configured.is_none() { + continue; + } + for binding in resolved { + if !configured_bindings.contains(binding) { + configured_bindings.push(*binding); + } + } + } + configured_bindings +} + +fn resolve_new_default_bindings( configured: Option<&KeybindingsSpec>, fallback: &[KeyBinding], configured_bindings_to_preserve: &[KeyBinding], @@ -1973,6 +2245,58 @@ mod tests { expect_conflict(&keymap, "list.jump_top", "approval.approve"); } + #[test] + fn configured_legacy_vim_normal_bindings_prune_new_change_operator_default() { + let mut keymap = TuiKeymap::default(); + keymap.vim_normal.move_left = Some(one("c")); + + let runtime = RuntimeKeymap::from_config(&keymap).expect("config should parse"); + + assert_eq!( + runtime.vim_normal.move_left, + vec![key_hint::plain(KeyCode::Char('c'))] + ); + assert_eq!(runtime.vim_normal.start_change_operator, Vec::new()); + } + + #[test] + fn explicit_new_vim_normal_binding_still_conflicts_with_legacy_binding() { + let mut keymap = TuiKeymap::default(); + keymap.vim_normal.move_left = Some(one("c")); + keymap.vim_normal.start_change_operator = Some(one("c")); + + expect_conflict(&keymap, "move_left", "start_change_operator"); + } + + #[test] + fn configured_legacy_vim_operator_bindings_prune_new_text_object_defaults() { + let mut keymap = TuiKeymap::default(); + keymap.vim_operator.motion_left = Some(one("i")); + keymap.vim_operator.motion_right = Some(one("a")); + + let runtime = RuntimeKeymap::from_config(&keymap).expect("config should parse"); + + assert_eq!( + runtime.vim_operator.motion_left, + vec![key_hint::plain(KeyCode::Char('i'))] + ); + assert_eq!( + runtime.vim_operator.motion_right, + vec![key_hint::plain(KeyCode::Char('a'))] + ); + assert_eq!(runtime.vim_operator.select_inner_text_object, Vec::new()); + assert_eq!(runtime.vim_operator.select_around_text_object, Vec::new()); + } + + #[test] + fn explicit_new_vim_operator_binding_still_conflicts_with_legacy_binding() { + let mut keymap = TuiKeymap::default(); + keymap.vim_operator.motion_left = Some(one("i")); + keymap.vim_operator.select_inner_text_object = Some(one("i")); + + expect_conflict(&keymap, "motion_left", "select_inner_text_object"); + } + #[test] fn vim_normal_defaults_include_insert_and_arrow_aliases() { let runtime = RuntimeKeymap::defaults(); diff --git a/codex-rs/tui/src/keymap_setup/actions.rs b/codex-rs/tui/src/keymap_setup/actions.rs index fb3ecd9f2c..c8815c8c98 100644 --- a/codex-rs/tui/src/keymap_setup/actions.rs +++ b/codex-rs/tui/src/keymap_setup/actions.rs @@ -140,6 +140,7 @@ pub(super) const KEYMAP_ACTIONS: &[KeymapActionDescriptor] = &[ action("vim_normal", "Vim normal", "paste_after", "Paste after the cursor."), action("vim_normal", "Vim normal", "start_delete_operator", "Begin a delete operator and wait for a motion."), action("vim_normal", "Vim normal", "start_yank_operator", "Begin a yank operator and wait for a motion."), + action("vim_normal", "Vim normal", "start_change_operator", "Begin a change operator and wait for a text object."), action("vim_normal", "Vim normal", "cancel_operator", "Cancel a pending Vim operator."), action("vim_operator", "Vim operator", "delete_line", "Repeat delete operator to delete the whole line."), action("vim_operator", "Vim operator", "yank_line", "Repeat yank operator to yank the whole line."), @@ -152,7 +153,18 @@ pub(super) const KEYMAP_ACTIONS: &[KeymapActionDescriptor] = &[ action("vim_operator", "Vim operator", "motion_word_end", "Operator motion to end of word."), action("vim_operator", "Vim operator", "motion_line_start", "Operator motion to line start."), action("vim_operator", "Vim operator", "motion_line_end", "Operator motion to line end."), + action("vim_operator", "Vim operator", "select_inner_text_object", "Select an inner text object."), + action("vim_operator", "Vim operator", "select_around_text_object", "Select an around text object."), action("vim_operator", "Vim operator", "cancel", "Cancel the pending operator."), + action("vim_text_object", "Vim text object", "word", "Target the current word."), + action("vim_text_object", "Vim text object", "big_word", "Target the current WORD."), + action("vim_text_object", "Vim text object", "parentheses", "Target enclosing parentheses."), + action("vim_text_object", "Vim text object", "brackets", "Target enclosing brackets."), + action("vim_text_object", "Vim text object", "braces", "Target enclosing braces."), + action("vim_text_object", "Vim text object", "double_quote", "Target enclosing double quotes."), + action("vim_text_object", "Vim text object", "single_quote", "Target enclosing single quotes."), + action("vim_text_object", "Vim text object", "backtick", "Target enclosing backticks."), + action("vim_text_object", "Vim text object", "cancel", "Cancel the pending text object."), action("pager", "Pager", "scroll_up", "Scroll up by one row."), action("pager", "Pager", "scroll_down", "Scroll down by one row."), action("pager", "Pager", "page_up", "Scroll up by one page."), @@ -269,6 +281,7 @@ pub(super) fn binding_slot<'a>( ("vim_normal", "paste_after") => Some(&mut keymap.vim_normal.paste_after), ("vim_normal", "start_delete_operator") => Some(&mut keymap.vim_normal.start_delete_operator), ("vim_normal", "start_yank_operator") => Some(&mut keymap.vim_normal.start_yank_operator), + ("vim_normal", "start_change_operator") => Some(&mut keymap.vim_normal.start_change_operator), ("vim_normal", "cancel_operator") => Some(&mut keymap.vim_normal.cancel_operator), ("vim_operator", "delete_line") => Some(&mut keymap.vim_operator.delete_line), ("vim_operator", "yank_line") => Some(&mut keymap.vim_operator.yank_line), @@ -281,7 +294,18 @@ pub(super) fn binding_slot<'a>( ("vim_operator", "motion_word_end") => Some(&mut keymap.vim_operator.motion_word_end), ("vim_operator", "motion_line_start") => Some(&mut keymap.vim_operator.motion_line_start), ("vim_operator", "motion_line_end") => Some(&mut keymap.vim_operator.motion_line_end), + ("vim_operator", "select_inner_text_object") => Some(&mut keymap.vim_operator.select_inner_text_object), + ("vim_operator", "select_around_text_object") => Some(&mut keymap.vim_operator.select_around_text_object), ("vim_operator", "cancel") => Some(&mut keymap.vim_operator.cancel), + ("vim_text_object", "word") => Some(&mut keymap.vim_text_object.word), + ("vim_text_object", "big_word") => Some(&mut keymap.vim_text_object.big_word), + ("vim_text_object", "parentheses") => Some(&mut keymap.vim_text_object.parentheses), + ("vim_text_object", "brackets") => Some(&mut keymap.vim_text_object.brackets), + ("vim_text_object", "braces") => Some(&mut keymap.vim_text_object.braces), + ("vim_text_object", "double_quote") => Some(&mut keymap.vim_text_object.double_quote), + ("vim_text_object", "single_quote") => Some(&mut keymap.vim_text_object.single_quote), + ("vim_text_object", "backtick") => Some(&mut keymap.vim_text_object.backtick), + ("vim_text_object", "cancel") => Some(&mut keymap.vim_text_object.cancel), ("pager", "scroll_up") => Some(&mut keymap.pager.scroll_up), ("pager", "scroll_down") => Some(&mut keymap.pager.scroll_down), ("pager", "page_up") => Some(&mut keymap.pager.page_up), @@ -380,6 +404,7 @@ pub(super) fn bindings_for_action<'a>( ("vim_normal", "paste_after") => Some(runtime_keymap.vim_normal.paste_after.as_slice()), ("vim_normal", "start_delete_operator") => Some(runtime_keymap.vim_normal.start_delete_operator.as_slice()), ("vim_normal", "start_yank_operator") => Some(runtime_keymap.vim_normal.start_yank_operator.as_slice()), + ("vim_normal", "start_change_operator") => Some(runtime_keymap.vim_normal.start_change_operator.as_slice()), ("vim_normal", "cancel_operator") => Some(runtime_keymap.vim_normal.cancel_operator.as_slice()), ("vim_operator", "delete_line") => Some(runtime_keymap.vim_operator.delete_line.as_slice()), ("vim_operator", "yank_line") => Some(runtime_keymap.vim_operator.yank_line.as_slice()), @@ -392,7 +417,18 @@ pub(super) fn bindings_for_action<'a>( ("vim_operator", "motion_word_end") => Some(runtime_keymap.vim_operator.motion_word_end.as_slice()), ("vim_operator", "motion_line_start") => Some(runtime_keymap.vim_operator.motion_line_start.as_slice()), ("vim_operator", "motion_line_end") => Some(runtime_keymap.vim_operator.motion_line_end.as_slice()), + ("vim_operator", "select_inner_text_object") => Some(runtime_keymap.vim_operator.select_inner_text_object.as_slice()), + ("vim_operator", "select_around_text_object") => Some(runtime_keymap.vim_operator.select_around_text_object.as_slice()), ("vim_operator", "cancel") => Some(runtime_keymap.vim_operator.cancel.as_slice()), + ("vim_text_object", "word") => Some(runtime_keymap.vim_text_object.word.as_slice()), + ("vim_text_object", "big_word") => Some(runtime_keymap.vim_text_object.big_word.as_slice()), + ("vim_text_object", "parentheses") => Some(runtime_keymap.vim_text_object.parentheses.as_slice()), + ("vim_text_object", "brackets") => Some(runtime_keymap.vim_text_object.brackets.as_slice()), + ("vim_text_object", "braces") => Some(runtime_keymap.vim_text_object.braces.as_slice()), + ("vim_text_object", "double_quote") => Some(runtime_keymap.vim_text_object.double_quote.as_slice()), + ("vim_text_object", "single_quote") => Some(runtime_keymap.vim_text_object.single_quote.as_slice()), + ("vim_text_object", "backtick") => Some(runtime_keymap.vim_text_object.backtick.as_slice()), + ("vim_text_object", "cancel") => Some(runtime_keymap.vim_text_object.cancel.as_slice()), ("pager", "scroll_up") => Some(runtime_keymap.pager.scroll_up.as_slice()), ("pager", "scroll_down") => Some(runtime_keymap.pager.scroll_down.as_slice()), ("pager", "page_up") => Some(runtime_keymap.pager.page_up.as_slice()), diff --git a/codex-rs/tui/src/keymap_setup/picker.rs b/codex-rs/tui/src/keymap_setup/picker.rs index b6c89d9585..52ec4a4b8b 100644 --- a/codex-rs/tui/src/keymap_setup/picker.rs +++ b/codex-rs/tui/src/keymap_setup/picker.rs @@ -104,7 +104,7 @@ const KEYMAP_CONTEXT_TABS: &[KeymapContextTab] = &[ id: "vim-shortcuts", label: "Vim", description: "Vim normal-mode and operator shortcuts.", - contexts: &["vim_normal", "vim_operator"], + contexts: &["vim_normal", "vim_operator", "vim_text_object"], }, KeymapContextTab { id: "navigation-shortcuts", diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap index bc41e50b24..e212226ef5 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap @@ -5,7 +5,7 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 94 actions, 1 customized, 2 unbound. + 106 actions, 1 customized, 2 unbound. [All] Common Customized (1) Unbound (2) App Composer Editor Vim Navigation Approval Debug diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_fast_mode_enabled.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_fast_mode_enabled.snap index e0c1aa7ffe..22c0537540 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_fast_mode_enabled.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_fast_mode_enabled.snap @@ -5,7 +5,7 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 95 actions, 0 customized, 3 unbound. + 107 actions, 0 customized, 3 unbound. [All] Common Customized (0) Unbound (3) App Composer Editor Vim Navigation Approval Debug diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap index 856ac05d1e..4a88465ba8 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap @@ -2,14 +2,14 @@ source: tui/src/keymap_setup.rs expression: snapshot --- -tab: All (94 selectable) +tab: All (106 selectable) tab: Common (19 selectable) tab: Customized (0) (0 selectable) tab: Unbound (2) (2 selectable) tab: App (9 selectable) tab: Composer (5 selectable) tab: Editor (17 selectable) -tab: Vim (35 selectable) +tab: Vim (47 selectable) tab: Navigation (20 selectable) tab: Approval (8 selectable) tab: Debug (1 selectable) diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap index 57b0131002..26af4bd57a 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap @@ -5,7 +5,7 @@ expression: "render_picker(params, 78)" Keymap All configurable shortcuts. - 94 actions, 0 customized, 2 unbound. + 106 actions, 0 customized, 2 unbound. [All] Common Customized (0) Unbound (2) App Composer Editor Vim Navigation Approval Debug diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap index 4d10013649..36f6a1d04c 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap @@ -5,7 +5,7 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 94 actions, 0 customized, 2 unbound. + 106 actions, 0 customized, 2 unbound. [All] Common Customized (0) Unbound (2) App Composer Editor Vim Navigation Approval Debug From 2d1ad374a7d2469e6bc889d4cdf7bfb58d82b436 Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Wed, 27 May 2026 15:59:17 -0300 Subject: [PATCH 31/87] feat(tui): make turn interruption keybind configurable (#24766) ## Why Interrupting an active turn is currently fixed to `Esc`, which is easy to hit accidentally and cannot be customized through `/keymap`. This gives users a less accidental binding while preserving the existing default. ## What Changed - Adds `tui.keymap.chat.interrupt_turn` to `/keymap`, defaulting to `esc` and supporting remapping or unbinding. - Uses the configured interrupt binding for running-turn status, queued steer interruption, and `request_user_input`, including the visible hints. - Preserves local `Esc` behavior for popups, Vim insert mode, and `/agent` editing while validating conflicts with fixed/backtrack and request-input navigation bindings. - Adds behavior and snapshot coverage for remapped interruption paths. ## How to Test 1. Run Codex and open `/keymap`, then set **Interrupt Turn** to `f12`. 2. Start a turn and confirm `Esc` no longer interrupts it while `f12` does; the running hint should display `f12 to interrupt`. 3. Queue a steer while a turn is running and confirm the preview displays `f12`; pressing it should interrupt and submit the steer immediately. 4. Trigger a `request_user_input` prompt and confirm its footer uses `f12`; with notes open, `Esc` should still clear notes while `f12` interrupts the turn. 5. Clear the Interrupt Turn binding and confirm the key-specific interrupt hint is removed while `Ctrl+C` remains available. Targeted validation: - `just write-config-schema` - `just fix -p codex-config` - `just fix -p codex-tui` - `just fmt` - `just argument-comment-lint-from-source -p codex-config -p codex-tui` - `just test -p codex-config` - `cargo insta pending-snapshots --manifest-path tui/Cargo.toml` - `just test -p codex-tui keymap_setup::tests` - `just test -p codex-tui` (fails in two pre-existing guardian feature-flag tests unrelated to this diff; the intentional picker snapshot updates were reviewed and accepted) --- codex-rs/config/src/tui_keymap.rs | 2 + codex-rs/core/config.schema.json | 14 +++- codex-rs/tui/src/bottom_pane/mod.rs | 45 ++++++++-- .../src/bottom_pane/pending_input_preview.rs | 39 +++++++-- .../src/bottom_pane/request_user_input/mod.rs | 78 +++++++++++++++-- ...ser_input_freeform_remapped_interrupt.snap | 13 +++ ...steer_with_remapped_interrupt_binding.snap | 20 +++++ codex-rs/tui/src/chatwidget/interaction.rs | 3 +- .../chatwidget/tests/composer_submission.rs | 26 ++++++ codex-rs/tui/src/keymap.rs | 83 ++++++++++++++++++- codex-rs/tui/src/keymap_setup.rs | 1 + codex-rs/tui/src/keymap_setup/actions.rs | 3 + codex-rs/tui/src/keymap_setup/picker.rs | 1 + ...__tests__keymap_picker_all_tab_search.snap | 2 +- ...ap_setup__tests__keymap_picker_custom.snap | 4 +- ...ests__keymap_picker_fast_mode_enabled.snap | 4 +- ...p__tests__keymap_picker_first_actions.snap | 8 +- ...ap_setup__tests__keymap_picker_narrow.snap | 4 +- ...ymap_setup__tests__keymap_picker_wide.snap | 4 +- ...ests__renders_remapped_interrupt_hint.snap | 5 ++ codex-rs/tui/src/status_indicator_widget.rs | 35 +++++++- 21 files changed, 352 insertions(+), 42 deletions(-) create mode 100644 codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_freeform_remapped_interrupt.snap create mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__pending_input_preview__tests__render_one_pending_steer_with_remapped_interrupt_binding.snap create mode 100644 codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_remapped_interrupt_hint.snap diff --git a/codex-rs/config/src/tui_keymap.rs b/codex-rs/config/src/tui_keymap.rs index e41ec860ee..e063cac0e8 100644 --- a/codex-rs/config/src/tui_keymap.rs +++ b/codex-rs/config/src/tui_keymap.rs @@ -115,6 +115,8 @@ pub struct TuiGlobalKeymap { #[serde(deny_unknown_fields)] #[schemars(deny_unknown_fields)] pub struct TuiChatKeymap { + /// Interrupt the active turn. + pub interrupt_turn: Option, /// Decrease the active reasoning effort. pub decrease_reasoning_effort: Option, /// Increase the active reasoning effort. diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index f8ffe9c832..ab855d733b 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -2766,7 +2766,8 @@ "chat": { "decrease_reasoning_effort": null, "edit_queued_message": null, - "increase_reasoning_effort": null + "increase_reasoning_effort": null, + "interrupt_turn": null }, "composer": { "history_search_next": null, @@ -3094,6 +3095,14 @@ } ], "description": "Increase the active reasoning effort." + }, + "interrupt_turn": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Interrupt the active turn." } }, "type": "object" @@ -3405,7 +3414,8 @@ "default": { "decrease_reasoning_effort": null, "edit_queued_message": null, - "increase_reasoning_effort": null + "increase_reasoning_effort": null, + "interrupt_turn": null } }, "composer": { diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 65770b61ae..1fcc7635c0 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -25,7 +25,9 @@ use crate::bottom_pane::pending_thread_approvals::PendingThreadApprovals; use crate::bottom_pane::unified_exec_footer::UnifiedExecFooter; use crate::key_hint; use crate::key_hint::KeyBinding; +use crate::key_hint::KeyBindingListExt; use crate::keymap::RuntimeKeymap; +use crate::keymap::primary_binding; use crate::render::renderable::FlexRenderable; use crate::render::renderable::Renderable; use crate::render::renderable::RenderableItem; @@ -352,6 +354,12 @@ impl BottomPane { pub fn set_keymap_bindings(&mut self, keymap: &RuntimeKeymap) { self.keymap = keymap.clone(); self.composer.set_keymap_bindings(keymap); + let interrupt_binding = primary_binding(&keymap.chat.interrupt_turn); + self.pending_input_preview + .set_interrupt_binding(interrupt_binding); + if let Some(status) = self.status.as_mut() { + status.set_interrupt_binding(interrupt_binding); + } self.request_redraw(); } @@ -617,13 +625,12 @@ impl BottomPane { .and_then(parse_slash_name) .is_some_and(|(name, _, _)| name == "agent"); - // If a task is running and a status line is visible, allow Esc to - // send an interrupt even while the composer has focus. + // If a task is running and a status line is visible, allow the + // configured action to interrupt even while the composer has focus. // When a popup is active, prefer dismissing it over interrupting the task. - if key_event.code == KeyCode::Esc - && matches!(key_event.kind, KeyEventKind::Press | KeyEventKind::Repeat) + if self.keymap.chat.interrupt_turn.is_pressed(key_event) && self.is_task_running - && !is_agent_command + && !(is_agent_command && key_event.code == KeyCode::Esc) && !self.composer.popup_active() && !self.composer_should_handle_vim_insert_escape(key_event) && let Some(status) = &self.status @@ -989,6 +996,7 @@ impl BottomPane { } if let Some(status) = self.status.as_mut() { status.set_interrupt_hint_visible(/*visible*/ true); + status.set_interrupt_binding(primary_binding(&self.keymap.chat.interrupt_turn)); } self.sync_status_inline_message(); self.request_redraw(); @@ -1017,6 +1025,9 @@ impl BottomPane { self.frame_requester.clone(), self.animations_enabled, )); + if let Some(status) = self.status.as_mut() { + status.set_interrupt_binding(primary_binding(&self.keymap.chat.interrupt_turn)); + } self.sync_status_inline_message(); self.request_redraw(); } @@ -2731,6 +2742,30 @@ mod tests { ); } + #[test] + fn remapped_interrupt_turn_uses_configured_key_including_agent_drafts() { + let (tx_raw, mut rx) = unbounded_channel::(); + let tx = AppEventSender::new(tx_raw); + let mut pane = test_pane(tx); + let mut keymap = RuntimeKeymap::defaults(); + keymap.chat.interrupt_turn = vec![crate::key_hint::plain(KeyCode::F(12))]; + pane.set_keymap_bindings(&keymap); + pane.set_task_running(/*running*/ true); + pane.insert_str("/agent "); + + pane.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); + assert!( + rx.try_recv().is_err(), + "expected Esc to remain local after remapping interruption" + ); + + pane.handle_key_event(KeyEvent::new(KeyCode::F(12), KeyModifiers::NONE)); + assert!( + matches!(rx.try_recv(), Ok(AppEvent::CodexOp(Op::Interrupt))), + "expected configured key to interrupt while `/agent` is being edited" + ); + } + #[test] fn selection_view_esc_respects_remapped_list_cancel() { let (tx_raw, mut rx) = unbounded_channel::(); diff --git a/codex-rs/tui/src/bottom_pane/pending_input_preview.rs b/codex-rs/tui/src/bottom_pane/pending_input_preview.rs index 34e7d7d390..96cc1bdfd8 100644 --- a/codex-rs/tui/src/bottom_pane/pending_input_preview.rs +++ b/codex-rs/tui/src/bottom_pane/pending_input_preview.rs @@ -15,7 +15,7 @@ use crate::wrapping::adaptive_wrap_lines; /// The widget renders pending steers first, then rejected steers that will be /// resubmitted at end of turn, then ordinary queued user messages. Pending /// steers explain that they will be submitted after the next tool/result -/// boundary unless the user presses Esc to interrupt and send them +/// boundary unless the user invokes the interrupt binding to send them /// immediately. The edit hint at the bottom only appears when there are actual /// queued user inputs to pop back into the composer. Because some terminals /// intercept certain modifier-key combinations, the displayed binding is @@ -27,6 +27,8 @@ pub(crate) struct PendingInputPreview { /// Key combination rendered in the hint line. Defaults to Alt+Up but may /// be overridden for terminals where that chord is unavailable. edit_binding: Option, + /// Key combination rendered for immediately interrupting and sending steers. + interrupt_binding: Option, } const PREVIEW_LINE_LIMIT: usize = 3; @@ -38,6 +40,7 @@ impl PendingInputPreview { rejected_steers: Vec::new(), queued_messages: Vec::new(), edit_binding: Some(key_hint::alt(KeyCode::Up)), + interrupt_binding: Some(key_hint::plain(KeyCode::Esc)), } } @@ -48,6 +51,10 @@ impl PendingInputPreview { self.edit_binding = binding; } + pub(crate) fn set_interrupt_binding(&mut self, binding: Option) { + self.interrupt_binding = binding; + } + fn push_truncated_preview_lines( lines: &mut Vec>, wrapped: Vec>, @@ -81,16 +88,15 @@ impl PendingInputPreview { let mut lines = vec![]; if !self.pending_steers.is_empty() { - Self::push_section_header( - &mut lines, - width, - Line::from(vec![ - "Messages to be submitted after next tool call".into(), + let mut header = vec!["Messages to be submitted after next tool call".into()]; + if let Some(interrupt_binding) = self.interrupt_binding { + header.extend(vec![ " (press ".dim(), - key_hint::plain(KeyCode::Esc).into(), + interrupt_binding.into(), " to interrupt and send immediately)".dim(), - ]), - ); + ]); + } + Self::push_section_header(&mut lines, width, Line::from(header)); for steer in &self.pending_steers { let wrapped = adaptive_wrap_lines( @@ -327,6 +333,21 @@ mod tests { assert_snapshot!("render_one_pending_steer", format!("{buf:?}")); } + #[test] + fn render_one_pending_steer_with_remapped_interrupt_binding() { + let mut queue = PendingInputPreview::new(); + queue.pending_steers.push("Please continue.".to_string()); + queue.set_interrupt_binding(Some(key_hint::plain(KeyCode::F(12)))); + let width = 48; + let height = queue.desired_height(width); + let mut buf = Buffer::empty(Rect::new(0, 0, width, height)); + queue.render(Rect::new(0, 0, width, height), &mut buf); + assert_snapshot!( + "render_one_pending_steer_with_remapped_interrupt_binding", + format!("{buf:?}") + ); + } + #[test] fn render_pending_steers_above_queued_messages() { let mut queue = PendingInputPreview::new(); diff --git a/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs b/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs index 16baedc097..d328d9f1fd 100644 --- a/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs +++ b/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs @@ -143,6 +143,7 @@ pub(crate) struct RequestUserInputOverlay { pending_submission_draft: Option, confirm_unanswered: Option, composer_submit_keys: Vec, + interrupt_turn_keys: Vec, list_keymap: ListKeymap, } @@ -198,6 +199,7 @@ impl RequestUserInputOverlay { pending_submission_draft: None, confirm_unanswered: None, composer_submit_keys: keymap.composer.submit.clone(), + interrupt_turn_keys: keymap.chat.interrupt_turn.clone(), list_keymap: keymap.list, }; overlay.reset_for_request(); @@ -506,8 +508,15 @@ impl RequestUserInputOverlay { tips.push(FooterTip::new("ctrl + p / ctrl + n change question")); } } - if !(self.has_options() && notes_visible) { - tips.push(FooterTip::new("esc to interrupt")); + if let Some(interrupt_key) = self.interrupt_turn_keys.first() + && !(self.has_options() + && notes_visible + && *interrupt_key == crate::key_hint::plain(KeyCode::Esc)) + { + tips.push(FooterTip::new(format!( + "{} to interrupt", + interrupt_key.display_label() + ))); } tips } @@ -1056,11 +1065,12 @@ impl BottomPaneView for RequestUserInputOverlay { return; } - if matches!(key_event.code, KeyCode::Esc) { - if self.has_options() && self.notes_ui_visible() { - self.clear_notes_and_focus_options(); - return; - } + if matches!(key_event.code, KeyCode::Esc) && self.has_options() && self.notes_ui_visible() { + self.clear_notes_and_focus_options(); + return; + } + + if self.interrupt_turn_keys.is_pressed(key_event) { // TODO: Emit interrupted request_user_input results (including committed answers) // once core supports persisting them reliably without follow-up turn issues. self.app_event_tx.interrupt(); @@ -2043,6 +2053,40 @@ mod tests { ); } + #[test] + fn request_user_input_uses_remapped_interrupt_binding_while_notes_are_visible() { + let (tx, mut rx) = test_sender(); + let mut keymap = RuntimeKeymap::defaults(); + keymap.chat.interrupt_turn = vec![crate::key_hint::plain(KeyCode::F(12))]; + let mut overlay = RequestUserInputOverlay::new_with_keymap( + request_event("turn-1", vec![question_with_options("q1", "Pick one")]), + tx, + /*has_input_focus*/ true, + /*enhanced_keys_supported*/ false, + /*disable_paste_burst*/ false, + keymap, + ); + let answer = overlay.current_answer_mut().expect("answer missing"); + answer.options_state.selected_idx = Some(0); + overlay.handle_key_event(KeyEvent::from(KeyCode::Tab)); + + let tips = overlay.footer_tips(); + let tip_texts = tips.iter().map(|tip| tip.text.as_str()).collect::>(); + assert_eq!( + tip_texts, + vec![ + "tab or esc to clear notes", + "enter to submit answer", + "f12 to interrupt", + ] + ); + + overlay.handle_key_event(KeyEvent::from(KeyCode::F(12))); + + assert_eq!(overlay.done, true); + expect_interrupt_only(&mut rx); + } + #[test] fn tab_opens_notes_when_option_selected() { let (tx, _rx) = test_sender(); @@ -3171,6 +3215,26 @@ mod tests { ); } + #[test] + fn request_user_input_freeform_remapped_interrupt_snapshot() { + let (tx, _rx) = test_sender(); + let mut keymap = RuntimeKeymap::defaults(); + keymap.chat.interrupt_turn = vec![crate::key_hint::plain(KeyCode::F(12))]; + let overlay = RequestUserInputOverlay::new_with_keymap( + request_event("turn-1", vec![question_without_options("q1", "Goal")]), + tx, + /*has_input_focus*/ true, + /*enhanced_keys_supported*/ false, + /*disable_paste_burst*/ false, + keymap, + ); + let area = Rect::new(0, 0, 120, 10); + insta::assert_snapshot!( + "request_user_input_freeform_remapped_interrupt", + render_snapshot(&overlay, area) + ); + } + #[test] fn request_user_input_multi_question_first_snapshot() { let (tx, _rx) = test_sender(); diff --git a/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_freeform_remapped_interrupt.snap b/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_freeform_remapped_interrupt.snap new file mode 100644 index 0000000000..0b26b354e3 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/request_user_input/snapshots/codex_tui__bottom_pane__request_user_input__tests__request_user_input_freeform_remapped_interrupt.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/request_user_input/mod.rs +expression: "render_snapshot(&overlay, area)" +--- + + Question 1/1 (1 unanswered) + Share details. + + › Type your answer (optional) + + + + enter to submit answer | f12 to interrupt diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__pending_input_preview__tests__render_one_pending_steer_with_remapped_interrupt_binding.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__pending_input_preview__tests__render_one_pending_steer_with_remapped_interrupt_binding.snap new file mode 100644 index 0000000000..f8a3caa1bf --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__pending_input_preview__tests__render_one_pending_steer_with_remapped_interrupt_binding.snap @@ -0,0 +1,20 @@ +--- +source: tui/src/bottom_pane/pending_input_preview.rs +expression: "format!(\"{buf:?}\")" +--- +Buffer { + area: Rect { x: 0, y: 0, width: 48, height: 3 }, + content: [ + "• Messages to be submitted after next tool call ", + " (press f12 to interrupt and send immediately) ", + " ↳ Please continue. ", + ], + styles: [ + x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 2, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 47, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 0, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 20, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + ] +} diff --git a/codex-rs/tui/src/chatwidget/interaction.rs b/codex-rs/tui/src/chatwidget/interaction.rs index 0819bd6c03..d9451fc9d5 100644 --- a/codex-rs/tui/src/chatwidget/interaction.rs +++ b/codex-rs/tui/src/chatwidget/interaction.rs @@ -112,8 +112,7 @@ impl ChatWidget { return; } - if matches!(key_event.code, KeyCode::Esc) - && matches!(key_event.kind, KeyEventKind::Press | KeyEventKind::Repeat) + if self.chat_keymap.interrupt_turn.is_pressed(key_event) && !self.input_queue.pending_steers.is_empty() && self.bottom_pane.is_task_running() && self.bottom_pane.no_modal_or_popup_active() diff --git a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs index bfe563b94c..23487af437 100644 --- a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs +++ b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs @@ -953,6 +953,32 @@ async fn pending_steer_esc_does_not_steal_vim_insert_escape() { assert!(chat.input_queue.submit_pending_steers_after_interrupt); } +#[tokio::test] +async fn pending_steer_interrupt_uses_remapped_binding() { + let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + let mut keymap = crate::keymap::RuntimeKeymap::defaults(); + keymap.chat.interrupt_turn = vec![crate::key_hint::plain(KeyCode::F(12))]; + chat.chat_keymap = keymap.chat.clone(); + chat.bottom_pane.set_keymap_bindings(&keymap); + chat.bottom_pane.set_task_running(/*running*/ true); + chat.input_queue + .pending_steers + .push_back(pending_steer("queued steer")); + + chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); + + assert!(!chat.input_queue.submit_pending_steers_after_interrupt); + assert!(op_rx.try_recv().is_err()); + + chat.handle_key_event(KeyEvent::new(KeyCode::F(12), KeyModifiers::NONE)); + + match op_rx.try_recv() { + Ok(Op::Interrupt) => {} + other => panic!("expected Op::Interrupt, got {other:?}"), + } + assert!(chat.input_queue.submit_pending_steers_after_interrupt); +} + #[tokio::test] async fn restore_thread_input_state_syncs_sleep_inhibitor_state() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; diff --git a/codex-rs/tui/src/keymap.rs b/codex-rs/tui/src/keymap.rs index 8e6d40c764..7bb554a257 100644 --- a/codex-rs/tui/src/keymap.rs +++ b/codex-rs/tui/src/keymap.rs @@ -78,6 +78,8 @@ pub(crate) struct AppKeymap { /// handler code, not here. #[derive(Clone, Debug)] pub(crate) struct ChatKeymap { + /// Interrupt the active turn. + pub(crate) interrupt_turn: Vec, /// Decrease the active reasoning effort. pub(crate) decrease_reasoning_effort: Vec, /// Increase the active reasoning effort. @@ -420,6 +422,11 @@ impl RuntimeKeymap { }; let chat = ChatKeymap { + interrupt_turn: resolve_bindings( + keymap.chat.interrupt_turn.as_ref(), + &defaults.chat.interrupt_turn, + "tui.keymap.chat.interrupt_turn", + )?, decrease_reasoning_effort: resolve_bindings( keymap.chat.decrease_reasoning_effort.as_ref(), &defaults.chat.decrease_reasoning_effort, @@ -878,6 +885,7 @@ impl RuntimeKeymap { toggle_raw_output: default_bindings![alt(KeyCode::Char('r'))], }, chat: ChatKeymap { + interrupt_turn: default_bindings![plain(KeyCode::Esc)], decrease_reasoning_effort: default_bindings![alt(KeyCode::Char(','))], increase_reasoning_effort: default_bindings![alt(KeyCode::Char('.'))], edit_queued_message: default_bindings![alt(KeyCode::Up), shift(KeyCode::Left)], @@ -1127,6 +1135,7 @@ impl RuntimeKeymap { ("toggle_vim_mode", self.app.toggle_vim_mode.as_slice()), ("toggle_fast_mode", self.app.toggle_fast_mode.as_slice()), ("toggle_raw_output", self.app.toggle_raw_output.as_slice()), + ("chat.interrupt_turn", self.chat.interrupt_turn.as_slice()), ( "chat.decrease_reasoning_effort", self.chat.decrease_reasoning_effort.as_slice(), @@ -1169,6 +1178,7 @@ impl RuntimeKeymap { ("toggle_vim_mode", self.app.toggle_vim_mode.as_slice()), ("toggle_fast_mode", self.app.toggle_fast_mode.as_slice()), ("toggle_raw_output", self.app.toggle_raw_output.as_slice()), + ("chat.interrupt_turn", self.chat.interrupt_turn.as_slice()), ( "chat.decrease_reasoning_effort", self.chat.decrease_reasoning_effort.as_slice(), @@ -1197,6 +1207,11 @@ impl RuntimeKeymap { ), ], MAIN_RESERVED_BINDINGS, + [( + "chat.interrupt_turn", + "fixed.backtrack", + key_hint::plain(KeyCode::Esc), + )], )?; validate_no_shadow_with_allowed_overlaps( @@ -1249,6 +1264,18 @@ impl RuntimeKeymap { )], )?; + // The request-user-input overlay consumes turn interruption before + // configurable question navigation reaches its list handler. + validate_no_shadow_with_allowed_overlaps( + "request_user_input", + [("chat.interrupt_turn", self.chat.interrupt_turn.as_slice())], + [ + ("list.move_left", self.list.move_left.as_slice()), + ("list.move_right", self.list.move_right.as_slice()), + ], + [], + )?; + // While the composer is focused, these main-surface handlers always // consume matching keys before the event reaches the textarea editor. validate_no_shadow_with_allowed_overlaps( @@ -1261,6 +1288,7 @@ impl RuntimeKeymap { ), ("copy", self.app.copy.as_slice()), ("clear_terminal", self.app.clear_terminal.as_slice()), + ("chat.interrupt_turn", self.chat.interrupt_turn.as_slice()), ( "chat.decrease_reasoning_effort", self.chat.decrease_reasoning_effort.as_slice(), @@ -1522,6 +1550,7 @@ impl RuntimeKeymap { ("close_transcript", self.pager.close_transcript.as_slice()), ], TRANSCRIPT_BACKTRACK_RESERVED_BINDINGS, + [], )?; validate_unique( @@ -1675,10 +1704,11 @@ See the Codex keymap documentation for supported actions and examples." Ok(()) } -fn validate_no_reserved( +fn validate_no_reserved( context: &str, pairs: [(&'static str, &[KeyBinding]); N], reserved: &[(&'static str, KeyBinding)], + allowed_overlaps: [(&'static str, &'static str, KeyBinding); A], ) -> Result<(), String> { for (action, bindings) in pairs { for binding in bindings { @@ -1687,6 +1717,15 @@ fn validate_no_reserved( .iter() .find(|(_, reserved_binding)| reserved_binding.parts() == key) { + if allowed_overlaps.iter().any( + |(allowed_action, allowed_reserved_action, allowed_binding)| { + *allowed_action == action + && *allowed_reserved_action == *reserved_action + && allowed_binding.parts() == key + }, + ) { + continue; + } return Err(format!( "Ambiguous `tui.keymap.{context}` bindings: `{action}` uses a key reserved by `{reserved_action}`. \ Set a different key in `~/.codex/config.toml` and retry. \ @@ -2076,6 +2115,10 @@ mod tests { vec![key_hint::ctrl(KeyCode::Char('l'))] ); assert_eq!(runtime.app.toggle_fast_mode, Vec::new()); + assert_eq!( + runtime.chat.interrupt_turn, + vec![key_hint::plain(KeyCode::Esc)] + ); assert_eq!( runtime.chat.decrease_reasoning_effort, vec![key_hint::alt(KeyCode::Char(','))] @@ -2498,6 +2541,44 @@ mod tests { expect_conflict(&keymap, "composer.submit", "fixed.paste_image"); } + #[test] + fn interrupt_turn_allows_backtrack_escape_and_can_be_remapped_or_unbound() { + let mut keymap = TuiKeymap::default(); + let runtime = RuntimeKeymap::from_config(&keymap).expect("default keymap should parse"); + assert_eq!( + runtime.chat.interrupt_turn, + vec![key_hint::plain(KeyCode::Esc)] + ); + + keymap.chat.interrupt_turn = Some(one("f12")); + let runtime = RuntimeKeymap::from_config(&keymap).expect("remapped keymap should parse"); + assert_eq!( + runtime.chat.interrupt_turn, + vec![key_hint::plain(KeyCode::F(12))] + ); + + keymap.chat.interrupt_turn = Some(KeybindingsSpec::Many(vec![])); + let runtime = RuntimeKeymap::from_config(&keymap).expect("unbound keymap should parse"); + assert!(runtime.chat.interrupt_turn.is_empty()); + } + + #[test] + fn interrupt_turn_rejects_other_fixed_shortcuts() { + let mut keymap = TuiKeymap::default(); + keymap.chat.interrupt_turn = Some(one("ctrl-v")); + + expect_conflict(&keymap, "chat.interrupt_turn", "fixed.paste_image"); + } + + #[test] + fn interrupt_turn_rejects_request_user_input_question_navigation_bindings() { + let mut keymap = TuiKeymap::default(); + keymap.chat.interrupt_turn = Some(one("f12")); + keymap.list.move_right = Some(one("f12")); + + expect_conflict(&keymap, "chat.interrupt_turn", "list.move_right"); + } + #[test] fn rejects_pager_bindings_that_collide_with_transcript_backtrack_keys() { let mut keymap = TuiKeymap::default(); diff --git a/codex-rs/tui/src/keymap_setup.rs b/codex-rs/tui/src/keymap_setup.rs index f67ea20cb0..8c1292aeb1 100644 --- a/codex-rs/tui/src/keymap_setup.rs +++ b/codex-rs/tui/src/keymap_setup.rs @@ -1009,6 +1009,7 @@ mod tests { actions, vec![ "Composer.submit", + "Chat.interrupt_turn", "Editor.insert_newline", "Composer.queue", "Global.open_external_editor", diff --git a/codex-rs/tui/src/keymap_setup/actions.rs b/codex-rs/tui/src/keymap_setup/actions.rs index c8815c8c98..87d65b225f 100644 --- a/codex-rs/tui/src/keymap_setup/actions.rs +++ b/codex-rs/tui/src/keymap_setup/actions.rs @@ -93,6 +93,7 @@ pub(super) const KEYMAP_ACTIONS: &[KeymapActionDescriptor] = &[ action("global", "Global", "toggle_vim_mode", "Turn Vim composer mode on or off."), gated_action("global", "Global", "toggle_fast_mode", "Turn Fast mode on or off.", KeymapActionFeature::FastMode), action("global", "Global", "toggle_raw_output", "Toggle raw scrollback mode."), + action("chat", "Chat", "interrupt_turn", "Interrupt the active turn."), action("chat", "Chat", "decrease_reasoning_effort", "Decrease reasoning effort."), action("chat", "Chat", "increase_reasoning_effort", "Increase reasoning effort."), action("chat", "Chat", "edit_queued_message", "Edit the most recently queued message."), @@ -234,6 +235,7 @@ pub(super) fn binding_slot<'a>( ("global", "toggle_vim_mode") => Some(&mut keymap.global.toggle_vim_mode), ("global", "toggle_fast_mode") => Some(&mut keymap.global.toggle_fast_mode), ("global", "toggle_raw_output") => Some(&mut keymap.global.toggle_raw_output), + ("chat", "interrupt_turn") => Some(&mut keymap.chat.interrupt_turn), ("chat", "decrease_reasoning_effort") => Some(&mut keymap.chat.decrease_reasoning_effort), ("chat", "increase_reasoning_effort") => Some(&mut keymap.chat.increase_reasoning_effort), ("chat", "edit_queued_message") => Some(&mut keymap.chat.edit_queued_message), @@ -357,6 +359,7 @@ pub(super) fn bindings_for_action<'a>( ("global", "toggle_vim_mode") => Some(runtime_keymap.app.toggle_vim_mode.as_slice()), ("global", "toggle_fast_mode") => Some(runtime_keymap.app.toggle_fast_mode.as_slice()), ("global", "toggle_raw_output") => Some(runtime_keymap.app.toggle_raw_output.as_slice()), + ("chat", "interrupt_turn") => Some(runtime_keymap.chat.interrupt_turn.as_slice()), ("chat", "decrease_reasoning_effort") => Some(runtime_keymap.chat.decrease_reasoning_effort.as_slice()), ("chat", "increase_reasoning_effort") => Some(runtime_keymap.chat.increase_reasoning_effort.as_slice()), ("chat", "edit_queued_message") => Some(runtime_keymap.chat.edit_queued_message.as_slice()), diff --git a/codex-rs/tui/src/keymap_setup/picker.rs b/codex-rs/tui/src/keymap_setup/picker.rs index 52ec4a4b8b..883f9f898f 100644 --- a/codex-rs/tui/src/keymap_setup/picker.rs +++ b/codex-rs/tui/src/keymap_setup/picker.rs @@ -60,6 +60,7 @@ struct KeymapContextTab { const KEYMAP_COMMON_ACTIONS: &[(&str, &str)] = &[ ("composer", "submit"), + ("chat", "interrupt_turn"), ("editor", "insert_newline"), ("composer", "queue"), ("global", "toggle_fast_mode"), diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_all_tab_search.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_all_tab_search.snap index 4e388179a3..06efc646a8 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_all_tab_search.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_all_tab_search.snap @@ -8,9 +8,9 @@ Copy | ctrl-o | Global copy Copy Copy the last agent response to the clipboard. Clear Terminal | ctrl-l | Global clear_terminal Clear Terminal Clear the terminal UI. ctrl-l Default Toggle Vim Mode | unbound | Global toggle_vim_mode Toggle Vim Mode Turn Vim composer mode on or off. unbound Default Toggle Raw Output | alt-r | Global toggle_raw_output Toggle Raw Output Toggle raw scrollback mode. alt-r Default +Interrupt Turn | esc | Chat interrupt_turn Interrupt Turn Interrupt the active turn. esc Default Decrease Reasoning Effort | alt-, | Chat decrease_reasoning_effort Decrease Reasoning Effort Decrease reasoning effort. alt-, Default Increase Reasoning Effort | alt-. | Chat increase_reasoning_effort Increase Reasoning Effort Increase reasoning effort. alt-. Default Edit Queued Message | alt-up, shift-left | Chat edit_queued_message Edit Queued Message Edit the most recently queued message. alt-up, shift-left Default Submit | enter | Composer submit Submit Submit the current composer draft. enter Default Queue | tab | Composer queue Queue Queue the draft while a task is running. tab Default -Toggle Shortcuts | ?, shift-? | Composer toggle_shortcuts Toggle Shortcuts Show or hide the composer shortcut overlay. ?, shift-? Default diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap index e212226ef5..6c0d008aad 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap @@ -5,7 +5,7 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 106 actions, 1 customized, 2 unbound. + 107 actions, 1 customized, 2 unbound. [All] Common Customized (1) Unbound (2) App Composer Editor Vim Navigation Approval Debug @@ -16,7 +16,7 @@ expression: "render_picker(params, 120)" Global Clear Terminal ctrl-l Global - Toggle Vim Mode unbound Global Toggle Raw Output alt-r + Chat Interrupt Turn esc Chat Decrease Reasoning Effort alt-, - Chat Increase Reasoning Effort alt-. left/right group · enter edit shortcut · * custom · - unbound · esc close diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_fast_mode_enabled.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_fast_mode_enabled.snap index 22c0537540..b7384d1c4a 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_fast_mode_enabled.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_fast_mode_enabled.snap @@ -5,7 +5,7 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 107 actions, 0 customized, 3 unbound. + 108 actions, 0 customized, 3 unbound. [All] Common Customized (0) Unbound (3) App Composer Editor Vim Navigation Approval Debug @@ -17,6 +17,6 @@ expression: "render_picker(params, 120)" Global - Toggle Vim Mode unbound Global - Toggle Fast Mode unbound Global Toggle Raw Output alt-r - Chat Decrease Reasoning Effort alt-, + Chat Interrupt Turn esc left/right group · enter edit shortcut · * custom · - unbound · esc close diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap index 4a88465ba8..53ccba7e37 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap @@ -2,11 +2,11 @@ source: tui/src/keymap_setup.rs expression: snapshot --- -tab: All (106 selectable) -tab: Common (19 selectable) +tab: All (107 selectable) +tab: Common (20 selectable) tab: Customized (0) (0 selectable) tab: Unbound (2) (2 selectable) -tab: App (9 selectable) +tab: App (10 selectable) tab: Composer (5 selectable) tab: Editor (17 selectable) tab: Vim (47 selectable) @@ -19,9 +19,9 @@ Copy | ctrl-o | Global copy Copy Copy the last agent response to the clipboard. Clear Terminal | ctrl-l | Global clear_terminal Clear Terminal Clear the terminal UI. ctrl-l Default Toggle Vim Mode | unbound | Global toggle_vim_mode Toggle Vim Mode Turn Vim composer mode on or off. unbound Default Toggle Raw Output | alt-r | Global toggle_raw_output Toggle Raw Output Toggle raw scrollback mode. alt-r Default +Interrupt Turn | esc | Chat interrupt_turn Interrupt Turn Interrupt the active turn. esc Default Decrease Reasoning Effort | alt-, | Chat decrease_reasoning_effort Decrease Reasoning Effort Decrease reasoning effort. alt-, Default Increase Reasoning Effort | alt-. | Chat increase_reasoning_effort Increase Reasoning Effort Increase reasoning effort. alt-. Default Edit Queued Message | alt-up, shift-left | Chat edit_queued_message Edit Queued Message Edit the most recently queued message. alt-up, shift-left Default Submit | enter | Composer submit Submit Submit the current composer draft. enter Default Queue | tab | Composer queue Queue Queue the draft while a task is running. tab Default -Toggle Shortcuts | ?, shift-? | Composer toggle_shortcuts Toggle Shortcuts Show or hide the composer shortcut overlay. ?, shift-? Default diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap index 26af4bd57a..3b9ebb7a6a 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap @@ -5,7 +5,7 @@ expression: "render_picker(params, 78)" Keymap All configurable shortcuts. - 106 actions, 0 customized, 2 unbound. + 107 actions, 0 customized, 2 unbound. [All] Common Customized (0) Unbound (2) App Composer Editor Vim Navigation Approval Debug @@ -17,7 +17,7 @@ expression: "render_picker(params, 78)" Global Clear Terminal ctrl-l Global - Toggle Vim Mode unbound Global Toggle Raw Output alt-r + Chat Interrupt Turn esc Chat Decrease Reasoning Effort alt-, - Chat Increase Reasoning Effort alt-. left/right group · enter edit shortcut · * custom · - unbound · esc close diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap index 36f6a1d04c..c5efeb30e5 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap @@ -5,7 +5,7 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 106 actions, 0 customized, 2 unbound. + 107 actions, 0 customized, 2 unbound. [All] Common Customized (0) Unbound (2) App Composer Editor Vim Navigation Approval Debug @@ -16,7 +16,7 @@ expression: "render_picker(params, 120)" Global Clear Terminal ctrl-l Global - Toggle Vim Mode unbound Global Toggle Raw Output alt-r + Chat Interrupt Turn esc Chat Decrease Reasoning Effort alt-, - Chat Increase Reasoning Effort alt-. left/right group · enter edit shortcut · * custom · - unbound · esc close diff --git a/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_remapped_interrupt_hint.snap b/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_remapped_interrupt_hint.snap new file mode 100644 index 0000000000..ac32ebe046 --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_remapped_interrupt_hint.snap @@ -0,0 +1,5 @@ +--- +source: tui/src/status_indicator_widget.rs +expression: terminal.backend() +--- +"Working (0s • f12 to interrupt) " diff --git a/codex-rs/tui/src/status_indicator_widget.rs b/codex-rs/tui/src/status_indicator_widget.rs index dabe005358..0438c206d5 100644 --- a/codex-rs/tui/src/status_indicator_widget.rs +++ b/codex-rs/tui/src/status_indicator_widget.rs @@ -20,6 +20,7 @@ use unicode_width::UnicodeWidthStr; use crate::app_event_sender::AppEventSender; use crate::key_hint; +use crate::key_hint::KeyBinding; use crate::line_truncation::truncate_line_with_ellipsis_if_overflow; use crate::motion::MotionMode; use crate::motion::ReducedMotionIndicator; @@ -49,6 +50,7 @@ pub(crate) struct StatusIndicatorWidget { /// Optional suffix rendered after the elapsed/interrupt segment. inline_message: Option, show_interrupt_hint: bool, + interrupt_binding: Option, elapsed_running: Duration, last_resume_at: Instant, @@ -87,6 +89,7 @@ impl StatusIndicatorWidget { details_max_lines: STATUS_DETAILS_DEFAULT_MAX_LINES, inline_message: None, show_interrupt_hint: true, + interrupt_binding: Some(key_hint::plain(KeyCode::Esc)), elapsed_running: Duration::ZERO, last_resume_at: Instant::now(), is_paused: false, @@ -125,7 +128,7 @@ impl StatusIndicatorWidget { }); } - /// Update the inline suffix text shown after `({elapsed} • esc to interrupt)`. + /// Update the inline suffix text shown after the elapsed/interrupt hint. /// /// Callers should provide plain, already-contextualized text. Passing /// verbose status prose here can cause frequent width truncation and hide @@ -150,6 +153,10 @@ impl StatusIndicatorWidget { self.show_interrupt_hint = visible; } + pub(crate) fn set_interrupt_binding(&mut self, binding: Option) { + self.interrupt_binding = binding; + } + pub(crate) fn pause_timer(&mut self) { self.pause_timer_at(Instant::now()); } @@ -257,10 +264,12 @@ impl Renderable for StatusIndicatorWidget { if !spans.is_empty() { spans.push(" ".into()); } - if self.show_interrupt_hint { + if self.show_interrupt_hint + && let Some(interrupt_binding) = self.interrupt_binding + { spans.extend(vec![ format!("({pretty_elapsed} • ").dim(), - key_hint::plain(KeyCode::Esc).into(), + interrupt_binding.into(), " to interrupt)".dim(), ]); } else { @@ -405,6 +414,26 @@ mod tests { assert!(line.starts_with("Working (0s • esc to interrupt)")); } + #[test] + fn renders_remapped_interrupt_hint() { + let (tx_raw, _rx) = unbounded_channel::(); + let tx = AppEventSender::new(tx_raw); + let mut w = StatusIndicatorWidget::new( + tx, + crate::tui::FrameRequester::test_dummy(), + /*animations_enabled*/ false, + ); + w.set_interrupt_binding(Some(key_hint::plain(KeyCode::F(12)))); + w.is_paused = true; + w.elapsed_running = Duration::ZERO; + + let mut terminal = Terminal::new(TestBackend::new(80, 1)).expect("terminal"); + terminal + .draw(|f| w.render(f.area(), f.buffer_mut())) + .expect("draw"); + insta::assert_snapshot!(terminal.backend()); + } + #[test] fn timer_pauses_when_requested() { let (tx_raw, _rx) = unbounded_channel::(); From 6b4b15a1ed2271c2b35d7e73bdf532cf11e33388 Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Wed, 27 May 2026 16:18:24 -0300 Subject: [PATCH 32/87] feat(tui): render markdown tables in app style [1 of 2] (#24489) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Stack - **Current: #24489 [1 of 2]** - render markdown tables in app style. - **Stacked follow-up: #24636 [2 of 2]** - render cramped markdown tables as key/value records. ## Why Markdown tables currently render as boxed terminal grids, which gives ordinary assistant output a heavier visual treatment than surrounding rich text. This row-separated layout is the best match for how the App renders tables, while accented headers remain distinguishable even when a terminal font renders bold subtly.

Codex CLI - Before

CleanShot 2026-05-25 at 18 46 17

Codex CLI - After

image

Codex App

CleanShot 2026-05-25 at 18 45 04
## What Changed - Render markdown tables as padded, aligned rows without an enclosing box. - Style table headers with the active syntax-theme accent plus bold text, while keeping separators low contrast and theme-aware. - Use a segmented heavy header rule and thin body-row rules, preserving wrapping, narrow-width fallback, streaming parity, and rich-history rendering. - Update focused assertions and snapshots for the final table layout. ## How to Test 1. Render a markdown table in the TUI with several rows and columns. 2. Confirm the header uses the active theme accent, rows use one-character interior padding, and the table has no enclosing box. 3. Confirm the header is followed by segmented `━` rules and multiple body rows are separated by muted segmented `─` rules. 4. Render the same table while streaming and in history/raw-mode toggles; the final rich layout should remain stable. 5. Render a narrow table with long content and verify wrapping or pipe fallback does not overflow. ## Validation - `just test -p codex-tui table` - `just test -p codex-tui streaming::controller::tests` - `just argument-comment-lint-from-source -p codex-tui -- --all-targets` - `just fix -p codex-tui` - `just fmt` `just test -p codex-tui` was also run after accepting the snapshots; it fails only in the unrelated existing guardian app tests `update_feature_flags_disabling_guardian_clears_review_policy_and_restores_default` and `update_feature_flags_disabling_guardian_clears_manual_review_policy_without_history`. --- .../src/chatwidget/tests/status_and_layout.rs | 4 +- ...ll__tests__raw_mode_toggle_transcript.snap | 18 +-- codex-rs/tui/src/history_cell/tests.rs | 12 +- codex-rs/tui/src/markdown.rs | 14 +- codex-rs/tui/src/markdown_render.rs | 122 +++++++++++------- codex-rs/tui/src/markdown_render_tests.rs | 70 ++++++++-- codex-rs/tui/src/markdown_stream.rs | 4 +- ...sts__markdown_render_complex_snapshot.snap | 8 +- ...collapsing_narrative_columns_snapshot.snap | 43 +++--- codex-rs/tui/src/streaming/controller.rs | 50 +++---- codex-rs/tui/src/style.rs | 71 ++++++++++ 11 files changed, 277 insertions(+), 139 deletions(-) diff --git a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs index 5690db4ab0..acb1df6c5a 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -393,8 +393,8 @@ async fn completed_plan_table_tail_skips_provisional_history_insert() { assert!(saw_source_backed_plan, "expected source-backed plan insert"); assert!( - rendered_plan.contains('│') || rendered_plan.contains('┌'), - "expected completed plan table to render as a boxed table, got: {rendered_plan:?}" + rendered_plan.contains('━'), + "expected completed plan table to render with separators, got: {rendered_plan:?}" ); assert!( !saw_stream_plan, diff --git a/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__tests__raw_mode_toggle_transcript.snap b/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__tests__raw_mode_toggle_transcript.snap index 1d89a4bd6a..f1c04cec85 100644 --- a/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__tests__raw_mode_toggle_transcript.snap +++ b/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__tests__raw_mode_toggle_transcript.snap @@ -1,5 +1,5 @@ --- -source: tui/src/history_cell.rs +source: tui/src/history_cell/tests.rs expression: rendered --- rich before: @@ -10,11 +10,9 @@ rich before: • - first item - second item - ┌──────┬───────┐ - │ Col │ Value │ - ├──────┼───────┤ - │ code │ x = 1 │ - └──────┴───────┘ + Col Value + ━━━━━━ ━━━━━━━ + code x = 1 copy me • Called @@ -48,11 +46,9 @@ rich after: • - first item - second item - ┌──────┬───────┐ - │ Col │ Value │ - ├──────┼───────┤ - │ code │ x = 1 │ - └──────┴───────┘ + Col Value + ━━━━━━ ━━━━━━━ + code x = 1 copy me • Called diff --git a/codex-rs/tui/src/history_cell/tests.rs b/codex-rs/tui/src/history_cell/tests.rs index 8c48ced86a..6c83982a41 100644 --- a/codex-rs/tui/src/history_cell/tests.rs +++ b/codex-rs/tui/src/history_cell/tests.rs @@ -278,10 +278,8 @@ fn proposed_plan_cell_renders_markdown_table() { let rendered = render_lines(&plan.display_lines(/*width*/ 80)); assert!( - rendered - .iter() - .any(|line| line.contains('│') || line.contains('┌')), - "expected boxed table in proposed plan output: {rendered:?}" + rendered.iter().any(|line| line.contains('━')), + "expected separated table in proposed plan output: {rendered:?}" ); assert!( !rendered @@ -308,10 +306,8 @@ fn proposed_plan_cell_unwraps_markdown_fenced_table() { let rendered = render_lines(&plan.display_lines(/*width*/ 80)); assert!( - rendered - .iter() - .any(|line| line.contains('│') || line.contains('┌')), - "expected boxed table for markdown-fenced proposed plan output: {rendered:?}" + rendered.iter().any(|line| line.contains('━')), + "expected separated table for markdown-fenced proposed plan output: {rendered:?}" ); assert!( !rendered.iter().any(|line| line.trim() == "```markdown"), diff --git a/codex-rs/tui/src/markdown.rs b/codex-rs/tui/src/markdown.rs index 6d0daf89b2..0cabee0805 100644 --- a/codex-rs/tui/src/markdown.rs +++ b/codex-rs/tui/src/markdown.rs @@ -397,8 +397,8 @@ mod tests { let mut out = Vec::new(); append_markdown_agent(src, /*width*/ None, &mut out); let rendered = lines_to_strings(&out); - assert!(rendered.iter().any(|line| line.contains("┌"))); - assert!(rendered.iter().any(|line| line.contains("│ 1 │ 2 │"))); + assert!(rendered.iter().any(|line| line.contains('━'))); + assert!(rendered.iter().any(|line| line.contains(" 1 2"))); } #[test] @@ -407,11 +407,11 @@ mod tests { let mut out = Vec::new(); append_markdown_agent(src, /*width*/ None, &mut out); let rendered = lines_to_strings(&out); - assert!(rendered.iter().any(|line| line.contains("┌"))); + assert!(rendered.iter().any(|line| line.contains('━'))); assert!( rendered .iter() - .any(|line| line.contains("│ Col A │ Col B │ Col C │")) + .any(|line| line.contains(" Col A Col B Col C")) ); assert!( !rendered @@ -426,8 +426,8 @@ mod tests { let mut out = Vec::new(); append_markdown_agent(src, /*width*/ None, &mut out); let rendered = lines_to_strings(&out); - assert!(rendered.iter().any(|line| line.contains("┌"))); - assert!(rendered.iter().any(|line| line.contains("│ A"))); + assert!(rendered.iter().any(|line| line.contains('━'))); + assert!(rendered.iter().any(|line| line.contains(" left right"))); assert!(!rendered.iter().any(|line| line.trim() == "A | B")); } @@ -437,7 +437,7 @@ mod tests { let mut out = Vec::new(); append_markdown_agent(src, /*width*/ None, &mut out); let rendered = lines_to_strings(&out); - assert!(rendered.iter().any(|line| line.contains("┌"))); + assert!(rendered.iter().any(|line| line.contains('━'))); assert!(!rendered.iter().any(|line| line.trim() == "| Only |")); } diff --git a/codex-rs/tui/src/markdown_render.rs b/codex-rs/tui/src/markdown_render.rs index 75125a1ac1..87861989c0 100644 --- a/codex-rs/tui/src/markdown_render.rs +++ b/codex-rs/tui/src/markdown_render.rs @@ -22,8 +22,9 @@ //! alignment count. //! 3. **Compute column widths** -- allocate widths with content-aware //! priority and iterative shrinking. -//! 4. **Render box grid** -- Unicode borders (`┌───┬───┐`) or fallback to pipe -//! format when the minimum cannot fit. +//! 4. **Render row-separated layout** -- theme-accented bold headers, a +//! heavier segmented header rule, and low-contrast segmented body +//! separators, or fallback to pipe format when the minimum cannot fit. //! 5. **Append spillover** -- extracted spillover rows rendered as plain text //! after the table. //! @@ -36,9 +37,11 @@ //! preserved last. When even 3-char-wide columns cannot fit, the table falls //! back to pipe-delimited format. +use crate::render::highlight::foreground_style_for_scopes; use crate::render::highlight::highlight_code_to_lines; use crate::render::line_utils::line_to_static; use crate::render::line_utils::push_owned_lines; +use crate::style::table_separator_style; use crate::wrapping::RtOptions; use crate::wrapping::adaptive_wrap_line; use crate::wrapping::word_wrap_line; @@ -66,6 +69,11 @@ use std::sync::LazyLock; use unicode_width::UnicodeWidthStr; use url::Url; +const TABLE_COLUMN_GAP: usize = 2; +const TABLE_CELL_PADDING: usize = 1; +const TABLE_HEADER_SEPARATOR_CHAR: char = '━'; +const TABLE_BODY_SEPARATOR_CHAR: char = '─'; + struct MarkdownStyles { h1: Style, h2: Style, @@ -207,7 +215,7 @@ impl TableState { /// Rendered table output split by wrapping behavior. /// -/// `table_lines` are either prewrapped grid rows (box rendering) or pipe +/// `table_lines` are either prewrapped aligned rows or pipe /// fallback rows that should still pass through normal wrapping. /// `spillover_lines` are prose rows extracted from parser artifacts and should /// be routed through normal wrapping. @@ -260,7 +268,7 @@ pub fn render_markdown_text(input: &str) -> Text<'static> { /// Render markdown constrained to a known terminal width. /// /// The renderer preserves table structure when possible and falls back to -/// pipe-table output when a box table cannot fit the available width. Passing +/// pipe-table output when an aligned table cannot fit the available width. Passing /// `None` keeps intrinsic line widths and disables width-driven wrapping in the /// markdown writer. Local file links render relative to the current process /// working directory. @@ -982,13 +990,12 @@ where } } - /// Convert a completed `TableState` into styled `Line`s with Unicode - /// box-drawing borders. + /// Convert a completed `TableState` into styled, row-separated `Line`s. /// /// Pipeline: filter spillover rows -> normalize column counts -> compute - /// column widths -> render box grid (or fall back to pipe format if the + /// column widths -> render aligned rows (or fall back to pipe format if the /// minimum column widths exceed available terminal width). Spillover rows - /// are appended as plain text after the table grid. + /// are appended as plain text after the table. /// /// Falls back to `render_table_pipe_fallback` (raw `| A | B |` format) /// when `compute_column_widths` returns `None` (terminal too narrow for @@ -1048,25 +1055,38 @@ where }; }; - let border_style = Style::new().dim(); - let mut out = Vec::with_capacity(3 + rows.len() * 2); - out.push(self.render_border_line('┌', '┬', '┐', &column_widths, border_style)); + let header_style = + foreground_style_for_scopes(&["entity.name.type", "support.type", "variable"]) + .unwrap_or(self.styles.strong) + .bold(); + let separator_style = table_separator_style(); + let mut out = Vec::with_capacity(2 + rows.len() * 2); out.extend(self.render_table_row( &header, &column_widths, &table_state.alignments, - border_style, + header_style, )); - out.push(self.render_border_line('├', '┼', '┤', &column_widths, border_style)); - for row in &rows { + out.push(Self::render_table_separator( + &column_widths, + TABLE_HEADER_SEPARATOR_CHAR, + separator_style, + )); + for (row_idx, row) in rows.iter().enumerate() { out.extend(self.render_table_row( row, &column_widths, &table_state.alignments, - border_style, + Style::default(), )); + if row_idx + 1 < rows.len() { + out.push(Self::render_table_separator( + &column_widths, + TABLE_BODY_SEPARATOR_CHAR, + separator_style, + )); + } } - out.push(self.render_border_line('└', '┴', '┘', &column_widths, border_style)); RenderedTableLines { table_lines: out, table_lines_prewrapped: true, @@ -1079,17 +1099,19 @@ where row.resize(column_count, TableCell::default()); } - /// subtracts the space eaten by border characters + /// Subtract horizontal gutters and per-cell padding from the content budget. fn available_table_width(&self, column_count: usize) -> Option { self.wrap_width.map(|wrap_width| { let prefix_width = Self::spans_display_width(&self.prefix_spans(self.pending_marker_line)); - let reserved = prefix_width + 1 + (column_count * 3); + let reserved = prefix_width + + (column_count.saturating_sub(1) * TABLE_COLUMN_GAP) + + (column_count * TABLE_CELL_PADDING * 2); wrap_width.saturating_sub(reserved) }) } - /// Allocate column widths for box-drawing table rendering. + /// Allocate column widths for aligned, row-separated table rendering. /// /// Each column starts at its natural (max cell content) width, then columns /// are iteratively shrunk one character at a time until the total fits within @@ -1277,25 +1299,19 @@ where } } - fn render_border_line( - &self, - left: char, - sep: char, - right: char, + fn render_table_separator( column_widths: &[usize], + separator_char: char, style: Style, ) -> Line<'static> { - let mut spans = Vec::with_capacity(column_widths.len() * 2 + 1); - spans.push(Span::styled(String::from(left), style)); - for (idx, width) in column_widths.iter().enumerate() { - spans.push(Span::styled("─".repeat(*width + 2), style)); - if idx + 1 == column_widths.len() { - spans.push(Span::styled(String::from(right), style)); - } else { - spans.push(Span::styled(String::from(sep), style)); - } - } - Line::from(spans) + let segment_char = separator_char.to_string(); + let gap = " ".repeat(TABLE_COLUMN_GAP); + let text = column_widths + .iter() + .map(|width| segment_char.repeat(*width + (TABLE_CELL_PADDING * 2))) + .collect::>() + .join(&gap); + Line::from(Span::styled(text, style)) } fn render_table_row( @@ -1303,7 +1319,7 @@ where row: &[TableCell], column_widths: &[usize], alignments: &[Alignment], - border_style: Style, + row_style: Style, ) -> Vec> { let wrapped_cells: Vec>> = row .iter() @@ -1314,10 +1330,21 @@ where let mut out = Vec::with_capacity(row_height); for row_line in 0..row_height { + let Some(last_visible_column) = wrapped_cells.iter().rposition(|lines| { + lines + .get(row_line) + .is_some_and(|line| Self::line_display_width(line) > 0) + }) else { + out.push(Line::default().style(row_style)); + continue; + }; let mut spans = Vec::new(); - spans.push(Span::styled("│", border_style)); - for (column, width) in column_widths.iter().enumerate() { - spans.push(Span::raw(" ")); + for (column, width) in column_widths + .iter() + .enumerate() + .take(last_visible_column + 1) + { + spans.push(Span::raw(" ".repeat(TABLE_CELL_PADDING))); let line = wrapped_cells[column] .get(row_line) .cloned() @@ -1333,13 +1360,18 @@ where spans.push(Span::raw(" ".repeat(left_padding))); } spans.extend(line.spans); - if right_padding > 0 { + let is_last_column = column == last_visible_column; + if right_padding > 0 && !is_last_column { spans.push(Span::raw(" ".repeat(right_padding))); } - spans.push(Span::raw(" ")); - spans.push(Span::styled("│", border_style)); + if !is_last_column { + spans.push(Span::raw(" ".repeat(TABLE_CELL_PADDING))); + } + if !is_last_column { + spans.push(Span::raw(" ".repeat(TABLE_COLUMN_GAP))); + } } - out.push(Line::from(spans)); + out.push(Line::from(spans).style(row_style)); } out } @@ -1635,8 +1667,8 @@ where /// Push a line that has already been laid out at the correct width, skipping /// word wrapping. /// - /// Table lines are pre-formatted with exact column widths and box-drawing - /// borders. Passing them through `word_wrap_line` would break the grid at + /// Table lines are pre-formatted with exact column widths and separators. + /// Passing them through `word_wrap_line` would break the layout at /// arbitrary positions. This method prepends the indent/blockquote prefix /// and pushes directly to `self.text.lines`. fn is_blockquote_active(&self) -> bool { diff --git a/codex-rs/tui/src/markdown_render_tests.rs b/codex-rs/tui/src/markdown_render_tests.rs index d02fef76dd..68e1961699 100644 --- a/codex-rs/tui/src/markdown_render_tests.rs +++ b/codex-rs/tui/src/markdown_render_tests.rs @@ -1,4 +1,5 @@ use pretty_assertions::assert_eq; +use ratatui::style::Modifier; use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::text::Span; @@ -1471,7 +1472,7 @@ fn code_block_preserves_trailing_blank_lines() { } #[test] -fn table_renders_unicode_box() { +fn table_renders_app_style_rows_with_themed_bold_header() { let md = "| A | B |\n|---|---|\n| 1 | 2 |\n"; let text = render_markdown_text(md); let lines: Vec = text @@ -1483,13 +1484,33 @@ fn table_renders_unicode_box() { assert_eq!( lines, vec![ - "┌─────┬─────┐".to_string(), - "│ A │ B │".to_string(), - "├─────┼─────┤".to_string(), - "│ 1 │ 2 │".to_string(), - "└─────┴─────┘".to_string(), + " A B".to_string(), + "━━━━━ ━━━━━".to_string(), + " 1 2".to_string(), ] ); + assert!( + text.lines[0] + .style + .add_modifier + .contains(Modifier::BOLD) + ); + assert!( + text.lines[0].style.fg.is_some(), + "expected the syntax theme to provide a table header accent" + ); + assert!( + text.lines[1].spans[0] + .style + .add_modifier + .contains(Modifier::DIM) + ); + assert!( + !text.lines[2] + .style + .add_modifier + .contains(Modifier::BOLD) + ); } #[test] @@ -1502,13 +1523,13 @@ fn table_alignment_respects_markers() { .map(|line| line.spans.iter().map(|span| span.content.clone()).collect()) .collect(); - assert_eq!(lines[1], "│ Left │ Center │ Right │"); - assert_eq!(lines[3], "│ a │ b │ c │"); + assert_eq!(lines[0], " Left Center Right"); + assert_eq!(lines[2], " a b c"); } #[test] -fn table_wraps_cell_content_when_width_is_narrow() { - let md = "| Key | Description |\n| --- | --- |\n| -v | Enable very verbose logging output for debugging |\n"; +fn table_separates_logical_rows_after_wrapped_content() { + let md = "| Key | Description |\n| --- | --- |\n| -v | Enable very verbose logging output for debugging |\n| -q | Quiet output |\n"; let text = crate::markdown_render::render_markdown_text_with_width(md, Some(30)); let lines: Vec = text .lines @@ -1516,7 +1537,6 @@ fn table_wraps_cell_content_when_width_is_narrow() { .map(|line| line.spans.iter().map(|span| span.content.clone()).collect()) .collect(); - assert!(lines[0].starts_with('┌') && lines[0].ends_with('┐')); assert!( lines .iter() @@ -1524,6 +1544,26 @@ fn table_wraps_cell_content_when_width_is_narrow() { && lines.iter().any(|line| line.contains("logging output")), "expected wrapped row content: {lines:?}" ); + let separator_indices: Vec = lines + .iter() + .enumerate() + .filter_map(|(idx, line)| { + ((line.contains('━') || line.contains('─')) + && line.chars().all(|ch| matches!(ch, '━' | '─' | ' '))) + .then_some(idx) + }) + .collect(); + let wrapped_row_end = lines + .iter() + .position(|line| line.contains("logging output")) + .expect("expected final wrapped line"); + assert_eq!(separator_indices.len(), 2); + assert!(separator_indices[1] > wrapped_row_end); + assert!( + !lines + .last() + .is_some_and(|line| line.contains('━') || line.contains('─')) + ); } #[test] @@ -1553,7 +1593,7 @@ fn table_inside_blockquote_has_quote_prefix() { .collect(); assert!(lines.iter().all(|line| line.starts_with("> "))); - assert!(lines.iter().any(|line| line.contains("┌─────┬─────┐"))); + assert!(lines.iter().any(|line| line.contains("━━━━━ ━━━━━"))); } #[test] @@ -1580,5 +1620,9 @@ fn table_falls_back_to_pipe_rendering_if_it_cannot_fit() { .collect(); assert!(lines.first().is_some_and(|line| line.starts_with('|'))); - assert!(!lines.iter().any(|line| line.contains('┌'))); + assert!( + !lines + .iter() + .any(|line| line.contains('━') || line.contains('─')) + ); } diff --git a/codex-rs/tui/src/markdown_stream.rs b/codex-rs/tui/src/markdown_stream.rs index ad8cc68e7d..c3f2474b5b 100644 --- a/codex-rs/tui/src/markdown_stream.rs +++ b/codex-rs/tui/src/markdown_stream.rs @@ -878,8 +878,8 @@ mod tests { let rendered_strs = lines_to_plain_strings(&rendered); assert!( - rendered_strs.iter().any(|line| line.contains('┌')), - "expected markdown-fenced table to render as boxed table: {rendered_strs:?}" + rendered_strs.iter().any(|line| line.contains('━')), + "expected markdown-fenced table to render with a separator: {rendered_strs:?}" ); assert!( !rendered_strs.iter().any(|line| line.trim() == "| A | B |"), diff --git a/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__markdown_render_complex_snapshot.snap b/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__markdown_render_complex_snapshot.snap index 1e32d7c61d..30c768ab6b 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__markdown_render_complex_snapshot.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__markdown_render_complex_snapshot.snap @@ -30,11 +30,9 @@ Image: alt text Table below (alignment test): -┌──────┬────────┬───────┐ -│ Left │ Center │ Right │ -├──────┼────────┼───────┤ -│ a │ b │ c │ -└──────┴────────┴───────┘ + Left Center Right +━━━━━━ ━━━━━━━━ ━━━━━━━ + a b c Inline HTML: sup and sub. HTML block:
inline block
diff --git a/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__table_wraps_file_paths_before_collapsing_narrative_columns_snapshot.snap b/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__table_wraps_file_paths_before_collapsing_narrative_columns_snapshot.snap index 400b402023..95ad94f647 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__table_wraps_file_paths_before_collapsing_narrative_columns_snapshot.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__table_wraps_file_paths_before_collapsing_narrative_columns_snapshot.snap @@ -2,25 +2,24 @@ source: tui/src/markdown_render_tests.rs expression: "plain_lines(&text).join(\"\\n\")" --- -┌────────────────────────────────────────┬──────────────────┬──────┬─────────┬─────────────────────────────────────────┐ -│ Unit │ Files │ Adds │ Removes │ What It Adds │ -├────────────────────────────────────────┼──────────────────┼──────┼─────────┼─────────────────────────────────────────┤ -│ Suggestion engine and unit coverage │ codex-rs/core/ │ 704 │ 0 │ Sampling workflow, stable-history │ -│ │ src/ │ │ │ checks, tool-flow suppression, fast │ -│ │ next_prompt_sugg │ │ │ reasoning profile, filtering rules, │ -│ │ estion.rs:1, │ │ │ cancellation and timeout. │ -│ │ codex-rs/core/ │ │ │ │ -│ │ src/ │ │ │ │ -│ │ next_prompt_sugg │ │ │ │ -│ │ estion_tests.rs: │ │ │ │ -│ │ 1 │ │ │ │ -│ Model instruction fragment and │ codex-rs/core/ │ 54 │ 0 │ Synthetic suggestion prompt and an │ -│ contextual isolation │ src/context/ │ │ │ isolation test for ordinary user text. │ -│ │ next_prompt_sugg │ │ │ │ -│ │ estion.rs:1, │ │ │ │ -│ │ codex-rs/core/ │ │ │ │ -│ │ src/context/ │ │ │ │ -│ │ contextual_user_ │ │ │ │ -│ │ message_tests.rs │ │ │ │ -│ │ :1 │ │ │ │ -└────────────────────────────────────────┴──────────────────┴──────┴─────────┴─────────────────────────────────────────┘ + Unit Files Adds Removes What It Adds +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━ ━━━━━━ ━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Suggestion engine and unit coverage codex-rs/core/ 704 0 Sampling workflow, stable-history + src/ checks, tool-flow suppression, fast + next_prompt_sugg reasoning profile, filtering rules, + estion.rs:1, cancellation and timeout. + codex-rs/core/ + src/ + next_prompt_sugg + estion_tests.rs: + 1 +─────────────────────────────────────── ────────────────── ────── ───────── ──────────────────────────────────────── + Model instruction fragment and codex-rs/core/ 54 0 Synthetic suggestion prompt and an + contextual isolation src/context/ isolation test for ordinary user text. + next_prompt_sugg + estion.rs:1, + codex-rs/core/ + src/context/ + contextual_user_ + message_tests.rs + :1 diff --git a/codex-rs/tui/src/streaming/controller.rs b/codex-rs/tui/src/streaming/controller.rs index a653f1b210..0cdf956087 100644 --- a/codex-rs/tui/src/streaming/controller.rs +++ b/codex-rs/tui/src/streaming/controller.rs @@ -1362,7 +1362,7 @@ mod tests { } #[test] - fn controller_renders_unicode_for_multi_table_response_shape() { + fn controller_renders_separators_for_multi_table_response_shape() { let source = "Absolutely. Here are several different Markdown table patterns you can use for rendering tests.\n\n| Name | Role | Location |\n|-------|-----------|----------|\n| Ava | Engineer | NYC |\n| Malik | Designer | Berlin |\n| Priya | PM | Remote |\n\n| Item | Qty | Price | In Stock |\n|:------------|----:|------:|:--------:|\n| Keyboard | 2 | 49.99 | Yes |\n| Mouse | 10 @@ -1378,13 +1378,13 @@ mod tests { let deltas = chunked.iter().map(String::as_str).collect::>(); let streamed = collect_streamed_lines(&deltas, Some(120)); assert!( - streamed.iter().any(|line| line.contains('┌')), - "expected unicode table border in streamed output: {streamed:?}" + streamed.iter().any(|line| line.contains('━')), + "expected table separator in streamed output: {streamed:?}" ); } #[test] - fn controller_renders_unicode_for_no_outer_pipes_table_shape() { + fn controller_renders_separators_for_no_outer_pipes_table_shape() { let source = "### 1) Basic\n\n| Name | Role | Active |\n|---|---|---|\n| Alice | Engineer | Yes |\n| Bob | Designer | No |\n\n### 2) No outer pipes\n\nCol A | Col B | Col C\n--- | --- | ---\nx | y | z\n10 | 20 | 30\n\n### 3) Another table\n\n| Key | Value |\n|---|---|\n| a | b |\n"; @@ -1407,6 +1407,10 @@ mod tests { !has_raw_no_outer_header, "no-outer-pipes header should not remain raw in final streamed output: {streamed:?}" ); + assert!( + streamed.iter().any(|line| line.contains('━')), + "expected table separator in final streamed output: {streamed:?}" + ); } #[test] @@ -1429,8 +1433,8 @@ mod tests { assert_eq!(streamed, expected); assert!( - streamed.iter().any(|line| line.contains('┌')), - "expected unicode table border for no-outer-pipes streaming: {streamed:?}" + streamed.iter().any(|line| line.contains('━')), + "expected table separator for no-outer-pipes streaming: {streamed:?}" ); assert!( !streamed @@ -1458,8 +1462,8 @@ mod tests { assert_eq!(streamed, expected); assert!( - streamed.iter().any(|line| line.contains('┌')), - "expected unicode table border for two-column no-outer table: {streamed:?}" + streamed.iter().any(|line| line.contains('━')), + "expected table separator for two-column no-outer table: {streamed:?}" ); assert!( !streamed.iter().any(|line| line.trim() == "A | B"), @@ -1490,8 +1494,8 @@ mod tests { assert!( streamed .iter() - .any(|line| line.contains("┌───────┬───────┬───────┐")), - "expected converted no-outer table border in streamed output: {streamed:?}" + .any(|line| line.contains(" Col A Col B Col C")), + "expected converted no-outer table header in streamed output: {streamed:?}" ); } @@ -1513,8 +1517,8 @@ mod tests { assert_eq!(streamed, expected); assert!( - streamed.iter().any(|line| line.contains('┌')), - "expected unicode table border in streamed output: {streamed:?}" + streamed.iter().any(|line| line.contains('━')), + "expected table separator in streamed output: {streamed:?}" ); assert!( !streamed.iter().any(|line| line.trim() == "| A | B |"), @@ -1542,8 +1546,8 @@ mod tests { assert_eq!(streamed, expected); assert!( - streamed.iter().any(|line| line.contains('┌')), - "expected unicode table border in streamed output: {streamed:?}" + streamed.iter().any(|line| line.contains('━')), + "expected table separator in streamed output: {streamed:?}" ); assert!( !streamed @@ -1621,8 +1625,10 @@ mod tests { "expected code-fenced pipe line to remain raw: {streamed:?}" ); assert!( - !streamed.iter().any(|line| line.contains('┌')), - "did not expect unicode table border for non-markdown fence: {streamed:?}" + !streamed + .iter() + .any(|line| line.contains('━') || line.contains('─')), + "did not expect a table separator for non-markdown fence: {streamed:?}" ); } @@ -1643,10 +1649,8 @@ mod tests { assert_eq!(streamed, baseline); assert!( - streamed - .iter() - .any(|line| line.contains('│') || line.contains('└') || line.contains('┌')), - "expected unicode table box drawing chars in plan streamed output: {streamed:?}" + streamed.iter().any(|line| line.contains('━')), + "expected table separators in plan streamed output: {streamed:?}" ); assert!( !streamed @@ -1675,10 +1679,8 @@ mod tests { assert_eq!(streamed, baseline); assert!( - streamed - .iter() - .any(|line| line.contains('│') || line.contains('└') || line.contains('┌')), - "expected unicode table box drawing chars in fenced plan output: {streamed:?}" + streamed.iter().any(|line| line.contains('━')), + "expected table separators in fenced plan output: {streamed:?}" ); assert!( !streamed diff --git a/codex-rs/tui/src/style.rs b/codex-rs/tui/src/style.rs index 10c269174f..0284df9142 100644 --- a/codex-rs/tui/src/style.rs +++ b/codex-rs/tui/src/style.rs @@ -1,12 +1,18 @@ use crate::color::blend; use crate::color::is_light; +use crate::terminal_palette::StdoutColorLevel; use crate::terminal_palette::best_color; use crate::terminal_palette::default_bg; +use crate::terminal_palette::default_fg; +use crate::terminal_palette::rgb_color; +use crate::terminal_palette::stdout_color_level; use ratatui::style::Color; use ratatui::style::Style; use ratatui::style::Stylize; const LIGHT_BG_ACCENT_RGB: (u8, u8, u8) = (0, 95, 135); +// Decorative table rules should remain visible without competing with cell content. +const TABLE_SEPARATOR_FG_ALPHA: f32 = 0.20; pub fn user_message_style() -> Style { user_message_style_for(default_bg()) @@ -16,6 +22,11 @@ pub fn proposed_plan_style() -> Style { proposed_plan_style_for(default_bg()) } +/// Returns a low-contrast rule style for separators within markdown tables. +pub(crate) fn table_separator_style() -> Style { + table_separator_style_for(default_fg(), default_bg(), stdout_color_level()) +} + /// Returns the shared accent style for active or selected TUI controls. pub(crate) fn accent_style() -> Style { accent_style_for(default_bg()) @@ -45,6 +56,22 @@ pub(crate) fn accent_style_for(terminal_bg: Option<(u8, u8, u8)>) -> Style { } } +fn table_separator_style_for( + terminal_fg: Option<(u8, u8, u8)>, + terminal_bg: Option<(u8, u8, u8)>, + color_level: StdoutColorLevel, +) -> Style { + let (Some(fg), Some(bg)) = (terminal_fg, terminal_bg) else { + return Style::default().dim(); + }; + let separator_rgb = blend(fg, bg, TABLE_SEPARATOR_FG_ALPHA); + match color_level { + StdoutColorLevel::TrueColor => Style::default().fg(rgb_color(separator_rgb)), + StdoutColorLevel::Ansi256 => Style::default().fg(best_color(separator_rgb)), + StdoutColorLevel::Ansi16 | StdoutColorLevel::Unknown => Style::default().dim(), + } +} + #[allow(clippy::disallowed_methods)] pub fn user_message_bg(terminal_bg: (u8, u8, u8)) -> Color { let (top, alpha) = if is_light(terminal_bg) { @@ -81,4 +108,48 @@ mod tests { assert_eq!(accent_style_for(Some((0, 0, 0))), expected); assert_eq!(accent_style_for(/*terminal_bg*/ None), expected); } + + #[test] + fn table_separator_blends_toward_dark_background() { + let style = table_separator_style_for( + Some((255, 255, 255)), + Some((0, 0, 0)), + StdoutColorLevel::TrueColor, + ); + + assert_eq!(style.fg, Some(rgb_color((51, 51, 51)))); + } + + #[test] + fn table_separator_blends_toward_light_background() { + let style = table_separator_style_for( + Some((0, 0, 0)), + Some((255, 255, 255)), + StdoutColorLevel::TrueColor, + ); + + assert_eq!(style.fg, Some(rgb_color((204, 204, 204)))); + } + + #[test] + fn table_separator_dims_when_palette_aware_color_is_unavailable() { + let expected = Style::default().dim(); + + assert_eq!( + table_separator_style_for( + Some((255, 255, 255)), + Some((0, 0, 0)), + StdoutColorLevel::Ansi16, + ), + expected + ); + assert_eq!( + table_separator_style_for( + /*terminal_fg*/ None, + Some((0, 0, 0)), + StdoutColorLevel::TrueColor, + ), + expected + ); + } } From 07a930138f675a1ccdd15339f74bb337f9f02398 Mon Sep 17 00:00:00 2001 From: Celia Chen Date: Wed, 27 May 2026 12:39:01 -0700 Subject: [PATCH 33/87] chore: enable namespace tools for Bedrock (#24713) Client-side namespace tools are now supported by bedrock. Enable `namespace_tools` for the Amazon Bedrock provider while continuing to disable unsupported hosted tools such as image generation and web search. --- .../v2/model_provider_capabilities_read.rs | 2 +- codex-rs/core/src/tools/spec_plan_tests.rs | 18 +++++++++--------- .../model-provider/src/amazon_bedrock/mod.rs | 6 +++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/codex-rs/app-server/tests/suite/v2/model_provider_capabilities_read.rs b/codex-rs/app-server/tests/suite/v2/model_provider_capabilities_read.rs index 6dcb9ac4ec..529f238798 100644 --- a/codex-rs/app-server/tests/suite/v2/model_provider_capabilities_read.rs +++ b/codex-rs/app-server/tests/suite/v2/model_provider_capabilities_read.rs @@ -60,7 +60,7 @@ async fn read_amazon_bedrock_provider_capabilities() -> Result<()> { let received: ModelProviderCapabilitiesReadResponse = to_response(response)?; let expected = ModelProviderCapabilitiesReadResponse { - namespace_tools: false, + namespace_tools: true, image_generation: false, web_search: false, }; diff --git a/codex-rs/core/src/tools/spec_plan_tests.rs b/codex-rs/core/src/tools/spec_plan_tests.rs index 2eb6eea5d2..a2f12d1c01 100644 --- a/codex-rs/core/src/tools/spec_plan_tests.rs +++ b/codex-rs/core/src/tools/spec_plan_tests.rs @@ -526,7 +526,7 @@ async fn mcp_and_tool_search_follow_direct_and_deferred_tool_exposure() { "read_mcp_resource", ]); - let missing_namespace_capability = probe_with( + let bedrock_namespace_capability = probe_with( |turn| { turn.model_info.supports_search_tool = true; use_bedrock_provider(turn); @@ -537,7 +537,7 @@ async fn mcp_and_tool_search_follow_direct_and_deferred_tool_exposure() { }, ) .await; - missing_namespace_capability.assert_visible_lacks(&["tool_search"]); + bedrock_namespace_capability.assert_visible_contains(&["tool_search"]); let enabled = probe_with( |turn| { @@ -890,7 +890,7 @@ async fn multi_agent_v2_can_use_configured_tool_namespace() { } #[tokio::test] -async fn multi_agent_v2_namespace_is_ignored_without_provider_namespace_support() { +async fn multi_agent_v2_namespace_is_supported_by_bedrock_provider() { let plan = probe(|turn| { set_feature(turn, Feature::MultiAgentV2, /*enabled*/ true); update_config(turn, |config| { @@ -900,15 +900,15 @@ async fn multi_agent_v2_namespace_is_ignored_without_provider_namespace_support( }) .await; - plan.assert_visible_contains(&["spawn_agent", "send_message", "list_agents"]); - plan.assert_visible_lacks(&["agents"]); - assert!( - plan.registered_names - .contains(&ToolName::plain("spawn_agent").to_string()) - ); + plan.assert_visible_contains(&["agents"]); + plan.assert_visible_lacks(&["spawn_agent", "send_message", "list_agents"]); assert!( !plan .registered_names + .contains(&ToolName::plain("spawn_agent").to_string()) + ); + assert!( + plan.registered_names .contains(&ToolName::namespaced("agents", "spawn_agent").to_string()) ); } diff --git a/codex-rs/model-provider/src/amazon_bedrock/mod.rs b/codex-rs/model-provider/src/amazon_bedrock/mod.rs index 3940f73fcd..4d6a9afb3c 100644 --- a/codex-rs/model-provider/src/amazon_bedrock/mod.rs +++ b/codex-rs/model-provider/src/amazon_bedrock/mod.rs @@ -57,7 +57,7 @@ impl ModelProvider for AmazonBedrockModelProvider { fn capabilities(&self) -> ProviderCapabilities { ProviderCapabilities { - namespace_tools: false, + namespace_tools: true, image_generation: false, web_search: false, } @@ -131,7 +131,7 @@ mod tests { } #[test] - fn capabilities_disable_unsupported_launch_features() { + fn capabilities_disable_unsupported_hosted_tools() { let provider = AmazonBedrockModelProvider::new( ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None), ); @@ -139,7 +139,7 @@ mod tests { assert_eq!( provider.capabilities(), ProviderCapabilities { - namespace_tools: false, + namespace_tools: true, image_generation: false, web_search: false, } From 9152ebd289f2d9103ffc41c2fec69f6c623a6eab Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Wed, 27 May 2026 12:59:11 -0700 Subject: [PATCH 34/87] fix(linux-sandbox): preserve shell cleanup on interruption (#22729) ## Why Interrupted `shell_command` calls can race with the outer tool-dispatch cancellation path. When that happens, the runtime future may be dropped before the spawned process gets a chance to run `SIGTERM` cleanup. For bwrapd-backed Linux sandbox commands, that can leave synthetic protected-path mount bookkeeping such as `.git/.codex` registrations under `/tmp` behind after a TUI interruption. The relevant cancellation points are the outer dispatch race in [`core/src/tools/parallel.rs`](https://github.com/openai/codex/blob/bd184ba84703cc924921ed883f0cf17d3dba60ff/codex-rs/core/src/tools/parallel.rs#L91-L132) and the process shutdown logic in [`core/src/exec.rs`](https://github.com/openai/codex/blob/bd184ba84703cc924921ed883f0cf17d3dba60ff/codex-rs/core/src/exec.rs#L1367-L1393). ## What changed - Keep `shell_command` dispatch alive long enough for the runtime to finish cancellation cleanup instead of immediately returning the synthetic aborted response. - Fold shell-turn cancellation into the existing `ExecExpiration` path in [`core/src/tools/runtimes/shell.rs`](https://github.com/openai/codex/blob/bd184ba84703cc924921ed883f0cf17d3dba60ff/codex-rs/core/src/tools/runtimes/shell.rs#L267-L274), so cancellation and timeout behavior stay centralized. - On cancellation, send `SIGTERM` first, wait briefly for cleanup to run, then hard-kill any remaining descendants in the original process group. - Treat `ESRCH` as an already-gone process-group cleanup case in `codex-utils-pty`, which keeps best-effort teardown from surfacing a stale-process race as an error. ## Verification - `cargo test -p codex-core cancellation` - Added regression coverage for: - `shell_tool_cancellation_waits_for_runtime_cleanup` - `process_exec_tool_call_cancellation_allows_sigterm_cleanup` --- codex-rs/core/src/exec.rs | 53 ++++- codex-rs/core/src/exec_tests.rs | 109 ++++++++++ codex-rs/core/src/session/tests.rs | 70 +++++++ codex-rs/core/src/tools/handlers/shell.rs | 4 + .../src/tools/handlers/shell/shell_command.rs | 6 + codex-rs/core/src/tools/parallel.rs | 193 ++++++++++++++++-- codex-rs/core/src/tools/registry.rs | 69 +++++-- codex-rs/core/src/tools/router.rs | 6 + codex-rs/core/src/tools/runtimes/shell.rs | 3 + codex-rs/utils/pty/src/process_group.rs | 8 +- 10 files changed, 474 insertions(+), 47 deletions(-) diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 5cec3b49bb..a485047a32 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -56,6 +56,7 @@ const SIGKILL_CODE: i32 = 9; const TIMEOUT_CODE: i32 = 64; const EXIT_CODE_SIGNAL_BASE: i32 = 128; // conventional shell: 128 + signal const EXEC_TIMEOUT_EXIT_CODE: i32 = 124; // conventional timeout exit code +const CANCELLATION_TERMINATION_GRACE_PERIOD: Duration = Duration::from_millis(50); // I/O buffer sizing const READ_CHUNK_SIZE: usize = 8192; // bytes per read @@ -1358,15 +1359,49 @@ async fn consume_output( (exit_status, false) } outcome = &mut expiration_wait => { - kill_child_process_group(&mut child)?; - child.start_kill()?; - let timed_out = matches!(outcome, Some(ExecExpirationOutcome::TimedOut)); - let exit_status = if timed_out { - synthetic_exit_status(EXIT_CODE_SIGNAL_BASE + TIMEOUT_CODE) - } else { - synthetic_exit_status_for_code(/*code*/ 1) - }; - (exit_status, timed_out) + match outcome { + Some(ExecExpirationOutcome::TimedOut) => { + kill_child_process_group(&mut child)?; + child.start_kill()?; + ( + synthetic_exit_status(EXIT_CODE_SIGNAL_BASE + TIMEOUT_CODE), + true, + ) + } + Some(ExecExpirationOutcome::Cancelled) => { + // Let TERM-aware processes run cleanup briefly, then kill any + // remaining members of the original process group. + let process_group_id = child.id(); + let should_escalate = if let Some(process_group_id) = process_group_id { + codex_utils_pty::process_group::terminate_process_group(process_group_id)? + } else { + false + }; + match tokio::time::timeout( + CANCELLATION_TERMINATION_GRACE_PERIOD, + child.wait(), + ) + .await + { + Ok(status) => { + status?; + if should_escalate + && let Some(process_group_id) = process_group_id + { + codex_utils_pty::process_group::kill_process_group( + process_group_id, + )?; + } + } + Err(_) => { + kill_child_process_group(&mut child)?; + child.start_kill()?; + } + } + (synthetic_exit_status_for_code(/*code*/ 1), false) + } + None => unreachable!("expiration wait only resolves while expiration is active"), + } } _ = tokio::signal::ctrl_c() => { kill_child_process_group(&mut child)?; diff --git a/codex-rs/core/src/exec_tests.rs b/codex-rs/core/src/exec_tests.rs index 23db019bb9..504bac622e 100644 --- a/codex-rs/core/src/exec_tests.rs +++ b/codex-rs/core/src/exec_tests.rs @@ -1128,6 +1128,115 @@ async fn process_exec_tool_call_respects_cancellation_token() -> Result<()> { Ok(()) } +#[cfg(unix)] +#[tokio::test] +async fn process_exec_tool_call_cancellation_allows_sigterm_cleanup() -> Result<()> { + let temp_dir = tempfile::TempDir::new()?; + let ready_marker = temp_dir.path().join("ready"); + let cleanup_marker = temp_dir.path().join("cleanup"); + let descendant_pid_marker = temp_dir.path().join("descendant-pid"); + // The parent handles TERM and records cleanup, while a TERM-ignoring child + // proves cancellation still escalates any survivors in the process group. + let command = vec![ + "/bin/sh".to_string(), + "-c".to_string(), + r#"(trap '' TERM; sleep 60) & +printf '%s' "$!" > "$DESCENDANT_PID_MARKER" +trap 'printf cleaned > "$CLEANUP_MARKER"; exit 0' TERM +printf ready > "$READY_MARKER" +while :; do sleep 1; done"# + .to_string(), + ]; + let cwd = codex_utils_absolute_path::AbsolutePathBuf::current_dir()?; + let mut env: HashMap = std::env::vars().collect(); + env.insert( + "READY_MARKER".to_string(), + ready_marker.to_string_lossy().into_owned(), + ); + env.insert( + "CLEANUP_MARKER".to_string(), + cleanup_marker.to_string_lossy().into_owned(), + ); + env.insert( + "DESCENDANT_PID_MARKER".to_string(), + descendant_pid_marker.to_string_lossy().into_owned(), + ); + let cancel_token = CancellationToken::new(); + let cancel_tx = cancel_token.clone(); + tokio::spawn(async move { + for _ in 0..50 { + if ready_marker.exists() { + cancel_tx.cancel(); + return; + } + tokio::time::sleep(Duration::from_millis(20)).await; + } + cancel_tx.cancel(); + }); + let params = ExecParams { + command, + cwd: cwd.clone(), + expiration: ExecExpiration::DefaultTimeout.with_cancellation(cancel_token), + capture_policy: ExecCapturePolicy::ShellTool, + env, + network: None, + sandbox_permissions: SandboxPermissions::UseDefault, + windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled, + windows_sandbox_private_desktop: false, + justification: None, + arg0: None, + }; + + let result = timeout( + Duration::from_secs(5), + process_exec_tool_call( + params, + &PermissionProfile::Disabled, + &cwd, + &None, + /*use_legacy_landlock*/ false, + /*stdout_stream*/ None, + ), + ) + .await + .expect("cancellation should stop the process promptly"); + let output = result.expect("cancellation should return a non-timeout exec result"); + assert!(!output.timed_out); + assert_eq!( + std::fs::read_to_string(cleanup_marker)?, + "cleaned", + "SIGTERM cleanup trap should run before cancellation falls back to a hard kill" + ); + let descendant_pid = std::fs::read_to_string(descendant_pid_marker)? + .parse::() + .map_err(|error| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("failed to parse descendant pid: {error}"), + ) + })?; + let mut killed = false; + for _ in 0..20 { + if unsafe { libc::kill(descendant_pid, 0) } == -1 + && let Some(libc::ESRCH) = std::io::Error::last_os_error().raw_os_error() + { + killed = true; + break; + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + if !killed { + unsafe { + libc::kill(descendant_pid, libc::SIGKILL); + } + } + assert!( + killed, + "TERM-ignoring descendant process with pid {descendant_pid} is still alive" + ); + Ok(()) +} + #[cfg(unix)] fn long_running_command() -> Vec { vec![ diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 5cb8f438ab..f8bc8ae4e4 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -10131,6 +10131,76 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() { ExecApprovalRequirement::Skip { .. } )); } + +#[cfg(unix)] +#[tokio::test] +async fn shell_tool_cancellation_waits_for_runtime_cleanup() -> anyhow::Result<()> { + let session = make_session_with_config(|config| { + let cwd = config.cwd.clone(); + config + .permissions + .set_legacy_sandbox_policy(SandboxPolicy::DangerFullAccess, cwd.as_path()) + .expect("test setup should allow sandbox policy"); + }) + .await?; + let turn_context = session.new_default_turn().await; + let session = Arc::new(session); + let turn_context = Arc::new(turn_context); + let temp_dir = tempfile::TempDir::new()?; + let ready_marker = temp_dir.path().join("ready"); + let cleanup_marker = temp_dir.path().join("cleanup"); + // Interrupt after the shell starts, then verify dispatch waits for its TERM cleanup trap. + let command = format!( + r#"trap 'printf cleaned > "{}"; exit 0' TERM +printf ready > "{}" +while :; do sleep 1; done"#, + cleanup_marker.display(), + ready_marker.display(), + ); + let item = ResponseItem::FunctionCall { + id: None, + name: "shell_command".to_string(), + namespace: None, + arguments: serde_json::json!({ + "command": command, + "timeout_ms": 60_000, + }) + .to_string(), + call_id: "shell-cleanup-call".to_string(), + }; + let call = ToolRouter::build_tool_call(item)? + .expect("shell command response item should build a tool call"); + let cancellation_token = CancellationToken::new(); + let cancellation_tx = cancellation_token.clone(); + let handle = tokio::spawn( + test_tool_runtime(Arc::clone(&session), Arc::clone(&turn_context)) + .handle_tool_call(call, cancellation_token), + ); + + let mut ready = false; + for _ in 0..50 { + if ready_marker.exists() { + ready = true; + break; + } + tokio::time::sleep(Duration::from_millis(20)).await; + } + if !ready { + cancellation_tx.cancel(); + let _ = timeout(Duration::from_secs(5), handle).await; + anyhow::bail!("shell command should reach the ready marker"); + } + + cancellation_tx.cancel(); + timeout(Duration::from_secs(5), handle) + .await + .expect("cancelled shell tool should finish promptly") + .expect("shell tool task should join") + .expect("cancelled shell tool should return a response item"); + assert_eq!(std::fs::read_to_string(cleanup_marker)?, "cleaned"); + Ok(()) +} + #[tokio::test] async fn unified_exec_rejects_escalated_permissions_when_policy_not_on_request() { use crate::sandboxing::SandboxPermissions; diff --git a/codex-rs/core/src/tools/handlers/shell.rs b/codex-rs/core/src/tools/handlers/shell.rs index 09eb29b1e4..c890e0d8da 100644 --- a/codex-rs/core/src/tools/handlers/shell.rs +++ b/codex-rs/core/src/tools/handlers/shell.rs @@ -2,6 +2,7 @@ use codex_features::Feature; use codex_protocol::models::ShellCommandToolCallParams; use serde_json::Value as JsonValue; use std::sync::Arc; +use tokio_util::sync::CancellationToken; use crate::exec::ExecParams; use crate::exec_policy::ExecApprovalRequest; @@ -44,6 +45,7 @@ fn shell_command_payload_command(payload: &ToolPayload) -> Option { struct RunExecLikeArgs { tool_name: ToolName, exec_params: ExecParams, + cancellation_token: CancellationToken, hook_command: String, shell_type: Option, additional_permissions: Option, @@ -59,6 +61,7 @@ async fn run_exec_like(args: RunExecLikeArgs) -> Result Result for ShellCommandHandler { let ToolInvocation { session, turn, + cancellation_token, tracker, call_id, payload, @@ -185,6 +186,7 @@ impl ToolExecutor for ShellCommandHandler { run_exec_like(RunExecLikeArgs { tool_name, exec_params, + cancellation_token, hook_command: params.command, shell_type, additional_permissions: params.additional_permissions.clone(), @@ -205,6 +207,10 @@ impl CoreToolRuntime for ShellCommandHandler { matches!(payload, ToolPayload::Function { .. }) } + fn waits_for_runtime_cancellation(&self) -> bool { + true + } + fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option { shell_command_payload_command(&invocation.payload).map(|command| PreToolUsePayload { tool_name: HookToolName::bash(), diff --git a/codex-rs/core/src/tools/parallel.rs b/codex-rs/core/src/tools/parallel.rs index bac6361329..7db1ec9643 100644 --- a/codex-rs/core/src/tools/parallel.rs +++ b/codex-rs/core/src/tools/parallel.rs @@ -92,6 +92,7 @@ impl ToolCallRuntime { let tracker = Arc::clone(&self.tracker); let lock = Arc::clone(&self.parallel_execution); let invocation_cancellation_token = cancellation_token.clone(); + let wait_for_runtime_cancellation = self.router.tool_waits_for_runtime_cancellation(&call); let started = Instant::now(); let abort_session = Arc::clone(&session); let abort_source = source.clone(); @@ -140,23 +141,35 @@ impl ToolCallRuntime { } else { let secs = started.elapsed().as_secs_f32().max(0.1); abort_dispatch_span.record("aborted", true); - handle.abort(); - match handle.await { - Ok(result) => result, - Err(err) if err.is_cancelled() => { - let response = Self::aborted_response(&call, secs); - notify_tool_aborted( - abort_session.as_ref(), - abort_turn.as_ref(), - call.call_id.as_str(), - &call.tool_name, - abort_source, - ) - .await; - Ok(response) + if wait_for_runtime_cancellation { + if terminal_outcome_reached.swap(true, Ordering::AcqRel) { + return handle.await.map_err(Self::tool_task_join_error)?; + } + // The abort owns the terminal outcome; await only so + // the runtime can finish process teardown. + match handle.await { + Ok(_) => {} + Err(err) if err.is_cancelled() => {} + Err(err) => return Err(Self::tool_task_join_error(err)), + } + } else { + handle.abort(); + match handle.await { + Ok(result) => return result, + Err(err) if err.is_cancelled() => {} + Err(err) => return Err(Self::tool_task_join_error(err)), } - Err(err) => Err(Self::tool_task_join_error(err)), } + let response = Self::aborted_response(&call, secs); + notify_tool_aborted( + abort_session.as_ref(), + abort_turn.as_ref(), + call.call_id.as_str(), + &call.tool_name, + abort_source, + ) + .await; + Ok(response) } }, } @@ -274,6 +287,85 @@ mod tests { impl CoreToolRuntime for ImmediateHandler {} + struct CancellationCleanupHandler { + tool_name: codex_tools::ToolName, + started: std::sync::Mutex>>, + cleanup_started: std::sync::Mutex>>, + allow_cleanup: Arc, + } + + #[async_trait::async_trait] + impl ToolExecutor for CancellationCleanupHandler { + fn tool_name(&self) -> codex_tools::ToolName { + self.tool_name.clone() + } + + fn spec(&self) -> codex_tools::ToolSpec { + codex_tools::ToolSpec::Function(codex_tools::ResponsesApiTool { + name: self.tool_name.name.clone(), + description: "Cancellation cleanup test tool.".to_string(), + strict: false, + defer_loading: None, + parameters: codex_tools::JsonSchema::default(), + output_schema: None, + }) + } + + async fn handle( + &self, + invocation: ToolInvocation, + ) -> Result, FunctionCallError> { + let started = self + .started + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .take(); + if let Some(started) = started { + let _ = started.send(()); + } + invocation.cancellation_token.cancelled().await; + let cleanup_started = self + .cleanup_started + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .take(); + if let Some(cleanup_started) = cleanup_started { + let _ = cleanup_started.send(()); + } + self.allow_cleanup.notified().await; + Ok(Box::new(FunctionToolOutput::from_text( + "cleanup complete".to_string(), + Some(false), + ))) + } + } + + impl CoreToolRuntime for CancellationCleanupHandler { + fn waits_for_runtime_cancellation(&self) -> bool { + true + } + } + + struct FinishRecorder { + records: Arc>>, + } + + impl codex_extension_api::ToolLifecycleContributor for FinishRecorder { + fn on_tool_finish<'a>( + &'a self, + input: codex_extension_api::ToolFinishInput<'a>, + ) -> codex_extension_api::ToolLifecycleFuture<'a> { + let records = Arc::clone(&self.records); + let outcome = input.outcome; + Box::pin(async move { + records + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .push(outcome); + }) + } + } + struct BlockingFinishContributor { records: Arc>>, finish_started: std::sync::Mutex>>, @@ -375,4 +467,75 @@ mod tests { Ok(()) } + + #[tokio::test] + async fn cancellation_waiting_for_runtime_cleanup_emits_only_aborted_lifecycle() + -> anyhow::Result<()> { + let (mut session, turn_context) = crate::session::tests::make_session_and_context().await; + let records = Arc::new(std::sync::Mutex::new(Vec::new())); + let mut builder = + codex_extension_api::ExtensionRegistryBuilder::::new(); + builder.tool_lifecycle_contributor(Arc::new(FinishRecorder { + records: Arc::clone(&records), + })); + session.services.extensions = Arc::new(builder.build()); + + let session = Arc::new(session); + let turn_context = Arc::new(turn_context); + let tool_name = codex_tools::ToolName::plain("cleanup_tool"); + let (started_tx, started_rx) = oneshot::channel(); + let (cleanup_started_tx, cleanup_started_rx) = oneshot::channel(); + let allow_cleanup = Arc::new(Notify::new()); + let handler = Arc::new(CancellationCleanupHandler { + tool_name: tool_name.clone(), + started: std::sync::Mutex::new(Some(started_tx)), + cleanup_started: std::sync::Mutex::new(Some(cleanup_started_tx)), + allow_cleanup: Arc::clone(&allow_cleanup), + }) as Arc; + let router = Arc::new(ToolRouter::from_parts( + ToolRegistry::from_tools([handler]), + Vec::new(), + )); + let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new())); + let runtime = ToolCallRuntime::new(router, session, turn_context, tracker); + let cancellation_token = CancellationToken::new(); + let call = ToolCall { + tool_name, + call_id: "call-1".to_string(), + payload: ToolPayload::Function { + arguments: "{}".to_string(), + }, + }; + + let response_task = + tokio::spawn(runtime.handle_tool_call(call, cancellation_token.clone())); + started_rx.await.expect("handler should start"); + cancellation_token.cancel(); + cleanup_started_rx + .await + .expect("handler should start cleanup"); + tokio::time::sleep(Duration::from_millis(10)).await; + allow_cleanup.notify_one(); + + let response = tokio::time::timeout(Duration::from_secs(1), response_task) + .await + .expect("timed out waiting for tool response") + .expect("tool response task should join")?; + let ResponseInputItem::FunctionCallOutput { output, .. } = response else { + anyhow::bail!("cancelled tool should return function output"); + }; + let FunctionCallOutputBody::Text(text) = output.body else { + anyhow::bail!("cancelled tool output should be text"); + }; + assert!(text.contains("aborted by user")); + + let actual = records + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .drain(..) + .collect::>(); + assert_eq!(vec![ToolCallOutcome::Aborted], actual); + + Ok(()) + } } diff --git a/codex-rs/core/src/tools/registry.rs b/codex-rs/core/src/tools/registry.rs index a08d5c7834..f0fdf424a9 100644 --- a/codex-rs/core/src/tools/registry.rs +++ b/codex-rs/core/src/tools/registry.rs @@ -57,6 +57,12 @@ pub(crate) trait CoreToolRuntime: ToolExecutor { ) } + /// Whether cancellation should let the handler finish teardown before the + /// host returns an aborted tool response. + fn waits_for_runtime_cancellation(&self) -> bool { + false + } + fn telemetry_tags<'a>( &'a self, _invocation: &'a ToolInvocation, @@ -284,6 +290,10 @@ impl CoreToolRuntime for ExposureOverride { self.handler.matches_kind(payload) } + fn waits_for_runtime_cancellation(&self) -> bool { + self.handler.waits_for_runtime_cancellation() + } + fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option { self.handler.pre_tool_use_payload(invocation) } @@ -381,6 +391,11 @@ impl ToolRegistry { Some(tool.supports_parallel_tool_calls()) } + pub(crate) fn waits_for_runtime_cancellation(&self, name: &ToolName) -> Option { + let tool = self.tool(name)?; + Some(tool.waits_for_runtime_cancellation()) + } + #[allow(dead_code)] pub(crate) async fn dispatch_any( &self, @@ -497,10 +512,12 @@ impl ToolRegistry { PreToolUseHookResult::Blocked(message) => { let err = FunctionCallError::RespondToModel(message); dispatch_trace.record_failed(&err); - if let Some(terminal_outcome_reached) = &terminal_outcome_reached { - terminal_outcome_reached.store(true, Ordering::Release); - } - notify_tool_finish(&invocation, ToolCallOutcome::Blocked).await; + notify_tool_finish_if_unclaimed( + &invocation, + terminal_outcome_reached.as_deref(), + ToolCallOutcome::Blocked, + ) + .await; return Err(err); } PreToolUseHookResult::Continue { @@ -511,11 +528,9 @@ impl ToolRegistry { } Err(err) => { dispatch_trace.record_failed(&err); - if let Some(terminal_outcome_reached) = &terminal_outcome_reached { - terminal_outcome_reached.store(true, Ordering::Release); - } - notify_tool_finish( + notify_tool_finish_if_unclaimed( &invocation, + terminal_outcome_reached.as_deref(), ToolCallOutcome::Failed { handler_executed: false, }, @@ -638,18 +653,21 @@ impl ToolRegistry { handler_executed: true, }, }; - if let Some(terminal_outcome_reached) = &terminal_outcome_reached { - terminal_outcome_reached.store(true, Ordering::Release); - } - notify_tool_finish(&invocation, lifecycle_outcome).await; + let finished = notify_tool_finish_if_unclaimed( + &invocation, + terminal_outcome_reached.as_deref(), + lifecycle_outcome, + ) + .await; - if let Err(err) = invocation - .session - .goal_runtime_apply(GoalRuntimeEvent::ToolCompleted { - turn_context: invocation.turn.as_ref(), - tool_name: tool_name.name.as_str(), - }) - .await + if finished + && let Err(err) = invocation + .session + .goal_runtime_apply(GoalRuntimeEvent::ToolCompleted { + turn_context: invocation.turn.as_ref(), + tool_name: tool_name.name.as_str(), + }) + .await { warn!("failed to account thread goal progress after tool call: {err}"); } @@ -676,6 +694,19 @@ impl ToolRegistry { } } +async fn notify_tool_finish_if_unclaimed( + invocation: &ToolInvocation, + terminal_outcome_reached: Option<&AtomicBool>, + outcome: ToolCallOutcome, +) -> bool { + if terminal_outcome_reached.is_some_and(|reached| reached.swap(true, Ordering::AcqRel)) { + return false; + } + + notify_tool_finish(invocation, outcome).await; + true +} + async fn handle_any_tool( tool: &dyn CoreToolRuntime, invocation: ToolInvocation, diff --git a/codex-rs/core/src/tools/router.rs b/codex-rs/core/src/tools/router.rs index cfd4776890..a095499ea0 100644 --- a/codex-rs/core/src/tools/router.rs +++ b/codex-rs/core/src/tools/router.rs @@ -86,6 +86,12 @@ impl ToolRouter { .unwrap_or(false) } + pub fn tool_waits_for_runtime_cancellation(&self, call: &ToolCall) -> bool { + self.registry + .waits_for_runtime_cancellation(&call.tool_name) + .unwrap_or(false) + } + #[instrument(level = "trace", skip_all, err)] pub fn build_tool_call(item: ResponseItem) -> Result, FunctionCallError> { match item { diff --git a/codex-rs/core/src/tools/runtimes/shell.rs b/codex-rs/core/src/tools/runtimes/shell.rs index 6251267773..df63f6cd47 100644 --- a/codex-rs/core/src/tools/runtimes/shell.rs +++ b/codex-rs/core/src/tools/runtimes/shell.rs @@ -44,6 +44,7 @@ use codex_shell_command::powershell::prefix_powershell_script_with_utf8; use codex_utils_absolute_path::AbsolutePathBuf; use futures::future::BoxFuture; use std::collections::HashMap; +use tokio_util::sync::CancellationToken; #[derive(Clone, Debug)] pub struct ShellRequest { @@ -52,6 +53,7 @@ pub struct ShellRequest { pub hook_command: String, pub cwd: AbsolutePathBuf, pub timeout_ms: Option, + pub cancellation_token: CancellationToken, pub env: HashMap, pub explicit_env_overrides: HashMap, pub network: Option, @@ -265,6 +267,7 @@ impl ToolRuntime for ShellRuntime { let command = build_sandbox_command(&command, &req.cwd, &env, req.additional_permissions.clone())?; let mut expiration: crate::exec::ExecExpiration = req.timeout_ms.into(); + expiration = expiration.with_cancellation(req.cancellation_token.clone()); if let Some(cancellation) = attempt.network_denial_cancellation_token.clone() { expiration = expiration.with_cancellation(cancellation); } diff --git a/codex-rs/utils/pty/src/process_group.rs b/codex-rs/utils/pty/src/process_group.rs index f8056acc66..22a934fab7 100644 --- a/codex-rs/utils/pty/src/process_group.rs +++ b/codex-rs/utils/pty/src/process_group.rs @@ -94,7 +94,7 @@ pub fn kill_process_group_by_pid(pid: u32) -> io::Result<()> { let pgid = unsafe { libc::getpgid(pid) }; if pgid == -1 { let err = io::Error::last_os_error(); - if err.kind() != ErrorKind::NotFound { + if err.kind() != ErrorKind::NotFound && err.raw_os_error() != Some(libc::ESRCH) { return Err(err); } return Ok(()); @@ -103,7 +103,7 @@ pub fn kill_process_group_by_pid(pid: u32) -> io::Result<()> { let result = unsafe { libc::killpg(pgid, libc::SIGKILL) }; if result == -1 { let err = io::Error::last_os_error(); - if err.kind() != ErrorKind::NotFound { + if err.kind() != ErrorKind::NotFound && err.raw_os_error() != Some(libc::ESRCH) { return Err(err); } } @@ -129,7 +129,7 @@ pub fn terminate_process_group(process_group_id: u32) -> io::Result { let result = unsafe { libc::killpg(pgid, libc::SIGTERM) }; if result == -1 { let err = io::Error::last_os_error(); - if err.kind() == ErrorKind::NotFound { + if err.kind() == ErrorKind::NotFound || err.raw_os_error() == Some(libc::ESRCH) { return Ok(false); } return Err(err); @@ -153,7 +153,7 @@ pub fn kill_process_group(process_group_id: u32) -> io::Result<()> { let result = unsafe { libc::killpg(pgid, libc::SIGKILL) }; if result == -1 { let err = io::Error::last_os_error(); - if err.kind() != ErrorKind::NotFound { + if err.kind() != ErrorKind::NotFound && err.raw_os_error() != Some(libc::ESRCH) { return Err(err); } } From 7a26497836be29ec77fb8007347b33cad952b497 Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Wed, 27 May 2026 17:14:55 -0300 Subject: [PATCH 35/87] feat(tui): add OSC 8 web links to rich content (#24472) ## Why Wrapped URLs in rich TUI output, especially URLs rendered inside Markdown tables, are split across terminal rows. In terminals that support OSC 8 hyperlinks, treating each visible fragment as part of the complete destination enables reliable open-link and copy-link actions even after table layout wraps the URL. This addresses the semantic-link portion of #12200 and the behavior described in https://github.com/openai/codex/issues/12200#issuecomment-4535452980. It does not change ordinary drag-selection across bordered table rows. ## What Changed - Added shared TUI OSC 8 support that validates `http://` and `https://` destinations, sanitizes terminal payloads, and applies metadata separately from visible line width/layout. - Added semantic web-link annotations to assistant and proposed-plan Markdown, including explicit web links and bare web URLs in prose and table cells while excluding code and non-web Markdown destinations. - Preserved complete URL targets through table wrapping, narrow pipe fallback, streaming, transcript overlay rendering, history insertion, and resize replay. - Routed intentional Codex-owned links in notices, status/setup/app-link, feedback, onboarding, MCP/plugin help, memories, and update surfaces through the shared hyperlink handling. ## How to Test 1. Run Codex in a terminal with OSC 8 link support, such as Ghostty, and request an assistant response containing a Markdown table whose last column contains a long `https://` URL. 2. Make the terminal narrow enough for the URL to wrap across multiple bordered table rows. 3. Use the terminal's open-link or copy-link action on more than one wrapped URL fragment and confirm each fragment resolves to the complete original URL. 4. Resize the terminal after the table is rendered and repeat the link action to confirm the destination survives scrollback replay. 5. Open the transcript overlay while rich output is present and confirm web links remain interactive there. 6. As a regression check, render inline/fenced code containing URL text and a Markdown link such as `[https://example.com](mailto:support@example.com)`; confirm these do not acquire a web OSC 8 destination. Targeted automated coverage exercised Markdown links and exclusions, wrapped and pipe-fallback tables, streaming/transcript overlay propagation, status-link truncation, and rendered word-wrapping cell alignment. `just test -p codex-tui` was also run; it passed the hyperlink coverage and reproduced two unrelated existing guardian feature-flag test failures. --- codex-rs/tui/src/app.rs | 4 +- codex-rs/tui/src/app/resize_reflow.rs | 45 +- codex-rs/tui/src/app/tests.rs | 11 +- codex-rs/tui/src/app_backtrack.rs | 14 +- codex-rs/tui/src/bottom_pane/app_link_view.rs | 11 +- codex-rs/tui/src/bottom_pane/feedback_view.rs | 4 +- .../src/bottom_pane/memories_settings_view.rs | 1 + codex-rs/tui/src/chatwidget.rs | 23 +- .../chatwidget/tests/popups_and_settings.rs | 2 +- codex-rs/tui/src/history_cell/base.rs | 61 ++ codex-rs/tui/src/history_cell/mcp.rs | 9 +- codex-rs/tui/src/history_cell/messages.rs | 67 +- codex-rs/tui/src/history_cell/mod.rs | 39 +- codex-rs/tui/src/history_cell/notices.rs | 16 + codex-rs/tui/src/history_cell/plans.rs | 57 +- codex-rs/tui/src/history_cell/tests.rs | 41 ++ codex-rs/tui/src/insert_history.rs | 71 +- codex-rs/tui/src/lib.rs | 1 + codex-rs/tui/src/markdown.rs | 19 +- codex-rs/tui/src/markdown_render.rs | 421 +++++++++--- codex-rs/tui/src/onboarding/auth.rs | 60 +- codex-rs/tui/src/pager_overlay.rs | 94 ++- codex-rs/tui/src/status/card.rs | 39 +- codex-rs/tui/src/status/tests.rs | 19 + codex-rs/tui/src/streaming/controller.rs | 150 +++-- codex-rs/tui/src/streaming/mod.rs | 16 +- codex-rs/tui/src/terminal_hyperlinks.rs | 627 ++++++++++++++++++ codex-rs/tui/src/tui.rs | 17 +- codex-rs/tui/src/update_prompt.rs | 7 +- 29 files changed, 1659 insertions(+), 287 deletions(-) create mode 100644 codex-rs/tui/src/terminal_hyperlinks.rs diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 8c89c9153b..bd0cc45548 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -473,7 +473,7 @@ struct SessionSummary { #[derive(Debug, Default)] struct InitialHistoryReplayBuffer { - retained_lines: VecDeque>, + retained_lines: VecDeque, render_from_transcript_tail: bool, } @@ -498,7 +498,7 @@ pub(crate) struct App { // Pager overlay state (Transcript or Static like Diff) pub(crate) overlay: Option, - pub(crate) deferred_history_lines: Vec>, + pub(crate) deferred_history_lines: Vec, has_emitted_history_lines: bool, transcript_reflow: TranscriptReflowState, initial_history_replay_buffer: Option, diff --git a/codex-rs/tui/src/app/resize_reflow.rs b/codex-rs/tui/src/app/resize_reflow.rs index 6ef1721266..34e25697e2 100644 --- a/codex-rs/tui/src/app/resize_reflow.rs +++ b/codex-rs/tui/src/app/resize_reflow.rs @@ -27,11 +27,12 @@ use super::InitialHistoryReplayBuffer; use crate::history_cell; use crate::history_cell::HistoryCell; use crate::insert_history::HistoryLineWrapPolicy; +use crate::terminal_hyperlinks::HyperlinkLine; use crate::transcript_reflow::TRANSCRIPT_REFLOW_DEBOUNCE; use crate::tui; struct ReflowCellDisplay { - lines: Vec>, + lines: Vec, is_stream_continuation: bool, } @@ -41,7 +42,7 @@ struct ReflowCellDisplay { /// already-wrapped rows. Callers should keep treating `transcript_cells` as the source of truth; the /// rows here are a transient render product for a single terminal width. pub(super) struct ReflowRenderResult { - pub(super) lines: Vec>, + pub(super) lines: Vec, } pub(super) fn trailing_run_start(transcript_cells: &[Arc]) -> usize { @@ -75,12 +76,12 @@ impl App { &mut self, cell: &dyn HistoryCell, width: u16, - ) -> Vec> { + ) -> Vec { let mut display = - cell.display_lines_for_mode(width, self.chat_widget.history_render_mode()); + cell.display_hyperlink_lines_for_mode(width, self.chat_widget.history_render_mode()); if !display.is_empty() && !cell.is_stream_continuation() { if self.has_emitted_history_lines { - display.insert(0, Line::from("")); + display.insert(/*index*/ 0, HyperlinkLine::new(Line::from(""))); } else { self.has_emitted_history_lines = true; } @@ -101,7 +102,10 @@ impl App { if self.overlay.is_some() { self.deferred_history_lines.extend(display); } else { - tui.insert_history_lines_with_wrap_policy(display, self.history_line_wrap_policy()); + tui.insert_history_hyperlink_lines_with_wrap_policy( + display, + self.history_line_wrap_policy(), + ); } } @@ -153,14 +157,20 @@ impl App { let width = tui.terminal.last_known_screen_size.width; let reflowed_lines = self.render_transcript_lines_for_reflow(width).lines; if !reflowed_lines.is_empty() { - tui.insert_history_lines(reflowed_lines); + tui.insert_history_hyperlink_lines_with_wrap_policy( + reflowed_lines, + self.history_line_wrap_policy(), + ); } } return; } let retained_lines = buffer.retained_lines.into_iter().collect::>(); - tui.insert_history_lines_with_wrap_policy(retained_lines, self.history_line_wrap_policy()); + tui.insert_history_hyperlink_lines_with_wrap_policy( + retained_lines, + self.history_line_wrap_policy(), + ); } pub(super) fn insert_history_cell_lines_with_initial_replay_buffer( @@ -190,7 +200,10 @@ impl App { } else if self.overlay.is_some() { self.deferred_history_lines.extend(display); } else { - tui.insert_history_lines_with_wrap_policy(display, self.history_line_wrap_policy()); + tui.insert_history_hyperlink_lines_with_wrap_policy( + display, + self.history_line_wrap_policy(), + ); } } } @@ -210,7 +223,7 @@ impl App { /// here would make copy, transcript overlay, and future replay paths disagree about history. pub(super) fn buffer_initial_history_replay_display_lines( buffer: &mut InitialHistoryReplayBuffer, - display: Vec>, + display: Vec, max_rows: usize, ) { buffer.retained_lines.extend(display); @@ -437,7 +450,7 @@ impl App { self.deferred_history_lines.clear(); if !reflowed_lines.is_empty() { - tui.insert_history_lines_with_wrap_policy( + tui.insert_history_hyperlink_lines_with_wrap_policy( reflowed_lines, self.history_line_wrap_policy(), ); @@ -462,7 +475,8 @@ impl App { while start > 0 { start -= 1; let cell = self.transcript_cells[start].clone(); - let lines = cell.display_lines_for_mode(width, self.chat_widget.history_render_mode()); + let lines = cell + .display_hyperlink_lines_for_mode(width, self.chat_widget.history_render_mode()); rendered_rows += lines.len(); cell_displays.push_front(ReflowCellDisplay { lines, @@ -482,7 +496,10 @@ impl App { start -= 1; let cell = self.transcript_cells[start].clone(); cell_displays.push_front(ReflowCellDisplay { - lines: cell.display_lines_for_mode(width, self.chat_widget.history_render_mode()), + lines: cell.display_hyperlink_lines_for_mode( + width, + self.chat_widget.history_render_mode(), + ), is_stream_continuation: cell.is_stream_continuation(), }); } @@ -492,7 +509,7 @@ impl App { for display in cell_displays { if !display.lines.is_empty() && !display.is_stream_continuation { if has_emitted_history_lines { - reflowed_lines.push(Line::from("")); + reflowed_lines.push(HyperlinkLine::new(Line::from(""))); } else { has_emitted_history_lines = true; } diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index c2138ba31a..f4c8a4df79 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -3906,8 +3906,9 @@ fn plain_line_cell(text: impl Into) -> Arc { Arc::new(PlainHistoryCell::new(vec![Line::from(text.into())])) as Arc } -fn rendered_line_text(line: &Line<'static>) -> String { - line.spans +fn rendered_line_text(line: &crate::terminal_hyperlinks::HyperlinkLine) -> String { + line.line + .spans .iter() .map(|span| span.content.as_ref()) .collect() @@ -4019,7 +4020,7 @@ async fn initial_replay_buffer_keeps_recent_rows_when_row_cap_present() { app.initial_history_replay_buffer .as_mut() .expect("initial replay buffer active"), - vec![Line::from(format!("line {index}"))], + vec![Line::from(format!("line {index}")).into()], /*max_rows*/ 3, ); } @@ -4925,7 +4926,7 @@ async fn queued_rollback_syncs_overlay_and_clears_deferred_history() { app.transcript_cells.clone(), app.keymap.pager.clone(), )); - app.deferred_history_lines = vec![Line::from("stale buffered line")]; + app.deferred_history_lines = vec![Line::from("stale buffered line").into()]; app.backtrack.overlay_preview_active = true; app.backtrack.nth_user_message = 1; @@ -5459,7 +5460,7 @@ async fn clear_only_ui_reset_preserves_chat_session_state() { app.transcript_cells.clone(), crate::keymap::RuntimeKeymap::defaults().pager, )); - app.deferred_history_lines = vec![Line::from("stale buffered line")]; + app.deferred_history_lines = vec![Line::from("stale buffered line").into()]; app.has_emitted_history_lines = true; app.backtrack.primed = true; app.backtrack.overlay_preview_active = true; diff --git a/codex-rs/tui/src/app_backtrack.rs b/codex-rs/tui/src/app_backtrack.rs index 625f8d0841..075a4a5ae2 100644 --- a/codex-rs/tui/src/app_backtrack.rs +++ b/codex-rs/tui/src/app_backtrack.rs @@ -245,7 +245,10 @@ impl App { let was_backtrack = self.backtrack.overlay_preview_active; if !self.deferred_history_lines.is_empty() { let lines = std::mem::take(&mut self.deferred_history_lines); - tui.insert_history_lines_with_wrap_policy(lines, self.history_line_wrap_policy()); + tui.insert_history_hyperlink_lines_with_wrap_policy( + lines, + self.history_line_wrap_policy(), + ); } self.overlay = None; self.backtrack.overlay_preview_active = false; @@ -263,8 +266,11 @@ impl App { .chat_widget .history_wrap_width(tui.terminal.last_known_screen_size.width); for cell in &self.transcript_cells { - tui.insert_history_lines_with_wrap_policy( - cell.display_lines_for_mode(width, self.chat_widget.history_render_mode()), + tui.insert_history_hyperlink_lines_with_wrap_policy( + cell.display_hyperlink_lines_for_mode( + width, + self.chat_widget.history_render_mode(), + ), self.history_line_wrap_policy(), ); } @@ -399,7 +405,7 @@ impl App { tui.draw(u16::MAX, |frame| { let width = frame.area().width.max(1); t.sync_live_tail(width, active_key, |w| { - chat_widget.active_cell_transcript_lines(w) + chat_widget.active_cell_transcript_hyperlink_lines(w) }); t.render(frame.area(), frame.buffer); })?; diff --git a/codex-rs/tui/src/bottom_pane/app_link_view.rs b/codex-rs/tui/src/bottom_pane/app_link_view.rs index 7fe180a16f..bbd7f355c3 100644 --- a/codex-rs/tui/src/bottom_pane/app_link_view.rs +++ b/codex-rs/tui/src/bottom_pane/app_link_view.rs @@ -814,6 +814,7 @@ impl crate::render::renderable::Renderable for AppLinkView { Paragraph::new(lines) .wrap(Wrap { trim: false }) .render(inner, buf); + crate::terminal_hyperlinks::mark_url_hyperlink(buf, inner, &self.url); if actions_area.height > 0 { let actions_area = Rect { @@ -1005,7 +1006,10 @@ mod tests { if symbol.is_empty() { ' ' } else { - symbol.chars().next().unwrap_or(' ') + crate::terminal_hyperlinks::strip_osc8(symbol) + .chars() + .next() + .unwrap_or(' ') } }) .collect::() @@ -1325,7 +1329,10 @@ mod tests { if symbol.is_empty() { ' ' } else { - symbol.chars().next().unwrap_or(' ') + crate::terminal_hyperlinks::strip_osc8(symbol) + .chars() + .next() + .unwrap_or(' ') } }) .collect::() diff --git a/codex-rs/tui/src/bottom_pane/feedback_view.rs b/codex-rs/tui/src/bottom_pane/feedback_view.rs index 2770598fc4..ce380a867d 100644 --- a/codex-rs/tui/src/bottom_pane/feedback_view.rs +++ b/codex-rs/tui/src/bottom_pane/feedback_view.rs @@ -313,7 +313,7 @@ pub(crate) fn feedback_success_cell( include_logs: bool, thread_id: &str, feedback_audience: FeedbackAudience, -) -> history_cell::PlainHistoryCell { +) -> history_cell::WebHyperlinkHistoryCell { let prefix = if include_logs { "• Feedback uploaded." } else { @@ -359,7 +359,7 @@ pub(crate) fn feedback_success_cell( ]); } } - history_cell::PlainHistoryCell::new(lines) + history_cell::WebHyperlinkHistoryCell::new(lines) } fn issue_url_for_category( diff --git a/codex-rs/tui/src/bottom_pane/memories_settings_view.rs b/codex-rs/tui/src/bottom_pane/memories_settings_view.rs index 06e9b31eb8..793596b468 100644 --- a/codex-rs/tui/src/bottom_pane/memories_settings_view.rs +++ b/codex-rs/tui/src/bottom_pane/memories_settings_view.rs @@ -421,6 +421,7 @@ impl Renderable for MemoriesSettingsView { } if self.reset_confirmation.is_none() { self.docs_link.clone().render(docs_area, buf); + crate::terminal_hyperlinks::mark_url_hyperlink(buf, docs_area, MEMORIES_DOC_URL); } let hint_area = Rect { diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 16ac980f35..d3e5cf68ef 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -10,7 +10,8 @@ //! visible immediately. //! //! The transcript overlay is kept in sync by `App::overlay_forward_event`, which syncs a live tail -//! during draws using `active_cell_transcript_key()` and `active_cell_transcript_lines()`. The +//! during draws using `active_cell_transcript_key()` and +//! `active_cell_transcript_hyperlink_lines()`. The //! cache key is designed to change when the active cell mutates in place or when its transcript //! output is time-dependent so the overlay can refresh its cached tail without rebuilding it on //! every draw. @@ -77,6 +78,7 @@ use crate::status::StatusHistoryHandle; use crate::status::format_directory_display; use crate::status::format_tokens_compact; use crate::status::rate_limit_snapshot_display_for_limit; +use crate::terminal_hyperlinks::HyperlinkLine; use crate::terminal_title::SetTerminalTitleResult; use crate::terminal_title::clear_terminal_title; use crate::terminal_title::set_terminal_title; @@ -1824,28 +1826,37 @@ impl ChatWidget { }) } - /// Returns the active cell's transcript lines for a given terminal width. + /// Returns the active cell's annotated transcript lines for a given terminal width. /// /// This is a convenience for the transcript overlay live-tail path, and it intentionally /// filters out empty results so the overlay can treat "nothing to render" as "no tail". Callers /// should pass the same width the overlay uses; using a different width will cause wrapping /// mismatches between the main viewport and the transcript overlay. - pub(crate) fn active_cell_transcript_lines(&self, width: u16) -> Option>> { + pub(crate) fn active_cell_transcript_hyperlink_lines( + &self, + width: u16, + ) -> Option> { let mut lines = Vec::new(); if let Some(cell) = self.transcript.active_cell.as_ref() { - lines.extend(cell.transcript_lines(width)); + lines.extend(cell.transcript_hyperlink_lines(width)); } if let Some(hook_cell) = self.active_hook_cell.as_ref() { // Compute hook lines first so hidden hooks do not add a separator. - let hook_lines = hook_cell.transcript_lines(width); + let hook_lines = hook_cell.transcript_hyperlink_lines(width); if !hook_lines.is_empty() && !lines.is_empty() { - lines.push("".into()); + lines.push(HyperlinkLine::from("")); } lines.extend(hook_lines); } (!lines.is_empty()).then_some(lines) } + #[cfg(test)] + pub(crate) fn active_cell_transcript_lines(&self, width: u16) -> Option>> { + self.active_cell_transcript_hyperlink_lines(width) + .map(crate::terminal_hyperlinks::visible_lines) + } + /// Return a reference to the widget's current config (includes any /// runtime overrides applied via TUI, e.g., model or approval policy). pub(crate) fn config_ref(&self) -> &Config { diff --git a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs index 55aadefb6d..ade74b8908 100644 --- a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs +++ b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs @@ -2200,7 +2200,7 @@ async fn memories_settings_popup_snapshot() { chat.open_memories_popup(); - let popup = render_bottom_popup(&chat, /*width*/ 80); + let popup = strip_osc8_for_snapshot(&render_bottom_popup(&chat, /*width*/ 80)); assert_chatwidget_snapshot!("memories_settings_popup", popup); } diff --git a/codex-rs/tui/src/history_cell/base.rs b/codex-rs/tui/src/history_cell/base.rs index e3b33aa9d1..e9ade84945 100644 --- a/codex-rs/tui/src/history_cell/base.rs +++ b/codex-rs/tui/src/history_cell/base.rs @@ -22,6 +22,35 @@ impl HistoryCell for PlainHistoryCell { plain_lines(self.lines.clone()) } } + +#[derive(Debug)] +pub(crate) struct WebHyperlinkHistoryCell { + lines: Vec>, +} + +impl WebHyperlinkHistoryCell { + pub(crate) fn new(lines: Vec>) -> Self { + Self { lines } + } +} + +impl HistoryCell for WebHyperlinkHistoryCell { + fn display_lines(&self, _width: u16) -> Vec> { + self.lines.clone() + } + + fn display_hyperlink_lines(&self, _width: u16) -> Vec { + crate::terminal_hyperlinks::annotate_web_urls(self.lines.clone()) + } + + fn transcript_hyperlink_lines(&self, width: u16) -> Vec { + self.display_hyperlink_lines(width) + } + + fn raw_lines(&self) -> Vec> { + plain_lines(self.lines.clone()) + } +} #[derive(Debug)] pub(crate) struct PrefixedWrappedHistoryCell { text: Text<'static>, @@ -86,6 +115,38 @@ impl HistoryCell for CompositeHistoryCell { out } + fn display_hyperlink_lines(&self, width: u16) -> Vec { + let mut out = Vec::new(); + let mut first = true; + for part in &self.parts { + let mut lines = part.display_hyperlink_lines(width); + if !lines.is_empty() { + if !first { + out.push(HyperlinkLine::from("")); + } + out.append(&mut lines); + first = false; + } + } + out + } + + fn transcript_hyperlink_lines(&self, width: u16) -> Vec { + let mut out = Vec::new(); + let mut first = true; + for part in &self.parts { + let mut lines = part.transcript_hyperlink_lines(width); + if !lines.is_empty() { + if !first { + out.push(HyperlinkLine::from("")); + } + out.append(&mut lines); + first = false; + } + } + out + } + fn raw_lines(&self) -> Vec> { let mut out: Vec> = Vec::new(); let mut first = true; diff --git a/codex-rs/tui/src/history_cell/mcp.rs b/codex-rs/tui/src/history_cell/mcp.rs index 7b1e8eebc4..599a44f092 100644 --- a/codex-rs/tui/src/history_cell/mcp.rs +++ b/codex-rs/tui/src/history_cell/mcp.rs @@ -325,14 +325,17 @@ pub(crate) fn empty_mcp_output() -> PlainHistoryCell { " • No MCP servers configured.".italic().into(), Line::from(vec![ " See the ".into(), - "\u{1b}]8;;https://developers.openai.com/codex/mcp\u{7}MCP docs\u{1b}]8;;\u{7}" - .underlined(), + crate::terminal_hyperlinks::osc8_hyperlink( + "https://developers.openai.com/codex/mcp", + "MCP docs", + ) + .underlined(), " to configure them.".into(), ]) .style(Style::default().add_modifier(Modifier::DIM)), ]; - PlainHistoryCell { lines } + PlainHistoryCell::new(lines) } #[cfg(test)] diff --git a/codex-rs/tui/src/history_cell/messages.rs b/codex-rs/tui/src/history_cell/messages.rs index 19a6ec2903..ed899f3b2e 100644 --- a/codex-rs/tui/src/history_cell/messages.rs +++ b/codex-rs/tui/src/history_cell/messages.rs @@ -268,12 +268,20 @@ impl HistoryCell for ReasoningSummaryCell { #[derive(Debug)] pub(crate) struct AgentMessageCell { - lines: Vec>, + lines: Vec, is_first_line: bool, } impl AgentMessageCell { + #[cfg(test)] pub(crate) fn new(lines: Vec>, is_first_line: bool) -> Self { + Self { + lines: plain_hyperlink_lines(lines), + is_first_line, + } + } + + pub(crate) fn new_hyperlink_lines(lines: Vec, is_first_line: bool) -> Self { Self { lines, is_first_line, @@ -283,6 +291,10 @@ impl AgentMessageCell { impl HistoryCell for AgentMessageCell { fn display_lines(&self, width: u16) -> Vec> { + visible_lines(self.display_hyperlink_lines(width)) + } + + fn display_hyperlink_lines(&self, width: u16) -> Vec { let mut wrapped = Vec::new(); for (index, line) in self.lines.iter().enumerate() { let initial_indent = if index == 0 && self.is_first_line { @@ -293,20 +305,23 @@ impl HistoryCell for AgentMessageCell { let mut subsequent_indent = Line::from(" "); subsequent_indent .spans - .extend(crate::insert_history::leading_whitespace_prefix(line).spans); - let line_wrapped = adaptive_wrap_line( - line, + .extend(crate::insert_history::leading_whitespace_prefix(&line.line).spans); + wrapped.extend(crate::terminal_hyperlinks::adaptive_wrap_hyperlink_lines( + std::slice::from_ref(line), RtOptions::new(width as usize) .initial_indent(initial_indent) .subsequent_indent(subsequent_indent), - ); - push_owned_lines(&line_wrapped, &mut wrapped); + )); } wrapped } + fn transcript_hyperlink_lines(&self, width: u16) -> Vec { + self.display_hyperlink_lines(width) + } + fn raw_lines(&self) -> Vec> { - plain_lines(self.lines.clone()) + plain_lines(visible_lines(self.lines.clone())) } fn is_stream_continuation(&self) -> bool { @@ -347,22 +362,32 @@ impl AgentMarkdownCell { impl HistoryCell for AgentMarkdownCell { fn display_lines(&self, width: u16) -> Vec> { + visible_lines(self.display_hyperlink_lines(width)) + } + + fn display_hyperlink_lines(&self, width: u16) -> Vec { let Some(wrap_width) = crate::width::usable_content_width_u16(width, /*reserved_cols*/ 2) else { - return prefix_lines(vec![Line::default()], "• ".dim(), " ".into()); + return prefix_hyperlink_lines( + vec![HyperlinkLine::new(Line::default())], + "• ".dim(), + " ".into(), + ); }; - let mut lines: Vec> = Vec::new(); // Re-render markdown from source at the current width. Reserve 2 columns for the "• " / // " " prefix prepended below. - crate::markdown::append_markdown_agent_with_cwd( + let lines = crate::markdown::render_markdown_agent_with_links_and_cwd( &self.markdown_source, Some(wrap_width), Some(self.cwd.as_path()), - &mut lines, ); - prefix_lines(lines, "• ".dim(), " ".into()) + prefix_hyperlink_lines(lines, "• ".dim(), " ".into()) + } + + fn transcript_hyperlink_lines(&self, width: u16) -> Vec { + self.display_hyperlink_lines(width) } fn raw_lines(&self) -> Vec> { @@ -377,12 +402,12 @@ impl HistoryCell for AgentMarkdownCell { /// every delta and cleared when the stream finalizes. #[derive(Debug)] pub(crate) struct StreamingAgentTailCell { - lines: Vec>, + lines: Vec, is_first_line: bool, } impl StreamingAgentTailCell { - pub(crate) fn new(lines: Vec>, is_first_line: bool) -> Self { + pub(crate) fn new(lines: Vec, is_first_line: bool) -> Self { Self { lines, is_first_line, @@ -391,10 +416,14 @@ impl StreamingAgentTailCell { } impl HistoryCell for StreamingAgentTailCell { - fn display_lines(&self, _width: u16) -> Vec> { + fn display_lines(&self, width: u16) -> Vec> { + visible_lines(self.display_hyperlink_lines(width)) + } + + fn display_hyperlink_lines(&self, _width: u16) -> Vec { // Tail lines are already rendered at the controller's current stream width. // Re-wrapping them here can split table borders and produce malformed in-flight rows. - prefix_lines( + prefix_hyperlink_lines( self.lines.clone(), if self.is_first_line { "• ".dim() @@ -405,8 +434,12 @@ impl HistoryCell for StreamingAgentTailCell { ) } + fn transcript_hyperlink_lines(&self, width: u16) -> Vec { + self.display_hyperlink_lines(width) + } + fn raw_lines(&self) -> Vec> { - plain_lines(self.display_lines(u16::MAX)) + plain_lines(self.display_lines(/*width*/ u16::MAX)) } fn is_stream_continuation(&self) -> bool { diff --git a/codex-rs/tui/src/history_cell/mod.rs b/codex-rs/tui/src/history_cell/mod.rs index 12788a59f0..9cef3e4fa5 100644 --- a/codex-rs/tui/src/history_cell/mod.rs +++ b/codex-rs/tui/src/history_cell/mod.rs @@ -22,7 +22,6 @@ use crate::exec_command::strip_bash_lc_and_escape; use crate::legacy_core::config::Config; use crate::live_wrap::take_prefix_by_width; use crate::markdown::append_markdown; -use crate::markdown::append_markdown_agent_with_cwd; use crate::motion::MotionMode; use crate::motion::ReducedMotionIndicator; use crate::motion::activity_indicator; @@ -33,6 +32,11 @@ use crate::render::renderable::Renderable; use crate::session_state::ThreadSessionState; use crate::style::proposed_plan_style; use crate::style::user_message_style; +use crate::terminal_hyperlinks::HyperlinkLine; +use crate::terminal_hyperlinks::mark_buffer_hyperlinks; +use crate::terminal_hyperlinks::plain_hyperlink_lines; +use crate::terminal_hyperlinks::prefix_hyperlink_lines; +use crate::terminal_hyperlinks::visible_lines; #[cfg(test)] use crate::test_support::PathBufExt; #[cfg(test)] @@ -189,13 +193,29 @@ pub(crate) trait HistoryCell: std::fmt::Debug + Send + Sync + Any { /// Returns copy-friendly plain logical lines for raw scrollback mode. fn raw_lines(&self) -> Vec>; + /// Returns rich visible lines plus terminal hyperlink metadata. + fn display_hyperlink_lines(&self, width: u16) -> Vec { + plain_hyperlink_lines(self.display_lines(width)) + } + fn display_lines_for_mode(&self, width: u16, mode: HistoryRenderMode) -> Vec> { match mode { - HistoryRenderMode::Rich => self.display_lines(width), + HistoryRenderMode::Rich => visible_lines(self.display_hyperlink_lines(width)), HistoryRenderMode::Raw => self.raw_lines(), } } + fn display_hyperlink_lines_for_mode( + &self, + width: u16, + mode: HistoryRenderMode, + ) -> Vec { + match mode { + HistoryRenderMode::Rich => self.display_hyperlink_lines(width), + HistoryRenderMode::Raw => plain_hyperlink_lines(self.raw_lines()), + } + } + /// Returns the number of viewport rows needed to render this cell. /// /// The default delegates to `Paragraph::line_count` with @@ -224,13 +244,22 @@ pub(crate) trait HistoryCell: std::fmt::Debug + Send + Sync + Any { self.display_lines(width) } + /// Returns transcript-overlay lines plus terminal hyperlink metadata. + /// + /// Defaults to the plain transcript representation because some cells render different + /// display and transcript content. Rich cells whose transcript mirrors their display should + /// delegate to `display_hyperlink_lines`. + fn transcript_hyperlink_lines(&self, width: u16) -> Vec { + plain_hyperlink_lines(self.transcript_lines(width)) + } + /// Returns the number of viewport rows for the transcript overlay. /// /// Uses the same `Paragraph::line_count` measurement as /// `desired_height`. Contains a workaround for a ratatui bug where /// a single whitespace-only line reports 2 rows instead of 1. fn desired_transcript_height(&self, width: u16) -> u16 { - let lines = self.transcript_lines(width); + let lines = visible_lines(self.transcript_hyperlink_lines(width)); // Workaround: ratatui's line_count returns 2 for a single // whitespace-only line. Clamp to 1 in that case. if let [line] = &lines[..] @@ -270,7 +299,8 @@ pub(crate) trait HistoryCell: std::fmt::Debug + Send + Sync + Any { impl Renderable for Box { fn render(&self, area: Rect, buf: &mut Buffer) { - let lines = self.display_lines(area.width); + let hyperlink_lines = self.display_hyperlink_lines(area.width); + let lines = visible_lines(hyperlink_lines.clone()); let paragraph = Paragraph::new(Text::from(lines)).wrap(Wrap { trim: false }); let y = if area.height == 0 { 0 @@ -284,6 +314,7 @@ impl Renderable for Box { // entire draw area first so stale glyphs from previous frames never linger. Clear.render(area, buf); paragraph.scroll((y, 0)).render(area, buf); + mark_buffer_hyperlinks(buf, area, &hyperlink_lines, usize::from(y)); } fn desired_height(&self, width: u16) -> u16 { HistoryCell::desired_height(self.as_ref(), width) diff --git a/codex-rs/tui/src/history_cell/notices.rs b/codex-rs/tui/src/history_cell/notices.rs index 7fb994442c..d9dff4f5a0 100644 --- a/codex-rs/tui/src/history_cell/notices.rs +++ b/codex-rs/tui/src/history_cell/notices.rs @@ -71,6 +71,14 @@ impl HistoryCell for UpdateAvailableHistoryCell { Line::from("https://github.com/openai/codex/releases/latest"), ] } + + fn display_hyperlink_lines(&self, width: u16) -> Vec { + crate::terminal_hyperlinks::annotate_web_urls(self.display_lines(width)) + } + + fn transcript_hyperlink_lines(&self, width: u16) -> Vec { + self.display_hyperlink_lines(width) + } } #[allow(clippy::disallowed_methods)] pub(crate) fn new_warning_event(message: String) -> PrefixedWrappedHistoryCell { @@ -129,6 +137,14 @@ impl HistoryCell for CyberPolicyNoticeCell { Line::from(TRUSTED_ACCESS_FOR_CYBER_URL), ] } + + fn display_hyperlink_lines(&self, width: u16) -> Vec { + crate::terminal_hyperlinks::annotate_web_urls(self.display_lines(width)) + } + + fn transcript_hyperlink_lines(&self, width: u16) -> Vec { + self.display_hyperlink_lines(width) + } } #[derive(Debug)] diff --git a/codex-rs/tui/src/history_cell/plans.rs b/codex-rs/tui/src/history_cell/plans.rs index 6ea56195da..6b644f9e36 100644 --- a/codex-rs/tui/src/history_cell/plans.rs +++ b/codex-rs/tui/src/history_cell/plans.rs @@ -9,12 +9,12 @@ use super::*; /// preview-only during streaming. #[derive(Debug)] pub(crate) struct StreamingPlanTailCell { - lines: Vec>, + lines: Vec, is_stream_continuation: bool, } impl StreamingPlanTailCell { - pub(crate) fn new(lines: Vec>, is_stream_continuation: bool) -> Self { + pub(crate) fn new(lines: Vec, is_stream_continuation: bool) -> Self { Self { lines, is_stream_continuation, @@ -24,11 +24,19 @@ impl StreamingPlanTailCell { impl HistoryCell for StreamingPlanTailCell { fn display_lines(&self, _width: u16) -> Vec> { + visible_lines(self.lines.clone()) + } + + fn display_hyperlink_lines(&self, _width: u16) -> Vec { self.lines.clone() } + fn transcript_hyperlink_lines(&self, width: u16) -> Vec { + self.display_hyperlink_lines(width) + } + fn raw_lines(&self) -> Vec> { - plain_lines(self.lines.clone()) + plain_lines(visible_lines(self.lines.clone())) } fn is_stream_continuation(&self) -> bool { @@ -58,11 +66,11 @@ pub(crate) fn new_proposed_plan(plan_markdown: String, cwd: &Path) -> ProposedPl /// Stream cells are display fragments, not source-backed history. They should be replaced by /// `ProposedPlanCell` during consolidation before relying on resize reflow for finalized history. pub(crate) fn new_proposed_plan_stream( - lines: Vec>, + lines: Vec>, is_stream_continuation: bool, ) -> ProposedPlanStreamCell { ProposedPlanStreamCell { - lines, + lines: lines.into_iter().map(Into::into).collect(), is_stream_continuation, } } @@ -85,36 +93,43 @@ pub(crate) struct ProposedPlanCell { /// terminal resize. #[derive(Debug)] pub(crate) struct ProposedPlanStreamCell { - lines: Vec>, + lines: Vec, is_stream_continuation: bool, } impl HistoryCell for ProposedPlanCell { fn display_lines(&self, width: u16) -> Vec> { - let mut lines: Vec> = Vec::new(); - lines.push(vec!["• ".dim(), "Proposed Plan".bold()].into()); - lines.push(Line::from(" ")); + visible_lines(self.display_hyperlink_lines(width)) + } - let mut plan_lines: Vec> = vec![Line::from(" ")]; + fn display_hyperlink_lines(&self, width: u16) -> Vec { + let mut lines = vec![ + HyperlinkLine::new(vec!["• ".dim(), "Proposed Plan".bold()].into()), + HyperlinkLine::new(Line::from(" ")), + ]; + + let mut plan_lines = vec![HyperlinkLine::new(Line::from(" "))]; let plan_style = proposed_plan_style(); let wrap_width = width.saturating_sub(4).max(1) as usize; - let mut body: Vec> = Vec::new(); - append_markdown_agent_with_cwd( + let mut body = crate::markdown::render_markdown_agent_with_links_and_cwd( &self.plan_markdown, Some(wrap_width), Some(self.cwd.as_path()), - &mut body, ); if body.is_empty() { - body.push(Line::from("(empty)".dim().italic())); + body.push(HyperlinkLine::new(Line::from("(empty)".dim().italic()))); } - plan_lines.extend(prefix_lines(body, " ".into(), " ".into())); - plan_lines.push(Line::from(" ")); + plan_lines.extend(prefix_hyperlink_lines(body, " ".into(), " ".into())); + plan_lines.push(HyperlinkLine::new(Line::from(" "))); lines.extend(plan_lines.into_iter().map(|line| line.style(plan_style))); lines } + fn transcript_hyperlink_lines(&self, width: u16) -> Vec { + self.display_hyperlink_lines(width) + } + fn raw_lines(&self) -> Vec> { raw_lines_from_source(&self.plan_markdown) } @@ -122,11 +137,19 @@ impl HistoryCell for ProposedPlanCell { impl HistoryCell for ProposedPlanStreamCell { fn display_lines(&self, _width: u16) -> Vec> { + visible_lines(self.lines.clone()) + } + + fn display_hyperlink_lines(&self, _width: u16) -> Vec { self.lines.clone() } + fn transcript_hyperlink_lines(&self, width: u16) -> Vec { + self.display_hyperlink_lines(width) + } + fn raw_lines(&self) -> Vec> { - plain_lines(self.lines.clone()) + plain_lines(visible_lines(self.lines.clone())) } fn is_stream_continuation(&self) -> bool { diff --git a/codex-rs/tui/src/history_cell/tests.rs b/codex-rs/tui/src/history_cell/tests.rs index 6c83982a41..b4674cec8a 100644 --- a/codex-rs/tui/src/history_cell/tests.rs +++ b/codex-rs/tui/src/history_cell/tests.rs @@ -295,6 +295,47 @@ fn proposed_plan_cell_renders_markdown_table() { ); } +#[test] +fn proposed_plan_cell_preserves_wrapped_table_web_links() { + let destination = "https://example.com/a/very/long/path/to/a/table/artifact"; + let plan = new_proposed_plan( + format!("| Step | URL |\n| --- | --- |\n| Verify | {destination} |\n"), + &test_cwd(), + ); + + let lines = plan.display_hyperlink_lines(/*width*/ 32); + let linked_rows = lines + .iter() + .filter(|line| !line.hyperlinks.is_empty()) + .collect::>(); + + assert!(linked_rows.len() > 1); + assert!(linked_rows.iter().all(|line| { + line.hyperlinks + .iter() + .all(|link| link.destination == destination) + })); +} + +#[test] +fn composite_cell_preserves_child_web_links() { + let destination = "https://chatgpt.com/codex/settings/usage"; + let cell = CompositeHistoryCell::new(vec![ + Box::new(PlainHistoryCell::new(vec![Line::from("/status")])), + Box::new(WebHyperlinkHistoryCell::new(vec![Line::from(destination)])), + ]); + + let lines = cell.display_hyperlink_lines(/*width*/ 80); + + assert_eq!( + lines[2].hyperlinks, + vec![crate::terminal_hyperlinks::TerminalHyperlink { + columns: 0..destination.len(), + destination: destination.to_string(), + }] + ); +} + #[test] fn proposed_plan_cell_unwraps_markdown_fenced_table() { let plan = new_proposed_plan( diff --git a/codex-rs/tui/src/insert_history.rs b/codex-rs/tui/src/insert_history.rs index 40da6ccbcc..760b516421 100644 --- a/codex-rs/tui/src/insert_history.rs +++ b/codex-rs/tui/src/insert_history.rs @@ -7,6 +7,11 @@ use std::fmt; use std::io; use std::io::Write; +use crate::render::line_utils::line_to_static; +use crate::terminal_hyperlinks::HyperlinkLine; +use crate::terminal_hyperlinks::decorate_spans; +use crate::terminal_hyperlinks::plain_hyperlink_lines; +use crate::terminal_hyperlinks::remap_wrapped_line; use crate::wrapping::RtOptions; use crate::wrapping::adaptive_wrap_line; use crate::wrapping::line_contains_url_like; @@ -85,6 +90,23 @@ pub(crate) fn insert_history_lines_with_mode_and_wrap_policy( mode: InsertHistoryMode, wrap_policy: HistoryLineWrapPolicy, ) -> io::Result<()> +where + B: Backend + Write, +{ + insert_history_hyperlink_lines_with_mode_and_wrap_policy( + terminal, + plain_hyperlink_lines(lines.iter().map(line_to_static).collect()), + mode, + wrap_policy, + ) +} + +pub(crate) fn insert_history_hyperlink_lines_with_mode_and_wrap_policy( + terminal: &mut crate::custom_terminal::Terminal, + lines: Vec, + mode: InsertHistoryMode, + wrap_policy: HistoryLineWrapPolicy, +) -> io::Result<()> where B: Backend + Write, { @@ -113,13 +135,21 @@ where let line_wrapped = match wrap_policy { HistoryLineWrapPolicy::Terminal => vec![line.clone()], HistoryLineWrapPolicy::PreWrap - if line_contains_url_like(line) && !line_has_mixed_url_and_non_url_tokens(line) => + if line_contains_url_like(&line.line) + && !line_has_mixed_url_and_non_url_tokens(&line.line) => { vec![line.clone()] } - HistoryLineWrapPolicy::PreWrap => adaptive_wrap_line( + HistoryLineWrapPolicy::PreWrap => remap_wrapped_line( line, - RtOptions::new(wrap_width).subsequent_indent(leading_whitespace_prefix(line)), + adaptive_wrap_line( + &line.line, + RtOptions::new(wrap_width) + .subsequent_indent(leading_whitespace_prefix(&line.line)), + ) + .into_iter() + .map(|line| line_to_static(&line)) + .collect(), ), }; wrapped_rows += line_wrapped @@ -249,7 +279,11 @@ pub(crate) fn leading_whitespace_prefix(line: &Line<'_>) -> Line<'static> { /// Render a single wrapped history line: clear continuation rows for wide lines, /// set foreground/background colors, and write styled spans. Caller is responsible /// for cursor positioning and any leading `\r\n`. -fn write_history_line(writer: &mut W, line: &Line, wrap_width: usize) -> io::Result<()> { +fn write_history_line( + writer: &mut W, + line: &HyperlinkLine, + wrap_width: usize, +) -> io::Result<()> { let physical_rows = line.width().max(1).div_ceil(wrap_width) as u16; if physical_rows > 1 { queue!(writer, SavePosition)?; @@ -262,11 +296,13 @@ fn write_history_line(writer: &mut W, line: &Line, wrap_width: usize) queue!( writer, SetColors(Colors::new( - line.style + line.line + .style .fg .map(std::convert::Into::into) .unwrap_or(CColor::Reset), - line.style + line.line + .style .bg .map(std::convert::Into::into) .unwrap_or(CColor::Reset) @@ -276,14 +312,20 @@ fn write_history_line(writer: &mut W, line: &Line, wrap_width: usize) // Merge line-level style into each span so that ANSI colors reflect // line styles (e.g., blockquotes with green fg). let merged_spans: Vec = line + .line .spans .iter() .map(|s| Span { - style: s.style.patch(line.style), + style: s.style.patch(line.line.style), content: s.content.clone(), }) .collect(); - write_spans(writer, merged_spans.iter()) + let merged_line = HyperlinkLine { + line: Line::from(merged_spans), + hyperlinks: line.hyperlinks.clone(), + }; + let decorated = decorate_spans(&merged_line); + write_spans(writer, decorated.iter()) } #[derive(Debug, Clone, PartialEq, Eq)] @@ -470,6 +512,19 @@ mod tests { ); } + #[test] + fn writes_semantic_web_link_without_changing_visible_text() { + let destination = "https://example.com/long/path"; + let line = crate::terminal_hyperlinks::annotate_web_urls_in_line(Line::from(destination)); + let mut actual = Vec::new(); + + write_history_line(&mut actual, &line, /*wrap_width*/ 80).expect("write history line"); + + let output = String::from_utf8(actual).expect("UTF-8 terminal output"); + assert!(output.contains("\x1b]8;;https://example.com/long/path\x07")); + assert_eq!(line.line.spans[0].content, destination); + } + #[test] fn vt100_blockquote_line_emits_green_fg() { // Set up a small off-screen terminal diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 34065b7df8..a90fd7a362 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -177,6 +177,7 @@ mod status; mod status_indicator_widget; mod streaming; mod style; +mod terminal_hyperlinks; mod terminal_palette; mod terminal_probe; mod terminal_title; diff --git a/codex-rs/tui/src/markdown.rs b/codex-rs/tui/src/markdown.rs index 0cabee0805..31b8d09a76 100644 --- a/codex-rs/tui/src/markdown.rs +++ b/codex-rs/tui/src/markdown.rs @@ -25,6 +25,7 @@ use std::ops::Range; use std::path::Path; use crate::table_detect; +use crate::terminal_hyperlinks::HyperlinkLine; /// Render markdown source to styled ratatui lines and append them to `lines`. /// @@ -56,20 +57,22 @@ pub(crate) fn append_markdown_agent( width: Option, lines: &mut Vec>, ) { - append_markdown_agent_with_cwd(markdown_source, width, /*cwd*/ None, lines); + let normalized = unwrap_markdown_fences(markdown_source); + let rendered = crate::markdown_render::render_markdown_text_with_width_and_cwd( + &normalized, + width, + /*cwd*/ None, + ); + crate::render::line_utils::push_owned_lines(&rendered.lines, lines); } -/// Render an agent message while resolving local file links relative to `cwd`. -pub(crate) fn append_markdown_agent_with_cwd( +pub(crate) fn render_markdown_agent_with_links_and_cwd( markdown_source: &str, width: Option, cwd: Option<&Path>, - lines: &mut Vec>, -) { +) -> Vec { let normalized = unwrap_markdown_fences(markdown_source); - let rendered = - crate::markdown_render::render_markdown_text_with_width_and_cwd(&normalized, width, cwd); - crate::render::line_utils::push_owned_lines(&rendered.lines, lines); + crate::markdown_render::render_markdown_lines_with_width_and_cwd(&normalized, width, cwd) } /// Strip `` ```md ``/`` ```markdown `` fences that contain tables, emitting their content as bare diff --git a/codex-rs/tui/src/markdown_render.rs b/codex-rs/tui/src/markdown_render.rs index 87861989c0..61792b464c 100644 --- a/codex-rs/tui/src/markdown_render.rs +++ b/codex-rs/tui/src/markdown_render.rs @@ -40,8 +40,12 @@ use crate::render::highlight::foreground_style_for_scopes; use crate::render::highlight::highlight_code_to_lines; use crate::render::line_utils::line_to_static; -use crate::render::line_utils::push_owned_lines; use crate::style::table_separator_style; +use crate::terminal_hyperlinks::HyperlinkLine; +use crate::terminal_hyperlinks::annotate_web_urls_in_line; +use crate::terminal_hyperlinks::remap_wrapped_line; +use crate::terminal_hyperlinks::visible_lines; +use crate::terminal_hyperlinks::web_destination; use crate::wrapping::RtOptions; use crate::wrapping::adaptive_wrap_line; use crate::wrapping::word_wrap_line; @@ -66,6 +70,7 @@ use std::ops::Range; use std::path::Path; use std::path::PathBuf; use std::sync::LazyLock; +use unicode_width::UnicodeWidthChar; use unicode_width::UnicodeWidthStr; use url::Url; @@ -136,7 +141,7 @@ impl IndentContext { /// `lines` are used for final rendering. #[derive(Clone, Debug, Default)] struct TableCell { - lines: Vec>, + lines: Vec, } // TableCell mutators inlined — called per-span during table event parsing. @@ -144,7 +149,7 @@ impl TableCell { #[inline] fn ensure_line(&mut self) { if self.lines.is_empty() { - self.lines.push(Line::default()); + self.lines.push(HyperlinkLine::new(Line::default())); } } @@ -152,13 +157,26 @@ impl TableCell { fn push_span(&mut self, span: Span<'static>) { self.ensure_line(); if let Some(line) = self.lines.last_mut() { - line.push_span(span); + line.line.push_span(span); + } + } + + fn push_annotated(&mut self, mut appended: HyperlinkLine) { + self.ensure_line(); + if let Some(line) = self.lines.last_mut() { + let shift = line.width(); + line.line.spans.append(&mut appended.line.spans); + line.hyperlinks + .extend(appended.hyperlinks.into_iter().map(|mut link| { + link.columns = link.columns.start + shift..link.columns.end + shift; + link + })); } } #[inline] fn hard_break(&mut self) { - self.lines.push(Line::default()); + self.lines.push(HyperlinkLine::new(Line::default())); } fn plain_text(&self) -> String { @@ -168,7 +186,7 @@ impl TableCell { if i > 0 { buf.push(' '); } - for span in &line.spans { + for span in &line.line.spans { let _ = write!(buf, "{}", span.content); } } @@ -220,9 +238,9 @@ impl TableState { /// `spillover_lines` are prose rows extracted from parser artifacts and should /// be routed through normal wrapping. struct RenderedTableLines { - table_lines: Vec>, + table_lines: Vec, table_lines_prewrapped: bool, - spillover_lines: Vec>, + spillover_lines: Vec, } /// Classification of a table column for width-allocation priority. @@ -287,6 +305,16 @@ pub(crate) fn render_markdown_text_with_width_and_cwd( width: Option, cwd: Option<&Path>, ) -> Text<'static> { + Text::from(visible_lines(render_markdown_lines_with_width_and_cwd( + input, width, cwd, + ))) +} + +pub(crate) fn render_markdown_lines_with_width_and_cwd( + input: &str, + width: Option, + cwd: Option<&Path>, +) -> Vec { let mut options = Options::empty(); options.insert(Options::ENABLE_STRIKETHROUGH); options.insert(Options::ENABLE_TABLES); @@ -338,7 +366,7 @@ where { input: &'a str, iter: I, - text: Text<'static>, + text: Vec, styles: MarkdownStyles, inline_styles: Vec