permissions: expose active profile metadata

This commit is contained in:
Michael Bolin
2026-04-28 18:11:42 -07:00
parent 24be9ac0a4
commit 46f2ed235c
41 changed files with 715 additions and 1178 deletions
@@ -65,7 +65,6 @@ use codex_app_server_protocol::InitializeCapabilities;
use codex_app_server_protocol::InitializeParams;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::NonSteerableTurnKind;
use codex_app_server_protocol::PermissionProfile as AppServerPermissionProfile;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::SandboxPolicy as AppServerSandboxPolicy;
use codex_app_server_protocol::ServerNotification;
@@ -154,16 +153,12 @@ fn sample_thread_start_response(thread_id: &str, ephemeral: bool, model: &str) -
approval_policy: AppServerAskForApproval::OnFailure,
approvals_reviewer: AppServerApprovalsReviewer::User,
sandbox: AppServerSandboxPolicy::DangerFullAccess,
permission_profile: Some(sample_permission_profile()),
active_permission_profile: None,
reasoning_effort: None,
},
}
}
fn sample_permission_profile() -> AppServerPermissionProfile {
CorePermissionProfile::Disabled.into()
}
fn sample_app_server_client_metadata() -> CodexAppServerClientMetadata {
CodexAppServerClientMetadata {
product_client_id: DEFAULT_ORIGINATOR.to_string(),
@@ -210,7 +205,7 @@ fn sample_thread_resume_response_with_source(
approval_policy: AppServerAskForApproval::OnFailure,
approvals_reviewer: AppServerApprovalsReviewer::User,
sandbox: AppServerSandboxPolicy::DangerFullAccess,
permission_profile: Some(sample_permission_profile()),
active_permission_profile: None,
reasoning_effort: None,
},
}
@@ -5365,6 +5365,26 @@
"title": "AccountUpdatedNotification",
"type": "object"
},
"ActivePermissionProfile": {
"properties": {
"extends": {
"default": null,
"description": "Parent profile identifier once permissions profiles support inheritance. This is currently always `null`.",
"type": [
"string",
"null"
]
},
"id": {
"description": "Identifier from `default_permissions`, such as a built-in `:workspace` profile or a user-defined `[permissions.<id>]` profile.",
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"AddCreditsNudgeCreditType": {
"enum": [
"credits",
@@ -14668,7 +14688,7 @@
"$ref": "#/definitions/v2/SandboxPolicy"
}
],
"description": "Legacy sandbox policy retained for compatibility. New clients should use `permissionProfile` when present as the canonical active permissions view."
"description": "Legacy sandbox policy retained for compatibility. New clients should use `activePermissionProfile` when they need stable permissions profile identity."
},
"serviceTier": {
"anyOf": [
@@ -16179,7 +16199,7 @@
"$ref": "#/definitions/v2/SandboxPolicy"
}
],
"description": "Legacy sandbox policy retained for compatibility. New clients should use `permissionProfile` when present as the canonical active permissions view."
"description": "Legacy sandbox policy retained for compatibility. New clients should use `activePermissionProfile` when they need stable permissions profile identity."
},
"serviceTier": {
"anyOf": [
@@ -16483,7 +16503,7 @@
"$ref": "#/definitions/v2/SandboxPolicy"
}
],
"description": "Legacy sandbox policy retained for compatibility. New clients should use `permissionProfile` when present as the canonical active permissions view."
"description": "Legacy sandbox policy retained for compatibility. New clients should use `activePermissionProfile` when they need stable permissions profile identity."
},
"serviceTier": {
"anyOf": [
@@ -130,6 +130,26 @@
"title": "AccountUpdatedNotification",
"type": "object"
},
"ActivePermissionProfile": {
"properties": {
"extends": {
"default": null,
"description": "Parent profile identifier once permissions profiles support inheritance. This is currently always `null`.",
"type": [
"string",
"null"
]
},
"id": {
"description": "Identifier from `default_permissions`, such as a built-in `:workspace` profile or a user-defined `[permissions.<id>]` profile.",
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"AddCreditsNudgeCreditType": {
"enum": [
"credits",
@@ -12554,7 +12574,7 @@
"$ref": "#/definitions/SandboxPolicy"
}
],
"description": "Legacy sandbox policy retained for compatibility. New clients should use `permissionProfile` when present as the canonical active permissions view."
"description": "Legacy sandbox policy retained for compatibility. New clients should use `activePermissionProfile` when they need stable permissions profile identity."
},
"serviceTier": {
"anyOf": [
@@ -14065,7 +14085,7 @@
"$ref": "#/definitions/SandboxPolicy"
}
],
"description": "Legacy sandbox policy retained for compatibility. New clients should use `permissionProfile` when present as the canonical active permissions view."
"description": "Legacy sandbox policy retained for compatibility. New clients should use `activePermissionProfile` when they need stable permissions profile identity."
},
"serviceTier": {
"anyOf": [
@@ -14369,7 +14389,7 @@
"$ref": "#/definitions/SandboxPolicy"
}
],
"description": "Legacy sandbox policy retained for compatibility. New clients should use `permissionProfile` when present as the canonical active permissions view."
"description": "Legacy sandbox policy retained for compatibility. New clients should use `activePermissionProfile` when they need stable permissions profile identity."
},
"serviceTier": {
"anyOf": [
@@ -5,6 +5,26 @@
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
"ActivePermissionProfile": {
"properties": {
"extends": {
"default": null,
"description": "Parent profile identifier once permissions profiles support inheritance. This is currently always `null`.",
"type": [
"string",
"null"
]
},
"id": {
"description": "Identifier from `default_permissions`, such as a built-in `:workspace` profile or a user-defined `[permissions.<id>]` profile.",
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"AgentPath": {
"type": "string"
},
@@ -450,202 +470,6 @@
],
"type": "string"
},
"FileSystemAccessMode": {
"enum": [
"read",
"write",
"none"
],
"type": "string"
},
"FileSystemPath": {
"oneOf": [
{
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"path"
],
"title": "PathFileSystemPathType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "PathFileSystemPath",
"type": "object"
},
{
"properties": {
"pattern": {
"type": "string"
},
"type": {
"enum": [
"glob_pattern"
],
"title": "GlobPatternFileSystemPathType",
"type": "string"
}
},
"required": [
"pattern",
"type"
],
"title": "GlobPatternFileSystemPath",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"special"
],
"title": "SpecialFileSystemPathType",
"type": "string"
},
"value": {
"$ref": "#/definitions/FileSystemSpecialPath"
}
},
"required": [
"type",
"value"
],
"title": "SpecialFileSystemPath",
"type": "object"
}
]
},
"FileSystemSandboxEntry": {
"properties": {
"access": {
"$ref": "#/definitions/FileSystemAccessMode"
},
"path": {
"$ref": "#/definitions/FileSystemPath"
}
},
"required": [
"access",
"path"
],
"type": "object"
},
"FileSystemSpecialPath": {
"oneOf": [
{
"properties": {
"kind": {
"enum": [
"root"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "RootFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"minimal"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "MinimalFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"project_roots"
],
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind"
],
"title": "KindFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"tmpdir"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "TmpdirFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"slash_tmp"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "SlashTmpFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"unknown"
],
"type": "string"
},
"path": {
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind",
"path"
],
"type": "object"
}
]
},
"FileUpdateChange": {
"properties": {
"diff": {
@@ -884,135 +708,6 @@
}
]
},
"PermissionProfile": {
"oneOf": [
{
"description": "Codex owns sandbox construction for this profile.",
"properties": {
"fileSystem": {
"$ref": "#/definitions/PermissionProfileFileSystemPermissions"
},
"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"
},
{
"description": "Filesystem isolation is enforced by an external caller.",
"properties": {
"network": {
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
},
"type": {
"enum": [
"external"
],
"title": "ExternalPermissionProfileType",
"type": "string"
}
},
"required": [
"network",
"type"
],
"title": "ExternalPermissionProfile",
"type": "object"
}
]
},
"PermissionProfileFileSystemPermissions": {
"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"
}
},
"required": [
"entries",
"type"
],
"title": "RestrictedPermissionProfileFileSystemPermissions",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"unrestricted"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
"type": "string"
}
},
"required": [
"type"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
"type": "object"
}
]
},
"PermissionProfileNetworkPermissions": {
"properties": {
"enabled": {
"type": "boolean"
}
},
"required": [
"enabled"
],
"type": "object"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
@@ -2501,7 +2196,7 @@
"$ref": "#/definitions/SandboxPolicy"
}
],
"description": "Legacy sandbox policy retained for compatibility. New clients should use `permissionProfile` when present as the canonical active permissions view."
"description": "Legacy sandbox policy retained for compatibility. New clients should use `activePermissionProfile` when they need stable permissions profile identity."
},
"serviceTier": {
"anyOf": [
@@ -5,6 +5,26 @@
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
"ActivePermissionProfile": {
"properties": {
"extends": {
"default": null,
"description": "Parent profile identifier once permissions profiles support inheritance. This is currently always `null`.",
"type": [
"string",
"null"
]
},
"id": {
"description": "Identifier from `default_permissions`, such as a built-in `:workspace` profile or a user-defined `[permissions.<id>]` profile.",
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"AgentPath": {
"type": "string"
},
@@ -450,202 +470,6 @@
],
"type": "string"
},
"FileSystemAccessMode": {
"enum": [
"read",
"write",
"none"
],
"type": "string"
},
"FileSystemPath": {
"oneOf": [
{
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"path"
],
"title": "PathFileSystemPathType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "PathFileSystemPath",
"type": "object"
},
{
"properties": {
"pattern": {
"type": "string"
},
"type": {
"enum": [
"glob_pattern"
],
"title": "GlobPatternFileSystemPathType",
"type": "string"
}
},
"required": [
"pattern",
"type"
],
"title": "GlobPatternFileSystemPath",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"special"
],
"title": "SpecialFileSystemPathType",
"type": "string"
},
"value": {
"$ref": "#/definitions/FileSystemSpecialPath"
}
},
"required": [
"type",
"value"
],
"title": "SpecialFileSystemPath",
"type": "object"
}
]
},
"FileSystemSandboxEntry": {
"properties": {
"access": {
"$ref": "#/definitions/FileSystemAccessMode"
},
"path": {
"$ref": "#/definitions/FileSystemPath"
}
},
"required": [
"access",
"path"
],
"type": "object"
},
"FileSystemSpecialPath": {
"oneOf": [
{
"properties": {
"kind": {
"enum": [
"root"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "RootFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"minimal"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "MinimalFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"project_roots"
],
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind"
],
"title": "KindFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"tmpdir"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "TmpdirFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"slash_tmp"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "SlashTmpFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"unknown"
],
"type": "string"
},
"path": {
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind",
"path"
],
"type": "object"
}
]
},
"FileUpdateChange": {
"properties": {
"diff": {
@@ -884,135 +708,6 @@
}
]
},
"PermissionProfile": {
"oneOf": [
{
"description": "Codex owns sandbox construction for this profile.",
"properties": {
"fileSystem": {
"$ref": "#/definitions/PermissionProfileFileSystemPermissions"
},
"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"
},
{
"description": "Filesystem isolation is enforced by an external caller.",
"properties": {
"network": {
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
},
"type": {
"enum": [
"external"
],
"title": "ExternalPermissionProfileType",
"type": "string"
}
},
"required": [
"network",
"type"
],
"title": "ExternalPermissionProfile",
"type": "object"
}
]
},
"PermissionProfileFileSystemPermissions": {
"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"
}
},
"required": [
"entries",
"type"
],
"title": "RestrictedPermissionProfileFileSystemPermissions",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"unrestricted"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
"type": "string"
}
},
"required": [
"type"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
"type": "object"
}
]
},
"PermissionProfileNetworkPermissions": {
"properties": {
"enabled": {
"type": "boolean"
}
},
"required": [
"enabled"
],
"type": "object"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
@@ -2501,7 +2196,7 @@
"$ref": "#/definitions/SandboxPolicy"
}
],
"description": "Legacy sandbox policy retained for compatibility. New clients should use `permissionProfile` when present as the canonical active permissions view."
"description": "Legacy sandbox policy retained for compatibility. New clients should use `activePermissionProfile` when they need stable permissions profile identity."
},
"serviceTier": {
"anyOf": [
@@ -5,6 +5,26 @@
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
"ActivePermissionProfile": {
"properties": {
"extends": {
"default": null,
"description": "Parent profile identifier once permissions profiles support inheritance. This is currently always `null`.",
"type": [
"string",
"null"
]
},
"id": {
"description": "Identifier from `default_permissions`, such as a built-in `:workspace` profile or a user-defined `[permissions.<id>]` profile.",
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"AgentPath": {
"type": "string"
},
@@ -450,202 +470,6 @@
],
"type": "string"
},
"FileSystemAccessMode": {
"enum": [
"read",
"write",
"none"
],
"type": "string"
},
"FileSystemPath": {
"oneOf": [
{
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"path"
],
"title": "PathFileSystemPathType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "PathFileSystemPath",
"type": "object"
},
{
"properties": {
"pattern": {
"type": "string"
},
"type": {
"enum": [
"glob_pattern"
],
"title": "GlobPatternFileSystemPathType",
"type": "string"
}
},
"required": [
"pattern",
"type"
],
"title": "GlobPatternFileSystemPath",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"special"
],
"title": "SpecialFileSystemPathType",
"type": "string"
},
"value": {
"$ref": "#/definitions/FileSystemSpecialPath"
}
},
"required": [
"type",
"value"
],
"title": "SpecialFileSystemPath",
"type": "object"
}
]
},
"FileSystemSandboxEntry": {
"properties": {
"access": {
"$ref": "#/definitions/FileSystemAccessMode"
},
"path": {
"$ref": "#/definitions/FileSystemPath"
}
},
"required": [
"access",
"path"
],
"type": "object"
},
"FileSystemSpecialPath": {
"oneOf": [
{
"properties": {
"kind": {
"enum": [
"root"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "RootFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"minimal"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "MinimalFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"project_roots"
],
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind"
],
"title": "KindFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"tmpdir"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "TmpdirFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"slash_tmp"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "SlashTmpFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"unknown"
],
"type": "string"
},
"path": {
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind",
"path"
],
"type": "object"
}
]
},
"FileUpdateChange": {
"properties": {
"diff": {
@@ -884,135 +708,6 @@
}
]
},
"PermissionProfile": {
"oneOf": [
{
"description": "Codex owns sandbox construction for this profile.",
"properties": {
"fileSystem": {
"$ref": "#/definitions/PermissionProfileFileSystemPermissions"
},
"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"
},
{
"description": "Filesystem isolation is enforced by an external caller.",
"properties": {
"network": {
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
},
"type": {
"enum": [
"external"
],
"title": "ExternalPermissionProfileType",
"type": "string"
}
},
"required": [
"network",
"type"
],
"title": "ExternalPermissionProfile",
"type": "object"
}
]
},
"PermissionProfileFileSystemPermissions": {
"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"
}
},
"required": [
"entries",
"type"
],
"title": "RestrictedPermissionProfileFileSystemPermissions",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"unrestricted"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
"type": "string"
}
},
"required": [
"type"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
"type": "object"
}
]
},
"PermissionProfileNetworkPermissions": {
"properties": {
"enabled": {
"type": "boolean"
}
},
"required": [
"enabled"
],
"type": "object"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
@@ -2501,7 +2196,7 @@
"$ref": "#/definitions/SandboxPolicy"
}
],
"description": "Legacy sandbox policy retained for compatibility. New clients should use `permissionProfile` when present as the canonical active permissions view."
"description": "Legacy sandbox policy retained for compatibility. New clients should use `activePermissionProfile` when they need stable permissions profile identity."
},
"serviceTier": {
"anyOf": [
@@ -0,0 +1,15 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ActivePermissionProfile = {
/**
* Identifier from `default_permissions`, such as a built-in `:workspace`
* profile or a user-defined `[permissions.<id>]` profile.
*/
id: string,
/**
* Parent profile identifier once permissions profiles support
* inheritance. This is currently always `null`.
*/
extends: string | null, };
@@ -17,7 +17,7 @@ instructionSources: Array<AbsolutePathBuf>, approvalPolicy: AskForApproval, /**
*/
approvalsReviewer: ApprovalsReviewer, /**
* Legacy sandbox policy retained for compatibility. New clients should use
* `permissionProfile` when present as the canonical active permissions
* view.
* `activePermissionProfile` when they need stable permissions profile
* identity.
*/
sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null};
@@ -17,7 +17,7 @@ instructionSources: Array<AbsolutePathBuf>, approvalPolicy: AskForApproval, /**
*/
approvalsReviewer: ApprovalsReviewer, /**
* Legacy sandbox policy retained for compatibility. New clients should use
* `permissionProfile` when present as the canonical active permissions
* view.
* `activePermissionProfile` when they need stable permissions profile
* identity.
*/
sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null};
@@ -17,7 +17,7 @@ instructionSources: Array<AbsolutePathBuf>, approvalPolicy: AskForApproval, /**
*/
approvalsReviewer: ApprovalsReviewer, /**
* Legacy sandbox policy retained for compatibility. New clients should use
* `permissionProfile` when present as the canonical active permissions
* view.
* `activePermissionProfile` when they need stable permissions profile
* identity.
*/
sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null};
@@ -4,6 +4,7 @@ export type { Account } from "./Account";
export type { AccountLoginCompletedNotification } from "./AccountLoginCompletedNotification";
export type { AccountRateLimitsUpdatedNotification } from "./AccountRateLimitsUpdatedNotification";
export type { AccountUpdatedNotification } from "./AccountUpdatedNotification";
export type { ActivePermissionProfile } from "./ActivePermissionProfile";
export type { AddCreditsNudgeCreditType } from "./AddCreditsNudgeCreditType";
export type { AddCreditsNudgeEmailStatus } from "./AddCreditsNudgeEmailStatus";
export type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions";
@@ -1998,12 +1998,7 @@ mod tests {
approval_policy: v2::AskForApproval::OnFailure,
approvals_reviewer: v2::ApprovalsReviewer::User,
sandbox: v2::SandboxPolicy::DangerFullAccess,
permission_profile: Some(
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
&codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
)
.into(),
),
active_permission_profile: None,
reasoning_effort: None,
},
};
@@ -2046,9 +2041,7 @@ mod tests {
"sandbox": {
"type": "dangerFullAccess"
},
"permissionProfile": {
"type": "disabled"
},
"activePermissionProfile": null,
"reasoningEffort": null
}
}),
+53 -18
View File
@@ -38,6 +38,7 @@ 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::ActivePermissionProfile as CoreActivePermissionProfile;
use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile;
use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions;
use codex_protocol::models::ManagedFileSystemPermissions as CoreManagedFileSystemPermissions;
@@ -1703,6 +1704,37 @@ impl From<PermissionProfile> for CorePermissionProfile {
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ActivePermissionProfile {
/// Identifier from `default_permissions`, such as a built-in `:workspace`
/// profile or a user-defined `[permissions.<id>]` profile.
pub id: String,
/// Parent profile identifier once permissions profiles support
/// inheritance. This is currently always `null`.
#[serde(default)]
pub extends: Option<String>,
}
impl From<CoreActivePermissionProfile> for ActivePermissionProfile {
fn from(value: CoreActivePermissionProfile) -> Self {
Self {
id: value.id,
extends: value.extends,
}
}
}
impl From<ActivePermissionProfile> for CoreActivePermissionProfile {
fn from(value: ActivePermissionProfile) -> Self {
Self {
id: value.id,
extends: value.extends,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -3527,13 +3559,14 @@ pub struct ThreadStartResponse {
/// Reviewer currently used for approval requests on this thread.
pub approvals_reviewer: ApprovalsReviewer,
/// Legacy sandbox policy retained for compatibility. New clients should use
/// `permissionProfile` when present as the canonical active permissions
/// view.
/// `activePermissionProfile` when they need stable permissions profile
/// identity.
pub sandbox: SandboxPolicy,
/// Canonical active permissions view for this thread.
#[experimental("thread/start.permissionProfile")]
/// Named profile that produced the active permissions, when the active
/// permissions came from `default_permissions`.
#[experimental("thread/start.activePermissionProfile")]
#[serde(default)]
pub permission_profile: Option<PermissionProfile>,
pub active_permission_profile: Option<ActivePermissionProfile>,
pub reasoning_effort: Option<ReasoningEffort>,
}
@@ -3633,13 +3666,14 @@ pub struct ThreadResumeResponse {
/// Reviewer currently used for approval requests on this thread.
pub approvals_reviewer: ApprovalsReviewer,
/// Legacy sandbox policy retained for compatibility. New clients should use
/// `permissionProfile` when present as the canonical active permissions
/// view.
/// `activePermissionProfile` when they need stable permissions profile
/// identity.
pub sandbox: SandboxPolicy,
/// Canonical active permissions view for this thread.
#[experimental("thread/resume.permissionProfile")]
/// Named profile that produced the active permissions, when the active
/// permissions came from `default_permissions`.
#[experimental("thread/resume.activePermissionProfile")]
#[serde(default)]
pub permission_profile: Option<PermissionProfile>,
pub active_permission_profile: Option<ActivePermissionProfile>,
pub reasoning_effort: Option<ReasoningEffort>,
}
@@ -3730,13 +3764,14 @@ pub struct ThreadForkResponse {
/// Reviewer currently used for approval requests on this thread.
pub approvals_reviewer: ApprovalsReviewer,
/// Legacy sandbox policy retained for compatibility. New clients should use
/// `permissionProfile` when present as the canonical active permissions
/// view.
/// `activePermissionProfile` when they need stable permissions profile
/// identity.
pub sandbox: SandboxPolicy,
/// Canonical active permissions view for this thread.
#[experimental("thread/fork.permissionProfile")]
/// Named profile that produced the active permissions, when the active
/// permissions came from `default_permissions`.
#[experimental("thread/fork.activePermissionProfile")]
#[serde(default)]
pub permission_profile: Option<PermissionProfile>,
pub active_permission_profile: Option<ActivePermissionProfile>,
pub reasoning_effort: Option<ReasoningEffort>,
}
@@ -10720,9 +10755,9 @@ mod tests {
assert_eq!(start.instruction_sources, Vec::<AbsolutePathBuf>::new());
assert_eq!(resume.instruction_sources, Vec::<AbsolutePathBuf>::new());
assert_eq!(fork.instruction_sources, Vec::<AbsolutePathBuf>::new());
assert_eq!(start.permission_profile, None);
assert_eq!(resume.permission_profile, None);
assert_eq!(fork.permission_profile, None);
assert_eq!(start.active_permission_profile, None);
assert_eq!(resume.active_permission_profile, None);
assert_eq!(fork.active_permission_profile, None);
}
#[test]
@@ -2796,8 +2796,9 @@ impl CodexMessageProcessor {
&config_snapshot.permission_profile,
config_snapshot.cwd.as_path(),
);
let permission_profile =
thread_response_permission_profile(config_snapshot.permission_profile);
let active_permission_profile = thread_response_active_permission_profile(
config_snapshot.active_permission_profile,
);
let response = ThreadStartResponse {
thread: thread.clone(),
@@ -2809,7 +2810,7 @@ impl CodexMessageProcessor {
approval_policy: config_snapshot.approval_policy.into(),
approvals_reviewer: config_snapshot.approvals_reviewer.into(),
sandbox,
permission_profile,
active_permission_profile,
reasoning_effort: config_snapshot.reasoning_effort,
};
Ok::<_, JSONRPCErrorError>((response, thread_started_notification(thread)))
@@ -4347,8 +4348,9 @@ impl CodexMessageProcessor {
&config_snapshot.permission_profile,
config_snapshot.cwd.as_path(),
);
let permission_profile =
thread_response_permission_profile(config_snapshot.permission_profile.clone());
let active_permission_profile = thread_response_active_permission_profile(
config_snapshot.active_permission_profile,
);
let response = ThreadResumeResponse {
thread,
@@ -4360,7 +4362,7 @@ impl CodexMessageProcessor {
approval_policy: session_configured.approval_policy.into(),
approvals_reviewer: session_configured.approvals_reviewer.into(),
sandbox,
permission_profile,
active_permission_profile,
reasoning_effort: session_configured.reasoning_effort,
};
self.analytics_events_client.track_response(
@@ -4947,8 +4949,9 @@ impl CodexMessageProcessor {
&config_snapshot.permission_profile,
config_snapshot.cwd.as_path(),
);
let permission_profile =
thread_response_permission_profile(config_snapshot.permission_profile);
let active_permission_profile = thread_response_active_permission_profile(
config_snapshot.active_permission_profile,
);
let response = ThreadForkResponse {
thread: thread.clone(),
@@ -4960,7 +4963,7 @@ impl CodexMessageProcessor {
approval_policy: session_configured.approval_policy.into(),
approvals_reviewer: session_configured.approvals_reviewer.into(),
sandbox,
permission_profile,
active_permission_profile,
reasoning_effort: session_configured.reasoning_effort,
};
@@ -8223,13 +8226,15 @@ async fn handle_pending_thread_resume_request(
approval_policy,
approvals_reviewer,
permission_profile,
active_permission_profile,
cwd,
reasoning_effort,
..
} = pending.config_snapshot;
let instruction_sources = pending.instruction_sources;
let sandbox = thread_response_sandbox_policy(&permission_profile, cwd.as_path());
let permission_profile = thread_response_permission_profile(permission_profile);
let active_permission_profile =
thread_response_active_permission_profile(active_permission_profile);
let response = ThreadResumeResponse {
thread,
@@ -8241,7 +8246,7 @@ async fn handle_pending_thread_resume_request(
approval_policy: approval_policy.into(),
approvals_reviewer: approvals_reviewer.into(),
sandbox,
permission_profile,
active_permission_profile,
reasoning_effort,
};
let token_usage_thread = pending.include_turns.then(|| response.thread.clone());
@@ -9365,10 +9370,10 @@ fn with_thread_spawn_agent_metadata(
}
}
fn thread_response_permission_profile(
permission_profile: codex_protocol::models::PermissionProfile,
) -> Option<codex_app_server_protocol::PermissionProfile> {
Some(permission_profile.into())
fn thread_response_active_permission_profile(
active_permission_profile: Option<codex_protocol::models::ActivePermissionProfile>,
) -> Option<codex_app_server_protocol::ActivePermissionProfile> {
active_permission_profile.map(Into::into)
}
fn thread_response_sandbox_policy(
@@ -9912,23 +9917,6 @@ mod tests {
);
}
#[test]
fn thread_response_permission_profile_preserves_enforcement() {
let full_access_profile = codex_protocol::models::PermissionProfile::Disabled;
let external_profile = codex_protocol::models::PermissionProfile::External {
network: codex_protocol::permissions::NetworkSandboxPolicy::Restricted,
};
assert_eq!(
thread_response_permission_profile(external_profile.clone()),
Some(external_profile.into())
);
assert_eq!(
thread_response_permission_profile(full_access_profile.clone()),
Some(full_access_profile.into())
);
}
#[test]
fn requested_permissions_trust_project_uses_permission_profile_intent() {
let cwd = test_path_buf("/tmp/project").abs();
@@ -10165,6 +10153,7 @@ mod tests {
approval_policy: codex_protocol::protocol::AskForApproval::OnRequest,
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User,
permission_profile: codex_protocol::models::PermissionProfile::Disabled,
active_permission_profile: None,
cwd,
ephemeral: false,
reasoning_effort: None,
+2
View File
@@ -15,6 +15,7 @@ use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::mcp::CallToolResult;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::ContentItem;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::ResponseInputItem;
@@ -48,6 +49,7 @@ pub struct ThreadConfigSnapshot {
pub approval_policy: AskForApproval,
pub approvals_reviewer: ApprovalsReviewer,
pub permission_profile: PermissionProfile,
pub active_permission_profile: Option<ActivePermissionProfile>,
pub cwd: AbsolutePathBuf,
pub ephemeral: bool,
pub reasoning_effort: Option<ReasoningEffort>,
+84
View File
@@ -60,6 +60,7 @@ 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::ActivePermissionProfile;
use codex_protocol::models::ManagedFileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::SandboxEnforcement;
@@ -907,6 +908,14 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::
config.permissions.network_sandbox_policy(),
NetworkSandboxPolicy::Restricted
);
assert_eq!(
config
.permissions
.active_permission_profile()
.as_ref()
.map(|active| active.id.as_str()),
Some("workspace")
);
Ok(())
}
@@ -928,6 +937,7 @@ async fn permission_profile_override_populates_runtime_permissions() -> std::io:
.await?;
assert_eq!(config.permissions.permission_profile(), permission_profile);
assert_eq!(config.permissions.active_permission_profile(), None);
assert_eq!(
&config.legacy_sandbox_policy(),
&SandboxPolicy::DangerFullAccess
@@ -1267,6 +1277,14 @@ async fn default_permissions_can_select_builtin_profile_without_permissions_tabl
.await?;
let policy = config.permissions.file_system_sandbox_policy();
assert_eq!(
config
.permissions
.active_permission_profile()
.as_ref()
.map(|active| active.id.as_str()),
Some(":workspace")
);
assert!(
policy.can_write_path_with_cwd(cwd.path(), cwd.path()),
"expected :workspace to allow writing the project root, policy: {policy:?}"
@@ -1278,6 +1296,48 @@ async fn default_permissions_can_select_builtin_profile_without_permissions_tabl
Ok(())
}
#[tokio::test]
async fn explicit_builtin_workspace_profile_ignores_legacy_workspace_write_settings()
-> std::io::Result<()> {
let codex_home = TempDir::new()?;
let cwd = TempDir::new()?;
let extra_root = TempDir::new()?;
let config = Config::load_from_base_config_with_overrides(
ConfigToml {
default_permissions: Some(":workspace".to_string()),
sandbox_workspace_write: Some(SandboxWorkspaceWrite {
writable_roots: vec![extra_root.path().abs()],
network_access: true,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
}),
..Default::default()
},
ConfigOverrides {
cwd: Some(cwd.path().to_path_buf()),
..Default::default()
},
codex_home.abs(),
)
.await?;
let policy = config.permissions.file_system_sandbox_policy();
assert_eq!(
config.permissions.network_sandbox_policy(),
NetworkSandboxPolicy::Restricted
);
assert!(
!policy.entries.iter().any(|entry| matches!(
&entry.path,
FileSystemPath::Path { path } if path.as_path() == extra_root.path()
)),
"explicit :workspace should not inherit sandbox_workspace_write roots as concrete grants, \
policy: {policy:?}"
);
Ok(())
}
#[tokio::test]
async fn empty_config_defaults_to_builtin_profile_for_trusted_project() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
@@ -1303,6 +1363,18 @@ async fn empty_config_defaults_to_builtin_profile_for_trusted_project() -> std::
.await?;
let policy = config.permissions.file_system_sandbox_policy();
assert_eq!(
config
.permissions
.active_permission_profile()
.as_ref()
.map(|active| active.id.as_str()),
Some(if cfg!(target_os = "windows") {
":read-only"
} else {
":workspace"
})
);
if cfg!(target_os = "windows") {
assert!(
!policy.can_write_path_with_cwd(cwd.path(), cwd.path()),
@@ -1484,6 +1556,14 @@ async fn default_permissions_can_select_builtin_no_sandbox_profile() -> std::io:
config.permissions.permission_profile(),
PermissionProfile::Disabled
);
assert_eq!(
config
.permissions
.active_permission_profile()
.as_ref()
.map(|active| active.id.as_str()),
Some(":danger-no-sandbox")
);
Ok(())
}
@@ -5894,6 +5974,7 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
permissions: Permissions {
approval_policy: Constrained::allow_any(AskForApproval::Never),
permission_profile: Constrained::allow_any(PermissionProfile::read_only()),
active_permission_profile: Some(ActivePermissionProfile::new(":read-only")),
network: None,
allow_login_shell: true,
shell_environment_policy: ShellEnvironmentPolicy::default(),
@@ -6088,6 +6169,7 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
permissions: Permissions {
approval_policy: Constrained::allow_any(AskForApproval::UnlessTrusted),
permission_profile: Constrained::allow_any(PermissionProfile::read_only()),
active_permission_profile: Some(ActivePermissionProfile::new(":read-only")),
network: None,
allow_login_shell: true,
shell_environment_policy: ShellEnvironmentPolicy::default(),
@@ -6236,6 +6318,7 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
permissions: Permissions {
approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
permission_profile: Constrained::allow_any(PermissionProfile::read_only()),
active_permission_profile: Some(ActivePermissionProfile::new(":read-only")),
network: None,
allow_login_shell: true,
shell_environment_policy: ShellEnvironmentPolicy::default(),
@@ -6369,6 +6452,7 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
permissions: Permissions {
approval_policy: Constrained::allow_any(AskForApproval::OnFailure),
permission_profile: Constrained::allow_any(PermissionProfile::read_only()),
active_permission_profile: Some(ActivePermissionProfile::new(":read-only")),
network: None,
allow_login_shell: true,
shell_environment_policy: ShellEnvironmentPolicy::default(),
+36 -2
View File
@@ -83,6 +83,7 @@ use codex_protocol::config_types::Verbosity;
use codex_protocol::config_types::WebSearchConfig;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::SandboxEnforcement;
use codex_protocol::openai_models::ModelsResponse;
@@ -226,6 +227,9 @@ pub struct Permissions {
/// Canonical effective runtime permissions after config requirements and
/// runtime readable-root additions have been applied.
pub permission_profile: Constrained<PermissionProfile>,
/// Named profile selected by config, when the active permissions came from
/// `default_permissions` rather than an ad-hoc override.
pub active_permission_profile: Option<ActivePermissionProfile>,
/// Effective network configuration applied to all spawned processes.
pub network: Option<NetworkProxySpec>,
/// Whether the model may request a login shell for shell-based tools.
@@ -253,6 +257,11 @@ impl Permissions {
self.permission_profile.get().clone()
}
/// Named profile selected by config, if the current profile has one.
pub fn active_permission_profile(&self) -> Option<ActivePermissionProfile> {
self.active_permission_profile.clone()
}
/// Effective filesystem sandbox policy derived from the canonical profile.
pub fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy {
self.permission_profile.get().file_system_sandbox_policy()
@@ -311,6 +320,7 @@ impl Permissions {
);
self.permission_profile.set(permission_profile)?;
self.active_permission_profile = None;
Ok(())
}
@@ -318,10 +328,24 @@ impl Permissions {
pub fn set_permission_profile(
&mut self,
permission_profile: PermissionProfile,
) -> ConstraintResult<()> {
self.set_permission_profile_with_active_profile(
permission_profile,
/*active_permission_profile*/ None,
)
}
/// Replace permissions from the canonical profile and record the named
/// source profile, if one is known.
pub fn set_permission_profile_with_active_profile(
&mut self,
permission_profile: PermissionProfile,
active_permission_profile: Option<ActivePermissionProfile>,
) -> ConstraintResult<()> {
self.permission_profile.can_set(&permission_profile)?;
self.permission_profile.set(permission_profile)?;
self.active_permission_profile = active_permission_profile;
Ok(())
}
}
@@ -2007,6 +2031,7 @@ impl Config {
configured_network_proxy_config,
permission_profile,
file_system_sandbox_policy,
active_permission_profile,
) = if let Some(mut permission_profile) = permission_profile {
let (mut file_system_sandbox_policy, network_sandbox_policy) =
permission_profile.to_runtime_permissions();
@@ -2052,11 +2077,17 @@ impl Config {
configured_network_proxy_config,
permission_profile,
file_system_sandbox_policy,
None,
)
} else if profiles_are_active {
let default_permissions = cfg.default_permissions.as_deref().unwrap_or_else(|| {
default_builtin_permission_profile_name(&active_project, windows_sandbox_level)
});
let builtin_workspace_write_settings = if using_implicit_builtin_profile {
cfg.sandbox_workspace_write.as_ref()
} else {
None
};
let configured_network_proxy_config = network_proxy_config_for_profile_selection(
cfg.permissions.as_ref(),
default_permissions,
@@ -2065,12 +2096,12 @@ impl Config {
compile_permission_profile_selection(
cfg.permissions.as_ref(),
default_permissions,
cfg.sandbox_workspace_write.as_ref(),
builtin_workspace_write_settings,
resolved_cwd.as_path(),
&mut startup_warnings,
)?;
let mut permission_profile = if let Some(permission_profile) =
builtin_permission_profile(default_permissions, cfg.sandbox_workspace_write.as_ref())
builtin_permission_profile(default_permissions, builtin_workspace_write_settings)
{
permission_profile
} else {
@@ -2106,6 +2137,7 @@ impl Config {
configured_network_proxy_config,
permission_profile,
file_system_sandbox_policy,
Some(ActivePermissionProfile::new(default_permissions)),
)
} else {
let configured_network_proxy_config = NetworkProxyConfig::default();
@@ -2167,6 +2199,7 @@ impl Config {
configured_network_proxy_config,
permission_profile,
file_system_sandbox_policy,
None,
)
};
let approval_policy_was_explicit = approval_policy_override.is_some()
@@ -2624,6 +2657,7 @@ impl Config {
permissions: Permissions {
approval_policy: constrained_approval_policy.value,
permission_profile: constrained_permission_profile.value,
active_permission_profile,
network,
allow_login_shell,
shell_environment_policy,
+2
View File
@@ -91,6 +91,7 @@ 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::ActivePermissionProfile;
use codex_protocol::models::AdditionalPermissionProfile;
use codex_protocol::models::BaseInstructions;
use codex_protocol::models::PermissionProfile;
@@ -602,6 +603,7 @@ impl Codex {
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(),
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
codex_home: config.codex_home.clone(),
+10
View File
@@ -60,6 +60,8 @@ pub(crate) struct SessionConfiguration {
pub(super) approvals_reviewer: ApprovalsReviewer,
/// Canonical permission profile for the session.
pub(super) permission_profile: Constrained<PermissionProfile>,
/// Named permissions profile selected from config, if any.
pub(super) active_permission_profile: Option<ActivePermissionProfile>,
pub(super) windows_sandbox_level: WindowsSandboxLevel,
/// Absolute working directory that should be treated as the *root* of the
@@ -97,6 +99,10 @@ impl SessionConfiguration {
self.permission_profile.get().clone()
}
pub(super) fn active_permission_profile(&self) -> Option<ActivePermissionProfile> {
self.active_permission_profile.clone()
}
pub(super) fn sandbox_policy(&self) -> SandboxPolicy {
self.permission_profile()
.to_legacy_sandbox_policy(&self.cwd)
@@ -127,6 +133,7 @@ impl SessionConfiguration {
approval_policy: self.approval_policy.value(),
approvals_reviewer: self.approvals_reviewer,
permission_profile: self.permission_profile(),
active_permission_profile: self.active_permission_profile(),
cwd: self.cwd.clone(),
ephemeral: self.original_config_do_not_use.ephemeral,
reasoning_effort: self.collaboration_mode.reasoning_effort(),
@@ -210,6 +217,7 @@ impl SessionConfiguration {
permission_profile,
Some(&current_file_system_sandbox_policy),
)?;
next_configuration.active_permission_profile = None;
} else if let Some(sandbox_policy) = updates.sandbox_policy.clone() {
let file_system_sandbox_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries(
@@ -225,6 +233,7 @@ impl SessionConfiguration {
network_sandbox_policy,
),
)?;
next_configuration.active_permission_profile = None;
} else if cwd_changed
&& file_system_policy_matches_legacy
&& file_system_policy_has_rebindable_project_root_write
@@ -887,6 +896,7 @@ impl Session {
approval_policy: session_configuration.approval_policy.value(),
approvals_reviewer: session_configuration.approvals_reviewer,
permission_profile: session_configuration.permission_profile(),
active_permission_profile: session_configuration.active_permission_profile(),
cwd: session_configuration.cwd.clone(),
reasoning_effort: session_configuration.collaboration_mode.reasoning_effort(),
history_log_id,
+7
View File
@@ -2323,6 +2323,7 @@ async fn set_rate_limits_retains_previous_credits() {
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(),
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
codex_home: config.codex_home.clone(),
@@ -2425,6 +2426,7 @@ async fn set_rate_limits_updates_plan_type_when_present() {
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(),
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
codex_home: config.codex_home.clone(),
@@ -2872,6 +2874,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(),
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
codex_home: config.codex_home.clone(),
@@ -3308,6 +3311,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(),
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
codex_home: config.codex_home.clone(),
@@ -3412,6 +3416,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(),
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
codex_home: config.codex_home.clone(),
@@ -3624,6 +3629,7 @@ async fn make_session_with_config_and_rx(
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(),
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
codex_home: config.codex_home.clone(),
@@ -4806,6 +4812,7 @@ where
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(),
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
codex_home: config.codex_home.clone(),
+46 -43
View File
@@ -82,7 +82,6 @@ use codex_protocol::protocol::ReviewRequest;
use codex_protocol::protocol::ReviewTarget;
use codex_protocol::protocol::RolloutItem;
use codex_protocol::protocol::RolloutLine;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionConfiguredEvent;
use codex_protocol::protocol::SessionSource;
use codex_protocol::user_input::UserInput;
@@ -660,37 +659,24 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
// Handle resume subcommand through existing `thread/list` + `thread/resume`
// APIs so exec no longer reaches into rollout storage directly.
let (primary_thread_id, fallback_session_configured) =
if let Some(ExecCommand::Resume(args)) = command.as_ref() {
if let Some(thread_id) = resolve_resume_thread_id(&client, &config, args).await? {
let response: ThreadResumeResponse = send_request_with_response(
&client,
ClientRequest::ThreadResume {
request_id: request_ids.next(),
params: thread_resume_params_from_config(&config, thread_id),
},
"thread/resume",
)
.await
.map_err(anyhow::Error::msg)?;
let session_configured = session_configured_from_thread_resume_response(&response)
let (primary_thread_id, fallback_session_configured) = if let Some(ExecCommand::Resume(args)) =
command.as_ref()
{
if let Some(thread_id) = resolve_resume_thread_id(&client, &config, args).await? {
let response: ThreadResumeResponse = send_request_with_response(
&client,
ClientRequest::ThreadResume {
request_id: request_ids.next(),
params: thread_resume_params_from_config(&config, thread_id),
},
"thread/resume",
)
.await
.map_err(anyhow::Error::msg)?;
let session_configured =
session_configured_from_thread_resume_response(&response, &config)
.map_err(anyhow::Error::msg)?;
(session_configured.session_id, session_configured)
} else {
let response: ThreadStartResponse = send_request_with_response(
&client,
ClientRequest::ThreadStart {
request_id: request_ids.next(),
params: thread_start_params_from_config(&config),
},
"thread/start",
)
.await
.map_err(anyhow::Error::msg)?;
let session_configured = session_configured_from_thread_start_response(&response)
.map_err(anyhow::Error::msg)?;
(session_configured.session_id, session_configured)
}
(session_configured.session_id, session_configured)
} else {
let response: ThreadStartResponse = send_request_with_response(
&client,
@@ -702,10 +688,26 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
)
.await
.map_err(anyhow::Error::msg)?;
let session_configured = session_configured_from_thread_start_response(&response)
.map_err(anyhow::Error::msg)?;
let session_configured =
session_configured_from_thread_start_response(&response, &config)
.map_err(anyhow::Error::msg)?;
(session_configured.session_id, session_configured)
};
}
} else {
let response: ThreadStartResponse = send_request_with_response(
&client,
ClientRequest::ThreadStart {
request_id: request_ids.next(),
params: thread_start_params_from_config(&config),
},
"thread/start",
)
.await
.map_err(anyhow::Error::msg)?;
let session_configured = session_configured_from_thread_start_response(&response, &config)
.map_err(anyhow::Error::msg)?;
(session_configured.session_id, session_configured)
};
let primary_thread_id_for_span = primary_thread_id.to_string();
// Use the start/resume response as the authoritative bootstrap payload.
@@ -972,6 +974,7 @@ where
fn session_configured_from_thread_start_response(
response: &ThreadStartResponse,
config: &Config,
) -> Result<SessionConfiguredEvent, String> {
session_configured_from_thread_response(
&response.thread.id,
@@ -982,8 +985,8 @@ fn session_configured_from_thread_start_response(
response.service_tier,
response.approval_policy.to_core(),
response.approvals_reviewer.to_core(),
response.sandbox.to_core(),
response.permission_profile.clone().map(Into::into),
config.permissions.permission_profile(),
response.active_permission_profile.clone().map(Into::into),
response.cwd.clone(),
response.reasoning_effort,
)
@@ -991,6 +994,7 @@ fn session_configured_from_thread_start_response(
fn session_configured_from_thread_resume_response(
response: &ThreadResumeResponse,
config: &Config,
) -> Result<SessionConfiguredEvent, String> {
session_configured_from_thread_response(
&response.thread.id,
@@ -1001,8 +1005,8 @@ fn session_configured_from_thread_resume_response(
response.service_tier,
response.approval_policy.to_core(),
response.approvals_reviewer.to_core(),
response.sandbox.to_core(),
response.permission_profile.clone().map(Into::into),
config.permissions.permission_profile(),
response.active_permission_profile.clone().map(Into::into),
response.cwd.clone(),
response.reasoning_effort,
)
@@ -1030,8 +1034,8 @@ fn session_configured_from_thread_response(
service_tier: Option<codex_protocol::config_types::ServiceTier>,
approval_policy: AskForApproval,
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer,
sandbox_policy: SandboxPolicy,
permission_profile: Option<PermissionProfile>,
permission_profile: PermissionProfile,
active_permission_profile: Option<codex_protocol::models::ActivePermissionProfile>,
cwd: AbsolutePathBuf,
reasoning_effort: Option<codex_protocol::openai_models::ReasoningEffort>,
) -> Result<SessionConfiguredEvent, String> {
@@ -1047,9 +1051,8 @@ fn session_configured_from_thread_response(
service_tier,
approval_policy,
approvals_reviewer,
permission_profile: permission_profile.unwrap_or_else(|| {
PermissionProfile::from_legacy_sandbox_policy_for_cwd(&sandbox_policy, cwd.as_path())
}),
permission_profile,
active_permission_profile,
cwd,
reasoning_effort,
history_log_id: 0,
+12 -9
View File
@@ -391,8 +391,16 @@ async fn thread_start_params_include_review_policy_when_auto_review_is_enabled()
);
}
#[test]
fn session_configured_from_thread_response_uses_review_policy_from_response() {
#[tokio::test]
async fn session_configured_from_thread_response_uses_review_policy_from_response() {
let codex_home = tempdir().expect("create temp codex home");
let cwd = tempdir().expect("create temp cwd");
let config = ConfigBuilder::default()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(cwd.path().to_path_buf()))
.build()
.await
.expect("build config");
let response = ThreadStartResponse {
thread: codex_app_server_protocol::Thread {
id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(),
@@ -426,16 +434,11 @@ fn session_configured_from_thread_response_uses_review_policy_from_response() {
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
},
permission_profile: Some(
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
&codex_protocol::protocol::SandboxPolicy::new_workspace_write_policy(),
)
.into(),
),
active_permission_profile: None,
reasoning_effort: None,
};
let event = session_configured_from_thread_start_response(&response)
let event = session_configured_from_thread_start_response(&response, &config)
.expect("build bootstrap session configured event");
assert_eq!(event.approvals_reviewer, ApprovalsReviewer::AutoReview);
@@ -115,6 +115,7 @@ fn session_configured_produces_thread_started_event() {
approval_policy: AskForApproval::Never,
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/tmp/project").abs(),
reasoning_effort: None,
history_log_id: 0,
@@ -305,6 +305,7 @@ mod tests {
approval_policy: AskForApproval::Never,
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffort::default()),
history_log_id: 1,
@@ -349,6 +350,7 @@ mod tests {
approval_policy: AskForApproval::Never,
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffort::default()),
history_log_id: 1,
@@ -416,6 +418,7 @@ mod tests {
approval_policy: AskForApproval::Never,
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffort::default()),
history_log_id: 1,
+28
View File
@@ -317,6 +317,34 @@ pub enum PermissionProfile {
External { network: NetworkSandboxPolicy },
}
/// Metadata for the named permissions profile that produced the active
/// `PermissionProfile`.
///
/// The runtime must honor `PermissionProfile`; this sidecar exists so clients
/// can display stable profile identity without trying to reverse-engineer a
/// name from the compiled permissions.
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
pub struct ActivePermissionProfile {
/// Profile identifier from `default_permissions`, such as `:workspace` or a
/// user-defined `[permissions.<id>]` profile.
pub id: String,
/// Optional parent profile identifier once permissions profiles support
/// inheritance. This is always `None` until that config feature exists.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub extends: Option<String>,
}
impl ActivePermissionProfile {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
extends: None,
}
}
}
impl Default for PermissionProfile {
fn default() -> Self {
Self::Managed {
+11
View File
@@ -33,6 +33,7 @@ use crate::mcp::ResourceTemplate as McpResourceTemplate;
use crate::mcp::Tool as McpTool;
use crate::memory_citation::MemoryCitation;
use crate::message_history::HistoryEntry;
use crate::models::ActivePermissionProfile;
use crate::models::BaseInstructions;
use crate::models::ContentItem;
use crate::models::MessagePhase;
@@ -3508,6 +3509,12 @@ pub struct SessionConfiguredEvent {
/// Canonical effective permissions for commands executed in the session.
pub permission_profile: PermissionProfile,
/// Named profile that produced `permission_profile`, when the active
/// permissions came from `default_permissions`.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub active_permission_profile: Option<ActivePermissionProfile>,
/// Working directory that should be treated as the *root* of the
/// session.
pub cwd: AbsolutePathBuf,
@@ -3559,6 +3566,8 @@ impl<'de> Deserialize<'de> for SessionConfiguredEvent {
// and immediately project it into the canonical `permission_profile`.
sandbox_policy: Option<SandboxPolicy>,
permission_profile: Option<PermissionProfile>,
#[serde(default)]
active_permission_profile: Option<ActivePermissionProfile>,
cwd: AbsolutePathBuf,
reasoning_effort: Option<ReasoningEffortConfig>,
history_log_id: u64,
@@ -3590,6 +3599,7 @@ impl<'de> Deserialize<'de> for SessionConfiguredEvent {
approval_policy: wire.approval_policy,
approvals_reviewer: wire.approvals_reviewer,
permission_profile,
active_permission_profile: wire.active_permission_profile,
cwd: wire.cwd,
reasoning_effort: wire.reasoning_effort,
history_log_id: wire.history_log_id,
@@ -5102,6 +5112,7 @@ mod tests {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: permission_profile.clone(),
active_permission_profile: None,
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -648,6 +648,7 @@ mod tests {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: next_cwd.clone().abs(),
reasoning_effort: None,
history_log_id: 0,
+7
View File
@@ -3570,6 +3570,7 @@ async fn render_clear_ui_header_after_long_transcript_for_snapshot() -> String {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/tmp/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::High),
history_log_id: 0,
@@ -3809,6 +3810,7 @@ fn test_thread_session(thread_id: ThreadId, cwd: PathBuf) -> ThreadSessionState
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: cwd.abs(),
instruction_source_paths: Vec::new(),
reasoning_effort: None,
@@ -4321,6 +4323,7 @@ async fn backtrack_selection_with_duplicate_history_targets_unique_turn() {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: None,
history_log_id: 0,
@@ -4384,6 +4387,7 @@ async fn backtrack_selection_with_duplicate_history_targets_unique_turn() {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: None,
history_log_id: 0,
@@ -4477,6 +4481,7 @@ async fn backtrack_resubmit_preserves_data_image_urls_in_user_turn() {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: None,
history_log_id: 0,
@@ -4863,6 +4868,7 @@ async fn new_session_requests_shutdown_for_previous_conversation() {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: None,
history_log_id: 0,
@@ -4975,6 +4981,7 @@ async fn clear_only_ui_reset_preserves_chat_session_state() {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/tmp/project").abs(),
reasoning_effort: None,
history_log_id: 0,
+1
View File
@@ -350,6 +350,7 @@ mod tests {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: cwd.abs(),
instruction_source_paths: Vec::new(),
reasoning_effort: None,
+20 -2
View File
@@ -3,6 +3,7 @@ use crate::app_server_session::ThreadSessionState;
use crate::read_session_model;
use codex_app_server_protocol::Thread;
use codex_protocol::ThreadId;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::PermissionProfile;
impl App {
@@ -18,10 +19,16 @@ impl App {
.config_ref()
.permissions
.permission_profile();
let active_permission_profile = self
.chat_widget
.config_ref()
.permissions
.active_permission_profile();
let update_session = |session: &mut ThreadSessionState| {
session.approval_policy = approval_policy;
session.approvals_reviewer = approvals_reviewer;
session.permission_profile = permission_profile.clone();
session.active_permission_profile = active_permission_profile.clone();
};
if self.primary_thread_id == Some(active_thread_id)
@@ -43,7 +50,8 @@ impl App {
thread_id: ThreadId,
thread: &Thread,
) -> ThreadSessionState {
let permission_profile = self.active_permission_profile();
let permission_profile = self.current_permission_profile();
let active_permission_profile = self.current_active_permission_profile();
let mut session = self
.primary_session_configured
.clone()
@@ -58,6 +66,7 @@ impl App {
approval_policy: self.config.permissions.approval_policy.value(),
approvals_reviewer: self.config.approvals_reviewer,
permission_profile: permission_profile.clone(),
active_permission_profile: active_permission_profile.clone(),
cwd: thread.cwd.clone(),
instruction_source_paths: Vec::new(),
reasoning_effort: self.chat_widget.current_reasoning_effort(),
@@ -71,6 +80,7 @@ impl App {
session.model_provider_id = thread.model_provider.clone();
session.cwd = thread.cwd.clone();
session.permission_profile = permission_profile;
session.active_permission_profile = active_permission_profile;
session.instruction_source_paths = Vec::new();
session.rollout_path = thread.path.clone();
if let Some(model) =
@@ -85,12 +95,19 @@ impl App {
session
}
fn active_permission_profile(&self) -> PermissionProfile {
fn current_permission_profile(&self) -> PermissionProfile {
self.chat_widget
.config_ref()
.permissions
.permission_profile()
}
fn current_active_permission_profile(&self) -> Option<ActivePermissionProfile> {
self.chat_widget
.config_ref()
.permissions
.active_permission_profile()
}
}
#[cfg(test)]
@@ -125,6 +142,7 @@ mod tests {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: cwd.abs(),
instruction_source_paths: Vec::new(),
reasoning_effort: None,
+89 -54
View File
@@ -93,6 +93,7 @@ use codex_app_server_protocol::TurnSteerParams;
use codex_app_server_protocol::TurnSteerResponse;
use codex_otel::TelemetryAuthMode;
use codex_protocol::ThreadId;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ModelAvailabilityNux;
@@ -156,10 +157,17 @@ pub(crate) struct ThreadSessionState {
pub(crate) service_tier: Option<codex_protocol::config_types::ServiceTier>,
pub(crate) approval_policy: AskForApproval,
pub(crate) approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer,
/// Canonical active permissions for this session. Legacy app-server
/// responses are converted to a profile at ingestion time using the
/// response cwd so cached sessions do not reinterpret cwd-bound grants.
/// Canonical active permissions for this session.
///
/// Embedded app-server responses intentionally do not echo the
/// experimental profile payload, so the TUI keeps its local high-fidelity
/// profile. Remote responses are converted from the legacy sandbox field
/// using the response cwd so cached sessions do not reinterpret cwd-bound
/// grants.
pub(crate) permission_profile: PermissionProfile,
/// Named profile that produced `permission_profile`, when the server knows
/// the permissions came from `default_permissions`.
pub(crate) active_permission_profile: Option<ActivePermissionProfile>,
pub(crate) cwd: AbsolutePathBuf,
pub(crate) instruction_source_paths: Vec<AbsolutePathBuf>,
pub(crate) reasoning_effort: Option<codex_protocol::openai_models::ReasoningEffort>,
@@ -365,7 +373,7 @@ impl AppServerSession {
})
.await
.wrap_err("thread/start failed during TUI bootstrap")?;
started_thread_from_start_response(response, config).await
started_thread_from_start_response(response, config, self.thread_params_mode()).await
}
pub(crate) async fn resume_thread(
@@ -390,7 +398,9 @@ impl AppServerSession {
let fork_parent_title = self
.fork_parent_title_from_app_server(response.thread.forked_from_id.as_deref())
.await;
let mut started = started_thread_from_resume_response(response, &config).await?;
let mut started =
started_thread_from_resume_response(response, &config, self.thread_params_mode())
.await?;
started.session.fork_parent_title = fork_parent_title;
Ok(started)
}
@@ -417,7 +427,8 @@ impl AppServerSession {
let fork_parent_title = self
.fork_parent_title_from_app_server(response.thread.forked_from_id.as_deref())
.await;
let mut started = started_thread_from_fork_response(response, &config).await?;
let mut started =
started_thread_from_fork_response(response, &config, self.thread_params_mode()).await?;
started.session.fork_parent_title = fork_parent_title;
Ok(started)
}
@@ -1251,10 +1262,12 @@ fn thread_cwd_from_config(
async fn started_thread_from_start_response(
response: ThreadStartResponse,
config: &Config,
thread_params_mode: ThreadParamsMode,
) -> Result<AppServerStartedThread> {
let session = thread_session_state_from_thread_start_response(&response, config)
.await
.map_err(color_eyre::eyre::Report::msg)?;
let session =
thread_session_state_from_thread_start_response(&response, config, thread_params_mode)
.await
.map_err(color_eyre::eyre::Report::msg)?;
Ok(AppServerStartedThread {
session,
turns: response.thread.turns,
@@ -1264,10 +1277,12 @@ async fn started_thread_from_start_response(
async fn started_thread_from_resume_response(
response: ThreadResumeResponse,
config: &Config,
thread_params_mode: ThreadParamsMode,
) -> Result<AppServerStartedThread> {
let session = thread_session_state_from_thread_resume_response(&response, config)
.await
.map_err(color_eyre::eyre::Report::msg)?;
let session =
thread_session_state_from_thread_resume_response(&response, config, thread_params_mode)
.await
.map_err(color_eyre::eyre::Report::msg)?;
Ok(AppServerStartedThread {
session,
turns: response.thread.turns,
@@ -1277,10 +1292,12 @@ async fn started_thread_from_resume_response(
async fn started_thread_from_fork_response(
response: ThreadForkResponse,
config: &Config,
thread_params_mode: ThreadParamsMode,
) -> Result<AppServerStartedThread> {
let session = thread_session_state_from_thread_fork_response(&response, config)
.await
.map_err(color_eyre::eyre::Report::msg)?;
let session =
thread_session_state_from_thread_fork_response(&response, config, thread_params_mode)
.await
.map_err(color_eyre::eyre::Report::msg)?;
Ok(AppServerStartedThread {
session,
turns: response.thread.turns,
@@ -1290,7 +1307,14 @@ async fn started_thread_from_fork_response(
async fn thread_session_state_from_thread_start_response(
response: &ThreadStartResponse,
config: &Config,
thread_params_mode: ThreadParamsMode,
) -> Result<ThreadSessionState, String> {
let permission_profile = permission_profile_from_thread_response(
&response.sandbox,
response.cwd.as_path(),
config,
thread_params_mode,
);
thread_session_state_from_thread_response(
&response.thread.id,
response.thread.forked_from_id.clone(),
@@ -1301,16 +1325,8 @@ async fn thread_session_state_from_thread_start_response(
response.service_tier,
response.approval_policy.to_core(),
response.approvals_reviewer.to_core(),
response
.permission_profile
.clone()
.map(Into::into)
.unwrap_or_else(|| {
PermissionProfile::from_legacy_sandbox_policy_for_cwd(
&response.sandbox.to_core(),
response.cwd.as_path(),
)
}),
permission_profile,
response.active_permission_profile.clone().map(Into::into),
response.cwd.clone(),
response.instruction_sources.clone(),
response.reasoning_effort,
@@ -1322,7 +1338,14 @@ async fn thread_session_state_from_thread_start_response(
async fn thread_session_state_from_thread_resume_response(
response: &ThreadResumeResponse,
config: &Config,
thread_params_mode: ThreadParamsMode,
) -> Result<ThreadSessionState, String> {
let permission_profile = permission_profile_from_thread_response(
&response.sandbox,
response.cwd.as_path(),
config,
thread_params_mode,
);
thread_session_state_from_thread_response(
&response.thread.id,
response.thread.forked_from_id.clone(),
@@ -1333,16 +1356,8 @@ async fn thread_session_state_from_thread_resume_response(
response.service_tier,
response.approval_policy.to_core(),
response.approvals_reviewer.to_core(),
response
.permission_profile
.clone()
.map(Into::into)
.unwrap_or_else(|| {
PermissionProfile::from_legacy_sandbox_policy_for_cwd(
&response.sandbox.to_core(),
response.cwd.as_path(),
)
}),
permission_profile,
response.active_permission_profile.clone().map(Into::into),
response.cwd.clone(),
response.instruction_sources.clone(),
response.reasoning_effort,
@@ -1354,7 +1369,14 @@ async fn thread_session_state_from_thread_resume_response(
async fn thread_session_state_from_thread_fork_response(
response: &ThreadForkResponse,
config: &Config,
thread_params_mode: ThreadParamsMode,
) -> Result<ThreadSessionState, String> {
let permission_profile = permission_profile_from_thread_response(
&response.sandbox,
response.cwd.as_path(),
config,
thread_params_mode,
);
thread_session_state_from_thread_response(
&response.thread.id,
response.thread.forked_from_id.clone(),
@@ -1365,16 +1387,8 @@ async fn thread_session_state_from_thread_fork_response(
response.service_tier,
response.approval_policy.to_core(),
response.approvals_reviewer.to_core(),
response
.permission_profile
.clone()
.map(Into::into)
.unwrap_or_else(|| {
PermissionProfile::from_legacy_sandbox_policy_for_cwd(
&response.sandbox.to_core(),
response.cwd.as_path(),
)
}),
permission_profile,
response.active_permission_profile.clone().map(Into::into),
response.cwd.clone(),
response.instruction_sources.clone(),
response.reasoning_effort,
@@ -1383,6 +1397,20 @@ async fn thread_session_state_from_thread_fork_response(
.await
}
fn permission_profile_from_thread_response(
sandbox: &codex_app_server_protocol::SandboxPolicy,
cwd: &std::path::Path,
config: &Config,
thread_params_mode: ThreadParamsMode,
) -> PermissionProfile {
match thread_params_mode {
ThreadParamsMode::Embedded => config.permissions.permission_profile(),
ThreadParamsMode::Remote => {
PermissionProfile::from_legacy_sandbox_policy_for_cwd(&sandbox.to_core(), cwd)
}
}
}
fn review_target_to_app_server(
target: CoreReviewTarget,
) -> codex_app_server_protocol::ReviewTarget {
@@ -1417,6 +1445,7 @@ async fn thread_session_state_from_thread_response(
approval_policy: AskForApproval,
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer,
permission_profile: PermissionProfile,
active_permission_profile: Option<ActivePermissionProfile>,
cwd: AbsolutePathBuf,
instruction_source_paths: Vec<AbsolutePathBuf>,
reasoning_effort: Option<codex_protocol::openai_models::ReasoningEffort>,
@@ -1442,6 +1471,7 @@ async fn thread_session_state_from_thread_response(
approval_policy,
approvals_reviewer,
permission_profile,
active_permission_profile,
cwd,
instruction_source_paths,
reasoning_effort,
@@ -1791,13 +1821,17 @@ mod tests {
.to_legacy_sandbox_policy(test_path_buf("/tmp/project").as_path())
.expect("read-only profile must be legacy-compatible")
.into(),
permission_profile: Some(read_only_profile.into()),
active_permission_profile: None,
reasoning_effort: None,
};
let started = started_thread_from_resume_response(response.clone(), &config)
.await
.expect("resume response should map");
let started = started_thread_from_resume_response(
response.clone(),
&config,
ThreadParamsMode::Remote,
)
.await
.expect("resume response should map");
assert_eq!(started.session.forked_from_id, Some(forked_from_id));
assert_eq!(
started.session.instruction_source_paths,
@@ -1805,11 +1839,10 @@ mod tests {
);
assert_eq!(
started.session.permission_profile,
response
.permission_profile
.clone()
.map(Into::into)
.expect("response includes profile")
PermissionProfile::from_legacy_sandbox_policy_for_cwd(
&response.sandbox.to_core(),
response.cwd.as_path(),
)
);
assert_eq!(started.turns.len(), 1);
assert_eq!(started.turns[0], response.thread.turns[0]);
@@ -1839,6 +1872,7 @@ mod tests {
AskForApproval::Never,
codex_protocol::config_types::ApprovalsReviewer::User,
PermissionProfile::read_only(),
/*active_permission_profile*/ None,
test_path_buf("/tmp/project").abs(),
Vec::new(),
/*reasoning_effort*/ None,
@@ -1869,6 +1903,7 @@ mod tests {
AskForApproval::Never,
codex_protocol::config_types::ApprovalsReviewer::User,
PermissionProfile::read_only(),
/*active_permission_profile*/ None,
test_path_buf("/tmp/project").abs(),
Vec::new(),
/*reasoning_effort*/ None,
+7 -1
View File
@@ -1651,6 +1651,7 @@ fn thread_session_state_to_legacy_event(
approval_policy: session.approval_policy,
approvals_reviewer: session.approvals_reviewer,
permission_profile: session.permission_profile,
active_permission_profile: session.active_permission_profile,
cwd: session.cwd,
reasoning_effort: session.reasoning_effort,
history_log_id: session.history_log_id,
@@ -2383,11 +2384,16 @@ impl ChatWidget {
let permission_sync = self
.config
.permissions
.set_permission_profile(event.permission_profile.clone());
.set_permission_profile_with_active_profile(
event.permission_profile.clone(),
event.active_permission_profile.clone(),
);
if let Err(err) = permission_sync {
tracing::warn!(%err, "failed to sync permissions from SessionConfigured");
self.config.permissions.permission_profile =
Constrained::allow_only(event.permission_profile.clone());
self.config.permissions.active_permission_profile =
event.active_permission_profile.clone();
}
self.config.approvals_reviewer = event.approvals_reviewer;
self.status_line_project_root_name_cache = None;
@@ -17,6 +17,7 @@ async fn submission_preserves_text_elements_and_local_images() {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -121,6 +122,7 @@ async fn submission_includes_configured_permission_profile() {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: expected_permission_profile.clone(),
active_permission_profile: None,
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -171,6 +173,7 @@ async fn submission_keeps_profile_when_legacy_projection_is_external() {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: expected_permission_profile.clone(),
active_permission_profile: None,
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -214,6 +217,7 @@ async fn submission_with_remote_and_local_images_keeps_local_placeholder_numberi
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -309,6 +313,7 @@ async fn enter_with_only_remote_images_submits_user_turn() {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -374,6 +379,7 @@ async fn shift_enter_with_only_remote_images_does_not_submit_user_turn() {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -414,6 +420,7 @@ async fn enter_with_only_remote_images_does_not_submit_when_modal_is_active() {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -454,6 +461,7 @@ async fn enter_with_only_remote_images_does_not_submit_when_input_disabled() {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -497,6 +505,7 @@ async fn submission_prefers_selected_duplicate_skill_path() {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -1059,6 +1059,7 @@ async fn bang_shell_enter_while_task_running_submits_run_user_shell_command() {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -24,6 +24,7 @@ async fn resumed_initial_messages_render_history() {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -137,6 +138,7 @@ async fn replayed_user_message_preserves_text_elements_and_local_images() {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -198,6 +200,7 @@ async fn replayed_user_message_preserves_remote_image_urls() {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -286,6 +289,7 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: expected_permission_profile.clone(),
active_permission_profile: None,
cwd: expected_cwd.clone(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -344,6 +348,7 @@ async fn session_configured_external_sandbox_keeps_external_runtime_policy() {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: expected_permission_profile,
active_permission_profile: None,
cwd: test_path_buf("/home/user/external").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -393,6 +398,7 @@ async fn replayed_user_message_with_only_remote_images_renders_history_cell() {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -446,6 +452,7 @@ async fn replayed_user_message_with_only_local_images_does_not_render_history_ce
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -767,6 +774,7 @@ async fn replayed_reasoning_item_hides_raw_reasoning_when_disabled() {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_project_path().abs(),
reasoning_effort: None,
history_log_id: 0,
@@ -814,6 +822,7 @@ async fn replayed_reasoning_item_shows_raw_reasoning_when_enabled() {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_project_path().abs(),
reasoning_effort: None,
history_log_id: 0,
@@ -542,6 +542,7 @@ async fn permissions_selection_marks_auto_review_current_after_session_configure
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::AutoReview,
permission_profile: PermissionProfile::workspace_write(),
active_permission_profile: None,
cwd: test_project_path().abs(),
reasoning_effort: None,
history_log_id: 0,
@@ -596,6 +597,7 @@ async fn permissions_selection_marks_auto_review_current_with_custom_workspace_w
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::AutoReview,
permission_profile,
active_permission_profile: None,
cwd,
reasoning_effort: None,
history_log_id: 0,
@@ -1196,6 +1196,7 @@ async fn submit_user_message_emits_structured_plugin_mentions_from_bindings() {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -1441,6 +1442,7 @@ async fn plan_slash_command_with_args_submits_prompt_in_plan_mode() {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -1791,6 +1791,7 @@ async fn session_configured_clears_goal_status_footer() {
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
+1
View File
@@ -3194,6 +3194,7 @@ mod tests {
approval_policy: AskForApproval::Never,
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User,
permission_profile: PermissionProfile::read_only(),
active_permission_profile: None,
cwd: test_path_buf("/tmp/project").abs(),
reasoning_effort: None,
history_log_id: 0,
+47 -11
View File
@@ -9,6 +9,7 @@ use chrono::Local;
use codex_model_provider_info::WireApi;
use codex_protocol::ThreadId;
use codex_protocol::account::PlanType;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AskForApproval;
@@ -281,18 +282,15 @@ impl StatusHistoryCell {
.map(|(_, v)| v.clone())
.unwrap_or_else(|| "<unknown>".to_string());
let permission_profile = config.permissions.permission_profile();
let active_permission_profile = config.permissions.active_permission_profile();
let sandbox = status_permission_summary(&permission_profile, config.cwd.as_path());
let permissions = if config.permissions.approval_policy.value() == AskForApproval::OnRequest
&& permission_profile == PermissionProfile::workspace_write()
{
"Default".to_string()
} else if config.permissions.approval_policy.value() == AskForApproval::Never
&& permission_profile == PermissionProfile::Disabled
{
"Full Access".to_string()
} else {
format!("Custom ({sandbox}, {approval})")
};
let permissions = status_permissions_label(
active_permission_profile.as_ref(),
&permission_profile,
config.permissions.approval_policy.value(),
&sandbox,
&approval,
);
let model_provider = format_model_provider(config);
let account = compose_account_display(account_display);
let session_id = session_id.as_ref().map(std::string::ToString::to_string);
@@ -549,6 +547,44 @@ fn status_permission_summary(permission_profile: &PermissionProfile, cwd: &Path)
summary
}
fn status_permissions_label(
active_permission_profile: Option<&ActivePermissionProfile>,
permission_profile: &PermissionProfile,
approval_policy: AskForApproval,
sandbox: &str,
approval: &str,
) -> String {
if active_permission_profile.is_some_and(|active| active.id == ":workspace")
&& approval_policy == AskForApproval::OnRequest
&& sandbox == "workspace-write"
{
return "Default".to_string();
}
if active_permission_profile.is_some_and(|active| active.id == ":danger-no-sandbox")
&& approval_policy == AskForApproval::Never
&& permission_profile == &PermissionProfile::Disabled
{
return "Full Access".to_string();
}
if let Some(active) = active_permission_profile
&& active.id != ":workspace"
&& active.id != ":danger-no-sandbox"
{
return format!("Profile {} ({sandbox}, {approval})", active.id);
}
if approval_policy == AskForApproval::OnRequest
&& permission_profile == &PermissionProfile::workspace_write()
{
return "Default".to_string();
}
if approval_policy == AskForApproval::Never
&& permission_profile == &PermissionProfile::Disabled
{
return "Full Access".to_string();
}
format!("Custom ({sandbox}, {approval})")
}
impl HistoryCell for StatusHistoryCell {
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
let mut lines: Vec<Line<'static>> = Vec::new();
+67
View File
@@ -13,6 +13,7 @@ use chrono::TimeZone;
use chrono::Utc;
use codex_protocol::ThreadId;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::ManagedFileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ReasoningEffort;
@@ -239,6 +240,72 @@ async fn status_permissions_non_default_workspace_write_is_custom() {
);
}
#[tokio::test]
async fn status_permissions_named_workspace_profile_is_default() {
let temp_home = TempDir::new().expect("temp home");
let mut config = test_config(&temp_home).await;
config
.permissions
.approval_policy
.set(AskForApproval::OnRequest)
.expect("set approval policy");
config
.permissions
.set_permission_profile_with_active_profile(
PermissionProfile::workspace_write(),
Some(ActivePermissionProfile::new(":workspace")),
)
.expect("set permission profile");
assert_eq!(permissions_text_for(&config).as_deref(), Some("Default"));
}
#[tokio::test]
async fn status_permissions_broadened_workspace_profile_shows_name() {
let temp_home = TempDir::new().expect("temp home");
let mut config = test_config(&temp_home).await;
config
.permissions
.approval_policy
.set(AskForApproval::OnRequest)
.expect("set approval policy");
config
.permissions
.set_permission_profile_with_active_profile(
PermissionProfile::workspace_write_with(
&[],
NetworkSandboxPolicy::Enabled,
/*exclude_tmpdir_env_var*/ false,
/*exclude_slash_tmp*/ false,
),
Some(ActivePermissionProfile::new(":workspace")),
)
.expect("set permission profile");
assert_eq!(
permissions_text_for(&config).as_deref(),
Some("Custom (workspace-write with network access, on-request)")
);
}
#[tokio::test]
async fn status_permissions_user_defined_profile_shows_name() {
let temp_home = TempDir::new().expect("temp home");
let mut config = test_config(&temp_home).await;
config
.permissions
.set_permission_profile_with_active_profile(
PermissionProfile::read_only(),
Some(ActivePermissionProfile::new("locked")),
)
.expect("set permission profile");
assert_eq!(
permissions_text_for(&config).as_deref(),
Some("Profile locked (read-only, on-request)")
);
}
#[tokio::test]
async fn status_permissions_full_disk_managed_with_network_is_danger_full_access() {
let temp_home = TempDir::new().expect("temp home");