Expose managed application network requirements (#42417)

## What changed

- Add `application.network` managed requirements with exact-domain allow/deny
  rules, default-enabled behavior, domain normalization, and normal managed TOML
  precedence.
- Return the policy separately from agent network requirements through the
  experimental `configRequirements/read` API. The endpoint reports the policy
  but does not enforce it.
- Export the new protocol types and schemas, and document the TOML format.

## Testing

- Cover parsing, validation, normalization, layered precedence, and cloud
  requirement composition.
- Verify app-server responses, defaults, separation from agent policy, and
  rejection of invalid domains.

GitOrigin-RevId: 563eeed296d9748430b819797204a44a8db92f3b
This commit is contained in:
acrognale-oai
2026-09-03 02:09:10 +00:00
committed by copyberry
parent 88912c04cd
commit b27a6321fa
27 changed files with 658 additions and 5 deletions
@@ -7482,6 +7482,40 @@
"AppToolsConfig": {
"type": "object"
},
"ApplicationNetworkRequirements": {
"properties": {
"domains": {
"additionalProperties": {
"$ref": "#/definitions/v2/NetworkDomainPermission"
},
"type": "object"
},
"enabled": {
"description": "When enabled, only explicitly allowed exact domains may be contacted.",
"type": "boolean"
}
},
"required": [
"domains",
"enabled"
],
"type": "object"
},
"ApplicationRequirements": {
"properties": {
"network": {
"anyOf": [
{
"$ref": "#/definitions/v2/ApplicationNetworkRequirements"
},
{
"type": "null"
}
]
}
},
"type": "object"
},
"ApprovalsReviewer": {
"description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.",
"enum": [
@@ -966,6 +966,40 @@
"AppToolsConfig": {
"type": "object"
},
"ApplicationNetworkRequirements": {
"properties": {
"domains": {
"additionalProperties": {
"$ref": "#/definitions/NetworkDomainPermission"
},
"type": "object"
},
"enabled": {
"description": "When enabled, only explicitly allowed exact domains may be contacted.",
"type": "boolean"
}
},
"required": [
"domains",
"enabled"
],
"type": "object"
},
"ApplicationRequirements": {
"properties": {
"network": {
"anyOf": [
{
"$ref": "#/definitions/ApplicationNetworkRequirements"
},
{
"type": "null"
}
]
}
},
"type": "object"
},
"ApprovalsReviewer": {
"description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.",
"enum": [
@@ -8,6 +8,40 @@
],
"type": "string"
},
"ApplicationNetworkRequirements": {
"properties": {
"domains": {
"additionalProperties": {
"$ref": "#/definitions/NetworkDomainPermission"
},
"type": "object"
},
"enabled": {
"description": "When enabled, only explicitly allowed exact domains may be contacted.",
"type": "boolean"
}
},
"required": [
"domains",
"enabled"
],
"type": "object"
},
"ApplicationRequirements": {
"properties": {
"network": {
"anyOf": [
{
"$ref": "#/definitions/ApplicationNetworkRequirements"
},
{
"type": "null"
}
]
}
},
"type": "object"
},
"ApprovalsReviewer": {
"description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.",
"enum": [
@@ -0,0 +1,10 @@
// 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.
import type { NetworkDomainPermission } from "./NetworkDomainPermission";
export type ApplicationNetworkRequirements = {
/**
* When enabled, only explicitly allowed exact domains may be contacted.
*/
enabled: boolean, domains: { [key in string]?: NetworkDomainPermission }, };
@@ -0,0 +1,6 @@
// 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.
import type { ApplicationNetworkRequirements } from "./ApplicationNetworkRequirements";
export type ApplicationRequirements = { network: ApplicationNetworkRequirements | null, };
@@ -31,6 +31,8 @@ export type { AppTemplateUnavailableReason } from "./AppTemplateUnavailableReaso
export type { AppToolApproval } from "./AppToolApproval";
export type { AppToolSummary } from "./AppToolSummary";
export type { AppToolsConfig } from "./AppToolsConfig";
export type { ApplicationNetworkRequirements } from "./ApplicationNetworkRequirements";
export type { ApplicationRequirements } from "./ApplicationRequirements";
export type { ApprovalsReviewer } from "./ApprovalsReviewer";
export type { AppsConfig } from "./AppsConfig";
export type { AppsDefaultConfig } from "./AppsDefaultConfig";
@@ -0,0 +1,24 @@
//! Managed requirements for application traffic, separate from agent permissions.
use super::NetworkDomainPermission;
use crate::JsonSchema;
use crate::TS;
use serde::Deserialize;
use serde::Serialize;
use std::collections::BTreeMap;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ApplicationRequirements {
pub network: Option<ApplicationNetworkRequirements>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ApplicationNetworkRequirements {
/// When enabled, only explicitly allowed exact domains may be contacted.
pub enabled: bool,
pub domains: BTreeMap<String, NetworkDomainPermission>,
}
@@ -1,3 +1,4 @@
use super::ApplicationRequirements;
use super::ApprovalsReviewer;
use super::AskForApproval;
use super::BrowserUseConfig;
@@ -432,6 +433,8 @@ pub struct ConfigRequirements {
pub enforce_residency: Option<ResidencyRequirement>,
#[experimental("configRequirements/read.network")]
pub network: Option<NetworkRequirements>,
#[experimental("configRequirements/read.application")]
pub application: Option<ApplicationRequirements>,
pub auto_review: Option<AutoReviewRequirements>,
pub models: Option<ModelsRequirements>,
#[schemars(with = "Option<String>")]
@@ -1,6 +1,7 @@
mod shared;
mod account;
mod application;
mod apps;
mod attestation;
mod bedrock;
@@ -35,6 +36,7 @@ mod turn;
mod windows_sandbox;
pub use account::*;
pub use application::*;
pub use apps::*;
pub use attestation::*;
pub use bedrock::*;
@@ -2112,6 +2112,7 @@ fn config_approvals_reviewer_is_marked_experimental() {
fn config_requirements_granular_allowed_approval_policy_is_marked_experimental() {
let reason =
crate::experimental_api::ExperimentalApi::experimental_reason(&ConfigRequirements {
application: None,
cli_auth_credentials_store: None,
chatgpt_base_url: None,
additional_developer_instructions: None,
+14
View File
@@ -322,6 +322,20 @@ metadata in `data._meta`. Other operation failures retain the existing
internal-error response. Tool results with `isError: true` remain results,
including their `_meta`.
### Application requirements
With experimental API support enabled, `configRequirements/read` returns
`application.network` from managed requirements, separately from agent-network
policy in `network`. This endpoint reports policy; it does not enforce it.
```toml
[application.network.domains]
"managed.example.com" = "allow"
```
Application rules use normal managed TOML precedence. A present network block
is enabled by default and denies unlisted domains; `enabled = false` disables it.
### Plugin configuration scope
Plugin activation and MCP settings use the existing merged configuration, including
@@ -381,6 +381,22 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR
.and_then(|windows| windows.sandbox_private_desktop);
ConfigRequirements {
application: requirements.application.map(|application| {
codex_app_server_protocol::ApplicationRequirements {
network: application.network.map(|network| {
codex_app_server_protocol::ApplicationNetworkRequirements {
enabled: network.enabled,
domains: network
.domains
.into_iter()
.map(|(domain, permission)| {
(domain, map_network_domain_permission_to_api(permission))
})
.collect(),
}
}),
}
}),
cli_auth_credentials_store: requirements.cli_auth_credentials_store.map(
|mode| match mode {
codex_config::types::AuthCredentialsStoreMode::File => {
@@ -0,0 +1,118 @@
//! Public requirements responses expose managed application policy.
use anyhow::Result;
use app_test_support::TestAppServer;
use codex_app_server_protocol::RequestId;
use pretty_assertions::assert_eq;
use serde_json::Value;
use serde_json::json;
use std::time::Duration;
use tempfile::TempDir;
use tokio::time::timeout;
const READ_TIMEOUT: Duration = Duration::from_secs(/*secs*/ 60);
async fn start_server(home: &TempDir) -> Result<TestAppServer> {
TestAppServer::builder()
.with_codex_home(home.path())
.build_initialized_with_timeout(READ_TIMEOUT)
.await
}
async fn read_requirements(server: &mut TestAppServer) -> Result<Value> {
let request_id = server.send_config_requirements_read_request().await?;
timeout(READ_TIMEOUT, server.read_response(request_id)).await?
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn application_network_defaults_and_null_base_url_are_exposed() -> Result<()> {
for (contents, application) in [
("", Value::Null),
("[application]", json!({"network": null})),
(
"[application.network]",
json!({"network": {"enabled": true, "domains": {}}}),
),
(
"[application.network]\nenabled = false",
json!({"network": {"enabled": false, "domains": {}}}),
),
] {
let home = TempDir::new()?;
std::fs::write(
home.path().join("requirements.toml"),
format!("allow_remote_control = false\n{contents}"),
)?;
let mut server = start_server(&home).await?;
let wire = read_requirements(&mut server).await?;
let requirements = wire["requirements"]
.as_object()
.expect("requirements object");
assert_eq!(
(
requirements.get("application"),
requirements.get("chatgptBaseUrl")
),
(Some(&application), Some(&Value::Null)),
);
}
let home = TempDir::new()?;
let mut server = start_server(&home).await?;
assert_eq!(
read_requirements(&mut server).await?,
json!({"requirements": null})
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn application_destinations_stay_separate_from_agent_network() -> Result<()> {
let home = TempDir::new()?;
std::fs::write(
home.path().join("requirements.toml"),
r#"
chatgpt_base_url = "https://managed.example.com/backend-api/"
[experimental_network.domains]
"github.com" = "allow"
[application.network.domains]
"GOV.example.com." = "allow"
"github.com" = "deny"
"#,
)?;
let mut server = start_server(&home).await?;
let wire = read_requirements(&mut server).await?;
assert_eq!(
(
&wire["requirements"]["application"],
&wire["requirements"]["network"]["domains"],
&wire["requirements"]["chatgptBaseUrl"]
),
(
&json!({"network": {"enabled": true, "domains": {"gov.example.com": "allow", "github.com": "deny"}}}),
&json!({"github.com": "allow"}),
&json!("https://managed.example.com/backend-api/")
),
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn application_requirements_read_rejects_invalid_configuration() -> Result<()> {
let home = TempDir::new()?;
let mut server = start_server(&home).await?;
std::fs::write(
home.path().join("requirements.toml"),
"[application.network.domains]\n'*.example.com' = 'allow'",
)?;
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??;
assert!(
error.error.message.contains("application.network.domains"),
"{error:?}"
);
Ok(())
}
@@ -13,6 +13,7 @@ mod collaboration_mode_list;
#[cfg(unix)]
mod command_exec;
mod compaction;
mod config_requirements_application;
mod config_requirements_in_app_browser;
mod config_rpc;
mod connection_handling_websocket;
@@ -0,0 +1,71 @@
//! Managed application destinations are independent of agent network permissions.
//! Enabled policies deny unlisted domains and use normal managed TOML precedence.
use crate::NetworkDomainPermissionToml;
use serde::Deserialize;
use serde::de::Error;
use std::collections::BTreeMap;
#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct ApplicationRequirementsToml {
pub network: Option<ApplicationNetworkRequirementsToml>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApplicationNetworkRequirementsToml {
pub enabled: bool,
pub domains: BTreeMap<String, NetworkDomainPermissionToml>,
}
impl<'de> Deserialize<'de> for ApplicationNetworkRequirementsToml {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Network {
enabled: Option<bool>,
#[serde(default)]
domains: BTreeMap<String, NetworkDomainPermissionToml>,
}
let raw = Network::deserialize(deserializer)?;
let mut domains = BTreeMap::new();
for (domain, permission) in raw.domains {
let normalized = domain
.strip_suffix('.')
.unwrap_or(&domain)
.to_ascii_lowercase();
if normalized.len() > 253
|| normalized.split('.').any(|label| {
label.is_empty()
|| label.len() > 63
|| !label.starts_with(|c: char| c.is_ascii_alphanumeric())
|| !label.ends_with(|c: char| c.is_ascii_alphanumeric())
|| !label
.bytes()
.all(|c| c.is_ascii_alphanumeric() || c == b'-')
})
{
return Err(D::Error::custom(
"application.network.domains requires exact ASCII domain names without URLs, ports, or wildcards",
));
}
if domains.insert(normalized, permission).is_some() {
return Err(D::Error::custom(format!(
"duplicate application.network domain after normalization: {domain:?}"
)));
}
}
Ok(Self {
enabled: raw.enabled.unwrap_or(true),
domains,
})
}
}
#[cfg(test)]
#[path = "application_requirements_tests.rs"]
mod tests;
@@ -0,0 +1,251 @@
//! Application requirements use managed precedence and deny unlisted destinations.
use super::*;
use crate::ConfigRequirements;
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 INSTALLATION: &str = r#"
[application.network.domains]
"shared.example.com" = "allow"
"installed.example.com" = "allow"
"blocked.example.com" = "deny"
"#;
const WORKSPACE: &str = r#"
[application.network.domains]
"shared.example.com" = "allow"
"workspace.example.com" = "allow"
"blocked.example.com" = "allow"
"#;
const MERGED: &str = r#"
[application.network.domains]
"shared.example.com" = "allow"
"installed.example.com" = "allow"
"workspace.example.com" = "allow"
"blocked.example.com" = "allow"
"#;
fn parse(contents: &str) -> ConfigRequirementsToml {
toml::from_str(contents).expect("valid application requirements")
}
fn cloud_source() -> RequirementSource {
RequirementSource::EnterpriseManaged {
id: "workspace".to_string(),
name: "Workspace policy".to_string(),
}
}
fn managed_source() -> RequirementSource {
RequirementSource::MdmManagedPreferences {
domain: "com.openai.codex".to_string(),
key: "requirements".to_string(),
}
}
#[test]
fn application_network_defaults_to_enabled_and_denies_unlisted_domains() {
assert!(parse("[application]").is_empty());
let requirements = parse("[application.network]");
assert!(!requirements.is_empty());
assert_eq!(
requirements.application,
Some(ApplicationRequirementsToml {
network: Some(ApplicationNetworkRequirementsToml {
enabled: true,
domains: BTreeMap::new(),
}),
}),
);
assert_eq!(
parse("[application.network.domains]\n\"EXAMPLE.com.\" = \"deny\""),
parse("[application.network.domains]\n\"example.com\" = \"deny\""),
);
}
#[test]
fn application_network_is_preserved_from_each_managed_source() {
for source in [
RequirementSource::SystemRequirementsToml {
file: crate::AbsolutePathBuf::try_from(std::env::temp_dir().join("requirements.toml"))
.expect("absolute requirements path"),
},
managed_source(),
cloud_source(),
] {
let requirements = compose_requirements_for_hostname(
[RequirementsLayerEntry::from_toml(
source.clone(),
INSTALLATION,
)],
/*hostname*/ None,
)
.expect("compose application requirements")
.expect("application requirements are not empty");
assert_eq!(
requirements,
ConfigRequirementsWithSources {
application: Some(Sourced::new(
parse(INSTALLATION).application.expect("application"),
source,
)),
..Default::default()
},
);
let normalized: ConfigRequirements =
requirements.clone().try_into().expect("runtime policy");
assert_eq!(normalized.application, requirements.application);
}
}
#[test]
fn application_network_uses_regular_toml_precedence() {
let installed_wins = MERGED.replace(
"\"blocked.example.com\" = \"allow\"",
"\"blocked.example.com\" = \"deny\"",
);
let disabled_installation = format!("[application.network]\nenabled = false\n{INSTALLATION}");
for (low, high, expected) in [
(INSTALLATION, WORKSPACE, MERGED),
(WORKSPACE, INSTALLATION, installed_wins.as_str()),
(
INSTALLATION,
"[application.network]\nenabled = false",
disabled_installation.as_str(),
),
(
disabled_installation.as_str(),
"[application.network]\nenabled = true",
INSTALLATION,
),
(
"[application.network]\nenabled = false",
INSTALLATION,
disabled_installation.as_str(),
),
(INSTALLATION, "[application.network]", INSTALLATION),
(INSTALLATION, "", INSTALLATION),
("[application]", INSTALLATION, INSTALLATION),
("[application.network]", WORKSPACE, WORKSPACE),
(
"[application.network.domains]\n'example.com' = 'allow'",
"[application.network.domains]\n'EXAMPLE.COM.' = 'deny'",
"[application.network.domains]\n'example.com' = 'deny'",
),
(
"[application.network.domains]\n'EXAMPLE.COM.' = 'deny'",
"[application.network.domains]\n'example.com' = 'allow'",
"[application.network.domains]\n'example.com' = 'allow'",
),
] {
let composed = compose_requirements_for_hostname(
[
RequirementsLayerEntry::from_toml(managed_source(), low),
RequirementsLayerEntry::from_toml(cloud_source(), high),
],
/*hostname*/ None,
)
.expect("compose application requirements")
.expect("configured requirements");
assert_eq!(
composed.into_toml(),
parse(expected),
"low: {low}, high: {high}"
);
}
}
#[test]
fn legacy_application_merge_uses_whole_table_precedence() {
for (high, low) in [
(INSTALLATION, WORKSPACE),
(WORKSPACE, INSTALLATION),
("[application.network]\nenabled = false", INSTALLATION),
("[application]", INSTALLATION),
] {
let mut composed = ConfigRequirementsWithSources::default();
composed.merge_unset_fields(managed_source(), parse(high));
composed.merge_unset_fields(cloud_source(), parse(low));
assert_eq!(
composed,
ConfigRequirementsWithSources {
application: Some(Sourced::new(
parse(high).application.expect("application"),
managed_source(),
)),
..Default::default()
},
);
}
let mut composed = ConfigRequirementsWithSources::default();
composed.merge_unset_fields(managed_source(), parse(""));
composed.merge_unset_fields(cloud_source(), parse(INSTALLATION));
assert_eq!(composed.into_toml(), parse(INSTALLATION));
}
#[test]
fn invalid_application_policy_is_rejected_before_layer_overrides() {
for invalid in [
"[application.network]\nenabled = 'true'",
"[application.network]\nallowed_domains = ['example.com']",
"[application.network.domains]\n'example.com' = 'prompt'",
"[application.network.domains]\n'*.example.com' = 'allow'",
"[application.network.domains]\n'https://example.com' = 'allow'",
"[application.network.domains]\n'example.com:443' = 'allow'",
"[application.network.domains]\n'example..com' = 'allow'",
"[application.network.domains]\n' example.com' = 'allow'",
"[application.network.domains]\n'EXAMPLE.com' = 'allow'\n'example.com.' = 'deny'",
] {
assert!(
toml::from_str::<ConfigRequirementsToml>(invalid).is_err(),
"{invalid}"
);
assert!(
compose_requirements_for_hostname(
[
RequirementsLayerEntry::from_toml(cloud_source(), invalid),
RequirementsLayerEntry::from_toml(managed_source(), INSTALLATION),
],
/*hostname*/ None,
)
.is_err(),
"{invalid}"
);
}
}
#[test]
fn cloud_bundle_application_network_uses_managed_precedence() {
let base_dir =
crate::AbsolutePathBuf::try_from(std::env::temp_dir()).expect("absolute base directory");
let bundle = crate::CloudConfigBundle {
requirements_toml: crate::CloudRequirementsTomlBundle {
enterprise_managed: vec![crate::CloudRequirementsFragment {
id: "workspace".to_string(),
name: "Workspace".to_string(),
contents: WORKSPACE.to_string(),
}],
},
..Default::default()
};
let mut layers = crate::CloudConfigBundleLayers::from_bundle(bundle, &base_dir)
.expect("load cloud bundle")
.enterprise_managed_requirements;
layers.push(RequirementsLayerEntry::from_toml(
managed_source(),
INSTALLATION,
));
let composed = compose_requirements_for_hostname(layers, /*hostname*/ None)
.expect("compose cloud and installation requirements")
.expect("requirements");
let expected = MERGED.replace(
"\"blocked.example.com\" = \"allow\"",
"\"blocked.example.com\" = \"deny\"",
);
assert_eq!(composed.into_toml(), parse(&expected));
}
@@ -1,3 +1,4 @@
use crate::ApplicationRequirementsToml;
use codex_features::FeatureToml;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::ForcedLoginMethod;
@@ -189,6 +190,7 @@ pub struct ConfigRequirements {
pub enforce_residency: ConstrainedWithSource<Option<ResidencyRequirement>>,
/// Managed network constraints derived from requirements.
pub network: Option<Sourced<NetworkConstraints>>,
pub application: Option<Sourced<ApplicationRequirementsToml>>,
/// Managed filesystem constraints derived from requirements.
pub filesystem: Option<Sourced<FilesystemConstraints>>,
/// Managed instructions included independently of ordinary developer instructions.
@@ -247,6 +249,7 @@ impl Default for ConfigRequirements {
/*source*/ None,
),
network: None,
application: None,
filesystem: None,
additional_developer_instructions: None,
guardian_policy_config_source: None,
@@ -1015,6 +1018,7 @@ pub struct ConfigRequirementsToml {
pub enforce_residency: Option<ResidencyRequirement>,
#[serde(rename = "experimental_network")]
pub network: Option<NetworkRequirementsToml>,
pub application: Option<ApplicationRequirementsToml>,
pub permissions: Option<PermissionsRequirementsToml>,
pub auto_review: Option<AutoReviewRequirementsToml>,
pub models: Option<ModelsRequirementsToml>,
@@ -1117,6 +1121,7 @@ pub struct ConfigRequirementsWithSources {
pub rules: Option<Sourced<RequirementsExecPolicyToml>>,
pub enforce_residency: Option<Sourced<ResidencyRequirement>>,
pub network: Option<Sourced<NetworkRequirementsToml>>,
pub application: Option<Sourced<ApplicationRequirementsToml>>,
pub permissions: Option<Sourced<PermissionsRequirementsToml>>,
pub auto_review: Option<Sourced<AutoReviewRequirementsToml>>,
pub models: Option<Sourced<ModelsRequirementsToml>>,
@@ -1177,6 +1182,7 @@ impl ConfigRequirementsWithSources {
rules: _,
enforce_residency: _,
network: _,
application: _,
permissions: _,
auto_review: _,
models: _,
@@ -1229,6 +1235,7 @@ impl ConfigRequirementsWithSources {
rules,
enforce_residency,
network,
application,
permissions,
models,
additional_developer_instructions,
@@ -1312,6 +1319,7 @@ impl ConfigRequirementsWithSources {
rules,
enforce_residency,
network,
application,
permissions,
auto_review,
models,
@@ -1354,6 +1362,7 @@ impl ConfigRequirementsWithSources {
rules: rules.map(|sourced| sourced.value),
enforce_residency: enforce_residency.map(|sourced| sourced.value),
network: network.map(|sourced| sourced.value),
application: application.map(|sourced| sourced.value),
permissions: permissions.map(|sourced| sourced.value),
auto_review: auto_review.map(|sourced| sourced.value),
models: models.map(|sourced| sourced.value),
@@ -1492,6 +1501,10 @@ impl ConfigRequirementsToml {
&& self.rules.is_none()
&& self.enforce_residency.is_none()
&& self.network.is_none()
&& self
.application
.as_ref()
.is_none_or(|application| application.network.is_none())
&& self.permissions.is_none()
&& self.auto_review.as_ref().is_none_or(|auto_review| {
auto_review.ignore_rules.as_ref().is_none_or(Vec::is_empty)
@@ -1678,6 +1691,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
rules,
enforce_residency,
network,
application,
permissions,
auto_review,
models: _,
@@ -2037,6 +2051,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
exec_policy,
enforce_residency,
network,
application,
filesystem,
additional_developer_instructions,
guardian_policy_config_source,
@@ -2215,6 +2230,7 @@ mod tests {
rules,
enforce_residency,
network,
application,
permissions,
auto_review,
models,
@@ -2275,6 +2291,7 @@ mod tests {
enforce_residency: enforce_residency
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
network: network.map(|value| Sourced::new(value, RequirementSource::Unknown)),
application: application.map(|value| Sourced::new(value, RequirementSource::Unknown)),
permissions: permissions.map(|value| Sourced::new(value, RequirementSource::Unknown)),
auto_review: auto_review.map(|value| Sourced::new(value, RequirementSource::Unknown)),
models: models.map(|value| Sourced::new(value, RequirementSource::Unknown)),
@@ -2777,6 +2794,7 @@ mod tests {
rules: None,
enforce_residency: Some(enforce_residency),
network: None,
application: None,
permissions: None,
auto_review: Some(auto_review.clone()),
models: Some(models.clone()),
@@ -2861,6 +2879,7 @@ mod tests {
rules: None,
enforce_residency: Some(Sourced::new(enforce_residency, enforce_source)),
network: None,
application: None,
permissions: None,
auto_review: Some(Sourced::new(auto_review, source.clone())),
models: Some(Sourced::new(models, source.clone())),
@@ -2916,6 +2935,7 @@ mod tests {
rules: None,
enforce_residency: None,
network: None,
application: None,
permissions: None,
models: None,
guardian_policy_config: None,
@@ -2975,6 +2995,7 @@ mod tests {
rules: None,
enforce_residency: None,
network: None,
application: None,
permissions: None,
models: None,
guardian_policy_config: None,
+3
View File
@@ -1,3 +1,4 @@
mod application_requirements;
mod auth_policy;
mod browser_computer_use_requirements;
mod browser_use;
@@ -39,6 +40,8 @@ pub mod types;
pub const CONFIG_TOML_FILE: &str = "config.toml";
pub use application_requirements::ApplicationNetworkRequirementsToml;
pub use application_requirements::ApplicationRequirementsToml;
pub use auth_policy::ManagedAuthPolicy;
pub use browser_computer_use_requirements::AllowDenyRequirementToml;
pub use browser_computer_use_requirements::BrowserUseAccessApprovalLifetimeToml;
+6 -2
View File
@@ -98,7 +98,7 @@ fn merge_toml_values_at_path(base: &mut TomlValue, overlay: &TomlValue, path: &m
normalize_key_aliases(path, base_table);
let mut overlay_table = overlay_table.clone();
normalize_key_aliases(path, &mut overlay_table);
if is_permission_network_domains_path(path) {
if is_network_domains_path(path) {
normalize_network_domain_keys(base_table);
normalize_network_domain_keys(&mut overlay_table);
}
@@ -187,11 +187,15 @@ pub fn shell_environment_filter_entry<'a>(
.find(|(candidate, _)| candidate.to_lowercase() == pattern)
}
fn is_permission_network_domains_path(path: &[String]) -> bool {
fn is_network_domains_path(path: &[String]) -> bool {
matches!(
path,
[permissions, _, network, domains]
if permissions == "permissions" && network == "network" && domains == "domains"
) || matches!(
path,
[application, network, domains]
if application == "application" && network == "network" && domains == "domains"
)
}
@@ -245,6 +245,7 @@ fn populate_merged_regular_fields_with_sources(
rules: _,
enforce_residency,
network,
application,
permissions,
auto_review,
models,
@@ -296,6 +297,7 @@ fn populate_merged_regular_fields_with_sources(
set_sourced!(apps, &["apps"]);
set_sourced!(enforce_residency, &["enforce_residency"]);
set_sourced!(network, &["experimental_network"]);
set_sourced!(application, &["application"]);
set_sourced!(permissions, &["permissions"]);
set_sourced!(models, &["models"]);
set_sourced!(
+1
View File
@@ -9971,6 +9971,7 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset()
default_permissions: None,
remote_sandbox_config: None,
allowed_web_search_modes: Some(vec![codex_config::WebSearchModeRequirement::Cached]),
application: None,
allow_managed_hooks_only: None,
allow_appshots: None,
allow_remote_control: None,
+1
View File
@@ -3220,6 +3220,7 @@ impl Config {
exec_policy: _,
enforce_residency,
network: network_requirements,
application: _,
filesystem: filesystem_requirements,
additional_developer_instructions: _,
guardian_policy_config_source: _,
@@ -169,7 +169,7 @@ async fn shared_overview_keeps_rows_and_replays_changes_over_stale_reads() -> Re
Ok(())
}
#[tokio::test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn shared_overview_seeds_once_and_retains_locally_resumed_history() -> Result<()> {
let mut app = make_test_app().await;
let mut ids = Vec::new();
@@ -3358,8 +3358,7 @@ fn session_lifecycle_avoids_redundant_subagent_metadata_reads() -> Result<()> {
};
threads.push(discovered);
}
app.handle_event(&mut tui, &mut app_server, completion)
.await?;
Box::pin(app.handle_event(&mut tui, &mut app_server, completion)).await?;
assert_eq!(
app.agent_navigation
.ordered_threads()
+1
View File
@@ -955,6 +955,7 @@ interrupt_message = false
};
let requirements_toml = ConfigRequirementsToml {
application: None,
allowed_login_methods: None,
allowed_chatgpt_workspaces: None,
cli_auth_credentials_store: None,