mirror of
https://github.com/openai/codex.git
synced 2026-11-10 08:43:33 +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:
1 parent
88912c04cd
commit
b27a6321fa
27 files changed
+658
-5
No files matched your search
+34
@@ -7482,6 +7482,40 @@
|
|||||||
"AppToolsConfig": {
|
"AppToolsConfig": {
|
||||||
"type": "object"
|
"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": {
|
"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.",
|
"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": [
|
"enum": [
|
||||||
|
|||||||
+34
@@ -966,6 +966,40 @@
|
|||||||
"AppToolsConfig": {
|
"AppToolsConfig": {
|
||||||
"type": "object"
|
"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": {
|
"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.",
|
"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": [
|
"enum": [
|
||||||
|
|||||||
+34
@@ -8,6 +8,40 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"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": {
|
"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.",
|
"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": [
|
"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 { AppToolApproval } from "./AppToolApproval";
|
||||||
export type { AppToolSummary } from "./AppToolSummary";
|
export type { AppToolSummary } from "./AppToolSummary";
|
||||||
export type { AppToolsConfig } from "./AppToolsConfig";
|
export type { AppToolsConfig } from "./AppToolsConfig";
|
||||||
|
export type { ApplicationNetworkRequirements } from "./ApplicationNetworkRequirements";
|
||||||
|
export type { ApplicationRequirements } from "./ApplicationRequirements";
|
||||||
export type { ApprovalsReviewer } from "./ApprovalsReviewer";
|
export type { ApprovalsReviewer } from "./ApprovalsReviewer";
|
||||||
export type { AppsConfig } from "./AppsConfig";
|
export type { AppsConfig } from "./AppsConfig";
|
||||||
export type { AppsDefaultConfig } from "./AppsDefaultConfig";
|
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::ApprovalsReviewer;
|
||||||
use super::AskForApproval;
|
use super::AskForApproval;
|
||||||
use super::BrowserUseConfig;
|
use super::BrowserUseConfig;
|
||||||
@@ -432,6 +433,8 @@ pub struct ConfigRequirements {
|
|||||||
pub enforce_residency: Option<ResidencyRequirement>,
|
pub enforce_residency: Option<ResidencyRequirement>,
|
||||||
#[experimental("configRequirements/read.network")]
|
#[experimental("configRequirements/read.network")]
|
||||||
pub network: Option<NetworkRequirements>,
|
pub network: Option<NetworkRequirements>,
|
||||||
|
#[experimental("configRequirements/read.application")]
|
||||||
|
pub application: Option<ApplicationRequirements>,
|
||||||
pub auto_review: Option<AutoReviewRequirements>,
|
pub auto_review: Option<AutoReviewRequirements>,
|
||||||
pub models: Option<ModelsRequirements>,
|
pub models: Option<ModelsRequirements>,
|
||||||
#[schemars(with = "Option<String>")]
|
#[schemars(with = "Option<String>")]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
mod shared;
|
mod shared;
|
||||||
|
|
||||||
mod account;
|
mod account;
|
||||||
|
mod application;
|
||||||
mod apps;
|
mod apps;
|
||||||
mod attestation;
|
mod attestation;
|
||||||
mod bedrock;
|
mod bedrock;
|
||||||
@@ -35,6 +36,7 @@ mod turn;
|
|||||||
mod windows_sandbox;
|
mod windows_sandbox;
|
||||||
|
|
||||||
pub use account::*;
|
pub use account::*;
|
||||||
|
pub use application::*;
|
||||||
pub use apps::*;
|
pub use apps::*;
|
||||||
pub use attestation::*;
|
pub use attestation::*;
|
||||||
pub use bedrock::*;
|
pub use bedrock::*;
|
||||||
|
|||||||
@@ -2112,6 +2112,7 @@ fn config_approvals_reviewer_is_marked_experimental() {
|
|||||||
fn config_requirements_granular_allowed_approval_policy_is_marked_experimental() {
|
fn config_requirements_granular_allowed_approval_policy_is_marked_experimental() {
|
||||||
let reason =
|
let reason =
|
||||||
crate::experimental_api::ExperimentalApi::experimental_reason(&ConfigRequirements {
|
crate::experimental_api::ExperimentalApi::experimental_reason(&ConfigRequirements {
|
||||||
|
application: None,
|
||||||
cli_auth_credentials_store: None,
|
cli_auth_credentials_store: None,
|
||||||
chatgpt_base_url: None,
|
chatgpt_base_url: None,
|
||||||
additional_developer_instructions: 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,
|
internal-error response. Tool results with `isError: true` remain results,
|
||||||
including their `_meta`.
|
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 configuration scope
|
||||||
|
|
||||||
Plugin activation and MCP settings use the existing merged configuration, including
|
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);
|
.and_then(|windows| windows.sandbox_private_desktop);
|
||||||
|
|
||||||
ConfigRequirements {
|
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(
|
cli_auth_credentials_store: requirements.cli_auth_credentials_store.map(
|
||||||
|mode| match mode {
|
|mode| match mode {
|
||||||
codex_config::types::AuthCredentialsStoreMode::File => {
|
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)]
|
#[cfg(unix)]
|
||||||
mod command_exec;
|
mod command_exec;
|
||||||
mod compaction;
|
mod compaction;
|
||||||
|
mod config_requirements_application;
|
||||||
mod config_requirements_in_app_browser;
|
mod config_requirements_in_app_browser;
|
||||||
mod config_rpc;
|
mod config_rpc;
|
||||||
mod connection_handling_websocket;
|
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_features::FeatureToml;
|
||||||
use codex_protocol::config_types::ApprovalsReviewer;
|
use codex_protocol::config_types::ApprovalsReviewer;
|
||||||
use codex_protocol::config_types::ForcedLoginMethod;
|
use codex_protocol::config_types::ForcedLoginMethod;
|
||||||
@@ -189,6 +190,7 @@ pub struct ConfigRequirements {
|
|||||||
pub enforce_residency: ConstrainedWithSource<Option<ResidencyRequirement>>,
|
pub enforce_residency: ConstrainedWithSource<Option<ResidencyRequirement>>,
|
||||||
/// Managed network constraints derived from requirements.
|
/// Managed network constraints derived from requirements.
|
||||||
pub network: Option<Sourced<NetworkConstraints>>,
|
pub network: Option<Sourced<NetworkConstraints>>,
|
||||||
|
pub application: Option<Sourced<ApplicationRequirementsToml>>,
|
||||||
/// Managed filesystem constraints derived from requirements.
|
/// Managed filesystem constraints derived from requirements.
|
||||||
pub filesystem: Option<Sourced<FilesystemConstraints>>,
|
pub filesystem: Option<Sourced<FilesystemConstraints>>,
|
||||||
/// Managed instructions included independently of ordinary developer instructions.
|
/// Managed instructions included independently of ordinary developer instructions.
|
||||||
@@ -247,6 +249,7 @@ impl Default for ConfigRequirements {
|
|||||||
/*source*/ None,
|
/*source*/ None,
|
||||||
),
|
),
|
||||||
network: None,
|
network: None,
|
||||||
|
application: None,
|
||||||
filesystem: None,
|
filesystem: None,
|
||||||
additional_developer_instructions: None,
|
additional_developer_instructions: None,
|
||||||
guardian_policy_config_source: None,
|
guardian_policy_config_source: None,
|
||||||
@@ -1015,6 +1018,7 @@ pub struct ConfigRequirementsToml {
|
|||||||
pub enforce_residency: Option<ResidencyRequirement>,
|
pub enforce_residency: Option<ResidencyRequirement>,
|
||||||
#[serde(rename = "experimental_network")]
|
#[serde(rename = "experimental_network")]
|
||||||
pub network: Option<NetworkRequirementsToml>,
|
pub network: Option<NetworkRequirementsToml>,
|
||||||
|
pub application: Option<ApplicationRequirementsToml>,
|
||||||
pub permissions: Option<PermissionsRequirementsToml>,
|
pub permissions: Option<PermissionsRequirementsToml>,
|
||||||
pub auto_review: Option<AutoReviewRequirementsToml>,
|
pub auto_review: Option<AutoReviewRequirementsToml>,
|
||||||
pub models: Option<ModelsRequirementsToml>,
|
pub models: Option<ModelsRequirementsToml>,
|
||||||
@@ -1117,6 +1121,7 @@ pub struct ConfigRequirementsWithSources {
|
|||||||
pub rules: Option<Sourced<RequirementsExecPolicyToml>>,
|
pub rules: Option<Sourced<RequirementsExecPolicyToml>>,
|
||||||
pub enforce_residency: Option<Sourced<ResidencyRequirement>>,
|
pub enforce_residency: Option<Sourced<ResidencyRequirement>>,
|
||||||
pub network: Option<Sourced<NetworkRequirementsToml>>,
|
pub network: Option<Sourced<NetworkRequirementsToml>>,
|
||||||
|
pub application: Option<Sourced<ApplicationRequirementsToml>>,
|
||||||
pub permissions: Option<Sourced<PermissionsRequirementsToml>>,
|
pub permissions: Option<Sourced<PermissionsRequirementsToml>>,
|
||||||
pub auto_review: Option<Sourced<AutoReviewRequirementsToml>>,
|
pub auto_review: Option<Sourced<AutoReviewRequirementsToml>>,
|
||||||
pub models: Option<Sourced<ModelsRequirementsToml>>,
|
pub models: Option<Sourced<ModelsRequirementsToml>>,
|
||||||
@@ -1177,6 +1182,7 @@ impl ConfigRequirementsWithSources {
|
|||||||
rules: _,
|
rules: _,
|
||||||
enforce_residency: _,
|
enforce_residency: _,
|
||||||
network: _,
|
network: _,
|
||||||
|
application: _,
|
||||||
permissions: _,
|
permissions: _,
|
||||||
auto_review: _,
|
auto_review: _,
|
||||||
models: _,
|
models: _,
|
||||||
@@ -1229,6 +1235,7 @@ impl ConfigRequirementsWithSources {
|
|||||||
rules,
|
rules,
|
||||||
enforce_residency,
|
enforce_residency,
|
||||||
network,
|
network,
|
||||||
|
application,
|
||||||
permissions,
|
permissions,
|
||||||
models,
|
models,
|
||||||
additional_developer_instructions,
|
additional_developer_instructions,
|
||||||
@@ -1312,6 +1319,7 @@ impl ConfigRequirementsWithSources {
|
|||||||
rules,
|
rules,
|
||||||
enforce_residency,
|
enforce_residency,
|
||||||
network,
|
network,
|
||||||
|
application,
|
||||||
permissions,
|
permissions,
|
||||||
auto_review,
|
auto_review,
|
||||||
models,
|
models,
|
||||||
@@ -1354,6 +1362,7 @@ impl ConfigRequirementsWithSources {
|
|||||||
rules: rules.map(|sourced| sourced.value),
|
rules: rules.map(|sourced| sourced.value),
|
||||||
enforce_residency: enforce_residency.map(|sourced| sourced.value),
|
enforce_residency: enforce_residency.map(|sourced| sourced.value),
|
||||||
network: network.map(|sourced| sourced.value),
|
network: network.map(|sourced| sourced.value),
|
||||||
|
application: application.map(|sourced| sourced.value),
|
||||||
permissions: permissions.map(|sourced| sourced.value),
|
permissions: permissions.map(|sourced| sourced.value),
|
||||||
auto_review: auto_review.map(|sourced| sourced.value),
|
auto_review: auto_review.map(|sourced| sourced.value),
|
||||||
models: models.map(|sourced| sourced.value),
|
models: models.map(|sourced| sourced.value),
|
||||||
@@ -1492,6 +1501,10 @@ impl ConfigRequirementsToml {
|
|||||||
&& self.rules.is_none()
|
&& self.rules.is_none()
|
||||||
&& self.enforce_residency.is_none()
|
&& self.enforce_residency.is_none()
|
||||||
&& self.network.is_none()
|
&& self.network.is_none()
|
||||||
|
&& self
|
||||||
|
.application
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|application| application.network.is_none())
|
||||||
&& self.permissions.is_none()
|
&& self.permissions.is_none()
|
||||||
&& self.auto_review.as_ref().is_none_or(|auto_review| {
|
&& self.auto_review.as_ref().is_none_or(|auto_review| {
|
||||||
auto_review.ignore_rules.as_ref().is_none_or(Vec::is_empty)
|
auto_review.ignore_rules.as_ref().is_none_or(Vec::is_empty)
|
||||||
@@ -1678,6 +1691,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
|||||||
rules,
|
rules,
|
||||||
enforce_residency,
|
enforce_residency,
|
||||||
network,
|
network,
|
||||||
|
application,
|
||||||
permissions,
|
permissions,
|
||||||
auto_review,
|
auto_review,
|
||||||
models: _,
|
models: _,
|
||||||
@@ -2037,6 +2051,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
|||||||
exec_policy,
|
exec_policy,
|
||||||
enforce_residency,
|
enforce_residency,
|
||||||
network,
|
network,
|
||||||
|
application,
|
||||||
filesystem,
|
filesystem,
|
||||||
additional_developer_instructions,
|
additional_developer_instructions,
|
||||||
guardian_policy_config_source,
|
guardian_policy_config_source,
|
||||||
@@ -2215,6 +2230,7 @@ mod tests {
|
|||||||
rules,
|
rules,
|
||||||
enforce_residency,
|
enforce_residency,
|
||||||
network,
|
network,
|
||||||
|
application,
|
||||||
permissions,
|
permissions,
|
||||||
auto_review,
|
auto_review,
|
||||||
models,
|
models,
|
||||||
@@ -2275,6 +2291,7 @@ mod tests {
|
|||||||
enforce_residency: enforce_residency
|
enforce_residency: enforce_residency
|
||||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||||
network: network.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)),
|
permissions: permissions.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||||
auto_review: auto_review.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)),
|
models: models.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||||
@@ -2777,6 +2794,7 @@ mod tests {
|
|||||||
rules: None,
|
rules: None,
|
||||||
enforce_residency: Some(enforce_residency),
|
enforce_residency: Some(enforce_residency),
|
||||||
network: None,
|
network: None,
|
||||||
|
application: None,
|
||||||
permissions: None,
|
permissions: None,
|
||||||
auto_review: Some(auto_review.clone()),
|
auto_review: Some(auto_review.clone()),
|
||||||
models: Some(models.clone()),
|
models: Some(models.clone()),
|
||||||
@@ -2861,6 +2879,7 @@ mod tests {
|
|||||||
rules: None,
|
rules: None,
|
||||||
enforce_residency: Some(Sourced::new(enforce_residency, enforce_source)),
|
enforce_residency: Some(Sourced::new(enforce_residency, enforce_source)),
|
||||||
network: None,
|
network: None,
|
||||||
|
application: None,
|
||||||
permissions: None,
|
permissions: None,
|
||||||
auto_review: Some(Sourced::new(auto_review, source.clone())),
|
auto_review: Some(Sourced::new(auto_review, source.clone())),
|
||||||
models: Some(Sourced::new(models, source.clone())),
|
models: Some(Sourced::new(models, source.clone())),
|
||||||
@@ -2916,6 +2935,7 @@ mod tests {
|
|||||||
rules: None,
|
rules: None,
|
||||||
enforce_residency: None,
|
enforce_residency: None,
|
||||||
network: None,
|
network: None,
|
||||||
|
application: None,
|
||||||
permissions: None,
|
permissions: None,
|
||||||
models: None,
|
models: None,
|
||||||
guardian_policy_config: None,
|
guardian_policy_config: None,
|
||||||
@@ -2975,6 +2995,7 @@ mod tests {
|
|||||||
rules: None,
|
rules: None,
|
||||||
enforce_residency: None,
|
enforce_residency: None,
|
||||||
network: None,
|
network: None,
|
||||||
|
application: None,
|
||||||
permissions: None,
|
permissions: None,
|
||||||
models: None,
|
models: None,
|
||||||
guardian_policy_config: None,
|
guardian_policy_config: None,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
mod application_requirements;
|
||||||
mod auth_policy;
|
mod auth_policy;
|
||||||
mod browser_computer_use_requirements;
|
mod browser_computer_use_requirements;
|
||||||
mod browser_use;
|
mod browser_use;
|
||||||
@@ -39,6 +40,8 @@ pub mod types;
|
|||||||
|
|
||||||
pub const CONFIG_TOML_FILE: &str = "config.toml";
|
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 auth_policy::ManagedAuthPolicy;
|
||||||
pub use browser_computer_use_requirements::AllowDenyRequirementToml;
|
pub use browser_computer_use_requirements::AllowDenyRequirementToml;
|
||||||
pub use browser_computer_use_requirements::BrowserUseAccessApprovalLifetimeToml;
|
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);
|
normalize_key_aliases(path, base_table);
|
||||||
let mut overlay_table = overlay_table.clone();
|
let mut overlay_table = overlay_table.clone();
|
||||||
normalize_key_aliases(path, &mut overlay_table);
|
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(base_table);
|
||||||
normalize_network_domain_keys(&mut overlay_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)
|
.find(|(candidate, _)| candidate.to_lowercase() == pattern)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_permission_network_domains_path(path: &[String]) -> bool {
|
fn is_network_domains_path(path: &[String]) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
path,
|
path,
|
||||||
[permissions, _, network, domains]
|
[permissions, _, network, domains]
|
||||||
if permissions == "permissions" && network == "network" && domains == "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: _,
|
rules: _,
|
||||||
enforce_residency,
|
enforce_residency,
|
||||||
network,
|
network,
|
||||||
|
application,
|
||||||
permissions,
|
permissions,
|
||||||
auto_review,
|
auto_review,
|
||||||
models,
|
models,
|
||||||
@@ -296,6 +297,7 @@ fn populate_merged_regular_fields_with_sources(
|
|||||||
set_sourced!(apps, &["apps"]);
|
set_sourced!(apps, &["apps"]);
|
||||||
set_sourced!(enforce_residency, &["enforce_residency"]);
|
set_sourced!(enforce_residency, &["enforce_residency"]);
|
||||||
set_sourced!(network, &["experimental_network"]);
|
set_sourced!(network, &["experimental_network"]);
|
||||||
|
set_sourced!(application, &["application"]);
|
||||||
set_sourced!(permissions, &["permissions"]);
|
set_sourced!(permissions, &["permissions"]);
|
||||||
set_sourced!(models, &["models"]);
|
set_sourced!(models, &["models"]);
|
||||||
set_sourced!(
|
set_sourced!(
|
||||||
|
|||||||
@@ -9971,6 +9971,7 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset()
|
|||||||
default_permissions: None,
|
default_permissions: None,
|
||||||
remote_sandbox_config: None,
|
remote_sandbox_config: None,
|
||||||
allowed_web_search_modes: Some(vec![codex_config::WebSearchModeRequirement::Cached]),
|
allowed_web_search_modes: Some(vec![codex_config::WebSearchModeRequirement::Cached]),
|
||||||
|
application: None,
|
||||||
allow_managed_hooks_only: None,
|
allow_managed_hooks_only: None,
|
||||||
allow_appshots: None,
|
allow_appshots: None,
|
||||||
allow_remote_control: None,
|
allow_remote_control: None,
|
||||||
|
|||||||
@@ -3220,6 +3220,7 @@ impl Config {
|
|||||||
exec_policy: _,
|
exec_policy: _,
|
||||||
enforce_residency,
|
enforce_residency,
|
||||||
network: network_requirements,
|
network: network_requirements,
|
||||||
|
application: _,
|
||||||
filesystem: filesystem_requirements,
|
filesystem: filesystem_requirements,
|
||||||
additional_developer_instructions: _,
|
additional_developer_instructions: _,
|
||||||
guardian_policy_config_source: _,
|
guardian_policy_config_source: _,
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ async fn shared_overview_keeps_rows_and_replays_changes_over_stale_reads() -> Re
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn shared_overview_seeds_once_and_retains_locally_resumed_history() -> Result<()> {
|
async fn shared_overview_seeds_once_and_retains_locally_resumed_history() -> Result<()> {
|
||||||
let mut app = make_test_app().await;
|
let mut app = make_test_app().await;
|
||||||
let mut ids = Vec::new();
|
let mut ids = Vec::new();
|
||||||
|
|||||||
@@ -3358,8 +3358,7 @@ fn session_lifecycle_avoids_redundant_subagent_metadata_reads() -> Result<()> {
|
|||||||
};
|
};
|
||||||
threads.push(discovered);
|
threads.push(discovered);
|
||||||
}
|
}
|
||||||
app.handle_event(&mut tui, &mut app_server, completion)
|
Box::pin(app.handle_event(&mut tui, &mut app_server, completion)).await?;
|
||||||
.await?;
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.agent_navigation
|
app.agent_navigation
|
||||||
.ordered_threads()
|
.ordered_threads()
|
||||||
|
|||||||
@@ -955,6 +955,7 @@ interrupt_message = false
|
|||||||
};
|
};
|
||||||
|
|
||||||
let requirements_toml = ConfigRequirementsToml {
|
let requirements_toml = ConfigRequirementsToml {
|
||||||
|
application: None,
|
||||||
allowed_login_methods: None,
|
allowed_login_methods: None,
|
||||||
allowed_chatgpt_workspaces: None,
|
allowed_chatgpt_workspaces: None,
|
||||||
cli_auth_credentials_store: None,
|
cli_auth_credentials_store: None,
|
||||||
|
|||||||
Reference in new issue
Block a user