diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 4f1f6886a3c2..81406c1aaf2a 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -250,7 +250,7 @@ Example with notification opt-out: - `externalAgentConfig/import/readHistories` — read completed import histories and connector candidates detected from successfully imported session histories. Connector candidates include a normalized display `name`, the number of imported sessions that used the connector, and the source metadata field used for detection. - `config/value/write` — write a single config key/value to the user's config.toml on disk; dotted paths such as `desktop.someKey` use the same generic write surface. Writes that overlap a managed requirement are rejected with `configRequirementReadonly`. - `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads, including multiple `desktop.*` edits. Session-static model, reasoning-effort, Plan-mode reasoning-effort, service-tier, and personality defaults do not reload existing threads. -- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including exact managed values (`sqliteHome`, `logDir`, `modelCatalogJson`, `checkForUpdateOnStartup`, `allowLoginShell`, `feedback.enabled`, and `windowsSandboxPrivateDesktop`), allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), remote-control policy (`allowRemoteControl`; `false` force-disables remote control while `true` or `null` preserves existing behavior), computer use policy (`computerUse`), Browser Use policy (`browserUse.disableAutoReview`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`, including each command handler's optional `additionalContextLimit`), `enforceResidency`, managed new-thread defaults (`models.newThread.model`, `models.newThread.modelReasoningEffort`, and `models.newThread.serviceTier`), and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`. +- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including exact managed values (`sqliteHome`, `logDir`, `modelCatalogJson`, `checkForUpdateOnStartup`, `allowLoginShell`, `feedback.enabled`, and `windowsSandboxPrivateDesktop`), allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), remote-control policy (`allowRemoteControl`; `false` force-disables remote control while `true` or `null` preserves existing behavior), computer use policy (`computerUse`), Browser Use policy (`browserUse.disableAutoReview`), pinned feature values (`featureRequirements`, including the default-allowed `in_app_updates` policy that administrators can set to `false`), managed lifecycle hooks (`hooks`, including each command handler's optional `additionalContextLimit`), `enforceResidency`, managed new-thread defaults (`models.newThread.model`, `models.newThread.modelReasoningEffort`, and `models.newThread.serviceTier`), and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`. ### Example: Start or resume a thread diff --git a/codex-rs/app-server/tests/suite/v2/config_rpc.rs b/codex-rs/app-server/tests/suite/v2/config_rpc.rs index 3f6c1c575d96..cd95082905e5 100644 --- a/codex-rs/app-server/tests/suite/v2/config_rpc.rs +++ b/codex-rs/app-server/tests/suite/v2/config_rpc.rs @@ -125,6 +125,39 @@ disable_auto_review = true Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn config_requirements_read_includes_in_app_updates_policy() -> Result<()> { + let codex_home = TempDir::new()?; + std::fs::write( + codex_home.path().join("requirements.toml"), + r#" +[features] +in_app_updates = false +"#, + )?; + let mut mcp = TestAppServer::builder() + .with_codex_home(codex_home.path()) + .without_auto_env() + .build() + .await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp.send_config_requirements_read_request().await?; + let response: ConfigRequirementsReadResponse = + timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(request_id)).await??; + + assert_eq!( + response + .requirements + .and_then(|requirements| requirements.feature_requirements), + Some(std::collections::BTreeMap::from([( + "in_app_updates".to_string(), + false, + )])) + ); + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn config_requirements_read_includes_new_thread_model_defaults() -> Result<()> { let codex_home = TempDir::new()?; diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 0e42e3e9aadf..f3fb1b36c631 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -573,6 +573,9 @@ "in_app_browser": { "type": "boolean" }, + "in_app_updates": { + "type": "boolean" + }, "item_ids": { "type": "boolean" }, @@ -5123,6 +5126,9 @@ "in_app_browser": { "type": "boolean" }, + "in_app_updates": { + "type": "boolean" + }, "item_ids": { "type": "boolean" }, diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index 5b5622f14ee6..f2cabb753874 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -32,6 +32,7 @@ use codex_config::loader::load_config_layers_state; use codex_config::loader::load_requirements_toml; use codex_config::test_support::CloudConfigBundleFixture; use codex_exec_server::LOCAL_FS; +use codex_features::Feature; use codex_protocol::config_types::EnvironmentVariablePattern; use codex_protocol::config_types::TrustLevel; use codex_protocol::config_types::WebSearchMode; @@ -1350,6 +1351,43 @@ personality = true Ok(()) } +#[tokio::test] +async fn system_requirements_control_in_app_updates() -> anyhow::Result<()> { + let tmp = tempdir()?; + let codex_home = tmp.path().join("home"); + tokio::fs::create_dir_all(&codex_home).await?; + let cwd = AbsolutePathBuf::from_absolute_path(tmp.path())?; + + let default_config = ConfigBuilder::default() + .codex_home(codex_home.clone()) + .fallback_cwd(Some(cwd.to_path_buf())) + .loader_overrides(LoaderOverrides::without_managed_config_for_tests()) + .build() + .await?; + assert!(default_config.features.enabled(Feature::InAppUpdates)); + + let requirements_path = tmp.path().join("requirements.toml"); + tokio::fs::write( + &requirements_path, + r#" +[features] +in_app_updates = false +"#, + ) + .await?; + let mut overrides = LoaderOverrides::without_managed_config_for_tests(); + overrides.system_requirements_path = Some(requirements_path); + let managed_config = ConfigBuilder::default() + .codex_home(codex_home) + .fallback_cwd(Some(cwd.to_path_buf())) + .loader_overrides(overrides) + .build() + .await?; + + assert!(!managed_config.features.enabled(Feature::InAppUpdates)); + Ok(()) +} + #[cfg(target_os = "macos")] #[tokio::test] async fn mdm_requirements_take_precedence_over_cloud_config_bundle() -> anyhow::Result<()> { diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index 000f2c4bf428..8713496eed22 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -186,6 +186,10 @@ pub enum Feature { /// /// Requirements-only gate: this should be set from requirements, not user config. InAppBrowser, + /// Allow desktop apps to perform in-app updates. + /// + /// Requirements-only gate: this should be set from requirements, not user config. + InAppUpdates, /// Allow Browser Use agent integration in desktop apps. /// /// Requirements-only gate: this should be set from requirements, not user config. @@ -1181,6 +1185,12 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::Stable, default_enabled: true, }, + FeatureSpec { + id: Feature::InAppUpdates, + key: "in_app_updates", + stage: Stage::Stable, + default_enabled: true, + }, FeatureSpec { id: Feature::BrowserUse, key: "browser_use",