permissions: expose active profile metadata (#20095)

This commit is contained in:
Michael Bolin
2026-04-29 20:54:59 -07:00
committed by GitHub
parent ebe602d005
commit ac4332c05b
71 changed files with 2480 additions and 1657 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;
@@ -158,15 +157,12 @@ fn sample_thread_start_response(
approval_policy: AppServerAskForApproval::OnFailure,
approvals_reviewer: AppServerApprovalsReviewer::User,
sandbox: AppServerSandboxPolicy::DangerFullAccess,
permission_profile: Some(sample_permission_profile()),
permission_profile: None,
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(),
@@ -215,7 +211,8 @@ fn sample_thread_resume_response_with_source(
approval_policy: AppServerAskForApproval::OnFailure,
approvals_reviewer: AppServerApprovalsReviewer::User,
sandbox: AppServerSandboxPolicy::DangerFullAccess,
permission_profile: Some(sample_permission_profile()),
permission_profile: None,
active_permission_profile: None,
reasoning_effort: None,
})
}
+3
View File
@@ -111,6 +111,7 @@ fn sample_thread_start_response() -> ClientResponsePayload {
approvals_reviewer: AppServerApprovalsReviewer::User,
sandbox: AppServerSandboxPolicy::DangerFullAccess,
permission_profile: Some(sample_permission_profile()),
active_permission_profile: None,
reasoning_effort: None,
})
}
@@ -127,6 +128,7 @@ fn sample_thread_resume_response() -> ClientResponsePayload {
approvals_reviewer: AppServerApprovalsReviewer::User,
sandbox: AppServerSandboxPolicy::DangerFullAccess,
permission_profile: Some(sample_permission_profile()),
active_permission_profile: None,
reasoning_effort: None,
})
}
@@ -143,6 +145,7 @@ fn sample_thread_fork_response() -> ClientResponsePayload {
approvals_reviewer: AppServerApprovalsReviewer::User,
sandbox: AppServerSandboxPolicy::DangerFullAccess,
permission_profile: Some(sample_permission_profile()),
active_permission_profile: None,
reasoning_effort: None,
})
}
@@ -2039,6 +2039,31 @@
}
]
},
"PermissionProfileModificationParams": {
"oneOf": [
{
"description": "Additional concrete directory that should be writable.",
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"additionalWritableRoot"
],
"title": "AdditionalWritableRootPermissionProfileModificationParamsType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "AdditionalWritableRootPermissionProfileModificationParams",
"type": "object"
}
]
},
"PermissionProfileNetworkPermissions": {
"properties": {
"enabled": {
@@ -2050,6 +2075,40 @@
],
"type": "object"
},
"PermissionProfileSelectionParams": {
"oneOf": [
{
"description": "Select a named built-in or user-defined profile and optionally apply bounded modifications that Codex knows how to validate.",
"properties": {
"id": {
"type": "string"
},
"modifications": {
"items": {
"$ref": "#/definitions/PermissionProfileModificationParams"
},
"type": [
"array",
"null"
]
},
"type": {
"enum": [
"profile"
],
"title": "ProfilePermissionProfileSelectionParamsType",
"type": "string"
}
},
"required": [
"id",
"type"
],
"title": "ProfilePermissionProfileSelectionParams",
"type": "object"
}
]
},
"Personality": {
"enum": [
"none",
@@ -5413,6 +5413,59 @@
"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` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.<id>]` profile.",
"type": "string"
},
"modifications": {
"default": [],
"description": "Bounded user-requested modifications applied on top of the named profile, if any.",
"items": {
"$ref": "#/definitions/v2/ActivePermissionProfileModification"
},
"type": "array"
}
},
"required": [
"id"
],
"type": "object"
},
"ActivePermissionProfileModification": {
"oneOf": [
{
"description": "Additional concrete directory that should be writable.",
"properties": {
"path": {
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
"type": {
"enum": [
"additionalWritableRoot"
],
"title": "AdditionalWritableRootActivePermissionProfileModificationType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "AdditionalWritableRootActivePermissionProfileModification",
"type": "object"
}
]
},
"AddCreditsNudgeCreditType": {
"enum": [
"credits",
@@ -11688,6 +11741,31 @@
}
]
},
"PermissionProfileModificationParams": {
"oneOf": [
{
"description": "Additional concrete directory that should be writable.",
"properties": {
"path": {
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
"type": {
"enum": [
"additionalWritableRoot"
],
"title": "AdditionalWritableRootPermissionProfileModificationParamsType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "AdditionalWritableRootPermissionProfileModificationParams",
"type": "object"
}
]
},
"PermissionProfileNetworkPermissions": {
"properties": {
"enabled": {
@@ -11699,6 +11777,40 @@
],
"type": "object"
},
"PermissionProfileSelectionParams": {
"oneOf": [
{
"description": "Select a named built-in or user-defined profile and optionally apply bounded modifications that Codex knows how to validate.",
"properties": {
"id": {
"type": "string"
},
"modifications": {
"items": {
"$ref": "#/definitions/v2/PermissionProfileModificationParams"
},
"type": [
"array",
"null"
]
},
"type": {
"enum": [
"profile"
],
"title": "ProfilePermissionProfileSelectionParamsType",
"type": "string"
}
},
"required": [
"id",
"type"
],
"title": "ProfilePermissionProfileSelectionParams",
"type": "object"
}
]
},
"Personality": {
"enum": [
"none",
@@ -14880,7 +14992,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. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
},
"serviceTier": {
"anyOf": [
@@ -16391,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. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
},
"serviceTier": {
"anyOf": [
@@ -16695,7 +16807,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. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
},
"serviceTier": {
"anyOf": [
@@ -130,6 +130,59 @@
"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` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.<id>]` profile.",
"type": "string"
},
"modifications": {
"default": [],
"description": "Bounded user-requested modifications applied on top of the named profile, if any.",
"items": {
"$ref": "#/definitions/ActivePermissionProfileModification"
},
"type": "array"
}
},
"required": [
"id"
],
"type": "object"
},
"ActivePermissionProfileModification": {
"oneOf": [
{
"description": "Additional concrete directory that should be writable.",
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"additionalWritableRoot"
],
"title": "AdditionalWritableRootActivePermissionProfileModificationType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "AdditionalWritableRootActivePermissionProfileModification",
"type": "object"
}
]
},
"AddCreditsNudgeCreditType": {
"enum": [
"credits",
@@ -8342,6 +8395,31 @@
}
]
},
"PermissionProfileModificationParams": {
"oneOf": [
{
"description": "Additional concrete directory that should be writable.",
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"additionalWritableRoot"
],
"title": "AdditionalWritableRootPermissionProfileModificationParamsType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "AdditionalWritableRootPermissionProfileModificationParams",
"type": "object"
}
]
},
"PermissionProfileNetworkPermissions": {
"properties": {
"enabled": {
@@ -8353,6 +8431,40 @@
],
"type": "object"
},
"PermissionProfileSelectionParams": {
"oneOf": [
{
"description": "Select a named built-in or user-defined profile and optionally apply bounded modifications that Codex knows how to validate.",
"properties": {
"id": {
"type": "string"
},
"modifications": {
"items": {
"$ref": "#/definitions/PermissionProfileModificationParams"
},
"type": [
"array",
"null"
]
},
"type": {
"enum": [
"profile"
],
"title": "ProfilePermissionProfileSelectionParamsType",
"type": "string"
}
},
"required": [
"id",
"type"
],
"title": "ProfilePermissionProfileSelectionParams",
"type": "object"
}
]
},
"Personality": {
"enum": [
"none",
@@ -12766,7 +12878,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. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
},
"serviceTier": {
"anyOf": [
@@ -14277,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. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
},
"serviceTier": {
"anyOf": [
@@ -14581,7 +14693,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. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
},
"serviceTier": {
"anyOf": [
@@ -64,26 +64,19 @@
}
]
},
"FileSystemAccessMode": {
"enum": [
"read",
"write",
"none"
],
"type": "string"
},
"FileSystemPath": {
"PermissionProfileModificationParams": {
"oneOf": [
{
"description": "Additional concrete directory that should be writable.",
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"path"
"additionalWritableRoot"
],
"title": "PathFileSystemPathType",
"title": "AdditionalWritableRootPermissionProfileModificationParamsType",
"type": "string"
}
},
@@ -91,304 +84,45 @@
"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",
"title": "AdditionalWritableRootPermissionProfileModificationParams",
"type": "object"
}
]
},
"FileSystemSandboxEntry": {
"properties": {
"access": {
"$ref": "#/definitions/FileSystemAccessMode"
},
"path": {
"$ref": "#/definitions/FileSystemPath"
}
},
"required": [
"access",
"path"
],
"type": "object"
},
"FileSystemSpecialPath": {
"PermissionProfileSelectionParams": {
"oneOf": [
{
"description": "Select a named built-in or user-defined profile and optionally apply bounded modifications that Codex knows how to validate.",
"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"
],
"id": {
"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"
}
]
},
"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": {
"modifications": {
"items": {
"$ref": "#/definitions/FileSystemSandboxEntry"
"$ref": "#/definitions/PermissionProfileModificationParams"
},
"type": "array"
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"array",
"null"
]
},
"type": {
"enum": [
"restricted"
"profile"
],
"title": "RestrictedPermissionProfileFileSystemPermissionsType",
"title": "ProfilePermissionProfileSelectionParamsType",
"type": "string"
}
},
"required": [
"entries",
"id",
"type"
],
"title": "RestrictedPermissionProfileFileSystemPermissions",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"unrestricted"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
"type": "string"
}
},
"required": [
"type"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
"title": "ProfilePermissionProfileSelectionParams",
"type": "object"
}
]
},
"PermissionProfileNetworkPermissions": {
"properties": {
"enabled": {
"type": "boolean"
}
},
"required": [
"enabled"
],
"type": "object"
},
"SandboxMode": {
"enum": [
"read-only",
@@ -5,6 +5,59 @@
"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` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.<id>]` profile.",
"type": "string"
},
"modifications": {
"default": [],
"description": "Bounded user-requested modifications applied on top of the named profile, if any.",
"items": {
"$ref": "#/definitions/ActivePermissionProfileModification"
},
"type": "array"
}
},
"required": [
"id"
],
"type": "object"
},
"ActivePermissionProfileModification": {
"oneOf": [
{
"description": "Additional concrete directory that should be writable.",
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"additionalWritableRoot"
],
"title": "AdditionalWritableRootActivePermissionProfileModificationType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "AdditionalWritableRootActivePermissionProfileModification",
"type": "object"
}
]
},
"AgentPath": {
"type": "string"
},
@@ -2501,7 +2554,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. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
},
"serviceTier": {
"anyOf": [
@@ -138,202 +138,6 @@
}
]
},
"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"
}
]
},
"FunctionCallOutputBody": {
"anyOf": [
{
@@ -494,135 +298,65 @@
}
]
},
"PermissionProfile": {
"PermissionProfileModificationParams": {
"oneOf": [
{
"description": "Codex owns sandbox construction for this profile.",
"description": "Additional concrete directory that should be writable.",
"properties": {
"fileSystem": {
"$ref": "#/definitions/PermissionProfileFileSystemPermissions"
},
"network": {
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"managed"
"additionalWritableRoot"
],
"title": "ManagedPermissionProfileType",
"title": "AdditionalWritableRootPermissionProfileModificationParamsType",
"type": "string"
}
},
"required": [
"fileSystem",
"network",
"path",
"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",
"title": "AdditionalWritableRootPermissionProfileModificationParams",
"type": "object"
}
]
},
"PermissionProfileFileSystemPermissions": {
"PermissionProfileSelectionParams": {
"oneOf": [
{
"description": "Select a named built-in or user-defined profile and optionally apply bounded modifications that Codex knows how to validate.",
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/FileSystemSandboxEntry"
},
"type": "array"
"id": {
"type": "string"
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"modifications": {
"items": {
"$ref": "#/definitions/PermissionProfileModificationParams"
},
"type": [
"integer",
"array",
"null"
]
},
"type": {
"enum": [
"restricted"
"profile"
],
"title": "RestrictedPermissionProfileFileSystemPermissionsType",
"title": "ProfilePermissionProfileSelectionParamsType",
"type": "string"
}
},
"required": [
"entries",
"id",
"type"
],
"title": "RestrictedPermissionProfileFileSystemPermissions",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"unrestricted"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
"type": "string"
}
},
"required": [
"type"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
"title": "ProfilePermissionProfileSelectionParams",
"type": "object"
}
]
},
"PermissionProfileNetworkPermissions": {
"properties": {
"enabled": {
"type": "boolean"
}
},
"required": [
"enabled"
],
"type": "object"
},
"Personality": {
"enum": [
"none",
@@ -5,6 +5,59 @@
"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` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.<id>]` profile.",
"type": "string"
},
"modifications": {
"default": [],
"description": "Bounded user-requested modifications applied on top of the named profile, if any.",
"items": {
"$ref": "#/definitions/ActivePermissionProfileModification"
},
"type": "array"
}
},
"required": [
"id"
],
"type": "object"
},
"ActivePermissionProfileModification": {
"oneOf": [
{
"description": "Additional concrete directory that should be writable.",
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"additionalWritableRoot"
],
"title": "AdditionalWritableRootActivePermissionProfileModificationType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "AdditionalWritableRootActivePermissionProfileModification",
"type": "object"
}
]
},
"AgentPath": {
"type": "string"
},
@@ -2501,7 +2554,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. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
},
"serviceTier": {
"anyOf": [
@@ -90,26 +90,19 @@
],
"type": "object"
},
"FileSystemAccessMode": {
"enum": [
"read",
"write",
"none"
],
"type": "string"
},
"FileSystemPath": {
"PermissionProfileModificationParams": {
"oneOf": [
{
"description": "Additional concrete directory that should be writable.",
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"path"
"additionalWritableRoot"
],
"title": "PathFileSystemPathType",
"title": "AdditionalWritableRootPermissionProfileModificationParamsType",
"type": "string"
}
},
@@ -117,304 +110,45 @@
"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",
"title": "AdditionalWritableRootPermissionProfileModificationParams",
"type": "object"
}
]
},
"FileSystemSandboxEntry": {
"properties": {
"access": {
"$ref": "#/definitions/FileSystemAccessMode"
},
"path": {
"$ref": "#/definitions/FileSystemPath"
}
},
"required": [
"access",
"path"
],
"type": "object"
},
"FileSystemSpecialPath": {
"PermissionProfileSelectionParams": {
"oneOf": [
{
"description": "Select a named built-in or user-defined profile and optionally apply bounded modifications that Codex knows how to validate.",
"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"
],
"id": {
"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"
}
]
},
"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": {
"modifications": {
"items": {
"$ref": "#/definitions/FileSystemSandboxEntry"
"$ref": "#/definitions/PermissionProfileModificationParams"
},
"type": "array"
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"array",
"null"
]
},
"type": {
"enum": [
"restricted"
"profile"
],
"title": "RestrictedPermissionProfileFileSystemPermissionsType",
"title": "ProfilePermissionProfileSelectionParamsType",
"type": "string"
}
},
"required": [
"entries",
"id",
"type"
],
"title": "RestrictedPermissionProfileFileSystemPermissions",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"unrestricted"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
"type": "string"
}
},
"required": [
"type"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
"title": "ProfilePermissionProfileSelectionParams",
"type": "object"
}
]
},
"PermissionProfileNetworkPermissions": {
"properties": {
"enabled": {
"type": "boolean"
}
},
"required": [
"enabled"
],
"type": "object"
},
"Personality": {
"enum": [
"none",
@@ -5,6 +5,59 @@
"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` or the implicit built-in default, such as `:workspace` or a user-defined `[permissions.<id>]` profile.",
"type": "string"
},
"modifications": {
"default": [],
"description": "Bounded user-requested modifications applied on top of the named profile, if any.",
"items": {
"$ref": "#/definitions/ActivePermissionProfileModification"
},
"type": "array"
}
},
"required": [
"id"
],
"type": "object"
},
"ActivePermissionProfileModification": {
"oneOf": [
{
"description": "Additional concrete directory that should be writable.",
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"additionalWritableRoot"
],
"title": "AdditionalWritableRootActivePermissionProfileModificationType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "AdditionalWritableRootActivePermissionProfileModification",
"type": "object"
}
]
},
"AgentPath": {
"type": "string"
},
@@ -2501,7 +2554,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. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
},
"serviceTier": {
"anyOf": [
+21 -287
View File
@@ -99,202 +99,6 @@
],
"type": "object"
},
"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"
}
]
},
"ModeKind": {
"description": "Initial collaboration mode to use when the TUI starts.",
"enum": [
@@ -310,135 +114,65 @@
],
"type": "string"
},
"PermissionProfile": {
"PermissionProfileModificationParams": {
"oneOf": [
{
"description": "Codex owns sandbox construction for this profile.",
"description": "Additional concrete directory that should be writable.",
"properties": {
"fileSystem": {
"$ref": "#/definitions/PermissionProfileFileSystemPermissions"
},
"network": {
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"managed"
"additionalWritableRoot"
],
"title": "ManagedPermissionProfileType",
"title": "AdditionalWritableRootPermissionProfileModificationParamsType",
"type": "string"
}
},
"required": [
"fileSystem",
"network",
"path",
"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",
"title": "AdditionalWritableRootPermissionProfileModificationParams",
"type": "object"
}
]
},
"PermissionProfileFileSystemPermissions": {
"PermissionProfileSelectionParams": {
"oneOf": [
{
"description": "Select a named built-in or user-defined profile and optionally apply bounded modifications that Codex knows how to validate.",
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/FileSystemSandboxEntry"
},
"type": "array"
"id": {
"type": "string"
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"modifications": {
"items": {
"$ref": "#/definitions/PermissionProfileModificationParams"
},
"type": [
"integer",
"array",
"null"
]
},
"type": {
"enum": [
"restricted"
"profile"
],
"title": "RestrictedPermissionProfileFileSystemPermissionsType",
"title": "ProfilePermissionProfileSelectionParamsType",
"type": "string"
}
},
"required": [
"entries",
"id",
"type"
],
"title": "RestrictedPermissionProfileFileSystemPermissions",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"unrestricted"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
"type": "string"
}
},
"required": [
"type"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
"title": "ProfilePermissionProfileSelectionParams",
"type": "object"
}
]
},
"PermissionProfileNetworkPermissions": {
"properties": {
"enabled": {
"type": "boolean"
}
},
"required": [
"enabled"
],
"type": "object"
},
"Personality": {
"enum": [
"none",
@@ -0,0 +1,21 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ActivePermissionProfileModification } from "./ActivePermissionProfileModification";
export type ActivePermissionProfile = {
/**
* Identifier from `default_permissions` or the implicit built-in default,
* such as `:workspace` 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,
/**
* Bounded user-requested modifications applied on top of the named
* profile, if any.
*/
modifications: Array<ActivePermissionProfileModification>, };
@@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
export type ActivePermissionProfileModification = { "type": "additionalWritableRoot", path: AbsolutePathBuf, };
@@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
export type PermissionProfileModificationParams = { "type": "additionalWritableRoot", path: AbsolutePathBuf, };
@@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PermissionProfileModificationParams } from "./PermissionProfileModificationParams";
export type PermissionProfileSelectionParams = { "type": "profile", id: string, modifications?: Array<PermissionProfileModificationParams> | null, };
@@ -16,8 +16,8 @@ instructionSources: Array<AbsolutePathBuf>, approvalPolicy: AskForApproval, /**
* Reviewer currently used for approval requests on this thread.
*/
approvalsReviewer: ApprovalsReviewer, /**
* Legacy sandbox policy retained for compatibility. New clients should use
* `permissionProfile` when present as the canonical active permissions
* view.
* Legacy sandbox policy retained for compatibility. Experimental clients
* should prefer `permissionProfile` when they need exact runtime
* permissions.
*/
sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null};
@@ -16,8 +16,8 @@ instructionSources: Array<AbsolutePathBuf>, approvalPolicy: AskForApproval, /**
* Reviewer currently used for approval requests on this thread.
*/
approvalsReviewer: ApprovalsReviewer, /**
* Legacy sandbox policy retained for compatibility. New clients should use
* `permissionProfile` when present as the canonical active permissions
* view.
* Legacy sandbox policy retained for compatibility. Experimental clients
* should prefer `permissionProfile` when they need exact runtime
* permissions.
*/
sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null};
@@ -16,8 +16,8 @@ instructionSources: Array<AbsolutePathBuf>, approvalPolicy: AskForApproval, /**
* Reviewer currently used for approval requests on this thread.
*/
approvalsReviewer: ApprovalsReviewer, /**
* Legacy sandbox policy retained for compatibility. New clients should use
* `permissionProfile` when present as the canonical active permissions
* view.
* Legacy sandbox policy retained for compatibility. Experimental clients
* should prefer `permissionProfile` when they need exact runtime
* permissions.
*/
sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null};
@@ -4,6 +4,8 @@ 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 { ActivePermissionProfileModification } from "./ActivePermissionProfileModification";
export type { AddCreditsNudgeCreditType } from "./AddCreditsNudgeCreditType";
export type { AddCreditsNudgeEmailStatus } from "./AddCreditsNudgeEmailStatus";
export type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions";
@@ -261,7 +263,9 @@ export type { PatchChangeKind } from "./PatchChangeKind";
export type { PermissionGrantScope } from "./PermissionGrantScope";
export type { PermissionProfile } from "./PermissionProfile";
export type { PermissionProfileFileSystemPermissions } from "./PermissionProfileFileSystemPermissions";
export type { PermissionProfileModificationParams } from "./PermissionProfileModificationParams";
export type { PermissionProfileNetworkPermissions } from "./PermissionProfileNetworkPermissions";
export type { PermissionProfileSelectionParams } from "./PermissionProfileSelectionParams";
export type { PermissionsRequestApprovalParams } from "./PermissionsRequestApprovalParams";
export type { PermissionsRequestApprovalResponse } from "./PermissionsRequestApprovalResponse";
export type { PlanDeltaNotification } from "./PlanDeltaNotification";
@@ -2131,12 +2131,8 @@ 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(),
),
permission_profile: None,
active_permission_profile: None,
reasoning_effort: None,
},
};
@@ -2179,9 +2175,8 @@ mod tests {
"sandbox": {
"type": "dangerFullAccess"
},
"permissionProfile": {
"type": "disabled"
},
"permissionProfile": null,
"activePermissionProfile": null,
"reasoningEffort": null
}
}),
+160 -29
View File
@@ -38,6 +38,8 @@ 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::ActivePermissionProfileModification as CoreActivePermissionProfileModification;
use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile;
use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions;
use codex_protocol::models::ManagedFileSystemPermissions as CoreManagedFileSystemPermissions;
@@ -1703,6 +1705,109 @@ 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` or the implicit built-in default,
/// such as `:workspace` 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>,
/// Bounded user-requested modifications applied on top of the named
/// profile, if any.
#[serde(default)]
pub modifications: Vec<ActivePermissionProfileModification>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "camelCase")]
#[ts(tag = "type")]
#[ts(export_to = "v2/")]
pub enum ActivePermissionProfileModification {
/// Additional concrete directory that should be writable.
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
AdditionalWritableRoot { path: AbsolutePathBuf },
}
impl From<CoreActivePermissionProfileModification> for ActivePermissionProfileModification {
fn from(value: CoreActivePermissionProfileModification) -> Self {
match value {
CoreActivePermissionProfileModification::AdditionalWritableRoot { path } => {
Self::AdditionalWritableRoot { path }
}
}
}
}
impl From<ActivePermissionProfileModification> for CoreActivePermissionProfileModification {
fn from(value: ActivePermissionProfileModification) -> Self {
match value {
ActivePermissionProfileModification::AdditionalWritableRoot { path } => {
Self::AdditionalWritableRoot { path }
}
}
}
}
impl From<CoreActivePermissionProfile> for ActivePermissionProfile {
fn from(value: CoreActivePermissionProfile) -> Self {
Self {
id: value.id,
extends: value.extends,
modifications: value
.modifications
.into_iter()
.map(ActivePermissionProfileModification::from)
.collect(),
}
}
}
impl From<ActivePermissionProfile> for CoreActivePermissionProfile {
fn from(value: ActivePermissionProfile) -> Self {
Self {
id: value.id,
extends: value.extends,
modifications: value
.modifications
.into_iter()
.map(CoreActivePermissionProfileModification::from)
.collect(),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "camelCase")]
#[ts(tag = "type")]
#[ts(export_to = "v2/")]
pub enum PermissionProfileSelectionParams {
/// Select a named built-in or user-defined profile and optionally apply
/// bounded modifications that Codex knows how to validate.
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
Profile {
id: String,
#[ts(optional = nullable)]
modifications: Option<Vec<PermissionProfileModificationParams>>,
},
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "camelCase")]
#[ts(tag = "type")]
#[ts(export_to = "v2/")]
pub enum PermissionProfileModificationParams {
/// Additional concrete directory that should be writable.
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
AdditionalWritableRoot { path: AbsolutePathBuf },
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -3462,11 +3567,12 @@ pub struct ThreadStartParams {
pub approvals_reviewer: Option<ApprovalsReviewer>,
#[ts(optional = nullable)]
pub sandbox: Option<SandboxMode>,
/// Full permissions override for this thread. Cannot be combined with
/// `sandbox`.
#[experimental("thread/start.permissionProfile")]
/// Named profile selection for this thread. Cannot be combined with
/// `sandbox`. Use bounded `modifications` for supported turn/thread
/// adjustments instead of replacing the full permissions profile.
#[experimental("thread/start.permissions")]
#[ts(optional = nullable)]
pub permission_profile: Option<PermissionProfile>,
pub permissions: Option<PermissionProfileSelectionParams>,
#[ts(optional = nullable)]
pub config: Option<HashMap<String, JsonValue>>,
#[ts(optional = nullable)]
@@ -3543,14 +3649,20 @@ pub struct ThreadStartResponse {
pub approval_policy: AskForApproval,
/// 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.
/// Legacy sandbox policy retained for compatibility. Experimental clients
/// should prefer `permissionProfile` when they need exact runtime
/// permissions.
pub sandbox: SandboxPolicy,
/// Canonical active permissions view for this thread.
/// Full active permissions for this thread. `activePermissionProfile`
/// carries display/provenance metadata for this runtime profile.
#[experimental("thread/start.permissionProfile")]
#[serde(default)]
pub permission_profile: Option<PermissionProfile>,
/// Named or implicit built-in profile that produced the active
/// permissions, when known.
#[experimental("thread/start.activePermissionProfile")]
#[serde(default)]
pub active_permission_profile: Option<ActivePermissionProfile>,
pub reasoning_effort: Option<ReasoningEffort>,
}
@@ -3608,11 +3720,12 @@ pub struct ThreadResumeParams {
pub approvals_reviewer: Option<ApprovalsReviewer>,
#[ts(optional = nullable)]
pub sandbox: Option<SandboxMode>,
/// Full permissions override for the resumed thread. Cannot be combined
/// with `sandbox`.
#[experimental("thread/resume.permissionProfile")]
/// Named profile selection for the resumed thread. Cannot be combined
/// with `sandbox`. Use bounded `modifications` for supported thread
/// adjustments instead of replacing the full permissions profile.
#[experimental("thread/resume.permissions")]
#[ts(optional = nullable)]
pub permission_profile: Option<PermissionProfile>,
pub permissions: Option<PermissionProfileSelectionParams>,
#[ts(optional = nullable)]
pub config: Option<HashMap<String, serde_json::Value>>,
#[ts(optional = nullable)]
@@ -3649,14 +3762,20 @@ pub struct ThreadResumeResponse {
pub approval_policy: AskForApproval,
/// 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.
/// Legacy sandbox policy retained for compatibility. Experimental clients
/// should prefer `permissionProfile` when they need exact runtime
/// permissions.
pub sandbox: SandboxPolicy,
/// Canonical active permissions view for this thread.
/// Full active permissions for this thread. `activePermissionProfile`
/// carries display/provenance metadata for this runtime profile.
#[experimental("thread/resume.permissionProfile")]
#[serde(default)]
pub permission_profile: Option<PermissionProfile>,
/// Named or implicit built-in profile that produced the active
/// permissions, when known.
#[experimental("thread/resume.activePermissionProfile")]
#[serde(default)]
pub active_permission_profile: Option<ActivePermissionProfile>,
pub reasoning_effort: Option<ReasoningEffort>,
}
@@ -3705,11 +3824,12 @@ pub struct ThreadForkParams {
pub approvals_reviewer: Option<ApprovalsReviewer>,
#[ts(optional = nullable)]
pub sandbox: Option<SandboxMode>,
/// Full permissions override for the forked thread. Cannot be combined
/// with `sandbox`.
#[experimental("thread/fork.permissionProfile")]
/// Named profile selection for the forked thread. Cannot be combined with
/// `sandbox`. Use bounded `modifications` for supported thread
/// adjustments instead of replacing the full permissions profile.
#[experimental("thread/fork.permissions")]
#[ts(optional = nullable)]
pub permission_profile: Option<PermissionProfile>,
pub permissions: Option<PermissionProfileSelectionParams>,
#[ts(optional = nullable)]
pub config: Option<HashMap<String, serde_json::Value>>,
#[ts(optional = nullable)]
@@ -3746,14 +3866,20 @@ pub struct ThreadForkResponse {
pub approval_policy: AskForApproval,
/// 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.
/// Legacy sandbox policy retained for compatibility. Experimental clients
/// should prefer `permissionProfile` when they need exact runtime
/// permissions.
pub sandbox: SandboxPolicy,
/// Canonical active permissions view for this thread.
/// Full active permissions for this thread. `activePermissionProfile`
/// carries display/provenance metadata for this runtime profile.
#[experimental("thread/fork.permissionProfile")]
#[serde(default)]
pub permission_profile: Option<PermissionProfile>,
/// Named or implicit built-in profile that produced the active
/// permissions, when known.
#[experimental("thread/fork.activePermissionProfile")]
#[serde(default)]
pub active_permission_profile: Option<ActivePermissionProfile>,
pub reasoning_effort: Option<ReasoningEffort>,
}
@@ -5338,11 +5464,13 @@ pub struct TurnStartParams {
/// Override the sandbox policy for this turn and subsequent turns.
#[ts(optional = nullable)]
pub sandbox_policy: Option<SandboxPolicy>,
/// Override the full permissions profile for this turn and subsequent
/// turns. Cannot be combined with `sandboxPolicy`.
#[experimental("turn/start.permissionProfile")]
/// Select a named permissions profile for this turn and subsequent turns.
/// Cannot be combined with `sandboxPolicy`. Use bounded `modifications`
/// for supported turn adjustments instead of replacing the full
/// permissions profile.
#[experimental("turn/start.permissions")]
#[ts(optional = nullable)]
pub permission_profile: Option<PermissionProfile>,
pub permissions: Option<PermissionProfileSelectionParams>,
/// Override the model for this turn and subsequent turns.
#[ts(optional = nullable)]
pub model: Option<String>,
@@ -10790,6 +10918,9 @@ mod tests {
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]
@@ -10817,7 +10948,7 @@ mod tests {
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
permissions: None,
model: None,
service_tier: None,
effort: None,
+10 -7
View File
@@ -88,7 +88,7 @@ Use the thread APIs to create, list, or archive conversations. Drive a conversat
- Initialize once per connection: Immediately after opening a transport connection, send an `initialize` request with your client metadata, then emit an `initialized` notification. Any other request on that connection before this handshake gets rejected.
- Start (or resume) a thread: Call `thread/start` to open a fresh conversation. The response returns the thread object and you’ll also get a `thread/started` notification. If you’re continuing an existing conversation, call `thread/resume` with its ID instead. If you want to branch from an existing conversation, call `thread/fork` to create a new thread id with copied history. Like `thread/start`, `thread/fork` also accepts `ephemeral: true` for an in-memory temporary thread.
The returned `thread.ephemeral` flag tells you whether the session is intentionally in-memory only; when it is `true`, `thread.path` is `null`.
- Begin a turn: To send user input, call `turn/start` with the target `threadId` and the user's input. Optional fields let you override model, cwd, sandbox policy or `permissionProfile`, approval policy, approvals reviewer, etc. This immediately returns the new turn object. The app-server emits `turn/started` when that turn actually begins running.
- Begin a turn: To send user input, call `turn/start` with the target `threadId` and the user's input. Optional fields let you override model, cwd, sandbox policy or experimental `permissions` profile selection, approval policy, approvals reviewer, etc. This immediately returns the new turn object. The app-server emits `turn/started` when that turn actually begins running.
- Stream events: After `turn/start`, keep reading JSON-RPC notifications on stdout. You’ll see `item/started`, `item/completed`, deltas like `item/agentMessage/delta`, tool progress, etc. These represent streaming model output plus any side effects (commands, tool calls, reasoning notes).
- Finish the turn: When the model is done (or the turn is interrupted via making the `turn/interrupt` call), the server sends `turn/completed` with the final turn state and token usage.
@@ -142,9 +142,10 @@ Example with notification opt-out:
## API Overview
- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. For permissions, prefer `permissionProfile`; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissionProfile`. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd`.
- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. For permissions, prefer experimental `permissions` profile selection; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissions`. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd`.
- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`.
- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; if the source thread is currently mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Pass `excludeTurns: true` when the client plans to page fork history via `thread/turns/list` instead of receiving the full turn array immediately. Accepts the same permission override rules as `thread/start`.
- `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. Experimental clients can read response `permissionProfile` for the exact active runtime permissions and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known.
- `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded.
- `thread/loaded/list` — list the thread ids currently loaded in memory.
- `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded.
@@ -166,7 +167,7 @@ Example with notification opt-out:
- `thread/shellCommand` — run a user-initiated `!` shell command against a thread; this runs unsandboxed with full access rather than inheriting the thread sandbox policy. Returns `{}` immediately while progress streams through standard turn/item notifications and any active turn receives the formatted output in its message stream.
- `thread/backgroundTerminals/clean` — terminate all running background terminals for a thread (experimental; requires `capabilities.experimentalApi`); returns `{}` when the cleanup request is accepted.
- `thread/rollback` — drop the last N turns from the agent’s in-memory context and persist a rollback marker in the rollout so future resumes see the pruned history; returns the updated `thread` (with `turns` populated) on success.
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. Prefer `permissionProfile` for permission overrides; the legacy `sandboxPolicy` field is still accepted but cannot be combined with `permissionProfile`. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode".
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. Prefer experimental `permissions` profile selection for permission overrides; the legacy `sandboxPolicy` field is still accepted but cannot be combined with `permissions`. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode".
- `thread/inject_items` — append raw Responses API items to a loaded thread’s model-visible history without starting a user turn; returns `{}` on success.
- `turn/steer` — add user input to an already in-flight regular turn without starting a new turn; returns the active `turnId` that accepted the input. Review and manual compaction turns reject `turn/steer`.
- `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`.
@@ -238,8 +239,9 @@ Start a fresh thread when you need a new Codex conversation.
"cwd": "/Users/me/project",
"approvalPolicy": "never",
"sandbox": "workspaceWrite",
// Prefer "permissionProfile" for full permission overrides. Do not send
// both "sandbox" and "permissionProfile".
// Prefer experimental profile selection:
// "permissions": { "type": "profile", "id": ":workspace" }
// Do not send both "sandbox" and "permissions".
"personality": "friendly",
"serviceName": "my_app_server_client", // optional metrics tag (`service_name`)
"sessionStartSource": "startup", // optional: "startup" (default) or "clear"
@@ -636,8 +638,9 @@ You can optionally specify config overrides on the new turn. If specified, these
"writableRoots": ["/Users/me/project"],
"networkAccess": true
},
// Prefer "permissionProfile" for full permission overrides. Do not send
// both "sandboxPolicy" and "permissionProfile".
// Prefer experimental profile selection:
// "permissions": { "type": "profile", "id": ":workspace" }
// Do not send both "sandboxPolicy" and "permissions".
"model": "gpt-5.1-codex",
"effort": "medium",
"summary": "concise",
@@ -109,7 +109,8 @@ use codex_app_server_protocol::MockExperimentalMethodParams;
use codex_app_server_protocol::MockExperimentalMethodResponse;
use codex_app_server_protocol::ModelListParams;
use codex_app_server_protocol::ModelListResponse;
use codex_app_server_protocol::PermissionProfile as ApiPermissionProfile;
use codex_app_server_protocol::PermissionProfileModificationParams;
use codex_app_server_protocol::PermissionProfileSelectionParams;
use codex_app_server_protocol::PluginDetail;
use codex_app_server_protocol::PluginInstallParams;
use codex_app_server_protocol::PluginInstallResponse;
@@ -2438,7 +2439,7 @@ impl CodexMessageProcessor {
approval_policy,
approvals_reviewer,
sandbox,
permission_profile,
permissions,
config,
service_name,
base_instructions,
@@ -2452,11 +2453,11 @@ impl CodexMessageProcessor {
environments,
persist_extended_history,
} = params;
if sandbox.is_some() && permission_profile.is_some() {
if sandbox.is_some() && permissions.is_some() {
self.outgoing
.send_error(
request_id,
invalid_request("`permissionProfile` cannot be combined with `sandbox`"),
invalid_request("`permissions` cannot be combined with `sandbox`"),
)
.await;
return;
@@ -2491,7 +2492,7 @@ impl CodexMessageProcessor {
approval_policy,
approvals_reviewer,
sandbox,
permission_profile,
permissions,
base_instructions,
developer_instructions,
personality,
@@ -2550,7 +2551,7 @@ impl CodexMessageProcessor {
/*approval_policy*/ None,
/*approvals_reviewer*/ None,
/*sandbox*/ None,
/*permission_profile*/ None,
/*permissions*/ None,
/*base_instructions*/ None,
/*developer_instructions*/ None,
/*personality*/ None,
@@ -2861,8 +2862,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(),
@@ -2874,7 +2876,8 @@ impl CodexMessageProcessor {
approval_policy: config_snapshot.approval_policy.into(),
approvals_reviewer: config_snapshot.approvals_reviewer.into(),
sandbox,
permission_profile,
permission_profile: Some(config_snapshot.permission_profile.into()),
active_permission_profile,
reasoning_effort: config_snapshot.reasoning_effort,
};
Ok::<_, JSONRPCErrorError>((response, thread_started_notification(thread)))
@@ -2920,12 +2923,12 @@ impl CodexMessageProcessor {
approval_policy: Option<codex_app_server_protocol::AskForApproval>,
approvals_reviewer: Option<codex_app_server_protocol::ApprovalsReviewer>,
sandbox: Option<SandboxMode>,
permission_profile: Option<ApiPermissionProfile>,
permissions: Option<PermissionProfileSelectionParams>,
base_instructions: Option<String>,
developer_instructions: Option<String>,
personality: Option<Personality>,
) -> ConfigOverrides {
ConfigOverrides {
let mut overrides = ConfigOverrides {
model,
model_provider,
service_tier,
@@ -2935,14 +2938,15 @@ impl CodexMessageProcessor {
approvals_reviewer: approvals_reviewer
.map(codex_app_server_protocol::ApprovalsReviewer::to_core),
sandbox_mode: sandbox.map(SandboxMode::to_core),
permission_profile: permission_profile.map(Into::into),
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),
base_instructions,
developer_instructions,
personality,
..Default::default()
}
};
apply_permission_profile_selection_to_config_overrides(&mut overrides, permissions);
overrides
}
async fn thread_archive(&self, request_id: ConnectionRequestId, params: ThreadArchiveParams) {
@@ -4223,11 +4227,11 @@ impl CodexMessageProcessor {
return;
}
if params.sandbox.is_some() && params.permission_profile.is_some() {
if params.sandbox.is_some() && params.permissions.is_some() {
self.outgoing
.send_error(
request_id,
invalid_request("`permissionProfile` cannot be combined with `sandbox`"),
invalid_request("`permissions` cannot be combined with `sandbox`"),
)
.await;
return;
@@ -4260,7 +4264,7 @@ impl CodexMessageProcessor {
approval_policy,
approvals_reviewer,
sandbox,
permission_profile,
permissions,
config: mut request_overrides,
base_instructions,
developer_instructions,
@@ -4295,7 +4299,7 @@ impl CodexMessageProcessor {
approval_policy,
approvals_reviewer,
sandbox,
permission_profile,
permissions,
base_instructions,
developer_instructions,
personality,
@@ -4401,8 +4405,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,
@@ -4414,7 +4419,8 @@ impl CodexMessageProcessor {
approval_policy: session_configured.approval_policy.into(),
approvals_reviewer: session_configured.approvals_reviewer.into(),
sandbox,
permission_profile,
permission_profile: Some(config_snapshot.permission_profile.into()),
active_permission_profile,
reasoning_effort: session_configured.reasoning_effort,
};
@@ -4813,7 +4819,7 @@ impl CodexMessageProcessor {
approval_policy,
approvals_reviewer,
sandbox,
permission_profile,
permissions,
config: cli_overrides,
base_instructions,
developer_instructions,
@@ -4823,9 +4829,9 @@ impl CodexMessageProcessor {
} = params;
let include_turns = !exclude_turns;
let result = async {
if sandbox.is_some() && permission_profile.is_some() {
if sandbox.is_some() && permissions.is_some() {
return Err(invalid_request(
"`permissionProfile` cannot be combined with `sandbox`",
"`permissions` cannot be combined with `sandbox`",
));
}
@@ -4878,7 +4884,7 @@ impl CodexMessageProcessor {
approval_policy,
approvals_reviewer,
sandbox,
permission_profile,
permissions,
base_instructions,
developer_instructions,
/*personality*/ None,
@@ -4993,8 +4999,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(),
@@ -5006,7 +5013,8 @@ impl CodexMessageProcessor {
approval_policy: session_configured.approval_policy.into(),
approvals_reviewer: session_configured.approvals_reviewer.into(),
sandbox,
permission_profile,
permission_profile: Some(config_snapshot.permission_profile.into()),
active_permission_profile,
reasoning_effort: session_configured.reasoning_effort,
};
@@ -6581,7 +6589,7 @@ impl CodexMessageProcessor {
|| params.approval_policy.is_some()
|| params.approvals_reviewer.is_some()
|| params.sandbox_policy.is_some()
|| params.permission_profile.is_some()
|| params.permissions.is_some()
|| params.model.is_some()
|| params.service_tier.is_some()
|| params.effort.is_some()
@@ -6589,9 +6597,9 @@ impl CodexMessageProcessor {
|| collaboration_mode.is_some()
|| params.personality.is_some();
if params.sandbox_policy.is_some() && params.permission_profile.is_some() {
if params.sandbox_policy.is_some() && params.permissions.is_some() {
return Err(invalid_request(
"`permissionProfile` cannot be combined with `sandboxPolicy`",
"`permissions` cannot be combined with `sandboxPolicy`",
));
}
@@ -6601,7 +6609,45 @@ impl CodexMessageProcessor {
.approvals_reviewer
.map(codex_app_server_protocol::ApprovalsReviewer::to_core);
let sandbox_policy = params.sandbox_policy.map(|p| p.to_core());
let permission_profile = params.permission_profile.map(Into::into);
let (permission_profile, active_permission_profile) =
if let Some(permissions) = params.permissions {
let snapshot = thread.config_snapshot().await;
let mut overrides = ConfigOverrides {
cwd: cwd.clone(),
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),
..Default::default()
};
apply_permission_profile_selection_to_config_overrides(
&mut overrides,
Some(permissions),
);
let config = self
.config_manager
.load_for_cwd(
/*request_overrides*/ None,
overrides,
Some(snapshot.cwd.to_path_buf()),
)
.await
.map_err(|err| config_load_error(&err))?;
// Startup config is allowed to fall back when requirements
// disallow a configured profile. An explicit turn request
// is different: reject it before accepting user input.
if let Some(warning) = config.startup_warnings.iter().find(|warning| {
warning.contains("Configured value for `permission_profile` is disallowed")
}) {
return Err(invalid_request(format!(
"invalid turn context override: {warning}"
)));
}
(
Some(config.permissions.permission_profile()),
config.permissions.active_permission_profile(),
)
} else {
(None, None)
};
let model = params.model;
let effort = params.effort.map(Some);
let summary = params.summary;
@@ -6619,6 +6665,7 @@ impl CodexMessageProcessor {
approvals_reviewer,
sandbox_policy: sandbox_policy.clone(),
permission_profile: permission_profile.clone(),
active_permission_profile: active_permission_profile.clone(),
windows_sandbox_level: None,
model: model.clone(),
effort,
@@ -6645,6 +6692,7 @@ impl CodexMessageProcessor {
approvals_reviewer,
sandbox_policy,
permission_profile,
active_permission_profile,
windows_sandbox_level: None,
model,
effort,
@@ -8314,13 +8362,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,
@@ -8332,7 +8382,8 @@ async fn handle_pending_thread_resume_request(
approval_policy: approval_policy.into(),
approvals_reviewer: approvals_reviewer.into(),
sandbox,
permission_profile,
permission_profile: Some(permission_profile.into()),
active_permission_profile,
reasoning_effort,
};
let token_usage_thread = pending.include_turns.then(|| response.thread.clone());
@@ -8559,15 +8610,11 @@ fn collect_resume_override_mismatches(
));
}
}
if let Some(requested_permission_profile) = request.permission_profile.as_ref() {
let requested_permission_profile =
codex_protocol::models::PermissionProfile::from(requested_permission_profile.clone());
if requested_permission_profile != config_snapshot.permission_profile {
mismatch_details.push(format!(
"permission_profile requested={requested_permission_profile:?} active={:?}",
config_snapshot.permission_profile
));
}
if request.permissions.is_some() {
mismatch_details.push(format!(
"permissions override was provided and ignored while running; active={:?}",
config_snapshot.active_permission_profile
));
}
if let Some(requested_personality) = request.personality.as_ref()
&& config_snapshot.personality.as_ref() != Some(requested_personality)
@@ -9474,10 +9521,29 @@ 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 apply_permission_profile_selection_to_config_overrides(
overrides: &mut ConfigOverrides,
permissions: Option<PermissionProfileSelectionParams>,
) {
let Some(PermissionProfileSelectionParams::Profile { id, modifications }) = permissions else {
return;
};
overrides.default_permissions = Some(id);
overrides
.additional_writable_roots
.extend(modifications.unwrap_or_default().into_iter().map(
|modification| match modification {
PermissionProfileModificationParams::AdditionalWritableRoot { path } => {
path.to_path_buf()
}
},
));
}
fn thread_response_sandbox_policy(
@@ -9505,6 +9571,13 @@ fn requested_permissions_trust_project(overrides: &ConfigOverrides, cwd: &Path)
return true;
}
if matches!(
overrides.default_permissions.as_deref(),
Some(":workspace" | ":danger-no-sandbox")
) {
return true;
}
overrides
.permission_profile
.as_ref()
@@ -10021,23 +10094,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();
@@ -10082,6 +10138,20 @@ mod tests {
},
cwd.as_path()
));
assert!(requested_permissions_trust_project(
&ConfigOverrides {
default_permissions: Some(":workspace".to_string()),
..Default::default()
},
cwd.as_path()
));
assert!(requested_permissions_trust_project(
&ConfigOverrides {
default_permissions: Some(":danger-no-sandbox".to_string()),
..Default::default()
},
cwd.as_path()
));
assert!(!requested_permissions_trust_project(
&ConfigOverrides {
permission_profile: Some(read_only_profile),
@@ -10089,6 +10159,13 @@ mod tests {
},
cwd.as_path()
));
assert!(!requested_permissions_trust_project(
&ConfigOverrides {
default_permissions: Some(":read-only".to_string()),
..Default::default()
},
cwd.as_path()
));
}
#[test]
@@ -10259,7 +10336,7 @@ mod tests {
approval_policy: None,
approvals_reviewer: None,
sandbox: None,
permission_profile: None,
permissions: None,
config: None,
base_instructions: None,
developer_instructions: None,
@@ -10274,6 +10351,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,
@@ -761,7 +761,7 @@ async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> {
cwd: None,
approval_policy: None,
sandbox_policy: None,
permission_profile: None,
permissions: None,
approvals_reviewer: None,
model: None,
service_tier: None,
@@ -667,7 +667,7 @@ async fn skills_changed_notification_is_emitted_after_skill_change() -> Result<(
approval_policy: None,
approvals_reviewer: None,
sandbox: None,
permission_profile: None,
permissions: None,
config: None,
service_name: None,
base_instructions: None,
@@ -27,9 +27,6 @@ use codex_app_server_protocol::FileChangeApprovalDecision;
use codex_app_server_protocol::FileChangeOutputDeltaNotification;
use codex_app_server_protocol::FileChangePatchUpdatedNotification;
use codex_app_server_protocol::FileChangeRequestApprovalResponse;
use codex_app_server_protocol::FileSystemAccessMode;
use codex_app_server_protocol::FileSystemPath;
use codex_app_server_protocol::FileSystemSandboxEntry;
use codex_app_server_protocol::ItemCompletedNotification;
use codex_app_server_protocol::ItemStartedNotification;
use codex_app_server_protocol::JSONRPCError;
@@ -38,9 +35,7 @@ use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::PatchApplyStatus;
use codex_app_server_protocol::PatchChangeKind;
use codex_app_server_protocol::PermissionProfile;
use codex_app_server_protocol::PermissionProfileFileSystemPermissions;
use codex_app_server_protocol::PermissionProfileNetworkPermissions;
use codex_app_server_protocol::PermissionProfileSelectionParams;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ServerRequest;
use codex_app_server_protocol::ServerRequestResolvedNotification;
@@ -67,7 +62,6 @@ use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::Settings;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS;
use codex_utils_absolute_path::AbsolutePathBuf;
use core_test_support::responses;
use core_test_support::skip_if_no_network;
use pretty_assertions::assert_eq;
@@ -675,9 +669,8 @@ async fn turn_start_rejects_combined_oversized_text_input() -> Result<()> {
}
#[tokio::test]
async fn turn_start_rejects_invalid_permission_profile_before_starting_turn() -> Result<()> {
async fn turn_start_rejects_invalid_permission_selection_before_starting_turn() -> Result<()> {
let codex_home = TempDir::new()?;
let disallowed_write_root = TempDir::new()?;
create_config_toml(
codex_home.path(),
"http://localhost/unused",
@@ -704,9 +697,6 @@ async fn turn_start_rejects_invalid_permission_profile_before_starting_turn() ->
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
let disallowed_write_root = AbsolutePathBuf::from_absolute_path(disallowed_write_root.path())
.expect("tempdir path should be absolute");
let turn_req = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id,
@@ -714,17 +704,9 @@ async fn turn_start_rejects_invalid_permission_profile_before_starting_turn() ->
text: "Hello".to_string(),
text_elements: Vec::new(),
}],
permission_profile: Some(PermissionProfile::Managed {
network: PermissionProfileNetworkPermissions { enabled: false },
file_system: PermissionProfileFileSystemPermissions::Restricted {
entries: vec![FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: disallowed_write_root,
},
access: FileSystemAccessMode::Write,
}],
glob_scan_max_depth: None,
},
permissions: Some(PermissionProfileSelectionParams::Profile {
id: ":danger-no-sandbox".to_string(),
modifications: None,
}),
..Default::default()
})
@@ -749,7 +731,7 @@ async fn turn_start_rejects_invalid_permission_profile_before_starting_turn() ->
.await;
assert!(
turn_started.is_err(),
"did not expect a turn/started notification after rejected permissionProfile"
"did not expect a turn/started notification after rejected permissions selection"
);
Ok(())
@@ -1833,7 +1815,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
}),
permission_profile: None,
permissions: None,
model: Some("mock-model".to_string()),
effort: Some(ReasoningEffort::Medium),
summary: Some(ReasoningSummary::Auto),
@@ -1869,7 +1851,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
approval_policy: Some(codex_app_server_protocol::AskForApproval::Never),
approvals_reviewer: None,
sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::DangerFullAccess),
permission_profile: None,
permissions: None,
model: Some("mock-model".to_string()),
effort: Some(ReasoningEffort::Medium),
summary: Some(ReasoningSummary::Auto),
+5
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>,
@@ -75,6 +77,7 @@ pub struct CodexThreadTurnContextOverrides {
pub approvals_reviewer: Option<ApprovalsReviewer>,
pub sandbox_policy: Option<SandboxPolicy>,
pub permission_profile: Option<PermissionProfile>,
pub active_permission_profile: Option<ActivePermissionProfile>,
pub windows_sandbox_level: Option<WindowsSandboxLevel>,
pub model: Option<String>,
pub effort: Option<Option<ReasoningEffort>>,
@@ -225,6 +228,7 @@ impl CodexThread {
approvals_reviewer,
sandbox_policy,
permission_profile,
active_permission_profile,
windows_sandbox_level,
model,
effort,
@@ -249,6 +253,7 @@ impl CodexThread {
approvals_reviewer,
sandbox_policy,
permission_profile,
active_permission_profile,
windows_sandbox_level,
collaboration_mode: Some(collaboration_mode),
reasoning_summary: summary,
+164
View File
@@ -60,6 +60,8 @@ 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::ActivePermissionProfileModification;
use codex_protocol::models::ManagedFileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::SandboxEnforcement;
@@ -907,6 +909,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 +938,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 +1278,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 +1297,86 @@ async fn default_permissions_can_select_builtin_profile_without_permissions_tabl
Ok(())
}
#[tokio::test]
async fn default_permissions_read_only_applies_additional_writable_roots_as_modifications()
-> std::io::Result<()> {
let codex_home = TempDir::new()?;
let cwd = TempDir::new()?;
let extra_root = TempDir::new()?;
let extra_root = extra_root.path().abs();
let config = Config::load_from_base_config_with_overrides(
ConfigToml {
default_permissions: Some(":read-only".to_string()),
..Default::default()
},
ConfigOverrides {
cwd: Some(cwd.path().to_path_buf()),
additional_writable_roots: vec![extra_root.to_path_buf()],
..Default::default()
},
codex_home.abs(),
)
.await?;
let policy = config.permissions.file_system_sandbox_policy();
assert!(
policy.can_write_path_with_cwd(extra_root.as_path(), cwd.path()),
"expected additional writable root to modify :read-only, policy: {policy:?}"
);
assert_eq!(
config.permissions.active_permission_profile(),
Some(
ActivePermissionProfile::new(":read-only").with_modifications(vec![
ActivePermissionProfileModification::AdditionalWritableRoot { path: extra_root },
])
)
);
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 +1402,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()),
@@ -1367,6 +1478,12 @@ async fn implicit_builtin_workspace_profile_preserves_sandbox_workspace_write_se
config.permissions.network_sandbox_policy(),
NetworkSandboxPolicy::Enabled
);
assert_eq!(
config.permissions.active_permission_profile(),
None,
"implicit :workspace cannot be faithfully re-selected when it includes \
legacy sandbox_workspace_write settings"
);
match config.legacy_sandbox_policy() {
SandboxPolicy::WorkspaceWrite {
writable_roots,
@@ -1484,6 +1601,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(())
}
@@ -6151,6 +6276,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(),
@@ -6346,6 +6472,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(),
@@ -6495,6 +6622,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(),
@@ -6629,6 +6757,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(),
@@ -7518,6 +7647,41 @@ async fn permission_profile_override_falls_back_when_disallowed_by_requirements(
Ok(())
}
#[tokio::test]
async fn active_profile_is_cleared_when_requirements_force_fallback() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let requirements = codex_config::ConfigRequirementsToml {
allowed_sandbox_modes: Some(vec![codex_config::SandboxModeRequirement::ReadOnly]),
..Default::default()
};
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.harness_overrides(ConfigOverrides {
default_permissions: Some(":danger-no-sandbox".to_string()),
..Default::default()
})
.cloud_requirements(CloudRequirementsLoader::new(async move {
Ok(Some(requirements))
}))
.build()
.await?;
assert_eq!(
config.permissions.permission_profile(),
PermissionProfile::read_only()
);
assert_eq!(config.permissions.active_permission_profile(), None);
assert!(
config.startup_warnings.iter().any(|warning| warning
.contains("Configured value for `permission_profile` is disallowed by requirements")),
"{:?}",
config.startup_warnings
);
Ok(())
}
#[tokio::test]
async fn permission_profile_override_preserves_split_write_roots() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
+115 -13
View File
@@ -86,6 +86,8 @@ 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::ActivePermissionProfileModification;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::SandboxEnforcement;
use codex_protocol::openai_models::ModelsResponse;
@@ -105,6 +107,7 @@ use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use crate::config::permissions::BUILT_IN_WORKSPACE_PROFILE;
use crate::config::permissions::builtin_permission_profile;
use crate::config::permissions::compile_permission_profile_selection;
use crate::config::permissions::default_builtin_permission_profile_name;
@@ -229,6 +232,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 or implicit built-in profile selected by config, 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.
@@ -256,6 +262,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()
@@ -314,6 +325,7 @@ impl Permissions {
);
self.permission_profile.set(permission_profile)?;
self.active_permission_profile = None;
Ok(())
}
@@ -321,10 +333,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(())
}
}
@@ -1254,7 +1280,7 @@ fn apply_requirement_constrained_value<T>(
configured_value: T,
constrained_value: &mut ConstrainedWithSource<T>,
startup_warnings: &mut Vec<String>,
) -> std::io::Result<()>
) -> std::io::Result<bool>
where
T: Clone + std::fmt::Debug + Send + Sync,
{
@@ -1279,9 +1305,10 @@ where
),
)
})?;
return Ok(true);
}
Ok(())
Ok(false)
}
fn mcp_server_matches_requirement(
@@ -1681,6 +1708,7 @@ pub struct ConfigOverrides {
pub approvals_reviewer: Option<ApprovalsReviewer>,
pub sandbox_mode: Option<SandboxMode>,
pub permission_profile: Option<PermissionProfile>,
pub default_permissions: Option<String>,
pub model_provider: Option<String>,
pub service_tier: Option<Option<ServiceTier>>,
pub config_profile: Option<String>,
@@ -1945,6 +1973,7 @@ impl Config {
approvals_reviewer: approvals_reviewer_override,
sandbox_mode,
permission_profile,
default_permissions: default_permissions_override,
model_provider,
service_tier: service_tier_override,
config_profile: config_profile_key,
@@ -1969,6 +1998,18 @@ impl Config {
"`sandbox_mode` and `permission_profile` overrides cannot both be set",
));
}
if sandbox_mode.is_some() && default_permissions_override.is_some() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"`sandbox_mode` and `default_permissions` overrides cannot both be set",
));
}
if permission_profile.is_some() && default_permissions_override.is_some() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"`permission_profile` and `default_permissions` overrides cannot both be set",
));
}
let active_profile_name = config_profile_key
.as_ref()
@@ -2040,6 +2081,7 @@ impl Config {
.into_iter()
.map(|path| AbsolutePathBuf::resolve_path_against_base(path, resolved_cwd.as_path()))
.collect();
let requested_additional_writable_roots = additional_writable_roots.clone();
let repo_root = resolve_root_git_project_for_trust(fs, &resolved_cwd).await;
let active_project = cfg
.get_active_project(
@@ -2057,13 +2099,16 @@ impl Config {
.permissions
.as_ref()
.is_some_and(|profiles| !profiles.is_empty());
let default_permissions = default_permissions_override
.as_deref()
.or(cfg.default_permissions.as_deref());
validate_user_permission_profile_names(cfg.permissions.as_ref())?;
if has_permission_profiles
&& !matches!(
permission_config_syntax,
Some(PermissionConfigSyntax::Legacy)
)
&& cfg.default_permissions.is_none()
&& default_permissions.is_none()
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
@@ -2085,15 +2130,19 @@ impl Config {
additional_writable_roots.push(memories_root);
}
let profiles_are_active = matches!(
permission_config_syntax,
Some(PermissionConfigSyntax::Profiles)
) || permission_config_syntax.is_none();
let using_implicit_builtin_profile = permission_config_syntax.is_none();
let profiles_are_active = default_permissions_override.is_some()
|| matches!(
permission_config_syntax,
Some(PermissionConfigSyntax::Profiles)
)
|| permission_config_syntax.is_none();
let using_implicit_builtin_profile =
permission_config_syntax.is_none() && default_permissions.is_none();
let (
configured_network_proxy_config,
permission_profile,
file_system_sandbox_policy,
mut 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();
@@ -2104,7 +2153,7 @@ impl Config {
// PermissionProfile carries the active network sandbox bit, not the configured
// proxy/allowlist policy. Keep that config so active profiles can round-trip
// without broadening network behavior.
let default_permissions = cfg.default_permissions.as_deref().unwrap_or_else(|| {
let default_permissions = default_permissions.unwrap_or_else(|| {
default_builtin_permission_profile_name(
&active_project,
windows_sandbox_level,
@@ -2139,11 +2188,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(|| {
let default_permissions = default_permissions.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,
@@ -2152,12 +2207,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 {
@@ -2188,11 +2243,51 @@ impl Config {
&file_system_sandbox_policy,
network_sandbox_policy,
);
} else if matches!(permission_profile, PermissionProfile::Managed { .. })
&& !requested_additional_writable_roots.is_empty()
{
file_system_sandbox_policy = file_system_sandbox_policy.with_additional_writable_roots(
resolved_cwd.as_path(),
&requested_additional_writable_roots,
);
permission_profile = PermissionProfile::from_runtime_permissions(
&file_system_sandbox_policy,
network_sandbox_policy,
);
}
let active_permission_profile = if using_implicit_builtin_profile
&& default_permissions == BUILT_IN_WORKSPACE_PROFILE
&& cfg.sandbox_workspace_write.is_some()
{
// The implicit built-in profile preserves legacy
// `[sandbox_workspace_write]` customizations, but explicitly
// selecting `:workspace` intentionally ignores those legacy
// settings. Do not advertise a re-selectable active profile
// when doing so would lose roots, network, or tmp settings.
None
} else {
let active_permission_profile = if !requested_additional_writable_roots.is_empty()
&& matches!(permission_profile, PermissionProfile::Managed { .. })
{
ActivePermissionProfile::new(default_permissions).with_modifications(
requested_additional_writable_roots
.iter()
.cloned()
.map(|path| {
ActivePermissionProfileModification::AdditionalWritableRoot { path }
})
.collect(),
)
} else {
ActivePermissionProfile::new(default_permissions)
};
Some(active_permission_profile)
};
(
configured_network_proxy_config,
permission_profile,
file_system_sandbox_policy,
active_permission_profile,
)
} else {
let configured_network_proxy_config = NetworkProxyConfig::default();
@@ -2254,6 +2349,7 @@ impl Config {
configured_network_proxy_config,
permission_profile,
file_system_sandbox_policy,
None,
)
};
let approval_policy_was_explicit = approval_policy_override.is_some()
@@ -2633,12 +2729,17 @@ impl Config {
&mut constrained_approvals_reviewer,
&mut startup_warnings,
)?;
apply_requirement_constrained_value(
let permission_profile_was_constrained = apply_requirement_constrained_value(
"permission_profile",
permission_profile,
&mut constrained_permission_profile,
&mut startup_warnings,
)?;
if permission_profile_was_constrained {
// The selected profile no longer describes the effective
// permissions after requirements forced a fallback.
active_permission_profile = None;
}
apply_requirement_constrained_value(
"web_search_mode",
web_search_mode,
@@ -2721,6 +2822,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,
+3
View File
@@ -161,6 +161,7 @@ pub(super) async fn user_input_or_turn_inner(
approvals_reviewer,
sandbox_policy: Some(sandbox_policy),
permission_profile,
active_permission_profile: None,
windows_sandbox_level: None,
collaboration_mode,
reasoning_summary: summary,
@@ -180,6 +181,7 @@ pub(super) async fn user_input_or_turn_inner(
approvals_reviewer,
sandbox_policy,
permission_profile,
active_permission_profile,
windows_sandbox_level,
model,
effort,
@@ -211,6 +213,7 @@ pub(super) async fn user_input_or_turn_inner(
approvals_reviewer,
sandbox_policy,
permission_profile,
active_permission_profile,
windows_sandbox_level,
collaboration_mode,
reasoning_summary: summary,
+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;
@@ -603,6 +604,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(),
+20
View File
@@ -60,6 +60,9 @@ pub(crate) struct SessionConfiguration {
pub(super) approvals_reviewer: ApprovalsReviewer,
/// Canonical permission profile for the session.
pub(super) permission_profile: Constrained<PermissionProfile>,
/// Named or implicit built-in 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 +100,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 +134,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(),
@@ -206,10 +214,19 @@ impl SessionConfiguration {
}
if let Some(permission_profile) = updates.permission_profile.clone() {
let active_permission_profile =
updates.active_permission_profile.clone().or_else(|| {
if permission_profile == self.permission_profile() {
self.active_permission_profile.clone()
} else {
None
}
});
next_configuration.set_permission_profile_projection(
permission_profile,
Some(&current_file_system_sandbox_policy),
)?;
next_configuration.active_permission_profile = active_permission_profile;
} 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 +242,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
@@ -285,6 +303,7 @@ pub(crate) struct SessionSettingsUpdate {
pub(crate) approvals_reviewer: Option<ApprovalsReviewer>,
pub(crate) sandbox_policy: Option<SandboxPolicy>,
pub(crate) permission_profile: Option<PermissionProfile>,
pub(crate) active_permission_profile: Option<ActivePermissionProfile>,
pub(crate) windows_sandbox_level: Option<WindowsSandboxLevel>,
pub(crate) collaboration_mode: Option<CollaborationMode>,
pub(crate) reasoning_summary: Option<ReasoningSummaryConfig>,
@@ -887,6 +906,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,
+8
View File
@@ -2324,6 +2324,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(),
@@ -2426,6 +2427,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(),
@@ -2873,6 +2875,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(),
@@ -3309,6 +3312,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(),
@@ -3413,6 +3417,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(),
@@ -3625,6 +3630,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(),
@@ -4206,6 +4212,7 @@ fn op_kind_distinguishes_turn_ops() {
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
active_permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
@@ -4839,6 +4846,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(),
+128 -49
View File
@@ -25,6 +25,8 @@ use codex_app_server_protocol::ConfigWarningNotification;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::McpServerElicitationAction;
use codex_app_server_protocol::McpServerElicitationRequestResponse;
use codex_app_server_protocol::PermissionProfileModificationParams;
use codex_app_server_protocol::PermissionProfileSelectionParams;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ReviewStartParams;
use codex_app_server_protocol::ReviewStartResponse;
@@ -76,13 +78,14 @@ use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID;
use codex_otel::set_parent_from_context;
use codex_otel::traceparent_context_from_env;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::ActivePermissionProfileModification;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
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;
@@ -399,6 +402,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
approvals_reviewer: None,
sandbox_mode,
permission_profile: None,
default_permissions: None,
cwd: resolved_cwd,
model_provider: model_provider.clone(),
service_tier: None,
@@ -665,37 +669,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,
@@ -707,10 +698,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.
@@ -745,7 +752,6 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
items,
output_schema,
} => {
let permission_profile = Some(config.permissions.permission_profile().into());
let response: TurnStartResponse = send_request_with_response(
&client,
ClientRequest::TurnStart {
@@ -759,7 +765,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
approval_policy: Some(default_approval_policy.into()),
approvals_reviewer: None,
sandbox_policy: None,
permission_profile,
permissions: None,
model: None,
service_tier: None,
effort: default_effort,
@@ -916,14 +922,21 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
}
fn thread_start_params_from_config(config: &Config) -> ThreadStartParams {
let permissions = permissions_selection_from_config(config);
let sandbox = permissions.is_none().then(|| {
sandbox_mode_from_permission_profile(
&config.permissions.permission_profile(),
config.cwd.as_path(),
)
});
ThreadStartParams {
model: config.model.clone(),
model_provider: Some(config.model_provider_id.clone()),
cwd: Some(config.cwd.to_string_lossy().to_string()),
approval_policy: Some(config.permissions.approval_policy.value().into()),
approvals_reviewer: approvals_reviewer_override_from_config(config),
sandbox: None,
permission_profile: Some(config.permissions.permission_profile().into()),
sandbox: sandbox.flatten(),
permissions,
config: config_request_overrides_from_config(config),
ephemeral: Some(config.ephemeral),
..ThreadStartParams::default()
@@ -931,6 +944,13 @@ fn thread_start_params_from_config(config: &Config) -> ThreadStartParams {
}
fn thread_resume_params_from_config(config: &Config, thread_id: String) -> ThreadResumeParams {
let permissions = permissions_selection_from_config(config);
let sandbox = permissions.is_none().then(|| {
sandbox_mode_from_permission_profile(
&config.permissions.permission_profile(),
config.cwd.as_path(),
)
});
ThreadResumeParams {
thread_id,
model: config.model.clone(),
@@ -938,13 +958,63 @@ fn thread_resume_params_from_config(config: &Config, thread_id: String) -> Threa
cwd: Some(config.cwd.to_string_lossy().to_string()),
approval_policy: Some(config.permissions.approval_policy.value().into()),
approvals_reviewer: approvals_reviewer_override_from_config(config),
sandbox: None,
permission_profile: Some(config.permissions.permission_profile().into()),
sandbox: sandbox.flatten(),
permissions,
config: config_request_overrides_from_config(config),
..ThreadResumeParams::default()
}
}
fn permissions_selection_from_config(config: &Config) -> Option<PermissionProfileSelectionParams> {
config
.permissions
.active_permission_profile()
.map(permissions_selection_from_active_profile)
}
fn permissions_selection_from_active_profile(
active: ActivePermissionProfile,
) -> PermissionProfileSelectionParams {
let modifications = active
.modifications
.into_iter()
.map(|modification| match modification {
ActivePermissionProfileModification::AdditionalWritableRoot { path } => {
PermissionProfileModificationParams::AdditionalWritableRoot { path }
}
})
.collect::<Vec<_>>();
PermissionProfileSelectionParams::Profile {
id: active.id,
modifications: (!modifications.is_empty()).then_some(modifications),
}
}
fn sandbox_mode_from_permission_profile(
permission_profile: &PermissionProfile,
cwd: &Path,
) -> Option<codex_app_server_protocol::SandboxMode> {
match permission_profile {
PermissionProfile::Disabled => {
Some(codex_app_server_protocol::SandboxMode::DangerFullAccess)
}
PermissionProfile::External { .. } => None,
PermissionProfile::Managed { .. } => {
let file_system_policy = permission_profile.file_system_sandbox_policy();
if file_system_policy.has_full_disk_write_access() {
permission_profile
.network_sandbox_policy()
.is_enabled()
.then_some(codex_app_server_protocol::SandboxMode::DangerFullAccess)
} else if file_system_policy.can_write_path_with_cwd(cwd, cwd) {
Some(codex_app_server_protocol::SandboxMode::WorkspaceWrite)
} else {
Some(codex_app_server_protocol::SandboxMode::ReadOnly)
}
}
}
}
fn config_request_overrides_from_config(config: &Config) -> Option<HashMap<String, Value>> {
config
.active_profile
@@ -977,6 +1047,7 @@ where
fn session_configured_from_thread_start_response(
response: &ThreadStartResponse,
config: &Config,
) -> Result<SessionConfiguredEvent, String> {
session_configured_from_thread_response(
&response.thread.id,
@@ -987,8 +1058,12 @@ 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),
response
.permission_profile
.clone()
.map(Into::into)
.unwrap_or_else(|| config.permissions.permission_profile()),
response.active_permission_profile.clone().map(Into::into),
response.cwd.clone(),
response.reasoning_effort,
)
@@ -996,6 +1071,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,
@@ -1006,8 +1082,12 @@ 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),
response
.permission_profile
.clone()
.map(Into::into)
.unwrap_or_else(|| config.permissions.permission_profile()),
response.active_permission_profile.clone().map(Into::into),
response.cwd.clone(),
response.reasoning_effort,
)
@@ -1035,8 +1115,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> {
@@ -1052,9 +1132,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,
+75 -17
View File
@@ -363,8 +363,8 @@ async fn thread_start_params_include_review_policy_when_review_policy_is_manual_
);
assert_eq!(params.sandbox, None);
assert_eq!(
params.permission_profile,
Some(config.permissions.permission_profile().into())
params.permissions,
permissions_selection_from_config(&config)
);
}
@@ -391,9 +391,76 @@ 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() {
let response = ThreadStartResponse {
#[tokio::test]
async fn thread_lifecycle_params_include_legacy_sandbox_when_no_active_profile() {
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())
.harness_overrides(ConfigOverrides {
sandbox_mode: Some(SandboxMode::DangerFullAccess),
..Default::default()
})
.fallback_cwd(Some(cwd.path().to_path_buf()))
.build()
.await
.expect("build config with legacy sandbox override");
let start_params = thread_start_params_from_config(&config);
let resume_params = thread_resume_params_from_config(&config, "thread-id".to_string());
assert_eq!(config.permissions.active_permission_profile(), None);
assert_eq!(
start_params.sandbox,
Some(codex_app_server_protocol::SandboxMode::DangerFullAccess)
);
assert_eq!(start_params.permissions, None);
assert_eq!(
resume_params.sandbox,
Some(codex_app_server_protocol::SandboxMode::DangerFullAccess)
);
assert_eq!(resume_params.permissions, None);
}
#[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 = sample_thread_start_response();
let event = session_configured_from_thread_start_response(&response, &config)
.expect("build bootstrap session configured event");
assert_eq!(event.approvals_reviewer, ApprovalsReviewer::AutoReview);
}
#[tokio::test]
async fn session_configured_from_thread_response_uses_permission_profile_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 mut response = sample_thread_start_response();
response.permission_profile = Some(PermissionProfile::Disabled.into());
let event = session_configured_from_thread_start_response(&response, &config)
.expect("build bootstrap session configured event");
assert_eq!(event.permission_profile, PermissionProfile::Disabled);
}
fn sample_thread_start_response() -> ThreadStartResponse {
ThreadStartResponse {
thread: codex_app_server_protocol::Thread {
id: "67e55044-10b1-426f-9247-bb680e5fe0c8".to_string(),
forked_from_id: None,
@@ -426,17 +493,8 @@ 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(),
),
permission_profile: None,
active_permission_profile: None,
reasoning_effort: None,
};
let event = session_configured_from_thread_start_response(&response)
.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,
+53
View File
@@ -317,6 +317,59 @@ pub enum PermissionProfile {
External { network: NetworkSandboxPolicy },
}
/// Metadata for the named or implicit built-in 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` or the implicit built-in
/// default, 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>,
/// Bounded user-requested modifications applied on top of the named
/// profile, if any.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub modifications: Vec<ActivePermissionProfileModification>,
}
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "snake_case")]
#[ts(tag = "type")]
pub enum ActivePermissionProfileModification {
/// Additional concrete directory that should be writable.
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
AdditionalWritableRoot { path: AbsolutePathBuf },
}
impl ActivePermissionProfile {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
extends: None,
modifications: Vec::new(),
}
}
pub fn with_modifications(
mut self,
modifications: Vec<ActivePermissionProfileModification>,
) -> Self {
self.modifications = modifications;
self
}
}
impl Default for PermissionProfile {
fn default() -> Self {
Self::Managed {
+17
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;
@@ -479,6 +480,12 @@ pub enum Op {
#[serde(skip_serializing_if = "Option::is_none")]
permission_profile: Option<PermissionProfile>,
/// Named or built-in profile that produced `permission_profile`, if
/// the update selected a profile rather than supplying raw
/// permissions.
#[serde(skip_serializing_if = "Option::is_none")]
active_permission_profile: Option<ActivePermissionProfile>,
/// Updated Windows sandbox mode for tool execution.
#[serde(skip_serializing_if = "Option::is_none")]
windows_sandbox_level: Option<WindowsSandboxLevel>,
@@ -3508,6 +3515,12 @@ pub struct SessionConfiguredEvent {
/// Canonical effective permissions for commands executed in the session.
pub permission_profile: PermissionProfile,
/// Named or implicit built-in profile that produced `permission_profile`,
/// when known.
#[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 +3572,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 +3605,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,
@@ -5124,6 +5140,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,
@@ -165,6 +165,7 @@ fn new_config(model: Option<String>, arg0_paths: Arg0DispatchPaths) -> anyhow::R
permissions: Permissions {
approval_policy: Constrained::allow_any(AskForApproval::Never),
permission_profile: Constrained::allow_any(PermissionProfile::default()),
active_permission_profile: None,
network: None,
allow_login_shell: true,
shell_environment_policy: ShellEnvironmentPolicy::default(),
@@ -646,6 +646,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
@@ -3571,6 +3571,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,
@@ -3810,6 +3811,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,
@@ -4322,6 +4324,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,
@@ -4385,6 +4388,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,
@@ -4478,6 +4482,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,
@@ -4864,6 +4869,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,
@@ -4976,6 +4982,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,
+11 -2
View File
@@ -604,15 +604,24 @@ impl App {
}
}
if should_start_turn {
let config = self.chat_widget.config_ref();
let approvals_reviewer =
approvals_reviewer.unwrap_or(config.approvals_reviewer);
let active_permission_profile =
if config.permissions.permission_profile() == permission_profile.clone() {
config.permissions.active_permission_profile()
} else {
None
};
app_server
.turn_start(
thread_id,
items.to_vec(),
cwd.clone(),
approval_policy,
approvals_reviewer
.unwrap_or(self.chat_widget.config_ref().approvals_reviewer),
approvals_reviewer,
permission_profile.clone(),
active_permission_profile,
model.to_string(),
effort,
*summary,
+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,
+309 -99
View File
@@ -29,6 +29,8 @@ use codex_app_server_protocol::MemoryResetResponse;
use codex_app_server_protocol::Model as ApiModel;
use codex_app_server_protocol::ModelListParams;
use codex_app_server_protocol::ModelListResponse;
use codex_app_server_protocol::PermissionProfileModificationParams;
use codex_app_server_protocol::PermissionProfileSelectionParams;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ReviewDelivery;
use codex_app_server_protocol::ReviewStartParams;
@@ -93,6 +95,8 @@ 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::ActivePermissionProfileModification;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ModelAvailabilityNux;
@@ -156,10 +160,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.
///
/// App-server responses may echo the experimental profile payload; when
/// they do, the TUI uses that exact runtime profile. Older/missing
/// responses fall back to the local profile for embedded sessions or to a
/// response-cwd legacy sandbox projection for remote sessions so cached
/// sessions do not reinterpret cwd-bound grants.
pub(crate) permission_profile: PermissionProfile,
/// Named or implicit built-in profile that produced `permission_profile`,
/// when the server knows it.
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 +376,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 +401,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 +430,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)
}
@@ -533,6 +547,7 @@ impl AppServerSession {
approval_policy: AskForApproval,
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer,
permission_profile: PermissionProfile,
active_permission_profile: Option<ActivePermissionProfile>,
model: String,
effort: Option<codex_protocol::openai_models::ReasoningEffort>,
summary: Option<codex_protocol::config_types::ReasoningSummary>,
@@ -542,26 +557,12 @@ impl AppServerSession {
output_schema: Option<serde_json::Value>,
) -> Result<TurnStartResponse> {
let request_id = self.next_request_id();
let sandbox_policy = if matches!(self.thread_params_mode(), ThreadParamsMode::Remote) {
let legacy_profile =
legacy_compatible_permission_profile(&permission_profile, cwd.as_path());
let policy = legacy_profile
.to_legacy_sandbox_policy(cwd.as_path())
.unwrap_or_else(|err| {
unreachable!(
"legacy-compatible permissions must project to legacy policy: {err}"
)
});
Some(policy.into())
} else {
None
};
let permission_profile = if matches!(self.thread_params_mode(), ThreadParamsMode::Embedded)
{
Some(permission_profile.into())
} else {
None
};
let (sandbox_policy, permissions) = turn_permissions_overrides(
&permission_profile,
active_permission_profile,
cwd.as_path(),
self.thread_params_mode(),
);
self.client
.request_typed(ClientRequest::TurnStart {
request_id,
@@ -574,7 +575,7 @@ impl AppServerSession {
approval_policy: Some(approval_policy.into()),
approvals_reviewer: Some(approvals_reviewer.into()),
sandbox_policy,
permission_profile,
permissions,
model: Some(model),
service_tier,
effort,
@@ -1127,15 +1128,64 @@ fn sandbox_mode_from_permission_profile(
}
}
fn permission_profile_override_from_config(
fn permissions_selection_from_active_profile(
active: ActivePermissionProfile,
) -> PermissionProfileSelectionParams {
let modifications = active
.modifications
.into_iter()
.map(|modification| match modification {
ActivePermissionProfileModification::AdditionalWritableRoot { path } => {
PermissionProfileModificationParams::AdditionalWritableRoot { path }
}
})
.collect::<Vec<_>>();
PermissionProfileSelectionParams::Profile {
id: active.id,
modifications: (!modifications.is_empty()).then_some(modifications),
}
}
fn turn_permissions_overrides(
permission_profile: &PermissionProfile,
active_permission_profile: Option<ActivePermissionProfile>,
cwd: &std::path::Path,
thread_params_mode: ThreadParamsMode,
) -> (
Option<codex_app_server_protocol::SandboxPolicy>,
Option<PermissionProfileSelectionParams>,
) {
let permissions = if matches!(thread_params_mode, ThreadParamsMode::Embedded) {
active_permission_profile.map(permissions_selection_from_active_profile)
} else {
None
};
let sandbox_policy = (matches!(thread_params_mode, ThreadParamsMode::Remote)
|| permissions.is_none())
.then(|| {
let legacy_profile = legacy_compatible_permission_profile(permission_profile, cwd);
let policy = legacy_profile
.to_legacy_sandbox_policy(cwd)
.unwrap_or_else(|err| {
unreachable!("legacy-compatible permissions must project to legacy policy: {err}")
});
policy.into()
});
(sandbox_policy, permissions)
}
fn permissions_selection_from_config(
config: &Config,
thread_params_mode: ThreadParamsMode,
) -> Option<codex_app_server_protocol::PermissionProfile> {
) -> Option<PermissionProfileSelectionParams> {
if matches!(thread_params_mode, ThreadParamsMode::Remote) {
return None;
}
Some(config.permissions.permission_profile().into())
config
.permissions
.active_permission_profile()
.map(permissions_selection_from_active_profile)
}
fn thread_start_params_from_config(
@@ -1144,8 +1194,8 @@ fn thread_start_params_from_config(
remote_cwd_override: Option<&std::path::Path>,
session_start_source: Option<ThreadStartSource>,
) -> ThreadStartParams {
let permission_profile = permission_profile_override_from_config(config, thread_params_mode);
let sandbox = permission_profile
let permissions = permissions_selection_from_config(config, thread_params_mode);
let sandbox = permissions
.is_none()
.then(|| {
sandbox_mode_from_permission_profile(
@@ -1161,7 +1211,7 @@ fn thread_start_params_from_config(
approval_policy: Some(config.permissions.approval_policy.value().into()),
approvals_reviewer: approvals_reviewer_override_from_config(config),
sandbox,
permission_profile,
permissions,
config: config_request_overrides_from_config(config),
ephemeral: Some(config.ephemeral),
session_start_source,
@@ -1176,8 +1226,8 @@ fn thread_resume_params_from_config(
thread_params_mode: ThreadParamsMode,
remote_cwd_override: Option<&std::path::Path>,
) -> ThreadResumeParams {
let permission_profile = permission_profile_override_from_config(&config, thread_params_mode);
let sandbox = permission_profile
let permissions = permissions_selection_from_config(&config, thread_params_mode);
let sandbox = permissions
.is_none()
.then(|| {
sandbox_mode_from_permission_profile(
@@ -1194,7 +1244,7 @@ fn thread_resume_params_from_config(
approval_policy: Some(config.permissions.approval_policy.value().into()),
approvals_reviewer: approvals_reviewer_override_from_config(&config),
sandbox,
permission_profile,
permissions,
config: config_request_overrides_from_config(&config),
persist_extended_history: true,
..ThreadResumeParams::default()
@@ -1207,8 +1257,8 @@ fn thread_fork_params_from_config(
thread_params_mode: ThreadParamsMode,
remote_cwd_override: Option<&std::path::Path>,
) -> ThreadForkParams {
let permission_profile = permission_profile_override_from_config(&config, thread_params_mode);
let sandbox = permission_profile
let permissions = permissions_selection_from_config(&config, thread_params_mode);
let sandbox = permissions
.is_none()
.then(|| {
sandbox_mode_from_permission_profile(
@@ -1225,7 +1275,7 @@ fn thread_fork_params_from_config(
approval_policy: Some(config.permissions.approval_policy.value().into()),
approvals_reviewer: approvals_reviewer_override_from_config(&config),
sandbox,
permission_profile,
permissions,
config: config_request_overrides_from_config(&config),
base_instructions: config.base_instructions.clone(),
developer_instructions: config.developer_instructions.clone(),
@@ -1251,10 +1301,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 +1316,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 +1331,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 +1346,15 @@ 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.permission_profile.as_ref(),
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 +1365,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 +1378,15 @@ 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.permission_profile.as_ref(),
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 +1397,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 +1410,15 @@ 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.permission_profile.as_ref(),
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 +1429,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 +1439,24 @@ async fn thread_session_state_from_thread_fork_response(
.await
}
fn permission_profile_from_thread_response(
sandbox: &codex_app_server_protocol::SandboxPolicy,
permission_profile: Option<&codex_app_server_protocol::PermissionProfile>,
cwd: &std::path::Path,
config: &Config,
thread_params_mode: ThreadParamsMode,
) -> PermissionProfile {
if let Some(permission_profile) = permission_profile {
return permission_profile.clone().into();
}
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 +1491,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 +1517,7 @@ async fn thread_session_state_from_thread_response(
approval_policy,
approvals_reviewer,
permission_profile,
active_permission_profile,
cwd,
instruction_source_paths,
reasoning_effort,
@@ -1505,6 +1581,7 @@ fn app_server_credits_snapshot_to_core(
mod tests {
use super::*;
use crate::legacy_core::config::ConfigBuilder;
use crate::legacy_core::config::ConfigOverrides;
use codex_app_server_protocol::ThreadStatus;
use codex_app_server_protocol::Turn;
use codex_app_server_protocol::TurnStatus;
@@ -1530,7 +1607,15 @@ mod tests {
#[tokio::test]
async fn thread_start_params_include_cwd_for_embedded_sessions() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let config = build_config(&temp_dir).await;
let config = ConfigBuilder::default()
.codex_home(temp_dir.path().to_path_buf())
.harness_overrides(ConfigOverrides {
default_permissions: Some(":workspace".to_string()),
..ConfigOverrides::default()
})
.build()
.await
.expect("config should build");
let params = thread_start_params_from_config(
&config,
@@ -1542,8 +1627,11 @@ mod tests {
assert_eq!(params.cwd, Some(config.cwd.to_string_lossy().to_string()));
assert_eq!(params.sandbox, None);
assert_eq!(
params.permission_profile,
Some(config.permissions.permission_profile().into())
params.permissions,
config
.permissions
.active_permission_profile()
.map(permissions_selection_from_active_profile)
);
assert_eq!(params.model_provider, Some(config.model_provider_id));
}
@@ -1563,6 +1651,64 @@ mod tests {
assert_eq!(params.session_start_source, Some(ThreadStartSource::Clear));
}
#[test]
fn embedded_turn_permissions_use_active_profile_selection() {
let cwd = test_path_buf("/workspace/project").abs();
let active_permission_profile = ActivePermissionProfile::new(":workspace");
let expected_permissions =
permissions_selection_from_active_profile(active_permission_profile.clone());
let (sandbox_policy, permissions) = turn_permissions_overrides(
&PermissionProfile::workspace_write(),
Some(active_permission_profile),
cwd.as_path(),
ThreadParamsMode::Embedded,
);
assert_eq!(sandbox_policy, None);
assert_eq!(permissions, Some(expected_permissions));
}
#[test]
fn embedded_turn_permissions_fall_back_to_sandbox_without_active_profile() {
let cwd = test_path_buf("/workspace/project").abs();
let (sandbox_policy, permissions) = turn_permissions_overrides(
&PermissionProfile::read_only(),
/*active_permission_profile*/ None,
cwd.as_path(),
ThreadParamsMode::Embedded,
);
assert_eq!(
sandbox_policy,
Some(codex_app_server_protocol::SandboxPolicy::ReadOnly {
network_access: false
})
);
assert_eq!(permissions, None);
}
#[test]
fn remote_turn_permissions_use_sandbox_even_with_active_profile() {
let cwd = test_path_buf("/workspace/project").abs();
let (sandbox_policy, permissions) = turn_permissions_overrides(
&PermissionProfile::read_only(),
Some(ActivePermissionProfile::new(":read-only")),
cwd.as_path(),
ThreadParamsMode::Remote,
);
assert_eq!(
sandbox_policy,
Some(codex_app_server_protocol::SandboxPolicy::ReadOnly {
network_access: false
})
);
assert_eq!(permissions, None);
}
#[tokio::test]
async fn thread_lifecycle_params_omit_cwd_without_remote_override_for_remote_sessions() {
let temp_dir = tempfile::tempdir().expect("tempdir");
@@ -1601,9 +1747,9 @@ mod tests {
assert_eq!(start.sandbox, expected_sandbox);
assert_eq!(resume.sandbox, expected_sandbox);
assert_eq!(fork.sandbox, expected_sandbox);
assert_eq!(start.permission_profile, None);
assert_eq!(resume.permission_profile, None);
assert_eq!(fork.permission_profile, None);
assert_eq!(start.permissions, None);
assert_eq!(resume.permissions, None);
assert_eq!(fork.permissions, None);
}
#[test]
@@ -1704,9 +1850,9 @@ mod tests {
assert_eq!(start.sandbox, expected_sandbox);
assert_eq!(resume.sandbox, expected_sandbox);
assert_eq!(fork.sandbox, expected_sandbox);
assert_eq!(start.permission_profile, None);
assert_eq!(resume.permission_profile, None);
assert_eq!(fork.permission_profile, None);
assert_eq!(start.permissions, None);
assert_eq!(resume.permissions, None);
assert_eq!(fork.permissions, None);
}
#[tokio::test]
@@ -1791,30 +1937,92 @@ 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()),
permission_profile: Some(read_only_profile.clone().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,
response.instruction_sources
);
assert_eq!(
started.session.permission_profile,
response
.permission_profile
.clone()
.map(Into::into)
.expect("response includes profile")
);
assert_eq!(started.session.permission_profile, read_only_profile);
assert_eq!(started.turns.len(), 1);
assert_eq!(started.turns[0], response.thread.turns[0]);
}
#[tokio::test]
async fn remote_thread_response_prefers_permission_profile_over_legacy_sandbox() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let config = build_config(&temp_dir).await;
let cwd = test_path_buf("/tmp/project").abs();
let fallback_sandbox = PermissionProfile::read_only()
.to_legacy_sandbox_policy(cwd.as_path())
.expect("read-only profile must be legacy-compatible")
.into();
let split_profile = PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Restricted {
entries: vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Read,
},
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::ProjectRoots {
subpath: Some(".env".into()),
},
},
access: FileSystemAccessMode::None,
},
],
glob_scan_max_depth: None,
},
network: NetworkSandboxPolicy::Restricted,
};
let response_profile = split_profile.clone().into();
assert_eq!(
permission_profile_from_thread_response(
&fallback_sandbox,
Some(&response_profile),
cwd.as_path(),
&config,
ThreadParamsMode::Remote,
),
split_profile
);
}
#[tokio::test]
async fn embedded_thread_response_prefers_permission_profile_when_present() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let config = build_config(&temp_dir).await;
let cwd = test_path_buf("/tmp/project").abs();
let response_profile = PermissionProfile::read_only().into();
assert_eq!(
permission_profile_from_thread_response(
&codex_app_server_protocol::SandboxPolicy::DangerFullAccess,
Some(&response_profile),
cwd.as_path(),
&config,
ThreadParamsMode::Embedded,
),
PermissionProfile::read_only()
);
}
#[tokio::test]
async fn session_configured_populates_history_metadata() {
let temp_dir = tempfile::tempdir().expect("tempdir");
@@ -1839,6 +2047,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 +2078,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
@@ -1645,6 +1645,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,
@@ -2377,11 +2378,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,
+106 -13
View File
@@ -9,6 +9,9 @@ use chrono::Local;
use codex_model_provider_info::WireApi;
use codex_protocol::ThreadId;
use codex_protocol::account::PlanType;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::ActivePermissionProfileModification;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AskForApproval;
@@ -281,18 +284,20 @@ 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 approval = status_approval_label(
config.permissions.approval_policy.value(),
config.approvals_reviewer,
&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);
@@ -537,11 +542,17 @@ impl StatusHistoryCell {
fn status_permission_summary(permission_profile: &PermissionProfile, cwd: &Path) -> String {
let summary = summarize_permission_profile(permission_profile, cwd);
if let Some(details) = summary.strip_prefix("read-only") {
if details.contains("(network access enabled)") {
return "read-only with network access".to_string();
}
return "read-only".to_string();
}
if let Some(details) = summary.strip_prefix("workspace-write") {
if details.contains("(network access enabled)") {
return "workspace-write with network access".to_string();
return "workspace with network access".to_string();
}
return "workspace-write".to_string();
return "workspace".to_string();
}
if summary == "custom permissions (network access enabled)" {
return "custom permissions with network access".to_string();
@@ -549,6 +560,88 @@ 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 {
let active_id = active_permission_profile.map(|active| active.id.as_str());
let writable_root_modifications = active_permission_profile
.map(|active| {
active
.modifications
.iter()
.filter(|modification| {
matches!(
modification,
ActivePermissionProfileModification::AdditionalWritableRoot { .. }
)
})
.count()
})
.unwrap_or(0);
let modification_suffix = match writable_root_modifications {
0 => String::new(),
1 => " + 1 writable root".to_string(),
count => format!(" + {count} writable roots"),
};
match active_id {
Some(":read-only") => {
let label = if sandbox == "read-only with network access" {
"Read Only with network access"
} else {
"Read Only"
};
return format!("{label}{modification_suffix} ({approval})");
}
Some(":workspace") => match sandbox {
"workspace" => return format!("Workspace{modification_suffix} ({approval})"),
"workspace with network access" => {
return format!("Workspace with network access{modification_suffix} ({approval})");
}
_ => {}
},
Some(":danger-no-sandbox") if permission_profile == &PermissionProfile::Disabled => {
return if approval_policy == AskForApproval::Never {
"Full Access".to_string()
} else {
format!("No Sandbox ({approval})")
};
}
Some(id) => return format!("Profile {id}{modification_suffix} ({sandbox}, {approval})"),
None => {}
}
if sandbox == "read-only" {
return format!("Read Only ({approval})");
}
if approval_policy == AskForApproval::OnRequest && sandbox == "workspace" {
return format!("Workspace ({approval})");
}
if approval_policy == AskForApproval::Never
&& permission_profile == &PermissionProfile::Disabled
{
return "Full Access".to_string();
}
format!("Custom ({sandbox}, {approval})")
}
fn status_approval_label(
approval_policy: AskForApproval,
approvals_reviewer: ApprovalsReviewer,
approval: &str,
) -> String {
if approval_policy == AskForApproval::OnRequest
&& approvals_reviewer == ApprovalsReviewer::AutoReview
{
"auto-review".to_string()
} else {
approval.to_string()
}
}
impl HistoryCell for StatusHistoryCell {
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
let mut lines: Vec<Line<'static>> = Vec::new();
@@ -4,20 +4,20 @@ expression: sanitized
---
/status
╭─────────────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (workspace-write with network access, on-request) │
│ Agents.md: <none> │
│ │
│ Token usage: 1.05K total (700 input + 350 output) │
│ Context window: 100% left (1.45K used / 272K) │
│ 5h limit: [████████░░░░░░░░░░░░] 40% left (resets 11:32) │
│ Weekly limit: [█████████████░░░░░░░] 65% left (resets 11:52) │
│ Warning: limits may be stale - start new turn to refresh. │
╰─────────────────────────────────────────────────────────────────────────────╯
╭───────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (workspace with network access, on-request) │
│ Agents.md: <none> │
│ │
│ Token usage: 1.05K total (700 input + 350 output) │
│ Context window: 100% left (1.45K used / 272K) │
│ 5h limit: [████████░░░░░░░░░░░░] 40% left (resets 11:32) │
│ Weekly limit: [█████████████░░░░░░░] 65% left (resets 11:52) │
│ Warning: limits may be stale - start new turn to refresh. │
╰───────────────────────────────────────────────────────────────────────╯
@@ -4,20 +4,20 @@ expression: sanitized
---
/status
╭─────────────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (workspace-write with network access, on-request) │
│ Agents.md: <none> │
│ │
│ Token usage: 2K total (1.4K input + 600 output) │
│ Context window: 100% left (2.2K used / 272K) │
│ 5h limit: [███████████░░░░░░░░░] 55% left (resets 09:25) │
│ Weekly limit: [██████████████░░░░░░] 70% left (resets 09:55) │
│ Credits: 38 credits │
╰─────────────────────────────────────────────────────────────────────────────╯
╭───────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (workspace with network access, on-request) │
│ Agents.md: <none> │
│ │
│ Token usage: 2K total (1.4K input + 600 output) │
│ Context window: 100% left (2.2K used / 272K) │
│ 5h limit: [███████████░░░░░░░░░] 55% left (resets 09:25) │
│ Weekly limit: [██████████████░░░░░░] 70% left (resets 09:55) │
│ Credits: 38 credits │
╰───────────────────────────────────────────────────────────────────────╯
@@ -4,20 +4,20 @@ expression: sanitized
---
/status
╭─────────────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (workspace-write with network access, on-request) │
│ Agents.md: <none> │
│ Session: 0f0f3c13-6cf9-4aa4-8b80-7d49c2f1be2e │
│ Forked from: e9f18a88-8081-4e51-9d4e-8af5cde2d8dd │
│ │
│ Token usage: 1.2K total (800 input + 400 output) │
│ Context window: 100% left (1.2K used / 272K) │
│ Limits: data not available yet │
╰─────────────────────────────────────────────────────────────────────────────╯
╭───────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (workspace with network access, on-request) │
│ Agents.md: <none> │
│ Session: 0f0f3c13-6cf9-4aa4-8b80-7d49c2f1be2e │
│ Forked from: e9f18a88-8081-4e51-9d4e-8af5cde2d8dd │
│ │
│ Token usage: 1.2K total (800 input + 400 output) │
│ Context window: 100% left (1.2K used / 272K) │
│ Limits: data not available yet │
╰───────────────────────────────────────────────────────────────────────╯
@@ -4,18 +4,18 @@ expression: sanitized
---
/status
╭─────────────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (workspace-write with network access, on-request) │
│ Agents.md: <none> │
│ │
│ Token usage: 1.2K total (800 input + 400 output) │
│ Context window: 100% left (1.2K used / 272K) │
│ Monthly limit: [██████████████████░░] 88% left (resets 07:08 on 7 May) │
╰─────────────────────────────────────────────────────────────────────────────╯
╭────────────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (workspace with network access, on-request) │
│ Agents.md: <none> │
│ │
│ Token usage: 1.2K total (800 input + 400 output) │
│ Context window: 100% left (1.2K used / 272K) │
│ Monthly limit: [██████████████████░░] 88% left (resets 07:08 on 7 May) │
╰────────────────────────────────────────────────────────────────────────────╯
@@ -12,7 +12,7 @@ expression: sanitized
│ │
│ Model: gpt-5.1-codex-max (reasoning high, summaries detailed) │
│ Directory: [[workspace]] │
│ Permissions: Default │
│ Permissions: Workspace (on-request) │
│ Agents.md: <none> │
│ │
│ Token usage: 1.9K total (1K input + 900 output) │
@@ -0,0 +1,21 @@
---
source: tui/src/status/tests.rs
expression: sanitized
---
/status
╭───────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Profile locked (read-only, on-request) │
│ Agents.md: <none> │
│ │
│ Token usage: 0 total (0 input + 0 output) │
│ Context window: 100% left (0 used / 272K) │
│ Limits: data not available yet │
╰───────────────────────────────────────────────────────────────────────╯
@@ -0,0 +1,21 @@
---
source: tui/src/status/tests.rs
expression: sanitized
---
/status
╭───────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Workspace (auto-review) │
│ Agents.md: <none> │
│ │
│ Token usage: 0 total (0 input + 0 output) │
│ Context window: 100% left (0 used / 272K) │
│ Limits: data not available yet │
╰───────────────────────────────────────────────────────────────────────╯
@@ -4,18 +4,18 @@ expression: sanitized
---
/status
╭─────────────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (workspace-write with network access, on-request) │
│ Agents.md: <none> │
│ │
│ Token usage: 750 total (500 input + 250 output) │
│ Context window: 100% left (750 used / 272K) │
│ Limits: data not available yet │
╰─────────────────────────────────────────────────────────────────────────────╯
╭───────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (workspace with network access, on-request) │
│ Agents.md: <none> │
│ │
│ Token usage: 750 total (500 input + 250 output) │
│ Context window: 100% left (750 used / 272K) │
│ Limits: data not available yet │
╰───────────────────────────────────────────────────────────────────────╯
@@ -4,19 +4,19 @@ expression: sanitized
---
/status
╭─────────────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (workspace-write with network access, on-request) │
│ Agents.md: <none> │
│ │
│ Token usage: 750 total (500 input + 250 output) │
│ Context window: 100% left (750 used / 272K) │
│ 5h limit: [███████████░░░░░░░░░] 55% left (resets 08:24) │
│ Weekly limit: [██████████████░░░░░░] 70% left (resets 08:54) │
╰─────────────────────────────────────────────────────────────────────────────╯
╭───────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (workspace with network access, on-request) │
│ Agents.md: <none> │
│ │
│ Token usage: 750 total (500 input + 250 output) │
│ Context window: 100% left (750 used / 272K) │
│ 5h limit: [███████████░░░░░░░░░] 55% left (resets 08:24) │
│ Weekly limit: [██████████████░░░░░░] 70% left (resets 08:54) │
╰───────────────────────────────────────────────────────────────────────╯
@@ -4,20 +4,20 @@ expression: sanitized
---
/status
╭─────────────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (workspace-write with network access, on-request) │
│ Agents.md: <none> │
│ │
│ Token usage: 1.9K total (1K input + 900 output) │
│ Context window: 100% left (2.25K used / 272K) │
│ 5h limit: [██████░░░░░░░░░░░░░░] 28% left (resets 03:14) │
│ Weekly limit: [████████████░░░░░░░░] 60% left (resets 03:34) │
│ Warning: limits may be stale - start new turn to refresh. │
╰─────────────────────────────────────────────────────────────────────────────╯
╭───────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (workspace with network access, on-request) │
│ Agents.md: <none> │
│ │
│ Token usage: 1.9K total (1K input + 900 output) │
│ Context window: 100% left (2.25K used / 272K) │
│ 5h limit: [██████░░░░░░░░░░░░░░] 28% left (resets 03:14) │
│ Weekly limit: [████████████░░░░░░░░] 60% left (resets 03:34) │
│ Warning: limits may be stale - start new turn to refresh. │
╰───────────────────────────────────────────────────────────────────────╯
@@ -4,18 +4,18 @@ expression: sanitized
---
/status
╭─────────────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (workspace-write with network access, on-request) │
│ Agents.md: <none> │
│ │
│ Token usage: 750 total (500 input + 250 output) │
│ Context window: 100% left (750 used / 272K) │
│ Limits: not available for this account │
╰─────────────────────────────────────────────────────────────────────────────╯
╭───────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (workspace with network access, on-request) │
│ Agents.md: <none> │
│ │
│ Token usage: 750 total (500 input + 250 output) │
│ Context window: 100% left (750 used / 272K) │
│ Limits: not available for this account │
╰───────────────────────────────────────────────────────────────────────╯
@@ -4,18 +4,18 @@ expression: sanitized
---
/status
╭─────────────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (workspace-write with network access, on-request) │
│ Agents.md: <none> │
│ │
│ Token usage: 750 total (500 input + 250 output) │
│ Context window: 100% left (750 used / 272K) │
│ Limits: not available for this account │
╰─────────────────────────────────────────────────────────────────────────────╯
╭───────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex-max (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (workspace with network access, on-request) │
│ Agents.md: <none> │
│ │
│ Token usage: 750 total (500 input + 250 output) │
│ Context window: 100% left (750 used / 272K) │
│ Limits: not available for this account │
╰───────────────────────────────────────────────────────────────────────╯
@@ -12,7 +12,7 @@ expression: sanitized
│ │
│ Model: gpt-5.1-codex-max (reasoning high, summaries de │
│ Directory: [[workspace]] │
│ Permissions: Custom (workspace-write with network access, on │
│ Permissions: Custom (workspace with network access, on-reque │
│ Agents.md: <none> │
│ │
│ Token usage: 1.9K total (1K input + 900 output) │
@@ -4,18 +4,18 @@ expression: sanitized
---
/status
╭─────────────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex-max (reasoning medium, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (workspace-write with network access, on-request) │
│ Agents.md: <none> │
│ │
│ Token usage: 750 total (500 input + 250 output) │
│ Context window: 100% left (750 used / 272K) │
│ Limits: data not available yet │
╰─────────────────────────────────────────────────────────────────────────────╯
╭─────────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.1-codex-max (reasoning medium, summaries auto) │
│ Directory: [[workspace]] │
│ Permissions: Custom (workspace with network access, on-request) │
│ Agents.md: <none> │
│ │
│ Token usage: 750 total (500 input + 250 output) │
│ Context window: 100% left (750 used / 272K) │
│ Limits: data not available yet │
╰─────────────────────────────────────────────────────────────────────────╯
+287 -2
View File
@@ -12,7 +12,10 @@ use chrono::Duration as ChronoDuration;
use chrono::TimeZone;
use chrono::Utc;
use codex_protocol::ThreadId;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::ActivePermissionProfileModification;
use codex_protocol::models::ManagedFileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ReasoningEffort;
@@ -212,7 +215,7 @@ async fn status_snapshot_includes_reasoning_details() {
}
#[tokio::test]
async fn status_permissions_non_default_workspace_write_is_custom() {
async fn status_permissions_non_default_workspace_write_uses_workspace_label() {
let temp_home = TempDir::new().expect("temp home");
let mut config = test_config(&temp_home).await;
config.model = Some("gpt-5.1-codex-max".to_string());
@@ -235,10 +238,292 @@ async fn status_permissions_non_default_workspace_write_is_custom() {
assert_eq!(
permissions_text_for(&config).as_deref(),
Some("Custom (workspace-write with network access, on-request)")
Some("Custom (workspace with network access, on-request)")
);
}
#[tokio::test]
async fn status_permissions_named_read_only_profile_shows_builtin_label() {
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::read_only(),
Some(ActivePermissionProfile::new(":read-only")),
)
.expect("set permission profile");
assert_eq!(
permissions_text_for(&config).as_deref(),
Some("Read Only (on-request)")
);
}
#[tokio::test]
async fn status_permissions_read_only_profile_shows_additional_writable_roots() {
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");
let extra_root = test_path_buf("/workspace/extra").abs();
let file_system_policy = PermissionProfile::read_only()
.file_system_sandbox_policy()
.with_additional_writable_roots(config.cwd.as_path(), std::slice::from_ref(&extra_root));
config
.permissions
.set_permission_profile_with_active_profile(
PermissionProfile::from_runtime_permissions(
&file_system_policy,
NetworkSandboxPolicy::Restricted,
),
Some(
ActivePermissionProfile::new(":read-only").with_modifications(vec![
ActivePermissionProfileModification::AdditionalWritableRoot {
path: extra_root,
},
]),
),
)
.expect("set permission profile");
assert_eq!(
permissions_text_for(&config).as_deref(),
Some("Read Only + 1 writable root (on-request)")
);
}
#[tokio::test]
async fn status_permissions_named_workspace_profile_shows_builtin_label() {
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("Workspace (on-request)")
);
}
#[tokio::test]
async fn status_permissions_workspace_auto_review_shows_reviewer_label() {
let temp_home = TempDir::new().expect("temp home");
let mut config = test_config(&temp_home).await;
config.approvals_reviewer = ApprovalsReviewer::AutoReview;
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("Workspace (auto-review)")
);
}
#[tokio::test]
async fn status_permissions_named_profile_shows_additional_writable_roots() {
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");
let extra_root = test_path_buf("/workspace/extra").abs();
config
.permissions
.set_permission_profile_with_active_profile(
PermissionProfile::workspace_write_with(
std::slice::from_ref(&extra_root),
NetworkSandboxPolicy::Restricted,
/*exclude_tmpdir_env_var*/ false,
/*exclude_slash_tmp*/ false,
),
Some(
ActivePermissionProfile::new(":workspace").with_modifications(vec![
ActivePermissionProfileModification::AdditionalWritableRoot {
path: extra_root,
},
]),
),
)
.expect("set permission profile");
assert_eq!(
permissions_text_for(&config).as_deref(),
Some("Workspace + 1 writable root (on-request)")
);
}
#[tokio::test]
async fn status_permissions_broadened_workspace_profile_shows_builtin_label() {
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("Workspace 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_snapshot_shows_active_user_defined_profile() {
let temp_home = TempDir::new().expect("temp home");
let mut config = test_config(&temp_home).await;
config.model = Some("gpt-5.1-codex-max".to_string());
config.cwd = test_path_buf("/workspace/tests").abs();
config
.permissions
.set_permission_profile_with_active_profile(
PermissionProfile::read_only(),
Some(ActivePermissionProfile::new("locked")),
)
.expect("set permission profile");
let usage = TokenUsage::default();
let captured_at = chrono::Local
.with_ymd_and_hms(2024, 1, 2, 3, 4, 5)
.single()
.expect("timestamp");
let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
test_status_account_display().as_ref(),
Some(&token_info),
&usage,
&None,
/*thread_name*/ None,
/*forked_from*/ None,
/*rate_limits*/ None,
None,
captured_at,
&model_slug,
/*collaboration_mode*/ None,
/*reasoning_effort_override*/ None,
);
let mut rendered_lines = render_lines(&composite.display_lines(/*width*/ 80));
if cfg!(windows) {
for line in &mut rendered_lines {
*line = line.replace('\\', "/");
}
}
let sanitized = sanitize_directory(rendered_lines).join("\n");
assert_snapshot!(sanitized);
}
#[tokio::test]
async fn status_snapshot_shows_auto_review_permissions() {
let temp_home = TempDir::new().expect("temp home");
let mut config = test_config(&temp_home).await;
config.model = Some("gpt-5.1-codex-max".to_string());
config.cwd = test_path_buf("/workspace/tests").abs();
config.approvals_reviewer = ApprovalsReviewer::AutoReview;
config
.permissions
.set_permission_profile_with_active_profile(
PermissionProfile::workspace_write(),
Some(ActivePermissionProfile::new(":workspace")),
)
.expect("set permission profile");
let usage = TokenUsage::default();
let captured_at = chrono::Local
.with_ymd_and_hms(2024, 1, 2, 3, 4, 5)
.single()
.expect("timestamp");
let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
let token_info = token_info_for(&model_slug, &config, &usage);
let composite = new_status_output(
&config,
test_status_account_display().as_ref(),
Some(&token_info),
&usage,
&None,
/*thread_name*/ None,
/*forked_from*/ None,
/*rate_limits*/ None,
None,
captured_at,
&model_slug,
/*collaboration_mode*/ None,
/*reasoning_effort_override*/ None,
);
let mut rendered_lines = render_lines(&composite.display_lines(/*width*/ 80));
if cfg!(windows) {
for line in &mut rendered_lines {
*line = line.replace('\\', "/");
}
}
let sanitized = sanitize_directory(rendered_lines).join("\n");
assert_snapshot!(sanitized);
}
#[tokio::test]
async fn status_permissions_full_disk_managed_with_network_is_danger_full_access() {
let temp_home = TempDir::new().expect("temp home");