[codex] Add managed in-app updates feature (#2463)

## Summary

- register the requirements-only `in_app_updates` feature as stable and
default-enabled
- prove that managed `[features] in_app_updates = false` requirements
disable the canonical feature
- prove that `configRequirements/read` preserves and emits the managed
value
- regenerate the canonical config schema fixture for the registered
feature
- document the app-server feature requirement

The existing managed requirements parser, source precedence, canonical
enforcement, and app-server mapping remain unchanged. No generated
protocol files are hand-edited.

Closes [CXA-4250](https://linear.app/openai/issue/CXA-4250).

Upstream dependency for
[openai/openai#1166248](https://github.com/openai/openai/pull/1166248),
the admin-policy child stacked on
[openai/openai#1163708](https://github.com/openai/openai/pull/1163708).

## Test plan

- `just test -p codex-core system_requirements_control_in_app_updates`
  - intentional registry-key flaw: `0 passed, 1 failed, 3029 skipped`
  - restored production code: `1 passed, 3029 skipped`
- `just test -p codex-app-server
config_requirements_read_includes_in_app_updates_policy`
- intentional transport-mapping flaw: `0 passed, 1 failed, 982 skipped`
  - restored production code: `1 passed, 982 skipped`
- `just test -p codex-core config_schema_matches_fixture`
  - missing generated fixture: `0 passed, 1 failed, 3029 skipped`
  - regenerated fixture: `1 passed, 3029 skipped`
- `bazel test //codex-rs/core:core-unit-tests
--test_filter=config::schema::tests::config_schema_matches_fixture
--test_output=errors`
  - `1 test passes`
- `just fmt`
- `git diff --check`
GitOrigin-RevId: 7e7ed3c3b248dc828f743ccd98208ab987735765
This commit is contained in:
zm-oai
2026-07-26 19:11:54 -07:00
committed by Adam Perry
parent a453588416
commit 7d8f840fee
5 changed files with 88 additions and 1 deletions

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

View File

@@ -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()?;

View File

@@ -560,6 +560,9 @@
"in_app_browser": {
"type": "boolean"
},
"in_app_updates": {
"type": "boolean"
},
"item_ids": {
"type": "boolean"
},
@@ -5100,6 +5103,9 @@
"in_app_browser": {
"type": "boolean"
},
"in_app_updates": {
"type": "boolean"
},
"item_ids": {
"type": "boolean"
},

View File

@@ -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<()> {

View File

@@ -185,6 +185,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.
@@ -1172,6 +1176,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",