diff --git a/codex-rs/analytics/src/events.rs b/codex-rs/analytics/src/events.rs index 73f2886f2f49..98d0e6ff6b99 100644 --- a/codex-rs/analytics/src/events.rs +++ b/codex-rs/analytics/src/events.rs @@ -23,7 +23,7 @@ use codex_app_server_protocol::CodexErrorInfo; use codex_login::default_client::originator; use codex_plugin::PluginTelemetryMetadata; use codex_protocol::approvals::NetworkApprovalProtocol; -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::SandboxPermissions; use codex_protocol::protocol::GuardianAssessmentOutcome; use codex_protocol::protocol::GuardianCommandSource; @@ -180,17 +180,17 @@ pub enum GuardianApprovalRequestSource { pub enum GuardianReviewedAction { Shell { sandbox_permissions: SandboxPermissions, - additional_permissions: Option, + additional_permissions: Option, }, UnifiedExec { sandbox_permissions: SandboxPermissions, - additional_permissions: Option, + additional_permissions: Option, tty: bool, }, Execve { source: GuardianCommandSource, program: String, - additional_permissions: Option, + additional_permissions: Option, }, ApplyPatch {}, NetworkAccess { diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index db2065f750a2..d7631e157277 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1892,61 +1892,132 @@ "type": "string" }, "PermissionProfile": { - "properties": { - "fileSystem": { - "anyOf": [ - { + "oneOf": [ + { + "description": "Codex owns sandbox construction for this profile.", + "properties": { + "fileSystem": { "$ref": "#/definitions/PermissionProfileFileSystemPermissions" }, - { - "type": "null" + "network": { + "$ref": "#/definitions/PermissionProfileNetworkPermissions" + }, + "type": { + "enum": [ + "managed" + ], + "title": "ManagedPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "fileSystem", + "network", + "type" + ], + "title": "ManagedPermissionProfile", + "type": "object" }, - "network": { - "anyOf": [ - { + { + "description": "Do not apply an outer sandbox.", + "properties": { + "type": { + "enum": [ + "disabled" + ], + "title": "DisabledPermissionProfileType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "DisabledPermissionProfile", + "type": "object" + }, + { + "description": "Filesystem isolation is enforced by an external caller.", + "properties": { + "network": { "$ref": "#/definitions/PermissionProfileNetworkPermissions" }, - { - "type": "null" + "type": { + "enum": [ + "external" + ], + "title": "ExternalPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "network", + "type" + ], + "title": "ExternalPermissionProfile", + "type": "object" } - }, - "type": "object" + ] }, "PermissionProfileFileSystemPermissions": { - "properties": { - "entries": { - "items": { - "$ref": "#/definitions/FileSystemSandboxEntry" + "oneOf": [ + { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } }, - "type": "array" + "required": [ + "entries", + "type" + ], + "title": "RestrictedPermissionProfileFileSystemPermissions", + "type": "object" }, - "globScanMaxDepth": { - "format": "uint", - "minimum": 1.0, - "type": [ - "integer", - "null" - ] + { + "properties": { + "type": { + "enum": [ + "unrestricted" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissions", + "type": "object" } - }, - "required": [ - "entries" - ], - "type": "object" + ] }, "PermissionProfileNetworkPermissions": { "properties": { "enabled": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" } }, + "required": [ + "enabled" + ], "type": "object" }, "Personality": { diff --git a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json index 78e75d7c460c..76d265c5913a 100644 --- a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json +++ b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json @@ -78,7 +78,8 @@ { "type": "null" } - ] + ], + "description": "Partial overlay used for per-command permission requests." } }, "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/ServerRequest.json b/codex-rs/app-server-protocol/schema/json/ServerRequest.json index 84bf5247392b..50510adf9841 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ServerRequest.json @@ -78,7 +78,8 @@ { "type": "null" } - ] + ], + "description": "Partial overlay used for per-command permission requests." } }, "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 6904ef7165ec..0ef6502a9adc 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -25,7 +25,8 @@ { "type": "null" } - ] + ], + "description": "Partial overlay used for per-command permission requests." } }, "type": "object" @@ -11269,61 +11270,132 @@ ] }, "PermissionProfile": { - "properties": { - "fileSystem": { - "anyOf": [ - { + "oneOf": [ + { + "description": "Codex owns sandbox construction for this profile.", + "properties": { + "fileSystem": { "$ref": "#/definitions/v2/PermissionProfileFileSystemPermissions" }, - { - "type": "null" + "network": { + "$ref": "#/definitions/v2/PermissionProfileNetworkPermissions" + }, + "type": { + "enum": [ + "managed" + ], + "title": "ManagedPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "fileSystem", + "network", + "type" + ], + "title": "ManagedPermissionProfile", + "type": "object" }, - "network": { - "anyOf": [ - { + { + "description": "Do not apply an outer sandbox.", + "properties": { + "type": { + "enum": [ + "disabled" + ], + "title": "DisabledPermissionProfileType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "DisabledPermissionProfile", + "type": "object" + }, + { + "description": "Filesystem isolation is enforced by an external caller.", + "properties": { + "network": { "$ref": "#/definitions/v2/PermissionProfileNetworkPermissions" }, - { - "type": "null" + "type": { + "enum": [ + "external" + ], + "title": "ExternalPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "network", + "type" + ], + "title": "ExternalPermissionProfile", + "type": "object" } - }, - "type": "object" + ] }, "PermissionProfileFileSystemPermissions": { - "properties": { - "entries": { - "items": { - "$ref": "#/definitions/v2/FileSystemSandboxEntry" + "oneOf": [ + { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/v2/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } }, - "type": "array" + "required": [ + "entries", + "type" + ], + "title": "RestrictedPermissionProfileFileSystemPermissions", + "type": "object" }, - "globScanMaxDepth": { - "format": "uint", - "minimum": 1.0, - "type": [ - "integer", - "null" - ] + { + "properties": { + "type": { + "enum": [ + "unrestricted" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissions", + "type": "object" } - }, - "required": [ - "entries" - ], - "type": "object" + ] }, "PermissionProfileNetworkPermissions": { "properties": { "enabled": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" } }, + "required": [ + "enabled" + ], "type": "object" }, "Personality": { @@ -14544,7 +14616,7 @@ } ], "default": null, - "description": "Canonical active permissions view for this thread when representable. This is `null` for external sandbox policies because external enforcement cannot be round-tripped as a `PermissionProfile`." + "description": "Canonical active permissions view for this thread." }, "reasoningEffort": { "anyOf": [ @@ -15987,7 +16059,7 @@ } ], "default": null, - "description": "Canonical active permissions view for this thread when representable. This is `null` for external sandbox policies because external enforcement cannot be round-tripped as a `PermissionProfile`." + "description": "Canonical active permissions view for this thread." }, "reasoningEffort": { "anyOf": [ @@ -16314,7 +16386,7 @@ } ], "default": null, - "description": "Canonical active permissions view for this thread when representable. This is `null` for external sandbox policies because external enforcement cannot be round-tripped as a `PermissionProfile`." + "description": "Canonical active permissions view for this thread." }, "reasoningEffort": { "anyOf": [ diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index bc00828e68d4..a3c5e8de994c 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -7984,61 +7984,132 @@ ] }, "PermissionProfile": { - "properties": { - "fileSystem": { - "anyOf": [ - { + "oneOf": [ + { + "description": "Codex owns sandbox construction for this profile.", + "properties": { + "fileSystem": { "$ref": "#/definitions/PermissionProfileFileSystemPermissions" }, - { - "type": "null" + "network": { + "$ref": "#/definitions/PermissionProfileNetworkPermissions" + }, + "type": { + "enum": [ + "managed" + ], + "title": "ManagedPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "fileSystem", + "network", + "type" + ], + "title": "ManagedPermissionProfile", + "type": "object" }, - "network": { - "anyOf": [ - { + { + "description": "Do not apply an outer sandbox.", + "properties": { + "type": { + "enum": [ + "disabled" + ], + "title": "DisabledPermissionProfileType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "DisabledPermissionProfile", + "type": "object" + }, + { + "description": "Filesystem isolation is enforced by an external caller.", + "properties": { + "network": { "$ref": "#/definitions/PermissionProfileNetworkPermissions" }, - { - "type": "null" + "type": { + "enum": [ + "external" + ], + "title": "ExternalPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "network", + "type" + ], + "title": "ExternalPermissionProfile", + "type": "object" } - }, - "type": "object" + ] }, "PermissionProfileFileSystemPermissions": { - "properties": { - "entries": { - "items": { - "$ref": "#/definitions/FileSystemSandboxEntry" + "oneOf": [ + { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } }, - "type": "array" + "required": [ + "entries", + "type" + ], + "title": "RestrictedPermissionProfileFileSystemPermissions", + "type": "object" }, - "globScanMaxDepth": { - "format": "uint", - "minimum": 1.0, - "type": [ - "integer", - "null" - ] + { + "properties": { + "type": { + "enum": [ + "unrestricted" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissions", + "type": "object" } - }, - "required": [ - "entries" - ], - "type": "object" + ] }, "PermissionProfileNetworkPermissions": { "properties": { "enabled": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" } }, + "required": [ + "enabled" + ], "type": "object" }, "Personality": { @@ -12431,7 +12502,7 @@ } ], "default": null, - "description": "Canonical active permissions view for this thread when representable. This is `null` for external sandbox policies because external enforcement cannot be round-tripped as a `PermissionProfile`." + "description": "Canonical active permissions view for this thread." }, "reasoningEffort": { "anyOf": [ @@ -13874,7 +13945,7 @@ } ], "default": null, - "description": "Canonical active permissions view for this thread when representable. This is `null` for external sandbox policies because external enforcement cannot be round-tripped as a `PermissionProfile`." + "description": "Canonical active permissions view for this thread." }, "reasoningEffort": { "anyOf": [ @@ -14201,7 +14272,7 @@ } ], "default": null, - "description": "Canonical active permissions view for this thread when representable. This is `null` for external sandbox policies because external enforcement cannot be round-tripped as a `PermissionProfile`." + "description": "Canonical active permissions view for this thread." }, "reasoningEffort": { "anyOf": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json b/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json index 4def45c04998..6ba2fc0db469 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json @@ -246,61 +246,132 @@ "type": "string" }, "PermissionProfile": { - "properties": { - "fileSystem": { - "anyOf": [ - { + "oneOf": [ + { + "description": "Codex owns sandbox construction for this profile.", + "properties": { + "fileSystem": { "$ref": "#/definitions/PermissionProfileFileSystemPermissions" }, - { - "type": "null" + "network": { + "$ref": "#/definitions/PermissionProfileNetworkPermissions" + }, + "type": { + "enum": [ + "managed" + ], + "title": "ManagedPermissionProfileType", + "type": "string" + } + }, + "required": [ + "fileSystem", + "network", + "type" + ], + "title": "ManagedPermissionProfile", + "type": "object" + }, + { + "description": "Do not apply an outer sandbox.", + "properties": { + "type": { + "enum": [ + "disabled" + ], + "title": "DisabledPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "type" + ], + "title": "DisabledPermissionProfile", + "type": "object" }, - "network": { - "anyOf": [ - { + { + "description": "Filesystem isolation is enforced by an external caller.", + "properties": { + "network": { "$ref": "#/definitions/PermissionProfileNetworkPermissions" }, - { - "type": "null" + "type": { + "enum": [ + "external" + ], + "title": "ExternalPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "network", + "type" + ], + "title": "ExternalPermissionProfile", + "type": "object" } - }, - "type": "object" + ] }, "PermissionProfileFileSystemPermissions": { - "properties": { - "entries": { - "items": { - "$ref": "#/definitions/FileSystemSandboxEntry" + "oneOf": [ + { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } }, - "type": "array" + "required": [ + "entries", + "type" + ], + "title": "RestrictedPermissionProfileFileSystemPermissions", + "type": "object" }, - "globScanMaxDepth": { - "format": "uint", - "minimum": 1.0, - "type": [ - "integer", - "null" - ] + { + "properties": { + "type": { + "enum": [ + "unrestricted" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissions", + "type": "object" } - }, - "required": [ - "entries" - ], - "type": "object" + ] }, "PermissionProfileNetworkPermissions": { "properties": { "enabled": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" } }, + "required": [ + "enabled" + ], "type": "object" }, "ReadOnlyAccess": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json index a603aff1e8d6..d120fc8b5d53 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json @@ -276,61 +276,132 @@ ] }, "PermissionProfile": { - "properties": { - "fileSystem": { - "anyOf": [ - { + "oneOf": [ + { + "description": "Codex owns sandbox construction for this profile.", + "properties": { + "fileSystem": { "$ref": "#/definitions/PermissionProfileFileSystemPermissions" }, - { - "type": "null" + "network": { + "$ref": "#/definitions/PermissionProfileNetworkPermissions" + }, + "type": { + "enum": [ + "managed" + ], + "title": "ManagedPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "fileSystem", + "network", + "type" + ], + "title": "ManagedPermissionProfile", + "type": "object" }, - "network": { - "anyOf": [ - { + { + "description": "Do not apply an outer sandbox.", + "properties": { + "type": { + "enum": [ + "disabled" + ], + "title": "DisabledPermissionProfileType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "DisabledPermissionProfile", + "type": "object" + }, + { + "description": "Filesystem isolation is enforced by an external caller.", + "properties": { + "network": { "$ref": "#/definitions/PermissionProfileNetworkPermissions" }, - { - "type": "null" + "type": { + "enum": [ + "external" + ], + "title": "ExternalPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "network", + "type" + ], + "title": "ExternalPermissionProfile", + "type": "object" } - }, - "type": "object" + ] }, "PermissionProfileFileSystemPermissions": { - "properties": { - "entries": { - "items": { - "$ref": "#/definitions/FileSystemSandboxEntry" + "oneOf": [ + { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } }, - "type": "array" + "required": [ + "entries", + "type" + ], + "title": "RestrictedPermissionProfileFileSystemPermissions", + "type": "object" }, - "globScanMaxDepth": { - "format": "uint", - "minimum": 1.0, - "type": [ - "integer", - "null" - ] + { + "properties": { + "type": { + "enum": [ + "unrestricted" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissions", + "type": "object" } - }, - "required": [ - "entries" - ], - "type": "object" + ] }, "PermissionProfileNetworkPermissions": { "properties": { "enabled": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" } }, + "required": [ + "enabled" + ], "type": "object" }, "SandboxMode": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 47677da41e68..281650bb3a65 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -900,61 +900,132 @@ ] }, "PermissionProfile": { - "properties": { - "fileSystem": { - "anyOf": [ - { + "oneOf": [ + { + "description": "Codex owns sandbox construction for this profile.", + "properties": { + "fileSystem": { "$ref": "#/definitions/PermissionProfileFileSystemPermissions" }, - { - "type": "null" + "network": { + "$ref": "#/definitions/PermissionProfileNetworkPermissions" + }, + "type": { + "enum": [ + "managed" + ], + "title": "ManagedPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "fileSystem", + "network", + "type" + ], + "title": "ManagedPermissionProfile", + "type": "object" }, - "network": { - "anyOf": [ - { + { + "description": "Do not apply an outer sandbox.", + "properties": { + "type": { + "enum": [ + "disabled" + ], + "title": "DisabledPermissionProfileType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "DisabledPermissionProfile", + "type": "object" + }, + { + "description": "Filesystem isolation is enforced by an external caller.", + "properties": { + "network": { "$ref": "#/definitions/PermissionProfileNetworkPermissions" }, - { - "type": "null" + "type": { + "enum": [ + "external" + ], + "title": "ExternalPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "network", + "type" + ], + "title": "ExternalPermissionProfile", + "type": "object" } - }, - "type": "object" + ] }, "PermissionProfileFileSystemPermissions": { - "properties": { - "entries": { - "items": { - "$ref": "#/definitions/FileSystemSandboxEntry" + "oneOf": [ + { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } }, - "type": "array" + "required": [ + "entries", + "type" + ], + "title": "RestrictedPermissionProfileFileSystemPermissions", + "type": "object" }, - "globScanMaxDepth": { - "format": "uint", - "minimum": 1.0, - "type": [ - "integer", - "null" - ] + { + "properties": { + "type": { + "enum": [ + "unrestricted" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissions", + "type": "object" } - }, - "required": [ - "entries" - ], - "type": "object" + ] }, "PermissionProfileNetworkPermissions": { "properties": { "enabled": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" } }, + "required": [ + "enabled" + ], "type": "object" }, "ReadOnlyAccess": { @@ -2506,7 +2577,7 @@ } ], "default": null, - "description": "Canonical active permissions view for this thread when representable. This is `null` for external sandbox policies because external enforcement cannot be round-tripped as a `PermissionProfile`." + "description": "Canonical active permissions view for this thread." }, "reasoningEffort": { "anyOf": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json index 19ccad14c1f7..40ff83aeb391 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -542,61 +542,132 @@ ] }, "PermissionProfile": { - "properties": { - "fileSystem": { - "anyOf": [ - { + "oneOf": [ + { + "description": "Codex owns sandbox construction for this profile.", + "properties": { + "fileSystem": { "$ref": "#/definitions/PermissionProfileFileSystemPermissions" }, - { - "type": "null" + "network": { + "$ref": "#/definitions/PermissionProfileNetworkPermissions" + }, + "type": { + "enum": [ + "managed" + ], + "title": "ManagedPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "fileSystem", + "network", + "type" + ], + "title": "ManagedPermissionProfile", + "type": "object" }, - "network": { - "anyOf": [ - { + { + "description": "Do not apply an outer sandbox.", + "properties": { + "type": { + "enum": [ + "disabled" + ], + "title": "DisabledPermissionProfileType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "DisabledPermissionProfile", + "type": "object" + }, + { + "description": "Filesystem isolation is enforced by an external caller.", + "properties": { + "network": { "$ref": "#/definitions/PermissionProfileNetworkPermissions" }, - { - "type": "null" + "type": { + "enum": [ + "external" + ], + "title": "ExternalPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "network", + "type" + ], + "title": "ExternalPermissionProfile", + "type": "object" } - }, - "type": "object" + ] }, "PermissionProfileFileSystemPermissions": { - "properties": { - "entries": { - "items": { - "$ref": "#/definitions/FileSystemSandboxEntry" + "oneOf": [ + { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } }, - "type": "array" + "required": [ + "entries", + "type" + ], + "title": "RestrictedPermissionProfileFileSystemPermissions", + "type": "object" }, - "globScanMaxDepth": { - "format": "uint", - "minimum": 1.0, - "type": [ - "integer", - "null" - ] + { + "properties": { + "type": { + "enum": [ + "unrestricted" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissions", + "type": "object" } - }, - "required": [ - "entries" - ], - "type": "object" + ] }, "PermissionProfileNetworkPermissions": { "properties": { "enabled": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" } }, + "required": [ + "enabled" + ], "type": "object" }, "Personality": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 38b0eb0d3768..573cbe92d090 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -900,61 +900,132 @@ ] }, "PermissionProfile": { - "properties": { - "fileSystem": { - "anyOf": [ - { + "oneOf": [ + { + "description": "Codex owns sandbox construction for this profile.", + "properties": { + "fileSystem": { "$ref": "#/definitions/PermissionProfileFileSystemPermissions" }, - { - "type": "null" + "network": { + "$ref": "#/definitions/PermissionProfileNetworkPermissions" + }, + "type": { + "enum": [ + "managed" + ], + "title": "ManagedPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "fileSystem", + "network", + "type" + ], + "title": "ManagedPermissionProfile", + "type": "object" }, - "network": { - "anyOf": [ - { + { + "description": "Do not apply an outer sandbox.", + "properties": { + "type": { + "enum": [ + "disabled" + ], + "title": "DisabledPermissionProfileType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "DisabledPermissionProfile", + "type": "object" + }, + { + "description": "Filesystem isolation is enforced by an external caller.", + "properties": { + "network": { "$ref": "#/definitions/PermissionProfileNetworkPermissions" }, - { - "type": "null" + "type": { + "enum": [ + "external" + ], + "title": "ExternalPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "network", + "type" + ], + "title": "ExternalPermissionProfile", + "type": "object" } - }, - "type": "object" + ] }, "PermissionProfileFileSystemPermissions": { - "properties": { - "entries": { - "items": { - "$ref": "#/definitions/FileSystemSandboxEntry" + "oneOf": [ + { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } }, - "type": "array" + "required": [ + "entries", + "type" + ], + "title": "RestrictedPermissionProfileFileSystemPermissions", + "type": "object" }, - "globScanMaxDepth": { - "format": "uint", - "minimum": 1.0, - "type": [ - "integer", - "null" - ] + { + "properties": { + "type": { + "enum": [ + "unrestricted" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissions", + "type": "object" } - }, - "required": [ - "entries" - ], - "type": "object" + ] }, "PermissionProfileNetworkPermissions": { "properties": { "enabled": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" } }, + "required": [ + "enabled" + ], "type": "object" }, "ReadOnlyAccess": { @@ -2506,7 +2577,7 @@ } ], "default": null, - "description": "Canonical active permissions view for this thread when representable. This is `null` for external sandbox policies because external enforcement cannot be round-tripped as a `PermissionProfile`." + "description": "Canonical active permissions view for this thread." }, "reasoningEffort": { "anyOf": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json index 0c5f21764870..a1f1c84d8dd7 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json @@ -302,61 +302,132 @@ ] }, "PermissionProfile": { - "properties": { - "fileSystem": { - "anyOf": [ - { + "oneOf": [ + { + "description": "Codex owns sandbox construction for this profile.", + "properties": { + "fileSystem": { "$ref": "#/definitions/PermissionProfileFileSystemPermissions" }, - { - "type": "null" + "network": { + "$ref": "#/definitions/PermissionProfileNetworkPermissions" + }, + "type": { + "enum": [ + "managed" + ], + "title": "ManagedPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "fileSystem", + "network", + "type" + ], + "title": "ManagedPermissionProfile", + "type": "object" }, - "network": { - "anyOf": [ - { + { + "description": "Do not apply an outer sandbox.", + "properties": { + "type": { + "enum": [ + "disabled" + ], + "title": "DisabledPermissionProfileType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "DisabledPermissionProfile", + "type": "object" + }, + { + "description": "Filesystem isolation is enforced by an external caller.", + "properties": { + "network": { "$ref": "#/definitions/PermissionProfileNetworkPermissions" }, - { - "type": "null" + "type": { + "enum": [ + "external" + ], + "title": "ExternalPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "network", + "type" + ], + "title": "ExternalPermissionProfile", + "type": "object" } - }, - "type": "object" + ] }, "PermissionProfileFileSystemPermissions": { - "properties": { - "entries": { - "items": { - "$ref": "#/definitions/FileSystemSandboxEntry" + "oneOf": [ + { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } }, - "type": "array" + "required": [ + "entries", + "type" + ], + "title": "RestrictedPermissionProfileFileSystemPermissions", + "type": "object" }, - "globScanMaxDepth": { - "format": "uint", - "minimum": 1.0, - "type": [ - "integer", - "null" - ] + { + "properties": { + "type": { + "enum": [ + "unrestricted" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissions", + "type": "object" } - }, - "required": [ - "entries" - ], - "type": "object" + ] }, "PermissionProfileNetworkPermissions": { "properties": { "enabled": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" } }, + "required": [ + "enabled" + ], "type": "object" }, "Personality": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index 879fa5c68752..1de06c6039bc 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -900,61 +900,132 @@ ] }, "PermissionProfile": { - "properties": { - "fileSystem": { - "anyOf": [ - { + "oneOf": [ + { + "description": "Codex owns sandbox construction for this profile.", + "properties": { + "fileSystem": { "$ref": "#/definitions/PermissionProfileFileSystemPermissions" }, - { - "type": "null" + "network": { + "$ref": "#/definitions/PermissionProfileNetworkPermissions" + }, + "type": { + "enum": [ + "managed" + ], + "title": "ManagedPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "fileSystem", + "network", + "type" + ], + "title": "ManagedPermissionProfile", + "type": "object" }, - "network": { - "anyOf": [ - { + { + "description": "Do not apply an outer sandbox.", + "properties": { + "type": { + "enum": [ + "disabled" + ], + "title": "DisabledPermissionProfileType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "DisabledPermissionProfile", + "type": "object" + }, + { + "description": "Filesystem isolation is enforced by an external caller.", + "properties": { + "network": { "$ref": "#/definitions/PermissionProfileNetworkPermissions" }, - { - "type": "null" + "type": { + "enum": [ + "external" + ], + "title": "ExternalPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "network", + "type" + ], + "title": "ExternalPermissionProfile", + "type": "object" } - }, - "type": "object" + ] }, "PermissionProfileFileSystemPermissions": { - "properties": { - "entries": { - "items": { - "$ref": "#/definitions/FileSystemSandboxEntry" + "oneOf": [ + { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } }, - "type": "array" + "required": [ + "entries", + "type" + ], + "title": "RestrictedPermissionProfileFileSystemPermissions", + "type": "object" }, - "globScanMaxDepth": { - "format": "uint", - "minimum": 1.0, - "type": [ - "integer", - "null" - ] + { + "properties": { + "type": { + "enum": [ + "unrestricted" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissions", + "type": "object" } - }, - "required": [ - "entries" - ], - "type": "object" + ] }, "PermissionProfileNetworkPermissions": { "properties": { "enabled": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" } }, + "required": [ + "enabled" + ], "type": "object" }, "ReadOnlyAccess": { @@ -2506,7 +2577,7 @@ } ], "default": null, - "description": "Canonical active permissions view for this thread when representable. This is `null` for external sandbox policies because external enforcement cannot be round-tripped as a `PermissionProfile`." + "description": "Canonical active permissions view for this thread." }, "reasoningEffort": { "anyOf": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json index 062771029e85..245c57886ea6 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json @@ -326,61 +326,132 @@ "type": "string" }, "PermissionProfile": { - "properties": { - "fileSystem": { - "anyOf": [ - { + "oneOf": [ + { + "description": "Codex owns sandbox construction for this profile.", + "properties": { + "fileSystem": { "$ref": "#/definitions/PermissionProfileFileSystemPermissions" }, - { - "type": "null" + "network": { + "$ref": "#/definitions/PermissionProfileNetworkPermissions" + }, + "type": { + "enum": [ + "managed" + ], + "title": "ManagedPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "fileSystem", + "network", + "type" + ], + "title": "ManagedPermissionProfile", + "type": "object" }, - "network": { - "anyOf": [ - { + { + "description": "Do not apply an outer sandbox.", + "properties": { + "type": { + "enum": [ + "disabled" + ], + "title": "DisabledPermissionProfileType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "DisabledPermissionProfile", + "type": "object" + }, + { + "description": "Filesystem isolation is enforced by an external caller.", + "properties": { + "network": { "$ref": "#/definitions/PermissionProfileNetworkPermissions" }, - { - "type": "null" + "type": { + "enum": [ + "external" + ], + "title": "ExternalPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "network", + "type" + ], + "title": "ExternalPermissionProfile", + "type": "object" } - }, - "type": "object" + ] }, "PermissionProfileFileSystemPermissions": { - "properties": { - "entries": { - "items": { - "$ref": "#/definitions/FileSystemSandboxEntry" + "oneOf": [ + { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } }, - "type": "array" + "required": [ + "entries", + "type" + ], + "title": "RestrictedPermissionProfileFileSystemPermissions", + "type": "object" }, - "globScanMaxDepth": { - "format": "uint", - "minimum": 1.0, - "type": [ - "integer", - "null" - ] + { + "properties": { + "type": { + "enum": [ + "unrestricted" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissions", + "type": "object" } - }, - "required": [ - "entries" - ], - "type": "object" + ] }, "PermissionProfileNetworkPermissions": { "properties": { "enabled": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" } }, + "required": [ + "enabled" + ], "type": "object" }, "Personality": { diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalPermissionProfile.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalPermissionProfile.ts index 65836c119d56..5120ec3135df 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalPermissionProfile.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalPermissionProfile.ts @@ -4,4 +4,8 @@ import type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions"; import type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions"; -export type AdditionalPermissionProfile = { network: AdditionalNetworkPermissions | null, fileSystem: AdditionalFileSystemPermissions | null, }; +export type AdditionalPermissionProfile = { +/** + * Partial overlay used for per-command permission requests. + */ +network: AdditionalNetworkPermissions | null, fileSystem: AdditionalFileSystemPermissions | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfile.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfile.ts index c38bde54b067..7642c2765063 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfile.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfile.ts @@ -4,4 +4,4 @@ import type { PermissionProfileFileSystemPermissions } from "./PermissionProfileFileSystemPermissions"; import type { PermissionProfileNetworkPermissions } from "./PermissionProfileNetworkPermissions"; -export type PermissionProfile = { network: PermissionProfileNetworkPermissions | null, fileSystem: PermissionProfileFileSystemPermissions | null, }; +export type PermissionProfile = { "type": "managed", network: PermissionProfileNetworkPermissions, fileSystem: PermissionProfileFileSystemPermissions, } | { "type": "disabled" } | { "type": "external", network: PermissionProfileNetworkPermissions, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileFileSystemPermissions.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileFileSystemPermissions.ts index 204a42764c40..29aeceb433ba 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileFileSystemPermissions.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileFileSystemPermissions.ts @@ -3,4 +3,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { FileSystemSandboxEntry } from "./FileSystemSandboxEntry"; -export type PermissionProfileFileSystemPermissions = { entries: Array, globScanMaxDepth?: number, }; +export type PermissionProfileFileSystemPermissions = { "type": "restricted", entries: Array, globScanMaxDepth?: number, } | { "type": "unrestricted" }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileNetworkPermissions.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileNetworkPermissions.ts index 9aa130412a42..0b25a769a9fc 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileNetworkPermissions.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileNetworkPermissions.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type PermissionProfileNetworkPermissions = { enabled: boolean | null, }; +export type PermissionProfileNetworkPermissions = { enabled: boolean, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts index 5dc6b82a34bc..b69f1da01205 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts @@ -26,8 +26,6 @@ approvalsReviewer: ApprovalsReviewer, */ sandbox: SandboxPolicy, /** - * Canonical active permissions view for this thread when representable. - * This is `null` for external sandbox policies because external - * enforcement cannot be round-tripped as a `PermissionProfile`. + * Canonical active permissions view for this thread. */ permissionProfile: PermissionProfile | null, reasoningEffort: ReasoningEffort | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts index d76ad5a58af6..5ceec7f3fe60 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts @@ -26,8 +26,6 @@ approvalsReviewer: ApprovalsReviewer, */ sandbox: SandboxPolicy, /** - * Canonical active permissions view for this thread when representable. - * This is `null` for external sandbox policies because external - * enforcement cannot be round-tripped as a `PermissionProfile`. + * Canonical active permissions view for this thread. */ permissionProfile: PermissionProfile | null, reasoningEffort: ReasoningEffort | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts index 5a83011abd93..61d268afe858 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts @@ -26,8 +26,6 @@ approvalsReviewer: ApprovalsReviewer, */ sandbox: SandboxPolicy, /** - * Canonical active permissions view for this thread when representable. - * This is `null` for external sandbox policies because external - * enforcement cannot be round-tripped as a `PermissionProfile`. + * Canonical active permissions view for this thread. */ permissionProfile: PermissionProfile | null, reasoningEffort: ReasoningEffort | null, }; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 9fa6d98e7b5c..40855a09528b 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -1526,22 +1526,7 @@ mod tests { "type": "dangerFullAccess" }, "permissionProfile": { - "network": { - "enabled": true, - }, - "fileSystem": { - "entries": [ - { - "path": { - "type": "special", - "value": { - "kind": "root", - }, - }, - "access": "write", - }, - ], - }, + "type": "disabled" }, "reasoningEffort": null } diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index b6726679fb1a..080642eca574 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -37,7 +37,9 @@ use codex_protocol::mcp::ResourceTemplate as McpResourceTemplate; use codex_protocol::mcp::Tool as McpTool; use codex_protocol::memory_citation::MemoryCitation as CoreMemoryCitation; use codex_protocol::memory_citation::MemoryCitationEntry as CoreMemoryCitationEntry; +use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile; use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions; +use codex_protocol::models::ManagedFileSystemPermissions as CoreManagedFileSystemPermissions; use codex_protocol::models::MessagePhase; use codex_protocol::models::NetworkPermissions as CoreNetworkPermissions; use codex_protocol::models::PermissionProfile as CorePermissionProfile; @@ -51,6 +53,7 @@ use codex_protocol::permissions::FileSystemAccessMode as CoreFileSystemAccessMod use codex_protocol::permissions::FileSystemPath as CoreFileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry as CoreFileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSpecialPath as CoreFileSystemSpecialPath; +use codex_protocol::permissions::NetworkSandboxPolicy as CoreNetworkSandboxPolicy; use codex_protocol::plan_tool::PlanItemArg as CorePlanItemArg; use codex_protocol::plan_tool::StepStatus as CorePlanStepStatus; use codex_protocol::protocol::AgentStatus as CoreAgentStatus; @@ -1355,7 +1358,7 @@ pub struct AdditionalNetworkPermissions { #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct PermissionProfileNetworkPermissions { - pub enabled: Option, + pub enabled: bool, } impl From for AdditionalNetworkPermissions { @@ -1374,18 +1377,20 @@ impl From for CoreNetworkPermissions { } } -impl From for PermissionProfileNetworkPermissions { - fn from(value: CoreNetworkPermissions) -> Self { +impl From for PermissionProfileNetworkPermissions { + fn from(value: CoreNetworkSandboxPolicy) -> Self { Self { - enabled: value.enabled, + enabled: value.is_enabled(), } } } -impl From for CoreNetworkPermissions { +impl From for CoreNetworkSandboxPolicy { fn from(value: PermissionProfileNetworkPermissions) -> Self { - Self { - enabled: value.enabled, + if value.enabled { + Self::Enabled + } else { + Self::Restricted } } } @@ -1533,65 +1538,111 @@ impl From for CoreFileSystemSandboxEntry { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type")] #[ts(export_to = "v2/")] -pub struct PermissionProfileFileSystemPermissions { - pub entries: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub glob_scan_max_depth: Option, +pub enum PermissionProfileFileSystemPermissions { + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + Restricted { + entries: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + glob_scan_max_depth: Option, + }, + Unrestricted, } -impl From for PermissionProfileFileSystemPermissions { - fn from(value: CoreFileSystemPermissions) -> Self { - Self { - entries: value - .entries - .into_iter() - .map(FileSystemSandboxEntry::from) - .collect(), - glob_scan_max_depth: value.glob_scan_max_depth, +impl From for PermissionProfileFileSystemPermissions { + fn from(value: CoreManagedFileSystemPermissions) -> Self { + match value { + CoreManagedFileSystemPermissions::Restricted { + entries, + glob_scan_max_depth, + } => Self::Restricted { + entries: entries + .into_iter() + .map(FileSystemSandboxEntry::from) + .collect(), + glob_scan_max_depth, + }, + CoreManagedFileSystemPermissions::Unrestricted => Self::Unrestricted, } } } -impl From for CoreFileSystemPermissions { +impl From for CoreManagedFileSystemPermissions { fn from(value: PermissionProfileFileSystemPermissions) -> Self { - Self { - entries: value - .entries - .into_iter() - .map(CoreFileSystemSandboxEntry::from) - .collect(), - glob_scan_max_depth: value.glob_scan_max_depth, + match value { + PermissionProfileFileSystemPermissions::Restricted { + entries, + glob_scan_max_depth, + } => Self::Restricted { + entries: entries + .into_iter() + .map(CoreFileSystemSandboxEntry::from) + .collect(), + glob_scan_max_depth, + }, + PermissionProfileFileSystemPermissions::Unrestricted => Self::Unrestricted, } } } -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type")] #[ts(export_to = "v2/")] -pub struct PermissionProfile { - pub network: Option, - pub file_system: Option, +pub enum PermissionProfile { + /// Codex owns sandbox construction for this profile. + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + Managed { + network: PermissionProfileNetworkPermissions, + file_system: PermissionProfileFileSystemPermissions, + }, + /// Do not apply an outer sandbox. + Disabled, + /// Filesystem isolation is enforced by an external caller. + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + External { + network: PermissionProfileNetworkPermissions, + }, } impl From for PermissionProfile { fn from(value: CorePermissionProfile) -> Self { - Self { - network: value.network.map(PermissionProfileNetworkPermissions::from), - file_system: value - .file_system - .map(PermissionProfileFileSystemPermissions::from), + match value { + CorePermissionProfile::Managed { + file_system, + network, + } => Self::Managed { + network: network.into(), + file_system: file_system.into(), + }, + CorePermissionProfile::Disabled => Self::Disabled, + CorePermissionProfile::External { network } => Self::External { + network: network.into(), + }, } } } impl From for CorePermissionProfile { fn from(value: PermissionProfile) -> Self { - Self { - network: value.network.map(CoreNetworkPermissions::from), - file_system: value.file_system.map(CoreFileSystemPermissions::from), + match value { + PermissionProfile::Managed { + file_system, + network, + } => Self::Managed { + file_system: file_system.into(), + network: network.into(), + }, + PermissionProfile::Disabled => Self::Disabled, + PermissionProfile::External { network } => Self::External { + network: network.into(), + }, } } } @@ -1600,12 +1651,13 @@ impl From for CorePermissionProfile { #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct AdditionalPermissionProfile { + /// Partial overlay used for per-command permission requests. pub network: Option, pub file_system: Option, } -impl From for AdditionalPermissionProfile { - fn from(value: CorePermissionProfile) -> Self { +impl From for AdditionalPermissionProfile { + fn from(value: CoreAdditionalPermissionProfile) -> Self { Self { network: value.network.map(AdditionalNetworkPermissions::from), file_system: value.file_system.map(AdditionalFileSystemPermissions::from), @@ -1613,7 +1665,7 @@ impl From for AdditionalPermissionProfile { } } -impl From for CorePermissionProfile { +impl From for CoreAdditionalPermissionProfile { fn from(value: AdditionalPermissionProfile) -> Self { Self { network: value.network.map(CoreNetworkPermissions::from), @@ -1634,7 +1686,7 @@ pub struct GrantedPermissionProfile { pub file_system: Option, } -impl From for CorePermissionProfile { +impl From for CoreAdditionalPermissionProfile { fn from(value: GrantedPermissionProfile) -> Self { Self { network: value.network.map(CoreNetworkPermissions::from), @@ -3355,9 +3407,7 @@ pub struct ThreadStartResponse { /// `permissionProfile` when present as the canonical active permissions /// view. pub sandbox: SandboxPolicy, - /// Canonical active permissions view for this thread when representable. - /// This is `null` for external sandbox policies because external - /// enforcement cannot be round-tripped as a `PermissionProfile`. + /// Canonical active permissions view for this thread. #[serde(default)] pub permission_profile: Option, pub reasoning_effort: Option, @@ -3461,9 +3511,7 @@ pub struct ThreadResumeResponse { /// `permissionProfile` when present as the canonical active permissions /// view. pub sandbox: SandboxPolicy, - /// Canonical active permissions view for this thread when representable. - /// This is `null` for external sandbox policies because external - /// enforcement cannot be round-tripped as a `PermissionProfile`. + /// Canonical active permissions view for this thread. #[serde(default)] pub permission_profile: Option, pub reasoning_effort: Option, @@ -3558,9 +3606,7 @@ pub struct ThreadForkResponse { /// `permissionProfile` when present as the canonical active permissions /// view. pub sandbox: SandboxPolicy, - /// Canonical active permissions view for this thread when representable. - /// This is `null` for external sandbox policies because external - /// enforcement cannot be round-tripped as a `PermissionProfile`. + /// Canonical active permissions view for this thread. #[serde(default)] pub permission_profile: Option, pub reasoning_effort: Option, @@ -7868,7 +7914,7 @@ mod tests { #[test] fn permission_profile_file_system_permissions_preserves_glob_scan_depth() { - let core_permissions = CoreFileSystemPermissions { + let core_permissions = CoreManagedFileSystemPermissions::Restricted { entries: vec![CoreFileSystemSandboxEntry { path: CoreFileSystemPath::GlobPattern { pattern: "**/*.env".to_string(), @@ -7882,7 +7928,7 @@ mod tests { assert_eq!( permissions, - PermissionProfileFileSystemPermissions { + PermissionProfileFileSystemPermissions::Restricted { entries: vec![FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern: "**/*.env".to_string(), @@ -7893,7 +7939,7 @@ mod tests { } ); assert_eq!( - CoreFileSystemPermissions::from(permissions), + CoreManagedFileSystemPermissions::from(permissions), core_permissions ); } @@ -7901,6 +7947,7 @@ mod tests { #[test] fn permission_profile_file_system_permissions_rejects_zero_glob_scan_depth() { serde_json::from_value::(json!({ + "type": "restricted", "entries": [], "globScanMaxDepth": 0, })) @@ -7954,8 +8001,8 @@ mod tests { ); assert_eq!( - CorePermissionProfile::from(response.permissions), - CorePermissionProfile { + CoreAdditionalPermissionProfile::from(response.permissions), + CoreAdditionalPermissionProfile { network: Some(CoreNetworkPermissions { enabled: Some(true), }), diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index e46d785b3a6b..6caf2df56e5d 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -837,7 +837,8 @@ Run a standalone command (argv vector) in the server’s sandbox without creatin "env": { "FOO": "override" }, // optional; merges into the server env and overrides matching names "size": { "rows": 40, "cols": 120 }, // optional; PTY size in character cells, only valid with tty=true "permissionProfile": { // optional; defaults to user config - "fileSystem": { "entries": [ + "type": "managed", + "fileSystem": { "type": "restricted", "entries": [ { "path": { "type": "special", "value": { "kind": "root" } }, "access": "read" }, { "path": { "type": "special", "value": { "kind": "current_working_directory" } }, "access": "write" } ] }, diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 58a8c2fc8477..a4c424664c4c 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -122,7 +122,7 @@ use codex_protocol::ThreadId; use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem; use codex_protocol::dynamic_tools::DynamicToolResponse as CoreDynamicToolResponse; use codex_protocol::items::parse_hook_prompt_message; -use codex_protocol::models::PermissionProfile as CorePermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile; use codex_protocol::plan_tool::UpdatePlanArgs; use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo; use codex_protocol::protocol::Event; @@ -2719,7 +2719,7 @@ fn request_permissions_response_from_client_result( strict_auto_review: false, }); } - let granted_permissions: CorePermissionProfile = response.permissions.into(); + let granted_permissions: CoreAdditionalPermissionProfile = response.permissions.into(); let permissions = if granted_permissions.is_empty() { CoreRequestPermissionProfile::default() } else { diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index ae7514a9c291..a9aab5ed3417 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -314,7 +314,6 @@ use codex_protocol::error::CodexErr; use codex_protocol::error::Result as CodexResult; use codex_protocol::items::TurnItem; use codex_protocol::models::ResponseItem; -use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::protocol::AgentStatus; use codex_protocol::protocol::ConversationAudioParams; @@ -2361,24 +2360,8 @@ impl CodexMessageProcessor { file_system_sandbox_policy: &mut FileSystemSandboxPolicy, configured_file_system_sandbox_policy: &FileSystemSandboxPolicy, ) { - if file_system_sandbox_policy.glob_scan_max_depth.is_none() { - file_system_sandbox_policy.glob_scan_max_depth = - configured_file_system_sandbox_policy.glob_scan_max_depth; - } - - for deny_entry in configured_file_system_sandbox_policy - .entries - .iter() - .filter(|entry| entry.access == FileSystemAccessMode::None) - { - if !file_system_sandbox_policy - .entries - .iter() - .any(|entry| entry == deny_entry) - { - file_system_sandbox_policy.entries.push(deny_entry.clone()); - } - } + file_system_sandbox_policy + .preserve_deny_read_restrictions_from(configured_file_system_sandbox_policy); } async fn command_exec_write( @@ -2800,10 +2783,8 @@ impl CodexMessageProcessor { /*has_in_progress_turn*/ false, ); - let permission_profile = thread_response_permission_profile( - &config_snapshot.sandbox_policy, - config_snapshot.permission_profile, - ); + let permission_profile = + thread_response_permission_profile(config_snapshot.permission_profile); let response = ThreadStartResponse { thread: thread.clone(), @@ -4608,7 +4589,6 @@ impl CodexMessageProcessor { /*has_live_in_progress_turn*/ false, ); let permission_profile = thread_response_permission_profile( - &session_configured.sandbox_policy, codex_thread.config_snapshot().await.permission_profile, ); @@ -5279,7 +5259,6 @@ impl CodexMessageProcessor { /*has_in_progress_turn*/ false, ); let permission_profile = thread_response_permission_profile( - &session_configured.sandbox_policy, forked_thread.config_snapshot().await.permission_profile, ); @@ -8794,8 +8773,7 @@ async fn handle_pending_thread_resume_request( .. } = pending.config_snapshot; let instruction_sources = pending.instruction_sources; - let permission_profile = - thread_response_permission_profile(&sandbox_policy, permission_profile); + let permission_profile = thread_response_permission_profile(permission_profile); let response = ThreadResumeResponse { thread, @@ -9925,17 +9903,9 @@ fn with_thread_spawn_agent_metadata( } fn thread_response_permission_profile( - sandbox_policy: &codex_protocol::protocol::SandboxPolicy, permission_profile: codex_protocol::models::PermissionProfile, ) -> Option { - match sandbox_policy { - codex_protocol::protocol::SandboxPolicy::DangerFullAccess - | codex_protocol::protocol::SandboxPolicy::ReadOnly { .. } - | codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { .. } => { - Some(permission_profile.into()) - } - codex_protocol::protocol::SandboxPolicy::ExternalSandbox { .. } => None, - } + Some(permission_profile.into()) } fn requested_permissions_trust_project(overrides: &ConfigOverrides, cwd: &Path) -> bool { @@ -10252,6 +10222,7 @@ mod tests { use codex_model_provider_info::WireApi; use codex_protocol::ThreadId; use codex_protocol::openai_models::ReasoningEffort; + use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::protocol::AskForApproval; @@ -10457,25 +10428,28 @@ mod tests { } #[test] - fn thread_response_permission_profile_omits_external_sandbox() { + fn thread_response_permission_profile_preserves_enforcement() { let cwd = test_path_buf("/tmp").abs(); - let profile = codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( - &SandboxPolicy::DangerFullAccess, - cwd.as_path(), - ); - - assert_eq!( - thread_response_permission_profile( + let full_access_profile = + codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::DangerFullAccess, + cwd.as_path(), + ); + let external_profile = + codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( &SandboxPolicy::ExternalSandbox { network_access: codex_protocol::protocol::NetworkAccess::Restricted, }, - profile.clone(), - ), - None + cwd.as_path(), + ); + + assert_eq!( + thread_response_permission_profile(external_profile.clone()), + Some(external_profile.into()) ); assert_eq!( - thread_response_permission_profile(&SandboxPolicy::DangerFullAccess, profile.clone()), - Some(profile.into()) + thread_response_permission_profile(full_access_profile.clone()), + Some(full_access_profile.into()) ); } diff --git a/codex-rs/app-server/tests/suite/v2/command_exec.rs b/codex-rs/app-server/tests/suite/v2/command_exec.rs index c24d2e80db5b..83718a8dc7e7 100644 --- a/codex-rs/app-server/tests/suite/v2/command_exec.rs +++ b/codex-rs/app-server/tests/suite/v2/command_exec.rs @@ -256,17 +256,18 @@ async fn command_exec_permission_profile_cwd_uses_command_cwd() -> Result<()> { timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let mut permission_profile = root_read_only_permission_profile(); - permission_profile - .file_system - .as_mut() - .expect("root read-only helper should include filesystem permissions") - .entries - .push(FileSystemSandboxEntry { - path: FileSystemPath::Special { - value: FileSystemSpecialPath::CurrentWorkingDirectory, - }, - access: FileSystemAccessMode::Write, - }); + let PermissionProfile::Managed { file_system, .. } = &mut permission_profile else { + panic!("root read-only helper should use managed permissions"); + }; + let PermissionProfileFileSystemPermissions::Restricted { entries, .. } = file_system else { + panic!("root read-only helper should use restricted filesystem permissions"); + }; + entries.push(FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::CurrentWorkingDirectory, + }, + access: FileSystemAccessMode::Write, + }); let command_request_id = mcp .send_command_exec_request(CommandExecParams { @@ -1061,11 +1062,9 @@ fn decode_delta_notification( } fn root_read_only_permission_profile() -> PermissionProfile { - PermissionProfile { - network: Some(PermissionProfileNetworkPermissions { - enabled: Some(false), - }), - file_system: Some(PermissionProfileFileSystemPermissions { + PermissionProfile::Managed { + network: PermissionProfileNetworkPermissions { enabled: false }, + file_system: PermissionProfileFileSystemPermissions::Restricted { entries: vec![FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::Root, @@ -1073,7 +1072,7 @@ fn root_read_only_permission_profile() -> PermissionProfile { access: FileSystemAccessMode::Read, }], glob_scan_max_depth: None, - }), + }, } } diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index c6eb58a432ff..5234d0fdaca5 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -39,6 +39,7 @@ use codex_app_server_protocol::PatchApplyStatus; use codex_app_server_protocol::PatchChangeKind; use codex_app_server_protocol::PermissionProfile; use codex_app_server_protocol::PermissionProfileFileSystemPermissions; +use codex_app_server_protocol::PermissionProfileNetworkPermissions; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ServerRequest; use codex_app_server_protocol::ServerRequestResolvedNotification; @@ -779,9 +780,9 @@ async fn turn_start_rejects_invalid_permission_profile_before_starting_turn() -> text: "Hello".to_string(), text_elements: Vec::new(), }], - permission_profile: Some(PermissionProfile { - network: None, - file_system: Some(PermissionProfileFileSystemPermissions { + permission_profile: Some(PermissionProfile::Managed { + network: PermissionProfileNetworkPermissions { enabled: false }, + file_system: PermissionProfileFileSystemPermissions::Restricted { entries: vec![FileSystemSandboxEntry { path: FileSystemPath::Path { path: unsupported_write_root, @@ -789,7 +790,7 @@ async fn turn_start_rejects_invalid_permission_profile_before_starting_turn() -> access: FileSystemAccessMode::Write, }], glob_scan_max_depth: None, - }), + }, }), ..Default::default() }) diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index d46d1a316d93..9f7778c3c33f 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -55,8 +55,6 @@ use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID; use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID; use codex_model_provider_info::WireApi; use codex_models_manager::bundled_models_response; -use codex_protocol::models::FileSystemPermissions; -use codex_protocol::models::NetworkPermissions; use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; @@ -810,20 +808,7 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std:: async fn permission_profile_override_populates_runtime_permissions() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cwd = TempDir::new()?; - let permission_profile = PermissionProfile { - network: Some(NetworkPermissions { - enabled: Some(true), - }), - file_system: Some(FileSystemPermissions { - entries: vec![FileSystemSandboxEntry { - path: FileSystemPath::Special { - value: FileSystemSpecialPath::Root, - }, - access: FileSystemAccessMode::Write, - }], - glob_scan_max_depth: None, - }), - }; + let permission_profile = PermissionProfile::Disabled; let config = Config::load_from_base_config_with_overrides( ConfigToml::default(), @@ -848,20 +833,7 @@ async fn permission_profile_override_populates_runtime_permissions() -> std::io: async fn permission_profile_override_preserves_configured_network_proxy() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cwd = TempDir::new()?; - let permission_profile = PermissionProfile { - network: Some(NetworkPermissions { - enabled: Some(true), - }), - file_system: Some(FileSystemPermissions { - entries: vec![FileSystemSandboxEntry { - path: FileSystemPath::Special { - value: FileSystemSpecialPath::Root, - }, - access: FileSystemAccessMode::Write, - }], - glob_scan_max_depth: None, - }), - }; + let permission_profile = PermissionProfile::Disabled; let config = Config::load_from_base_config_with_overrides( ConfigToml { diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index cb6f78839911..13d4a5d0a19d 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -79,6 +79,7 @@ use codex_protocol::config_types::WebSearchConfig; use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::PermissionProfile; +use codex_protocol::models::SandboxEnforcement; use codex_protocol::openai_models::ModelsResponse; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::permissions::FileSystemSandboxPolicy; @@ -221,7 +222,8 @@ impl Permissions { /// Effective runtime permissions after config requirements and runtime /// readable-root additions have been applied. pub fn permission_profile(&self) -> PermissionProfile { - PermissionProfile::from_runtime_permissions( + PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(self.sandbox_policy.get()), &self.file_system_sandbox_policy, self.network_sandbox_policy, ) @@ -1773,9 +1775,9 @@ impl Config { })?; let profile = resolve_permission_profile(permissions, default_permissions)?; - // PermissionProfile only carries the network enabled bit today. Keep the - // configured proxy/allowlist policy so active profiles can round-trip without - // broadening network behavior. + // PermissionProfile carries the active network sandbox bit, not the configured + // proxy/allowlist policy. Keep that config so active profiles can round-trip + // without broadening network behavior. network_proxy_config_from_profile_network(profile.network.as_ref()) } else { NetworkProxyConfig::default() diff --git a/codex-rs/core/src/guardian/approval_request.rs b/codex-rs/core/src/guardian/approval_request.rs index 2afc5e08056f..fba227834af2 100644 --- a/codex-rs/core/src/guardian/approval_request.rs +++ b/codex-rs/core/src/guardian/approval_request.rs @@ -4,7 +4,7 @@ use codex_analytics::GuardianReviewedAction; use codex_protocol::approvals::GuardianAssessmentAction; use codex_protocol::approvals::GuardianCommandSource; use codex_protocol::approvals::NetworkApprovalProtocol; -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::request_permissions::RequestPermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; use serde::Serialize; @@ -20,7 +20,7 @@ pub(crate) enum GuardianApprovalRequest { command: Vec, cwd: AbsolutePathBuf, sandbox_permissions: crate::sandboxing::SandboxPermissions, - additional_permissions: Option, + additional_permissions: Option, justification: Option, }, ExecCommand { @@ -28,7 +28,7 @@ pub(crate) enum GuardianApprovalRequest { command: Vec, cwd: AbsolutePathBuf, sandbox_permissions: crate::sandboxing::SandboxPermissions, - additional_permissions: Option, + additional_permissions: Option, justification: Option, tty: bool, }, @@ -39,7 +39,7 @@ pub(crate) enum GuardianApprovalRequest { program: String, argv: Vec, cwd: AbsolutePathBuf, - additional_permissions: Option, + additional_permissions: Option, }, ApplyPatch { id: String, @@ -85,7 +85,7 @@ pub(crate) struct GuardianNetworkAccessTrigger { pub(crate) cwd: AbsolutePathBuf, pub(crate) sandbox_permissions: crate::sandboxing::SandboxPermissions, #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) additional_permissions: Option, + pub(crate) additional_permissions: Option, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) justification: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -109,7 +109,7 @@ struct CommandApprovalAction<'a> { cwd: &'a Path, sandbox_permissions: crate::sandboxing::SandboxPermissions, #[serde(skip_serializing_if = "Option::is_none")] - additional_permissions: Option<&'a PermissionProfile>, + additional_permissions: Option<&'a AdditionalPermissionProfile>, #[serde(skip_serializing_if = "Option::is_none")] justification: Option<&'a String>, #[serde(skip_serializing_if = "Option::is_none")] @@ -124,7 +124,7 @@ struct ExecveApprovalAction<'a> { argv: &'a [String], cwd: &'a Path, #[serde(skip_serializing_if = "Option::is_none")] - additional_permissions: Option<&'a PermissionProfile>, + additional_permissions: Option<&'a AdditionalPermissionProfile>, } #[derive(Serialize)] @@ -178,7 +178,7 @@ fn serialize_command_guardian_action( command: &[String], cwd: &Path, sandbox_permissions: crate::sandboxing::SandboxPermissions, - additional_permissions: Option<&PermissionProfile>, + additional_permissions: Option<&AdditionalPermissionProfile>, justification: Option<&String>, tty: Option, ) -> serde_json::Result { diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 22a322b2a3d5..2af3984cc253 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -91,11 +91,12 @@ use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::items::TurnItem; use codex_protocol::items::UserMessageItem; use codex_protocol::mcp::CallToolResult; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::BaseInstructions; use codex_protocol::models::PermissionProfile; +use codex_protocol::models::SandboxEnforcement; use codex_protocol::models::format_allow_prefixes; use codex_protocol::openai_models::ModelInfo; -use codex_protocol::permissions::FileSystemSandboxKind; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::FileChange; @@ -1800,7 +1801,7 @@ impl Session { reason: Option, network_approval_context: Option, proposed_execpolicy_amendment: Option, - additional_permissions: Option, + additional_permissions: Option, available_decisions: Option>, ) -> ReviewDecision { // command-level approvals use `call_id`. @@ -2222,7 +2223,8 @@ impl Session { PermissionGrantScope::Turn => { if let Some(turn_state) = originating_turn_state { let mut ts = turn_state.lock().await; - let permissions: PermissionProfile = response.permissions.clone().into(); + let permissions: AdditionalPermissionProfile = + response.permissions.clone().into(); ts.record_granted_permissions(permissions); if response.strict_auto_review { ts.enable_strict_auto_review(); @@ -2240,7 +2242,7 @@ impl Session { clippy::await_holding_invalid_type, reason = "active turn reads must stay consistent with the matching turn state" )] - pub(crate) async fn granted_turn_permissions(&self) -> Option { + pub(crate) async fn granted_turn_permissions(&self) -> Option { let active = self.active_turn.lock().await; let active = active.as_ref()?; let ts = active.turn_state.lock().await; @@ -2260,7 +2262,7 @@ impl Session { ts.strict_auto_review_enabled() } - pub(crate) async fn granted_session_permissions(&self) -> Option { + pub(crate) async fn granted_session_permissions(&self) -> Option { let state = self.state.lock().await; state.granted_permissions() } diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 42e98ea5869e..e3bf079bb9af 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -92,7 +92,8 @@ impl SessionConfiguration { } pub(super) fn permission_profile(&self) -> PermissionProfile { - PermissionProfile::from_runtime_permissions( + PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(self.sandbox_policy.get()), &self.file_system_sandbox_policy, self.network_sandbox_policy, ) @@ -175,26 +176,8 @@ impl SessionConfiguration { next_configuration.sandbox_policy.set(sandbox_policy)?; let (mut file_system_sandbox_policy, network_sandbox_policy) = permission_profile.to_runtime_permissions(); - if file_system_sandbox_policy.glob_scan_max_depth.is_none() { - file_system_sandbox_policy.glob_scan_max_depth = - self.file_system_sandbox_policy.glob_scan_max_depth; - } - for deny_entry in self - .file_system_sandbox_policy - .entries - .iter() - .filter(|entry| { - entry.access == codex_protocol::permissions::FileSystemAccessMode::None - }) - { - if !file_system_sandbox_policy - .entries - .iter() - .any(|entry| entry == deny_entry) - { - file_system_sandbox_policy.entries.push(deny_entry.clone()); - } - } + file_system_sandbox_policy + .preserve_deny_read_restrictions_from(&self.file_system_sandbox_policy); next_configuration.file_system_sandbox_policy = file_system_sandbox_policy; next_configuration.network_sandbox_policy = network_sandbox_policy; } else if let Some(sandbox_policy) = updates.sandbox_policy.clone() { @@ -814,14 +797,6 @@ impl Session { // Dispatch the SessionConfiguredEvent first and then report any errors. // If resuming, include converted initial messages in the payload so UIs can render them immediately. let initial_messages = initial_history.get_event_msgs(); - let permission_profile = if matches!( - session_configuration.file_system_sandbox_policy.kind, - FileSystemSandboxKind::ExternalSandbox - ) { - None - } else { - Some(session_configuration.permission_profile()) - }; let events = std::iter::once(Event { id: INITIAL_SUBMIT_ID.to_owned(), msg: EventMsg::SessionConfigured(SessionConfiguredEvent { @@ -834,7 +809,7 @@ impl Session { approval_policy: session_configuration.approval_policy.value(), approvals_reviewer: session_configuration.approvals_reviewer, sandbox_policy: session_configuration.sandbox_policy.get().clone(), - permission_profile, + permission_profile: Some(session_configuration.permission_profile()), cwd: session_configuration.cwd.clone(), reasoning_effort: session_configuration.collaboration_mode.reasoning_effort(), history_log_id, diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index b469e3e97123..0f7e3054baee 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -1471,7 +1471,8 @@ async fn record_initial_history_reconstructs_forked_transcript() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn session_configured_omits_permission_profile_for_external_sandbox() -> anyhow::Result<()> { +async fn session_configured_reports_permission_profile_for_external_sandbox() -> anyhow::Result<()> +{ let server = start_mock_server().await; let sandbox_policy = SandboxPolicy::ExternalSandbox { network_access: codex_protocol::protocol::NetworkAccess::Restricted, @@ -1489,10 +1490,15 @@ async fn session_configured_omits_permission_profile_for_external_sandbox() -> a test.session_configured.sandbox_policy, expected_sandbox_policy ); + let expected_permission_profile = + codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( + &expected_sandbox_policy, + test.session_configured.cwd.as_path(), + ); assert_eq!( - test.session_configured.permission_profile, None, - "ExternalSandbox is enforced outside the PermissionProfile model, so SessionConfigured must \ - not expose a lossy root-write profile" + test.session_configured.permission_profile, + Some(expected_permission_profile), + "ExternalSandbox is represented explicitly instead of as a lossy root-write profile" ); Ok(()) } diff --git a/codex-rs/core/src/session/tests/guardian_tests.rs b/codex-rs/core/src/session/tests/guardian_tests.rs index e8b0e3b0d621..88741b3e16a4 100644 --- a/codex-rs/core/src/session/tests/guardian_tests.rs +++ b/codex-rs/core/src/session/tests/guardian_tests.rs @@ -19,9 +19,9 @@ use codex_execpolicy::RuleMatch; use codex_features::Feature; use codex_model_provider::create_model_provider; use codex_protocol::config_types::ApprovalsReviewer; +use codex_protocol::models::AdditionalPermissionProfile as PermissionProfile; use codex_protocol::models::ContentItem; use codex_protocol::models::NetworkPermissions; -use codex_protocol::models::PermissionProfile; use codex_protocol::models::ResponseItem; use codex_protocol::models::function_call_output_content_items_to_text; use codex_protocol::permissions::FileSystemSandboxPolicy; diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index 2d547b65a6ce..004aaccc543b 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -1,8 +1,11 @@ use super::*; use codex_model_provider::SharedModelProvider; use codex_model_provider::create_model_provider; +use codex_protocol::models::AdditionalPermissionProfile; +use codex_protocol::models::SandboxEnforcement; use codex_protocol::protocol::TurnEnvironmentSelection; -use codex_sandboxing::policy_transforms::merge_permission_profiles; +use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy; +use codex_sandboxing::policy_transforms::effective_network_sandbox_policy; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; @@ -89,7 +92,8 @@ pub(crate) struct TurnContext { } impl TurnContext { pub(crate) fn permission_profile(&self) -> PermissionProfile { - PermissionProfile::from_runtime_permissions( + PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(&self.sandbox_policy), &self.file_system_sandbox_policy, self.network_sandbox_policy, ) @@ -236,12 +240,21 @@ impl TurnContext { pub(crate) fn file_system_sandbox_context( &self, - additional_permissions: Option, + additional_permissions: Option, ) -> FileSystemSandboxContext { - let base_permissions = self.permission_profile(); - let permissions = - merge_permission_profiles(Some(&base_permissions), additional_permissions.as_ref()) - .unwrap_or(base_permissions); + let file_system_sandbox_policy = effective_file_system_sandbox_policy( + &self.file_system_sandbox_policy, + additional_permissions.as_ref(), + ); + let network_sandbox_policy = effective_network_sandbox_policy( + self.network_sandbox_policy, + additional_permissions.as_ref(), + ); + let permissions = PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(&self.sandbox_policy), + &file_system_sandbox_policy, + network_sandbox_policy, + ); FileSystemSandboxContext { permissions, cwd: Some(self.cwd.clone()), diff --git a/codex-rs/core/src/state/session.rs b/codex-rs/core/src/state/session.rs index e9fd67633299..3bd4b8a26e78 100644 --- a/codex-rs/core/src/state/session.rs +++ b/codex-rs/core/src/state/session.rs @@ -1,6 +1,6 @@ //! Session-wide mutable state. -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::ResponseItem; use codex_sandboxing::policy_transforms::merge_permission_profiles; use std::collections::HashMap; @@ -32,7 +32,7 @@ pub(crate) struct SessionState { pub(crate) startup_prewarm: Option, pub(crate) active_connector_selection: HashSet, pub(crate) pending_session_start_source: Option, - granted_permissions: Option, + granted_permissions: Option, next_turn_is_first: bool, } @@ -218,12 +218,12 @@ impl SessionState { self.pending_session_start_source.take() } - pub(crate) fn record_granted_permissions(&mut self, permissions: PermissionProfile) { + pub(crate) fn record_granted_permissions(&mut self, permissions: AdditionalPermissionProfile) { self.granted_permissions = merge_permission_profiles(self.granted_permissions.as_ref(), Some(&permissions)); } - pub(crate) fn granted_permissions(&self) -> Option { + pub(crate) fn granted_permissions(&self) -> Option { self.granted_permissions.clone() } } diff --git a/codex-rs/core/src/state/turn.rs b/codex-rs/core/src/state/turn.rs index 5e1526ad5533..48b7a26ccb53 100644 --- a/codex-rs/core/src/state/turn.rs +++ b/codex-rs/core/src/state/turn.rs @@ -21,7 +21,7 @@ use tokio::sync::oneshot; use crate::session::turn_context::TurnContext; use crate::tasks::AnySessionTask; -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::TokenUsage; @@ -105,7 +105,7 @@ pub(crate) struct TurnState { pending_dynamic_tools: HashMap>, pending_input: Vec, mailbox_delivery_phase: MailboxDeliveryPhase, - granted_permissions: Option, + granted_permissions: Option, strict_auto_review_enabled: bool, pub(crate) tool_calls: u64, pub(crate) has_memory_citation: bool, @@ -247,12 +247,12 @@ impl TurnState { self.mailbox_delivery_phase = phase; } - pub(crate) fn record_granted_permissions(&mut self, permissions: PermissionProfile) { + pub(crate) fn record_granted_permissions(&mut self, permissions: AdditionalPermissionProfile) { self.granted_permissions = merge_permission_profiles(self.granted_permissions.as_ref(), Some(&permissions)); } - pub(crate) fn granted_permissions(&self) -> Option { + pub(crate) fn granted_permissions(&self) -> Option { self.granted_permissions.clone() } diff --git a/codex-rs/core/src/tools/handlers/apply_patch.rs b/codex-rs/core/src/tools/handlers/apply_patch.rs index 91d5543c0f52..9d63ad0a8b5b 100644 --- a/codex-rs/core/src/tools/handlers/apply_patch.rs +++ b/codex-rs/core/src/tools/handlers/apply_patch.rs @@ -39,8 +39,8 @@ use codex_apply_patch::Hunk; use codex_apply_patch::parse_patch_streaming; use codex_exec_server::ExecutorFileSystem; use codex_features::Feature; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::FileSystemPermissions; -use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::FileChange; use codex_protocol::protocol::PatchApplyUpdatedEvent; @@ -215,7 +215,7 @@ fn write_permissions_for_paths( file_paths: &[AbsolutePathBuf], file_system_sandbox_policy: &codex_protocol::permissions::FileSystemSandboxPolicy, cwd: &AbsolutePathBuf, -) -> Option { +) -> Option { let write_paths = file_paths .iter() .map(|path| { @@ -232,7 +232,7 @@ fn write_permissions_for_paths( .collect::, _>>() .ok()?; - let permissions = (!write_paths.is_empty()).then_some(PermissionProfile { + let permissions = (!write_paths.is_empty()).then_some(AdditionalPermissionProfile { file_system: Some(FileSystemPermissions::from_read_write_roots( Some(vec![]), Some(write_paths), diff --git a/codex-rs/core/src/tools/handlers/mod.rs b/codex-rs/core/src/tools/handlers/mod.rs index af4178f71fd4..7878c1092c5f 100644 --- a/codex-rs/core/src/tools/handlers/mod.rs +++ b/codex-rs/core/src/tools/handlers/mod.rs @@ -34,7 +34,7 @@ use crate::session::session::Session; pub(crate) use crate::tools::code_mode::CodeModeExecuteHandler; pub(crate) use crate::tools::code_mode::CodeModeWaitHandler; pub use apply_patch::ApplyPatchHandler; -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::protocol::AskForApproval; pub use dynamic::DynamicToolHandler; pub use js_repl::JsReplHandler; @@ -93,10 +93,10 @@ pub(crate) fn normalize_and_validate_additional_permissions( additional_permissions_allowed: bool, approval_policy: AskForApproval, sandbox_permissions: SandboxPermissions, - additional_permissions: Option, + additional_permissions: Option, permissions_preapproved: bool, _cwd: &Path, -) -> Result, String> { +) -> Result, String> { let uses_additional_permissions = matches!( sandbox_permissions, SandboxPermissions::WithAdditionalPermissions @@ -146,15 +146,15 @@ pub(crate) fn normalize_and_validate_additional_permissions( pub(super) struct EffectiveAdditionalPermissions { pub sandbox_permissions: SandboxPermissions, - pub additional_permissions: Option, + pub additional_permissions: Option, pub permissions_preapproved: bool, } pub(super) fn implicit_granted_permissions( sandbox_permissions: SandboxPermissions, - additional_permissions: Option<&PermissionProfile>, + additional_permissions: Option<&AdditionalPermissionProfile>, effective_additional_permissions: &EffectiveAdditionalPermissions, -) -> Option { +) -> Option { if !sandbox_permissions.uses_additional_permissions() && !matches!(sandbox_permissions, SandboxPermissions::RequireEscalated) && additional_permissions.is_none() @@ -171,7 +171,7 @@ pub(super) async fn apply_granted_turn_permissions( session: &Session, cwd: &std::path::Path, sandbox_permissions: SandboxPermissions, - additional_permissions: Option, + additional_permissions: Option, ) -> EffectiveAdditionalPermissions { if matches!(sandbox_permissions, SandboxPermissions::RequireEscalated) { return EffectiveAdditionalPermissions { @@ -213,8 +213,8 @@ pub(super) async fn apply_granted_turn_permissions( } fn permissions_are_preapproved( - effective_permissions: &PermissionProfile, - granted_permissions: PermissionProfile, + effective_permissions: &AdditionalPermissionProfile, + granted_permissions: AdditionalPermissionProfile, cwd: &Path, ) -> bool { let materialized_effective_permissions = intersect_permission_profiles( @@ -233,9 +233,9 @@ mod tests { use super::normalize_and_validate_additional_permissions; use super::permissions_are_preapproved; use crate::sandboxing::SandboxPermissions; + use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::NetworkPermissions; - use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; @@ -248,8 +248,8 @@ mod tests { use pretty_assertions::assert_eq; use tempfile::tempdir; - fn network_permissions() -> PermissionProfile { - PermissionProfile { + fn network_permissions() -> AdditionalPermissionProfile { + AdditionalPermissionProfile { network: Some(NetworkPermissions { enabled: Some(true), }), @@ -257,8 +257,8 @@ mod tests { } } - fn file_system_permissions(path: &std::path::Path) -> PermissionProfile { - PermissionProfile { + fn file_system_permissions(path: &std::path::Path) -> AdditionalPermissionProfile { + AdditionalPermissionProfile { file_system: Some(FileSystemPermissions::from_read_write_roots( /*read*/ None, Some(vec![ @@ -350,7 +350,7 @@ mod tests { #[test] fn relative_deny_glob_grants_remain_preapproved_after_materialization() { let cwd = tempdir().expect("tempdir"); - let requested_permissions = PermissionProfile { + let requested_permissions = AdditionalPermissionProfile { file_system: Some(FileSystemPermissions { entries: vec![ FileSystemSandboxEntry { diff --git a/codex-rs/core/src/tools/handlers/shell.rs b/codex-rs/core/src/tools/handlers/shell.rs index f4ab61b52481..17daaa738044 100644 --- a/codex-rs/core/src/tools/handlers/shell.rs +++ b/codex-rs/core/src/tools/handlers/shell.rs @@ -36,7 +36,7 @@ use crate::tools::runtimes::shell::ShellRuntime; use crate::tools::runtimes::shell::ShellRuntimeBackend; use crate::tools::sandboxing::ToolCtx; use codex_features::Feature; -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::protocol::ExecCommandSource; use codex_shell_command::is_safe_command::is_known_safe_command; use codex_tools::ShellCommandBackendConfig; @@ -79,7 +79,7 @@ struct RunExecLikeArgs { tool_name: String, exec_params: ExecParams, hook_command: String, - additional_permissions: Option, + additional_permissions: Option, prefix_rule: Option>, session: Arc, turn: Arc, diff --git a/codex-rs/core/src/tools/handlers/unified_exec.rs b/codex-rs/core/src/tools/handlers/unified_exec.rs index 05b54ff884a4..6a3203a05b73 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec.rs @@ -28,7 +28,7 @@ use crate::unified_exec::generate_chunk_id; use codex_features::Feature; use codex_otel::SessionTelemetry; use codex_otel::TOOL_CALL_UNIFIED_EXEC_METRIC; -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::TerminalInteractionEvent; use codex_shell_command::is_safe_command::is_known_safe_command; @@ -58,7 +58,7 @@ pub(crate) struct ExecCommandArgs { #[serde(default)] sandbox_permissions: SandboxPermissions, #[serde(default)] - additional_permissions: Option, + additional_permissions: Option, #[serde(default)] justification: Option, #[serde(default)] diff --git a/codex-rs/core/src/tools/handlers/unified_exec_tests.rs b/codex-rs/core/src/tools/handlers/unified_exec_tests.rs index e0a0af5edc7e..1bdd0b82f97c 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec_tests.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec_tests.rs @@ -2,8 +2,8 @@ use super::*; use crate::shell::default_user_shell; use crate::tools::handlers::parse_arguments_with_base_path; use crate::tools::handlers::resolve_workdir_base_path; +use codex_protocol::models::AdditionalPermissionProfile as PermissionProfile; use codex_protocol::models::FileSystemPermissions; -use codex_protocol::models::PermissionProfile; use codex_tools::UnifiedExecShellMode; use codex_tools::ZshForkConfig; use codex_utils_absolute_path::AbsolutePathBuf; diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index ed325a4e6842..ecf3ccdc04dc 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -23,7 +23,9 @@ use codex_protocol::error::CodexErr; use codex_protocol::error::SandboxErr; use codex_protocol::exec_output::ExecToolCallOutput; use codex_protocol::exec_output::StreamOutput; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::PermissionProfile; +use codex_protocol::models::SandboxEnforcement; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; @@ -33,7 +35,8 @@ use codex_protocol::protocol::FileChange; use codex_protocol::protocol::ReviewDecision; use codex_sandboxing::SandboxType; use codex_sandboxing::SandboxablePreference; -use codex_sandboxing::policy_transforms::merge_permission_profiles; +use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy; +use codex_sandboxing::policy_transforms::effective_network_sandbox_policy; use codex_utils_absolute_path::AbsolutePathBuf; use futures::future::BoxFuture; use std::path::PathBuf; @@ -45,7 +48,7 @@ pub struct ApplyPatchRequest { pub file_paths: Vec, pub changes: std::collections::HashMap, pub exec_approval_requirement: ExecApprovalRequirement, - pub additional_permissions: Option, + pub additional_permissions: Option, pub permissions_preapproved: bool, } @@ -77,13 +80,19 @@ impl ApplyPatchRuntime { return None; } - let base_permissions = PermissionProfile::from_runtime_permissions( + let file_system_policy = effective_file_system_sandbox_policy( attempt.file_system_policy, + req.additional_permissions.as_ref(), + ); + let network_policy = effective_network_sandbox_policy( attempt.network_policy, + req.additional_permissions.as_ref(), + ); + let permissions = PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(attempt.policy), + &file_system_policy, + network_policy, ); - let permissions = - merge_permission_profiles(Some(&base_permissions), req.additional_permissions.as_ref()) - .unwrap_or(base_permissions); Some(FileSystemSandboxContext { permissions, cwd: Some(attempt.sandbox_cwd.clone()), diff --git a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs index 112a3d3a56be..0bc4d2e6f950 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs @@ -1,6 +1,7 @@ use super::*; use crate::tools::sandboxing::SandboxAttempt; use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemSandboxPolicy; @@ -9,7 +10,8 @@ use codex_protocol::protocol::GranularApprovalConfig; use codex_protocol::protocol::SandboxPolicy; use codex_sandboxing::SandboxManager; use codex_sandboxing::SandboxType; -use codex_sandboxing::policy_transforms::merge_permission_profiles; +use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy; +use codex_sandboxing::policy_transforms::effective_network_sandbox_policy; use core_test_support::PathBufExt; use pretty_assertions::assert_eq; use std::collections::HashMap; @@ -116,7 +118,7 @@ fn file_system_sandbox_context_uses_active_attempt() { let path = std::env::temp_dir() .join("apply-patch-runtime-attempt.txt") .abs(); - let additional_permissions = PermissionProfile { + let additional_permissions = AdditionalPermissionProfile { network: None, file_system: Some(FileSystemPermissions::from_read_write_roots( Some(vec![path.clone()]), @@ -154,15 +156,14 @@ fn file_system_sandbox_context_uses_active_attempt() { let sandbox = ApplyPatchRuntime::file_system_sandbox_context_for_attempt(&req, &attempt) .expect("sandbox context"); - let base_permissions = PermissionProfile::from_runtime_permissions( - &file_system_policy, + let file_system_policy = + effective_file_system_sandbox_policy(&file_system_policy, Some(&additional_permissions)); + let network_policy = effective_network_sandbox_policy( NetworkSandboxPolicy::Restricted, + Some(&additional_permissions), ); - let Some(expected_permissions) = - merge_permission_profiles(Some(&base_permissions), Some(&additional_permissions)) - else { - panic!("merged permissions should not be empty"); - }; + let expected_permissions = + PermissionProfile::from_runtime_permissions(&file_system_policy, network_policy); assert_eq!(sandbox.permissions, expected_permissions); assert_eq!(sandbox.cwd, Some(path.clone())); assert_eq!( diff --git a/codex-rs/core/src/tools/runtimes/mod.rs b/codex-rs/core/src/tools/runtimes/mod.rs index 2246fa18f54b..a55f78f8a449 100644 --- a/codex-rs/core/src/tools/runtimes/mod.rs +++ b/codex-rs/core/src/tools/runtimes/mod.rs @@ -14,7 +14,7 @@ use codex_network_proxy::PROXY_ACTIVE_ENV_KEY; use codex_network_proxy::PROXY_ENV_KEYS; #[cfg(target_os = "macos")] use codex_network_proxy::PROXY_GIT_SSH_COMMAND_ENV_KEY; -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_sandboxing::SandboxCommand; use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashMap; @@ -29,7 +29,7 @@ pub(crate) fn build_sandbox_command( command: &[String], cwd: &AbsolutePathBuf, env: &HashMap, - additional_permissions: Option, + additional_permissions: Option, ) -> Result { let (program, args) = command .split_first() diff --git a/codex-rs/core/src/tools/runtimes/shell.rs b/codex-rs/core/src/tools/runtimes/shell.rs index e18d2c269ad3..edaa2f472142 100644 --- a/codex-rs/core/src/tools/runtimes/shell.rs +++ b/codex-rs/core/src/tools/runtimes/shell.rs @@ -35,7 +35,7 @@ use crate::tools::sandboxing::sandbox_override_for_first_attempt; use crate::tools::sandboxing::with_cached_approval; use codex_network_proxy::NetworkProxy; use codex_protocol::exec_output::ExecToolCallOutput; -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::protocol::ReviewDecision; use codex_sandboxing::SandboxablePreference; use codex_shell_command::powershell::prefix_powershell_script_with_utf8; @@ -53,7 +53,7 @@ pub struct ShellRequest { pub explicit_env_overrides: HashMap, pub network: Option, pub sandbox_permissions: SandboxPermissions, - pub additional_permissions: Option, + pub additional_permissions: Option, #[cfg(unix)] pub additional_permissions_preapproved: bool, pub justification: Option, @@ -97,7 +97,7 @@ pub(crate) struct ApprovalKey { command: Vec, cwd: AbsolutePathBuf, sandbox_permissions: SandboxPermissions, - additional_permissions: Option, + additional_permissions: Option, } impl ShellRuntime { diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index b29e9c593712..369689f9d083 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -30,7 +30,9 @@ use codex_protocol::error::CodexErr; use codex_protocol::error::SandboxErr; use codex_protocol::exec_output::ExecToolCallOutput; use codex_protocol::exec_output::StreamOutput; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::PermissionProfile; +use codex_protocol::models::SandboxEnforcement; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; @@ -311,7 +313,7 @@ struct CoreShellActionProvider { network_sandbox_policy: NetworkSandboxPolicy, sandbox_permissions: SandboxPermissions, approval_sandbox_permissions: SandboxPermissions, - prompt_permissions: Option, + prompt_permissions: Option, stopwatch: Stopwatch, } @@ -361,7 +363,7 @@ impl CoreShellActionProvider { sandbox_policy: &SandboxPolicy, file_system_sandbox_policy: &FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, - additional_permissions: Option<&PermissionProfile>, + additional_permissions: Option<&AdditionalPermissionProfile>, ) -> EscalationExecution { match sandbox_permissions { SandboxPermissions::UseDefault => EscalationExecution::TurnDefault, @@ -373,10 +375,14 @@ impl CoreShellActionProvider { EscalationExecution::Permissions( EscalationPermissions::ResolvedPermissionProfile( ResolvedPermissionProfile { - permission_profile: PermissionProfile::from_runtime_permissions( - file_system_sandbox_policy, - network_sandbox_policy, - ), + permission_profile: + PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy( + sandbox_policy, + ), + file_system_sandbox_policy, + network_sandbox_policy, + ), sandbox_policy: sandbox_policy.clone(), }, ), @@ -392,7 +398,7 @@ impl CoreShellActionProvider { argv: &[String], workdir: &AbsolutePathBuf, stopwatch: &Stopwatch, - additional_permissions: Option, + additional_permissions: Option, ) -> anyhow::Result { let command = join_program_and_argv(program, argv); let workdir = workdir.clone(); @@ -491,7 +497,7 @@ impl CoreShellActionProvider { program: &AbsolutePathBuf, argv: &[String], workdir: &AbsolutePathBuf, - prompt_permissions: Option, + prompt_permissions: Option, escalation_execution: EscalationExecution, decision_source: DecisionSource, ) -> anyhow::Result { @@ -757,7 +763,7 @@ struct PrepareSandboxedExecParams<'a> { sandbox_policy: &'a SandboxPolicy, file_system_sandbox_policy: &'a FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, - additional_permissions: Option, + additional_permissions: Option, } #[async_trait::async_trait] diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs index a2b6ca145e7c..86753a04d76d 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs @@ -16,6 +16,7 @@ use codex_execpolicy::PolicyParser; use codex_execpolicy::RuleMatch; use codex_hooks::Hooks; use codex_hooks::HooksConfig; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; @@ -257,7 +258,7 @@ fn map_exec_result_preserves_stdout_and_stderr() { #[test] fn shell_request_escalation_execution_is_explicit() { - let requested_permissions = PermissionProfile { + let requested_permissions = AdditionalPermissionProfile { file_system: Some(FileSystemPermissions::from_read_write_roots( /*read*/ None, Some(vec![ diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index 96d7abf64cd6..be185e5fefda 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -38,7 +38,7 @@ use crate::unified_exec::UnifiedExecProcessManager; use codex_network_proxy::NetworkProxy; use codex_protocol::error::CodexErr; use codex_protocol::error::SandboxErr; -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::protocol::ReviewDecision; use codex_sandboxing::SandboxablePreference; use codex_shell_command::powershell::prefix_powershell_script_with_utf8; @@ -61,7 +61,7 @@ pub struct UnifiedExecRequest { pub network: Option, pub tty: bool, pub sandbox_permissions: SandboxPermissions, - pub additional_permissions: Option, + pub additional_permissions: Option, #[cfg(unix)] pub additional_permissions_preapproved: bool, pub justification: Option, @@ -76,7 +76,7 @@ pub struct UnifiedExecApprovalKey { pub cwd: AbsolutePathBuf, pub tty: bool, pub sandbox_permissions: SandboxPermissions, - pub additional_permissions: Option, + pub additional_permissions: Option, } /// Runtime adapter that keeps policy and sandbox orchestration on the diff --git a/codex-rs/core/src/unified_exec/mod.rs b/codex-rs/core/src/unified_exec/mod.rs index a5a6e69f896d..acf361f94b8c 100644 --- a/codex-rs/core/src/unified_exec/mod.rs +++ b/codex-rs/core/src/unified_exec/mod.rs @@ -28,7 +28,7 @@ use std::sync::Arc; use std::sync::Weak; use codex_network_proxy::NetworkProxy; -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; use rand::Rng; use rand::rng; @@ -96,7 +96,7 @@ pub(crate) struct ExecCommandRequest { pub network: Option, pub tty: bool, pub sandbox_permissions: SandboxPermissions, - pub additional_permissions: Option, + pub additional_permissions: Option, pub additional_permissions_preapproved: bool, pub justification: Option, pub prefix_rule: Option>, diff --git a/codex-rs/core/tests/suite/request_permissions.rs b/codex-rs/core/tests/suite/request_permissions.rs index bcc938734de8..319e3ef8ecd5 100644 --- a/codex-rs/core/tests/suite/request_permissions.rs +++ b/codex-rs/core/tests/suite/request_permissions.rs @@ -5,8 +5,8 @@ use codex_core::config::Constrained; use codex_core::sandboxing::SandboxPermissions; use codex_features::Feature; use codex_protocol::config_types::ApprovalsReviewer; +use codex_protocol::models::AdditionalPermissionProfile as PermissionProfile; use codex_protocol::models::FileSystemPermissions; -use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ExecApprovalRequestEvent; diff --git a/codex-rs/exec-server/src/file_system.rs b/codex-rs/exec-server/src/file_system.rs index a474b3536840..37237f60dd77 100644 --- a/codex-rs/exec-server/src/file_system.rs +++ b/codex-rs/exec-server/src/file_system.rs @@ -5,7 +5,6 @@ use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxKind; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSpecialPath; -use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use std::path::Path; @@ -58,10 +57,8 @@ pub struct FileSystemSandboxContext { impl FileSystemSandboxContext { pub fn from_legacy_sandbox_policy(sandbox_policy: SandboxPolicy, cwd: AbsolutePathBuf) -> Self { - let permissions = PermissionProfile::from_runtime_permissions( - &FileSystemSandboxPolicy::from_legacy_sandbox_policy(&sandbox_policy, cwd.as_path()), - NetworkSandboxPolicy::from(&sandbox_policy), - ); + let permissions = + PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy, cwd.as_path()); Self::from_permission_profile_with_cwd(permissions, cwd) } diff --git a/codex-rs/exec-server/src/fs_sandbox.rs b/codex-rs/exec-server/src/fs_sandbox.rs index 9836a5b5bd61..b9f7456f3c6e 100644 --- a/codex-rs/exec-server/src/fs_sandbox.rs +++ b/codex-rs/exec-server/src/fs_sandbox.rs @@ -536,11 +536,9 @@ mod tests { }, access: FileSystemAccessMode::Write, }]); - let sandbox_context = - crate::FileSystemSandboxContext::from_permission_profile(PermissionProfile { - network: None, - file_system: Some((&policy).into()), - }); + let sandbox_context = crate::FileSystemSandboxContext::from_permission_profile( + PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted), + ); let err = sandbox_cwd(&sandbox_context).expect_err("missing cwd should be rejected"); diff --git a/codex-rs/exec-server/tests/file_system.rs b/codex-rs/exec-server/tests/file_system.rs index 0a7c000ae1b0..c42159a6ddd4 100644 --- a/codex-rs/exec-server/tests/file_system.rs +++ b/codex-rs/exec-server/tests/file_system.rs @@ -21,13 +21,16 @@ use codex_exec_server::FileSystemSandboxContext; use codex_exec_server::LocalFileSystem; use codex_exec_server::ReadDirectoryEntry; use codex_exec_server::RemoveOptions; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::FileSystemPermissions; -use codex_protocol::models::NetworkPermissions; use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; -use codex_sandboxing::policy_transforms::merge_permission_profiles; +use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy; +use codex_sandboxing::policy_transforms::effective_network_sandbox_policy; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use tempfile::TempDir; @@ -102,15 +105,10 @@ fn workspace_write_sandbox(writable_root: std::path::PathBuf) -> FileSystemSandb } fn sandbox_context(entries: Vec) -> FileSystemSandboxContext { - FileSystemSandboxContext::from_permission_profile(PermissionProfile { - network: Some(NetworkPermissions { - enabled: Some(false), - }), - file_system: Some(FileSystemPermissions { - entries, - glob_scan_max_depth: None, - }), - }) + FileSystemSandboxContext::from_permission_profile(PermissionProfile::from_runtime_permissions( + &FileSystemSandboxPolicy::restricted(entries), + NetworkSandboxPolicy::Restricted, + )) } #[test] @@ -604,19 +602,26 @@ async fn file_system_sandboxed_write_allows_additional_write_root(use_remote: bo std::fs::create_dir_all(&writable_dir)?; let mut sandbox = read_only_sandbox(readable_dir); - let additional_permissions = PermissionProfile { + let additional_permissions = AdditionalPermissionProfile { network: None, file_system: Some(FileSystemPermissions::from_read_write_roots( /*read*/ None, Some(vec![absolute_path(writable_dir)]), )), }; - let Some(permissions) = - merge_permission_profiles(Some(&sandbox.permissions), Some(&additional_permissions)) - else { - panic!("merged permissions should not be empty"); - }; - sandbox.permissions = permissions; + let file_system_policy = effective_file_system_sandbox_policy( + &sandbox.permissions.file_system_sandbox_policy(), + Some(&additional_permissions), + ); + let network_policy = effective_network_sandbox_policy( + sandbox.permissions.network_sandbox_policy(), + Some(&additional_permissions), + ); + sandbox.permissions = PermissionProfile::from_runtime_permissions_with_enforcement( + sandbox.permissions.enforcement(), + &file_system_policy, + network_policy, + ); file_system .write_file( diff --git a/codex-rs/protocol/src/approvals.rs b/codex-rs/protocol/src/approvals.rs index 2b9aea272171..6fc5e49b4954 100644 --- a/codex-rs/protocol/src/approvals.rs +++ b/codex-rs/protocol/src/approvals.rs @@ -1,4 +1,5 @@ use crate::mcp::RequestId; +use crate::models::AdditionalPermissionProfile; use crate::models::PermissionProfile; use crate::parse_command::ParsedCommand; use crate::protocol::FileChange; @@ -28,7 +29,7 @@ pub struct ResolvedPermissionProfile { #[derive(Debug, Clone, PartialEq, Eq)] pub enum EscalationPermissions { /// Permissions to merge with the active turn permissions. - AdditionalPermissionProfile(PermissionProfile), + AdditionalPermissionProfile(AdditionalPermissionProfile), /// Fully resolved permissions that should replace the active turn permissions. ResolvedPermissionProfile(ResolvedPermissionProfile), } @@ -249,7 +250,7 @@ pub struct ExecApprovalRequestEvent { /// Optional additional filesystem permissions requested for this command. #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] - pub additional_permissions: Option, + pub additional_permissions: Option, /// Ordered list of decisions the client may present for this prompt. /// /// When absent, clients should derive the legacy default set from the @@ -285,7 +286,7 @@ impl ExecApprovalRequestEvent { network_approval_context: Option<&NetworkApprovalContext>, proposed_execpolicy_amendment: Option<&ExecPolicyAmendment>, proposed_network_policy_amendments: Option<&[NetworkPolicyAmendment]>, - additional_permissions: Option<&PermissionProfile>, + additional_permissions: Option<&AdditionalPermissionProfile>, ) -> Vec { if network_approval_context.is_some() { let mut decisions = vec![ReviewDecision::Approved, ReviewDecision::ApprovedForSession]; diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 551723c77396..2b02ee88b36c 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -266,57 +266,217 @@ impl NetworkPermissions { } } +/// Partial permission overlay used for per-command requests and approved +/// session/turn grants. #[derive(Debug, Clone, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS)] -pub struct PermissionProfile { +pub struct AdditionalPermissionProfile { pub network: Option, pub file_system: Option, } -impl PermissionProfile { +impl AdditionalPermissionProfile { pub fn is_empty(&self) -> bool { self.network.is_none() && self.file_system.is_none() } +} + +#[derive( + Debug, Clone, Copy, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS, +)] +#[serde(rename_all = "snake_case")] +pub enum SandboxEnforcement { + /// Codex owns sandbox construction for this profile. + #[default] + Managed, + /// No outer filesystem sandbox should be applied. + Disabled, + /// Filesystem isolation is enforced by an external caller. + External, +} + +impl SandboxEnforcement { + pub fn from_legacy_sandbox_policy(sandbox_policy: &SandboxPolicy) -> Self { + match sandbox_policy { + SandboxPolicy::DangerFullAccess => Self::Disabled, + SandboxPolicy::ExternalSandbox { .. } => Self::External, + SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. } => Self::Managed, + } + } +} + +/// Filesystem permissions for profiles where Codex owns sandbox construction. +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "snake_case")] +#[ts(tag = "type")] +pub enum ManagedFileSystemPermissions { + /// Apply a managed filesystem sandbox from the listed entries. + #[serde(rename_all = "snake_case")] + #[ts(rename_all = "snake_case")] + Restricted { + entries: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + glob_scan_max_depth: Option, + }, + /// Apply a managed sandbox that allows all filesystem access. + Unrestricted, +} + +impl ManagedFileSystemPermissions { + fn from_sandbox_policy(file_system_sandbox_policy: &FileSystemSandboxPolicy) -> Self { + match file_system_sandbox_policy.kind { + FileSystemSandboxKind::Restricted => Self::Restricted { + entries: file_system_sandbox_policy.entries.clone(), + glob_scan_max_depth: file_system_sandbox_policy + .glob_scan_max_depth + .and_then(NonZeroUsize::new), + }, + FileSystemSandboxKind::Unrestricted => Self::Unrestricted, + FileSystemSandboxKind::ExternalSandbox => unreachable!( + "external filesystem policies are represented by PermissionProfile::External" + ), + } + } + + pub fn to_sandbox_policy(&self) -> FileSystemSandboxPolicy { + match self { + Self::Restricted { + entries, + glob_scan_max_depth, + } => FileSystemSandboxPolicy { + kind: FileSystemSandboxKind::Restricted, + glob_scan_max_depth: glob_scan_max_depth.map(usize::from), + entries: entries.clone(), + }, + Self::Unrestricted => FileSystemSandboxPolicy::unrestricted(), + } + } +} + +/// Canonical active runtime permissions for a conversation, turn, or command. +#[derive(Debug, Clone, Eq, PartialEq, Serialize, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "snake_case")] +#[ts(tag = "type")] +pub enum PermissionProfile { + /// Codex owns sandbox construction for this profile. + #[serde(rename_all = "snake_case")] + #[ts(rename_all = "snake_case")] + Managed { + file_system: ManagedFileSystemPermissions, + network: NetworkSandboxPolicy, + }, + /// Do not apply an outer sandbox. + Disabled, + /// Filesystem isolation is enforced by an external caller. + #[serde(rename_all = "snake_case")] + #[ts(rename_all = "snake_case")] + External { network: NetworkSandboxPolicy }, +} +impl Default for PermissionProfile { + fn default() -> Self { + Self::Managed { + file_system: ManagedFileSystemPermissions::Restricted { + entries: Vec::new(), + glob_scan_max_depth: None, + }, + network: NetworkSandboxPolicy::Restricted, + } + } +} + +impl PermissionProfile { pub fn from_runtime_permissions( file_system_sandbox_policy: &FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, ) -> Self { - Self { - network: Some(network_sandbox_policy.into()), - file_system: Some(file_system_sandbox_policy.into()), + let enforcement = match file_system_sandbox_policy.kind { + FileSystemSandboxKind::Restricted | FileSystemSandboxKind::Unrestricted => { + SandboxEnforcement::Managed + } + FileSystemSandboxKind::ExternalSandbox => SandboxEnforcement::External, + }; + Self::from_runtime_permissions_with_enforcement( + enforcement, + file_system_sandbox_policy, + network_sandbox_policy, + ) + } + + pub fn from_runtime_permissions_with_enforcement( + enforcement: SandboxEnforcement, + file_system_sandbox_policy: &FileSystemSandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, + ) -> Self { + match file_system_sandbox_policy.kind { + FileSystemSandboxKind::ExternalSandbox => Self::External { + network: network_sandbox_policy, + }, + FileSystemSandboxKind::Unrestricted + if enforcement == SandboxEnforcement::Disabled + && network_sandbox_policy.is_enabled() => + { + Self::Disabled + } + FileSystemSandboxKind::Restricted | FileSystemSandboxKind::Unrestricted => { + Self::Managed { + file_system: ManagedFileSystemPermissions::from_sandbox_policy( + file_system_sandbox_policy, + ), + network: network_sandbox_policy, + } + } } } pub fn from_legacy_sandbox_policy(sandbox_policy: &SandboxPolicy, cwd: &Path) -> Self { - Self::from_runtime_permissions( + Self::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(sandbox_policy), &FileSystemSandboxPolicy::from_legacy_sandbox_policy(sandbox_policy, cwd), NetworkSandboxPolicy::from(sandbox_policy), ) } + pub fn enforcement(&self) -> SandboxEnforcement { + match self { + Self::Managed { .. } => SandboxEnforcement::Managed, + Self::Disabled => SandboxEnforcement::Disabled, + Self::External { .. } => SandboxEnforcement::External, + } + } + pub fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy { - self.file_system.as_ref().map_or_else( - || FileSystemSandboxPolicy::restricted(Vec::new()), - FileSystemSandboxPolicy::from, - ) + match self { + Self::Managed { file_system, .. } => file_system.to_sandbox_policy(), + Self::Disabled => FileSystemSandboxPolicy::unrestricted(), + Self::External { .. } => FileSystemSandboxPolicy::external_sandbox(), + } } pub fn network_sandbox_policy(&self) -> NetworkSandboxPolicy { - if self - .network - .as_ref() - .and_then(|network| network.enabled) - .unwrap_or(false) - { - NetworkSandboxPolicy::Enabled - } else { - NetworkSandboxPolicy::Restricted + match self { + Self::Managed { network, .. } | Self::External { network } => *network, + Self::Disabled => NetworkSandboxPolicy::Enabled, } } pub fn to_legacy_sandbox_policy(&self, cwd: &Path) -> io::Result { - self.file_system_sandbox_policy() - .to_legacy_sandbox_policy(self.network_sandbox_policy(), cwd) + match self { + Self::Managed { + file_system, + network, + } => file_system + .to_sandbox_policy() + .to_legacy_sandbox_policy(*network, cwd), + Self::Disabled => Ok(SandboxPolicy::DangerFullAccess), + Self::External { network } => Ok(SandboxPolicy::ExternalSandbox { + network_access: if network.is_enabled() { + crate::protocol::NetworkAccess::Enabled + } else { + crate::protocol::NetworkAccess::Restricted + }, + }), + } } pub fn to_runtime_permissions(&self) -> (FileSystemSandboxPolicy, NetworkSandboxPolicy) { @@ -327,6 +487,85 @@ impl PermissionProfile { } } +#[derive(Debug, Clone, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +enum TaggedPermissionProfile { + #[serde(rename_all = "snake_case")] + Managed { + file_system: ManagedFileSystemPermissions, + network: NetworkSandboxPolicy, + }, + Disabled, + #[serde(rename_all = "snake_case")] + External { + network: NetworkSandboxPolicy, + }, +} + +impl From for PermissionProfile { + fn from(value: TaggedPermissionProfile) -> Self { + match value { + TaggedPermissionProfile::Managed { + file_system, + network, + } => Self::Managed { + file_system, + network, + }, + TaggedPermissionProfile::Disabled => Self::Disabled, + TaggedPermissionProfile::External { network } => Self::External { network }, + } + } +} + +/// Pre-tagged shape written to rollout files before `PermissionProfile` +/// represented enforcement explicitly. +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(deny_unknown_fields)] +struct LegacyPermissionProfile { + network: Option, + file_system: Option, +} + +impl From for PermissionProfile { + fn from(value: LegacyPermissionProfile) -> Self { + let file_system_sandbox_policy = value.file_system.as_ref().map_or_else( + || FileSystemSandboxPolicy::restricted(Vec::new()), + FileSystemSandboxPolicy::from, + ); + let network_sandbox_policy = if value + .network + .as_ref() + .and_then(|network| network.enabled) + .unwrap_or(false) + { + NetworkSandboxPolicy::Enabled + } else { + NetworkSandboxPolicy::Restricted + }; + Self::from_runtime_permissions(&file_system_sandbox_policy, network_sandbox_policy) + } +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(untagged)] +enum PermissionProfileDe { + Tagged(TaggedPermissionProfile), + Legacy(LegacyPermissionProfile), +} + +impl<'de> Deserialize<'de> for PermissionProfile { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ok(match PermissionProfileDe::deserialize(deserializer)? { + PermissionProfileDe::Tagged(tagged) => tagged.into(), + PermissionProfileDe::Legacy(legacy) => legacy.into(), + }) + } +} + impl From for NetworkPermissions { fn from(value: NetworkSandboxPolicy) -> Self { Self { @@ -977,7 +1216,7 @@ pub struct ShellToolCallParams { pub prefix_rule: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] - pub additional_permissions: Option, + pub additional_permissions: Option, #[serde(skip_serializing_if = "Option::is_none")] pub justification: Option, } @@ -1003,7 +1242,7 @@ pub struct ShellCommandToolCallParams { pub prefix_rule: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] - pub additional_permissions: Option, + pub additional_permissions: Option, #[serde(skip_serializing_if = "Option::is_none")] pub justification: Option, } @@ -1448,13 +1687,13 @@ mod tests { } #[test] - fn permission_profile_is_empty_when_all_fields_are_none() { - assert_eq!(PermissionProfile::default().is_empty(), true); + fn additional_permission_profile_is_empty_when_all_fields_are_none() { + assert_eq!(AdditionalPermissionProfile::default().is_empty(), true); } #[test] - fn permission_profile_is_not_empty_when_field_is_present_but_nested_empty() { - let permission_profile = PermissionProfile { + fn additional_permission_profile_is_not_empty_when_field_is_present_but_nested_empty() { + let permission_profile = AdditionalPermissionProfile { network: Some(NetworkPermissions { enabled: None }), file_system: None, }; @@ -1483,6 +1722,146 @@ mod tests { ); } + #[test] + fn permission_profile_deserializes_legacy_rollout_shape() -> Result<()> { + let legacy = serde_json::json!({ + "network": { + "enabled": true, + }, + "file_system": { + "entries": [{ + "path": { + "type": "special", + "value": { + "kind": "root", + }, + }, + "access": "write", + }], + "glob_scan_max_depth": 2, + }, + }); + + let permission_profile: PermissionProfile = serde_json::from_value(legacy)?; + + assert_eq!( + permission_profile, + PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Restricted { + entries: vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Write, + }], + glob_scan_max_depth: NonZeroUsize::new(2), + }, + network: NetworkSandboxPolicy::Enabled, + } + ); + Ok(()) + } + + #[test] + fn permission_profile_round_trip_preserves_disabled_sandbox() -> Result<()> { + let cwd = tempdir()?; + let permission_profile = PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::DangerFullAccess, + cwd.path(), + ); + + assert_eq!(permission_profile, PermissionProfile::Disabled); + assert_eq!( + permission_profile.to_legacy_sandbox_policy(cwd.path())?, + SandboxPolicy::DangerFullAccess + ); + assert_eq!( + permission_profile.to_runtime_permissions(), + ( + FileSystemSandboxPolicy::unrestricted(), + NetworkSandboxPolicy::Enabled + ) + ); + Ok(()) + } + + #[test] + fn permission_profile_from_runtime_permissions_preserves_external_sandbox() { + let permission_profile = PermissionProfile::from_runtime_permissions( + &FileSystemSandboxPolicy::external_sandbox(), + NetworkSandboxPolicy::Restricted, + ); + + assert_eq!( + permission_profile, + PermissionProfile::External { + network: NetworkSandboxPolicy::Restricted, + } + ); + assert_eq!( + PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::Managed, + &FileSystemSandboxPolicy::external_sandbox(), + NetworkSandboxPolicy::Restricted, + ), + permission_profile, + ); + } + + #[test] + fn permission_profile_from_runtime_permissions_preserves_unrestricted_managed_network() { + let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::External, + &FileSystemSandboxPolicy::unrestricted(), + NetworkSandboxPolicy::Restricted, + ); + + assert_eq!( + permission_profile, + PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Unrestricted, + network: NetworkSandboxPolicy::Restricted, + }, + "the legacy ExternalSandbox projection must not hide a split unrestricted filesystem policy" + ); + assert_eq!( + permission_profile.to_runtime_permissions(), + ( + FileSystemSandboxPolicy::unrestricted(), + NetworkSandboxPolicy::Restricted, + ) + ); + } + + #[test] + fn permission_profile_round_trip_preserves_external_sandbox() -> Result<()> { + let cwd = tempdir()?; + let sandbox_policy = SandboxPolicy::ExternalSandbox { + network_access: crate::protocol::NetworkAccess::Restricted, + }; + let permission_profile = + PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy, cwd.path()); + + assert_eq!( + permission_profile, + PermissionProfile::External { + network: NetworkSandboxPolicy::Restricted, + } + ); + assert_eq!( + permission_profile.to_legacy_sandbox_policy(cwd.path())?, + sandbox_policy + ); + assert_eq!( + permission_profile.to_runtime_permissions(), + ( + FileSystemSandboxPolicy::external_sandbox(), + NetworkSandboxPolicy::Restricted + ) + ); + Ok(()) + } + #[test] fn file_system_permissions_with_glob_scan_depth_uses_canonical_json() -> Result<()> { let path = AbsolutePathBuf::try_from(PathBuf::from(if cfg!(windows) { diff --git a/codex-rs/protocol/src/permissions.rs b/codex-rs/protocol/src/permissions.rs index ed5656745433..f06fc7798cbf 100644 --- a/codex-rs/protocol/src/permissions.rs +++ b/codex-rs/protocol/src/permissions.rs @@ -340,6 +340,41 @@ impl FileSystemSandboxPolicy { rebuilt } + /// Preserve explicit read-deny rules from `existing` when a caller + /// replaces the allow side of a policy. + pub fn preserve_deny_read_restrictions_from(&mut self, existing: &Self) { + let has_deny_read_entries = existing + .entries + .iter() + .any(|entry| entry.access == FileSystemAccessMode::None); + if matches!(self.kind, FileSystemSandboxKind::Unrestricted) && has_deny_read_entries { + *self = Self::restricted(vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Write, + }]); + } + + if !matches!(self.kind, FileSystemSandboxKind::Restricted) { + return; + } + + if self.glob_scan_max_depth.is_none() { + self.glob_scan_max_depth = existing.glob_scan_max_depth; + } + + for deny_entry in existing + .entries + .iter() + .filter(|entry| entry.access == FileSystemAccessMode::None) + { + if !self.entries.iter().any(|entry| entry == deny_entry) { + self.entries.push(deny_entry.clone()); + } + } + } + /// Returns true when a restricted policy contains any entry that really /// reduces a broader `:root = write` grant. /// @@ -2297,6 +2332,28 @@ mod tests { ); } + #[test] + fn preserving_deny_entries_keeps_unrestricted_policy_enforceable() { + let deny_entry = unreadable_glob_entry("/tmp/project/**/*.env".to_string()); + let mut existing = FileSystemSandboxPolicy::restricted(vec![deny_entry.clone()]); + existing.glob_scan_max_depth = Some(2); + let mut replacement = FileSystemSandboxPolicy::unrestricted(); + + replacement.preserve_deny_read_restrictions_from(&existing); + + let mut expected = FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Write, + }, + deny_entry, + ]); + expected.glob_scan_max_depth = Some(2); + assert_eq!(replacement, expected); + } + fn deny_policy(path: &Path) -> FileSystemSandboxPolicy { FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { path: FileSystemPath::Path { diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 1f17b53c1ae1..3f87a2534144 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -41,6 +41,7 @@ use crate::models::MessagePhase; use crate::models::PermissionProfile; use crate::models::ResponseInputItem; use crate::models::ResponseItem; +use crate::models::SandboxEnforcement; use crate::models::WebSearchAction; use crate::num_format::format_with_separators; use crate::openai_models::ReasoningEffort as ReasoningEffortConfig; @@ -3062,7 +3063,8 @@ impl TurnContextItem { &self.cwd, ) }); - PermissionProfile::from_runtime_permissions( + PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(&self.sandbox_policy), &file_system_sandbox_policy, NetworkSandboxPolicy::from(&self.sandbox_policy), ) diff --git a/codex-rs/protocol/src/request_permissions.rs b/codex-rs/protocol/src/request_permissions.rs index 0649bf288653..6c7b699daf84 100644 --- a/codex-rs/protocol/src/request_permissions.rs +++ b/codex-rs/protocol/src/request_permissions.rs @@ -1,6 +1,6 @@ +use crate::models::AdditionalPermissionProfile; use crate::models::FileSystemPermissions; use crate::models::NetworkPermissions; -use crate::models::PermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; use schemars::JsonSchema; use serde::Deserialize; @@ -28,7 +28,7 @@ impl RequestPermissionProfile { } } -impl From for PermissionProfile { +impl From for AdditionalPermissionProfile { fn from(value: RequestPermissionProfile) -> Self { Self { network: value.network, @@ -37,8 +37,8 @@ impl From for PermissionProfile { } } -impl From for RequestPermissionProfile { - fn from(value: PermissionProfile) -> Self { +impl From for RequestPermissionProfile { + fn from(value: AdditionalPermissionProfile) -> Self { Self { network: value.network, file_system: value.file_system, diff --git a/codex-rs/rollout-trace/src/tool_dispatch.rs b/codex-rs/rollout-trace/src/tool_dispatch.rs index 0389814cbaa3..1c444a87dec9 100644 --- a/codex-rs/rollout-trace/src/tool_dispatch.rs +++ b/codex-rs/rollout-trace/src/tool_dispatch.rs @@ -7,7 +7,7 @@ use std::fmt::Display; use std::sync::Arc; -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::SandboxPermissions; use codex_protocol::models::SearchToolCallParams; @@ -90,7 +90,7 @@ pub enum ToolDispatchPayload { timeout_ms: Option, sandbox_permissions: Option, prefix_rule: Option>, - additional_permissions: Option, + additional_permissions: Option, justification: Option, }, Mcp { diff --git a/codex-rs/sandboxing/src/manager.rs b/codex-rs/sandboxing/src/manager.rs index 0836eb256517..a13f828fdf18 100644 --- a/codex-rs/sandboxing/src/manager.rs +++ b/codex-rs/sandboxing/src/manager.rs @@ -11,7 +11,7 @@ use crate::policy_transforms::effective_network_sandbox_policy; use crate::policy_transforms::should_require_platform_sandbox; use codex_network_proxy::NetworkProxy; use codex_protocol::config_types::WindowsSandboxLevel; -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::SandboxPolicy; @@ -68,7 +68,7 @@ pub struct SandboxCommand { pub args: Vec, pub cwd: AbsolutePathBuf, pub env: HashMap, - pub additional_permissions: Option, + pub additional_permissions: Option, } #[derive(Debug)] diff --git a/codex-rs/sandboxing/src/manager_tests.rs b/codex-rs/sandboxing/src/manager_tests.rs index e8697362947a..a7dca2bf576a 100644 --- a/codex-rs/sandboxing/src/manager_tests.rs +++ b/codex-rs/sandboxing/src/manager_tests.rs @@ -5,9 +5,9 @@ use super::SandboxType; use super::SandboxablePreference; use super::get_platform_sandbox; use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::AdditionalPermissionProfile as PermissionProfile; use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::NetworkPermissions; -use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; diff --git a/codex-rs/sandboxing/src/policy_transforms.rs b/codex-rs/sandboxing/src/policy_transforms.rs index 9b5e444d7c92..065d96e8bb5f 100644 --- a/codex-rs/sandboxing/src/policy_transforms.rs +++ b/codex-rs/sandboxing/src/policy_transforms.rs @@ -1,6 +1,6 @@ +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::NetworkPermissions; -use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; @@ -27,7 +27,7 @@ pub struct EffectiveSandboxPermissions { impl EffectiveSandboxPermissions { pub fn new( sandbox_policy: &SandboxPolicy, - additional_permissions: Option<&PermissionProfile>, + additional_permissions: Option<&AdditionalPermissionProfile>, ) -> Self { let Some(additional_permissions) = additional_permissions else { return Self { @@ -42,8 +42,8 @@ impl EffectiveSandboxPermissions { } pub fn normalize_additional_permissions( - additional_permissions: PermissionProfile, -) -> Result { + additional_permissions: AdditionalPermissionProfile, +) -> Result { let network = additional_permissions .network .filter(|network| !network.is_empty()); @@ -87,16 +87,16 @@ pub fn normalize_additional_permissions( } None => None, }; - Ok(PermissionProfile { + Ok(AdditionalPermissionProfile { network, file_system, }) } pub fn merge_permission_profiles( - base: Option<&PermissionProfile>, - permissions: Option<&PermissionProfile>, -) -> Option { + base: Option<&AdditionalPermissionProfile>, + permissions: Option<&AdditionalPermissionProfile>, +) -> Option { let Some(permissions) = permissions else { return base.cloned(); }; @@ -137,7 +137,7 @@ pub fn merge_permission_profiles( (None, None) => None, }; - Some(PermissionProfile { + Some(AdditionalPermissionProfile { network, file_system, }) @@ -148,10 +148,10 @@ pub fn merge_permission_profiles( } pub fn intersect_permission_profiles( - requested: PermissionProfile, - granted: PermissionProfile, + requested: AdditionalPermissionProfile, + granted: AdditionalPermissionProfile, cwd: &Path, -) -> PermissionProfile { +) -> AdditionalPermissionProfile { let file_system = requested .file_system .map(|requested_file_system| { @@ -213,7 +213,7 @@ pub fn intersect_permission_profiles( _ => None, }; - PermissionProfile { + AdditionalPermissionProfile { network, file_system, } @@ -458,7 +458,7 @@ fn dedup_absolute_paths(paths: Vec) -> Vec { } fn additional_permission_roots( - additional_permissions: &PermissionProfile, + additional_permissions: &AdditionalPermissionProfile, ) -> (Vec, Vec) { ( dedup_absolute_paths( @@ -516,7 +516,7 @@ fn merge_file_system_policy_with_additional_permissions( pub fn effective_file_system_sandbox_policy( file_system_policy: &FileSystemSandboxPolicy, - additional_permissions: Option<&PermissionProfile>, + additional_permissions: Option<&AdditionalPermissionProfile>, ) -> FileSystemSandboxPolicy { let Some(additional_permissions) = additional_permissions else { return file_system_policy.clone(); @@ -557,7 +557,7 @@ fn merge_read_only_access_with_additional_reads( fn merge_network_access( base_network_access: bool, - additional_permissions: &PermissionProfile, + additional_permissions: &AdditionalPermissionProfile, ) -> bool { base_network_access || additional_permissions @@ -569,7 +569,7 @@ fn merge_network_access( pub fn effective_network_sandbox_policy( network_policy: NetworkSandboxPolicy, - additional_permissions: Option<&PermissionProfile>, + additional_permissions: Option<&AdditionalPermissionProfile>, ) -> NetworkSandboxPolicy { if additional_permissions .is_some_and(|permissions| merge_network_access(network_policy.is_enabled(), permissions)) @@ -584,7 +584,7 @@ pub fn effective_network_sandbox_policy( fn sandbox_policy_with_additional_permissions( sandbox_policy: &SandboxPolicy, - additional_permissions: &PermissionProfile, + additional_permissions: &AdditionalPermissionProfile, ) -> SandboxPolicy { if additional_permissions.is_empty() { return sandbox_policy.clone(); @@ -654,7 +654,7 @@ fn sandbox_policy_with_additional_permissions( fn effective_sandbox_policy( sandbox_policy: &SandboxPolicy, - additional_permissions: Option<&PermissionProfile>, + additional_permissions: Option<&AdditionalPermissionProfile>, ) -> SandboxPolicy { additional_permissions.map_or_else( || sandbox_policy.clone(), diff --git a/codex-rs/sandboxing/src/policy_transforms_tests.rs b/codex-rs/sandboxing/src/policy_transforms_tests.rs index 38b4bd5f0239..876cbe9cb2f5 100644 --- a/codex-rs/sandboxing/src/policy_transforms_tests.rs +++ b/codex-rs/sandboxing/src/policy_transforms_tests.rs @@ -4,9 +4,9 @@ use super::merge_file_system_policy_with_additional_permissions; use super::normalize_additional_permissions; use super::sandbox_policy_with_additional_permissions; use super::should_require_platform_sandbox; +use codex_protocol::models::AdditionalPermissionProfile as PermissionProfile; use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::NetworkPermissions; -use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; diff --git a/codex-rs/shell-escalation/src/unix/escalate_server.rs b/codex-rs/shell-escalation/src/unix/escalate_server.rs index ce73f262f498..55abf7d73420 100644 --- a/codex-rs/shell-escalation/src/unix/escalate_server.rs +++ b/codex-rs/shell-escalation/src/unix/escalate_server.rs @@ -378,8 +378,8 @@ async fn handle_escalate_session_with_policy( mod tests { use super::*; use codex_protocol::approvals::EscalationPermissions; + use codex_protocol::models::AdditionalPermissionProfile as PermissionProfile; use codex_protocol::models::NetworkPermissions; - use codex_protocol::models::PermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use std::collections::HashMap; diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 8a90924412b6..62e074d6cfd1 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -289,7 +289,7 @@ fn default_exec_approval_decisions( proposed_network_policy_amendments: Option< &[codex_protocol::approvals::NetworkPolicyAmendment], >, - additional_permissions: Option<&codex_protocol::models::PermissionProfile>, + additional_permissions: Option<&codex_protocol::models::AdditionalPermissionProfile>, ) -> Vec { ExecApprovalRequestEvent::default_available_decisions( network_approval_context, diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index b2141977ce5b..320e0e1c8715 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -61,6 +61,7 @@ use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::CollaborationModeMask; use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Settings; +use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile; use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::NetworkPermissions; use codex_protocol::models::PermissionProfile; @@ -2476,7 +2477,7 @@ async fn inactive_thread_exec_approval_preserves_context() { ); assert_eq!( additional_permissions, - Some(PermissionProfile { + Some(CoreAdditionalPermissionProfile { network: Some(NetworkPermissions { enabled: Some(true), }), diff --git a/codex-rs/tui/src/app/thread_session_state.rs b/codex-rs/tui/src/app/thread_session_state.rs index 302bcb5a63d7..269a05037269 100644 --- a/codex-rs/tui/src/app/thread_session_state.rs +++ b/codex-rs/tui/src/app/thread_session_state.rs @@ -3,7 +3,6 @@ use crate::app_server_session::ThreadSessionState; use crate::read_session_model; use codex_app_server_protocol::Thread; use codex_protocol::ThreadId; -use codex_protocol::protocol::SandboxPolicy; impl App { pub(super) async fn sync_active_thread_permission_settings_to_cached_session(&mut self) { @@ -14,17 +13,12 @@ impl App { let approval_policy = self.config.permissions.approval_policy.value(); let approvals_reviewer = self.config.approvals_reviewer; let sandbox_policy = self.config.permissions.sandbox_policy.get().clone(); - let permission_profile = if matches!(sandbox_policy, SandboxPolicy::ExternalSandbox { .. }) - { - None - } else { - Some( - self.chat_widget - .config_ref() - .permissions - .permission_profile(), - ) - }; + let permission_profile = Some( + self.chat_widget + .config_ref() + .permissions + .permission_profile(), + ); let update_session = |session: &mut ThreadSessionState| { session.approval_policy = approval_policy; session.approvals_reviewer = approvals_reviewer; diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 8992d97d6798..e93ef6ddc3b0 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -154,7 +154,7 @@ pub(crate) struct ThreadSessionState { pub(crate) sandbox_policy: SandboxPolicy, /// Canonical active permissions when available. Consumers should prefer /// this over `sandbox_policy`; `None` means the session only has a legacy - /// sandbox projection or represents an external sandbox. + /// sandbox projection. pub(crate) permission_profile: Option, pub(crate) cwd: AbsolutePathBuf, pub(crate) instruction_source_paths: Vec, @@ -1050,15 +1050,12 @@ fn turn_start_permission_overrides( Option, Option, ) { - let is_external_sandbox = matches!(&sandbox_policy, SandboxPolicy::ExternalSandbox { .. }); - match (mode, is_external_sandbox, permission_profile) { - (ThreadParamsMode::Embedded, false, Some(permission_profile)) => { + match (mode, permission_profile) { + (ThreadParamsMode::Embedded, Some(permission_profile)) => { (None, Some(permission_profile.into())) } - (ThreadParamsMode::Embedded, false, None) => (None, None), - (ThreadParamsMode::Embedded, true, _) | (ThreadParamsMode::Remote, _, _) => { - (Some(sandbox_policy.into()), None) - } + (ThreadParamsMode::Embedded, None) => (None, None), + (ThreadParamsMode::Remote, _) => (Some(sandbox_policy.into()), None), } } @@ -1070,14 +1067,7 @@ fn permission_profile_override_from_config( return None; } - if matches!( - config.permissions.sandbox_policy.get(), - SandboxPolicy::ExternalSandbox { .. } - ) { - None - } else { - Some(config.permissions.permission_profile().into()) - } + Some(config.permissions.permission_profile().into()) } fn thread_start_params_from_config( @@ -1591,8 +1581,11 @@ mod tests { &cwd, )), ); - assert_eq!(sandbox, Some(external_sandbox.into())); - assert_eq!(profile, None); + assert_eq!(sandbox, None); + assert_eq!( + profile, + Some(PermissionProfile::from_legacy_sandbox_policy(&external_sandbox, &cwd).into()) + ); } #[tokio::test] diff --git a/codex-rs/tui/src/bottom_pane/approval_overlay.rs b/codex-rs/tui/src/bottom_pane/approval_overlay.rs index 1f7e30c37132..57d819e560dd 100644 --- a/codex-rs/tui/src/bottom_pane/approval_overlay.rs +++ b/codex-rs/tui/src/bottom_pane/approval_overlay.rs @@ -20,7 +20,7 @@ use crate::render::renderable::Renderable; use codex_features::Features; use codex_protocol::ThreadId; use codex_protocol::mcp::RequestId; -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; @@ -58,7 +58,7 @@ pub(crate) enum ApprovalRequest { reason: Option, available_decisions: Vec, network_approval_context: Option, - additional_permissions: Option, + additional_permissions: Option, }, Permissions { thread_id: ThreadId, @@ -719,7 +719,7 @@ impl ApprovalOption { fn exec_options( available_decisions: &[ReviewDecision], network_approval_context: Option<&NetworkApprovalContext>, - additional_permissions: Option<&PermissionProfile>, + additional_permissions: Option<&AdditionalPermissionProfile>, ) -> Vec { available_decisions .iter() @@ -808,7 +808,7 @@ fn exec_options( } pub(crate) fn format_additional_permissions_rule( - additional_permissions: &PermissionProfile, + additional_permissions: &AdditionalPermissionProfile, ) -> Option { let mut parts = Vec::new(); if additional_permissions @@ -1341,7 +1341,7 @@ mod tests { #[test] fn additional_permissions_exec_options_hide_execpolicy_amendment() { - let additional_permissions = PermissionProfile { + let additional_permissions = AdditionalPermissionProfile { file_system: Some(FileSystemPermissions::from_read_write_roots( Some(vec![absolute_path("/tmp/readme.txt")]), Some(vec![absolute_path("/tmp/out.txt")]), @@ -1383,7 +1383,7 @@ mod tests { #[test] fn additional_permissions_rule_shows_non_path_file_system_entries() { - let additional_permissions = PermissionProfile { + let additional_permissions = AdditionalPermissionProfile { file_system: Some(FileSystemPermissions { entries: vec![ FileSystemSandboxEntry { @@ -1477,7 +1477,7 @@ mod tests { reason: None, available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort], network_approval_context: None, - additional_permissions: Some(PermissionProfile { + additional_permissions: Some(AdditionalPermissionProfile { network: Some(NetworkPermissions { enabled: Some(true), }), @@ -1527,7 +1527,7 @@ mod tests { reason: Some("need filesystem access".into()), available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort], network_approval_context: None, - additional_permissions: Some(PermissionProfile { + additional_permissions: Some(AdditionalPermissionProfile { network: Some(NetworkPermissions { enabled: Some(true), }), diff --git a/codex-rs/tui/src/chatwidget/tests/approval_requests.rs b/codex-rs/tui/src/chatwidget/tests/approval_requests.rs index 320dc8a9c591..2e5c51307c2a 100644 --- a/codex-rs/tui/src/chatwidget/tests/approval_requests.rs +++ b/codex-rs/tui/src/chatwidget/tests/approval_requests.rs @@ -133,7 +133,7 @@ fn app_server_exec_approval_request_preserves_permissions_context() { ); assert_eq!( request.additional_permissions, - Some(PermissionProfile { + Some(codex_protocol::models::AdditionalPermissionProfile { network: Some(NetworkPermissions { enabled: Some(true), }), diff --git a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs index a23f137b40c2..c095d372d03e 100644 --- a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs +++ b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs @@ -92,11 +92,9 @@ async fn submission_includes_configured_permission_profile() { let conversation_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); - let expected_permission_profile = PermissionProfile { - network: Some(NetworkPermissions { - enabled: Some(false), - }), - file_system: Some(FileSystemPermissions { + let expected_permission_profile = PermissionProfile::Managed { + network: codex_protocol::permissions::NetworkSandboxPolicy::Restricted, + file_system: codex_protocol::models::ManagedFileSystemPermissions::Restricted { entries: vec![ codex_protocol::permissions::FileSystemSandboxEntry { path: codex_protocol::permissions::FileSystemPath::Special { @@ -112,7 +110,7 @@ async fn submission_includes_configured_permission_profile() { }, ], glob_scan_max_depth: None, - }), + }, }; let configured = codex_protocol::protocol::SessionConfiguredEvent { session_id: conversation_id,