Expose managed WebMCP policy through the app server (#42823)

## What changed

- Parse the optional `[browser_use].allow_webmcp` boolean from managed requirements and preserve requirement-layer precedence.
- Return the policy as `browserUse.allowWebmcp` from `configRequirements/read`, including explicit `true`, explicit `false`, and `null` when omitted.
- Update the generated JSON and TypeScript protocol schemas.

## Testing

- Cover parsing, invalid values, layered overrides, and app-server response serialization.

GitOrigin-RevId: a3fd7d1a6f838bb3ab0ac2a644409101a2338ca6
This commit is contained in:
nmccormack-oai
2026-09-04 18:30:26 +00:00
committed by copyberry
parent 387bc6ba59
commit 0f64d70808
17 changed files with 183 additions and 3 deletions
@@ -8098,6 +8098,12 @@
"null"
]
},
"allowWebmcp": {
"type": [
"boolean",
"null"
]
},
"defaultOriginPolicy": {
"anyOf": [
{
@@ -1582,6 +1582,12 @@
"null"
]
},
"allowWebmcp": {
"type": [
"boolean",
"null"
]
},
"defaultOriginPolicy": {
"anyOf": [
{
@@ -215,6 +215,12 @@
"null"
]
},
"allowWebmcp": {
"type": [
"boolean",
"null"
]
},
"defaultOriginPolicy": {
"anyOf": [
{
@@ -3,4 +3,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { BrowserUseOriginPolicy } from "./BrowserUseOriginPolicy";
export type BrowserUseRequirements = { allowHistoryAccess: boolean | null, disableAutoReview: boolean | null, allowGlobalPersistentApproval: boolean | null, defaultOriginPolicy: BrowserUseOriginPolicy | null, origins: { [key in string]?: BrowserUseOriginPolicy } | null, };
export type BrowserUseRequirements = { allowWebmcp: boolean | null, allowHistoryAccess: boolean | null, disableAutoReview: boolean | null, allowGlobalPersistentApproval: boolean | null, defaultOriginPolicy: BrowserUseOriginPolicy | null, origins: { [key in string]?: BrowserUseOriginPolicy } | null, };
@@ -505,6 +505,7 @@ pub struct ComputerUseRequirements {
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct BrowserUseRequirements {
pub allow_webmcp: Option<bool>,
pub allow_history_access: Option<bool>,
pub disable_auto_review: Option<bool>,
pub allow_global_persistent_approval: Option<bool>,
+2 -1
View File
@@ -314,7 +314,8 @@ 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`), requirements-only developer instructions (`additionalDeveloperInstructions`, supplied independently of ordinary developer instructions), 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), the Browser/Computer Use umbrella policy (`allowBrowserAndComputerUse`), computer use policy (`computerUse`, including persistent approval, default application access, and per-platform application rules), Browser Use policy (`browserUse`, including history access, origin rules, auto-review, and approval controls), 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`.
- `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`), requirements-only developer instructions (`additionalDeveloperInstructions`, supplied independently of ordinary developer instructions), 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), the Browser/Computer Use umbrella policy (`allowBrowserAndComputerUse`), computer use policy (`computerUse`, including persistent approval, default application access, and per-platform application rules), Browser Use policy (`browserUse`, including WebMCP enablement, history access, origin rules, auto-review, and approval controls), 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`.
- Managed `[browser_use].allow_webmcp` is returned as `browserUse.allowWebmcp`, preserving `true`, `false`, and omission as `null`. Desktop clients enable WebMCP when their WebMCP Statsig gate is on **or** this field is `true`. A `false` or omitted policy does not disable a gate-enabled feature. When policy is omitted, enterprise and consumer defaults come from Statsig. This is a requirements-only field; ordinary `config.toml` settings cannot enable it through this API.
`mcpServer/resource/read` and `mcpServer/tool/call` preserve MCP protocol errors
with their original `code`, `message`, and `data`, including authentication
@@ -561,6 +561,7 @@ fn map_browser_use_requirements_to_api(
browser_use: codex_config::BrowserUseRequirementsToml,
) -> BrowserUseRequirements {
BrowserUseRequirements {
allow_webmcp: browser_use.allow_webmcp,
allow_history_access: browser_use.allow_history_access,
disable_auto_review: browser_use.disable_auto_review,
allow_global_persistent_approval: browser_use.allow_global_persistent_approval,
@@ -940,6 +941,7 @@ mod tests {
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
allow_browser_and_computer_use: Some(false),
browser_use: Some(BrowserUseRequirementsToml {
allow_webmcp: Some(true),
allow_history_access: Some(false),
disable_auto_review: Some(true),
allow_global_persistent_approval: Some(false),
@@ -997,6 +999,7 @@ mod tests {
assert_eq!(
mapped.browser_use,
Some(BrowserUseRequirements {
allow_webmcp: Some(true),
allow_history_access: Some(false),
disable_auto_review: Some(true),
allow_global_persistent_approval: Some(false),
@@ -0,0 +1,72 @@
use anyhow::Result;
use app_test_support::TestAppServer;
use codex_app_server_protocol::ClientInfo;
use pretty_assertions::assert_eq;
use serde_json::Value;
use serde_json::json;
use std::time::Duration;
use tempfile::TempDir;
use tokio::time::timeout;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_requirements_read_preserves_webmcp_policy() -> Result<()> {
for (contents, expected_browser_use) in [
("", None),
("[browser_use]", None),
(
"[browser_use]\nallow_history_access = true",
Some((Some(true), None)),
),
(
"[browser_use]\nallow_webmcp = true",
Some((None, Some(true))),
),
(
"[browser_use]\nallow_webmcp = false",
Some((None, Some(false))),
),
] {
let codex_home = TempDir::new()?;
std::fs::write(codex_home.path().join("requirements.toml"), contents)?;
std::fs::write(
codex_home.path().join("config.toml"),
"[browser_use]\nallow_webmcp = true",
)?;
let mut server = TestAppServer::builder()
.with_codex_home(codex_home.path())
.build()
.await?;
let read_timeout = Duration::from_secs(/*secs*/ 60);
timeout(
read_timeout,
server.initialize_with_capabilities(
ClientInfo {
name: "webmcp_policy_test".to_string(),
title: None,
version: "0.1.0".to_string(),
},
/*capabilities*/ None,
),
)
.await??;
let request_id = server.send_config_requirements_read_request().await?;
let wire: Value = timeout(read_timeout, server.read_response(request_id)).await??;
if let Some((allow_history_access, allow_webmcp)) = expected_browser_use {
assert_eq!(
wire["requirements"]["browserUse"],
json!({
"allowWebmcp": allow_webmcp,
"allowHistoryAccess": allow_history_access,
"disableAutoReview": null,
"allowGlobalPersistentApproval": null,
"defaultOriginPolicy": null,
"origins": null,
}),
"{contents}",
);
} else {
assert_eq!(wire, json!({ "requirements": null }), "{contents}");
}
}
Ok(())
}
@@ -106,6 +106,7 @@ async fn import_policy_is_separate_from_browser_feature_and_agent_policy() -> Re
allow_external_browser_settings_import: Some(false),
}),
Some(BrowserUseRequirements {
allow_webmcp: None,
allow_history_access: None,
disable_auto_review: Some(true),
allow_global_persistent_approval: None,
@@ -297,6 +297,7 @@ access = "deny"
assert_eq!(
requirements.browser_use,
Some(BrowserUseRequirements {
allow_webmcp: None,
allow_history_access: Some(false),
disable_auto_review: Some(true),
allow_global_persistent_approval: Some(false),
@@ -822,6 +823,7 @@ access = "deny"
assert_eq!(
requirements.browser_use,
Some(BrowserUseRequirements {
allow_webmcp: None,
allow_history_access: Some(false),
disable_auto_review: None,
allow_global_persistent_approval: Some(false),
@@ -14,6 +14,8 @@ mod collaboration_mode_list;
mod command_exec;
mod compaction;
mod config_requirements_application;
#[path = "config_requirements_browser_use_tests.rs"]
mod config_requirements_browser_use;
mod config_requirements_in_app_browser;
mod config_rpc;
mod connection_handling_websocket;
@@ -42,6 +42,7 @@ pub enum BrowserUseAccessApprovalLifetimeToml {
#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct BrowserUseRequirementsToml {
pub allow_webmcp: Option<bool>,
pub allow_history_access: Option<bool>,
pub disable_auto_review: Option<bool>,
pub allow_global_persistent_approval: Option<bool>,
@@ -51,7 +52,8 @@ pub struct BrowserUseRequirementsToml {
impl BrowserUseRequirementsToml {
pub fn is_empty(&self) -> bool {
self.allow_history_access.is_none()
self.allow_webmcp.is_none()
&& self.allow_history_access.is_none()
&& self.disable_auto_review.is_none()
&& self.allow_global_persistent_approval.is_none()
&& self
@@ -108,3 +110,7 @@ impl ComputerUseRequirementsToml {
})
}
}
#[cfg(test)]
#[path = "browser_computer_use_requirements_tests.rs"]
mod tests;
@@ -0,0 +1,45 @@
use crate::BrowserUseRequirementsToml;
use crate::ConfigRequirementsToml;
use pretty_assertions::assert_eq;
#[test]
fn webmcp_requirements_preserve_explicit_values_and_omission() {
for (contents, expected_browser_use, expected_empty) in [
("", None, true),
("[browser_use]", Some(None), true),
(
"[browser_use]\nallow_webmcp = true",
Some(Some(true)),
false,
),
(
"[browser_use]\nallow_webmcp = false",
Some(Some(false)),
false,
),
] {
let requirements: ConfigRequirementsToml =
toml::from_str(contents).expect("parse managed WebMCP policy");
assert_eq!(
requirements,
ConfigRequirementsToml {
browser_use: expected_browser_use.map(|allow_webmcp| BrowserUseRequirementsToml {
allow_webmcp,
..Default::default()
}),
..Default::default()
},
);
assert_eq!(requirements.is_empty(), expected_empty, "{contents}");
}
}
#[test]
fn webmcp_requirements_reject_non_booleans() {
for value in ["\"true\"", "1", "[]"] {
let contents = format!("[browser_use]\nallow_webmcp = {value}");
let error = toml::from_str::<ConfigRequirementsToml>(&contents)
.expect_err("WebMCP policy must be a boolean");
assert!(error.to_string().contains("allow_webmcp"));
}
}
@@ -2482,6 +2482,7 @@ mod tests {
assert_eq!(
requirements.browser_use,
Some(BrowserUseRequirementsToml {
allow_webmcp: None,
allow_history_access: Some(false),
disable_auto_review: Some(true),
allow_global_persistent_approval: Some(false),
@@ -2714,6 +2715,7 @@ mod tests {
entries: BTreeMap::from([("personality".to_string(), true)]),
};
let browser_use = BrowserUseRequirementsToml {
allow_webmcp: None,
allow_history_access: Some(false),
disable_auto_review: Some(true),
allow_global_persistent_approval: None,
@@ -717,6 +717,33 @@ access = "deny"
);
}
#[test]
fn webmcp_requirements_preserve_managed_layer_precedence() {
for (lower, higher, expected) in [
("true", "[browser_use]", true),
("false", "[browser_use]", false),
("true", "[browser_use]\nallow_webmcp = false", false),
("false", "[browser_use]\nallow_webmcp = true", true),
] {
let lower = format!("[browser_use]\nallow_webmcp = {lower}");
let composed = compose(vec![
layer("req_low", "Low", &lower),
layer("req_high", "High", higher),
])
.expect("compose managed WebMCP policy");
assert_eq!(
composed,
Some(ConfigRequirementsToml {
browser_use: Some(crate::BrowserUseRequirementsToml {
allow_webmcp: Some(expected),
..Default::default()
}),
..Default::default()
}),
);
}
}
#[test]
fn windows_requirements_use_regular_toml_merge() {
let composed = compose(vec![