chore: Nest skill and protocol network permissions under network.enabled (#13427)

## Summary

Changes the permission profile shape from a bare network boolean to a
nested object.

Before:

```yaml
permissions:
  network: true
```

After:

```yaml
permissions:
  network:
    enabled: true
```

This also updates the shared Rust and app-server protocol types so
`PermissionProfile.network` is no longer `Option<bool>`, but
`Option<NetworkPermissions>` with `enabled: Option<bool>`.

## What Changed

- Updated `PermissionProfile` in `codex-rs/protocol/src/models.rs`:
- `pub network: Option<bool>` -> `pub network:
Option<NetworkPermissions>`
- Added `NetworkPermissions` with:
  - `pub enabled: Option<bool>`
- Changed emptiness semantics so `network` is only considered empty when
`enabled` is `None`
- Updated skill metadata parsing to accept `permissions.network.enabled`
- Updated core permission consumers to read
`network.enabled.unwrap_or(false)` where a concrete boolean is needed
- Updated app-server v2 protocol types and regenerated schema/TypeScript
outputs
- Updated docs to mention `additionalPermissions.network.enabled`
This commit is contained in:
Celia Chen
2026-03-03 20:57:29 -08:00
committed by GitHub
Unverified
parent 2e154a35bc
commit d622bff384
19 changed files with 216 additions and 40 deletions
@@ -65,6 +65,17 @@
},
"type": "object"
},
"AdditionalNetworkPermissions": {
"properties": {
"enabled": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"AdditionalPermissionProfile": {
"properties": {
"fileSystem": {
@@ -88,9 +99,13 @@
]
},
"network": {
"type": [
"boolean",
"null"
"anyOf": [
{
"$ref": "#/definitions/AdditionalNetworkPermissions"
},
{
"type": "null"
}
]
}
},
@@ -3888,6 +3888,17 @@
],
"type": "string"
},
"NetworkPermissions": {
"properties": {
"enabled": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"NetworkPolicyAmendment": {
"properties": {
"action": {
@@ -4052,9 +4063,13 @@
]
},
"network": {
"type": [
"boolean",
"null"
"anyOf": [
{
"$ref": "#/definitions/NetworkPermissions"
},
{
"type": "null"
}
]
}
},
@@ -65,6 +65,17 @@
},
"type": "object"
},
"AdditionalNetworkPermissions": {
"properties": {
"enabled": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"AdditionalPermissionProfile": {
"properties": {
"fileSystem": {
@@ -88,9 +99,13 @@
]
},
"network": {
"type": [
"boolean",
"null"
"anyOf": [
{
"$ref": "#/definitions/AdditionalNetworkPermissions"
},
{
"type": "null"
}
]
}
},
@@ -61,6 +61,17 @@
},
"type": "object"
},
"AdditionalNetworkPermissions": {
"properties": {
"enabled": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"AdditionalPermissionProfile": {
"properties": {
"fileSystem": {
@@ -84,9 +95,13 @@
]
},
"network": {
"type": [
"boolean",
"null"
"anyOf": [
{
"$ref": "#/definitions/AdditionalNetworkPermissions"
},
{
"type": "null"
}
]
}
},
@@ -5272,6 +5287,17 @@
],
"type": "string"
},
"NetworkPermissions": {
"properties": {
"enabled": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"NetworkPolicyAmendment": {
"properties": {
"action": {
@@ -5428,9 +5454,13 @@
]
},
"network": {
"type": [
"boolean",
"null"
"anyOf": [
{
"$ref": "#/definitions/NetworkPermissions"
},
{
"type": "null"
}
]
}
},
@@ -7824,6 +7824,17 @@
],
"type": "string"
},
"NetworkPermissions": {
"properties": {
"enabled": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"NetworkPolicyAmendment": {
"properties": {
"action": {
@@ -8148,9 +8159,13 @@
]
},
"network": {
"type": [
"boolean",
"null"
"anyOf": [
{
"$ref": "#/definitions/NetworkPermissions"
},
{
"type": "null"
}
]
}
},
@@ -0,0 +1,5 @@
// 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.
export type NetworkPermissions = { enabled: boolean | null, };
@@ -3,5 +3,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { FileSystemPermissions } from "./FileSystemPermissions";
import type { MacOsPermissions } from "./MacOsPermissions";
import type { NetworkPermissions } from "./NetworkPermissions";
export type PermissionProfile = { network: boolean | null, file_system: FileSystemPermissions | null, macos: MacOsPermissions | null, };
export type PermissionProfile = { network: NetworkPermissions | null, file_system: FileSystemPermissions | null, macos: MacOsPermissions | null, };
@@ -115,6 +115,7 @@ export type { ModelRerouteReason } from "./ModelRerouteReason";
export type { NetworkAccess } from "./NetworkAccess";
export type { NetworkApprovalContext } from "./NetworkApprovalContext";
export type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol";
export type { NetworkPermissions } from "./NetworkPermissions";
export type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment";
export type { NetworkPolicyRuleAction } from "./NetworkPolicyRuleAction";
export type { ParsedCommand } from "./ParsedCommand";
@@ -0,0 +1,5 @@
// 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.
export type AdditionalNetworkPermissions = { enabled: boolean | null, };
@@ -3,5 +3,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions";
import type { AdditionalMacOsPermissions } from "./AdditionalMacOsPermissions";
import type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions";
export type AdditionalPermissionProfile = { network: boolean | null, fileSystem: AdditionalFileSystemPermissions | null, macos: AdditionalMacOsPermissions | null, };
export type AdditionalPermissionProfile = { network: AdditionalNetworkPermissions | null, fileSystem: AdditionalFileSystemPermissions | null, macos: AdditionalMacOsPermissions | null, };
@@ -6,6 +6,7 @@ export type { AccountRateLimitsUpdatedNotification } from "./AccountRateLimitsUp
export type { AccountUpdatedNotification } from "./AccountUpdatedNotification";
export type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions";
export type { AdditionalMacOsPermissions } from "./AdditionalMacOsPermissions";
export type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions";
export type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile";
export type { AgentMessageDeltaNotification } from "./AgentMessageDeltaNotification";
export type { AnalyticsConfig } from "./AnalyticsConfig";
@@ -31,6 +31,7 @@ use codex_protocol::models::MacOsAutomationValue as CoreMacOsAutomationValue;
use codex_protocol::models::MacOsPermissions as CoreMacOsPermissions;
use codex_protocol::models::MacOsPreferencesValue as CoreMacOsPreferencesValue;
use codex_protocol::models::MessagePhase;
use codex_protocol::models::NetworkPermissions as CoreNetworkPermissions;
use codex_protocol::models::PermissionProfile as CorePermissionProfile;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::InputModality;
@@ -852,11 +853,26 @@ impl From<CoreMacOsPermissions> for AdditionalMacOsPermissions {
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AdditionalNetworkPermissions {
pub enabled: Option<bool>,
}
impl From<CoreNetworkPermissions> for AdditionalNetworkPermissions {
fn from(value: CoreNetworkPermissions) -> Self {
Self {
enabled: value.enabled,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AdditionalPermissionProfile {
pub network: Option<bool>,
pub network: Option<AdditionalNetworkPermissions>,
pub file_system: Option<AdditionalFileSystemPermissions>,
pub macos: Option<AdditionalMacOsPermissions>,
}
@@ -864,7 +880,7 @@ pub struct AdditionalPermissionProfile {
impl From<CorePermissionProfile> for AdditionalPermissionProfile {
fn from(value: CorePermissionProfile) -> Self {
Self {
network: value.network,
network: value.network.map(AdditionalNetworkPermissions::from),
file_system: value.file_system.map(AdditionalFileSystemPermissions::from),
macos: value.macos.map(AdditionalMacOsPermissions::from),
}
+1 -1
View File
@@ -742,7 +742,7 @@ Certain actions (shell commands or modifying files) may require explicit user ap
Order of messages:
1. `item/started` — shows the pending `commandExecution` item with `command`, `cwd`, and other fields so you can render the proposed action.
2. `item/commandExecution/requestApproval` (request) — carries the same `itemId`, `threadId`, `turnId`, optionally `approvalId` (for subcommand callbacks), and `reason`. For normal command approvals, it also includes `command`, `cwd`, and `commandActions` for friendly display. When `initialize.params.capabilities.experimentalApi = true`, it may also include experimental `additionalPermissions` describing requested per-command sandbox access; any filesystem paths in that payload are absolute on the wire. For network-only approvals, those command fields may be omitted and `networkApprovalContext` is provided instead. Optional persistence hints may also be included via `proposedExecpolicyAmendment` and `proposedNetworkPolicyAmendments`. Clients can prefer `availableDecisions` when present to render the exact set of choices the server wants to expose, while still falling back to the older heuristics if it is omitted.
2. `item/commandExecution/requestApproval` (request) — carries the same `itemId`, `threadId`, `turnId`, optionally `approvalId` (for subcommand callbacks), and `reason`. For normal command approvals, it also includes `command`, `cwd`, and `commandActions` for friendly display. When `initialize.params.capabilities.experimentalApi = true`, it may also include experimental `additionalPermissions` describing requested per-command sandbox access; any filesystem paths in that payload are absolute on the wire, and network access is represented as `additionalPermissions.network.enabled`. For network-only approvals, those command fields may be omitted and `networkApprovalContext` is provided instead. Optional persistence hints may also be included via `proposedExecpolicyAmendment` and `proposedNetworkPolicyAmendments`. Clients can prefer `availableDecisions` when present to render the exact set of choices the server wants to expose, while still falling back to the older heuristics if it is omitted.
3. Client response — for example `{ "decision": "accept" }`, `{ "decision": "acceptForSession" }`, `{ "decision": { "acceptWithExecpolicyAmendment": { "execpolicy_amendment": [...] } } }`, `{ "decision": { "applyNetworkPolicyAmendment": { "network_policy_amendment": { "host": "example.com", "action": "allow" } } } }`, `{ "decision": "decline" }`, or `{ "decision": "cancel" }`.
4. `serverRequest/resolved``{ threadId, requestId }` confirms the pending request has been resolved or cleared, including lifecycle cleanup on turn start/complete/interrupt.
5. `item/completed` — final `commandExecution` item with `status: "completed" | "failed" | "declined"` and execution output. Render this as the authoritative result.
+19 -4
View File
@@ -193,7 +193,12 @@ fn merge_network_access(
base_network_access: bool,
additional_permissions: &PermissionProfile,
) -> bool {
base_network_access || matches!(additional_permissions.network, Some(true))
base_network_access
|| additional_permissions
.network
.as_ref()
.and_then(|network| network.enabled)
.unwrap_or(false)
}
fn sandbox_policy_with_additional_permissions(
@@ -431,6 +436,7 @@ mod tests {
use crate::tools::sandboxing::SandboxablePreference;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::NetworkPermissions;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use dunce::canonicalize;
@@ -470,7 +476,9 @@ mod tests {
)
.expect("absolute temp dir");
let permissions = normalize_additional_permissions(PermissionProfile {
network: Some(true),
network: Some(NetworkPermissions {
enabled: Some(true),
}),
file_system: Some(FileSystemPermissions {
read: Some(vec![path.clone()]),
write: Some(vec![path.clone()]),
@@ -479,7 +487,12 @@ mod tests {
})
.expect("permissions");
assert_eq!(permissions.network, Some(true));
assert_eq!(
permissions.network,
Some(NetworkPermissions {
enabled: Some(true),
})
);
assert_eq!(
permissions.file_system,
Some(FileSystemPermissions {
@@ -505,7 +518,9 @@ mod tests {
network_access: false,
},
&PermissionProfile {
network: Some(true),
network: Some(NetworkPermissions {
enabled: Some(true),
}),
file_system: Some(FileSystemPermissions {
read: Some(vec![path.clone()]),
write: Some(Vec::new()),
+7 -2
View File
@@ -32,6 +32,8 @@ use tracing::error;
#[cfg(test)]
use crate::config::Config;
#[cfg(test)]
use codex_protocol::models::NetworkPermissions;
#[derive(Debug, Deserialize)]
struct SkillFrontmatter {
@@ -1387,7 +1389,8 @@ policy: {}
skill_dir,
r#"
permissions:
network: true
network:
enabled: true
file_system:
read:
- "./data"
@@ -1408,7 +1411,9 @@ permissions:
assert_eq!(
outcome.skills[0].permission_profile,
Some(PermissionProfile {
network: Some(true),
network: Some(NetworkPermissions {
enabled: Some(true),
}),
file_system: Some(FileSystemPermissions {
read: Some(vec![
AbsolutePathBuf::try_from(normalized(skill_dir.join("data").as_path()))
+11 -4
View File
@@ -44,7 +44,7 @@ pub(crate) fn compile_permission_profile(
file_system,
macos,
} = permissions?;
let network_access = network.unwrap_or_default();
let network_access = network.and_then(|value| value.enabled).unwrap_or_default();
let file_system = file_system.unwrap_or_default();
let fs_read = normalize_permission_paths(
file_system.read.as_deref().unwrap_or_default(),
@@ -232,6 +232,7 @@ mod tests {
use codex_protocol::models::MacOsPermissions;
#[cfg(target_os = "macos")]
use codex_protocol::models::MacOsPreferencesValue;
use codex_protocol::models::NetworkPermissions;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
@@ -251,7 +252,9 @@ mod tests {
fs::create_dir_all(&read_dir).expect("read dir");
let profile = compile_permission_profile(Some(PermissionProfile {
network: Some(true),
network: Some(NetworkPermissions {
enabled: Some(true),
}),
file_system: Some(FileSystemPermissions {
read: Some(vec![
absolute_path(&skill_dir.join("data")),
@@ -318,7 +321,9 @@ mod tests {
fs::create_dir_all(&skill_dir).expect("skill dir");
let profile = compile_permission_profile(Some(PermissionProfile {
network: Some(true),
network: Some(NetworkPermissions {
enabled: Some(true),
}),
..Default::default()
}))
.expect("profile");
@@ -353,7 +358,9 @@ mod tests {
fs::create_dir_all(&read_dir).expect("read dir");
let profile = compile_permission_profile(Some(PermissionProfile {
network: Some(true),
network: Some(NetworkPermissions {
enabled: Some(true),
}),
file_system: Some(FileSystemPermissions {
read: Some(vec![absolute_path(&skill_dir.join("data"))]),
write: Some(Vec::new()),
+16 -2
View File
@@ -84,6 +84,17 @@ impl MacOsPermissions {
}
}
#[derive(Debug, Clone, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
pub struct NetworkPermissions {
pub enabled: Option<bool>,
}
impl NetworkPermissions {
pub fn is_empty(&self) -> bool {
self.enabled.is_none()
}
}
#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
#[serde(untagged)]
pub enum MacOsPreferencesValue {
@@ -126,14 +137,17 @@ pub struct MacOsSeatbeltProfileExtensions {
#[derive(Debug, Clone, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
pub struct PermissionProfile {
pub network: Option<bool>,
pub network: Option<NetworkPermissions>,
pub file_system: Option<FileSystemPermissions>,
pub macos: Option<MacOsPermissions>,
}
impl PermissionProfile {
pub fn is_empty(&self) -> bool {
self.network.is_none()
self.network
.as_ref()
.map(NetworkPermissions::is_empty)
.unwrap_or(true)
&& self
.file_system
.as_ref()
@@ -277,6 +277,7 @@ async fn handle_escalate_session_with_policy(
mod tests {
use super::*;
use codex_protocol::approvals::EscalationPermissions;
use codex_protocol::models::NetworkPermissions;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
@@ -513,14 +514,18 @@ mod tests {
Arc::new(DeterministicEscalationPolicy {
decision: EscalationDecision::escalate(EscalationExecution::Permissions(
EscalationPermissions::PermissionProfile(PermissionProfile {
network: Some(true),
network: Some(NetworkPermissions {
enabled: Some(true),
}),
..Default::default()
}),
)),
}),
Arc::new(PermissionAssertingShellCommandExecutor {
expected_permissions: EscalationPermissions::PermissionProfile(PermissionProfile {
network: Some(true),
network: Some(NetworkPermissions {
enabled: Some(true),
}),
..Default::default()
}),
}),
@@ -641,7 +641,12 @@ fn format_additional_permissions_rule(
additional_permissions: &PermissionProfile,
) -> Option<String> {
let mut parts = Vec::new();
if matches!(additional_permissions.network, Some(true)) {
if additional_permissions
.network
.as_ref()
.and_then(|network| network.enabled)
.unwrap_or(false)
{
parts.push("network".to_string());
}
if let Some(file_system) = additional_permissions.file_system.as_ref() {
@@ -721,6 +726,7 @@ mod tests {
use super::*;
use crate::app_event::AppEvent;
use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::NetworkPermissions;
use codex_protocol::protocol::ExecPolicyAmendment;
use codex_protocol::protocol::NetworkApprovalProtocol;
use codex_protocol::protocol::NetworkPolicyAmendment;
@@ -1077,7 +1083,9 @@ mod tests {
available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort],
network_approval_context: None,
additional_permissions: Some(PermissionProfile {
network: Some(true),
network: Some(NetworkPermissions {
enabled: Some(true),
}),
file_system: Some(FileSystemPermissions {
read: Some(vec![absolute_path("/tmp/readme.txt")]),
write: Some(vec![absolute_path("/tmp/out.txt")]),
@@ -1123,7 +1131,9 @@ mod tests {
available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort],
network_approval_context: None,
additional_permissions: Some(PermissionProfile {
network: Some(true),
network: Some(NetworkPermissions {
enabled: Some(true),
}),
file_system: Some(FileSystemPermissions {
read: Some(vec![absolute_path("/tmp/readme.txt")]),
write: Some(vec![absolute_path("/tmp/out.txt")]),