diff --git a/codex-rs/common/src/config_summary.rs b/codex-rs/common/src/config_summary.rs index 8fc1bb26f3..f6ba1337b6 100644 --- a/codex-rs/common/src/config_summary.rs +++ b/codex-rs/common/src/config_summary.rs @@ -9,7 +9,7 @@ pub fn create_config_summary_entries(config: &Config) -> Vec<(&'static str, Stri ("workdir", config.cwd.display().to_string()), ("model", config.model.clone()), ("provider", config.model_provider_id.clone()), - ("approval", config.approval_policy.to_string()), + ("approval", (*config.approval_policy).to_string()), ("sandbox", summarize_sandbox_policy(&config.sandbox_policy)), ]; if config.model_provider.wire_api == WireApi::Responses diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 682861d648..4c8df97dfe 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -64,6 +64,7 @@ use crate::client_common::Prompt; use crate::client_common::ResponseEvent; use crate::compact::collect_user_messages; use crate::config::Config; +use crate::config::Constrained; use crate::config::types::ShellEnvironmentPolicy; use crate::context_manager::ContextManager; use crate::environment_context::EnvironmentContext; @@ -185,7 +186,7 @@ impl Codex { user_instructions, base_instructions: config.base_instructions.clone(), compact_prompt: config.compact_prompt.clone(), - approval_policy: config.approval_policy, + approval_policy: config.approval_policy.clone(), sandbox_policy: config.sandbox_policy.clone(), cwd: config.cwd.clone(), original_config_do_not_use: Arc::clone(&config), @@ -330,7 +331,7 @@ pub(crate) struct SessionConfiguration { compact_prompt: Option, /// When to escalate for approval for execution - approval_policy: AskForApproval, + approval_policy: Constrained, /// How to sandbox commands executed in the system sandbox_policy: SandboxPolicy, @@ -355,7 +356,7 @@ pub(crate) struct SessionConfiguration { } impl SessionConfiguration { - pub(crate) fn apply(&self, updates: &SessionSettingsUpdate) -> Self { + pub(crate) fn apply(&self, updates: &SessionSettingsUpdate) -> std::io::Result { let mut next_configuration = self.clone(); if let Some(model) = updates.model.clone() { next_configuration.model = model; @@ -367,7 +368,7 @@ impl SessionConfiguration { next_configuration.model_reasoning_summary = summary; } if let Some(approval_policy) = updates.approval_policy { - next_configuration.approval_policy = approval_policy; + next_configuration.approval_policy.set(approval_policy)?; } if let Some(sandbox_policy) = updates.sandbox_policy.clone() { next_configuration.sandbox_policy = sandbox_policy; @@ -375,7 +376,7 @@ impl SessionConfiguration { if let Some(cwd) = updates.cwd.clone() { next_configuration.cwd = cwd; } - next_configuration + Ok(next_configuration) } } @@ -440,7 +441,7 @@ impl Session { base_instructions: session_configuration.base_instructions.clone(), compact_prompt: session_configuration.compact_prompt.clone(), user_instructions: session_configuration.user_instructions.clone(), - approval_policy: session_configuration.approval_policy, + approval_policy: *session_configuration.approval_policy, sandbox_policy: session_configuration.sandbox_policy.clone(), shell_environment_policy: config.shell_environment_policy.clone(), tools_config, @@ -548,7 +549,7 @@ impl Session { config.model_reasoning_summary, config.model_context_window, config.model_auto_compact_token_limit, - config.approval_policy, + *config.approval_policy, config.sandbox_policy.clone(), config.mcp_servers.keys().map(String::as_str).collect(), config.active_profile.clone(), @@ -589,7 +590,7 @@ impl Session { session_id: conversation_id, model: session_configuration.model.clone(), model_provider_id: config.model_provider_id.clone(), - approval_policy: session_configuration.approval_policy, + approval_policy: *session_configuration.approval_policy, sandbox_policy: session_configuration.sandbox_policy.clone(), cwd: session_configuration.cwd.clone(), reasoning_effort: session_configuration.model_reasoning_effort, @@ -730,7 +731,12 @@ impl Session { pub(crate) async fn update_settings(&self, updates: SessionSettingsUpdate) { let mut state = self.state.lock().await; - state.session_configuration = state.session_configuration.apply(&updates); + match state.session_configuration.apply(&updates) { + Ok(updated) => state.session_configuration = updated, + Err(err) => { + warn!(%err, "rejected session settings update"); + } + } } pub(crate) async fn new_turn(&self, updates: SessionSettingsUpdate) -> Arc { @@ -745,9 +751,16 @@ impl Session { ) -> Arc { let session_configuration = { let mut state = self.state.lock().await; - let session_configuration = state.session_configuration.clone().apply(&updates); - state.session_configuration = session_configuration.clone(); - session_configuration + match state.session_configuration.clone().apply(&updates) { + Ok(next) => { + state.session_configuration = next.clone(); + next + } + Err(err) => { + warn!(%err, "rejected session settings update"); + state.session_configuration.clone() + } + } }; let mut turn_context: TurnContext = Self::make_turn_context( @@ -2571,7 +2584,7 @@ mod tests { user_instructions: config.user_instructions.clone(), base_instructions: config.base_instructions.clone(), compact_prompt: config.compact_prompt.clone(), - approval_policy: config.approval_policy, + approval_policy: config.approval_policy.clone(), sandbox_policy: config.sandbox_policy.clone(), cwd: config.cwd.clone(), original_config_do_not_use: Arc::clone(&config), @@ -2770,7 +2783,7 @@ mod tests { user_instructions: config.user_instructions.clone(), base_instructions: config.base_instructions.clone(), compact_prompt: config.compact_prompt.clone(), - approval_policy: config.approval_policy, + approval_policy: config.approval_policy.clone(), sandbox_policy: config.sandbox_policy.clone(), cwd: config.cwd.clone(), original_config_do_not_use: Arc::clone(&config), @@ -2848,7 +2861,7 @@ mod tests { user_instructions: config.user_instructions.clone(), base_instructions: config.base_instructions.clone(), compact_prompt: config.compact_prompt.clone(), - approval_policy: config.approval_policy, + approval_policy: config.approval_policy.clone(), sandbox_policy: config.sandbox_policy.clone(), cwd: config.cwd.clone(), original_config_do_not_use: Arc::clone(&config), diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 6276d3b6e3..a2faebb805 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -50,9 +50,11 @@ use serde::Deserialize; use similar::DiffableStr; use std::collections::BTreeMap; use std::collections::HashMap; +use std::fmt; use std::io::ErrorKind; use std::path::Path; use std::path::PathBuf; +use std::sync::Arc; use crate::config::profile::ConfigProfile; use toml::Value as TomlValue; @@ -73,6 +75,75 @@ pub(crate) const PROJECT_DOC_MAX_BYTES: usize = 32 * 1024; // 32 KiB pub const CONFIG_TOML_FILE: &str = "config.toml"; +type ConstraintValidator = dyn Fn(&T) -> std::io::Result<()> + Send + Sync; + +#[derive(Clone)] +pub struct Constrained { + value: T, + validator: Arc>, +} + +impl Constrained { + pub fn new( + initial_value: T, + validator: impl Fn(&T) -> std::io::Result<()> + Send + Sync + 'static, + ) -> std::io::Result { + let validator: Arc> = Arc::new(validator); + validator(&initial_value)?; + Ok(Self { + value: initial_value, + validator, + }) + } + + pub fn allow_any(initial_value: T) -> Self { + Self { + value: initial_value, + validator: Arc::new(|_| Ok(())), + } + } + + pub fn can_set(&self, candidate: &T) -> std::io::Result<()> { + (self.validator)(candidate) + } + + pub fn set(&mut self, value: T) -> std::io::Result<()> { + (self.validator)(&value)?; + self.value = value; + Ok(()) + } + + pub fn as_ref(&self) -> &T { + &self.value + } + + pub fn into_inner(self) -> T { + self.value + } +} + +impl std::ops::Deref for Constrained { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl fmt::Debug for Constrained { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Constrained") + .field("value", &self.value) + .finish() + } +} + +impl PartialEq for Constrained { + fn eq(&self, other: &Self) -> bool { + self.value == other.value + } +} + /// Application configuration loaded from disk and merged with overrides. #[derive(Debug, Clone, PartialEq)] pub struct Config { @@ -97,7 +168,7 @@ pub struct Config { pub model_provider: ModelProviderInfo, /// Approval policy for executing commands. - pub approval_policy: AskForApproval, + pub approval_policy: Constrained, pub sandbox_policy: SandboxPolicy, @@ -1053,6 +1124,7 @@ impl Config { AskForApproval::default() } }); + let approval_policy = Constrained::allow_any(approval_policy); let did_user_set_custom_approval_policy_or_sandbox_mode = approval_policy_override .is_some() || config_profile.approval_policy.is_some() @@ -1387,6 +1459,74 @@ mod tests { use std::time::Duration; use tempfile::TempDir; + #[test] + fn constrained_allow_any_accepts_any_value() { + let mut constrained = Constrained::allow_any(5); + constrained.set(-10).expect("allow any accepts all values"); + assert_eq!(*constrained, -10); + } + + #[test] + fn constrained_new_rejects_invalid_initial_value() { + let result = Constrained::new(0, |value| { + if *value > 0 { + Ok(()) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "must be positive", + )) + } + }); + + assert!(result.is_err()); + } + + #[test] + fn constrained_set_rejects_invalid_value_and_leaves_previous() { + let mut constrained = Constrained::new(1, |value| { + if *value > 0 { + Ok(()) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "must be positive", + )) + } + }) + .expect("initial value should be accepted"); + + let err = constrained + .set(-5) + .expect_err("negative values should be rejected"); + assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); + assert_eq!(*constrained, 1); + } + + #[test] + fn constrained_can_set_allows_probe_without_setting() { + let mut constrained = Constrained::new(1, |value| { + if *value > 0 { + Ok(()) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "must be positive", + )) + } + }) + .expect("initial value should be accepted"); + + constrained + .can_set(&2) + .expect("can_set should accept positive value"); + let err = constrained + .can_set(&-1) + .expect_err("can_set should reject negative value"); + assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); + assert_eq!(*constrained, 1); + } + #[test] fn test_toml_parsing() { let history_with_persistence = r#" @@ -2960,7 +3100,7 @@ model_verbosity = "high" model_auto_compact_token_limit: Some(180_000), model_provider_id: "openai".to_string(), model_provider: fixture.openai_provider.clone(), - approval_policy: AskForApproval::Never, + approval_policy: Constrained::allow_any(AskForApproval::Never), sandbox_policy: SandboxPolicy::new_read_only_policy(), did_user_set_custom_approval_policy_or_sandbox_mode: true, forced_auto_mode_downgraded_on_windows: false, @@ -3034,7 +3174,7 @@ model_verbosity = "high" model_auto_compact_token_limit: Some(14_746), model_provider_id: "openai-chat-completions".to_string(), model_provider: fixture.openai_chat_completions_provider.clone(), - approval_policy: AskForApproval::UnlessTrusted, + approval_policy: Constrained::allow_any(AskForApproval::UnlessTrusted), sandbox_policy: SandboxPolicy::new_read_only_policy(), did_user_set_custom_approval_policy_or_sandbox_mode: true, forced_auto_mode_downgraded_on_windows: false, @@ -3123,7 +3263,7 @@ model_verbosity = "high" model_auto_compact_token_limit: Some(180_000), model_provider_id: "openai".to_string(), model_provider: fixture.openai_provider.clone(), - approval_policy: AskForApproval::OnFailure, + approval_policy: Constrained::allow_any(AskForApproval::OnFailure), sandbox_policy: SandboxPolicy::new_read_only_policy(), did_user_set_custom_approval_policy_or_sandbox_mode: true, forced_auto_mode_downgraded_on_windows: false, @@ -3198,7 +3338,7 @@ model_verbosity = "high" model_auto_compact_token_limit: Some(244_800), model_provider_id: "openai".to_string(), model_provider: fixture.openai_provider.clone(), - approval_policy: AskForApproval::OnFailure, + approval_policy: Constrained::allow_any(AskForApproval::OnFailure), sandbox_policy: SandboxPolicy::new_read_only_policy(), did_user_set_custom_approval_policy_or_sandbox_mode: true, forced_auto_mode_downgraded_on_windows: false, @@ -3539,7 +3679,7 @@ trust_level = "untrusted" // Verify that untrusted projects get UnlessTrusted approval policy assert_eq!( - config.approval_policy, + *config.approval_policy, AskForApproval::UnlessTrusted, "Expected UnlessTrusted approval policy for untrusted project" ); diff --git a/codex-rs/core/tests/suite/approvals.rs b/codex-rs/core/tests/suite/approvals.rs index caa488a88f..ee66912463 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -1460,7 +1460,10 @@ async fn run_scenario(scenario: &ScenarioSpec) -> Result<()> { let model = model_override.unwrap_or("gpt-5.1"); let mut builder = test_codex().with_model(model).with_config(move |config| { - config.approval_policy = approval_policy; + config + .approval_policy + .set(approval_policy) + .expect("set approval policy"); config.sandbox_policy = sandbox_policy.clone(); for feature in features { config.features.enable(feature); diff --git a/codex-rs/core/tests/suite/codex_delegate.rs b/codex-rs/core/tests/suite/codex_delegate.rs index f5fe1a7df9..9005103153 100644 --- a/codex-rs/core/tests/suite/codex_delegate.rs +++ b/codex-rs/core/tests/suite/codex_delegate.rs @@ -60,7 +60,10 @@ async fn codex_delegate_forwards_exec_approval_and_proceeds_on_approval() { // Build a conversation configured to require approvals so the delegate // routes ExecApprovalRequest via the parent. let mut builder = test_codex().with_model("gpt-5.1").with_config(|config| { - config.approval_policy = AskForApproval::OnRequest; + config + .approval_policy + .set(AskForApproval::OnRequest) + .expect("set approval policy"); config.sandbox_policy = SandboxPolicy::ReadOnly; }); let test = builder.build(&server).await.expect("build test codex"); @@ -136,7 +139,10 @@ async fn codex_delegate_forwards_patch_approval_and_proceeds_on_decision() { mount_sse_sequence(&server, vec![sse1, sse2]).await; let mut builder = test_codex().with_model("gpt-5.1").with_config(|config| { - config.approval_policy = AskForApproval::OnRequest; + config + .approval_policy + .set(AskForApproval::OnRequest) + .expect("set approval policy"); // Use a restricted sandbox so patch approval is required config.sandbox_policy = SandboxPolicy::ReadOnly; config.include_apply_patch_tool = true; diff --git a/codex-rs/core/tests/suite/otel.rs b/codex-rs/core/tests/suite/otel.rs index 8665d3a8ea..62643f6494 100644 --- a/codex-rs/core/tests/suite/otel.rs +++ b/codex-rs/core/tests/suite/otel.rs @@ -773,7 +773,10 @@ async fn handle_container_exec_autoapprove_from_config_records_tool_decision() { let TestCodex { codex, .. } = test_codex() .with_config(|config| { - config.approval_policy = AskForApproval::OnRequest; + config + .approval_policy + .set(AskForApproval::OnRequest) + .expect("set approval policy"); config.sandbox_policy = SandboxPolicy::DangerFullAccess; }) .build(&server) @@ -822,7 +825,10 @@ async fn handle_container_exec_user_approved_records_tool_decision() { let TestCodex { codex, .. } = test_codex() .with_config(|config| { - config.approval_policy = AskForApproval::UnlessTrusted; + config + .approval_policy + .set(AskForApproval::UnlessTrusted) + .expect("set approval policy"); }) .build(&server) .await @@ -880,7 +886,10 @@ async fn handle_container_exec_user_approved_for_session_records_tool_decision() let TestCodex { codex, .. } = test_codex() .with_config(|config| { - config.approval_policy = AskForApproval::UnlessTrusted; + config + .approval_policy + .set(AskForApproval::UnlessTrusted) + .expect("set approval policy"); }) .build(&server) .await @@ -938,7 +947,10 @@ async fn handle_sandbox_error_user_approves_retry_records_tool_decision() { let TestCodex { codex, .. } = test_codex() .with_config(|config| { - config.approval_policy = AskForApproval::UnlessTrusted; + config + .approval_policy + .set(AskForApproval::UnlessTrusted) + .expect("set approval policy"); }) .build(&server) .await @@ -996,7 +1008,10 @@ async fn handle_container_exec_user_denies_records_tool_decision() { .await; let TestCodex { codex, .. } = test_codex() .with_config(|config| { - config.approval_policy = AskForApproval::UnlessTrusted; + config + .approval_policy + .set(AskForApproval::UnlessTrusted) + .expect("set approval policy"); }) .build(&server) .await @@ -1054,7 +1069,10 @@ async fn handle_sandbox_error_user_approves_for_session_records_tool_decision() let TestCodex { codex, .. } = test_codex() .with_config(|config| { - config.approval_policy = AskForApproval::UnlessTrusted; + config + .approval_policy + .set(AskForApproval::UnlessTrusted) + .expect("set approval policy"); }) .build(&server) .await @@ -1113,7 +1131,10 @@ async fn handle_sandbox_error_user_denies_records_tool_decision() { let TestCodex { codex, .. } = test_codex() .with_config(|config| { - config.approval_policy = AskForApproval::UnlessTrusted; + config + .approval_policy + .set(AskForApproval::UnlessTrusted) + .expect("set approval policy"); }) .build(&server) .await diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index f4455fd022..2220cf0eb6 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -573,7 +573,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a .await?; let default_cwd = config.cwd.clone(); - let default_approval_policy = config.approval_policy; + let default_approval_policy = *config.approval_policy; let default_sandbox_policy = config.sandbox_policy.clone(); let default_model = config.model.clone(); let default_effort = config.model_reasoning_effort; @@ -660,7 +660,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu .await?; let default_cwd = config.cwd.clone(); - let default_approval_policy = config.approval_policy; + let default_approval_policy = *config.approval_policy; let default_sandbox_policy = config.sandbox_policy.clone(); let default_model = config.model.clone(); let default_effort = config.model_reasoning_effort; diff --git a/codex-rs/core/tests/suite/resume_warning.rs b/codex-rs/core/tests/suite/resume_warning.rs index c8376e4109..0dc51b88ce 100644 --- a/codex-rs/core/tests/suite/resume_warning.rs +++ b/codex-rs/core/tests/suite/resume_warning.rs @@ -19,7 +19,7 @@ use tempfile::TempDir; fn resume_history(config: &codex_core::config::Config, previous_model: &str, rollout_path: &std::path::Path) -> InitialHistory { let turn_ctx = TurnContextItem { cwd: config.cwd.clone(), - approval_policy: config.approval_policy, + approval_policy: *config.approval_policy, sandbox_policy: config.sandbox_policy.clone(), model: previous_model.to_string(), effort: config.model_reasoning_effort, diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 0cf5aaf788..2fb7eb4145 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -262,7 +262,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any } let default_cwd = config.cwd.to_path_buf(); - let default_approval_policy = config.approval_policy; + let default_approval_policy = *config.approval_policy; let default_sandbox_policy = config.sandbox_policy.clone(); let default_model = config.model.clone(); let default_effort = config.model_reasoning_effort; diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index e0bb0d3a69..60b2284a31 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -2357,7 +2357,7 @@ impl ChatWidget { /// Open a popup to choose the approvals mode (ask for approval policy + sandbox policy). pub(crate) fn open_approvals_popup(&mut self) { - let current_approval = self.config.approval_policy; + let current_approval = *self.config.approval_policy; let current_sandbox = self.config.sandbox_policy.clone(); let mut items: Vec = Vec::new(); let presets: Vec = builtin_approval_presets(); @@ -2755,7 +2755,9 @@ impl ChatWidget { /// Set the approval policy in the widget's config copy. pub(crate) fn set_approval_policy(&mut self, policy: AskForApproval) { - self.config.approval_policy = policy; + if let Err(err) = self.config.approval_policy.set(policy) { + tracing::warn!(%err, "failed to set approval_policy on chat config"); + } } /// Set the sandbox policy in the widget's config copy. diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index c44401b3e7..305efd42ff 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -1959,7 +1959,10 @@ fn approval_modal_exec_snapshot() { // Build a chat widget with manual channels to avoid spawning the agent. let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); // Ensure policy allows surfacing approvals explicitly (not strictly required for direct event). - chat.config.approval_policy = AskForApproval::OnRequest; + chat.config + .approval_policy + .set(AskForApproval::OnRequest) + .expect("set approval policy"); // Inject an exec approval request to display the approval modal. let ev = ExecApprovalRequestEvent { call_id: "call-approve-cmd".into(), @@ -2007,7 +2010,10 @@ fn approval_modal_exec_snapshot() { #[test] fn approval_modal_exec_without_reason_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); - chat.config.approval_policy = AskForApproval::OnRequest; + chat.config + .approval_policy + .set(AskForApproval::OnRequest) + .expect("set approval policy"); let ev = ExecApprovalRequestEvent { call_id: "call-approve-cmd-noreason".into(), @@ -2040,7 +2046,10 @@ fn approval_modal_exec_without_reason_snapshot() { #[test] fn approval_modal_patch_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); - chat.config.approval_policy = AskForApproval::OnRequest; + chat.config + .approval_policy + .set(AskForApproval::OnRequest) + .expect("set approval policy"); // Build a small changeset and a reason/grant_root to exercise the prompt text. let mut changes = HashMap::new(); @@ -2612,7 +2621,10 @@ fn apply_patch_full_flow_integration_like() { fn apply_patch_untrusted_shows_approval_modal() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(); // Ensure approval policy is untrusted (OnRequest) - chat.config.approval_policy = AskForApproval::OnRequest; + chat.config + .approval_policy + .set(AskForApproval::OnRequest) + .expect("set approval policy"); // Simulate a patch approval request from backend let mut changes = HashMap::new(); @@ -2658,7 +2670,10 @@ fn apply_patch_request_shows_diff_summary() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(); // Ensure we are in OnRequest so an approval is surfaced - chat.config.approval_policy = AskForApproval::OnRequest; + chat.config + .approval_policy + .set(AskForApproval::OnRequest) + .expect("set approval policy"); // Simulate backend asking to apply a patch adding two lines to README.md let mut changes = HashMap::new();