mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Configure PSP routing through the feature system (#38056)
## What changed - Add the under-development `psp` feature and expose it in the config schema. - Use the feature to attach the PSP cookie to first-party ChatGPT clients. - Remove the hidden `--psp` flag and its process-scoped configuration plumbing. - Preserve configured ChatGPT cookies when creating the PSP client used for GET and POST requests. ## Testing - Update the config manager service test to verify that enabling `features.psp` retains the setting in the effective config and configures the expected ChatGPT cookie. GitOrigin-RevId: 53acb5495d2ff71e4ed25f674a0cff787aea474a
This commit is contained in:
1 parent
b28aa476f4
commit
285abc0368
24 files changed
+45
-105
No files matched your search
@@ -36,7 +36,6 @@ pub(crate) struct ConfigManager {
|
|||||||
cloud_config_bundle: Arc<RwLock<CloudConfigBundleLoader>>,
|
cloud_config_bundle: Arc<RwLock<CloudConfigBundleLoader>>,
|
||||||
arg0_paths: Arg0DispatchPaths,
|
arg0_paths: Arg0DispatchPaths,
|
||||||
thread_config_loader: Arc<RwLock<Arc<dyn ThreadConfigLoader>>>,
|
thread_config_loader: Arc<RwLock<Arc<dyn ThreadConfigLoader>>>,
|
||||||
pub(crate) psp: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigManager {
|
impl ConfigManager {
|
||||||
@@ -58,7 +57,6 @@ impl ConfigManager {
|
|||||||
cloud_config_bundle: Arc::new(RwLock::new(cloud_config_bundle)),
|
cloud_config_bundle: Arc::new(RwLock::new(cloud_config_bundle)),
|
||||||
arg0_paths,
|
arg0_paths,
|
||||||
thread_config_loader: Arc::new(RwLock::new(thread_config_loader)),
|
thread_config_loader: Arc::new(RwLock::new(thread_config_loader)),
|
||||||
psp: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +188,6 @@ impl ConfigManager {
|
|||||||
.cloud_config_bundle(CloudConfigBundleLoader::default())
|
.cloud_config_bundle(CloudConfigBundleLoader::default())
|
||||||
.build()
|
.build()
|
||||||
.await?;
|
.await?;
|
||||||
config.psp = self.psp;
|
|
||||||
self.apply_runtime_feature_enablement(&mut config);
|
self.apply_runtime_feature_enablement(&mut config);
|
||||||
self.apply_arg0_paths(&mut config);
|
self.apply_arg0_paths(&mut config);
|
||||||
Ok(config)
|
Ok(config)
|
||||||
@@ -251,8 +248,6 @@ impl ConfigManager {
|
|||||||
.map(|(key, value)| (key, json_to_toml(value))),
|
.map(|(key, value)| (key, json_to_toml(value))),
|
||||||
)
|
)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
typesafe_overrides.psp = Some(self.psp);
|
|
||||||
|
|
||||||
let mut config = codex_core::config::ConfigBuilder::default()
|
let mut config = codex_core::config::ConfigBuilder::default()
|
||||||
.codex_home(self.codex_home.clone())
|
.codex_home(self.codex_home.clone())
|
||||||
.cli_overrides(merged_cli_overrides)
|
.cli_overrides(merged_cli_overrides)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use axum::http::HeaderValue;
|
||||||
use codex_app_server_protocol::AppConfig;
|
use codex_app_server_protocol::AppConfig;
|
||||||
use codex_app_server_protocol::AppToolApproval;
|
use codex_app_server_protocol::AppToolApproval;
|
||||||
use codex_app_server_protocol::AppsConfig;
|
use codex_app_server_protocol::AppsConfig;
|
||||||
@@ -8,6 +9,8 @@ use codex_app_server_protocol::ConfigLayerSource as ApiConfigLayerSource;
|
|||||||
use codex_config::CloudConfigBundleLoader;
|
use codex_config::CloudConfigBundleLoader;
|
||||||
use codex_config::LoaderOverrides;
|
use codex_config::LoaderOverrides;
|
||||||
use codex_config::test_support::CloudConfigBundleFixture;
|
use codex_config::test_support::CloudConfigBundleFixture;
|
||||||
|
use codex_http_client::HttpClientFactory;
|
||||||
|
use codex_http_client::OutboundProxyPolicy;
|
||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
@@ -107,20 +110,22 @@ personality = true
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn process_routing_does_not_enter_config_layers() -> Result<()> {
|
async fn psp_feature_configures_first_party_routing() -> Result<()> {
|
||||||
let tmp = tempdir()?;
|
let tmp = tempdir()?;
|
||||||
let mut service = ConfigManager::new_for_tests(
|
let service = ConfigManager::new_for_tests(
|
||||||
tmp.path().to_path_buf(),
|
tmp.path().to_path_buf(),
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
LoaderOverrides::without_managed_config_for_tests(),
|
LoaderOverrides::without_managed_config_for_tests(),
|
||||||
CloudConfigBundleLoader::default(),
|
CloudConfigBundleLoader::default(),
|
||||||
);
|
);
|
||||||
service.psp = true;
|
|
||||||
|
|
||||||
let config = service
|
let config = service
|
||||||
.load_with_overrides(
|
.load_with_overrides(
|
||||||
Some(
|
Some(
|
||||||
[("features".to_string(), serde_json::json!({ "apps": true }))]
|
[(
|
||||||
|
"features".to_string(),
|
||||||
|
serde_json::json!({ "apps": true, "psp": true }),
|
||||||
|
)]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect(),
|
.collect(),
|
||||||
),
|
),
|
||||||
@@ -128,15 +133,19 @@ async fn process_routing_does_not_enter_config_layers() -> Result<()> {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
assert!(config.psp);
|
assert!(config.features.enabled(codex_features::Feature::Psp));
|
||||||
assert!(config.http_client_factory().has_chatgpt_cookies());
|
assert_eq!(
|
||||||
assert!(
|
config.http_client_factory(),
|
||||||
|
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault)
|
||||||
|
.with_chatgpt_cookies([HeaderValue::from_static("oai-chat-psp=true")])
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
config
|
config
|
||||||
.config_layer_stack
|
.config_layer_stack
|
||||||
.effective_config()
|
.effective_config()
|
||||||
.get("features")
|
.get("features")
|
||||||
.and_then(|features| features.get("psp"))
|
.and_then(|features| features.get("psp")),
|
||||||
.is_none()
|
Some(&toml::Value::Boolean(true))
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -446,7 +446,7 @@ async fn start_uninitialized(args: InProcessStartArgs) -> IoResult<InProcessClie
|
|||||||
));
|
));
|
||||||
|
|
||||||
let processor_outgoing = Arc::clone(&outgoing_message_sender);
|
let processor_outgoing = Arc::clone(&outgoing_message_sender);
|
||||||
let mut config_manager = ConfigManager::new(
|
let config_manager = ConfigManager::new(
|
||||||
args.config.codex_home.to_path_buf(),
|
args.config.codex_home.to_path_buf(),
|
||||||
args.cli_overrides,
|
args.cli_overrides,
|
||||||
args.loader_overrides,
|
args.loader_overrides,
|
||||||
@@ -455,7 +455,6 @@ async fn start_uninitialized(args: InProcessStartArgs) -> IoResult<InProcessClie
|
|||||||
args.arg0_paths.clone(),
|
args.arg0_paths.clone(),
|
||||||
args.thread_config_loader,
|
args.thread_config_loader,
|
||||||
);
|
);
|
||||||
config_manager.psp = args.config.psp;
|
|
||||||
let (processor_tx, mut processor_rx) = mpsc::channel::<ProcessorCommand>(channel_capacity);
|
let (processor_tx, mut processor_rx) = mpsc::channel::<ProcessorCommand>(channel_capacity);
|
||||||
let mut processor_handle = tokio::spawn(async move {
|
let mut processor_handle = tokio::spawn(async move {
|
||||||
let processor = Arc::new(MessageProcessor::new(MessageProcessorArgs {
|
let processor = Arc::new(MessageProcessor::new(MessageProcessorArgs {
|
||||||
|
|||||||
@@ -438,7 +438,6 @@ pub struct AppServerRuntimeOptions {
|
|||||||
pub plugin_startup_tasks: PluginStartupTasks,
|
pub plugin_startup_tasks: PluginStartupTasks,
|
||||||
pub remote_control_startup_mode: RemoteControlStartupMode,
|
pub remote_control_startup_mode: RemoteControlStartupMode,
|
||||||
pub install_shutdown_signal_handler: bool,
|
pub install_shutdown_signal_handler: bool,
|
||||||
pub psp: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AppServerRuntimeOptions {
|
impl Default for AppServerRuntimeOptions {
|
||||||
@@ -448,7 +447,6 @@ impl Default for AppServerRuntimeOptions {
|
|||||||
plugin_startup_tasks: PluginStartupTasks::Start,
|
plugin_startup_tasks: PluginStartupTasks::Start,
|
||||||
remote_control_startup_mode: RemoteControlStartupMode::ResolvePersisted,
|
remote_control_startup_mode: RemoteControlStartupMode::ResolvePersisted,
|
||||||
install_shutdown_signal_handler: true,
|
install_shutdown_signal_handler: true,
|
||||||
psp: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -489,7 +487,7 @@ pub async fn run_main_with_transport_options(
|
|||||||
arg0_paths.codex_linux_sandbox_exe.clone(),
|
arg0_paths.codex_linux_sandbox_exe.clone(),
|
||||||
)?;
|
)?;
|
||||||
let ignore_user_config = loader_overrides.ignore_user_config;
|
let ignore_user_config = loader_overrides.ignore_user_config;
|
||||||
let mut config_manager = ConfigManager::new(
|
let config_manager = ConfigManager::new(
|
||||||
codex_home.to_path_buf(),
|
codex_home.to_path_buf(),
|
||||||
cli_kv_overrides.clone(),
|
cli_kv_overrides.clone(),
|
||||||
loader_overrides,
|
loader_overrides,
|
||||||
@@ -498,7 +496,6 @@ pub async fn run_main_with_transport_options(
|
|||||||
arg0_paths.clone(),
|
arg0_paths.clone(),
|
||||||
Arc::new(NoopThreadConfigLoader),
|
Arc::new(NoopThreadConfigLoader),
|
||||||
);
|
);
|
||||||
config_manager.psp = runtime_options.psp;
|
|
||||||
match config_manager
|
match config_manager
|
||||||
.load_latest_config(/*fallback_cwd*/ None)
|
.load_latest_config(/*fallback_cwd*/ None)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -60,10 +60,6 @@ struct AppServerArgs {
|
|||||||
/// Enable remote control for this app-server process without changing persistence.
|
/// Enable remote control for this app-server process without changing persistence.
|
||||||
#[arg(long = "remote-control", hide = true)]
|
#[arg(long = "remote-control", hide = true)]
|
||||||
remote_control: bool,
|
remote_control: bool,
|
||||||
|
|
||||||
/// Enable process-only PSP routing for first-party ChatGPT requests.
|
|
||||||
#[arg(long, hide = true)]
|
|
||||||
psp: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
@@ -79,7 +75,6 @@ fn main() -> anyhow::Result<()> {
|
|||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
disable_plugin_startup_tasks_for_tests,
|
disable_plugin_startup_tasks_for_tests,
|
||||||
remote_control,
|
remote_control,
|
||||||
psp,
|
|
||||||
} = AppServerArgs::parse();
|
} = AppServerArgs::parse();
|
||||||
let loader_overrides = if disable_managed_config_from_debug_env() {
|
let loader_overrides = if disable_managed_config_from_debug_env() {
|
||||||
LoaderOverrides::without_managed_config_for_tests()
|
LoaderOverrides::without_managed_config_for_tests()
|
||||||
@@ -92,7 +87,6 @@ fn main() -> anyhow::Result<()> {
|
|||||||
let auth = auth.try_into_settings()?;
|
let auth = auth.try_into_settings()?;
|
||||||
let mut runtime_options = AppServerRuntimeOptions {
|
let mut runtime_options = AppServerRuntimeOptions {
|
||||||
code_mode_host_transport: code_mode_host.into(),
|
code_mode_host_transport: code_mode_host.into(),
|
||||||
psp,
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ fn app_server_accepts_process_scoped_code_mode_host() {
|
|||||||
"wss://example.test/code-mode",
|
"wss://example.test/code-mode",
|
||||||
"--listen",
|
"--listen",
|
||||||
"off",
|
"off",
|
||||||
"--psp",
|
|
||||||
])
|
])
|
||||||
.expect("parse app-server args");
|
.expect("parse app-server args");
|
||||||
|
|
||||||
@@ -55,7 +54,6 @@ fn app_server_accepts_process_scoped_code_mode_host() {
|
|||||||
Some(Url::parse("wss://example.test/code-mode").expect("test endpoint should parse"))
|
Some(Url::parse("wss://example.test/code-mode").expect("test endpoint should parse"))
|
||||||
);
|
);
|
||||||
assert_eq!(args.listen, AppServerTransport::Off);
|
assert_eq!(args.listen, AppServerTransport::Off);
|
||||||
assert!(args.psp);
|
|
||||||
assert_eq!(args.config_overrides.raw_overrides, Vec::<String>::new());
|
assert_eq!(args.config_overrides.raw_overrides, Vec::<String>::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,7 @@ static PSP_CHATGPT_CLIENT: LazyLock<Mutex<Option<CachedChatGptClient>>> =
|
|||||||
LazyLock::new(|| Mutex::new(None));
|
LazyLock::new(|| Mutex::new(None));
|
||||||
|
|
||||||
/// Reuse the default client while retaining its configured ChatGPT cookies.
|
/// Reuse the default client while retaining its configured ChatGPT cookies.
|
||||||
fn psp_chatgpt_client(config: &Config) -> HttpClient {
|
fn psp_chatgpt_client(factory: HttpClientFactory) -> HttpClient {
|
||||||
let factory = config.http_client_factory();
|
|
||||||
let residency = default_headers()
|
let residency = default_headers()
|
||||||
.get(RESIDENCY_HEADER_NAME)
|
.get(RESIDENCY_HEADER_NAME)
|
||||||
.map(|value| value.as_bytes().to_vec());
|
.map(|value| value.as_bytes().to_vec());
|
||||||
@@ -87,8 +86,9 @@ pub(crate) async fn chatgpt_get_request_with_timeout<T: DeserializeOwned>(
|
|||||||
path.trim_start_matches('/')
|
path.trim_start_matches('/')
|
||||||
);
|
);
|
||||||
|
|
||||||
let client = if config.psp {
|
let http_client_factory = config.http_client_factory();
|
||||||
psp_chatgpt_client(config)
|
let client = if http_client_factory.has_chatgpt_cookies() {
|
||||||
|
psp_chatgpt_client(http_client_factory)
|
||||||
} else {
|
} else {
|
||||||
create_client()
|
create_client()
|
||||||
};
|
};
|
||||||
@@ -145,8 +145,9 @@ pub(crate) async fn chatgpt_post_request_with_timeout<
|
|||||||
config.chatgpt_base_url.trim_end_matches('/'),
|
config.chatgpt_base_url.trim_end_matches('/'),
|
||||||
path.trim_start_matches('/')
|
path.trim_start_matches('/')
|
||||||
);
|
);
|
||||||
let client = if config.psp {
|
let http_client_factory = config.http_client_factory();
|
||||||
psp_chatgpt_client(config)
|
let client = if http_client_factory.has_chatgpt_cookies() {
|
||||||
|
psp_chatgpt_client(http_client_factory)
|
||||||
} else {
|
} else {
|
||||||
create_client()
|
create_client()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -105,10 +105,6 @@ use codex_terminal_detection::TerminalName;
|
|||||||
override_usage = "codex [OPTIONS] [PROMPT]\n codex [OPTIONS] <COMMAND> [ARGS]"
|
override_usage = "codex [OPTIONS] [PROMPT]\n codex [OPTIONS] <COMMAND> [ARGS]"
|
||||||
)]
|
)]
|
||||||
struct MultitoolCli {
|
struct MultitoolCli {
|
||||||
/// Enable process-only PSP routing for first-party ChatGPT requests.
|
|
||||||
#[arg(long, global = true, hide = true)]
|
|
||||||
psp: bool,
|
|
||||||
|
|
||||||
#[clap(flatten)]
|
#[clap(flatten)]
|
||||||
pub config_overrides: CliConfigOverrides,
|
pub config_overrides: CliConfigOverrides,
|
||||||
|
|
||||||
@@ -993,15 +989,12 @@ async fn cli_main(
|
|||||||
remote_control_disabled: bool,
|
remote_control_disabled: bool,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let MultitoolCli {
|
let MultitoolCli {
|
||||||
psp,
|
|
||||||
config_overrides: mut root_config_overrides,
|
config_overrides: mut root_config_overrides,
|
||||||
feature_toggles,
|
feature_toggles,
|
||||||
remote,
|
remote,
|
||||||
mut interactive,
|
mut interactive,
|
||||||
subcommand,
|
subcommand,
|
||||||
} = MultitoolCli::parse();
|
} = MultitoolCli::parse();
|
||||||
interactive.psp = psp;
|
|
||||||
|
|
||||||
// Fold --enable/--disable into config overrides so they flow to all subcommands.
|
// Fold --enable/--disable into config overrides so they flow to all subcommands.
|
||||||
let toggle_overrides = feature_toggles.to_overrides()?;
|
let toggle_overrides = feature_toggles.to_overrides()?;
|
||||||
root_config_overrides.raw_overrides.extend(toggle_overrides);
|
root_config_overrides.raw_overrides.extend(toggle_overrides);
|
||||||
@@ -1040,7 +1033,6 @@ async fn cli_main(
|
|||||||
exec_cli
|
exec_cli
|
||||||
.shared
|
.shared
|
||||||
.inherit_exec_root_options(&interactive.shared);
|
.inherit_exec_root_options(&interactive.shared);
|
||||||
exec_cli.psp = psp;
|
|
||||||
exec_cli.strict_config |= root_strict_config;
|
exec_cli.strict_config |= root_strict_config;
|
||||||
prepend_config_flags(
|
prepend_config_flags(
|
||||||
&mut exec_cli.config_overrides,
|
&mut exec_cli.config_overrides,
|
||||||
@@ -1061,7 +1053,6 @@ async fn cli_main(
|
|||||||
exec_cli
|
exec_cli
|
||||||
.shared
|
.shared
|
||||||
.inherit_exec_root_options(&interactive.shared);
|
.inherit_exec_root_options(&interactive.shared);
|
||||||
exec_cli.psp = psp;
|
|
||||||
exec_cli.command = Some(ExecCommand::Review(review_args));
|
exec_cli.command = Some(ExecCommand::Review(review_args));
|
||||||
exec_cli.strict_config = strict_config || root_strict_config;
|
exec_cli.strict_config = strict_config || root_strict_config;
|
||||||
prepend_config_flags(
|
prepend_config_flags(
|
||||||
@@ -1171,7 +1162,6 @@ async fn cli_main(
|
|||||||
codex_app_server::RemoteControlStartupMode::ResolvePersisted
|
codex_app_server::RemoteControlStartupMode::ResolvePersisted
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
psp,
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
codex_app_server::run_main_with_transport_options(
|
codex_app_server::run_main_with_transport_options(
|
||||||
@@ -1274,7 +1264,6 @@ async fn cli_main(
|
|||||||
remote_control_cli,
|
remote_control_cli,
|
||||||
arg0_paths.clone(),
|
arg0_paths.clone(),
|
||||||
root_config_overrides,
|
root_config_overrides,
|
||||||
psp,
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
@@ -2743,7 +2732,6 @@ mod tests {
|
|||||||
fn finalize_resume_from_args(args: &[&str]) -> TuiCli {
|
fn finalize_resume_from_args(args: &[&str]) -> TuiCli {
|
||||||
let cli = MultitoolCli::try_parse_from(args).expect("parse");
|
let cli = MultitoolCli::try_parse_from(args).expect("parse");
|
||||||
let MultitoolCli {
|
let MultitoolCli {
|
||||||
psp: _,
|
|
||||||
mut interactive,
|
mut interactive,
|
||||||
config_overrides: mut root_overrides,
|
config_overrides: mut root_overrides,
|
||||||
subcommand,
|
subcommand,
|
||||||
@@ -2781,7 +2769,6 @@ mod tests {
|
|||||||
fn finalize_fork_from_args(args: &[&str]) -> TuiCli {
|
fn finalize_fork_from_args(args: &[&str]) -> TuiCli {
|
||||||
let cli = MultitoolCli::try_parse_from(args).expect("parse");
|
let cli = MultitoolCli::try_parse_from(args).expect("parse");
|
||||||
let MultitoolCli {
|
let MultitoolCli {
|
||||||
psp: _,
|
|
||||||
mut interactive,
|
mut interactive,
|
||||||
config_overrides: mut root_overrides,
|
config_overrides: mut root_overrides,
|
||||||
subcommand,
|
subcommand,
|
||||||
@@ -2826,7 +2813,6 @@ mod tests {
|
|||||||
fn finalize_archive_from_args(args: &[&str]) -> (String, TuiCli, InteractiveRemoteOptions) {
|
fn finalize_archive_from_args(args: &[&str]) -> (String, TuiCli, InteractiveRemoteOptions) {
|
||||||
let cli = MultitoolCli::try_parse_from(args).expect("parse");
|
let cli = MultitoolCli::try_parse_from(args).expect("parse");
|
||||||
let MultitoolCli {
|
let MultitoolCli {
|
||||||
psp: _,
|
|
||||||
interactive,
|
interactive,
|
||||||
config_overrides: root_overrides,
|
config_overrides: root_overrides,
|
||||||
subcommand,
|
subcommand,
|
||||||
@@ -4055,19 +4041,6 @@ mod tests {
|
|||||||
assert!(err.to_string().contains("is empty"));
|
assert!(err.to_string().contains("is empty"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn psp_is_a_global_runtime_argument() {
|
|
||||||
for args in [
|
|
||||||
["codex", "--psp"].as_slice(),
|
|
||||||
["codex", "app-server", "--psp"].as_slice(),
|
|
||||||
["codex", "remote-control", "--psp"].as_slice(),
|
|
||||||
] {
|
|
||||||
let cli = MultitoolCli::try_parse_from(args).expect("parse runtime PSP flag");
|
|
||||||
assert!(cli.psp);
|
|
||||||
assert!(cli.config_overrides.raw_overrides.is_empty());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn app_server_code_mode_host_url_parses_independently_of_listen_transport() {
|
fn app_server_code_mode_host_url_parses_independently_of_listen_transport() {
|
||||||
let app_server = app_server_from_args(
|
let app_server = app_server_from_args(
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ pub(crate) async fn run(
|
|||||||
command: RemoteControlCommand,
|
command: RemoteControlCommand,
|
||||||
arg0_paths: Arg0DispatchPaths,
|
arg0_paths: Arg0DispatchPaths,
|
||||||
root_config_overrides: CliConfigOverrides,
|
root_config_overrides: CliConfigOverrides,
|
||||||
psp: bool,
|
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
match command.subcommand {
|
match command.subcommand {
|
||||||
None => {
|
None => {
|
||||||
@@ -73,8 +72,7 @@ pub(crate) async fn run(
|
|||||||
command.json,
|
command.json,
|
||||||
"Starting app-server with remote control enabled...",
|
"Starting app-server with remote control enabled...",
|
||||||
)?;
|
)?;
|
||||||
run_foreground_remote_control(command.json, arg0_paths, root_config_overrides, psp)
|
run_foreground_remote_control(command.json, arg0_paths, root_config_overrides).await?;
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
Some(RemoteControlSubcommand::Start) => {
|
Some(RemoteControlSubcommand::Start) => {
|
||||||
print_remote_control_progress(
|
print_remote_control_progress(
|
||||||
@@ -113,7 +111,6 @@ async fn run_foreground_remote_control(
|
|||||||
json: bool,
|
json: bool,
|
||||||
arg0_paths: Arg0DispatchPaths,
|
arg0_paths: Arg0DispatchPaths,
|
||||||
root_config_overrides: CliConfigOverrides,
|
root_config_overrides: CliConfigOverrides,
|
||||||
psp: bool,
|
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let socket_dir = tempfile::Builder::new()
|
let socket_dir = tempfile::Builder::new()
|
||||||
.prefix("codex-rc-")
|
.prefix("codex-rc-")
|
||||||
@@ -129,7 +126,6 @@ async fn run_foreground_remote_control(
|
|||||||
let runtime_options = AppServerRuntimeOptions {
|
let runtime_options = AppServerRuntimeOptions {
|
||||||
remote_control_startup_mode: codex_app_server::RemoteControlStartupMode::EnabledEphemeral,
|
remote_control_startup_mode: codex_app_server::RemoteControlStartupMode::EnabledEphemeral,
|
||||||
install_shutdown_signal_handler: false,
|
install_shutdown_signal_handler: false,
|
||||||
psp,
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let (stop_rx, stop_signal_task) = foreground_stop_signal();
|
let (stop_rx, stop_signal_task) = foreground_stop_signal();
|
||||||
|
|||||||
@@ -654,6 +654,9 @@
|
|||||||
"prevent_idle_sleep": {
|
"prevent_idle_sleep": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"psp": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"realtime_conversation": {
|
"realtime_conversation": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
@@ -5290,6 +5293,9 @@
|
|||||||
"prevent_idle_sleep": {
|
"prevent_idle_sleep": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"psp": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"realtime_conversation": {
|
"realtime_conversation": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -285,7 +285,6 @@ mod reload {
|
|||||||
.flatten(),
|
.flatten(),
|
||||||
model_provider: preserve_current_provider.then(|| config.model_provider_id.clone()),
|
model_provider: preserve_current_provider.then(|| config.model_provider_id.clone()),
|
||||||
service_tier: preserve_current_service_tier.then(|| config.service_tier.clone()),
|
service_tier: preserve_current_service_tier.then(|| config.service_tier.clone()),
|
||||||
psp: Some(config.psp),
|
|
||||||
codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(),
|
codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(),
|
||||||
main_execve_wrapper_exe: config.main_execve_wrapper_exe.clone(),
|
main_execve_wrapper_exe: config.main_execve_wrapper_exe.clone(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|||||||
@@ -162,7 +162,6 @@ async fn apply_role_preserves_unspecified_keys() {
|
|||||||
.await;
|
.await;
|
||||||
config.codex_linux_sandbox_exe = Some(PathBuf::from("/tmp/codex-linux-sandbox"));
|
config.codex_linux_sandbox_exe = Some(PathBuf::from("/tmp/codex-linux-sandbox"));
|
||||||
config.main_execve_wrapper_exe = Some(PathBuf::from("/tmp/codex-execve-wrapper"));
|
config.main_execve_wrapper_exe = Some(PathBuf::from("/tmp/codex-execve-wrapper"));
|
||||||
config.psp = true;
|
|
||||||
let role_path = write_role_config(
|
let role_path = write_role_config(
|
||||||
&home,
|
&home,
|
||||||
"instructions-only.toml",
|
"instructions-only.toml",
|
||||||
@@ -203,7 +202,6 @@ async fn apply_role_preserves_unspecified_keys() {
|
|||||||
config.main_execve_wrapper_exe,
|
config.main_execve_wrapper_exe,
|
||||||
Some(PathBuf::from("/tmp/codex-execve-wrapper"))
|
Some(PathBuf::from("/tmp/codex-execve-wrapper"))
|
||||||
);
|
);
|
||||||
assert!(config.psp);
|
|
||||||
assert_eq!(config.base_instructions, base_instructions);
|
assert_eq!(config.base_instructions, base_instructions);
|
||||||
assert_eq!(config.base_instructions_provenance, provenance);
|
assert_eq!(config.base_instructions_provenance, provenance);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5034,7 +5034,6 @@ async fn rebuild_preserving_session_layers_refreshes_requirements() -> std::io::
|
|||||||
refreshed_toml,
|
refreshed_toml,
|
||||||
ConfigOverrides {
|
ConfigOverrides {
|
||||||
cwd: Some(codex_home.path().to_path_buf()),
|
cwd: Some(codex_home.path().to_path_buf()),
|
||||||
psp: Some(true),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
codex_home.abs(),
|
codex_home.abs(),
|
||||||
@@ -5112,8 +5111,6 @@ async fn rebuild_preserving_session_layers_refreshes_requirements() -> std::io::
|
|||||||
.rebuild_preserving_session_layers(&refreshed_config)
|
.rebuild_preserving_session_layers(&refreshed_config)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
assert!(config.psp);
|
|
||||||
assert!(config.http_client_factory().has_chatgpt_cookies());
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
config.mcp_servers.get(),
|
config.mcp_servers.get(),
|
||||||
&HashMap::from([
|
&HashMap::from([
|
||||||
|
|||||||
@@ -977,9 +977,6 @@ pub struct Config {
|
|||||||
/// Whether Codex-owned clients should respect host system proxy settings.
|
/// Whether Codex-owned clients should respect host system proxy settings.
|
||||||
pub respect_system_proxy: bool,
|
pub respect_system_proxy: bool,
|
||||||
|
|
||||||
/// Process-only ChatGPT routing selection supplied when Codex is launched.
|
|
||||||
pub psp: bool,
|
|
||||||
|
|
||||||
/// Optional product SKU forwarded to the host-owned apps MCP server.
|
/// Optional product SKU forwarded to the host-owned apps MCP server.
|
||||||
pub apps_mcp_product_sku: Option<String>,
|
pub apps_mcp_product_sku: Option<String>,
|
||||||
|
|
||||||
@@ -1623,7 +1620,7 @@ impl Config {
|
|||||||
OutboundProxyPolicy::ReqwestDefault
|
OutboundProxyPolicy::ReqwestDefault
|
||||||
};
|
};
|
||||||
let factory = HttpClientFactory::new(outbound_proxy_policy);
|
let factory = HttpClientFactory::new(outbound_proxy_policy);
|
||||||
if self.psp {
|
if self.features.enabled(Feature::Psp) {
|
||||||
factory.with_chatgpt_cookies([HeaderValue::from_static("oai-chat-psp=true")])
|
factory.with_chatgpt_cookies([HeaderValue::from_static("oai-chat-psp=true")])
|
||||||
} else {
|
} else {
|
||||||
factory
|
factory
|
||||||
@@ -1835,7 +1832,6 @@ impl Config {
|
|||||||
ConfigOverrides {
|
ConfigOverrides {
|
||||||
cwd: Some(self.cwd.to_path_buf()),
|
cwd: Some(self.cwd.to_path_buf()),
|
||||||
default_zsh_path,
|
default_zsh_path,
|
||||||
psp: Some(refreshed_config.psp),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
refreshed_config.codex_home.clone(),
|
refreshed_config.codex_home.clone(),
|
||||||
@@ -2579,7 +2575,6 @@ pub struct ConfigOverrides {
|
|||||||
pub tools_web_search_request: Option<bool>,
|
pub tools_web_search_request: Option<bool>,
|
||||||
pub ephemeral: Option<bool>,
|
pub ephemeral: Option<bool>,
|
||||||
pub bypass_hook_trust: Option<bool>,
|
pub bypass_hook_trust: Option<bool>,
|
||||||
pub psp: Option<bool>,
|
|
||||||
/// Additional directories that should be treated as writable roots for this session.
|
/// Additional directories that should be treated as writable roots for this session.
|
||||||
pub additional_writable_roots: Vec<PathBuf>,
|
pub additional_writable_roots: Vec<PathBuf>,
|
||||||
/// Explicit absolute runtime workspace roots for this session. When set,
|
/// Explicit absolute runtime workspace roots for this session. When set,
|
||||||
@@ -3270,7 +3265,6 @@ impl Config {
|
|||||||
tools_web_search_request: override_tools_web_search_request,
|
tools_web_search_request: override_tools_web_search_request,
|
||||||
ephemeral,
|
ephemeral,
|
||||||
bypass_hook_trust,
|
bypass_hook_trust,
|
||||||
psp,
|
|
||||||
additional_writable_roots,
|
additional_writable_roots,
|
||||||
workspace_roots: workspace_roots_override,
|
workspace_roots: workspace_roots_override,
|
||||||
} = overrides;
|
} = overrides;
|
||||||
@@ -4192,7 +4186,6 @@ impl Config {
|
|||||||
.chatgpt_base_url
|
.chatgpt_base_url
|
||||||
.unwrap_or("https://chatgpt.com/backend-api/".to_string()),
|
.unwrap_or("https://chatgpt.com/backend-api/".to_string()),
|
||||||
respect_system_proxy,
|
respect_system_proxy,
|
||||||
psp: psp.unwrap_or_default(),
|
|
||||||
apps_mcp_product_sku: cfg.apps_mcp_product_sku.clone(),
|
apps_mcp_product_sku: cfg.apps_mcp_product_sku.clone(),
|
||||||
responses_api_metadata: cfg.responses_api_metadata.unwrap_or_default(),
|
responses_api_metadata: cfg.responses_api_metadata.unwrap_or_default(),
|
||||||
realtime_audio: cfg
|
realtime_audio: cfg
|
||||||
|
|||||||
@@ -12,10 +12,6 @@ use std::path::PathBuf;
|
|||||||
override_usage = "codex exec [OPTIONS] [PROMPT]\n codex exec [OPTIONS] <COMMAND> [ARGS]"
|
override_usage = "codex exec [OPTIONS] [PROMPT]\n codex exec [OPTIONS] <COMMAND> [ARGS]"
|
||||||
)]
|
)]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
/// Process-only PSP routing selected by the parent Codex CLI.
|
|
||||||
#[clap(skip)]
|
|
||||||
pub psp: bool,
|
|
||||||
|
|
||||||
/// Action to perform. If omitted, runs a new non-interactive session.
|
/// Action to perform. If omitted, runs a new non-interactive session.
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: Option<Command>,
|
pub command: Option<Command>,
|
||||||
|
|||||||
@@ -245,7 +245,6 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
|
|||||||
}
|
}
|
||||||
|
|
||||||
let Cli {
|
let Cli {
|
||||||
psp,
|
|
||||||
command,
|
command,
|
||||||
strict_config,
|
strict_config,
|
||||||
shared,
|
shared,
|
||||||
@@ -423,7 +422,6 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
|
|||||||
tools_web_search_request: None,
|
tools_web_search_request: None,
|
||||||
ephemeral: ephemeral.then_some(true),
|
ephemeral: ephemeral.then_some(true),
|
||||||
bypass_hook_trust: bypass_hook_trust.then_some(true),
|
bypass_hook_trust: bypass_hook_trust.then_some(true),
|
||||||
psp: Some(psp),
|
|
||||||
additional_writable_roots: add_dir,
|
additional_writable_roots: add_dir,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,9 +18,6 @@ use codex_utils_cli::CliConfigOverrides;
|
|||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
struct TopCli {
|
struct TopCli {
|
||||||
#[arg(long, global = true, hide = true)]
|
|
||||||
psp: bool,
|
|
||||||
|
|
||||||
#[clap(flatten)]
|
#[clap(flatten)]
|
||||||
config_overrides: CliConfigOverrides,
|
config_overrides: CliConfigOverrides,
|
||||||
|
|
||||||
@@ -33,7 +30,6 @@ fn main() -> anyhow::Result<()> {
|
|||||||
let top_cli = TopCli::parse();
|
let top_cli = TopCli::parse();
|
||||||
// Merge root-level overrides into inner CLI struct so downstream logic remains unchanged.
|
// Merge root-level overrides into inner CLI struct so downstream logic remains unchanged.
|
||||||
let mut inner = top_cli.inner;
|
let mut inner = top_cli.inner;
|
||||||
inner.psp = top_cli.psp;
|
|
||||||
inner
|
inner
|
||||||
.config_overrides
|
.config_overrides
|
||||||
.prepend_root_overrides(top_cli.config_overrides);
|
.prepend_root_overrides(top_cli.config_overrides);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ fn top_cli_parses_resume_prompt_after_config_flag() {
|
|||||||
let cli = TopCli::parse_from([
|
let cli = TopCli::parse_from([
|
||||||
"codex-exec",
|
"codex-exec",
|
||||||
"resume",
|
"resume",
|
||||||
"--psp",
|
|
||||||
"--strict-config",
|
"--strict-config",
|
||||||
"--last",
|
"--last",
|
||||||
"--json",
|
"--json",
|
||||||
@@ -19,7 +18,6 @@ fn top_cli_parses_resume_prompt_after_config_flag() {
|
|||||||
"--skip-git-repo-check",
|
"--skip-git-repo-check",
|
||||||
PROMPT,
|
PROMPT,
|
||||||
]);
|
]);
|
||||||
assert!(cli.psp);
|
|
||||||
let mut inner = cli.inner;
|
let mut inner = cli.inner;
|
||||||
inner
|
inner
|
||||||
.config_overrides
|
.config_overrides
|
||||||
|
|||||||
@@ -171,6 +171,8 @@ pub enum Feature {
|
|||||||
SpawnCsv,
|
SpawnCsv,
|
||||||
/// Enable apps.
|
/// Enable apps.
|
||||||
Apps,
|
Apps,
|
||||||
|
/// Route first-party ChatGPT requests through PSP.
|
||||||
|
Psp,
|
||||||
/// Enable MCP apps.
|
/// Enable MCP apps.
|
||||||
EnableMcpApps,
|
EnableMcpApps,
|
||||||
/// Enable MCP protocol version 2026-07-28 support.
|
/// Enable MCP protocol version 2026-07-28 support.
|
||||||
@@ -1092,6 +1094,12 @@ pub const FEATURES: &[FeatureSpec] = &[
|
|||||||
stage: Stage::Stable,
|
stage: Stage::Stable,
|
||||||
default_enabled: true,
|
default_enabled: true,
|
||||||
},
|
},
|
||||||
|
FeatureSpec {
|
||||||
|
id: Feature::Psp,
|
||||||
|
key: "psp",
|
||||||
|
stage: Stage::UnderDevelopment,
|
||||||
|
default_enabled: false,
|
||||||
|
},
|
||||||
FeatureSpec {
|
FeatureSpec {
|
||||||
id: Feature::EnableMcpApps,
|
id: Feature::EnableMcpApps,
|
||||||
key: "enable_mcp_apps",
|
key: "enable_mcp_apps",
|
||||||
|
|||||||
@@ -187,7 +187,6 @@ fn new_config(model: Option<String>, arg0_paths: Arg0DispatchPaths) -> anyhow::R
|
|||||||
config_layer_stack: ConfigLayerStack::default(),
|
config_layer_stack: ConfigLayerStack::default(),
|
||||||
startup_warnings: Vec::new(),
|
startup_warnings: Vec::new(),
|
||||||
bypass_hook_trust: false,
|
bypass_hook_trust: false,
|
||||||
psp: false,
|
|
||||||
model,
|
model,
|
||||||
service_tier: None,
|
service_tier: None,
|
||||||
review_model: None,
|
review_model: None,
|
||||||
|
|||||||
@@ -8,10 +8,6 @@ use codex_utils_cli::SharedCliOptions;
|
|||||||
#[derive(Parser, Clone, Debug)]
|
#[derive(Parser, Clone, Debug)]
|
||||||
#[command(version)]
|
#[command(version)]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
/// Process-only PSP routing selected by the parent Codex CLI.
|
|
||||||
#[clap(skip)]
|
|
||||||
pub psp: bool,
|
|
||||||
|
|
||||||
/// Optional user prompt to start the session.
|
/// Optional user prompt to start the session.
|
||||||
#[arg(value_name = "PROMPT", value_hint = clap::ValueHint::Other)]
|
#[arg(value_name = "PROMPT", value_hint = clap::ValueHint::Other)]
|
||||||
pub prompt: Option<String>,
|
pub prompt: Option<String>,
|
||||||
|
|||||||
@@ -943,7 +943,7 @@ pub async fn run_main(
|
|||||||
&cli_kv_overrides,
|
&cli_kv_overrides,
|
||||||
&launch_loader_overrides,
|
&launch_loader_overrides,
|
||||||
strict_config,
|
strict_config,
|
||||||
cli.bypass_hook_trust || cli.psp,
|
cli.bypass_hook_trust,
|
||||||
);
|
);
|
||||||
let default_daemon = if explicit_remote_endpoint.is_none() && reuse_implicit_local_daemon {
|
let default_daemon = if explicit_remote_endpoint.is_none() && reuse_implicit_local_daemon {
|
||||||
maybe_probe_default_daemon_socket(&codex_home).await
|
maybe_probe_default_daemon_socket(&codex_home).await
|
||||||
@@ -1077,7 +1077,6 @@ pub async fn run_main(
|
|||||||
main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(),
|
main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(),
|
||||||
show_raw_agent_reasoning: cli.oss.then_some(true),
|
show_raw_agent_reasoning: cli.oss.then_some(true),
|
||||||
bypass_hook_trust: cli.bypass_hook_trust.then_some(true),
|
bypass_hook_trust: cli.bypass_hook_trust.then_some(true),
|
||||||
psp: Some(cli.psp),
|
|
||||||
additional_writable_roots: additional_dirs,
|
additional_writable_roots: additional_dirs,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,9 +40,6 @@ fn format_exit_messages(exit_info: AppExitInfo, color_enabled: bool) -> Vec<Stri
|
|||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
struct TopCli {
|
struct TopCli {
|
||||||
#[arg(long, hide = true)]
|
|
||||||
psp: bool,
|
|
||||||
|
|
||||||
#[clap(flatten)]
|
#[clap(flatten)]
|
||||||
config_overrides: CliConfigOverrides,
|
config_overrides: CliConfigOverrides,
|
||||||
|
|
||||||
@@ -54,7 +51,6 @@ fn main() -> anyhow::Result<()> {
|
|||||||
arg0_dispatch_or_else(|arg0_paths: Arg0DispatchPaths| async move {
|
arg0_dispatch_or_else(|arg0_paths: Arg0DispatchPaths| async move {
|
||||||
let top_cli = TopCli::parse();
|
let top_cli = TopCli::parse();
|
||||||
let mut inner = top_cli.inner;
|
let mut inner = top_cli.inner;
|
||||||
inner.psp = top_cli.psp;
|
|
||||||
inner
|
inner
|
||||||
.config_overrides
|
.config_overrides
|
||||||
.raw_overrides
|
.raw_overrides
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ async fn start_app_server_for_archive_command(
|
|||||||
&cli_kv_overrides,
|
&cli_kv_overrides,
|
||||||
&launch_loader_overrides,
|
&launch_loader_overrides,
|
||||||
strict_config,
|
strict_config,
|
||||||
cli.bypass_hook_trust || cli.psp,
|
cli.bypass_hook_trust,
|
||||||
);
|
);
|
||||||
let default_daemon = if explicit_remote_endpoint.is_none() && reuse_implicit_local_daemon {
|
let default_daemon = if explicit_remote_endpoint.is_none() && reuse_implicit_local_daemon {
|
||||||
super::maybe_probe_default_daemon_socket(codex_home.as_path()).await
|
super::maybe_probe_default_daemon_socket(codex_home.as_path()).await
|
||||||
@@ -373,7 +373,6 @@ async fn start_app_server_for_archive_command(
|
|||||||
main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(),
|
main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(),
|
||||||
show_raw_agent_reasoning: cli.oss.then_some(true),
|
show_raw_agent_reasoning: cli.oss.then_some(true),
|
||||||
bypass_hook_trust: cli.bypass_hook_trust.then_some(true),
|
bypass_hook_trust: cli.bypass_hook_trust.then_some(true),
|
||||||
psp: Some(cli.psp),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.loader_overrides(loader_overrides.clone())
|
.loader_overrides(loader_overrides.clone())
|
||||||
|
|||||||
Reference in new issue
Block a user