mirror of
https://github.com/openai/codex.git
synced 2026-08-23 13:09:46 +00:00
[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:
@@ -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()?;
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user