mirror of
https://github.com/openai/codex.git
synced 2026-08-26 13:38:49 +00:00
## Why App-server clients can enable `background_paginated_rollout_migration` after the server has started, so the startup-only migration path does not handle that transition. ## What changed - Allow runtime enablement of `background_paginated_rollout_migration`. - Start the local rollout migration when the feature transitions from disabled to enabled at runtime. ## Testing Update the app-server integration test to enable migration through the runtime feature API, wait for paginated history, and verify that cold resume preserves model context. GitOrigin-RevId: d695624a03b4f056be94fcaac2075f91896f19e5
1097 lines
43 KiB
Rust
1097 lines
43 KiB
Rust
use std::sync::Arc;
|
|
|
|
use crate::config_manager::ConfigManager;
|
|
use crate::config_manager_service::ConfigManagerError;
|
|
use crate::error_code::internal_error;
|
|
use crate::error_code::invalid_request;
|
|
use crate::outgoing_message::ConnectionRequestId;
|
|
use crate::outgoing_message::OutgoingMessageSender;
|
|
use codex_analytics::AnalyticsEventsClient;
|
|
use codex_app_server_protocol::AllowDenyRequirement;
|
|
use codex_app_server_protocol::AutoReviewRequirements;
|
|
use codex_app_server_protocol::BrowserUseAccessApprovalLifetime;
|
|
use codex_app_server_protocol::BrowserUseOriginPolicy;
|
|
use codex_app_server_protocol::BrowserUseRequirements;
|
|
use codex_app_server_protocol::CliAuthCredentialsStoreMode;
|
|
use codex_app_server_protocol::ClientResponsePayload;
|
|
use codex_app_server_protocol::ComputerUseMacosRequirements;
|
|
use codex_app_server_protocol::ComputerUseRequirements;
|
|
use codex_app_server_protocol::ComputerUseWindowsExeRequirement;
|
|
use codex_app_server_protocol::ComputerUseWindowsRequirements;
|
|
use codex_app_server_protocol::ConfigBatchWriteParams;
|
|
use codex_app_server_protocol::ConfigReadParams;
|
|
use codex_app_server_protocol::ConfigReadResponse;
|
|
use codex_app_server_protocol::ConfigRequirements;
|
|
use codex_app_server_protocol::ConfigRequirementsReadResponse;
|
|
use codex_app_server_protocol::ConfigValueWriteParams;
|
|
use codex_app_server_protocol::ConfigWriteErrorCode;
|
|
use codex_app_server_protocol::ConfigWriteResponse;
|
|
use codex_app_server_protocol::ConfiguredHookHandler;
|
|
use codex_app_server_protocol::ConfiguredHookMatcherGroup;
|
|
use codex_app_server_protocol::ExperimentalFeatureEnablementSetParams;
|
|
use codex_app_server_protocol::ExperimentalFeatureEnablementSetResponse;
|
|
use codex_app_server_protocol::FeedbackRequirements;
|
|
use codex_app_server_protocol::InAppBrowserRequirements;
|
|
use codex_app_server_protocol::JSONRPCErrorError;
|
|
use codex_app_server_protocol::ManagedHooksRequirements;
|
|
use codex_app_server_protocol::ModelProviderCapabilitiesReadResponse;
|
|
use codex_app_server_protocol::ModelsRequirements;
|
|
use codex_app_server_protocol::NetworkDomainPermission;
|
|
use codex_app_server_protocol::NetworkRequirements;
|
|
use codex_app_server_protocol::NetworkUnixSocketPermission;
|
|
use codex_app_server_protocol::NewThreadModelDefaults;
|
|
use codex_app_server_protocol::SandboxMode;
|
|
use codex_app_server_protocol::WindowsSandboxSetupMode;
|
|
use codex_config::ConfigRequirementsToml;
|
|
use codex_config::HookEventsToml;
|
|
use codex_config::HookHandlerConfig as CoreHookHandlerConfig;
|
|
use codex_config::ManagedHooksRequirementsToml;
|
|
use codex_config::MatcherGroup as CoreMatcherGroup;
|
|
use codex_config::ResidencyRequirement as CoreResidencyRequirement;
|
|
use codex_config::SandboxModeRequirement as CoreSandboxModeRequirement;
|
|
use codex_core::ThreadManager;
|
|
use codex_features::Feature;
|
|
use codex_features::canonical_feature_for_key;
|
|
use codex_features::feature_for_key;
|
|
use codex_model_provider::create_model_provider;
|
|
use codex_plugin::PluginId;
|
|
use codex_protocol::config_types::WebSearchMode;
|
|
use serde_json::json;
|
|
use std::path::PathBuf;
|
|
|
|
const BACKGROUND_PAGINATED_ROLLOUT_MIGRATION_FEATURE: &str =
|
|
"background_paginated_rollout_migration";
|
|
|
|
const SUPPORTED_EXPERIMENTAL_FEATURE_ENABLEMENT: &[&str] = &[
|
|
"auth_elicitation",
|
|
BACKGROUND_PAGINATED_ROLLOUT_MIGRATION_FEATURE,
|
|
"mcp_2026_07_28",
|
|
"memories",
|
|
"mentions_v2",
|
|
"remote_control",
|
|
"remote_plugin",
|
|
"tool_suggest",
|
|
];
|
|
|
|
#[derive(Clone)]
|
|
pub(crate) struct ConfigRequestProcessor {
|
|
outgoing: Arc<OutgoingMessageSender>,
|
|
config_manager: ConfigManager,
|
|
thread_manager: Arc<ThreadManager>,
|
|
analytics_events_client: AnalyticsEventsClient,
|
|
}
|
|
|
|
impl ConfigRequestProcessor {
|
|
pub(crate) fn new(
|
|
outgoing: Arc<OutgoingMessageSender>,
|
|
config_manager: ConfigManager,
|
|
thread_manager: Arc<ThreadManager>,
|
|
analytics_events_client: AnalyticsEventsClient,
|
|
) -> Self {
|
|
Self {
|
|
outgoing,
|
|
config_manager,
|
|
thread_manager,
|
|
analytics_events_client,
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn read(
|
|
&self,
|
|
params: ConfigReadParams,
|
|
) -> Result<ConfigReadResponse, JSONRPCErrorError> {
|
|
let fallback_cwd = params.cwd.as_ref().map(PathBuf::from);
|
|
let mut response = self.config_manager.read(params).await.map_err(map_error)?;
|
|
let config = self.load_latest_config(fallback_cwd).await?;
|
|
for feature_key in SUPPORTED_EXPERIMENTAL_FEATURE_ENABLEMENT {
|
|
let Some(feature) = feature_for_key(feature_key) else {
|
|
continue;
|
|
};
|
|
let features = response
|
|
.config
|
|
.additional
|
|
.entry("features".to_string())
|
|
.or_insert_with(|| json!({}));
|
|
if !features.is_object() {
|
|
*features = json!({});
|
|
}
|
|
if let Some(features) = features.as_object_mut() {
|
|
features.insert(
|
|
(*feature_key).to_string(),
|
|
json!(config.features.enabled(feature)),
|
|
);
|
|
}
|
|
}
|
|
Ok(response)
|
|
}
|
|
|
|
pub(crate) async fn config_requirements_read(
|
|
&self,
|
|
) -> Result<ConfigRequirementsReadResponse, JSONRPCErrorError> {
|
|
let requirements = self
|
|
.config_manager
|
|
.read_requirements()
|
|
.await
|
|
.map_err(map_error)?
|
|
.map(map_requirements_toml_to_api);
|
|
|
|
Ok(ConfigRequirementsReadResponse { requirements })
|
|
}
|
|
|
|
pub(crate) async fn value_write(
|
|
&self,
|
|
params: ConfigValueWriteParams,
|
|
) -> Result<ClientResponsePayload, JSONRPCErrorError> {
|
|
self.handle_config_mutation_result(self.write_value(params).await)
|
|
.await
|
|
.map(ClientResponsePayload::ConfigValueWrite)
|
|
}
|
|
|
|
pub(crate) async fn batch_write(
|
|
&self,
|
|
params: ConfigBatchWriteParams,
|
|
) -> Result<ClientResponsePayload, JSONRPCErrorError> {
|
|
let session_defaults_only = !params.edits.is_empty()
|
|
&& params.edits.iter().all(|edit| {
|
|
matches!(
|
|
edit.key_path.as_str(),
|
|
"model"
|
|
| "model_reasoning_effort"
|
|
| "plan_mode_reasoning_effort"
|
|
| "service_tier"
|
|
| "personality"
|
|
)
|
|
});
|
|
let reload_user_config = params.reload_user_config;
|
|
let response = self.batch_write_inner(params).await?;
|
|
if !session_defaults_only {
|
|
self.handle_config_mutation().await;
|
|
if reload_user_config {
|
|
self.reload_user_config().await;
|
|
}
|
|
}
|
|
Ok(ClientResponsePayload::ConfigBatchWrite(response))
|
|
}
|
|
|
|
pub(crate) async fn experimental_feature_enablement_set(
|
|
&self,
|
|
request_id: ConnectionRequestId,
|
|
params: ExperimentalFeatureEnablementSetParams,
|
|
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
|
let response = self
|
|
.handle_config_mutation_result(self.set_experimental_feature_enablement(params).await)
|
|
.await?;
|
|
if !response.enablement.is_empty() {
|
|
self.reload_user_config().await;
|
|
}
|
|
self.outgoing
|
|
.send_response_as(
|
|
request_id,
|
|
ClientResponsePayload::ExperimentalFeatureEnablementSet(response),
|
|
)
|
|
.await;
|
|
Ok(None)
|
|
}
|
|
|
|
pub(crate) async fn model_provider_capabilities_read(
|
|
&self,
|
|
) -> Result<ModelProviderCapabilitiesReadResponse, JSONRPCErrorError> {
|
|
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
|
|
let provider = create_model_provider(config.model_provider, /*auth_manager*/ None);
|
|
let capabilities = provider.capabilities();
|
|
Ok(ModelProviderCapabilitiesReadResponse {
|
|
namespace_tools: capabilities.namespace_tools,
|
|
image_generation: capabilities.image_generation,
|
|
web_search: capabilities.web_search,
|
|
})
|
|
}
|
|
|
|
pub(crate) async fn handle_config_mutation(&self) {
|
|
self.thread_manager.plugins_manager().clear_cache();
|
|
self.thread_manager.skills_service().clear_cache();
|
|
}
|
|
|
|
async fn handle_config_mutation_result<T>(
|
|
&self,
|
|
result: std::result::Result<T, JSONRPCErrorError>,
|
|
) -> Result<T, JSONRPCErrorError> {
|
|
let response = result?;
|
|
self.handle_config_mutation().await;
|
|
Ok(response)
|
|
}
|
|
|
|
async fn load_latest_config(
|
|
&self,
|
|
fallback_cwd: Option<PathBuf>,
|
|
) -> Result<codex_core::config::Config, JSONRPCErrorError> {
|
|
self.config_manager
|
|
.load_latest_config(fallback_cwd)
|
|
.await
|
|
.map_err(|err| {
|
|
internal_error(format!(
|
|
"failed to resolve feature override precedence: {err}"
|
|
))
|
|
})
|
|
}
|
|
|
|
async fn write_value(
|
|
&self,
|
|
params: ConfigValueWriteParams,
|
|
) -> Result<ConfigWriteResponse, JSONRPCErrorError> {
|
|
let pending_changes = codex_core_plugins::toggles::collect_plugin_enabled_candidates(
|
|
[(¶ms.key_path, ¶ms.value)].into_iter(),
|
|
);
|
|
let response = self
|
|
.config_manager
|
|
.write_value(params)
|
|
.await
|
|
.map_err(map_error)?;
|
|
self.emit_plugin_toggle_events(pending_changes).await;
|
|
Ok(response)
|
|
}
|
|
|
|
async fn batch_write_inner(
|
|
&self,
|
|
params: ConfigBatchWriteParams,
|
|
) -> Result<ConfigWriteResponse, JSONRPCErrorError> {
|
|
let pending_changes = codex_core_plugins::toggles::collect_plugin_enabled_candidates(
|
|
params
|
|
.edits
|
|
.iter()
|
|
.map(|edit| (&edit.key_path, &edit.value)),
|
|
);
|
|
let response = self
|
|
.config_manager
|
|
.batch_write(params)
|
|
.await
|
|
.map_err(map_error)?;
|
|
self.emit_plugin_toggle_events(pending_changes).await;
|
|
Ok(response)
|
|
}
|
|
|
|
async fn set_experimental_feature_enablement(
|
|
&self,
|
|
params: ExperimentalFeatureEnablementSetParams,
|
|
) -> Result<ExperimentalFeatureEnablementSetResponse, JSONRPCErrorError> {
|
|
let ExperimentalFeatureEnablementSetParams { mut enablement } = params;
|
|
let mut invalid_keys = Vec::new();
|
|
enablement.retain(|key, _| {
|
|
let valid = canonical_feature_for_key(key).is_some()
|
|
&& SUPPORTED_EXPERIMENTAL_FEATURE_ENABLEMENT.contains(&key.as_str());
|
|
if !valid {
|
|
invalid_keys.push(key.clone());
|
|
}
|
|
valid
|
|
});
|
|
if !invalid_keys.is_empty() {
|
|
let invalid_keys = invalid_keys.join(", ");
|
|
tracing::warn!("ignoring invalid experimental feature enablement keys: {invalid_keys}");
|
|
}
|
|
|
|
if enablement.is_empty() {
|
|
return Ok(ExperimentalFeatureEnablementSetResponse { enablement });
|
|
}
|
|
|
|
// Most runtime features are read later from config. Background migration is a one-shot
|
|
// process-scoped task, so start it when runtime enablement first changes it to on.
|
|
let feature = Feature::BackgroundPaginatedRolloutMigration;
|
|
let should_start_background_rollout_migration = enablement
|
|
.get(BACKGROUND_PAGINATED_ROLLOUT_MIGRATION_FEATURE)
|
|
.is_some_and(|enabled| *enabled)
|
|
&& !self
|
|
.load_latest_config(/*fallback_cwd*/ None)
|
|
.await?
|
|
.features
|
|
.enabled(feature);
|
|
|
|
self.config_manager
|
|
.extend_runtime_feature_enablement(
|
|
enablement
|
|
.iter()
|
|
.map(|(name, enabled)| (name.clone(), *enabled)),
|
|
)
|
|
.map_err(|_| internal_error("failed to update feature enablement"))?;
|
|
|
|
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
|
|
if should_start_background_rollout_migration && config.features.enabled(feature) {
|
|
self.thread_manager.start_background_rollout_migration();
|
|
}
|
|
|
|
Ok(ExperimentalFeatureEnablementSetResponse { enablement })
|
|
}
|
|
|
|
async fn reload_user_config(&self) {
|
|
match self.load_latest_config(/*fallback_cwd*/ None).await {
|
|
Ok(_) => {}
|
|
Err(err) => {
|
|
tracing::warn!(
|
|
"failed to rebuild user config for runtime refresh: {}",
|
|
err.message
|
|
);
|
|
return;
|
|
}
|
|
};
|
|
let thread_ids = self.thread_manager.list_thread_ids().await;
|
|
for thread_id in thread_ids {
|
|
let Ok(thread) = self.thread_manager.get_thread(thread_id).await else {
|
|
continue;
|
|
};
|
|
let current_config = thread.config().await;
|
|
let next_config = match self
|
|
.config_manager
|
|
.load_latest_config_for_thread(current_config.as_ref())
|
|
.await
|
|
{
|
|
Ok(config) => config,
|
|
Err(err) => {
|
|
tracing::warn!(%thread_id, %err, "failed to reload thread configuration");
|
|
continue;
|
|
}
|
|
};
|
|
thread.refresh_runtime_config(next_config).await;
|
|
}
|
|
}
|
|
|
|
async fn emit_plugin_toggle_events(
|
|
&self,
|
|
pending_changes: std::collections::BTreeMap<String, bool>,
|
|
) {
|
|
let plugins_manager = self.thread_manager.plugins_manager();
|
|
for (plugin_id, enabled) in pending_changes {
|
|
let Ok(plugin_id) = PluginId::parse(&plugin_id) else {
|
|
continue;
|
|
};
|
|
let metadata = plugins_manager
|
|
.telemetry_metadata_for_installed_plugin(&plugin_id)
|
|
.await;
|
|
if enabled {
|
|
self.analytics_events_client.track_plugin_enabled(metadata);
|
|
} else {
|
|
self.analytics_events_client.track_plugin_disabled(metadata);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigRequirements {
|
|
let windows_sandbox_private_desktop = requirements
|
|
.windows
|
|
.as_ref()
|
|
.and_then(|windows| windows.sandbox_private_desktop);
|
|
|
|
ConfigRequirements {
|
|
cli_auth_credentials_store: requirements.cli_auth_credentials_store.map(
|
|
|mode| match mode {
|
|
codex_config::types::AuthCredentialsStoreMode::File => {
|
|
CliAuthCredentialsStoreMode::File
|
|
}
|
|
codex_config::types::AuthCredentialsStoreMode::Keyring => {
|
|
CliAuthCredentialsStoreMode::Keyring
|
|
}
|
|
codex_config::types::AuthCredentialsStoreMode::Auto => {
|
|
CliAuthCredentialsStoreMode::Auto
|
|
}
|
|
codex_config::types::AuthCredentialsStoreMode::Ephemeral => {
|
|
CliAuthCredentialsStoreMode::Ephemeral
|
|
}
|
|
},
|
|
),
|
|
chatgpt_base_url: requirements.chatgpt_base_url,
|
|
additional_developer_instructions: requirements.additional_developer_instructions,
|
|
allowed_approval_policies: requirements.allowed_approval_policies.map(|policies| {
|
|
policies
|
|
.into_iter()
|
|
.map(codex_app_server_protocol::AskForApproval::from)
|
|
.collect()
|
|
}),
|
|
allowed_approvals_reviewers: requirements.allowed_approvals_reviewers.map(|reviewers| {
|
|
reviewers
|
|
.into_iter()
|
|
.map(codex_app_server_protocol::ApprovalsReviewer::from)
|
|
.collect()
|
|
}),
|
|
allowed_sandbox_modes: requirements.allowed_sandbox_modes.map(|modes| {
|
|
modes
|
|
.into_iter()
|
|
.filter_map(map_sandbox_mode_requirement_to_api)
|
|
.collect()
|
|
}),
|
|
allowed_windows_sandbox_implementations: requirements.windows.and_then(|windows| {
|
|
windows
|
|
.allowed_sandbox_implementations
|
|
.map(|implementations| {
|
|
implementations
|
|
.into_iter()
|
|
.map(|implementation| match implementation {
|
|
codex_config::types::WindowsSandboxModeToml::Elevated => {
|
|
WindowsSandboxSetupMode::Elevated
|
|
}
|
|
codex_config::types::WindowsSandboxModeToml::Unelevated => {
|
|
WindowsSandboxSetupMode::Unelevated
|
|
}
|
|
})
|
|
.collect()
|
|
})
|
|
}),
|
|
allowed_permission_profiles: requirements.allowed_permission_profiles,
|
|
default_permissions: requirements.default_permissions,
|
|
allowed_web_search_modes: requirements.allowed_web_search_modes.map(|modes| {
|
|
let mut normalized = modes
|
|
.into_iter()
|
|
.map(Into::into)
|
|
.collect::<Vec<WebSearchMode>>();
|
|
if !normalized.contains(&WebSearchMode::Disabled) {
|
|
normalized.push(WebSearchMode::Disabled);
|
|
}
|
|
normalized
|
|
}),
|
|
allow_managed_hooks_only: requirements.allow_managed_hooks_only,
|
|
allow_browser_and_computer_use: requirements.allow_browser_and_computer_use,
|
|
allow_appshots: requirements.allow_appshots,
|
|
allow_remote_control: requirements.allow_remote_control,
|
|
computer_use: requirements
|
|
.computer_use
|
|
.map(map_computer_use_requirements_to_api),
|
|
browser_use: requirements
|
|
.browser_use
|
|
.map(map_browser_use_requirements_to_api),
|
|
in_app_browser: requirements.in_app_browser.map(|in_app_browser| {
|
|
InAppBrowserRequirements {
|
|
allow_external_browser_settings_import: in_app_browser
|
|
.allow_external_browser_settings_import,
|
|
}
|
|
}),
|
|
feature_requirements: requirements
|
|
.feature_requirements
|
|
.map(|requirements| requirements.entries),
|
|
hooks: requirements.hooks.map(map_hooks_requirements_to_api),
|
|
enforce_residency: requirements
|
|
.enforce_residency
|
|
.map(map_residency_requirement_to_api),
|
|
network: requirements.network.map(map_network_requirements_to_api),
|
|
auto_review: requirements
|
|
.auto_review
|
|
.map(|auto_review| AutoReviewRequirements {
|
|
required_on_models: auto_review.required_on_models,
|
|
ignore_rules: auto_review.ignore_rules,
|
|
}),
|
|
models: requirements.models.map(|models| ModelsRequirements {
|
|
new_thread: models.new_thread.map(|new_thread| NewThreadModelDefaults {
|
|
model: new_thread.model,
|
|
model_reasoning_effort: new_thread.model_reasoning_effort,
|
|
service_tier: new_thread.service_tier,
|
|
}),
|
|
}),
|
|
sqlite_home: requirements.sqlite_home.map(Into::into),
|
|
log_dir: requirements.log_dir.map(Into::into),
|
|
model_catalog_json: requirements.model_catalog_json.map(Into::into),
|
|
check_for_update_on_startup: requirements.check_for_update_on_startup,
|
|
allow_login_shell: requirements.allow_login_shell,
|
|
feedback: requirements.feedback.map(|feedback| FeedbackRequirements {
|
|
enabled: feedback.enabled,
|
|
}),
|
|
windows_sandbox_private_desktop,
|
|
}
|
|
}
|
|
|
|
fn map_computer_use_requirements_to_api(
|
|
computer_use: codex_config::ComputerUseRequirementsToml,
|
|
) -> ComputerUseRequirements {
|
|
ComputerUseRequirements {
|
|
allow_locked_computer_use: computer_use.allow_locked_computer_use,
|
|
allow_persistent_approval: computer_use.allow_persistent_approval,
|
|
default_app_access: computer_use
|
|
.default_app_access
|
|
.map(map_allow_deny_requirement_to_api),
|
|
macos: computer_use
|
|
.macos
|
|
.map(|macos| ComputerUseMacosRequirements {
|
|
bundle_ids: macos.bundle_ids.map(|bundle_ids| {
|
|
bundle_ids
|
|
.into_iter()
|
|
.map(|(bundle_id, requirement)| {
|
|
(bundle_id, map_allow_deny_requirement_to_api(requirement))
|
|
})
|
|
.collect()
|
|
}),
|
|
}),
|
|
windows: computer_use
|
|
.windows
|
|
.map(|windows| ComputerUseWindowsRequirements {
|
|
aumids: windows.aumids.map(|aumids| {
|
|
aumids
|
|
.into_iter()
|
|
.map(|(aumid, requirement)| {
|
|
(aumid, map_allow_deny_requirement_to_api(requirement))
|
|
})
|
|
.collect()
|
|
}),
|
|
exes: windows.exes.map(|exes| {
|
|
exes.into_iter()
|
|
.map(|exe| ComputerUseWindowsExeRequirement {
|
|
publisher_name: exe.publisher_name,
|
|
product_name: exe.product_name,
|
|
binary_name: exe.binary_name,
|
|
access: map_allow_deny_requirement_to_api(exe.access),
|
|
})
|
|
.collect()
|
|
}),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn map_browser_use_requirements_to_api(
|
|
browser_use: codex_config::BrowserUseRequirementsToml,
|
|
) -> BrowserUseRequirements {
|
|
BrowserUseRequirements {
|
|
allow_history_access: browser_use.allow_history_access,
|
|
disable_auto_review: browser_use.disable_auto_review,
|
|
allow_global_persistent_approval: browser_use.allow_global_persistent_approval,
|
|
default_origin_policy: browser_use
|
|
.default_origin_policy
|
|
.map(map_browser_use_origin_policy_to_api),
|
|
origins: browser_use.origins.map(|origins| {
|
|
origins
|
|
.into_iter()
|
|
.map(|(pattern, policy)| (pattern, map_browser_use_origin_policy_to_api(policy)))
|
|
.collect()
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn map_browser_use_origin_policy_to_api(
|
|
policy: codex_config::BrowserUseOriginPolicyToml,
|
|
) -> BrowserUseOriginPolicy {
|
|
BrowserUseOriginPolicy {
|
|
access: policy.access.map(map_allow_deny_requirement_to_api),
|
|
downloads: policy.downloads.map(map_allow_deny_requirement_to_api),
|
|
uploads: policy.uploads.map(map_allow_deny_requirement_to_api),
|
|
full_cdp_access: policy
|
|
.full_cdp_access
|
|
.map(map_allow_deny_requirement_to_api),
|
|
auto_review: policy.auto_review.map(map_allow_deny_requirement_to_api),
|
|
persistent_approval: policy.persistent_approval,
|
|
access_approval_lifetime: policy
|
|
.access_approval_lifetime
|
|
.map(map_browser_use_access_approval_lifetime_to_api),
|
|
}
|
|
}
|
|
|
|
fn map_allow_deny_requirement_to_api(
|
|
requirement: codex_config::AllowDenyRequirementToml,
|
|
) -> AllowDenyRequirement {
|
|
match requirement {
|
|
codex_config::AllowDenyRequirementToml::Allow => AllowDenyRequirement::Allow,
|
|
codex_config::AllowDenyRequirementToml::Deny => AllowDenyRequirement::Deny,
|
|
}
|
|
}
|
|
|
|
fn map_browser_use_access_approval_lifetime_to_api(
|
|
lifetime: codex_config::BrowserUseAccessApprovalLifetimeToml,
|
|
) -> BrowserUseAccessApprovalLifetime {
|
|
match lifetime {
|
|
codex_config::BrowserUseAccessApprovalLifetimeToml::Turn => {
|
|
BrowserUseAccessApprovalLifetime::Turn
|
|
}
|
|
codex_config::BrowserUseAccessApprovalLifetimeToml::Thread => {
|
|
BrowserUseAccessApprovalLifetime::Thread
|
|
}
|
|
}
|
|
}
|
|
|
|
fn map_hooks_requirements_to_api(hooks: ManagedHooksRequirementsToml) -> ManagedHooksRequirements {
|
|
let ManagedHooksRequirementsToml {
|
|
managed_dir,
|
|
windows_managed_dir,
|
|
hooks,
|
|
} = hooks;
|
|
let HookEventsToml {
|
|
pre_tool_use,
|
|
permission_request,
|
|
post_tool_use,
|
|
pre_compact,
|
|
post_compact,
|
|
session_start,
|
|
session_end,
|
|
user_prompt_submit,
|
|
subagent_start,
|
|
subagent_stop,
|
|
stop,
|
|
interrupt,
|
|
} = hooks;
|
|
|
|
ManagedHooksRequirements {
|
|
managed_dir,
|
|
windows_managed_dir,
|
|
pre_tool_use: map_hook_matcher_groups_to_api(pre_tool_use),
|
|
permission_request: map_hook_matcher_groups_to_api(permission_request),
|
|
post_tool_use: map_hook_matcher_groups_to_api(post_tool_use),
|
|
pre_compact: map_hook_matcher_groups_to_api(pre_compact),
|
|
post_compact: map_hook_matcher_groups_to_api(post_compact),
|
|
session_start: map_hook_matcher_groups_to_api(session_start),
|
|
session_end: map_hook_matcher_groups_to_api(session_end),
|
|
user_prompt_submit: map_hook_matcher_groups_to_api(user_prompt_submit),
|
|
subagent_start: map_hook_matcher_groups_to_api(subagent_start),
|
|
subagent_stop: map_hook_matcher_groups_to_api(subagent_stop),
|
|
stop: map_hook_matcher_groups_to_api(stop),
|
|
interrupt: map_hook_matcher_groups_to_api(interrupt),
|
|
}
|
|
}
|
|
|
|
fn map_hook_matcher_groups_to_api(
|
|
groups: Vec<CoreMatcherGroup>,
|
|
) -> Vec<ConfiguredHookMatcherGroup> {
|
|
groups
|
|
.into_iter()
|
|
.map(map_hook_matcher_group_to_api)
|
|
.collect()
|
|
}
|
|
|
|
fn map_hook_matcher_group_to_api(group: CoreMatcherGroup) -> ConfiguredHookMatcherGroup {
|
|
ConfiguredHookMatcherGroup {
|
|
matcher: group.matcher,
|
|
hooks: group
|
|
.hooks
|
|
.into_iter()
|
|
.map(map_hook_handler_to_api)
|
|
.collect(),
|
|
}
|
|
}
|
|
|
|
fn map_hook_handler_to_api(handler: CoreHookHandlerConfig) -> ConfiguredHookHandler {
|
|
match handler {
|
|
CoreHookHandlerConfig::Command {
|
|
command,
|
|
command_windows,
|
|
timeout_sec,
|
|
r#async,
|
|
status_message,
|
|
additional_context_limit,
|
|
} => ConfiguredHookHandler::Command {
|
|
command,
|
|
command_windows,
|
|
timeout_sec,
|
|
r#async,
|
|
status_message,
|
|
additional_context_limit,
|
|
},
|
|
CoreHookHandlerConfig::McpTool {
|
|
server,
|
|
tool,
|
|
input,
|
|
timeout_sec,
|
|
status_message,
|
|
} => ConfiguredHookHandler::McpTool {
|
|
server,
|
|
tool,
|
|
input,
|
|
timeout_sec,
|
|
status_message,
|
|
},
|
|
CoreHookHandlerConfig::Prompt {} => ConfiguredHookHandler::Prompt {},
|
|
CoreHookHandlerConfig::Agent {} => ConfiguredHookHandler::Agent {},
|
|
}
|
|
}
|
|
|
|
fn map_sandbox_mode_requirement_to_api(mode: CoreSandboxModeRequirement) -> Option<SandboxMode> {
|
|
match mode {
|
|
CoreSandboxModeRequirement::ReadOnly => Some(SandboxMode::ReadOnly),
|
|
CoreSandboxModeRequirement::WorkspaceWrite => Some(SandboxMode::WorkspaceWrite),
|
|
CoreSandboxModeRequirement::DangerFullAccess => Some(SandboxMode::DangerFullAccess),
|
|
CoreSandboxModeRequirement::ExternalSandbox => None,
|
|
}
|
|
}
|
|
|
|
fn map_residency_requirement_to_api(
|
|
residency: CoreResidencyRequirement,
|
|
) -> codex_app_server_protocol::ResidencyRequirement {
|
|
match residency {
|
|
CoreResidencyRequirement::Us => codex_app_server_protocol::ResidencyRequirement::Us,
|
|
}
|
|
}
|
|
|
|
fn map_network_requirements_to_api(
|
|
network: codex_config::NetworkRequirementsToml,
|
|
) -> NetworkRequirements {
|
|
let allowed_domains = network
|
|
.domains
|
|
.as_ref()
|
|
.and_then(codex_config::NetworkDomainPermissionsToml::allowed_domains);
|
|
let denied_domains = network
|
|
.domains
|
|
.as_ref()
|
|
.and_then(codex_config::NetworkDomainPermissionsToml::denied_domains);
|
|
let allow_unix_sockets = network
|
|
.unix_sockets
|
|
.as_ref()
|
|
.map(codex_config::NetworkUnixSocketPermissionsToml::allow_unix_sockets)
|
|
.filter(|entries| !entries.is_empty());
|
|
|
|
NetworkRequirements {
|
|
enabled: network.enabled,
|
|
http_port: network.http_port,
|
|
socks_port: network.socks_port,
|
|
allow_upstream_proxy: network.allow_upstream_proxy,
|
|
dangerously_allow_non_loopback_proxy: network.dangerously_allow_non_loopback_proxy,
|
|
dangerously_allow_all_unix_sockets: network.dangerously_allow_all_unix_sockets,
|
|
domains: network.domains.map(|domains| {
|
|
domains
|
|
.entries
|
|
.into_iter()
|
|
.map(|(pattern, permission)| {
|
|
(pattern, map_network_domain_permission_to_api(permission))
|
|
})
|
|
.collect()
|
|
}),
|
|
managed_allowed_domains_only: network.managed_allowed_domains_only,
|
|
allowed_domains,
|
|
denied_domains,
|
|
unix_sockets: network.unix_sockets.map(|unix_sockets| {
|
|
unix_sockets
|
|
.entries
|
|
.into_iter()
|
|
.map(|(path, permission)| {
|
|
(path, map_network_unix_socket_permission_to_api(permission))
|
|
})
|
|
.collect()
|
|
}),
|
|
allow_unix_sockets,
|
|
allow_local_binding: network.allow_local_binding,
|
|
}
|
|
}
|
|
|
|
fn map_network_domain_permission_to_api(
|
|
permission: codex_config::NetworkDomainPermissionToml,
|
|
) -> NetworkDomainPermission {
|
|
match permission {
|
|
codex_config::NetworkDomainPermissionToml::Allow => NetworkDomainPermission::Allow,
|
|
codex_config::NetworkDomainPermissionToml::Deny => NetworkDomainPermission::Deny,
|
|
}
|
|
}
|
|
|
|
fn map_network_unix_socket_permission_to_api(
|
|
permission: codex_config::NetworkUnixSocketPermissionToml,
|
|
) -> NetworkUnixSocketPermission {
|
|
match permission {
|
|
codex_config::NetworkUnixSocketPermissionToml::Allow => NetworkUnixSocketPermission::Allow,
|
|
codex_config::NetworkUnixSocketPermissionToml::Deny => NetworkUnixSocketPermission::Deny,
|
|
}
|
|
}
|
|
|
|
pub(super) fn map_error(err: ConfigManagerError) -> JSONRPCErrorError {
|
|
if let Some(code) = err.write_error_code() {
|
|
return config_write_error(code, err.to_string());
|
|
}
|
|
|
|
internal_error(err.to_string())
|
|
}
|
|
|
|
fn config_write_error(code: ConfigWriteErrorCode, message: impl Into<String>) -> JSONRPCErrorError {
|
|
let mut error = invalid_request(message);
|
|
error.data = Some(json!({
|
|
"config_write_error_code": code,
|
|
}));
|
|
error
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::map_requirements_toml_to_api;
|
|
use codex_app_server_protocol::AllowDenyRequirement;
|
|
use codex_app_server_protocol::AutoReviewRequirements;
|
|
use codex_app_server_protocol::BrowserUseAccessApprovalLifetime;
|
|
use codex_app_server_protocol::BrowserUseOriginPolicy;
|
|
use codex_app_server_protocol::BrowserUseRequirements;
|
|
use codex_app_server_protocol::ComputerUseMacosRequirements;
|
|
use codex_app_server_protocol::ComputerUseRequirements;
|
|
use codex_app_server_protocol::ComputerUseWindowsExeRequirement;
|
|
use codex_app_server_protocol::ComputerUseWindowsRequirements;
|
|
use codex_app_server_protocol::FeedbackRequirements;
|
|
use codex_app_server_protocol::WindowsSandboxSetupMode;
|
|
use codex_config::AllowDenyRequirementToml;
|
|
use codex_config::AutoReviewRequirementsToml;
|
|
use codex_config::BrowserUseAccessApprovalLifetimeToml;
|
|
use codex_config::BrowserUseOriginPolicyToml;
|
|
use codex_config::BrowserUseRequirementsToml;
|
|
use codex_config::ComputerUseMacosRequirementsToml;
|
|
use codex_config::ComputerUseRequirementsToml;
|
|
use codex_config::ComputerUseWindowsExeRequirementToml;
|
|
use codex_config::ComputerUseWindowsRequirementsToml;
|
|
use codex_config::ConfigRequirementsToml;
|
|
use codex_config::ModelsRequirementsToml;
|
|
use codex_config::NewThreadModelDefaultsToml;
|
|
use codex_config::WindowsRequirementsToml;
|
|
use codex_config::types::FeedbackConfigToml;
|
|
use codex_protocol::openai_models::ReasoningEffort;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use codex_utils_path_uri::PathUri;
|
|
use pretty_assertions::assert_eq;
|
|
use std::collections::BTreeMap;
|
|
|
|
#[test]
|
|
fn requirements_api_includes_allow_managed_hooks_only() {
|
|
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
|
|
allow_managed_hooks_only: Some(true),
|
|
..ConfigRequirementsToml::default()
|
|
});
|
|
|
|
assert_eq!(mapped.allow_managed_hooks_only, Some(true));
|
|
assert_eq!(mapped.hooks, None);
|
|
}
|
|
|
|
#[test]
|
|
fn requirements_api_includes_permission_default_and_allowlist() {
|
|
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
|
|
allowed_permission_profiles: Some(BTreeMap::from([
|
|
("managed-build".to_string(), false),
|
|
("managed-standard".to_string(), true),
|
|
])),
|
|
default_permissions: Some("managed-standard".to_string()),
|
|
..ConfigRequirementsToml::default()
|
|
});
|
|
|
|
assert_eq!(
|
|
mapped.allowed_permission_profiles,
|
|
Some(BTreeMap::from([
|
|
("managed-build".to_string(), false),
|
|
("managed-standard".to_string(), true),
|
|
]))
|
|
);
|
|
assert_eq!(
|
|
mapped.default_permissions,
|
|
Some("managed-standard".to_string())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn requirements_api_includes_allow_appshots() {
|
|
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
|
|
allow_appshots: Some(false),
|
|
..ConfigRequirementsToml::default()
|
|
});
|
|
|
|
assert_eq!(mapped.allow_appshots, Some(false));
|
|
assert_eq!(mapped.hooks, None);
|
|
}
|
|
|
|
#[test]
|
|
fn requirements_api_includes_allow_remote_control() {
|
|
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
|
|
allow_remote_control: Some(false),
|
|
..ConfigRequirementsToml::default()
|
|
});
|
|
|
|
assert_eq!(mapped.allow_remote_control, Some(false));
|
|
}
|
|
|
|
#[test]
|
|
fn requirements_api_includes_model_auto_review_and_new_thread_defaults() {
|
|
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
|
|
auto_review: Some(AutoReviewRequirementsToml {
|
|
required_on_models: Some(vec!["gpt-protected".to_string()]),
|
|
ignore_rules: Some(vec!["gpt-protected".to_string()]),
|
|
}),
|
|
models: Some(ModelsRequirementsToml {
|
|
new_thread: Some(NewThreadModelDefaultsToml {
|
|
model: Some("gpt-managed".to_string()),
|
|
model_reasoning_effort: Some(ReasoningEffort::Medium),
|
|
service_tier: Some("fast".to_string()),
|
|
}),
|
|
}),
|
|
..ConfigRequirementsToml::default()
|
|
});
|
|
|
|
assert_eq!(
|
|
mapped.auto_review,
|
|
Some(AutoReviewRequirements {
|
|
required_on_models: Some(vec!["gpt-protected".to_string()]),
|
|
ignore_rules: Some(vec!["gpt-protected".to_string()]),
|
|
})
|
|
);
|
|
let models = mapped.models.expect("managed model requirements");
|
|
let defaults = models.new_thread.expect("new-thread defaults");
|
|
assert_eq!(defaults.model.as_deref(), Some("gpt-managed"));
|
|
assert_eq!(
|
|
defaults.model_reasoning_effort,
|
|
Some(ReasoningEffort::Medium)
|
|
);
|
|
assert_eq!(defaults.service_tier.as_deref(), Some("fast"));
|
|
}
|
|
|
|
#[test]
|
|
fn requirements_api_includes_browser_and_computer_use_requirements() {
|
|
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
|
|
allow_browser_and_computer_use: Some(false),
|
|
browser_use: Some(BrowserUseRequirementsToml {
|
|
allow_history_access: Some(false),
|
|
disable_auto_review: Some(true),
|
|
allow_global_persistent_approval: Some(false),
|
|
default_origin_policy: Some(BrowserUseOriginPolicyToml {
|
|
access: Some(AllowDenyRequirementToml::Deny),
|
|
downloads: Some(AllowDenyRequirementToml::Allow),
|
|
uploads: Some(AllowDenyRequirementToml::Deny),
|
|
full_cdp_access: Some(AllowDenyRequirementToml::Allow),
|
|
auto_review: Some(AllowDenyRequirementToml::Deny),
|
|
persistent_approval: Some(false),
|
|
access_approval_lifetime: Some(BrowserUseAccessApprovalLifetimeToml::Turn),
|
|
}),
|
|
origins: Some(BTreeMap::from([(
|
|
"https://example.com".to_string(),
|
|
BrowserUseOriginPolicyToml {
|
|
access: Some(AllowDenyRequirementToml::Allow),
|
|
downloads: Some(AllowDenyRequirementToml::Deny),
|
|
uploads: Some(AllowDenyRequirementToml::Allow),
|
|
full_cdp_access: Some(AllowDenyRequirementToml::Deny),
|
|
auto_review: Some(AllowDenyRequirementToml::Deny),
|
|
persistent_approval: Some(true),
|
|
access_approval_lifetime: Some(
|
|
BrowserUseAccessApprovalLifetimeToml::Thread,
|
|
),
|
|
},
|
|
)])),
|
|
}),
|
|
computer_use: Some(ComputerUseRequirementsToml {
|
|
allow_locked_computer_use: Some(false),
|
|
allow_persistent_approval: Some(false),
|
|
default_app_access: Some(AllowDenyRequirementToml::Deny),
|
|
macos: Some(ComputerUseMacosRequirementsToml {
|
|
bundle_ids: Some(BTreeMap::from([(
|
|
"com.apple.Safari".to_string(),
|
|
AllowDenyRequirementToml::Allow,
|
|
)])),
|
|
}),
|
|
windows: Some(ComputerUseWindowsRequirementsToml {
|
|
aumids: Some(BTreeMap::from([(
|
|
"Microsoft.Paint_8wekyb3d8bbwe!App".to_string(),
|
|
AllowDenyRequirementToml::Allow,
|
|
)])),
|
|
exes: Some(vec![ComputerUseWindowsExeRequirementToml {
|
|
publisher_name: "CN=Google LLC".to_string(),
|
|
product_name: "Google Chrome".to_string(),
|
|
binary_name: Some("chrome.exe".to_string()),
|
|
access: AllowDenyRequirementToml::Deny,
|
|
}]),
|
|
}),
|
|
}),
|
|
..ConfigRequirementsToml::default()
|
|
});
|
|
|
|
assert_eq!(mapped.allow_browser_and_computer_use, Some(false));
|
|
assert_eq!(
|
|
mapped.browser_use,
|
|
Some(BrowserUseRequirements {
|
|
allow_history_access: Some(false),
|
|
disable_auto_review: Some(true),
|
|
allow_global_persistent_approval: Some(false),
|
|
default_origin_policy: Some(BrowserUseOriginPolicy {
|
|
access: Some(AllowDenyRequirement::Deny),
|
|
downloads: Some(AllowDenyRequirement::Allow),
|
|
uploads: Some(AllowDenyRequirement::Deny),
|
|
full_cdp_access: Some(AllowDenyRequirement::Allow),
|
|
auto_review: Some(AllowDenyRequirement::Deny),
|
|
persistent_approval: Some(false),
|
|
access_approval_lifetime: Some(BrowserUseAccessApprovalLifetime::Turn),
|
|
}),
|
|
origins: Some(BTreeMap::from([(
|
|
"https://example.com".to_string(),
|
|
BrowserUseOriginPolicy {
|
|
access: Some(AllowDenyRequirement::Allow),
|
|
downloads: Some(AllowDenyRequirement::Deny),
|
|
uploads: Some(AllowDenyRequirement::Allow),
|
|
full_cdp_access: Some(AllowDenyRequirement::Deny),
|
|
auto_review: Some(AllowDenyRequirement::Deny),
|
|
persistent_approval: Some(true),
|
|
access_approval_lifetime: Some(BrowserUseAccessApprovalLifetime::Thread),
|
|
},
|
|
)])),
|
|
})
|
|
);
|
|
assert_eq!(
|
|
mapped.computer_use,
|
|
Some(ComputerUseRequirements {
|
|
allow_locked_computer_use: Some(false),
|
|
allow_persistent_approval: Some(false),
|
|
default_app_access: Some(AllowDenyRequirement::Deny),
|
|
macos: Some(ComputerUseMacosRequirements {
|
|
bundle_ids: Some(BTreeMap::from([(
|
|
"com.apple.Safari".to_string(),
|
|
AllowDenyRequirement::Allow,
|
|
)])),
|
|
}),
|
|
windows: Some(ComputerUseWindowsRequirements {
|
|
aumids: Some(BTreeMap::from([(
|
|
"Microsoft.Paint_8wekyb3d8bbwe!App".to_string(),
|
|
AllowDenyRequirement::Allow,
|
|
)])),
|
|
exes: Some(vec![ComputerUseWindowsExeRequirement {
|
|
publisher_name: "CN=Google LLC".to_string(),
|
|
product_name: "Google Chrome".to_string(),
|
|
binary_name: Some("chrome.exe".to_string()),
|
|
access: AllowDenyRequirement::Deny,
|
|
}]),
|
|
}),
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn requirements_api_includes_allowed_windows_sandbox_implementations() {
|
|
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
|
|
windows: Some(WindowsRequirementsToml {
|
|
allowed_sandbox_implementations: Some(vec![
|
|
codex_config::types::WindowsSandboxModeToml::Elevated,
|
|
codex_config::types::WindowsSandboxModeToml::Unelevated,
|
|
]),
|
|
sandbox_private_desktop: Some(false),
|
|
}),
|
|
..ConfigRequirementsToml::default()
|
|
});
|
|
|
|
assert_eq!(
|
|
mapped.allowed_windows_sandbox_implementations,
|
|
Some(vec![
|
|
WindowsSandboxSetupMode::Elevated,
|
|
WindowsSandboxSetupMode::Unelevated,
|
|
])
|
|
);
|
|
assert_eq!(mapped.windows_sandbox_private_desktop, Some(false));
|
|
}
|
|
|
|
#[test]
|
|
fn requirements_api_includes_exact_managed_values() {
|
|
let sqlite_home = AbsolutePathBuf::try_from(std::env::temp_dir().join("managed-state"))
|
|
.expect("managed sqlite home should be absolute");
|
|
let log_dir = AbsolutePathBuf::try_from(std::env::temp_dir().join("managed-logs"))
|
|
.expect("managed log dir should be absolute");
|
|
let model_catalog_json =
|
|
AbsolutePathBuf::try_from(std::env::temp_dir().join("managed-models.json"))
|
|
.expect("managed model catalog path should be absolute");
|
|
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
|
|
sqlite_home: Some(sqlite_home.clone()),
|
|
log_dir: Some(log_dir.clone()),
|
|
model_catalog_json: Some(model_catalog_json.clone()),
|
|
check_for_update_on_startup: Some(false),
|
|
allow_login_shell: Some(false),
|
|
feedback: Some(FeedbackConfigToml {
|
|
enabled: Some(false),
|
|
}),
|
|
..ConfigRequirementsToml::default()
|
|
});
|
|
|
|
assert_eq!(mapped.sqlite_home, Some(PathUri::from(sqlite_home)));
|
|
assert_eq!(mapped.log_dir, Some(PathUri::from(log_dir)));
|
|
assert_eq!(
|
|
mapped.model_catalog_json,
|
|
Some(PathUri::from(model_catalog_json))
|
|
);
|
|
assert_eq!(mapped.check_for_update_on_startup, Some(false));
|
|
assert_eq!(mapped.allow_login_shell, Some(false));
|
|
assert_eq!(
|
|
mapped.feedback,
|
|
Some(FeedbackRequirements {
|
|
enabled: Some(false),
|
|
})
|
|
);
|
|
}
|
|
}
|