Expose managed policy for browser settings imports (#39720)

## What changed

- Add `in_app_browser.allow_external_browser_settings_import` to managed requirements, preserving explicit Boolean values through layered composition while leaving an omitted value unset.
- Return the policy as `inAppBrowser.allowExternalBrowserSettingsImport` from `configRequirements/read` and include it in the generated protocol schemas.
- Keep the import policy independent from the in-app browser feature flag and agent Browser Use requirements.

## Testing

- Cover parsing, managed-layer precedence, invalid values, user and session override resistance, and app-server response serialization.

GitOrigin-RevId: efa2621d2b1cf503f1bee2505d9914cb4fb7221d
This commit is contained in:
rafael-oai
2026-08-20 14:13:56 +00:00
committed by copyberry
parent 6d020311f0
commit 1674b0a130
22 changed files with 454 additions and 2 deletions
@@ -9064,6 +9064,16 @@
}
]
},
"inAppBrowser": {
"anyOf": [
{
"$ref": "#/definitions/v2/InAppBrowserRequirements"
},
{
"type": "null"
}
]
},
"logDir": {
"type": [
"string",
@@ -12419,6 +12429,17 @@
}
]
},
"InAppBrowserRequirements": {
"properties": {
"allowExternalBrowserSettingsImport": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"InputModality": {
"description": "Canonical user-input modality tags advertised by a model.",
"oneOf": [
@@ -5122,6 +5122,16 @@
}
]
},
"inAppBrowser": {
"anyOf": [
{
"$ref": "#/definitions/InAppBrowserRequirements"
},
{
"type": "null"
}
]
},
"logDir": {
"type": [
"string",
@@ -8588,6 +8598,17 @@
}
]
},
"InAppBrowserRequirements": {
"properties": {
"allowExternalBrowserSettingsImport": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"InitializeCapabilities": {
"description": "Client-declared capabilities negotiated during initialize.",
"properties": {
@@ -271,6 +271,16 @@
}
]
},
"inAppBrowser": {
"anyOf": [
{
"$ref": "#/definitions/InAppBrowserRequirements"
},
{
"type": "null"
}
]
},
"logDir": {
"type": [
"string",
@@ -471,6 +481,17 @@
},
"type": "object"
},
"InAppBrowserRequirements": {
"properties": {
"allowExternalBrowserSettingsImport": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"ManagedHooksRequirements": {
"properties": {
"PermissionRequest": {
@@ -9,9 +9,10 @@ import type { BrowserUseRequirements } from "./BrowserUseRequirements";
import type { CliAuthCredentialsStoreMode } from "./CliAuthCredentialsStoreMode";
import type { ComputerUseRequirements } from "./ComputerUseRequirements";
import type { FeedbackRequirements } from "./FeedbackRequirements";
import type { InAppBrowserRequirements } from "./InAppBrowserRequirements";
import type { ModelsRequirements } from "./ModelsRequirements";
import type { ResidencyRequirement } from "./ResidencyRequirement";
import type { SandboxMode } from "./SandboxMode";
import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode";
export type ConfigRequirements = {cliAuthCredentialsStore: CliAuthCredentialsStoreMode | null, chatgptBaseUrl: string | null, allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWindowsSandboxImplementations: Array<WindowsSandboxSetupMode> | null, allowedPermissionProfiles: { [key in string]?: boolean } | null, defaultPermissions: string | null, allowedWebSearchModes: Array<WebSearchMode> | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, allowRemoteControl: boolean | null, computerUse: ComputerUseRequirements | null, browserUse: BrowserUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null, autoReview: AutoReviewRequirements | null, models: ModelsRequirements | null, sqliteHome: PathUri | null, logDir: PathUri | null, modelCatalogJson: PathUri | null, checkForUpdateOnStartup: boolean | null, allowLoginShell: boolean | null, feedback: FeedbackRequirements | null, windowsSandboxPrivateDesktop: boolean | null};
export type ConfigRequirements = {cliAuthCredentialsStore: CliAuthCredentialsStoreMode | null, chatgptBaseUrl: string | null, allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWindowsSandboxImplementations: Array<WindowsSandboxSetupMode> | null, allowedPermissionProfiles: { [key in string]?: boolean } | null, defaultPermissions: string | null, allowedWebSearchModes: Array<WebSearchMode> | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, allowRemoteControl: boolean | null, computerUse: ComputerUseRequirements | null, browserUse: BrowserUseRequirements | null, inAppBrowser: InAppBrowserRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null, autoReview: AutoReviewRequirements | null, models: ModelsRequirements | null, sqliteHome: PathUri | null, logDir: PathUri | null, modelCatalogJson: PathUri | null, checkForUpdateOnStartup: boolean | null, allowLoginShell: boolean | null, feedback: FeedbackRequirements | null, windowsSandboxPrivateDesktop: boolean | null};
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type InAppBrowserRequirements = { allowExternalBrowserSettingsImport: boolean | null, };
@@ -208,6 +208,7 @@ export type { HookTrustStatus } from "./HookTrustStatus";
export type { HooksListEntry } from "./HooksListEntry";
export type { HooksListParams } from "./HooksListParams";
export type { HooksListResponse } from "./HooksListResponse";
export type { InAppBrowserRequirements } from "./InAppBrowserRequirements";
export type { InstalledApp } from "./InstalledApp";
export type { ItemCompletedNotification } from "./ItemCompletedNotification";
export type { ItemGuardianApprovalReviewCompletedNotification } from "./ItemGuardianApprovalReviewCompletedNotification";
@@ -399,6 +399,7 @@ pub struct ConfigRequirements {
pub allow_remote_control: Option<bool>,
pub computer_use: Option<ComputerUseRequirements>,
pub browser_use: Option<BrowserUseRequirements>,
pub in_app_browser: Option<InAppBrowserRequirements>,
pub feature_requirements: Option<BTreeMap<String, bool>>,
#[experimental("configRequirements/read.hooks")]
pub hooks: Option<ManagedHooksRequirements>,
@@ -474,6 +475,13 @@ pub struct BrowserUseRequirements {
pub disable_auto_review: Option<bool>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct InAppBrowserRequirements {
pub allow_external_browser_settings_import: Option<bool>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -2060,6 +2060,7 @@ fn config_requirements_granular_allowed_approval_policy_is_marked_experimental()
allow_remote_control: None,
computer_use: None,
browser_use: None,
in_app_browser: None,
feature_requirements: None,
hooks: None,
enforce_residency: None,
+1 -1
View File
@@ -288,7 +288,7 @@ Example with notification opt-out:
- `externalAgentConfig/import/readHistories` — read completed import histories and connector candidates detected from successfully imported session histories. Successful session entries include the original imported title when one was available. 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 (`cliAuthCredentialsStore`, `chatgptBaseUrl`, `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 command handlers with optional `additionalContextLimit` and `mcp_tool` handlers with `server`, `tool`, `input`, `timeoutSec`, and `statusMessage`), `enforceResidency`, managed automatic review (`autoReview.requiredOnModels` and `autoReview.ignoreRules`), model 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 (`cliAuthCredentialsStore`, `chatgptBaseUrl`, `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`), interactive browser import policy (`inAppBrowser.allowExternalBrowserSettingsImport`), pinned feature values (`featureRequirements`, including the default-allowed `in_app_updates` policy that administrators can set to `false`), managed lifecycle hooks (`hooks`, including command handlers with optional `additionalContextLimit` and `mcp_tool` handlers with `server`, `tool`, `input`, `timeoutSec`, and `statusMessage`), `enforceResidency`, managed automatic review (`autoReview.requiredOnModels` and `autoReview.ignoreRules`), model 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
@@ -25,6 +25,7 @@ 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;
@@ -426,6 +427,12 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR
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),
@@ -0,0 +1,143 @@
use anyhow::Result;
use anyhow::ensure;
use app_test_support::TestAppServer;
use codex_app_server_protocol::BrowserUseRequirements;
use codex_app_server_protocol::ClientInfo;
use codex_app_server_protocol::ConfigRequirementsReadResponse;
use codex_app_server_protocol::InAppBrowserRequirements;
use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::RequestId;
use pretty_assertions::assert_eq;
use serde_json::Value;
use serde_json::json;
use std::collections::BTreeMap;
use std::time::Duration;
use tempfile::TempDir;
use tokio::time::timeout;
const READ_TIMEOUT: Duration = Duration::from_secs(/*secs*/ 60);
const ALLOW: &str = "[in_app_browser]\nallow_external_browser_settings_import = true";
const DENY: &str = "[in_app_browser]\nallow_external_browser_settings_import = false";
async fn start_stable_server(codex_home: &TempDir) -> Result<TestAppServer> {
let mut server = TestAppServer::builder()
.with_codex_home(codex_home.path())
.build()
.await?;
let initialized = timeout(
READ_TIMEOUT,
server.initialize_with_capabilities(
ClientInfo {
name: "browser_import_policy_test".to_string(),
title: None,
version: "0.1.0".to_string(),
},
/*capabilities*/ None,
),
)
.await??;
ensure!(matches!(initialized, JSONRPCMessage::Response(_)));
Ok(server)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_requirements_read_preserves_import_policy() -> Result<()> {
for (contents, expected_table) in [
("", None),
("[in_app_browser]", None),
("allow_remote_control = true", None),
("allow_remote_control = true\n[in_app_browser]", Some(None)),
(ALLOW, Some(Some(true))),
(DENY, Some(Some(false))),
] {
let codex_home = TempDir::new()?;
std::fs::write(codex_home.path().join("requirements.toml"), contents)?;
// An unrecognized ordinary user setting cannot override managed policy.
std::fs::write(codex_home.path().join("config.toml"), ALLOW)?;
let mut server = start_stable_server(&codex_home).await?;
let request_id = server.send_config_requirements_read_request().await?;
let wire: Value = timeout(READ_TIMEOUT, server.read_response(request_id)).await??;
let expected_wire = expected_table
.map(|value| json!({ "allowExternalBrowserSettingsImport": value }))
.unwrap_or(Value::Null);
if let Some(requirements) = wire["requirements"].as_object() {
assert_eq!(requirements.get("inAppBrowser"), Some(&expected_wire));
} else {
assert_eq!(wire, json!({ "requirements": null }));
}
let response: ConfigRequirementsReadResponse = serde_json::from_value(wire)?;
let actual = response
.requirements
.and_then(|requirements| requirements.in_app_browser);
assert_eq!(
actual,
expected_table.map(|value| InAppBrowserRequirements {
allow_external_browser_settings_import: value,
}),
);
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn import_policy_is_separate_from_browser_feature_and_agent_policy() -> Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join("requirements.toml"),
format!(
"[features]\nin_app_browser = true\nbrowser_use = false\n\
[browser_use]\ndisable_auto_review = true\n{DENY}"
),
)?;
std::fs::write(codex_home.path().join("config.toml"), ALLOW)?;
let mut server = start_stable_server(&codex_home).await?;
let request_id = server.send_config_requirements_read_request().await?;
let response: ConfigRequirementsReadResponse =
timeout(READ_TIMEOUT, server.read_response(request_id)).await??;
let requirements = response.requirements.expect("managed requirements");
assert_eq!(
(
requirements.in_app_browser,
requirements.browser_use,
requirements.feature_requirements,
),
(
Some(InAppBrowserRequirements {
allow_external_browser_settings_import: Some(false),
}),
Some(BrowserUseRequirements {
disable_auto_review: Some(true),
}),
Some(BTreeMap::from([
("browser_use".to_string(), false),
("in_app_browser".to_string(), true),
])),
),
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_requirements_read_rejects_invalid_import_policy() -> Result<()> {
let codex_home = TempDir::new()?;
let mut server = start_stable_server(&codex_home).await?;
std::fs::write(
codex_home.path().join("requirements.toml"),
"[in_app_browser]\nallow_external_browser_settings_import = \"false\"",
)?;
let request_id = server.send_config_requirements_read_request().await?;
let error = timeout(
READ_TIMEOUT,
server.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
ensure!(
error
.error
.message
.contains("allow_external_browser_settings_import"),
"{}",
error.error.message,
);
Ok(())
}
@@ -12,6 +12,7 @@ mod collaboration_mode_list;
#[cfg(unix)]
mod command_exec;
mod compaction;
mod config_requirements_in_app_browser;
mod config_rpc;
mod connection_handling_websocket;
#[cfg(unix)]
@@ -21,6 +21,7 @@ use wildmatch::WildMatchPattern;
use super::requirements_exec_policy::RequirementsExecPolicyToml;
use crate::Constrained;
use crate::ConstraintError;
use crate::InAppBrowserRequirementsToml;
use crate::ManagedAuthPolicy;
use crate::ManagedHooksRequirementsToml;
use crate::McpServerRequirement;
@@ -942,6 +943,7 @@ pub struct ConfigRequirementsToml {
pub allow_remote_control: Option<bool>,
pub computer_use: Option<ComputerUseRequirementsToml>,
pub browser_use: Option<BrowserUseRequirementsToml>,
pub in_app_browser: Option<InAppBrowserRequirementsToml>,
pub windows: Option<WindowsRequirementsToml>,
#[serde(rename = "features", alias = "feature_requirements")]
pub feature_requirements: Option<FeatureRequirementsToml>,
@@ -1043,6 +1045,7 @@ pub struct ConfigRequirementsWithSources {
pub allow_remote_control: Option<Sourced<bool>>,
pub computer_use: Option<Sourced<ComputerUseRequirementsToml>>,
pub browser_use: Option<Sourced<BrowserUseRequirementsToml>>,
pub in_app_browser: Option<Sourced<InAppBrowserRequirementsToml>>,
pub windows: Option<Sourced<WindowsRequirementsToml>>,
pub feature_requirements: Option<Sourced<FeatureRequirementsToml>>,
pub hooks: Option<Sourced<ManagedHooksRequirementsToml>>,
@@ -1100,6 +1103,7 @@ impl ConfigRequirementsWithSources {
allow_remote_control: _,
computer_use: _,
browser_use: _,
in_app_browser: _,
windows: _,
feature_requirements: _,
hooks: _,
@@ -1150,6 +1154,7 @@ impl ConfigRequirementsWithSources {
allow_remote_control,
computer_use,
browser_use,
in_app_browser,
windows,
feature_requirements,
hooks,
@@ -1229,6 +1234,7 @@ impl ConfigRequirementsWithSources {
allow_remote_control,
computer_use,
browser_use,
in_app_browser,
windows,
feature_requirements,
hooks,
@@ -1267,6 +1273,7 @@ impl ConfigRequirementsWithSources {
allow_remote_control: allow_remote_control.map(|sourced| sourced.value),
computer_use: computer_use.map(|sourced| sourced.value),
browser_use: browser_use.map(|sourced| sourced.value),
in_app_browser: in_app_browser.map(|sourced| sourced.value),
windows: windows.map(|sourced| sourced.value),
feature_requirements: feature_requirements.map(|sourced| sourced.value),
hooks: hooks.map(|sourced| sourced.value),
@@ -1380,6 +1387,10 @@ impl ConfigRequirementsToml {
.browser_use
.as_ref()
.is_none_or(BrowserUseRequirementsToml::is_empty)
&& self
.in_app_browser
.as_ref()
.is_none_or(|requirements| requirements == &InAppBrowserRequirementsToml::default())
&& self
.windows
.as_ref()
@@ -1581,6 +1592,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
allow_remote_control,
computer_use,
browser_use: _,
in_app_browser: _,
windows,
feature_requirements,
hooks,
@@ -2108,6 +2120,7 @@ mod tests {
allow_remote_control,
computer_use,
browser_use,
in_app_browser,
windows,
feature_requirements,
hooks,
@@ -2161,6 +2174,8 @@ mod tests {
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
computer_use: computer_use.map(|value| Sourced::new(value, RequirementSource::Unknown)),
browser_use: browser_use.map(|value| Sourced::new(value, RequirementSource::Unknown)),
in_app_browser: in_app_browser
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
windows: windows.map(|value| Sourced::new(value, RequirementSource::Unknown)),
feature_requirements: feature_requirements
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
@@ -2457,6 +2472,7 @@ mod tests {
allow_remote_control: Some(false),
computer_use: Some(computer_use.clone()),
browser_use: None,
in_app_browser: None,
windows: Some(windows.clone()),
feature_requirements: Some(feature_requirements.clone()),
hooks: None,
@@ -2532,6 +2548,7 @@ mod tests {
)),
computer_use: Some(Sourced::new(computer_use, enforce_source.clone())),
browser_use: None,
in_app_browser: None,
windows: Some(Sourced::new(windows, enforce_source.clone())),
feature_requirements: Some(Sourced::new(
feature_requirements,
@@ -2585,6 +2602,7 @@ mod tests {
allow_remote_control: None,
computer_use: None,
browser_use: None,
in_app_browser: None,
windows: None,
feature_requirements: None,
hooks: None,
@@ -2643,6 +2661,7 @@ mod tests {
allow_remote_control: None,
computer_use: None,
browser_use: None,
in_app_browser: None,
windows: None,
feature_requirements: None,
hooks: None,
@@ -0,0 +1,14 @@
use serde::Deserialize;
/// Managed requirements for the interactive in-app browser, not agent Browser Use.
#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct InAppBrowserRequirementsToml {
/// Whether external browser settings may be imported. Only `Some(false)`
/// denies import; omission is effectively `true`. Keep omission unset while
/// composing managed layers so it cannot replace an explicit requirement.
pub allow_external_browser_settings_import: Option<bool>,
}
#[cfg(test)]
#[path = "in_app_browser_requirements_tests.rs"]
mod tests;
@@ -0,0 +1,144 @@
use super::InAppBrowserRequirementsToml;
use crate::ConfigRequirementsToml;
use crate::ConfigRequirementsWithSources;
use crate::RequirementSource;
use crate::RequirementsLayerEntry;
use crate::Sourced;
use crate::compose_requirements_for_hostname;
use pretty_assertions::assert_eq;
const EMPTY: &str = "[in_app_browser]";
const ALLOW: &str = "[in_app_browser]\nallow_external_browser_settings_import = true";
const DENY: &str = "[in_app_browser]\nallow_external_browser_settings_import = false";
fn parse(contents: &str) -> ConfigRequirementsToml {
toml::from_str(contents).expect("parse requirements")
}
fn source(name: &str) -> RequirementSource {
RequirementSource::EnterpriseManaged {
id: name.to_string(),
name: name.to_string(),
}
}
#[test]
fn import_requirement_preserves_unset_and_explicit_values() {
for (contents, expected_table) in [
("", None),
(EMPTY, Some(None)),
(ALLOW, Some(Some(true))),
(DENY, Some(Some(false))),
] {
let requirements = parse(contents);
assert_eq!(
requirements,
ConfigRequirementsToml {
in_app_browser: expected_table.map(|value| InAppBrowserRequirementsToml {
allow_external_browser_settings_import: value,
}),
..Default::default()
},
);
let configured = requirements
.in_app_browser
.as_ref()
.and_then(|policy| policy.allow_external_browser_settings_import);
assert_eq!(requirements.is_empty(), configured.is_none());
}
}
#[test]
fn import_requirement_uses_normal_managed_toml_precedence() {
for (low, high, expected) in [
("", EMPTY, None),
(DENY, "", Some(DENY)),
(DENY, EMPTY, Some(DENY)),
(DENY, ALLOW, Some(ALLOW)),
(ALLOW, DENY, Some(DENY)),
] {
let composed = compose_requirements_for_hostname(
[
RequirementsLayerEntry::from_toml(source("low"), low),
RequirementsLayerEntry::from_toml(source("high"), high),
],
/*hostname*/ None,
)
.expect("compose managed import policy")
.map(ConfigRequirementsWithSources::into_toml);
assert_eq!(composed, expected.map(parse));
}
let composed = compose_requirements_for_hostname(
[RequirementsLayerEntry::from_toml(source("managed"), DENY)],
/*hostname*/ None,
)
.expect("compose managed denial");
assert_eq!(
composed,
Some(ConfigRequirementsWithSources {
in_app_browser: Some(Sourced::new(
InAppBrowserRequirementsToml {
allow_external_browser_settings_import: Some(false),
},
source("managed"),
)),
..Default::default()
}),
);
}
#[test]
fn legacy_merge_uses_whole_table_precedence() {
for (high, low, expected, expected_source) in [
(EMPTY, DENY, EMPTY, "high"),
("", ALLOW, ALLOW, "low"),
(DENY, ALLOW, DENY, "high"),
(ALLOW, DENY, ALLOW, "high"),
] {
let mut composed = ConfigRequirementsWithSources::default();
composed.merge_unset_fields(source("high"), parse(high));
composed.merge_unset_fields(source("low"), parse(low));
assert_eq!(
composed,
ConfigRequirementsWithSources {
in_app_browser: Some(Sourced::new(
parse(expected).in_app_browser.expect("requirements table"),
source(expected_source),
)),
..Default::default()
},
);
assert_eq!(composed.into_toml(), parse(expected));
}
}
#[test]
fn invalid_import_requirement_is_rejected_even_when_overridden() {
for invalid in ["\"false\"", "1", "[]", "{}"] {
let contents =
format!("[in_app_browser]\nallow_external_browser_settings_import = {invalid}");
let error = toml::from_str::<ConfigRequirementsToml>(&contents)
.expect_err("import requirement must be a boolean");
assert!(
error
.to_string()
.contains("allow_external_browser_settings_import"),
"{error}",
);
let error = compose_requirements_for_hostname(
[
RequirementsLayerEntry::from_toml(source("invalid"), contents),
RequirementsLayerEntry::from_toml(source("higher"), ALLOW),
],
/*hostname*/ None,
)
.expect_err("invalid managed layers must not be silently ignored");
assert!(
error
.to_string()
.contains("allow_external_browser_settings_import"),
"{error}",
);
}
}
+2
View File
@@ -9,6 +9,7 @@ mod diagnostics;
mod fingerprint;
mod hook_config;
mod host_name;
mod in_app_browser_requirements;
mod key_aliases;
pub mod loader;
mod marketplace_edit;
@@ -119,6 +120,7 @@ pub use hook_config::HooksToml;
pub use hook_config::ManagedHooksRequirementsToml;
pub use hook_config::MatcherGroup;
pub use host_name::host_name;
pub use in_app_browser_requirements::InAppBrowserRequirementsToml;
pub use marketplace_edit::MarketplaceConfigUpdate;
pub use marketplace_edit::RemoveMarketplaceConfigOutcome;
pub use marketplace_edit::record_user_marketplace;
+39
View File
@@ -1,4 +1,5 @@
use super::*;
use crate::ConfigRequirementsToml;
use codex_file_system::CopyOptions;
use codex_file_system::CreateDirectoryOptions;
use codex_file_system::ExecutorFileSystemFuture;
@@ -16,6 +17,44 @@ use tempfile::tempdir;
pub(super) struct TestFileSystem;
#[tokio::test]
async fn managed_browser_import_denial_survives_user_and_session_config() {
let tmp = tempdir().expect("tempdir");
let allow = "[in_app_browser]\nallow_external_browser_settings_import = true";
let deny = "[in_app_browser]\nallow_external_browser_settings_import = false";
let requirements_path = tmp.path().join("requirements.toml");
std::fs::write(&requirements_path, allow).expect("write system requirements");
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), allow).expect("write user config");
let mut loader_overrides = LoaderOverrides::without_managed_config_for_tests();
loader_overrides.system_requirements_path = Some(requirements_path);
let stack = load_config_layers_state(
&TestFileSystem,
tmp.path(),
/*cwd*/ None,
&[(
"in_app_browser.allow_external_browser_settings_import".to_string(),
TomlValue::Boolean(true),
)],
ConfigLoadOptions {
loader_overrides,
strict_config: false,
cloud_config_bundle:
crate::test_support::CloudConfigBundleFixture::enterprise_requirement(deny)
.add_enterprise_requirement("[in_app_browser]")
.into_loader(),
},
&crate::NoopThreadConfigLoader,
)
.await
.expect("load managed browser import requirement");
assert_eq!(
stack.requirements_toml(),
&toml::from_str::<ConfigRequirementsToml>(deny).expect("expected requirement"),
);
}
impl ExecutorFileSystem for TestFileSystem {
fn canonicalize<'a>(
&'a self,
@@ -233,6 +233,7 @@ fn populate_merged_regular_fields_with_sources(
allow_remote_control,
computer_use,
browser_use,
in_app_browser,
windows,
feature_requirements,
hooks: _,
@@ -280,6 +281,7 @@ fn populate_merged_regular_fields_with_sources(
set_sourced!(auto_review, &["auto_review"]);
set_sourced!(computer_use, &["computer_use"]);
set_sourced!(browser_use, &["browser_use"]);
set_sourced!(in_app_browser, &["in_app_browser"]);
set_sourced!(windows, &["windows"]);
set_sourced!(feature_requirements, &["features", "feature_requirements"]);
set_sourced!(mcp_servers, &["mcp_servers"]);
+1
View File
@@ -9844,6 +9844,7 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset()
allow_remote_control: None,
computer_use: None,
browser_use: None,
in_app_browser: None,
windows: None,
feature_requirements: None,
hooks: None,
+1
View File
@@ -955,6 +955,7 @@ interrupt_message = false
allow_remote_control: Some(false),
computer_use: None,
browser_use: None,
in_app_browser: None,
windows: Some(WindowsRequirementsToml {
allowed_sandbox_implementations: None,
sandbox_private_desktop: Some(false),