mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
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:
+21
@@ -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": [
|
||||
|
||||
+21
@@ -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": {
|
||||
|
||||
+21
@@ -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": {
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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};
|
||||
|
||||
+5
@@ -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,
|
||||
|
||||
@@ -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}",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user