Add managed policy for in-app updates (#35537)

## What changed

- Add the stable, default-enabled `in_app_updates` requirements-only feature.
- Allow administrators to disable in-app updates through `[features]` in `requirements.toml`.
- Expose the policy through `configRequirements/read` and include it in the config schema.

## Testing

- Verify that in-app updates are enabled by default and disabled by the managed requirement.
- Verify that `configRequirements/read` returns the configured policy.

GitOrigin-RevId: e9c7074b516fe58385fdf7dc8420c0109c0c3df6
This commit is contained in:
zm-oai
2026-07-27 02:16:11 +00:00
committed by copyberry
parent 18f50c9e62
commit 95637f7056
5 changed files with 88 additions and 1 deletions
+1 -1
View File
@@ -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
@@ -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()?;
+6
View File
@@ -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"
},
@@ -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<()> {
+10
View File
@@ -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",