mirror of
https://github.com/openai/codex.git
synced 2026-11-03 19:53:25 +00:00
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:
committed by
copyberry
parent
387bc6ba59
commit
0f64d70808
+6
@@ -8098,6 +8098,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowWebmcp": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"defaultOriginPolicy": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
+6
@@ -1582,6 +1582,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowWebmcp": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"defaultOriginPolicy": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
+6
@@ -215,6 +215,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowWebmcp": {
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"defaultOriginPolicy": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+1
-1
@@ -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>,
|
||||
|
||||
@@ -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![
|
||||
|
||||
Reference in New Issue
Block a user