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