permissions: move workspace roots onto thread state

This commit is contained in:
Michael Bolin
2026-05-12 15:40:08 -07:00
parent 522e00e341
commit 0e434f4d02
156 changed files with 3774 additions and 3181 deletions
+3
View File
@@ -2698,6 +2698,7 @@ dependencies = [
"codex-utils-cargo-bin",
"codex-utils-cli",
"codex-utils-oss",
"codex-utils-sandbox-summary",
"core_test_support",
"libc",
"opentelemetry",
@@ -3495,6 +3496,7 @@ dependencies = [
"codex-otel",
"codex-protocol",
"codex-state",
"codex-utils-absolute-path",
"codex-utils-path",
"codex-utils-string",
"pretty_assertions",
@@ -3687,6 +3689,7 @@ dependencies = [
"codex-protocol",
"codex-rollout",
"codex-state",
"codex-utils-absolute-path",
"pretty_assertions",
"serde",
"serde_json",
@@ -201,11 +201,11 @@ fn sample_thread_start_response(
model_provider: "openai".to_string(),
service_tier: None,
cwd: test_path_buf("/tmp").abs(),
workspace_roots: Vec::new(),
instruction_sources: Vec::new(),
approval_policy: AppServerAskForApproval::OnFailure,
approvals_reviewer: AppServerApprovalsReviewer::User,
sandbox: AppServerSandboxPolicy::DangerFullAccess,
permission_profile: None,
active_permission_profile: None,
reasoning_effort: None,
})
@@ -257,11 +257,11 @@ fn sample_thread_resume_response_with_source(
model_provider: "openai".to_string(),
service_tier: None,
cwd: test_path_buf("/tmp").abs(),
workspace_roots: Vec::new(),
instruction_sources: Vec::new(),
approval_policy: AppServerAskForApproval::OnFailure,
approvals_reviewer: AppServerApprovalsReviewer::User,
sandbox: AppServerSandboxPolicy::DangerFullAccess,
permission_profile: None,
active_permission_profile: None,
reasoning_effort: None,
})
+3 -9
View File
@@ -12,7 +12,6 @@ use codex_app_server_protocol::ApprovalsReviewer as AppServerApprovalsReviewer;
use codex_app_server_protocol::AskForApproval as AppServerAskForApproval;
use codex_app_server_protocol::ClientRequest;
use codex_app_server_protocol::ClientResponsePayload;
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::SessionSource as AppServerSessionSource;
@@ -29,7 +28,6 @@ use codex_app_server_protocol::TurnStartResponse;
use codex_app_server_protocol::TurnStatus as AppServerTurnStatus;
use codex_app_server_protocol::TurnSteerParams;
use codex_app_server_protocol::TurnSteerResponse;
use codex_protocol::models::PermissionProfile as CorePermissionProfile;
use codex_utils_absolute_path::test_support::PathBufExt;
use codex_utils_absolute_path::test_support::test_path_buf;
use std::collections::HashSet;
@@ -142,10 +140,6 @@ fn sample_thread(thread_id: &str) -> Thread {
}
}
fn sample_permission_profile() -> AppServerPermissionProfile {
CorePermissionProfile::Disabled.into()
}
fn sample_thread_start_response() -> ClientResponsePayload {
ClientResponsePayload::ThreadStart(ThreadStartResponse {
thread: sample_thread("thread-1"),
@@ -153,11 +147,11 @@ fn sample_thread_start_response() -> ClientResponsePayload {
model_provider: "openai".to_string(),
service_tier: None,
cwd: test_path_buf("/tmp").abs(),
workspace_roots: Vec::new(),
instruction_sources: Vec::new(),
approval_policy: AppServerAskForApproval::OnFailure,
approvals_reviewer: AppServerApprovalsReviewer::User,
sandbox: AppServerSandboxPolicy::DangerFullAccess,
permission_profile: Some(sample_permission_profile()),
active_permission_profile: None,
reasoning_effort: None,
})
@@ -170,11 +164,11 @@ fn sample_thread_resume_response() -> ClientResponsePayload {
model_provider: "openai".to_string(),
service_tier: None,
cwd: test_path_buf("/tmp").abs(),
workspace_roots: Vec::new(),
instruction_sources: Vec::new(),
approval_policy: AppServerAskForApproval::OnFailure,
approvals_reviewer: AppServerApprovalsReviewer::User,
sandbox: AppServerSandboxPolicy::DangerFullAccess,
permission_profile: Some(sample_permission_profile()),
active_permission_profile: None,
reasoning_effort: None,
})
@@ -187,11 +181,11 @@ fn sample_thread_fork_response() -> ClientResponsePayload {
model_provider: "openai".to_string(),
service_tier: None,
cwd: test_path_buf("/tmp").abs(),
workspace_roots: Vec::new(),
instruction_sources: Vec::new(),
approval_policy: AppServerAskForApproval::OnFailure,
approvals_reviewer: AppServerApprovalsReviewer::User,
sandbox: AppServerSandboxPolicy::DangerFullAccess,
permission_profile: Some(sample_permission_profile()),
active_permission_profile: None,
reasoning_effort: None,
})
+5 -69
View File
@@ -1850,31 +1850,6 @@
}
]
},
"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": {
@@ -1886,40 +1861,6 @@
],
"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",
@@ -3133,13 +3074,6 @@
],
"title": "WorkspaceWriteSandboxPolicyType",
"type": "string"
},
"writableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
}
},
"required": [
@@ -3406,7 +3340,8 @@
{
"type": "null"
}
]
],
"description": "Deprecated for fork. When present, the server treats this as a compatibility spelling for selecting a matching named permissions profile."
},
"serviceTier": {
"type": [
@@ -3817,7 +3752,8 @@
{
"type": "null"
}
]
],
"description": "Deprecated for resume. When present, the server treats this as a compatibility spelling for selecting a matching named permissions profile."
},
"serviceTier": {
"type": [
@@ -4197,7 +4133,7 @@
"type": "null"
}
],
"description": "Override the sandbox policy for this turn and subsequent turns."
"description": "Deprecated for turns. When present, the server treats this as a compatibility spelling for selecting a matching named permissions profile."
},
"serviceTier": {
"description": "Override the service tier for this turn and subsequent turns.",
@@ -5583,14 +5583,6 @@
"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": [
@@ -5598,31 +5590,6 @@
],
"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",
@@ -11710,31 +11677,6 @@
}
]
},
"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": {
@@ -11746,40 +11688,6 @@
],
"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",
@@ -14403,13 +14311,6 @@
],
"title": "WorkspaceWriteSandboxPolicyType",
"type": "string"
},
"writableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
"type": "array"
}
},
"required": [
@@ -15397,7 +15298,8 @@
{
"type": "null"
}
]
],
"description": "Deprecated for fork. When present, the server treats this as a compatibility spelling for selecting a matching named permissions profile."
},
"serviceTier": {
"type": [
@@ -15473,7 +15375,7 @@
"$ref": "#/definitions/v2/SandboxPolicy"
}
],
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` and `workspaceRoots`."
},
"serviceTier": {
"type": [
@@ -16900,7 +16802,8 @@
{
"type": "null"
}
]
],
"description": "Deprecated for resume. When present, the server treats this as a compatibility spelling for selecting a matching named permissions profile."
},
"serviceTier": {
"type": [
@@ -16965,7 +16868,7 @@
"$ref": "#/definitions/v2/SandboxPolicy"
}
],
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` and `workspaceRoots`."
},
"serviceTier": {
"type": [
@@ -17273,7 +17176,7 @@
"$ref": "#/definitions/v2/SandboxPolicy"
}
],
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` and `workspaceRoots`."
},
"serviceTier": {
"type": [
@@ -17942,7 +17845,7 @@
"type": "null"
}
],
"description": "Override the sandbox policy for this turn and subsequent turns."
"description": "Deprecated for turns. When present, the server treats this as a compatibility spelling for selecting a matching named permissions profile."
},
"serviceTier": {
"description": "Override the service tier for this turn and subsequent turns.",
@@ -143,14 +143,6 @@
"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": [
@@ -158,31 +150,6 @@
],
"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",
@@ -8259,31 +8226,6 @@
}
]
},
"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": {
@@ -8295,40 +8237,6 @@
],
"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",
@@ -10952,13 +10860,6 @@
],
"title": "WorkspaceWriteSandboxPolicyType",
"type": "string"
},
"writableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
}
},
"required": [
@@ -13221,7 +13122,8 @@
{
"type": "null"
}
]
],
"description": "Deprecated for fork. When present, the server treats this as a compatibility spelling for selecting a matching named permissions profile."
},
"serviceTier": {
"type": [
@@ -13297,7 +13199,7 @@
"$ref": "#/definitions/SandboxPolicy"
}
],
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` and `workspaceRoots`."
},
"serviceTier": {
"type": [
@@ -14724,7 +14626,8 @@
{
"type": "null"
}
]
],
"description": "Deprecated for resume. When present, the server treats this as a compatibility spelling for selecting a matching named permissions profile."
},
"serviceTier": {
"type": [
@@ -14789,7 +14692,7 @@
"$ref": "#/definitions/SandboxPolicy"
}
],
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` and `workspaceRoots`."
},
"serviceTier": {
"type": [
@@ -15097,7 +15000,7 @@
"$ref": "#/definitions/SandboxPolicy"
}
],
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` and `workspaceRoots`."
},
"serviceTier": {
"type": [
@@ -15766,7 +15669,7 @@
"type": "null"
}
],
"description": "Override the sandbox policy for this turn and subsequent turns."
"description": "Deprecated for turns. When present, the server treats this as a compatibility spelling for selecting a matching named permissions profile."
},
"serviceTier": {
"description": "Override the service tier for this turn and subsequent turns.",
@@ -441,13 +441,6 @@
],
"title": "WorkspaceWriteSandboxPolicyType",
"type": "string"
},
"writableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
}
},
"required": [
@@ -64,65 +64,6 @@
}
]
},
"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"
}
]
},
"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"
}
]
},
"SandboxMode": {
"enum": [
"read-only",
@@ -212,7 +153,8 @@
{
"type": "null"
}
]
],
"description": "Deprecated for fork. When present, the server treats this as a compatibility spelling for selecting a matching named permissions profile."
},
"serviceTier": {
"type": [
@@ -18,14 +18,6 @@
"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": [
@@ -33,31 +25,6 @@
],
"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"
},
@@ -503,202 +470,6 @@
],
"type": "string"
},
"FileSystemAccessMode": {
"enum": [
"read",
"write",
"none"
],
"type": "string"
},
"FileSystemPath": {
"oneOf": [
{
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"path"
],
"title": "PathFileSystemPathType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "PathFileSystemPath",
"type": "object"
},
{
"properties": {
"pattern": {
"type": "string"
},
"type": {
"enum": [
"glob_pattern"
],
"title": "GlobPatternFileSystemPathType",
"type": "string"
}
},
"required": [
"pattern",
"type"
],
"title": "GlobPatternFileSystemPath",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"special"
],
"title": "SpecialFileSystemPathType",
"type": "string"
},
"value": {
"$ref": "#/definitions/FileSystemSpecialPath"
}
},
"required": [
"type",
"value"
],
"title": "SpecialFileSystemPath",
"type": "object"
}
]
},
"FileSystemSandboxEntry": {
"properties": {
"access": {
"$ref": "#/definitions/FileSystemAccessMode"
},
"path": {
"$ref": "#/definitions/FileSystemPath"
}
},
"required": [
"access",
"path"
],
"type": "object"
},
"FileSystemSpecialPath": {
"oneOf": [
{
"properties": {
"kind": {
"enum": [
"root"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "RootFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"minimal"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "MinimalFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"project_roots"
],
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind"
],
"title": "KindFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"tmpdir"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "TmpdirFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"slash_tmp"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "SlashTmpFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"unknown"
],
"type": "string"
},
"path": {
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind",
"path"
],
"type": "object"
}
]
},
"FileUpdateChange": {
"properties": {
"diff": {
@@ -937,135 +708,6 @@
}
]
},
"PermissionProfile": {
"oneOf": [
{
"description": "Codex owns sandbox construction for this profile.",
"properties": {
"fileSystem": {
"$ref": "#/definitions/PermissionProfileFileSystemPermissions"
},
"network": {
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
},
"type": {
"enum": [
"managed"
],
"title": "ManagedPermissionProfileType",
"type": "string"
}
},
"required": [
"fileSystem",
"network",
"type"
],
"title": "ManagedPermissionProfile",
"type": "object"
},
{
"description": "Do not apply an outer sandbox.",
"properties": {
"type": {
"enum": [
"disabled"
],
"title": "DisabledPermissionProfileType",
"type": "string"
}
},
"required": [
"type"
],
"title": "DisabledPermissionProfile",
"type": "object"
},
{
"description": "Filesystem isolation is enforced by an external caller.",
"properties": {
"network": {
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
},
"type": {
"enum": [
"external"
],
"title": "ExternalPermissionProfileType",
"type": "string"
}
},
"required": [
"network",
"type"
],
"title": "ExternalPermissionProfile",
"type": "object"
}
]
},
"PermissionProfileFileSystemPermissions": {
"oneOf": [
{
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/FileSystemSandboxEntry"
},
"type": "array"
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"null"
]
},
"type": {
"enum": [
"restricted"
],
"title": "RestrictedPermissionProfileFileSystemPermissionsType",
"type": "string"
}
},
"required": [
"entries",
"type"
],
"title": "RestrictedPermissionProfileFileSystemPermissions",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"unrestricted"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
"type": "string"
}
},
"required": [
"type"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
"type": "object"
}
]
},
"PermissionProfileNetworkPermissions": {
"properties": {
"enabled": {
"type": "boolean"
}
},
"required": [
"enabled"
],
"type": "object"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
@@ -1160,13 +802,6 @@
],
"title": "WorkspaceWriteSandboxPolicyType",
"type": "string"
},
"writableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
}
},
"required": [
@@ -2605,7 +2240,7 @@
"$ref": "#/definitions/SandboxPolicy"
}
],
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` and `workspaceRoots`."
},
"serviceTier": {
"type": [
@@ -298,65 +298,6 @@
}
]
},
"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"
}
]
},
"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",
@@ -1091,7 +1032,8 @@
{
"type": "null"
}
]
],
"description": "Deprecated for resume. When present, the server treats this as a compatibility spelling for selecting a matching named permissions profile."
},
"serviceTier": {
"type": [
@@ -18,14 +18,6 @@
"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": [
@@ -33,31 +25,6 @@
],
"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"
},
@@ -503,202 +470,6 @@
],
"type": "string"
},
"FileSystemAccessMode": {
"enum": [
"read",
"write",
"none"
],
"type": "string"
},
"FileSystemPath": {
"oneOf": [
{
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"path"
],
"title": "PathFileSystemPathType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "PathFileSystemPath",
"type": "object"
},
{
"properties": {
"pattern": {
"type": "string"
},
"type": {
"enum": [
"glob_pattern"
],
"title": "GlobPatternFileSystemPathType",
"type": "string"
}
},
"required": [
"pattern",
"type"
],
"title": "GlobPatternFileSystemPath",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"special"
],
"title": "SpecialFileSystemPathType",
"type": "string"
},
"value": {
"$ref": "#/definitions/FileSystemSpecialPath"
}
},
"required": [
"type",
"value"
],
"title": "SpecialFileSystemPath",
"type": "object"
}
]
},
"FileSystemSandboxEntry": {
"properties": {
"access": {
"$ref": "#/definitions/FileSystemAccessMode"
},
"path": {
"$ref": "#/definitions/FileSystemPath"
}
},
"required": [
"access",
"path"
],
"type": "object"
},
"FileSystemSpecialPath": {
"oneOf": [
{
"properties": {
"kind": {
"enum": [
"root"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "RootFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"minimal"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "MinimalFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"project_roots"
],
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind"
],
"title": "KindFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"tmpdir"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "TmpdirFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"slash_tmp"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "SlashTmpFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"unknown"
],
"type": "string"
},
"path": {
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind",
"path"
],
"type": "object"
}
]
},
"FileUpdateChange": {
"properties": {
"diff": {
@@ -937,135 +708,6 @@
}
]
},
"PermissionProfile": {
"oneOf": [
{
"description": "Codex owns sandbox construction for this profile.",
"properties": {
"fileSystem": {
"$ref": "#/definitions/PermissionProfileFileSystemPermissions"
},
"network": {
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
},
"type": {
"enum": [
"managed"
],
"title": "ManagedPermissionProfileType",
"type": "string"
}
},
"required": [
"fileSystem",
"network",
"type"
],
"title": "ManagedPermissionProfile",
"type": "object"
},
{
"description": "Do not apply an outer sandbox.",
"properties": {
"type": {
"enum": [
"disabled"
],
"title": "DisabledPermissionProfileType",
"type": "string"
}
},
"required": [
"type"
],
"title": "DisabledPermissionProfile",
"type": "object"
},
{
"description": "Filesystem isolation is enforced by an external caller.",
"properties": {
"network": {
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
},
"type": {
"enum": [
"external"
],
"title": "ExternalPermissionProfileType",
"type": "string"
}
},
"required": [
"network",
"type"
],
"title": "ExternalPermissionProfile",
"type": "object"
}
]
},
"PermissionProfileFileSystemPermissions": {
"oneOf": [
{
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/FileSystemSandboxEntry"
},
"type": "array"
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"null"
]
},
"type": {
"enum": [
"restricted"
],
"title": "RestrictedPermissionProfileFileSystemPermissionsType",
"type": "string"
}
},
"required": [
"entries",
"type"
],
"title": "RestrictedPermissionProfileFileSystemPermissions",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"unrestricted"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
"type": "string"
}
},
"required": [
"type"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
"type": "object"
}
]
},
"PermissionProfileNetworkPermissions": {
"properties": {
"enabled": {
"type": "boolean"
}
},
"required": [
"enabled"
],
"type": "object"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
@@ -1160,13 +802,6 @@
],
"title": "WorkspaceWriteSandboxPolicyType",
"type": "string"
},
"writableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
}
},
"required": [
@@ -2605,7 +2240,7 @@
"$ref": "#/definitions/SandboxPolicy"
}
],
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` and `workspaceRoots`."
},
"serviceTier": {
"type": [
@@ -90,65 +90,6 @@
],
"type": "object"
},
"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"
}
]
},
"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",
@@ -18,14 +18,6 @@
"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": [
@@ -33,31 +25,6 @@
],
"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"
},
@@ -503,202 +470,6 @@
],
"type": "string"
},
"FileSystemAccessMode": {
"enum": [
"read",
"write",
"none"
],
"type": "string"
},
"FileSystemPath": {
"oneOf": [
{
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"path"
],
"title": "PathFileSystemPathType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "PathFileSystemPath",
"type": "object"
},
{
"properties": {
"pattern": {
"type": "string"
},
"type": {
"enum": [
"glob_pattern"
],
"title": "GlobPatternFileSystemPathType",
"type": "string"
}
},
"required": [
"pattern",
"type"
],
"title": "GlobPatternFileSystemPath",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"special"
],
"title": "SpecialFileSystemPathType",
"type": "string"
},
"value": {
"$ref": "#/definitions/FileSystemSpecialPath"
}
},
"required": [
"type",
"value"
],
"title": "SpecialFileSystemPath",
"type": "object"
}
]
},
"FileSystemSandboxEntry": {
"properties": {
"access": {
"$ref": "#/definitions/FileSystemAccessMode"
},
"path": {
"$ref": "#/definitions/FileSystemPath"
}
},
"required": [
"access",
"path"
],
"type": "object"
},
"FileSystemSpecialPath": {
"oneOf": [
{
"properties": {
"kind": {
"enum": [
"root"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "RootFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"minimal"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "MinimalFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"project_roots"
],
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind"
],
"title": "KindFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"tmpdir"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "TmpdirFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"slash_tmp"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "SlashTmpFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"unknown"
],
"type": "string"
},
"path": {
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind",
"path"
],
"type": "object"
}
]
},
"FileUpdateChange": {
"properties": {
"diff": {
@@ -937,135 +708,6 @@
}
]
},
"PermissionProfile": {
"oneOf": [
{
"description": "Codex owns sandbox construction for this profile.",
"properties": {
"fileSystem": {
"$ref": "#/definitions/PermissionProfileFileSystemPermissions"
},
"network": {
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
},
"type": {
"enum": [
"managed"
],
"title": "ManagedPermissionProfileType",
"type": "string"
}
},
"required": [
"fileSystem",
"network",
"type"
],
"title": "ManagedPermissionProfile",
"type": "object"
},
{
"description": "Do not apply an outer sandbox.",
"properties": {
"type": {
"enum": [
"disabled"
],
"title": "DisabledPermissionProfileType",
"type": "string"
}
},
"required": [
"type"
],
"title": "DisabledPermissionProfile",
"type": "object"
},
{
"description": "Filesystem isolation is enforced by an external caller.",
"properties": {
"network": {
"$ref": "#/definitions/PermissionProfileNetworkPermissions"
},
"type": {
"enum": [
"external"
],
"title": "ExternalPermissionProfileType",
"type": "string"
}
},
"required": [
"network",
"type"
],
"title": "ExternalPermissionProfile",
"type": "object"
}
]
},
"PermissionProfileFileSystemPermissions": {
"oneOf": [
{
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/FileSystemSandboxEntry"
},
"type": "array"
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"null"
]
},
"type": {
"enum": [
"restricted"
],
"title": "RestrictedPermissionProfileFileSystemPermissionsType",
"type": "string"
}
},
"required": [
"entries",
"type"
],
"title": "RestrictedPermissionProfileFileSystemPermissions",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"unrestricted"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissionsType",
"type": "string"
}
},
"required": [
"type"
],
"title": "UnrestrictedPermissionProfileFileSystemPermissions",
"type": "object"
}
]
},
"PermissionProfileNetworkPermissions": {
"properties": {
"enabled": {
"type": "boolean"
}
},
"required": [
"enabled"
],
"type": "object"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
@@ -1160,13 +802,6 @@
],
"title": "WorkspaceWriteSandboxPolicyType",
"type": "string"
},
"writableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
}
},
"required": [
@@ -2605,7 +2240,7 @@
"$ref": "#/definitions/SandboxPolicy"
}
],
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `permissionProfile` when they need exact runtime permissions."
"description": "Legacy sandbox policy retained for compatibility. Experimental clients should prefer `activePermissionProfile` and `workspaceRoots`."
},
"serviceTier": {
"type": [
@@ -114,65 +114,6 @@
],
"type": "string"
},
"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"
}
]
},
"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",
@@ -295,13 +236,6 @@
],
"title": "WorkspaceWriteSandboxPolicyType",
"type": "string"
},
"writableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
}
},
"required": [
@@ -576,7 +510,7 @@
"type": "null"
}
],
"description": "Override the sandbox policy for this turn and subsequent turns."
"description": "Deprecated for turns. When present, the server treats this as a compatibility spelling for selecting a matching named permissions profile."
},
"serviceTier": {
"description": "Override the service tier for this turn and subsequent turns.",
@@ -1,7 +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 { ActivePermissionProfileModification } from "./ActivePermissionProfileModification";
export type ActivePermissionProfile = {
/**
@@ -13,9 +12,4 @@ 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>, };
extends: string | null, };
@@ -1,6 +0,0 @@
// 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, };
@@ -1,6 +0,0 @@
// 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, };
@@ -1,6 +0,0 @@
// 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, };
@@ -1,7 +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";
import type { NetworkAccess } from "./NetworkAccess";
export type SandboxPolicy = { "type": "dangerFullAccess" } | { "type": "readOnly", networkAccess: boolean, } | { "type": "externalSandbox", networkAccess: NetworkAccess, } | { "type": "workspaceWrite", writableRoots: Array<AbsolutePathBuf>, networkAccess: boolean, excludeTmpdirEnvVar: boolean, excludeSlashTmp: boolean, };
export type SandboxPolicy = { "type": "dangerFullAccess" } | { "type": "readOnly", networkAccess: boolean, } | { "type": "externalSandbox", networkAccess: NetworkAccess, } | { "type": "workspaceWrite", networkAccess: boolean, excludeTmpdirEnvVar: boolean, excludeSlashTmp: boolean, };
@@ -23,7 +23,12 @@ model?: string | null, modelProvider?: string | null, serviceTier?: string | nul
* Override where approval requests are routed for review on this thread
* and subsequent turns.
*/
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, ephemeral?: boolean, /**
approvalsReviewer?: ApprovalsReviewer | null, /**
* Deprecated for fork. When present, the server treats this as a
* compatibility spelling for selecting a matching named permissions
* profile.
*/
sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, ephemeral?: boolean, /**
* Optional client-supplied analytics source classification for this forked thread.
*/
threadSource?: ThreadSource | null};
@@ -16,7 +16,6 @@ instructionSources: Array<AbsolutePathBuf>, approvalPolicy: AskForApproval, /**
*/
approvalsReviewer: ApprovalsReviewer, /**
* Legacy sandbox policy retained for compatibility. Experimental clients
* should prefer `permissionProfile` when they need exact runtime
* permissions.
* should prefer `activePermissionProfile` and `workspaceRoots`.
*/
sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null};
@@ -25,4 +25,9 @@ model?: string | null, modelProvider?: string | null, serviceTier?: string | nul
* Override where approval requests are routed for review on this thread
* and subsequent turns.
*/
approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null};
approvalsReviewer?: ApprovalsReviewer | null, /**
* Deprecated for resume. When present, the server treats this as a
* compatibility spelling for selecting a matching named permissions
* profile.
*/
sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null};
@@ -16,7 +16,6 @@ instructionSources: Array<AbsolutePathBuf>, approvalPolicy: AskForApproval, /**
*/
approvalsReviewer: ApprovalsReviewer, /**
* Legacy sandbox policy retained for compatibility. Experimental clients
* should prefer `permissionProfile` when they need exact runtime
* permissions.
* should prefer `activePermissionProfile` and `workspaceRoots`.
*/
sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null};
@@ -16,7 +16,6 @@ instructionSources: Array<AbsolutePathBuf>, approvalPolicy: AskForApproval, /**
*/
approvalsReviewer: ApprovalsReviewer, /**
* Legacy sandbox policy retained for compatibility. Experimental clients
* should prefer `permissionProfile` when they need exact runtime
* permissions.
* should prefer `activePermissionProfile` and `workspaceRoots`.
*/
sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null};
@@ -21,7 +21,9 @@ approvalPolicy?: AskForApproval | null, /**
* subsequent turns.
*/
approvalsReviewer?: ApprovalsReviewer | null, /**
* Override the sandbox policy for this turn and subsequent turns.
* Deprecated for turns. When present, the server treats this as a
* compatibility spelling for selecting a matching named permissions
* profile.
*/
sandboxPolicy?: SandboxPolicy | null, /**
* Override the model for this turn and subsequent turns.
@@ -5,7 +5,6 @@ export type { AccountLoginCompletedNotification } from "./AccountLoginCompletedN
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";
@@ -257,9 +256,7 @@ 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";
@@ -2272,11 +2272,11 @@ mod tests {
model_provider: "openai".to_string(),
service_tier: None,
cwd,
workspace_roots: vec![],
instruction_sources: vec![absolute_path("/tmp/AGENTS.md")],
approval_policy: v2::AskForApproval::OnFailure,
approvals_reviewer: v2::ApprovalsReviewer::User,
sandbox: v2::SandboxPolicy::DangerFullAccess,
permission_profile: None,
active_permission_profile: None,
reasoning_effort: None,
},
@@ -2316,13 +2316,13 @@ mod tests {
"modelProvider": "openai",
"serviceTier": null,
"cwd": absolute_path_string("tmp"),
"workspaceRoots": [],
"instructionSources": [absolute_path_string("tmp/AGENTS.md")],
"approvalPolicy": "on-failure",
"approvalsReviewer": "user",
"sandbox": {
"type": "dangerFullAccess"
},
"permissionProfile": null,
"activePermissionProfile": null,
"reasoningEffort": null
}
@@ -5,7 +5,6 @@ use codex_protocol::approvals::NetworkApprovalProtocol as CoreNetworkApprovalPro
use codex_protocol::approvals::NetworkPolicyAmendment as CoreNetworkPolicyAmendment;
use codex_protocol::approvals::NetworkPolicyRuleAction as CoreNetworkPolicyRuleAction;
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;
@@ -437,41 +436,6 @@ pub struct ActivePermissionProfile {
/// 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 {
@@ -479,11 +443,6 @@ impl From<CoreActivePermissionProfile> for ActivePermissionProfile {
Self {
id: value.id,
extends: value.extends,
modifications: value
.modifications
.into_iter()
.map(ActivePermissionProfileModification::from)
.collect(),
}
}
}
@@ -493,42 +452,10 @@ impl From<ActivePermissionProfile> for CoreActivePermissionProfile {
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/")]
@@ -607,14 +534,16 @@ pub enum SandboxPolicy {
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
WorkspaceWrite {
#[serde(default)]
writable_roots: Vec<AbsolutePathBuf>,
#[serde(default)]
network_access: bool,
#[serde(default)]
exclude_tmpdir_env_var: bool,
#[serde(default)]
exclude_slash_tmp: bool,
#[serde(default, skip_serializing)]
#[schemars(skip)]
#[ts(skip)]
legacy_writable_roots: Vec<AbsolutePathBuf>,
},
}
@@ -690,10 +619,10 @@ impl<'de> Deserialize<'de> for SandboxPolicy {
));
}
Ok(SandboxPolicy::WorkspaceWrite {
writable_roots,
network_access,
exclude_tmpdir_env_var,
exclude_slash_tmp,
legacy_writable_roots: writable_roots,
})
}
}
@@ -720,18 +649,60 @@ impl SandboxPolicy {
}
}
SandboxPolicy::WorkspaceWrite {
writable_roots,
network_access,
exclude_tmpdir_env_var,
exclude_slash_tmp,
legacy_writable_roots: _,
} => codex_protocol::protocol::SandboxPolicy::WorkspaceWrite {
writable_roots: writable_roots.clone(),
network_access: *network_access,
exclude_tmpdir_env_var: *exclude_tmpdir_env_var,
exclude_slash_tmp: *exclude_slash_tmp,
},
}
}
pub fn legacy_writable_roots(&self) -> &[AbsolutePathBuf] {
match self {
SandboxPolicy::WorkspaceWrite {
legacy_writable_roots,
..
} => legacy_writable_roots,
SandboxPolicy::DangerFullAccess
| SandboxPolicy::ReadOnly { .. }
| SandboxPolicy::ExternalSandbox { .. } => &[],
}
}
pub fn to_permission_profile_for_cwd(&self, cwd: &std::path::Path) -> CorePermissionProfile {
match self {
SandboxPolicy::WorkspaceWrite {
legacy_writable_roots,
..
} if legacy_writable_roots.is_empty() => {
CorePermissionProfile::from_legacy_sandbox_policy_for_cwd(&self.to_core(), cwd)
}
SandboxPolicy::WorkspaceWrite {
network_access,
exclude_tmpdir_env_var,
exclude_slash_tmp,
legacy_writable_roots,
} => CorePermissionProfile::workspace_write_with(
legacy_writable_roots,
if *network_access {
CoreNetworkSandboxPolicy::Enabled
} else {
CoreNetworkSandboxPolicy::Restricted
},
*exclude_tmpdir_env_var,
*exclude_slash_tmp,
),
SandboxPolicy::DangerFullAccess
| SandboxPolicy::ReadOnly { .. }
| SandboxPolicy::ExternalSandbox { .. } => {
CorePermissionProfile::from_legacy_sandbox_policy_for_cwd(&self.to_core(), cwd)
}
}
}
}
impl From<codex_protocol::protocol::SandboxPolicy> for SandboxPolicy {
@@ -752,15 +723,14 @@ impl From<codex_protocol::protocol::SandboxPolicy> for SandboxPolicy {
}
}
codex_protocol::protocol::SandboxPolicy::WorkspaceWrite {
writable_roots,
network_access,
exclude_tmpdir_env_var,
exclude_slash_tmp,
} => SandboxPolicy::WorkspaceWrite {
writable_roots,
network_access,
exclude_tmpdir_env_var,
exclude_slash_tmp,
legacy_writable_roots: Vec::new(),
},
}
}
@@ -2019,17 +2019,16 @@ fn mcp_server_elicitation_response_serializes_nullable_content() {
#[test]
fn sandbox_policy_round_trips_workspace_write_access() {
let v2_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: true,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
legacy_writable_roots: Vec::new(),
};
let core_policy = v2_policy.to_core();
assert_eq!(
core_policy,
codex_protocol::protocol::SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: true,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
@@ -2060,10 +2059,9 @@ fn sandbox_policy_deserializes_legacy_read_only_full_access_field() {
#[test]
fn sandbox_policy_deserializes_legacy_workspace_write_full_access_field() {
let writable_root = absolute_path("/workspace");
let policy = serde_json::from_value::<SandboxPolicy>(json!({
"type": "workspaceWrite",
"writableRoots": [writable_root],
"writableRoots": [],
"readOnlyAccess": {
"type": "fullAccess"
},
@@ -2075,14 +2073,38 @@ fn sandbox_policy_deserializes_legacy_workspace_write_full_access_field() {
assert_eq!(
policy,
SandboxPolicy::WorkspaceWrite {
writable_roots: vec![absolute_path("/workspace")],
network_access: true,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
legacy_writable_roots: Vec::new(),
}
);
}
#[test]
fn sandbox_policy_deserializes_legacy_workspace_write_writable_roots_field() {
let writable_root = absolute_path("/workspace");
let policy = serde_json::from_value::<SandboxPolicy>(json!({
"type": "workspaceWrite",
"writableRoots": [writable_root],
"networkAccess": false,
"excludeTmpdirEnvVar": false,
"excludeSlashTmp": false
}))
.expect("workspace-write policy should accept legacy writableRoots field");
assert_eq!(policy.legacy_writable_roots(), &[writable_root]);
assert_eq!(
serde_json::to_value(policy).expect("policy should serialize"),
json!({
"type": "workspaceWrite",
"networkAccess": false,
"excludeTmpdirEnvVar": false,
"excludeSlashTmp": false
})
);
}
#[test]
fn sandbox_policy_rejects_legacy_read_only_restricted_access_field() {
let err = serde_json::from_value::<SandboxPolicy>(json!({
@@ -3389,9 +3411,6 @@ fn thread_lifecycle_responses_default_missing_optional_fields() {
assert_eq!(start.instruction_sources, Vec::<AbsolutePathBuf>::new());
assert_eq!(resume.instruction_sources, Vec::<AbsolutePathBuf>::new());
assert_eq!(fork.instruction_sources, Vec::<AbsolutePathBuf>::new());
assert_eq!(start.permission_profile, None);
assert_eq!(resume.permission_profile, None);
assert_eq!(fork.permission_profile, None);
assert_eq!(start.active_permission_profile, None);
assert_eq!(resume.active_permission_profile, None);
assert_eq!(fork.active_permission_profile, None);
@@ -3419,6 +3438,7 @@ fn turn_start_params_preserve_explicit_null_service_tier() {
responsesapi_client_metadata: None,
environments: None,
cwd: None,
workspace_roots: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
@@ -3436,6 +3456,20 @@ fn turn_start_params_preserve_explicit_null_service_tier() {
assert_eq!(serialized_without_override.get("serviceTier"), None);
}
#[test]
fn turn_start_permissions_uses_profile_id_string_shape() {
let params: TurnStartParams = serde_json::from_value(json!({
"threadId": "thread-1",
"input": [],
"permissions": ":workspace"
}))
.expect("turn start params should deserialize");
assert_eq!(params.permissions, Some(":workspace".to_string()));
let serialized = serde_json::to_value(&params).expect("params should serialize");
assert_eq!(serialized["permissions"], json!(":workspace"));
}
#[test]
fn turn_start_params_round_trip_environments() {
let cwd = test_absolute_path();
@@ -1,8 +1,6 @@
use super::ActivePermissionProfile;
use super::ApprovalsReviewer;
use super::AskForApproval;
use super::PermissionProfile;
use super::PermissionProfileSelectionParams;
use super::SandboxMode;
use super::SandboxPolicy;
use super::Thread;
@@ -107,6 +105,11 @@ pub struct ThreadStartParams {
pub service_tier: Option<Option<String>>,
#[ts(optional = nullable)]
pub cwd: Option<String>,
/// Optional workspace roots for this thread. Omitted uses the server's
/// configured roots, usually seeded from `cwd`.
#[experimental("thread/start.workspaceRoots")]
#[ts(optional = nullable)]
pub workspace_roots: Option<Vec<AbsolutePathBuf>>,
#[experimental(nested)]
#[ts(optional = nullable)]
pub approval_policy: Option<AskForApproval>,
@@ -116,12 +119,11 @@ pub struct ThreadStartParams {
pub approvals_reviewer: Option<ApprovalsReviewer>,
#[ts(optional = nullable)]
pub sandbox: Option<SandboxMode>,
/// 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.
/// Named permissions profile id for this new thread's initial permissions.
/// Cannot be combined with `sandbox`.
#[experimental("thread/start.permissions")]
#[ts(optional = nullable)]
pub permissions: Option<PermissionProfileSelectionParams>,
pub permissions: Option<String>,
#[ts(optional = nullable)]
pub config: Option<HashMap<String, JsonValue>>,
#[ts(optional = nullable)]
@@ -195,6 +197,11 @@ pub struct ThreadStartResponse {
pub model_provider: String,
pub service_tier: Option<String>,
pub cwd: AbsolutePathBuf,
/// Workspace roots used to realize symbolic `:project_roots` permission
/// entries for this thread.
#[experimental("thread/start.workspaceRoots")]
#[serde(default)]
pub workspace_roots: Vec<AbsolutePathBuf>,
/// Instruction source files currently loaded for this thread.
#[serde(default)]
pub instruction_sources: Vec<AbsolutePathBuf>,
@@ -203,14 +210,8 @@ pub struct ThreadStartResponse {
/// Reviewer currently used for approval requests on this thread.
pub approvals_reviewer: ApprovalsReviewer,
/// Legacy sandbox policy retained for compatibility. Experimental clients
/// should prefer `permissionProfile` when they need exact runtime
/// permissions.
/// should prefer `activePermissionProfile` and `workspaceRoots`.
pub sandbox: SandboxPolicy,
/// 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")]
@@ -264,6 +265,11 @@ pub struct ThreadResumeParams {
pub service_tier: Option<Option<String>>,
#[ts(optional = nullable)]
pub cwd: Option<String>,
/// Optional replacement workspace roots for the resumed thread. Omitted
/// preserves the persisted or configured roots.
#[experimental("thread/resume.workspaceRoots")]
#[ts(optional = nullable)]
pub workspace_roots: Option<Vec<AbsolutePathBuf>>,
#[experimental(nested)]
#[ts(optional = nullable)]
pub approval_policy: Option<AskForApproval>,
@@ -271,14 +277,16 @@ pub struct ThreadResumeParams {
/// and subsequent turns.
#[ts(optional = nullable)]
pub approvals_reviewer: Option<ApprovalsReviewer>,
/// Deprecated for resume. When present, the server treats this as a
/// compatibility spelling for selecting a matching named permissions
/// profile.
#[ts(optional = nullable)]
pub sandbox: Option<SandboxMode>,
/// 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.
/// Named permissions profile id for the resumed thread. Cannot be combined
/// with `sandbox`.
#[experimental("thread/resume.permissions")]
#[ts(optional = nullable)]
pub permissions: Option<PermissionProfileSelectionParams>,
pub permissions: Option<String>,
#[ts(optional = nullable)]
pub config: Option<HashMap<String, serde_json::Value>>,
#[ts(optional = nullable)]
@@ -310,6 +318,11 @@ pub struct ThreadResumeResponse {
pub model_provider: String,
pub service_tier: Option<String>,
pub cwd: AbsolutePathBuf,
/// Workspace roots used to realize symbolic `:project_roots` permission
/// entries for this thread.
#[experimental("thread/resume.workspaceRoots")]
#[serde(default)]
pub workspace_roots: Vec<AbsolutePathBuf>,
/// Instruction source files currently loaded for this thread.
#[serde(default)]
pub instruction_sources: Vec<AbsolutePathBuf>,
@@ -318,14 +331,8 @@ pub struct ThreadResumeResponse {
/// Reviewer currently used for approval requests on this thread.
pub approvals_reviewer: ApprovalsReviewer,
/// Legacy sandbox policy retained for compatibility. Experimental clients
/// should prefer `permissionProfile` when they need exact runtime
/// permissions.
/// should prefer `activePermissionProfile` and `workspaceRoots`.
pub sandbox: SandboxPolicy,
/// 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")]
@@ -370,6 +377,11 @@ pub struct ThreadForkParams {
pub service_tier: Option<Option<String>>,
#[ts(optional = nullable)]
pub cwd: Option<String>,
/// Optional replacement workspace roots for the forked thread. Omitted
/// preserves the source thread roots when available.
#[experimental("thread/fork.workspaceRoots")]
#[ts(optional = nullable)]
pub workspace_roots: Option<Vec<AbsolutePathBuf>>,
#[experimental(nested)]
#[ts(optional = nullable)]
pub approval_policy: Option<AskForApproval>,
@@ -377,14 +389,16 @@ pub struct ThreadForkParams {
/// and subsequent turns.
#[ts(optional = nullable)]
pub approvals_reviewer: Option<ApprovalsReviewer>,
/// Deprecated for fork. When present, the server treats this as a
/// compatibility spelling for selecting a matching named permissions
/// profile.
#[ts(optional = nullable)]
pub sandbox: Option<SandboxMode>,
/// 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.
/// Named permissions profile id for the forked thread. Cannot be combined
/// with `sandbox`.
#[experimental("thread/fork.permissions")]
#[ts(optional = nullable)]
pub permissions: Option<PermissionProfileSelectionParams>,
pub permissions: Option<String>,
#[ts(optional = nullable)]
pub config: Option<HashMap<String, serde_json::Value>>,
#[ts(optional = nullable)]
@@ -419,6 +433,11 @@ pub struct ThreadForkResponse {
pub model_provider: String,
pub service_tier: Option<String>,
pub cwd: AbsolutePathBuf,
/// Workspace roots used to realize symbolic `:project_roots` permission
/// entries for this thread.
#[experimental("thread/fork.workspaceRoots")]
#[serde(default)]
pub workspace_roots: Vec<AbsolutePathBuf>,
/// Instruction source files currently loaded for this thread.
#[serde(default)]
pub instruction_sources: Vec<AbsolutePathBuf>,
@@ -427,14 +446,8 @@ pub struct ThreadForkResponse {
/// Reviewer currently used for approval requests on this thread.
pub approvals_reviewer: ApprovalsReviewer,
/// Legacy sandbox policy retained for compatibility. Experimental clients
/// should prefer `permissionProfile` when they need exact runtime
/// permissions.
/// should prefer `activePermissionProfile` and `workspaceRoots`.
pub sandbox: SandboxPolicy,
/// 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")]
@@ -1,6 +1,5 @@
use super::ApprovalsReviewer;
use super::AskForApproval;
use super::PermissionProfileSelectionParams;
use super::SandboxPolicy;
use super::Turn;
use codex_experimental_api_macros::ExperimentalApi;
@@ -64,6 +63,11 @@ pub struct TurnStartParams {
/// Override the working directory for this turn and subsequent turns.
#[ts(optional = nullable)]
pub cwd: Option<PathBuf>,
/// Replace the workspace roots for this turn and subsequent turns. Omitted
/// preserves the current roots.
#[experimental("turn/start.workspaceRoots")]
#[ts(optional = nullable)]
pub workspace_roots: Option<Vec<AbsolutePathBuf>>,
/// Override the approval policy for this turn and subsequent turns.
#[experimental(nested)]
#[ts(optional = nullable)]
@@ -72,16 +76,16 @@ pub struct TurnStartParams {
/// subsequent turns.
#[ts(optional = nullable)]
pub approvals_reviewer: Option<ApprovalsReviewer>,
/// Override the sandbox policy for this turn and subsequent turns.
/// Deprecated for turns. When present, the server treats this as a
/// compatibility spelling for selecting a matching named permissions
/// profile.
#[ts(optional = nullable)]
pub sandbox_policy: Option<SandboxPolicy>,
/// 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.
/// Select a named permissions profile id for this turn and subsequent
/// turns. Cannot be combined with `sandboxPolicy`.
#[experimental("turn/start.permissions")]
#[ts(optional = nullable)]
pub permissions: Option<PermissionProfileSelectionParams>,
pub permissions: Option<String>,
/// Override the model for this turn and subsequent turns.
#[ts(optional = nullable)]
pub model: Option<String>,
+25 -18
View File
@@ -194,18 +194,24 @@ pub struct InProcessClientSender {
}
impl InProcessClientSender {
pub async fn request(&self, request: ClientRequest) -> IoResult<PendingClientRequestResponse> {
pub fn request(
&self,
request: ClientRequest,
) -> impl std::future::Future<Output = IoResult<PendingClientRequestResponse>> + '_ {
let (response_tx, response_rx) = oneshot::channel();
self.try_send_client_message(InProcessClientMessage::Request {
let send_result = self.try_send_client_message(InProcessClientMessage::Request {
request: Box::new(request),
response_tx,
})?;
response_rx.await.map_err(|err| {
IoError::new(
ErrorKind::BrokenPipe,
format!("in-process request response channel closed: {err}"),
)
})
});
async move {
send_result?;
response_rx.await.map_err(|err| {
IoError::new(
ErrorKind::BrokenPipe,
format!("in-process request response channel closed: {err}"),
)
})
}
}
pub fn notify(&self, notification: ClientNotification) -> IoResult<()> {
@@ -266,8 +272,11 @@ impl InProcessClientHandle {
/// request IDs unique among concurrent requests; reusing an in-flight ID
/// produces an `INVALID_REQUEST` response and can make request routing
/// ambiguous in the caller.
pub async fn request(&self, request: ClientRequest) -> IoResult<PendingClientRequestResponse> {
self.client.request(request).await
pub fn request(
&self,
request: ClientRequest,
) -> impl std::future::Future<Output = IoResult<PendingClientRequestResponse>> + '_ {
self.client.request(request)
}
/// Sends a typed client notification into the in-process runtime.
@@ -443,14 +452,13 @@ async fn start_uninitialized(args: InProcessStartArgs) -> IoResult<InProcessClie
match command {
Some(ProcessorCommand::Request(request)) => {
let was_initialized = session.initialized();
processor
.process_client_request(
Box::pin(processor.process_client_request(
IN_PROCESS_CONNECTION_ID,
*request,
request,
Arc::clone(&session),
&outbound_initialized,
)
.await;
))
.await;
let opted_out_notification_methods_snapshot =
session.opted_out_notification_methods();
let experimental_api_enabled =
@@ -521,7 +529,6 @@ async fn start_uninitialized(args: InProcessStartArgs) -> IoResult<InProcessClie
message = client_rx.recv() => {
match message {
Some(InProcessClientMessage::Request { request, response_tx }) => {
let request = *request;
let request_id = request.id().clone();
match pending_request_responses.entry(request_id.clone()) {
Entry::Vacant(entry) => {
@@ -535,7 +542,7 @@ async fn start_uninitialized(args: InProcessStartArgs) -> IoResult<InProcessClie
}
}
match processor_tx.try_send(ProcessorCommand::Request(Box::new(request))) {
match processor_tx.try_send(ProcessorCommand::Request(request)) {
Ok(()) => {}
Err(mpsc::error::TrySendError::Full(_)) => {
if let Some(response_tx) =
+1 -1
View File
@@ -585,7 +585,7 @@ pub async fn run_main_with_transport_options(
});
}
if let Some(warning) =
codex_core::config::system_bwrap_warning(config.permissions.permission_profile.get())
codex_core::config::system_bwrap_warning(config.permissions.permission_profile_ref())
{
config_warnings.push(ConfigWarningNotification {
summary: warning,
+34 -34
View File
@@ -1,5 +1,6 @@
use std::collections::HashSet;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::OnceLock;
use std::sync::atomic::AtomicBool;
@@ -528,7 +529,7 @@ impl MessageProcessor {
Self::run_request_with_context(
Arc::clone(&self.outgoing),
request_context.clone(),
async {
Box::pin(async {
let codex_request = serde_json::to_value(&request)
.map_err(|err| invalid_request(format!("Invalid request: {err}")))
.and_then(|request_json| {
@@ -541,13 +542,13 @@ impl MessageProcessor {
// session state into outbound state and sending initialize notifications to
// this specific connection. Passing `None` avoids marking the connection
// ready too early from inside the shared request handler.
self.handle_client_request(
Box::pin(self.handle_client_request(
request_id.clone(),
codex_request,
Arc::clone(&session),
/*outbound_initialized*/ None,
request_context.clone(),
)
))
.await
}
Err(error) => Err(error),
@@ -555,7 +556,7 @@ impl MessageProcessor {
if let Err(error) = result {
self.outgoing.send_error(request_id.clone(), error).await;
}
},
}),
)
.await;
}
@@ -567,7 +568,7 @@ impl MessageProcessor {
pub(crate) async fn process_client_request(
self: &Arc<Self>,
connection_id: ConnectionId,
request: ClientRequest,
request: Box<ClientRequest>,
session: Arc<ConnectionSessionState>,
outbound_initialized: &AtomicBool,
) {
@@ -575,8 +576,11 @@ impl MessageProcessor {
connection_id,
request_id: request.id().clone(),
};
let request_span =
crate::app_server_tracing::typed_request_span(&request, connection_id, &session);
let request_span = crate::app_server_tracing::typed_request_span(
request.as_ref(),
connection_id,
&session,
);
let request_context =
RequestContext::new(request_id.clone(), request_span, /*parent_trace*/ None);
tracing::trace!(
@@ -587,23 +591,22 @@ impl MessageProcessor {
Self::run_request_with_context(
Arc::clone(&self.outgoing),
request_context.clone(),
async {
Box::pin(async {
// In-process clients do not have the websocket transport loop that performs
// post-initialize bookkeeping, so they still finalize outbound readiness in
// the shared request handler.
let result = self
.handle_client_request(
request_id.clone(),
request,
Arc::clone(&session),
Some(outbound_initialized),
request_context.clone(),
)
.await;
let result = Box::pin(self.handle_client_request(
request_id.clone(),
*request,
Arc::clone(&session),
Some(outbound_initialized),
request_context.clone(),
))
.await;
if let Err(error) = result {
self.outgoing.send_error(request_id.clone(), error).await;
}
},
}),
)
.await;
}
@@ -621,13 +624,11 @@ impl MessageProcessor {
tracing::info!("<- typed notification: {:?}", notification);
}
async fn run_request_with_context<F>(
async fn run_request_with_context(
outgoing: Arc<OutgoingMessageSender>,
request_context: RequestContext,
request_fut: F,
) where
F: Future<Output = ()>,
{
request_fut: Pin<Box<dyn Future<Output = ()> + Send + '_>>,
) {
outgoing
.register_request_context(request_context.clone())
.await;
@@ -765,12 +766,12 @@ impl MessageProcessor {
return Ok(());
}
self.dispatch_initialized_client_request(
Box::pin(self.dispatch_initialized_client_request(
connection_request_id,
codex_request,
session,
request_context,
)
))
.await
}
@@ -808,15 +809,14 @@ impl MessageProcessor {
rpc_gate,
async move {
let processor_for_request = Arc::clone(&processor);
let result = processor_for_request
.handle_initialized_client_request(
connection_request_id,
codex_request,
request_context,
app_server_client_name,
client_version,
)
.await;
let result = Box::pin(processor_for_request.handle_initialized_client_request(
connection_request_id,
codex_request,
request_context,
app_server_client_name,
client_version,
))
.await;
if let Err(error) = result {
processor.outgoing.send_error(error_request_id, error).await;
}
@@ -658,6 +658,7 @@ async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> {
}],
responsesapi_client_metadata: None,
cwd: None,
workspace_roots: None,
approval_policy: None,
sandbox_policy: None,
permissions: None,
@@ -103,8 +103,6 @@ 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::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;
@@ -354,6 +352,8 @@ use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
#[cfg(test)]
use codex_protocol::items::TurnItem;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::ResponseItem;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::protocol::AgentStatus;
@@ -431,6 +431,11 @@ use uuid::Uuid;
#[cfg(test)]
use codex_app_server_protocol::ServerRequest;
struct ResolvedPermissionProfileSelection {
permission_profile: PermissionProfile,
active_permission_profile: ActivePermissionProfile,
}
mod account_processor;
mod apps_processor;
mod catalog_processor;
@@ -442,6 +447,7 @@ mod feedback_processor;
mod fs_processor;
mod git_processor;
mod initialize_processor;
mod legacy_sandbox_compat;
mod marketplace_processor;
mod mcp_processor;
mod plugins;
@@ -164,7 +164,7 @@ impl CommandExecRequestProcessor {
let started_network_proxy = match self.config.permissions.network.as_ref() {
Some(spec) => match spec
.start_proxy(
self.config.permissions.permission_profile.get(),
self.config.permissions.permission_profile_ref(),
/*policy_decider*/ None,
/*blocked_request_observer*/ None,
managed_network_requirements_enabled,
@@ -200,7 +200,8 @@ impl CommandExecRequestProcessor {
} else {
ExecCapturePolicy::ShellTool
};
let sandbox_cwd = if permission_profile.is_some() {
let has_request_permission_profile = permission_profile.is_some();
let sandbox_cwd = if has_request_permission_profile {
cwd.clone()
} else {
self.config.cwd.clone()
@@ -243,28 +244,20 @@ impl CommandExecRequestProcessor {
);
self.config
.permissions
.permission_profile
.permission_profile_constraint()
.can_set(&effective_permission_profile)
.map_err(|err| invalid_request(format!("invalid permission profile: {err}")))?;
effective_permission_profile
} else if let Some(policy) = sandbox_policy.map(|policy| policy.to_core()) {
} else if let Some(policy) = sandbox_policy {
let legacy_policy = policy.to_core();
self.config
.permissions
.can_set_legacy_sandbox_policy(&policy, &sandbox_cwd)
.can_set_legacy_sandbox_policy(&legacy_policy, &sandbox_cwd)
.map_err(|err| invalid_request(format!("invalid sandbox policy: {err}")))?;
let file_system_sandbox_policy =
codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&policy, &sandbox_cwd);
let network_sandbox_policy =
codex_protocol::permissions::NetworkSandboxPolicy::from(&policy);
let permission_profile =
codex_protocol::models::PermissionProfile::from_runtime_permissions_with_enforcement(
codex_protocol::models::SandboxEnforcement::from_legacy_sandbox_policy(&policy),
&file_system_sandbox_policy,
network_sandbox_policy,
);
let permission_profile = policy.to_permission_profile_for_cwd(&sandbox_cwd);
self.config
.permissions
.permission_profile
.permission_profile_constraint()
.can_set(&permission_profile)
.map_err(|err| invalid_request(format!("invalid sandbox policy: {err}")))?;
permission_profile
@@ -283,10 +276,16 @@ impl CommandExecRequestProcessor {
None => None,
};
let workspace_roots = if has_request_permission_profile {
vec![sandbox_cwd.clone()]
} else {
self.config.workspace_roots.clone()
};
let exec_request = codex_core::exec::build_exec_request(
exec_params,
&effective_permission_profile,
&sandbox_cwd,
&workspace_roots,
&codex_linux_sandbox_exe,
use_legacy_landlock,
)
@@ -0,0 +1,272 @@
use super::*;
const WORKSPACE_PERMISSION_PROFILE_ID: &str = ":workspace";
const READ_ONLY_PERMISSION_PROFILE_ID: &str = ":read-only";
const DANGER_NO_SANDBOX_PERMISSION_PROFILE_ID: &str = ":danger-no-sandbox";
pub(super) struct CurrentPermissionProfile<'a> {
pub(super) permission_profile: &'a PermissionProfile,
pub(super) workspace_roots: &'a [AbsolutePathBuf],
}
pub(super) struct LegacySandboxProfileSelection {
pub(super) permissions: String,
pub(super) workspace_roots: Option<Vec<AbsolutePathBuf>>,
expected_enforcement: codex_protocol::models::SandboxEnforcement,
expected_network: codex_protocol::permissions::NetworkSandboxPolicy,
}
pub(super) enum LegacySandboxResolution {
Noop {
workspace_roots: Option<Vec<AbsolutePathBuf>>,
},
Selection(LegacySandboxProfileSelection),
}
pub(super) fn resolve_legacy_sandbox_profile_selection(
sandbox_policy: &codex_app_server_protocol::SandboxPolicy,
current: Option<CurrentPermissionProfile<'_>>,
cwd: &AbsolutePathBuf,
explicit_workspace_roots: Option<&[AbsolutePathBuf]>,
field_name: &str,
) -> Result<LegacySandboxResolution, JSONRPCErrorError> {
let legacy_workspace_roots =
workspace_roots_from_implicit_legacy_sandbox(cwd, sandbox_policy, explicit_workspace_roots);
if let Some(current_match) = legacy_sandbox_current_match(
sandbox_policy,
current,
cwd,
explicit_workspace_roots,
legacy_workspace_roots.as_deref(),
) {
return Ok(LegacySandboxResolution::Noop {
workspace_roots: current_match.workspace_roots,
});
}
let expected_network =
codex_protocol::permissions::NetworkSandboxPolicy::from(&sandbox_policy.to_core());
match sandbox_policy {
codex_app_server_protocol::SandboxPolicy::DangerFullAccess => Ok(
LegacySandboxResolution::Selection(LegacySandboxProfileSelection {
permissions: DANGER_NO_SANDBOX_PERMISSION_PROFILE_ID.to_string(),
workspace_roots: None,
expected_enforcement: codex_protocol::models::SandboxEnforcement::Disabled,
expected_network,
}),
),
codex_app_server_protocol::SandboxPolicy::ReadOnly { network_access: _ } => Ok(
LegacySandboxResolution::Selection(LegacySandboxProfileSelection {
permissions: READ_ONLY_PERMISSION_PROFILE_ID.to_string(),
workspace_roots: None,
expected_enforcement: codex_protocol::models::SandboxEnforcement::Managed,
expected_network,
}),
),
codex_app_server_protocol::SandboxPolicy::WorkspaceWrite { .. } => Ok(
LegacySandboxResolution::Selection(LegacySandboxProfileSelection {
permissions: WORKSPACE_PERMISSION_PROFILE_ID.to_string(),
workspace_roots: legacy_workspace_roots,
expected_enforcement: codex_protocol::models::SandboxEnforcement::Managed,
expected_network,
}),
),
codex_app_server_protocol::SandboxPolicy::ExternalSandbox { .. } => {
Err(invalid_request(format!(
"`{field_name}` externalSandbox cannot be mapped to a named permissions profile"
)))
}
}
}
pub(super) fn sandbox_policy_from_legacy_mode(
sandbox_mode: SandboxMode,
) -> codex_app_server_protocol::SandboxPolicy {
match sandbox_mode {
SandboxMode::ReadOnly => codex_protocol::protocol::SandboxPolicy::new_read_only_policy(),
SandboxMode::WorkspaceWrite => {
codex_protocol::protocol::SandboxPolicy::new_workspace_write_policy()
}
SandboxMode::DangerFullAccess => codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
}
.into()
}
pub(super) fn validate_legacy_sandbox_profile_selection(
legacy_selection: &LegacySandboxProfileSelection,
resolved_selection: &ResolvedPermissionProfileSelection,
field_name: &str,
) -> Result<(), JSONRPCErrorError> {
let permission_profile = &resolved_selection.permission_profile;
if permission_profile.enforcement() != legacy_selection.expected_enforcement {
return Err(invalid_request(format!(
"`{field_name}` does not match permissions profile `{}`",
legacy_selection.permissions
)));
}
if permission_profile.network_sandbox_policy() != legacy_selection.expected_network {
return Err(invalid_request(format!(
"`{field_name}` network access does not match permissions profile `{}`",
legacy_selection.permissions
)));
}
Ok(())
}
pub(super) fn resolve_cwd_against_fallback(
cwd: Option<&Path>,
fallback_cwd: &AbsolutePathBuf,
) -> AbsolutePathBuf {
match cwd {
Some(cwd) => {
if let Ok(path) = AbsolutePathBuf::try_from(cwd) {
path
} else {
AbsolutePathBuf::resolve_path_against_base(cwd, fallback_cwd.as_path())
}
}
None => fallback_cwd.clone(),
}
}
struct LegacySandboxCurrentMatch {
workspace_roots: Option<Vec<AbsolutePathBuf>>,
}
fn legacy_sandbox_current_match(
sandbox_policy: &codex_app_server_protocol::SandboxPolicy,
current: Option<CurrentPermissionProfile<'_>>,
cwd: &AbsolutePathBuf,
explicit_workspace_roots: Option<&[AbsolutePathBuf]>,
legacy_workspace_roots: Option<&[AbsolutePathBuf]>,
) -> Option<LegacySandboxCurrentMatch> {
let current = current?;
let projection_workspace_roots = explicit_workspace_roots
.or(legacy_workspace_roots)
.unwrap_or(current.workspace_roots);
let materialized_profile = current
.permission_profile
.materialize_project_roots_with_workspace_roots(projection_workspace_roots);
let file_system_policy = materialized_profile.file_system_sandbox_policy();
let active_sandbox = codex_sandboxing::compatibility_sandbox_policy_for_permission_profile(
&materialized_profile,
&file_system_policy,
materialized_profile.network_sandbox_policy(),
cwd.as_path(),
);
if active_sandbox != sandbox_policy.to_core() {
return None;
}
let workspace_roots = legacy_workspace_roots
.filter(|roots| *roots != current.workspace_roots)
.map(<[AbsolutePathBuf]>::to_vec);
Some(LegacySandboxCurrentMatch { workspace_roots })
}
fn workspace_roots_from_implicit_legacy_sandbox(
cwd: &AbsolutePathBuf,
sandbox_policy: &codex_app_server_protocol::SandboxPolicy,
explicit_workspace_roots: Option<&[AbsolutePathBuf]>,
) -> Option<Vec<AbsolutePathBuf>> {
if explicit_workspace_roots.is_some()
|| !matches!(
sandbox_policy,
codex_app_server_protocol::SandboxPolicy::WorkspaceWrite { .. }
)
{
None
} else {
Some(workspace_roots_from_legacy_sandbox(cwd, sandbox_policy))
}
}
fn workspace_roots_from_legacy_sandbox(
cwd: &AbsolutePathBuf,
sandbox_policy: &codex_app_server_protocol::SandboxPolicy,
) -> Vec<AbsolutePathBuf> {
let mut roots = Vec::with_capacity(1 + sandbox_policy.legacy_writable_roots().len());
push_unique_root(&mut roots, cwd.clone());
for root in sandbox_policy.legacy_writable_roots() {
push_unique_root(&mut roots, root.clone());
}
roots
}
fn push_unique_root(roots: &mut Vec<AbsolutePathBuf>, root: AbsolutePathBuf) {
if !roots.iter().any(|existing| existing == &root) {
roots.push(root);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn abs_test_path(name: &str) -> AbsolutePathBuf {
AbsolutePathBuf::from_absolute_path(std::env::temp_dir().join(name))
.expect("temp dir path should be absolute")
}
fn workspace_write_policy() -> codex_app_server_protocol::SandboxPolicy {
codex_app_server_protocol::SandboxPolicy::WorkspaceWrite {
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
legacy_writable_roots: Vec::new(),
}
}
#[test]
fn legacy_workspace_sandbox_updates_roots_when_current_profile_matches_new_cwd() {
let old_root = abs_test_path("codex-old-workspace-root");
let cwd = abs_test_path("codex-new-workspace-root");
let policy = workspace_write_policy();
let resolution = resolve_legacy_sandbox_profile_selection(
&policy,
Some(CurrentPermissionProfile {
permission_profile: &PermissionProfile::workspace_write(),
workspace_roots: std::slice::from_ref(&old_root),
}),
&cwd,
/*explicit_workspace_roots*/ None,
"sandboxPolicy",
)
.expect("legacy sandbox should resolve");
match resolution {
LegacySandboxResolution::Noop {
workspace_roots: Some(workspace_roots),
} => assert_eq!(workspace_roots, vec![cwd]),
_ => panic!("expected workspace-roots-only resolution, got unexpected selection"),
}
}
#[test]
fn legacy_workspace_sandbox_is_noop_when_current_workspace_roots_match() {
let cwd = abs_test_path("codex-current-workspace-root");
let policy = workspace_write_policy();
let resolution = resolve_legacy_sandbox_profile_selection(
&policy,
Some(CurrentPermissionProfile {
permission_profile: &PermissionProfile::workspace_write(),
workspace_roots: std::slice::from_ref(&cwd),
}),
&cwd,
/*explicit_workspace_roots*/ None,
"sandboxPolicy",
)
.expect("legacy sandbox should resolve");
match resolution {
LegacySandboxResolution::Noop {
workspace_roots: None,
} => {}
_ => panic!("expected no-op resolution, got unexpected workspace roots or selection"),
}
}
}
@@ -604,11 +604,13 @@ pub(super) async fn handle_pending_thread_resume_request(
permission_profile,
active_permission_profile,
cwd,
workspace_roots,
reasoning_effort,
..
} = pending.config_snapshot;
let instruction_sources = pending.instruction_sources;
let sandbox = thread_response_sandbox_policy(&permission_profile, cwd.as_path());
let sandbox =
thread_response_sandbox_policy(&permission_profile, &workspace_roots, cwd.as_path());
let active_permission_profile =
thread_response_active_permission_profile(active_permission_profile);
let session_id = conversation.session_configured().session_id.to_string();
@@ -624,7 +626,7 @@ pub(super) async fn handle_pending_thread_resume_request(
approval_policy: approval_policy.into(),
approvals_reviewer: approvals_reviewer.into(),
sandbox,
permission_profile: Some(permission_profile.into()),
workspace_roots,
active_permission_profile,
reasoning_effort,
};
@@ -1,3 +1,4 @@
use super::legacy_sandbox_compat::*;
use super::*;
use crate::error_code::method_not_found;
@@ -17,6 +18,119 @@ struct ThreadListFilters {
use_state_db_only: bool,
}
#[derive(Clone)]
struct PersistedThreadPermissionState {
permission_profile: PermissionProfile,
active_permission_profile: Option<ActivePermissionProfile>,
workspace_roots: Vec<AbsolutePathBuf>,
}
fn absolute_path_from_history_path(
path: &Path,
base: Option<&AbsolutePathBuf>,
) -> Option<AbsolutePathBuf> {
if let Ok(path) = AbsolutePathBuf::try_from(path) {
Some(path)
} else if let Some(base) = base {
Some(AbsolutePathBuf::resolve_path_against_base(
path,
base.as_path(),
))
} else {
AbsolutePathBuf::relative_to_current_dir(path).ok()
}
}
fn roots_or_cwd(
roots: Vec<AbsolutePathBuf>,
cwd: Option<&AbsolutePathBuf>,
) -> Vec<AbsolutePathBuf> {
if roots.is_empty() {
cwd.cloned().into_iter().collect()
} else {
roots
}
}
fn effective_cwd_for_legacy_sandbox(
request_cwd: Option<&str>,
history_cwd: Option<&Path>,
persisted_permission_state: Option<&PersistedThreadPermissionState>,
) -> Option<AbsolutePathBuf> {
let history_cwd =
history_cwd.and_then(|cwd| absolute_path_from_history_path(cwd, /*base*/ None));
request_cwd
.and_then(|cwd| absolute_path_from_history_path(Path::new(cwd), history_cwd.as_ref()))
.or_else(|| history_cwd.clone())
.or_else(|| {
persisted_permission_state
.and_then(|state| state.workspace_roots.first())
.cloned()
})
}
fn persisted_thread_permission_state(
history: &InitialHistory,
fallback_cwd: Option<&Path>,
fallback_sandbox_policy: Option<&codex_protocol::protocol::SandboxPolicy>,
) -> Option<PersistedThreadPermissionState> {
let mut cwd =
fallback_cwd.and_then(|cwd| absolute_path_from_history_path(cwd, /*base*/ None));
let mut workspace_roots = None;
let mut permission_profile = None;
let mut active_permission_profile = None;
for item in history.get_rollout_items() {
match item {
RolloutItem::SessionMeta(meta_line) => {
cwd = absolute_path_from_history_path(meta_line.meta.cwd.as_path(), cwd.as_ref())
.or(cwd);
workspace_roots = Some(roots_or_cwd(meta_line.meta.workspace_roots, cwd.as_ref()));
}
RolloutItem::TurnContext(context) => {
cwd = absolute_path_from_history_path(context.cwd.as_path(), cwd.as_ref()).or(cwd);
workspace_roots = Some(context.workspace_roots);
let context_cwd = cwd
.as_ref()
.map(AbsolutePathBuf::as_path)
.unwrap_or(context.cwd.as_path());
permission_profile = Some(context.permission_profile.unwrap_or_else(|| {
PermissionProfile::from_legacy_sandbox_policy_for_cwd(
&context.sandbox_policy,
context_cwd,
)
}));
if context.active_permission_profile.is_some() {
active_permission_profile = context.active_permission_profile;
}
}
RolloutItem::EventMsg(EventMsg::SessionConfigured(event)) => {
cwd = Some(event.cwd.clone());
workspace_roots = Some(event.workspace_roots);
permission_profile = Some(event.permission_profile);
active_permission_profile = event.active_permission_profile;
}
RolloutItem::ResponseItem(_) | RolloutItem::Compacted(_) | RolloutItem::EventMsg(_) => {
}
}
}
if permission_profile.is_none() {
let cwd = cwd.as_ref()?;
let fallback_sandbox_policy = fallback_sandbox_policy?;
permission_profile = Some(PermissionProfile::from_legacy_sandbox_policy_for_cwd(
fallback_sandbox_policy,
cwd.as_path(),
));
}
Some(PersistedThreadPermissionState {
permission_profile: permission_profile?,
active_permission_profile,
workspace_roots: workspace_roots.unwrap_or_else(|| cwd.into_iter().collect()),
})
}
fn collect_resume_override_mismatches(
request: &ThreadResumeParams,
config_snapshot: &ThreadConfigSnapshot,
@@ -98,12 +212,6 @@ fn collect_resume_override_mismatches(
));
}
}
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)
{
@@ -802,6 +910,7 @@ impl ThreadRequestProcessor {
model_provider,
service_tier,
cwd,
workspace_roots,
approval_policy,
approvals_reviewer,
sandbox,
@@ -835,6 +944,7 @@ impl ThreadRequestProcessor {
model_provider,
service_tier,
cwd,
workspace_roots,
approval_policy,
approvals_reviewer,
sandbox,
@@ -975,7 +1085,7 @@ impl ThreadRequestProcessor {
let requested_permissions_trust_project =
requested_permissions_trust_project(&typesafe_overrides, config.cwd.as_path());
let effective_permissions_trust_project = permission_profile_trusts_project(
&config.permissions.permission_profile(),
config.permissions.permission_profile_ref(),
config.cwd.as_path(),
);
@@ -1160,6 +1270,7 @@ impl ThreadRequestProcessor {
let sandbox = thread_response_sandbox_policy(
&config_snapshot.permission_profile,
&config_snapshot.workspace_roots,
config_snapshot.cwd.as_path(),
);
let active_permission_profile =
@@ -1175,7 +1286,7 @@ impl ThreadRequestProcessor {
approval_policy: config_snapshot.approval_policy.into(),
approvals_reviewer: config_snapshot.approvals_reviewer.into(),
sandbox,
permission_profile: Some(config_snapshot.permission_profile.into()),
workspace_roots: config_snapshot.workspace_roots,
active_permission_profile,
reasoning_effort: config_snapshot.reasoning_effort,
};
@@ -1212,10 +1323,11 @@ impl ThreadRequestProcessor {
model_provider: Option<String>,
service_tier: Option<Option<String>>,
cwd: Option<String>,
workspace_roots: Option<Vec<AbsolutePathBuf>>,
approval_policy: Option<codex_app_server_protocol::AskForApproval>,
approvals_reviewer: Option<codex_app_server_protocol::ApprovalsReviewer>,
sandbox: Option<SandboxMode>,
permissions: Option<PermissionProfileSelectionParams>,
permissions: Option<String>,
base_instructions: Option<String>,
developer_instructions: Option<String>,
personality: Option<Personality>,
@@ -1225,6 +1337,8 @@ impl ThreadRequestProcessor {
model_provider,
service_tier,
cwd: cwd.map(PathBuf::from),
workspace_roots: workspace_roots
.map(|roots| roots.into_iter().map(|root| root.to_path_buf()).collect()),
approval_policy: approval_policy
.map(codex_app_server_protocol::AskForApproval::to_core),
approvals_reviewer: approvals_reviewer
@@ -1241,6 +1355,47 @@ impl ThreadRequestProcessor {
overrides
}
async fn validate_active_permission_profile_selection(
&self,
permissions: String,
request_overrides: Option<HashMap<String, serde_json::Value>>,
cwd: Option<PathBuf>,
fallback_cwd: Option<PathBuf>,
) -> Result<ResolvedPermissionProfileSelection, JSONRPCErrorError> {
let mut overrides = ConfigOverrides {
cwd,
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, overrides, fallback_cwd)
.await
.map_err(|err| config_load_error(&err))?;
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 permission profile selection: {warning}"
)));
}
let active_permission_profile =
config
.permissions
.active_permission_profile()
.ok_or_else(|| {
invalid_request(
"permission profile selection did not resolve to a named profile",
)
})?;
Ok(ResolvedPermissionProfileSelection {
permission_profile: config.permissions.permission_profile(),
active_permission_profile,
})
}
fn parse_environment_selections(
&self,
environments: Option<Vec<TurnEnvironmentParams>>,
@@ -2381,6 +2536,7 @@ impl ThreadRequestProcessor {
model_provider,
service_tier,
cwd,
workspace_roots,
approval_policy,
approvals_reviewer,
sandbox,
@@ -2393,6 +2549,7 @@ impl ThreadRequestProcessor {
persist_extended_history: _persist_extended_history,
} = params;
let include_turns = !exclude_turns;
let mut workspace_roots = workspace_roots;
let (thread_history, resume_source_thread) = match if let Some(history) = history {
self.resume_thread_from_history(history.as_slice())
@@ -2411,19 +2568,147 @@ impl ThreadRequestProcessor {
};
let history_cwd = thread_history.session_cwd();
let persisted_permission_state = persisted_thread_permission_state(
&thread_history,
history_cwd.as_deref(),
resume_source_thread
.as_ref()
.map(|thread| &thread.sandbox_policy),
);
let permission_profile_selection = if let Some(permissions) = permissions {
match self
.validate_active_permission_profile_selection(
permissions,
request_overrides.clone(),
cwd.clone().map(PathBuf::from),
history_cwd.clone(),
)
.await
{
Ok(selection) => Some(selection),
Err(error) => {
self.outgoing.send_error(request_id, error).await;
return Ok(());
}
}
} else if let Some(sandbox_mode) = sandbox {
let sandbox_policy = sandbox_policy_from_legacy_mode(sandbox_mode);
let Some(effective_cwd) = effective_cwd_for_legacy_sandbox(
cwd.as_deref(),
history_cwd.as_deref(),
persisted_permission_state.as_ref(),
) else {
self.outgoing
.send_error(
request_id,
invalid_request("`sandbox` requires a cwd to resolve legacy permissions"),
)
.await;
return Ok(());
};
match resolve_legacy_sandbox_profile_selection(
&sandbox_policy,
persisted_permission_state
.as_ref()
.map(|state| CurrentPermissionProfile {
permission_profile: &state.permission_profile,
workspace_roots: &state.workspace_roots,
}),
&effective_cwd,
workspace_roots.as_deref(),
"sandbox",
) {
Ok(LegacySandboxResolution::Noop {
workspace_roots: legacy_workspace_roots,
}) => {
if workspace_roots.is_none() {
workspace_roots = legacy_workspace_roots;
}
None
}
Ok(LegacySandboxResolution::Selection(legacy_selection)) => {
if workspace_roots.is_none() {
workspace_roots = legacy_selection.workspace_roots.clone();
}
match self
.validate_active_permission_profile_selection(
legacy_selection.permissions.clone(),
request_overrides.clone(),
cwd.clone().map(PathBuf::from),
history_cwd.clone(),
)
.await
.and_then(|selection| {
validate_legacy_sandbox_profile_selection(
&legacy_selection,
&selection,
"sandbox",
)
.map(|()| selection)
}) {
Ok(selection) => Some(selection),
Err(error) => {
self.outgoing.send_error(request_id, error).await;
return Ok(());
}
}
}
Err(error) => {
self.outgoing.send_error(request_id, error).await;
return Ok(());
}
}
} else {
None
};
let active_permission_profile = permission_profile_selection
.as_ref()
.map(|selection| selection.active_permission_profile.clone())
.or_else(|| {
persisted_permission_state
.as_ref()
.and_then(|state| state.active_permission_profile.clone())
});
let expected_active_permission_profile_source = permission_profile_selection
.as_ref()
.map(|selection| selection.permission_profile.clone())
.or_else(|| {
persisted_permission_state
.as_ref()
.map(|state| state.permission_profile.clone())
});
let workspace_roots_were_explicit = workspace_roots.is_some();
let mut typesafe_overrides = self.build_thread_config_overrides(
model,
model_provider,
service_tier,
cwd,
workspace_roots,
approval_policy,
approvals_reviewer,
sandbox,
permissions,
/*sandbox*/ None,
/*permissions*/ None,
base_instructions,
developer_instructions,
personality,
);
typesafe_overrides.permission_profile = expected_active_permission_profile_source.clone();
if !workspace_roots_were_explicit {
if let Some(persisted_permission_state) = persisted_permission_state.as_ref() {
typesafe_overrides.workspace_roots = Some(
persisted_permission_state
.workspace_roots
.iter()
.map(codex_utils_absolute_path::AbsolutePathBuf::to_path_buf)
.collect(),
);
} else if let Some(root) = history_cwd
.as_deref()
.and_then(|cwd| absolute_path_from_history_path(cwd, /*base*/ None))
{
typesafe_overrides.workspace_roots = Some(vec![root.to_path_buf()]);
}
}
self.load_and_apply_persisted_resume_metadata(
&thread_history,
&mut request_overrides,
@@ -2432,7 +2717,7 @@ impl ThreadRequestProcessor {
.await;
// Derive a Config using the same logic as new conversation, honoring overrides if provided.
let config = match self
let mut config = match self
.config_manager
.load_for_cwd(request_overrides, typesafe_overrides, history_cwd)
.await
@@ -2444,6 +2729,12 @@ impl ThreadRequestProcessor {
return Ok(());
}
};
config
.permissions
.set_active_permission_profile_for_current_profile(
active_permission_profile,
expected_active_permission_profile_source.as_ref(),
);
let instruction_sources = Self::instruction_sources_from_config(&config).await;
let response_history = thread_history.clone();
@@ -2537,6 +2828,7 @@ impl ThreadRequestProcessor {
let config_snapshot = codex_thread.config_snapshot().await;
let sandbox = thread_response_sandbox_policy(
&config_snapshot.permission_profile,
&config_snapshot.workspace_roots,
config_snapshot.cwd.as_path(),
);
let active_permission_profile = thread_response_active_permission_profile(
@@ -2557,7 +2849,7 @@ impl ThreadRequestProcessor {
approval_policy: session_configured.approval_policy.into(),
approvals_reviewer: session_configured.approvals_reviewer.into(),
sandbox,
permission_profile: Some(config_snapshot.permission_profile.into()),
workspace_roots: config_snapshot.workspace_roots,
active_permission_profile,
reasoning_effort: session_configured.reasoning_effort,
};
@@ -2711,6 +3003,94 @@ impl ThreadRequestProcessor {
)
.await?;
let config_snapshot = existing_thread.config_snapshot().await;
let mut workspace_roots = params.workspace_roots.clone();
let permission_profile_selection = if let Some(permissions) = params.permissions.clone()
{
Some(
self.validate_active_permission_profile_selection(
permissions,
/*request_overrides*/ None,
/*cwd*/ None,
Some(config_snapshot.cwd.to_path_buf()),
)
.await?,
)
} else if let Some(sandbox_mode) = params.sandbox {
let sandbox_policy = sandbox_policy_from_legacy_mode(sandbox_mode);
match resolve_legacy_sandbox_profile_selection(
&sandbox_policy,
Some(CurrentPermissionProfile {
permission_profile: &config_snapshot.permission_profile,
workspace_roots: &config_snapshot.workspace_roots,
}),
&config_snapshot.cwd,
workspace_roots.as_deref(),
"sandbox",
)? {
LegacySandboxResolution::Noop {
workspace_roots: legacy_workspace_roots,
} => {
if workspace_roots.is_none() {
workspace_roots = legacy_workspace_roots;
}
None
}
LegacySandboxResolution::Selection(legacy_selection) => {
if workspace_roots.is_none() {
workspace_roots = legacy_selection.workspace_roots.clone();
}
let selection = self
.validate_active_permission_profile_selection(
legacy_selection.permissions.clone(),
/*request_overrides*/ None,
/*cwd*/ None,
Some(config_snapshot.cwd.to_path_buf()),
)
.await?;
validate_legacy_sandbox_profile_selection(
&legacy_selection,
&selection,
"sandbox",
)?;
Some(selection)
}
}
} else {
None
};
if workspace_roots.is_some() || permission_profile_selection.is_some() {
existing_thread
.update_turn_context_overrides(CodexThreadTurnContextOverrides {
cwd: None,
workspace_roots: workspace_roots.map(|roots| {
roots
.into_iter()
.map(AbsolutePathBuf::into_path_buf)
.collect()
}),
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: permission_profile_selection
.as_ref()
.map(|selection| selection.permission_profile.clone()),
active_permission_profile: permission_profile_selection
.map(|selection| selection.active_permission_profile),
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await
.map_err(|err| {
invalid_request(format!("invalid thread resume override: {err}"))
})?;
}
let config_snapshot = existing_thread.config_snapshot().await;
let mismatch_details = collect_resume_override_mismatches(params, &config_snapshot);
if !mismatch_details.is_empty() {
@@ -3017,6 +3397,7 @@ impl ThreadRequestProcessor {
model_provider,
service_tier,
cwd,
workspace_roots,
approval_policy,
approvals_reviewer,
sandbox,
@@ -3077,26 +3458,123 @@ impl ThreadRequestProcessor {
} else {
Some(cli_overrides)
};
let fork_history = InitialHistory::Forked(history_items.clone());
let persisted_permission_state = persisted_thread_permission_state(
&fork_history,
history_cwd.as_deref(),
Some(&source_thread.sandbox_policy),
)
.ok_or_else(|| {
invalid_request("thread history is missing persisted permission configuration")
})?;
let mut workspace_roots = workspace_roots;
let permission_profile_selection = if let Some(permissions) = permissions {
Some(
self.validate_active_permission_profile_selection(
permissions,
request_overrides.clone(),
cwd.clone().map(PathBuf::from),
history_cwd.clone(),
)
.await?,
)
} else if let Some(sandbox_mode) = sandbox {
let sandbox_policy = sandbox_policy_from_legacy_mode(sandbox_mode);
let effective_cwd = effective_cwd_for_legacy_sandbox(
cwd.as_deref(),
history_cwd.as_deref(),
Some(&persisted_permission_state),
)
.ok_or_else(|| {
invalid_request("`sandbox` requires a cwd to resolve legacy permissions")
})?;
match resolve_legacy_sandbox_profile_selection(
&sandbox_policy,
Some(CurrentPermissionProfile {
permission_profile: &persisted_permission_state.permission_profile,
workspace_roots: &persisted_permission_state.workspace_roots,
}),
&effective_cwd,
workspace_roots.as_deref(),
"sandbox",
)? {
LegacySandboxResolution::Noop {
workspace_roots: legacy_workspace_roots,
} => {
if workspace_roots.is_none() {
workspace_roots = legacy_workspace_roots;
}
None
}
LegacySandboxResolution::Selection(legacy_selection) => {
if workspace_roots.is_none() {
workspace_roots = legacy_selection.workspace_roots.clone();
}
let selection = self
.validate_active_permission_profile_selection(
legacy_selection.permissions.clone(),
request_overrides.clone(),
cwd.clone().map(PathBuf::from),
history_cwd.clone(),
)
.await?;
validate_legacy_sandbox_profile_selection(
&legacy_selection,
&selection,
"sandbox",
)?;
Some(selection)
}
}
} else {
None
};
let active_permission_profile = permission_profile_selection
.as_ref()
.map(|selection| selection.active_permission_profile.clone())
.or_else(|| persisted_permission_state.active_permission_profile.clone());
let expected_active_permission_profile_source = permission_profile_selection
.as_ref()
.map(|selection| selection.permission_profile.clone())
.or_else(|| Some(persisted_permission_state.permission_profile.clone()));
let workspace_roots_were_explicit = workspace_roots.is_some();
let mut typesafe_overrides = self.build_thread_config_overrides(
model,
model_provider,
service_tier,
cwd,
workspace_roots,
approval_policy,
approvals_reviewer,
sandbox,
permissions,
/*sandbox*/ None,
/*permissions*/ None,
base_instructions,
developer_instructions,
/*personality*/ None,
);
typesafe_overrides.permission_profile = expected_active_permission_profile_source.clone();
if !workspace_roots_were_explicit {
typesafe_overrides.workspace_roots = Some(
persisted_permission_state
.workspace_roots
.iter()
.map(codex_utils_absolute_path::AbsolutePathBuf::to_path_buf)
.collect(),
);
}
typesafe_overrides.ephemeral = ephemeral.then_some(true);
// Derive a Config using the same logic as new conversation, honoring overrides if provided.
let config = self
let mut config = self
.config_manager
.load_for_cwd(request_overrides, typesafe_overrides, history_cwd)
.await
.map_err(|err| config_load_error(&err))?;
config
.permissions
.set_active_permission_profile_for_current_profile(
active_permission_profile,
expected_active_permission_profile_source.as_ref(),
);
let fallback_model_provider = config.model_provider_id.clone();
let instruction_sources = Self::instruction_sources_from_config(&config).await;
@@ -3200,6 +3678,7 @@ impl ThreadRequestProcessor {
let config_snapshot = forked_thread.config_snapshot().await;
let sandbox = thread_response_sandbox_policy(
&config_snapshot.permission_profile,
&config_snapshot.workspace_roots,
config_snapshot.cwd.as_path(),
);
let active_permission_profile =
@@ -3215,7 +3694,7 @@ impl ThreadRequestProcessor {
approval_policy: session_configured.approval_policy.into(),
approvals_reviewer: session_configured.approvals_reviewer.into(),
sandbox,
permission_profile: Some(config_snapshot.permission_profile.into()),
workspace_roots: config_snapshot.workspace_roots,
active_permission_profile,
reasoning_effort: session_configured.reasoning_effort,
};
@@ -630,6 +630,7 @@ mod thread_processor_behavior_tests {
model_provider: None,
service_tier: Some(Some("priority".to_string())),
cwd: None,
workspace_roots: None,
approval_policy: None,
approvals_reviewer: None,
sandbox: None,
@@ -650,6 +651,7 @@ mod thread_processor_behavior_tests {
permission_profile: codex_protocol::models::PermissionProfile::Disabled,
active_permission_profile: None,
cwd,
workspace_roots: Vec::new(),
ephemeral: false,
reasoning_effort: None,
personality: None,
@@ -177,32 +177,26 @@ pub(super) fn thread_response_active_permission_profile(
pub(super) fn apply_permission_profile_selection_to_config_overrides(
overrides: &mut ConfigOverrides,
permissions: Option<PermissionProfileSelectionParams>,
permissions: Option<String>,
) {
let Some(PermissionProfileSelectionParams::Profile { id, modifications }) = permissions else {
let Some(id) = 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()
}
},
));
}
pub(super) fn thread_response_sandbox_policy(
permission_profile: &codex_protocol::models::PermissionProfile,
workspace_roots: &[AbsolutePathBuf],
cwd: &Path,
) -> codex_app_server_protocol::SandboxPolicy {
let file_system_policy = permission_profile.file_system_sandbox_policy();
let materialized_permission_profile =
permission_profile.materialize_project_roots_with_workspace_roots(workspace_roots);
let file_system_policy = materialized_permission_profile.file_system_sandbox_policy();
let sandbox_policy = codex_sandboxing::compatibility_sandbox_policy_for_permission_profile(
permission_profile,
&materialized_permission_profile,
&file_system_policy,
permission_profile.network_sandbox_policy(),
materialized_permission_profile.network_sandbox_policy(),
cwd,
);
sandbox_policy.into()
@@ -1,3 +1,4 @@
use super::legacy_sandbox_compat::*;
use super::*;
#[derive(Clone)]
@@ -357,6 +358,7 @@ impl TurnRequestProcessor {
let turn_has_input = !mapped_items.is_empty();
let has_any_overrides = params.cwd.is_some()
|| params.workspace_roots.is_some()
|| params.approval_policy.is_some()
|| params.approvals_reviewer.is_some()
|| params.sandbox_policy.is_some()
@@ -375,47 +377,70 @@ impl TurnRequestProcessor {
}
let cwd = params.cwd;
let mut workspace_roots = params.workspace_roots;
let approval_policy = params.approval_policy.map(AskForApproval::to_core);
let approvals_reviewer = params
.approvals_reviewer
.map(codex_app_server_protocol::ApprovalsReviewer::to_core);
let sandbox_policy = params.sandbox_policy.map(|p| p.to_core());
let legacy_sandbox_policy = params.sandbox_policy;
let sandbox_policy = None;
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,
let selection = self
.validate_active_permission_profile_selection(
permissions,
cwd.clone(),
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}"
)));
}
.await?;
(
Some(config.permissions.permission_profile()),
config.permissions.active_permission_profile(),
Some(selection.permission_profile),
Some(selection.active_permission_profile),
)
} else if let Some(legacy_sandbox_policy) = legacy_sandbox_policy.as_ref() {
let snapshot = thread.config_snapshot().await;
let effective_cwd = resolve_cwd_against_fallback(cwd.as_deref(), &snapshot.cwd);
match resolve_legacy_sandbox_profile_selection(
legacy_sandbox_policy,
Some(CurrentPermissionProfile {
permission_profile: &snapshot.permission_profile,
workspace_roots: &snapshot.workspace_roots,
}),
&effective_cwd,
workspace_roots.as_deref(),
"sandboxPolicy",
)? {
LegacySandboxResolution::Noop {
workspace_roots: legacy_workspace_roots,
} => {
if workspace_roots.is_none() {
workspace_roots = legacy_workspace_roots;
}
(None, None)
}
LegacySandboxResolution::Selection(legacy_selection) => {
if workspace_roots.is_none() {
workspace_roots = legacy_selection.workspace_roots.clone();
}
let selection = self
.validate_active_permission_profile_selection(
legacy_selection.permissions.clone(),
cwd.clone(),
Some(snapshot.cwd.to_path_buf()),
)
.await?;
validate_legacy_sandbox_profile_selection(
&legacy_selection,
&selection,
"sandboxPolicy",
)?;
(
Some(selection.permission_profile),
Some(selection.active_permission_profile),
)
}
}
} else {
(None, None)
};
@@ -432,6 +457,9 @@ impl TurnRequestProcessor {
thread
.validate_turn_context_overrides(CodexThreadTurnContextOverrides {
cwd: cwd.clone(),
workspace_roots: workspace_roots
.clone()
.map(|roots| roots.into_iter().map(|root| root.to_path_buf()).collect()),
approval_policy,
approvals_reviewer,
sandbox_policy: sandbox_policy.clone(),
@@ -457,6 +485,7 @@ impl TurnRequestProcessor {
final_output_json_schema: params.output_schema,
responsesapi_client_metadata: params.responsesapi_client_metadata,
cwd,
workspace_roots,
approval_policy,
approvals_reviewer,
sandbox_policy,
@@ -516,6 +545,49 @@ impl TurnRequestProcessor {
Ok(TurnStartResponse { turn })
}
async fn validate_active_permission_profile_selection(
&self,
permissions: String,
cwd: Option<PathBuf>,
fallback_cwd: Option<PathBuf>,
) -> Result<ResolvedPermissionProfileSelection, JSONRPCErrorError> {
let mut overrides = ConfigOverrides {
cwd,
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, fallback_cwd)
.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}"
)));
}
let active_permission_profile =
config
.permissions
.active_permission_profile()
.ok_or_else(|| {
invalid_request(
"permission profile selection did not resolve to a named profile",
)
})?;
Ok(ResolvedPermissionProfileSelection {
permission_profile: config.permissions.permission_profile(),
active_permission_profile,
})
}
async fn thread_inject_items_response_inner(
&self,
params: ThreadInjectItemsParams,
@@ -135,6 +135,7 @@ pub fn create_fake_rollout_with_source(
forked_from_id: None,
timestamp: meta_rfc3339.to_string(),
cwd: PathBuf::from("/"),
workspace_roots: Vec::new(),
originator: "codex".to_string(),
cli_version: "0.0.0".to_string(),
source,
@@ -219,6 +220,7 @@ pub fn create_fake_rollout_with_text_elements(
forked_from_id: None,
timestamp: meta_rfc3339.to_string(),
cwd: PathBuf::from("/"),
workspace_roots: Vec::new(),
originator: "codex".to_string(),
cli_version: "0.0.0".to_string(),
source: SessionSource::Cli,
@@ -132,6 +132,7 @@ async fn get_conversation_summary_by_thread_id_reads_pathless_store_thread() ->
cwd: None,
model_provider: "test-provider".to_string(),
memory_mode: ThreadMemoryMode::Disabled,
workspace_roots: Vec::new(),
},
event_persistence_mode: ThreadEventPersistenceMode::default(),
})
@@ -618,6 +618,7 @@ async fn skills_changed_notification_is_emitted_after_skill_change() -> Result<(
approvals_reviewer: None,
sandbox: None,
permissions: None,
workspace_roots: None,
config: None,
service_name: None,
base_instructions: None,
@@ -610,6 +610,7 @@ sqlite = true
codex_home: codex_home.path().to_path_buf(),
sqlite_home: codex_home.path().to_path_buf(),
cwd: codex_home.path().to_path_buf(),
workspace_roots: Vec::new(),
model_provider_id: "mock_provider".to_string(),
generate_memories: false,
};
@@ -1288,6 +1288,7 @@ async fn seed_pathless_store_thread(
dynamic_tools: Vec::new(),
metadata: ThreadPersistenceMetadata {
cwd: None,
workspace_roots: Vec::new(),
model_provider: "test-provider".to_string(),
memory_mode: ThreadMemoryMode::Disabled,
},
@@ -26,6 +26,7 @@ use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::PatchApplyStatus;
use codex_app_server_protocol::PatchChangeKind;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::SandboxMode;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ServerRequest;
use codex_app_server_protocol::SessionSource;
@@ -1474,6 +1475,7 @@ stream_max_retries = 0
forked_from_id: None,
timestamp: "2025-01-05T12:00:00Z".to_string(),
cwd: repo_path.clone(),
workspace_roots: Vec::new(),
originator: "codex".to_string(),
cli_version: "0.0.0".to_string(),
source: RolloutSessionSource::Cli,
@@ -2235,6 +2237,122 @@ async fn thread_resume_rejoins_running_thread_even_with_override_mismatch() -> R
Ok(())
}
#[tokio::test]
async fn thread_resume_running_applies_workspace_roots_and_active_profile_name() -> Result<()> {
let server = responses::start_mock_server().await;
let first_response = responses::sse_response(responses::sse(vec![
responses::ev_response_created("resp-1"),
responses::ev_assistant_message("msg-1", "Done"),
responses::ev_completed("resp-1"),
]));
let second_response = responses::sse_response(responses::sse(vec![
responses::ev_response_created("resp-2"),
responses::ev_assistant_message("msg-2", "Done"),
responses::ev_completed("resp-2"),
]))
.set_delay(std::time::Duration::from_millis(500));
let _response_mock =
responses::mount_response_sequence(&server, vec![first_response, second_response]).await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri())?;
let workspace_root = codex_home.path().join("replacement-root");
std::fs::create_dir_all(&workspace_root)?;
let workspace_root = AbsolutePathBuf::from_absolute_path(workspace_root.canonicalize()?)?;
let mut primary = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, primary.initialize()).await??;
let start_id = primary
.send_thread_start_request(ThreadStartParams {
model: Some("gpt-5.4".to_string()),
..Default::default()
})
.await?;
let start_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
primary.read_stream_until_response_message(RequestId::Integer(start_id)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(start_resp)?;
let seed_turn_id = primary
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
input: vec![UserInput::Text {
text: "seed history".to_string(),
text_elements: Vec::new(),
}],
..Default::default()
})
.await?;
timeout(
DEFAULT_READ_TIMEOUT,
primary.read_stream_until_response_message(RequestId::Integer(seed_turn_id)),
)
.await??;
timeout(
DEFAULT_READ_TIMEOUT,
primary.read_stream_until_notification_message("turn/completed"),
)
.await??;
primary.clear_message_buffer();
let running_turn_id = primary
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
input: vec![UserInput::Text {
text: "keep running".to_string(),
text_elements: Vec::new(),
}],
..Default::default()
})
.await?;
timeout(
DEFAULT_READ_TIMEOUT,
primary.read_stream_until_response_message(RequestId::Integer(running_turn_id)),
)
.await??;
timeout(
DEFAULT_READ_TIMEOUT,
primary.read_stream_until_notification_message("turn/started"),
)
.await??;
let resume_id = primary
.send_thread_resume_request(ThreadResumeParams {
thread_id: thread.id.clone(),
workspace_roots: Some(vec![workspace_root.clone()]),
sandbox: Some(SandboxMode::WorkspaceWrite),
..Default::default()
})
.await?;
let resume_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
primary.read_stream_until_response_message(RequestId::Integer(resume_id)),
)
.await??;
let ThreadResumeResponse {
workspace_roots,
active_permission_profile,
..
} = to_response::<ThreadResumeResponse>(resume_resp)?;
assert_eq!(workspace_roots, vec![workspace_root]);
assert_eq!(
active_permission_profile
.as_ref()
.map(|profile| profile.id.as_str()),
Some(":workspace")
);
timeout(
DEFAULT_READ_TIMEOUT,
primary.read_stream_until_notification_message("turn/completed"),
)
.await??;
Ok(())
}
#[tokio::test]
async fn thread_resume_can_skip_turns_when_thread_is_running() -> Result<()> {
let server = responses::start_mock_server().await;
@@ -218,6 +218,7 @@ async fn thread_unarchive_preserves_pathless_store_metadata() -> Result<()> {
cwd: None,
model_provider: "test-provider".to_string(),
memory_mode: ThreadMemoryMode::Disabled,
workspace_roots: Vec::new(),
},
event_persistence_mode: ThreadEventPersistenceMode::default(),
})
+226 -23
View File
@@ -34,7 +34,6 @@ 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::PermissionProfileSelectionParams;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ServerRequest;
use codex_app_server_protocol::ServerRequestResolvedNotification;
@@ -291,7 +290,9 @@ async fn turn_start_emits_thread_scoped_warning_notification_for_trimmed_skills(
write_test_skill(codex_home.path(), "alpha-skill")?;
write_test_skill(codex_home.path(), "beta-skill")?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
let home_dir = codex_home.path().display().to_string();
let mut mcp =
McpProcess::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))]).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let thread_req = mcp
@@ -779,10 +780,7 @@ async fn turn_start_rejects_invalid_permission_selection_before_starting_turn()
text: "Hello".to_string(),
text_elements: Vec::new(),
}],
permissions: Some(PermissionProfileSelectionParams::Profile {
id: ":danger-no-sandbox".to_string(),
modifications: None,
}),
permissions: Some(":danger-no-sandbox".to_string()),
..Default::default()
})
.await?;
@@ -812,6 +810,87 @@ async fn turn_start_rejects_invalid_permission_selection_before_starting_turn()
Ok(())
}
#[tokio::test]
async fn turn_start_rejects_unknown_permission_selection_before_starting_turn() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(
codex_home.path(),
"http://localhost/unused",
"never",
&BTreeMap::from([(Feature::Personality, true)]),
)?;
let config_toml = codex_home.path().join("config.toml");
let config_contents = std::fs::read_to_string(&config_toml)?;
let config_contents = config_contents.replace(
"model_provider = \"mock_provider\"\n\n[features]",
r#"model_provider = "mock_provider"
default_permissions = "workspace"
[permissions.workspace.filesystem]
":minimal" = "read"
[permissions.workspace.filesystem.":project_roots"]
"." = "write"
[features]
"#,
);
std::fs::write(config_toml, config_contents)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let thread_req = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("mock-model".to_string()),
..Default::default()
})
.await?;
let thread_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(thread_req)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
let turn_req = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id,
input: vec![V2UserInput::Text {
text: "Hello".to_string(),
text_elements: Vec::new(),
}],
permissions: Some("missing-profile".to_string()),
..Default::default()
})
.await?;
let err: JSONRPCError = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(turn_req)),
)
.await??;
assert_eq!(err.error.code, INVALID_REQUEST_ERROR_CODE);
assert!(
err.error
.message
.contains("default_permissions refers to undefined profile `missing-profile`"),
"unexpected error message: {}",
err.error.message
);
let turn_started = tokio::time::timeout(
std::time::Duration::from_millis(250),
mcp.read_stream_until_notification_message("turn/started"),
)
.await;
assert!(
turn_started.is_err(),
"did not expect a turn/started notification after rejected permissions selection"
);
Ok(())
}
#[tokio::test]
async fn turn_start_rejects_unknown_environment_before_starting_turn() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
@@ -1657,7 +1736,6 @@ async fn turn_start_exec_approval_toggle_v2() -> Result<()> {
text_elements: Vec::new(),
}],
approval_policy: Some(codex_app_server_protocol::AskForApproval::Never),
sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::DangerFullAccess),
model: Some("mock-model".to_string()),
effort: Some(ReasoningEffort::Medium),
summary: Some(ReasoningSummary::Auto),
@@ -1825,7 +1903,7 @@ async fn turn_start_exec_approval_decline_v2() -> Result<()> {
}
#[tokio::test]
async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
async fn turn_start_updates_cwd_without_replacing_workspace_roots_v2() -> Result<()> {
skip_if_no_network!(Ok(()));
let tmp = TempDir::new()?;
@@ -1854,12 +1932,14 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
)?,
create_final_assistant_message_sse_response("done second")?,
];
let server = create_mock_responses_server_sequence(responses).await;
create_config_toml(
let server = responses::start_mock_server().await;
let response_mock = responses::mount_sse_sequence(&server, responses).await;
create_config_toml_with_sandbox(
&codex_home,
&server.uri(),
"untrusted",
&BTreeMap::default(),
"read-only",
)?;
let mut mcp = McpProcess::new(&codex_home).await?;
@@ -1879,7 +1959,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(start_resp)?;
// first turn with workspace-write sandbox and first_cwd
// first turn with first_cwd as the thread's workspace root
let first_turn = mcp
.send_turn_start_request(TurnStartParams {
environments: None,
@@ -1890,15 +1970,11 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
}],
responsesapi_client_metadata: None,
cwd: Some(first_cwd.clone()),
workspace_roots: Some(vec![first_cwd.clone().try_into()?]),
approval_policy: Some(codex_app_server_protocol::AskForApproval::Never),
approvals_reviewer: None,
sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::WorkspaceWrite {
writable_roots: vec![first_cwd.try_into()?],
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
}),
permissions: None,
sandbox_policy: None,
permissions: Some(":workspace".to_string()),
model: Some("mock-model".to_string()),
effort: Some(ReasoningEffort::Medium),
summary: Some(ReasoningSummary::Auto),
@@ -1920,7 +1996,8 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
.await??;
mcp.clear_message_buffer();
// second turn with workspace-write and second_cwd, ensure exec begins in second_cwd
// second turn changes cwd only; workspace roots stay on first_cwd while
// exec begins in second_cwd.
let second_turn = mcp
.send_turn_start_request(TurnStartParams {
environments: None,
@@ -1931,9 +2008,10 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
}],
responsesapi_client_metadata: None,
cwd: Some(second_cwd.clone()),
workspace_roots: None,
approval_policy: Some(codex_app_server_protocol::AskForApproval::Never),
approvals_reviewer: None,
sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::DangerFullAccess),
sandbox_policy: None,
permissions: None,
model: Some("mock-model".to_string()),
effort: Some(ReasoningEffort::Medium),
@@ -1981,6 +2059,22 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
assert_eq!(command, expected_command);
assert_eq!(status, CommandExecutionStatus::InProgress);
let requests = response_mock.requests();
assert!(
requests.len() >= 3,
"expected at least 3 model requests, got {}",
requests.len()
);
let second_turn_developer_text = requests[2].message_input_texts("developer").join("\n");
let first_cwd_name = first_cwd
.file_name()
.expect("first cwd should have a final path component")
.to_string_lossy();
assert!(
second_turn_developer_text.contains(first_cwd_name.as_ref()),
"second turn developer instructions should retain first_cwd as a workspace root; got {second_turn_developer_text:?}"
);
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/completed"),
@@ -1990,6 +2084,110 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn turn_start_legacy_workspace_sandbox_updates_workspace_roots_for_cwd() -> Result<()> {
let tmp = TempDir::new()?;
let codex_home = tmp.path().join("codex_home");
std::fs::create_dir(&codex_home)?;
let first_cwd = tmp.path().join("turn1");
let second_cwd = tmp.path().join("turn2");
std::fs::create_dir(&first_cwd)?;
std::fs::create_dir(&second_cwd)?;
let server = responses::start_mock_server().await;
let response_mock = responses::mount_sse_sequence(
&server,
vec![
create_final_assistant_message_sse_response("done first")?,
create_final_assistant_message_sse_response("done second")?,
],
)
.await;
create_config_toml(&codex_home, &server.uri(), "never", &BTreeMap::default())?;
let mut mcp = McpProcess::new(&codex_home).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let start_id = mcp
.send_thread_start_request(ThreadStartParams {
model: Some("mock-model".to_string()),
..Default::default()
})
.await?;
let start_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(start_id)),
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(start_resp)?;
let first_turn = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id.clone(),
input: vec![V2UserInput::Text {
text: "first turn".to_string(),
text_elements: Vec::new(),
}],
cwd: Some(first_cwd.clone()),
workspace_roots: Some(vec![first_cwd.clone().try_into()?]),
permissions: Some(":workspace".to_string()),
..Default::default()
})
.await?;
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(first_turn)),
)
.await??;
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/completed"),
)
.await??;
let second_turn = mcp
.send_turn_start_request(TurnStartParams {
thread_id: thread.id,
input: vec![V2UserInput::Text {
text: "second turn".to_string(),
text_elements: Vec::new(),
}],
cwd: Some(second_cwd.clone()),
sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::WorkspaceWrite {
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
legacy_writable_roots: Vec::new(),
}),
..Default::default()
})
.await?;
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(second_turn)),
)
.await??;
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_notification_message("turn/completed"),
)
.await??;
let requests = response_mock.requests();
assert_eq!(requests.len(), 2);
let second_turn_developer_text = requests[1].message_input_texts("developer").join("\n");
let second_cwd_name = second_cwd
.file_name()
.expect("second cwd should have a final path component")
.to_string_lossy();
assert!(
second_turn_developer_text.contains(second_cwd_name.as_ref()),
"legacy sandboxPolicy should rebind workspace roots to second_cwd; got {second_turn_developer_text:?}"
);
Ok(())
}
#[tokio::test]
async fn turn_start_resolves_sticky_thread_local_environment_and_turn_overrides() -> Result<()> {
let tmp = TempDir::new()?;
@@ -3306,7 +3504,6 @@ async fn command_execution_notifications_include_process_id() -> Result<()> {
text: "run a command".to_string(),
text_elements: Vec::new(),
}],
sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::DangerFullAccess),
..Default::default()
})
.await?;
@@ -3402,7 +3599,8 @@ async fn command_execution_notifications_include_process_id() -> Result<()> {
}
#[tokio::test]
async fn turn_start_with_elevated_override_does_not_persist_project_trust() -> Result<()> {
async fn turn_start_accepts_legacy_sandbox_policy_and_does_not_persist_project_trust() -> Result<()>
{
let responses = vec![create_final_assistant_message_sse_response("Done")?];
let server = create_mock_responses_server_sequence_unchecked(responses).await;
@@ -3436,7 +3634,12 @@ async fn turn_start_with_elevated_override_does_not_persist_project_trust() -> R
.send_turn_start_request(TurnStartParams {
thread_id: thread.id,
cwd: Some(workspace.path().to_path_buf()),
sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::DangerFullAccess),
sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::WorkspaceWrite {
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
legacy_writable_roots: Vec::new(),
}),
input: vec![V2UserInput::Text {
text: "Hello".to_string(),
text_elements: Vec::new(),
@@ -104,6 +104,7 @@ async fn turn_start_shell_zsh_fork_executes_command_v2() -> Result<()> {
.send_thread_start_request(ThreadStartParams {
model: Some("mock-model".to_string()),
cwd: Some(workspace.to_string_lossy().into_owned()),
sandbox: Some(codex_app_server_protocol::SandboxMode::DangerFullAccess),
..Default::default()
})
.await?;
@@ -123,7 +124,6 @@ async fn turn_start_shell_zsh_fork_executes_command_v2() -> Result<()> {
}],
cwd: Some(workspace.clone()),
approval_policy: Some(codex_app_server_protocol::AskForApproval::Never),
sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::DangerFullAccess),
model: Some("mock-model".to_string()),
effort: Some(codex_protocol::openai_models::ReasoningEffort::Medium),
summary: Some(codex_protocol::config_types::ReasoningSummary::Auto),
@@ -515,6 +515,7 @@ async fn turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2()
.send_thread_start_request(ThreadStartParams {
model: Some("mock-model".to_string()),
cwd: Some(workspace.to_string_lossy().into_owned()),
sandbox: Some(codex_app_server_protocol::SandboxMode::WorkspaceWrite),
..Default::default()
})
.await?;
@@ -533,13 +534,9 @@ async fn turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2()
text_elements: Vec::new(),
}],
cwd: Some(workspace.clone()),
workspace_roots: Some(vec![workspace.clone().try_into()?]),
approval_policy: Some(codex_app_server_protocol::AskForApproval::UnlessTrusted),
sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::WorkspaceWrite {
writable_roots: vec![workspace.clone().try_into()?],
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
}),
sandbox_policy: None,
model: Some("mock-model".to_string()),
effort: Some(codex_protocol::openai_models::ReasoningEffort::Medium),
summary: Some(codex_protocol::config_types::ReasoningSummary::Auto),
+14 -6
View File
@@ -229,7 +229,7 @@ async fn run_command_under_sandbox(
let network_proxy = match config.permissions.network.as_ref() {
Some(spec) => Some(
spec.start_proxy(
config.permissions.permission_profile.get(),
config.permissions.permission_profile_ref(),
/*policy_decider*/ None,
/*blocked_request_observer*/ None,
managed_network_requirements_enabled,
@@ -284,7 +284,7 @@ async fn run_command_under_sandbox(
let args = create_linux_sandbox_command_args_for_permission_profile(
command,
cwd.as_path(),
&config.permissions.permission_profile(),
config.permissions.permission_profile_ref(),
sandbox_policy_cwd.as_path(),
use_legacy_landlock,
allow_network_for_proxy(managed_network_requirements_enabled),
@@ -769,6 +769,16 @@ mod tests {
Ok(())
}
fn workspace_write_policy_for_codex_home(
codex_home: &TempDir,
) -> codex_protocol::permissions::FileSystemSandboxPolicy {
let memories_root = AbsolutePathBuf::try_from(codex_home.path().join("memories"))
.expect("codex home tempdir should be absolute");
codex_protocol::models::PermissionProfile::workspace_write()
.file_system_sandbox_policy()
.with_additional_legacy_workspace_writable_roots(std::slice::from_ref(&memories_root))
}
#[tokio::test]
async fn debug_sandbox_honors_active_permission_profiles() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
@@ -947,8 +957,7 @@ mod tests {
assert_eq!(
config.permissions.file_system_sandbox_policy(),
codex_protocol::models::PermissionProfile::workspace_write()
.file_system_sandbox_policy()
workspace_write_policy_for_codex_home(&codex_home)
);
Ok(())
@@ -980,8 +989,7 @@ mod tests {
assert_eq!(
config.permissions.file_system_sandbox_policy(),
codex_protocol::models::PermissionProfile::workspace_write()
.file_system_sandbox_policy()
workspace_write_policy_for_codex_home(&codex_home)
);
Ok(())
@@ -2144,7 +2144,6 @@ allowed_approvals_reviewers = ["user"]
let config: ConfigRequirementsToml = from_str(toml_str)?;
let requirements: ConfigRequirements = with_unknown_source(config).try_into()?;
let root = if cfg!(windows) { "C:\\repo" } else { "/repo" };
assert!(
requirements
.permission_profile
@@ -2154,7 +2153,6 @@ allowed_approvals_reviewers = ["user"]
.is_ok()
);
let workspace_write_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![AbsolutePathBuf::from_absolute_path(root)?],
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
@@ -2265,9 +2263,7 @@ allowed_approvals_reviewers = ["user"]
);
let requirements = ConfigRequirements::try_from(requirements_with_sources)?;
let root = if cfg!(windows) { "C:\\repo" } else { "/repo" };
let workspace_write_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![AbsolutePathBuf::from_absolute_path(root)?],
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
+2 -2
View File
@@ -757,7 +757,7 @@ impl ConfigToml {
SandboxMode::ReadOnly => PermissionProfile::read_only(),
SandboxMode::WorkspaceWrite => match self.sandbox_workspace_write.as_ref() {
Some(SandboxWorkspaceWrite {
writable_roots,
writable_roots: _,
network_access,
exclude_tmpdir_env_var,
exclude_slash_tmp,
@@ -768,7 +768,7 @@ impl ConfigToml {
NetworkSandboxPolicy::Restricted
};
PermissionProfile::workspace_write_with(
writable_roots,
&[],
network_policy,
*exclude_tmpdir_env_var,
*exclude_slash_tmp,
+30 -6
View File
@@ -59,6 +59,7 @@ pub struct ThreadConfigSnapshot {
pub permission_profile: PermissionProfile,
pub active_permission_profile: Option<ActivePermissionProfile>,
pub cwd: AbsolutePathBuf,
pub workspace_roots: Vec<AbsolutePathBuf>,
pub ephemeral: bool,
pub reasoning_effort: Option<ReasoningEffort>,
pub personality: Option<Personality>,
@@ -68,11 +69,15 @@ pub struct ThreadConfigSnapshot {
impl ThreadConfigSnapshot {
pub fn sandbox_policy(&self) -> SandboxPolicy {
let file_system_sandbox_policy = self.permission_profile.file_system_sandbox_policy();
let permission_profile = self
.permission_profile
.clone()
.materialize_project_roots_with_workspace_roots(&self.workspace_roots);
let file_system_sandbox_policy = permission_profile.file_system_sandbox_policy();
codex_sandboxing::compatibility_sandbox_policy_for_permission_profile(
&self.permission_profile,
&permission_profile,
&file_system_sandbox_policy,
self.permission_profile.network_sandbox_policy(),
permission_profile.network_sandbox_policy(),
self.cwd.as_path(),
)
}
@@ -82,6 +87,7 @@ impl ThreadConfigSnapshot {
#[derive(Clone, Default)]
pub struct CodexThreadTurnContextOverrides {
pub cwd: Option<PathBuf>,
pub workspace_roots: Option<Vec<PathBuf>>,
pub approval_policy: Option<AskForApproval>,
pub approvals_reviewer: Option<ApprovalsReviewer>,
pub sandbox_policy: Option<SandboxPolicy>,
@@ -241,8 +247,26 @@ impl CodexThread {
&self,
overrides: CodexThreadTurnContextOverrides,
) -> ConstraintResult<()> {
let updates = self.turn_context_updates_from_overrides(overrides).await;
self.codex.session.validate_settings(&updates).await
}
/// Apply persistent thread context overrides immediately.
pub async fn update_turn_context_overrides(
&self,
overrides: CodexThreadTurnContextOverrides,
) -> ConstraintResult<()> {
let updates = self.turn_context_updates_from_overrides(overrides).await;
self.codex.session.update_settings(updates).await
}
async fn turn_context_updates_from_overrides(
&self,
overrides: CodexThreadTurnContextOverrides,
) -> SessionSettingsUpdate {
let CodexThreadTurnContextOverrides {
cwd,
workspace_roots,
approval_policy,
approvals_reviewer,
sandbox_policy,
@@ -266,8 +290,9 @@ impl CodexThread {
.with_updates(model, effort, /*developer_instructions*/ None)
};
let updates = SessionSettingsUpdate {
SessionSettingsUpdate {
cwd,
workspace_roots,
approval_policy,
approvals_reviewer,
sandbox_policy,
@@ -279,8 +304,7 @@ impl CodexThread {
service_tier,
personality,
..Default::default()
};
self.codex.session.validate_settings(&updates).await
}
}
/// Use sparingly: this is intended to be removed soon.
@@ -527,9 +527,10 @@ writable_roots = ["~/code"]
let expected_root = AbsolutePathBuf::from_absolute_path(home.join("code"))?;
match &config.legacy_sandbox_policy() {
SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
SandboxPolicy::WorkspaceWrite { .. } => {
assert_eq!(
writable_roots
config
.workspace_roots
.iter()
.filter(|root| **root == expected_root)
.count(),
@@ -593,7 +594,6 @@ allowed_sandbox_modes = ["read-only"]
.permission_profile
.can_set(&PermissionProfile::from_legacy_sandbox_policy(
&SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
+62 -43
View File
@@ -68,7 +68,6 @@ use codex_model_provider_info::WireApi;
use codex_models_manager::bundled_models_response;
use codex_protocol::config_types::ServiceTier;
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;
@@ -100,6 +99,14 @@ use std::path::Path;
use std::time::Duration;
use tempfile::TempDir;
fn materialized_file_system_sandbox_policy(config: &Config) -> FileSystemSandboxPolicy {
config
.permissions
.permission_profile()
.materialize_project_roots_with_workspace_roots(&config.workspace_roots)
.file_system_sandbox_policy()
}
fn stdio_mcp(command: &str) -> McpServerConfig {
McpServerConfig {
transport: McpServerTransportConfig::Stdio {
@@ -1364,7 +1371,7 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::
},
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: memories_root.clone(),
path: memories_root,
},
access: FileSystemAccessMode::Write,
},
@@ -1373,7 +1380,6 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::
assert_eq!(
&config.legacy_sandbox_policy(),
&SandboxPolicy::WorkspaceWrite {
writable_roots: vec![memories_root],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
@@ -1550,7 +1556,6 @@ async fn permission_profile_override_applies_runtime_roots_to_legacy_projection(
assert_eq!(
&config.legacy_sandbox_policy(),
&SandboxPolicy::WorkspaceWrite {
writable_roots: vec![memories_root],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
@@ -1751,7 +1756,7 @@ async fn default_permissions_can_select_builtin_profile_without_permissions_tabl
)
.await?;
let policy = config.permissions.file_system_sandbox_policy();
let policy = materialized_file_system_sandbox_policy(&config);
assert_eq!(
config
.permissions
@@ -1772,7 +1777,7 @@ async fn default_permissions_can_select_builtin_profile_without_permissions_tabl
}
#[tokio::test]
async fn default_permissions_read_only_applies_additional_writable_roots_as_modifications()
async fn default_permissions_read_only_records_additional_writable_roots_as_workspace_roots()
-> std::io::Result<()> {
let codex_home = TempDir::new()?;
let cwd = TempDir::new()?;
@@ -1793,18 +1798,18 @@ async fn default_permissions_read_only_applies_additional_writable_roots_as_modi
)
.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:?}"
let policy = materialized_file_system_sandbox_policy(&config);
assert_eq!(
policy,
PermissionProfile::read_only().file_system_sandbox_policy()
);
assert_eq!(
config.workspace_roots,
vec![cwd.path().abs(), extra_root.clone()]
);
assert_eq!(
config.permissions.active_permission_profile(),
Some(
ActivePermissionProfile::new(":read-only").with_modifications(vec![
ActivePermissionProfileModification::AdditionalWritableRoot { path: extra_root },
])
)
Some(ActivePermissionProfile::new(":read-only"))
);
Ok(())
}
@@ -1835,7 +1840,7 @@ async fn explicit_builtin_workspace_profile_ignores_legacy_workspace_write_setti
)
.await?;
let policy = config.permissions.file_system_sandbox_policy();
let policy = materialized_file_system_sandbox_policy(&config);
assert_eq!(
config.permissions.network_sandbox_policy(),
NetworkSandboxPolicy::Restricted
@@ -1875,7 +1880,7 @@ async fn empty_config_defaults_to_builtin_profile_for_trusted_project() -> std::
)
.await?;
let policy = config.permissions.file_system_sandbox_policy();
let policy = materialized_file_system_sandbox_policy(&config);
assert_eq!(
config
.permissions
@@ -1943,7 +1948,7 @@ async fn implicit_builtin_workspace_profile_preserves_sandbox_workspace_write_se
)
.await?;
let policy = config.permissions.file_system_sandbox_policy();
let policy = materialized_file_system_sandbox_policy(&config);
assert!(
policy.can_write_path_with_cwd(extra_root.as_path(), cwd.path()),
"expected implicit :workspace to preserve sandbox_workspace_write.writable_roots, policy: {policy:?}"
@@ -1960,12 +1965,11 @@ async fn implicit_builtin_workspace_profile_preserves_sandbox_workspace_write_se
);
match config.legacy_sandbox_policy() {
SandboxPolicy::WorkspaceWrite {
writable_roots,
network_access,
exclude_tmpdir_env_var,
exclude_slash_tmp,
} => {
assert!(writable_roots.contains(&extra_root));
assert!(config.workspace_roots.contains(&extra_root));
assert!(network_access);
assert!(exclude_tmpdir_env_var);
assert!(!exclude_slash_tmp);
@@ -2009,7 +2013,7 @@ async fn implicit_builtin_workspace_profile_preserves_add_dir_metadata_carveouts
)
.await?;
let policy = config.permissions.file_system_sandbox_policy();
let policy = materialized_file_system_sandbox_policy(&config);
let extra_root = extra_root.path().abs();
assert!(
policy.can_write_path_with_cwd(extra_root.as_path(), cwd.path()),
@@ -2184,9 +2188,6 @@ async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root()
)
.await?;
let memories_root = AbsolutePathBuf::from_absolute_path(std::fs::canonicalize(
codex_home.path().join("memories"),
)?)?;
assert!(
config
.permissions
@@ -2196,7 +2197,6 @@ async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root()
assert_eq!(
&config.legacy_sandbox_policy(),
&SandboxPolicy::WorkspaceWrite {
writable_roots: vec![external_write_path, memories_root],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
@@ -2871,7 +2871,6 @@ trust_level = "trusted"
assert_eq!(
resolution,
SandboxPolicy::WorkspaceWrite {
writable_roots: vec![writable_root.clone()],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
@@ -2911,7 +2910,6 @@ exclude_slash_tmp = true
assert_eq!(
resolution,
SandboxPolicy::WorkspaceWrite {
writable_roots: vec![writable_root],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
@@ -2975,13 +2973,21 @@ exclude_slash_tmp = true
NetworkSandboxPolicy::from(&sandbox_policy),
"case `{name}` should preserve network semantics from legacy config"
);
assert_eq!(
file_system_policy
.to_legacy_sandbox_policy(network_policy, cwd.path())
.unwrap_or_else(|err| panic!("case `{name}` should round-trip: {err}")),
sandbox_policy,
"case `{name}` should preserve its legacy compatibility projection"
);
let direct_legacy_projection =
file_system_policy.to_legacy_sandbox_policy(network_policy, cwd.path());
if name == "workspace-write" && !cfg!(target_os = "windows") {
assert!(
direct_legacy_projection.is_err(),
"case `{name}` should require the compatibility projection for split workspace roots"
);
} else {
assert_eq!(
direct_legacy_projection
.unwrap_or_else(|err| panic!("case `{name}` should round-trip: {err}")),
sandbox_policy,
"case `{name}` should preserve its legacy compatibility projection"
);
}
match name.as_str() {
"danger-full-access" | "read-only" => {
@@ -3023,14 +3029,19 @@ exclude_slash_tmp = true
})
);
assert!(
file_system_policy
config.workspace_roots.contains(&extra_root),
"case `{name}` should store legacy writable roots as thread workspace roots"
);
assert!(
materialized_file_system_sandbox_policy(&config)
.entries
.contains(&FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: extra_root.clone(),
},
access: FileSystemAccessMode::Write,
})
}),
"case `{name}` should materialize workspace roots into runtime write access"
);
for subpath in [".git", ".agents", ".codex"] {
assert!(
@@ -3773,14 +3784,15 @@ async fn add_dir_override_extends_workspace_writable_roots() -> std::io::Result<
}
} else {
match &config.legacy_sandbox_policy() {
SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
SandboxPolicy::WorkspaceWrite { .. } => {
assert_eq!(
writable_roots
config
.workspace_roots
.iter()
.filter(|root| **root == expected_backend)
.count(),
1,
"expected single writable root entry for {}",
"expected single workspace root entry for {}",
expected_backend.display()
);
}
@@ -3840,13 +3852,16 @@ async fn workspace_write_always_includes_memories_root_once() -> std::io::Result
"expected memories root directory to exist at {}",
memories_root.display()
);
let expected_memories_root = memories_root.abs();
let expected_memories_root =
AbsolutePathBuf::from_absolute_path(std::fs::canonicalize(&memories_root)?)?;
match &config.legacy_sandbox_policy() {
SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
SandboxPolicy::WorkspaceWrite { .. } => {
let writable_roots = materialized_file_system_sandbox_policy(&config)
.get_writable_roots_with_cwd(config.cwd.as_path());
assert_eq!(
writable_roots
.iter()
.filter(|root| **root == expected_memories_root)
.filter(|root| root.root == expected_memories_root)
.count(),
1,
"expected single writable root entry for {}",
@@ -7294,6 +7309,7 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
user_instructions: None,
notify: None,
cwd: fixture.cwd(),
workspace_roots: vec![fixture.cwd()],
cli_auth_credentials_store_mode: Default::default(),
mcp_servers: Constrained::allow_any(HashMap::new()),
mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode(
@@ -7740,6 +7756,7 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
user_instructions: None,
notify: None,
cwd: fixture.cwd(),
workspace_roots: vec![fixture.cwd()],
cli_auth_credentials_store_mode: Default::default(),
mcp_servers: Constrained::allow_any(HashMap::new()),
mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode(
@@ -7900,6 +7917,7 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
user_instructions: None,
notify: None,
cwd: fixture.cwd(),
workspace_roots: vec![fixture.cwd()],
cli_auth_credentials_store_mode: Default::default(),
mcp_servers: Constrained::allow_any(HashMap::new()),
mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode(
@@ -8045,6 +8063,7 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
user_instructions: None,
notify: None,
cwd: fixture.cwd(),
workspace_roots: vec![fixture.cwd()],
cli_auth_credentials_store_mode: Default::default(),
mcp_servers: Constrained::allow_any(HashMap::new()),
mcp_oauth_credentials_store_mode: resolve_mcp_oauth_credentials_store_mode(
@@ -9052,7 +9071,7 @@ async fn requirements_web_search_mode_overrides_danger_full_access_default() ->
assert_eq!(
resolve_web_search_mode_for_turn(
&config.web_search_mode,
&config.permissions.permission_profile(),
config.permissions.permission_profile_ref(),
),
WebSearchMode::Cached,
);
+129 -67
View File
@@ -89,7 +89,6 @@ 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;
@@ -242,10 +241,10 @@ pub struct Permissions {
pub approval_policy: Constrained<AskForApproval>,
/// Canonical effective runtime permissions after config requirements and
/// runtime readable-root additions have been applied.
pub permission_profile: Constrained<PermissionProfile>,
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>,
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.
@@ -267,6 +266,35 @@ pub struct Permissions {
}
impl Permissions {
/// Build permissions from the two constrained values that are required for
/// a minimal in-process configuration.
pub fn from_approval_and_profile(
approval_policy: Constrained<AskForApproval>,
permission_profile: Constrained<PermissionProfile>,
) -> Self {
Self {
approval_policy,
permission_profile,
active_permission_profile: None,
network: None,
allow_login_shell: true,
shell_environment_policy: ShellEnvironmentPolicy::default(),
windows_sandbox_mode: None,
windows_sandbox_private_desktop: true,
}
}
/// Borrow the constrained canonical profile. This is for code that must
/// preserve or inspect the active requirements wrapper without mutating it.
pub fn permission_profile_constraint(&self) -> &Constrained<PermissionProfile> {
&self.permission_profile
}
/// Borrow the canonical effective runtime permissions without cloning.
pub fn permission_profile_ref(&self) -> &PermissionProfile {
self.permission_profile.get()
}
/// Effective runtime permissions after config requirements and runtime
/// readable-root additions have been applied.
pub fn permission_profile(&self) -> PermissionProfile {
@@ -278,6 +306,45 @@ impl Permissions {
self.active_permission_profile.clone()
}
/// Replace the full constrained profile value and clear any profile-name
/// sidecar because the new constraint may no longer match that name.
pub fn replace_permission_profile_constraint(
&mut self,
permission_profile: Constrained<PermissionProfile>,
) {
self.permission_profile = permission_profile;
self.active_permission_profile = None;
}
/// Replace the full constrained profile value and preserve the active
/// profile sidecar when the caller has already validated both together.
pub fn replace_permission_profile_constraint_with_active_profile(
&mut self,
permission_profile: Constrained<PermissionProfile>,
active_permission_profile: Option<ActivePermissionProfile>,
) {
self.permission_profile = permission_profile;
self.active_permission_profile = active_permission_profile;
}
/// Record the active profile id only if it still describes the current
/// canonical profile.
pub fn set_active_permission_profile_for_current_profile(
&mut self,
active_permission_profile: Option<ActivePermissionProfile>,
expected_permission_profile: Option<&PermissionProfile>,
) {
self.active_permission_profile =
match (active_permission_profile, expected_permission_profile) {
(Some(active_permission_profile), Some(expected_permission_profile))
if self.permission_profile.get() == expected_permission_profile =>
{
Some(active_permission_profile)
}
_ => None,
};
}
/// 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()
@@ -576,6 +643,10 @@ pub struct Config {
/// layer are resolved against this path.
pub cwd: AbsolutePathBuf,
/// Absolute roots that define the writable project/workspace set for
/// symbolic `:project_roots` permission entries.
pub workspace_roots: Vec<AbsolutePathBuf>,
/// Preferred store for CLI auth credentials.
/// file (default): Use a file in the Codex home directory.
/// keyring: Use an OS-specific keyring service.
@@ -1857,6 +1928,11 @@ fn apply_managed_filesystem_constraints(
}
}
fn dedupe_absolute_paths(paths: &mut Vec<AbsolutePathBuf>) {
let mut seen = HashSet::new();
paths.retain(|path| seen.insert(path.clone()));
}
/// Optional overrides for user configuration (e.g., from CLI flags).
#[derive(Default, Debug, Clone)]
pub struct ConfigOverrides {
@@ -1885,6 +1961,9 @@ pub struct ConfigOverrides {
pub ephemeral: Option<bool>,
/// Additional directories that should be treated as writable roots for this session.
pub additional_writable_roots: Vec<PathBuf>,
/// Explicit workspace roots for this session. When set, this is the full
/// root list rather than an additive override.
pub workspace_roots: Option<Vec<PathBuf>>,
}
/// Resolves the OSS provider from CLI override, profile config, or global config.
@@ -2166,6 +2245,7 @@ impl Config {
tools_web_search_request: override_tools_web_search_request,
ephemeral,
additional_writable_roots,
workspace_roots: workspace_roots_override,
} = overrides;
if sandbox_mode.is_some() && permission_profile.is_some() {
@@ -2254,11 +2334,10 @@ impl Config {
}
}
}))?;
let mut additional_writable_roots: Vec<AbsolutePathBuf> = additional_writable_roots
let requested_additional_writable_roots: Vec<AbsolutePathBuf> = additional_writable_roots
.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(
@@ -2300,12 +2379,7 @@ impl Config {
};
let memories_root = memory_root(&codex_home);
std::fs::create_dir_all(&memories_root)?;
if !additional_writable_roots
.iter()
.any(|existing| existing == &memories_root)
{
additional_writable_roots.push(memories_root);
}
let internal_writable_roots = vec![memories_root];
let profiles_are_active = default_permissions_override.is_some()
|| matches!(
@@ -2315,6 +2389,31 @@ impl Config {
|| permission_config_syntax.is_none();
let using_implicit_builtin_profile =
permission_config_syntax.is_none() && default_permissions.is_none();
let should_seed_legacy_workspace_roots =
default_permissions.is_none()
&& matches!(
permission_config_syntax,
None | Some(PermissionConfigSyntax::Legacy)
);
let mut workspace_roots = match workspace_roots_override {
Some(workspace_roots) => workspace_roots
.into_iter()
.map(|path| {
AbsolutePathBuf::resolve_path_against_base(path, resolved_cwd.as_path())
})
.collect(),
None => {
let mut workspace_roots = vec![resolved_cwd.clone()];
workspace_roots.extend(requested_additional_writable_roots.clone());
if should_seed_legacy_workspace_roots
&& let Some(sandbox_workspace_write) = cfg.sandbox_workspace_write.as_ref()
{
workspace_roots.extend(sandbox_workspace_write.writable_roots.clone());
}
workspace_roots
}
};
dedupe_absolute_paths(&mut workspace_roots);
let (
mut configured_network_proxy_config,
permission_profile,
@@ -2351,10 +2450,7 @@ impl Config {
);
if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) {
file_system_sandbox_policy = file_system_sandbox_policy
.with_additional_writable_roots(
resolved_cwd.as_path(),
&additional_writable_roots,
);
.with_additional_legacy_workspace_writable_roots(&internal_writable_roots);
permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
permission_profile.enforcement(),
&file_system_sandbox_policy,
@@ -2405,28 +2501,8 @@ impl Config {
resolved_cwd.as_path(),
);
if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. }) {
file_system_sandbox_policy = if using_implicit_builtin_profile {
file_system_sandbox_policy
.with_additional_legacy_workspace_writable_roots(
&additional_writable_roots,
)
} else {
file_system_sandbox_policy.with_additional_writable_roots(
resolved_cwd.as_path(),
&additional_writable_roots,
)
};
permission_profile = PermissionProfile::from_runtime_permissions(
&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,
);
file_system_sandbox_policy = file_system_sandbox_policy
.with_additional_legacy_workspace_writable_roots(&internal_writable_roots);
permission_profile = PermissionProfile::from_runtime_permissions(
&file_system_sandbox_policy,
network_sandbox_policy,
@@ -2443,22 +2519,7 @@ impl Config {
// 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)
Some(ActivePermissionProfile::new(default_permissions))
};
(
configured_network_proxy_config,
@@ -2497,25 +2558,25 @@ impl Config {
}
let (mut file_system_sandbox_policy, network_sandbox_policy) =
permission_profile.to_runtime_permissions();
// `additional_writable_roots` is a legacy workspace-write knob. It
// only applies when the derived managed profile has workspace-style
// write access to the project roots; read-only, disabled, external,
// and future non-workspace profiles must not silently grow extra
// write access.
// Internal writable roots only apply when the derived managed
// profile has workspace-style write access to the project roots;
// read-only, disabled, external, and future non-workspace profiles
// must not silently grow extra write access.
let materialized_file_system_sandbox_policy = permission_profile
.materialize_project_roots_with_workspace_roots(&workspace_roots)
.file_system_sandbox_policy();
if matches!(permission_profile.enforcement(), SandboxEnforcement::Managed)
&& file_system_sandbox_policy.can_write_path_with_cwd(
&& materialized_file_system_sandbox_policy.can_write_path_with_cwd(
resolved_cwd.as_path(),
resolved_cwd.as_path(),
)
&& !file_system_sandbox_policy.has_full_disk_write_access()
&& !materialized_file_system_sandbox_policy.has_full_disk_write_access()
{
// Keep legacy behavior for extra writable roots while storing
// the result as the canonical permission profile. Explicit
// extra roots are concrete paths, so their metadata carveouts
// are also concrete rather than symbolic `:project_roots`
// entries.
// Keep Codex runtime write access while storing the result as
// the canonical permission profile. Workspace roots themselves
// are held separately on the thread.
file_system_sandbox_policy = file_system_sandbox_policy
.with_additional_legacy_workspace_writable_roots(&additional_writable_roots);
.with_additional_legacy_workspace_writable_roots(&internal_writable_roots);
permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement(
permission_profile.enforcement(),
&file_system_sandbox_policy,
@@ -3009,6 +3070,7 @@ impl Config {
model_provider_id,
model_provider,
cwd: resolved_cwd,
workspace_roots,
startup_warnings,
permissions: Permissions {
approval_policy: constrained_approval_policy.value,
@@ -3298,7 +3360,7 @@ impl Config {
pub fn managed_network_requirements_enabled(&self) -> bool {
!matches!(
self.permissions.permission_profile.get(),
self.permissions.permission_profile_ref(),
PermissionProfile::Disabled
) && self
.config_layer_stack
+2 -2
View File
@@ -68,12 +68,12 @@ pub(crate) fn builtin_permission_profile(
BUILT_IN_READ_ONLY_PROFILE => Some(PermissionProfile::read_only()),
BUILT_IN_WORKSPACE_PROFILE => Some(match workspace_write {
Some(SandboxWorkspaceWrite {
writable_roots,
writable_roots: _,
network_access,
exclude_tmpdir_env_var,
exclude_slash_tmp,
}) => PermissionProfile::workspace_write_with(
writable_roots,
&[],
if *network_access {
NetworkSandboxPolicy::Enabled
} else {
@@ -30,6 +30,8 @@ const SANDBOX_MODE_DANGER_FULL_ACCESS: &str =
const SANDBOX_MODE_WORKSPACE_WRITE: &str =
include_str!("prompts/permissions/sandbox_mode/workspace_write.md");
const SANDBOX_MODE_READ_ONLY: &str = include_str!("prompts/permissions/sandbox_mode/read_only.md");
const MAX_WRITABLE_ROOTS_IN_PROMPT: usize = 8;
const MAX_WRITABLE_ROOT_LABEL_CHARS: usize = 160;
static SANDBOX_MODE_DANGER_FULL_ACCESS_TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
Template::parse(SANDBOX_MODE_DANGER_FULL_ACCESS.trim_end())
@@ -256,17 +258,42 @@ fn writable_roots_text(writable_roots: Option<Vec<WritableRoot>>) -> Option<Stri
return None;
}
let total_roots = roots.len();
let roots_list: Vec<String> = roots
.iter()
.map(|r| format!("`{}`", r.root.to_string_lossy()))
.take(MAX_WRITABLE_ROOTS_IN_PROMPT)
.map(|r| {
format!(
"`{}`",
truncate_writable_root_label(&r.root.to_string_lossy())
)
})
.collect();
Some(if roots_list.len() == 1 {
Some(if total_roots == 1 {
format!(" The writable root is {}.", roots_list[0])
} else {
} else if total_roots == roots_list.len() {
format!(" The writable roots are {}.", roots_list.join(", "))
} else {
format!(
" The writable roots include {}, and {} more.",
roots_list.join(", "),
total_roots - roots_list.len()
)
})
}
fn truncate_writable_root_label(label: &str) -> String {
if label.chars().count() <= MAX_WRITABLE_ROOT_LABEL_CHARS {
return label.to_string();
}
label
.chars()
.take(MAX_WRITABLE_ROOT_LABEL_CHARS - 3)
.chain("...".chars())
.collect()
}
fn approved_command_prefixes_text(exec_policy: &Policy) -> Option<String> {
format_allow_prefixes(exec_policy.get_allowed_prefixes())
.filter(|prefixes| !prefixes.is_empty())
@@ -13,7 +13,7 @@ use std::path::PathBuf;
fn renders_sandbox_mode_text() {
assert_eq!(
sandbox_text(SandboxMode::WorkspaceWrite, NetworkAccess::Restricted),
"Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `workspace-write`: The sandbox permits reading files, and editing files in `cwd` and `writable_roots`. Editing files in other directories requires approval. Network access is restricted."
"Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `workspace-write`: The sandbox permits reading files, and editing files in the configured writable roots. Editing files in other directories requires approval. Network access is restricted."
);
assert_eq!(
@@ -56,7 +56,6 @@ fn builds_permissions_with_network_access_override() {
#[test]
fn builds_permissions_from_policy() {
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: true,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
@@ -106,6 +105,45 @@ fn builds_permissions_from_profile() {
assert!(text.contains(writable_root.to_string_lossy().as_ref()));
}
#[test]
fn writable_roots_text_limits_long_root_lists() {
let roots = (0..MAX_WRITABLE_ROOTS_IN_PROMPT + 2)
.map(|index| WritableRoot {
root: AbsolutePathBuf::from_absolute_path(format!("/tmp/root-{index}"))
.expect("absolute path"),
read_only_subpaths: Vec::new(),
protected_metadata_names: Vec::new(),
})
.collect::<Vec<_>>();
let visible_root_labels = roots
.iter()
.map(|root| root.root.to_string_lossy().into_owned())
.collect::<Vec<_>>();
let text = writable_roots_text(Some(roots)).expect("writable roots text");
assert!(text.contains(&visible_root_labels[0]));
assert!(text.contains(&visible_root_labels[MAX_WRITABLE_ROOTS_IN_PROMPT - 1]));
assert!(!text.contains(&visible_root_labels[MAX_WRITABLE_ROOTS_IN_PROMPT]));
assert!(text.contains("and 2 more"));
}
#[test]
fn writable_roots_text_truncates_long_root_labels() {
let long_segment = "a".repeat(MAX_WRITABLE_ROOT_LABEL_CHARS + 20);
let root = WritableRoot {
root: AbsolutePathBuf::from_absolute_path(format!("/tmp/{long_segment}"))
.expect("absolute path"),
read_only_subpaths: Vec::new(),
protected_metadata_names: Vec::new(),
};
let text = writable_roots_text(Some(vec![root])).expect("writable roots text");
assert!(!text.contains(&long_segment));
assert!(text.contains("..."));
}
#[test]
fn includes_request_rule_instructions_for_on_request() {
let mut exec_policy = Policy::empty();
@@ -1 +1 @@
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `workspace-write`: The sandbox permits reading files, and editing files in `cwd` and `writable_roots`. Editing files in other directories requires approval. Network access is {{network_access}}.
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `workspace-write`: The sandbox permits reading files, and editing files in the configured writable roots. Editing files in other directories requires approval. Network access is {{network_access}}.
@@ -123,11 +123,13 @@ fn reference_context_item() -> TurnContextItem {
turn_id: Some("reference-turn".to_string()),
trace_id: None,
cwd: PathBuf::from("/tmp/reference-cwd"),
workspace_roots: Vec::new(),
current_date: Some("2026-03-23".to_string()),
timezone: Some("America/Los_Angeles".to_string()),
approval_policy: AskForApproval::OnRequest,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
active_permission_profile: None,
network: None,
file_system_sandbox_policy: None,
model: "gpt-test".to_string(),
+9 -2
View File
@@ -49,7 +49,14 @@ fn build_permissions_update_item(
}
let prev = previous?;
if prev.permission_profile() == next.permission_profile()
let prev_permission_profile = prev
.permission_profile()
.materialize_project_roots_with_workspace_roots(&prev.workspace_roots);
let next_permission_profile = next
.permission_profile
.clone()
.materialize_project_roots_with_workspace_roots(&next.workspace_roots);
if prev_permission_profile == next_permission_profile
&& prev.approval_policy == next.approval_policy.value()
{
return None;
@@ -57,7 +64,7 @@ fn build_permissions_update_item(
Some(
PermissionsInstructions::from_permission_profile(
&next.permission_profile,
&next_permission_profile,
next.approval_policy.value(),
next.config.approvals_reviewer,
exec_policy,
+34 -28
View File
@@ -99,11 +99,11 @@ pub struct ExecParams {
///
/// The elevated Windows backend consumes extra deny-read paths plus explicit
/// read and write roots during setup/refresh. The unelevated restricted-token
/// backend only consumes extra deny-write carveouts on top of the legacy
/// `WorkspaceWrite` allow set. Read-root overrides are layered on top of the
/// baseline helper roots that the elevated setup path needs to launch the
/// sandboxed command; split policies that opt into platform defaults carry
/// that explicitly with the override.
/// backend consumes explicit write roots and extra deny-write carveouts on top
/// of the legacy `WorkspaceWrite` shape.
/// Read-root overrides are layered on top of the baseline helper roots that the
/// elevated setup path needs to launch the sandboxed command. Split policies
/// that opt into platform defaults carry that explicitly with the override.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct WindowsSandboxFilesystemOverrides {
pub(crate) read_roots_override: Option<Vec<PathBuf>>,
@@ -296,6 +296,7 @@ pub async fn process_exec_tool_call(
params: ExecParams,
permission_profile: &PermissionProfile,
sandbox_cwd: &AbsolutePathBuf,
workspace_roots: &[AbsolutePathBuf],
codex_linux_sandbox_exe: &Option<PathBuf>,
use_legacy_landlock: bool,
stdout_stream: Option<StdoutStream>,
@@ -304,6 +305,7 @@ pub async fn process_exec_tool_call(
params,
permission_profile,
sandbox_cwd,
workspace_roots,
codex_linux_sandbox_exe,
use_legacy_landlock,
)?;
@@ -318,6 +320,7 @@ pub fn build_exec_request(
params: ExecParams,
permission_profile: &PermissionProfile,
sandbox_cwd: &AbsolutePathBuf,
workspace_roots: &[AbsolutePathBuf],
codex_linux_sandbox_exe: &Option<PathBuf>,
use_legacy_landlock: bool,
) -> Result<ExecRequest> {
@@ -379,6 +382,7 @@ pub fn build_exec_request(
enforce_managed_network,
network: network.as_ref(),
sandbox_policy_cwd: sandbox_cwd,
workspace_roots,
codex_linux_sandbox_exe: codex_linux_sandbox_exe.as_deref(),
use_legacy_landlock,
windows_sandbox_level,
@@ -612,11 +616,11 @@ async fn exec_windows_sandbox(
let additional_deny_read_paths = windows_sandbox_filesystem_overrides
.map(|overrides| overrides.additional_deny_read_paths.clone())
.unwrap_or_default();
let elevated_read_roots_override = windows_sandbox_filesystem_overrides
let read_roots_override = windows_sandbox_filesystem_overrides
.and_then(|overrides| overrides.read_roots_override.clone());
let elevated_read_roots_include_platform_defaults = windows_sandbox_filesystem_overrides
let read_roots_include_platform_defaults = windows_sandbox_filesystem_overrides
.is_some_and(|overrides| overrides.read_roots_include_platform_defaults);
let elevated_write_roots_override = windows_sandbox_filesystem_overrides
let write_roots_override = windows_sandbox_filesystem_overrides
.and_then(|overrides| overrides.write_roots_override.clone());
let spawn_res = tokio::task::spawn_blocking(move || {
if use_elevated {
@@ -631,10 +635,9 @@ async fn exec_windows_sandbox(
timeout_ms,
use_private_desktop: windows_sandbox_private_desktop,
proxy_enforced,
read_roots_override: elevated_read_roots_override.as_deref(),
read_roots_include_platform_defaults:
elevated_read_roots_include_platform_defaults,
write_roots_override: elevated_write_roots_override.as_deref(),
read_roots_override: read_roots_override.as_deref(),
read_roots_include_platform_defaults,
write_roots_override: write_roots_override.as_deref(),
deny_read_paths_override: &additional_deny_read_paths,
deny_write_paths_override: &additional_deny_write_paths,
},
@@ -648,6 +651,7 @@ async fn exec_windows_sandbox(
&cwd,
env,
timeout_ms,
write_roots_override.as_deref(),
&additional_deny_read_paths,
&additional_deny_write_paths,
windows_sandbox_private_desktop,
@@ -1075,12 +1079,18 @@ pub(crate) fn resolve_windows_restricted_token_filesystem_overrides(
.map(|root| normalize_windows_override_path(root.root.as_path()))
.collect::<std::result::Result<_, _>>()?;
if legacy_root_paths != split_root_paths {
let write_roots_override = if legacy_root_paths == split_root_paths {
None
} else if matches!(sandbox_policy, SandboxPolicy::WorkspaceWrite { .. })
&& !split_root_paths.iter().any(|path| path.parent().is_none())
{
Some(split_root_paths.iter().cloned().collect::<Vec<_>>())
} else {
return Err(
"windows unelevated restricted-token sandbox cannot enforce split writable root sets directly; refusing to run unsandboxed"
.to_string(),
);
}
};
for writable_root in &split_writable_roots {
for read_only_subpath in &writable_root.read_only_subpaths {
@@ -1102,22 +1112,18 @@ pub(crate) fn resolve_windows_restricted_token_filesystem_overrides(
let mut additional_deny_write_paths = BTreeSet::new();
for split_root in &split_writable_roots {
let split_root_path = normalize_windows_override_path(split_root.root.as_path())?;
let Some(legacy_root) = legacy_writable_roots.iter().find(|candidate| {
let legacy_root = legacy_writable_roots.iter().find(|candidate| {
normalize_windows_override_path(candidate.root.as_path())
.is_ok_and(|candidate_path| candidate_path == split_root_path)
}) else {
return Err(
"windows unelevated restricted-token sandbox cannot enforce split writable root sets directly; refusing to run unsandboxed"
.to_string(),
);
};
});
for read_only_subpath in &split_root.read_only_subpaths {
if !legacy_root
.read_only_subpaths
.iter()
.any(|candidate| candidate == read_only_subpath)
{
if !legacy_root.is_some_and(|legacy_root| {
legacy_root
.read_only_subpaths
.iter()
.any(|candidate| candidate == read_only_subpath)
}) {
additional_deny_write_paths.insert(normalize_windows_override_path(
read_only_subpath.as_path(),
)?);
@@ -1125,15 +1131,15 @@ pub(crate) fn resolve_windows_restricted_token_filesystem_overrides(
}
}
if additional_deny_read_paths.is_empty() && additional_deny_write_paths.is_empty() {
if write_roots_override.is_none() && additional_deny_write_paths.is_empty() {
return Ok(None);
}
Ok(Some(WindowsSandboxFilesystemOverrides {
read_roots_override: None,
read_roots_include_platform_defaults: false,
write_roots_override: None,
additional_deny_read_paths,
write_roots_override,
additional_deny_write_paths: additional_deny_write_paths
.into_iter()
.map(|path| AbsolutePathBuf::from_absolute_path(path).map_err(|err| err.to_string()))
+53 -9
View File
@@ -365,6 +365,7 @@ async fn process_exec_tool_call_preserves_full_buffer_capture_policy() -> Result
},
&permission_profile,
&cwd,
&[],
&None,
/*use_legacy_landlock*/ false,
/*stdout_stream*/ None,
@@ -470,7 +471,6 @@ fn windows_restricted_token_allows_legacy_restricted_policies() {
#[test]
fn windows_restricted_token_allows_legacy_workspace_write_policies() {
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
@@ -528,7 +528,6 @@ fn windows_restricted_token_rejects_split_only_filesystem_policies() {
let docs = temp_dir.path().join("docs");
std::fs::create_dir_all(&docs).expect("create docs");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
@@ -573,7 +572,6 @@ fn windows_restricted_token_rejects_root_write_read_only_carveouts() {
let docs = temp_dir.path().join("docs");
std::fs::create_dir_all(&docs).expect("create docs");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
@@ -619,7 +617,6 @@ fn windows_restricted_token_supports_full_read_split_write_read_carveouts() {
let docs = cwd.join("docs");
std::fs::create_dir_all(docs.as_path()).expect("create docs");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
@@ -677,7 +674,6 @@ fn windows_restricted_token_rejects_unreadable_split_carveouts() {
let blocked = cwd.join("blocked");
std::fs::create_dir_all(blocked.as_path()).expect("create blocked");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
@@ -719,6 +715,57 @@ fn windows_restricted_token_rejects_unreadable_split_carveouts() {
);
}
#[test]
fn windows_restricted_token_supports_extra_split_writable_root_override() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
let cwd = dunce::canonicalize(temp_dir.path())
.expect("canonicalize temp dir")
.abs();
let extra_root = cwd.join("extra-root");
std::fs::create_dir_all(extra_root.as_path()).expect("create extra root");
let policy = SandboxPolicy::WorkspaceWrite {
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let file_system_policy = FileSystemSandboxPolicy::restricted(vec![
codex_protocol::permissions::FileSystemSandboxEntry {
path: codex_protocol::permissions::FileSystemPath::Special {
value: codex_protocol::permissions::FileSystemSpecialPath::Root,
},
access: codex_protocol::permissions::FileSystemAccessMode::Read,
},
codex_protocol::permissions::FileSystemSandboxEntry {
path: codex_protocol::permissions::FileSystemPath::Path { path: cwd.clone() },
access: codex_protocol::permissions::FileSystemAccessMode::Write,
},
codex_protocol::permissions::FileSystemSandboxEntry {
path: codex_protocol::permissions::FileSystemPath::Path {
path: extra_root.clone(),
},
access: codex_protocol::permissions::FileSystemAccessMode::Write,
},
]);
assert_eq!(
resolve_windows_restricted_token_filesystem_overrides(
SandboxType::WindowsRestrictedToken,
&policy,
&file_system_policy,
NetworkSandboxPolicy::Restricted,
&cwd,
WindowsSandboxLevel::RestrictedToken,
),
Ok(Some(WindowsSandboxFilesystemOverrides {
read_roots_override: None,
read_roots_include_platform_defaults: false,
write_roots_override: Some(vec![cwd.to_path_buf(), extra_root.to_path_buf()]),
additional_deny_read_paths: vec![],
additional_deny_write_paths: vec![],
}))
);
}
#[test]
fn windows_elevated_supports_split_restricted_read_roots() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");
@@ -764,7 +811,6 @@ fn windows_elevated_supports_split_write_read_carveouts() {
std::fs::create_dir_all(&docs).expect("create docs");
let expected_docs = dunce::canonicalize(&docs).expect("canonical docs");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
@@ -822,7 +868,6 @@ fn windows_elevated_supports_unreadable_split_carveouts() {
std::fs::create_dir_all(&blocked).expect("create blocked");
let expected_blocked = dunce::canonicalize(&blocked).expect("canonical blocked");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
@@ -885,7 +930,6 @@ fn windows_elevated_supports_unreadable_globs() {
std::fs::create_dir_all(secret.parent().expect("parent")).expect("create parent");
std::fs::write(&secret, "secret").expect("write secret");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
@@ -942,7 +986,6 @@ fn windows_elevated_rejects_reopened_writable_descendants() {
let nested = docs.join("nested");
std::fs::create_dir_all(&nested).expect("create nested");
let policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
@@ -1115,6 +1158,7 @@ async fn process_exec_tool_call_respects_cancellation_token() -> Result<()> {
params,
&PermissionProfile::Disabled,
&cwd,
&[],
&None,
/*use_legacy_landlock*/ false,
/*stdout_stream*/ None,
+6 -4
View File
@@ -895,9 +895,11 @@ pub(crate) fn build_guardian_review_session_config(
guardian_config.developer_instructions = None;
guardian_config.permissions.approval_policy = Constrained::allow_only(AskForApproval::Never);
let sandbox_policy = SandboxPolicy::new_read_only_policy();
guardian_config.permissions.permission_profile = Constrained::allow_only(
PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy),
);
guardian_config
.permissions
.replace_permission_profile_constraint(Constrained::allow_only(
PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy),
));
guardian_config
.permissions
.set_legacy_sandbox_policy(sandbox_policy, guardian_config.cwd.as_path())
@@ -923,7 +925,7 @@ pub(crate) fn build_guardian_review_session_config(
guardian_config.permissions.network = Some(NetworkProxySpec::from_config_and_constraints(
live_network_config,
network_constraints,
guardian_config.permissions.permission_profile.get(),
guardian_config.permissions.permission_profile_ref(),
)?);
}
for feature in [
+4 -4
View File
@@ -2159,7 +2159,7 @@ async fn guardian_review_session_config_preserves_parent_network_proxy() {
}),
..Default::default()
}),
parent_config.permissions.permission_profile.get(),
parent_config.permissions.permission_profile_ref(),
)
.expect("network proxy spec");
parent_config.permissions.network = Some(network.clone());
@@ -2186,8 +2186,8 @@ async fn guardian_review_session_config_preserves_parent_network_proxy() {
Constrained::allow_only(AskForApproval::Never)
);
assert_eq!(
guardian_config.permissions.permission_profile,
Constrained::allow_only(PermissionProfile::from_legacy_sandbox_policy(
guardian_config.permissions.permission_profile_constraint(),
&Constrained::allow_only(PermissionProfile::from_legacy_sandbox_policy(
&SandboxPolicy::new_read_only_policy(),
))
);
@@ -2226,7 +2226,7 @@ async fn guardian_review_session_config_uses_live_network_proxy_state() {
NetworkProxySpec::from_config_and_constraints(
parent_network,
/*requirements*/ None,
parent_config.permissions.permission_profile.get(),
parent_config.permissions.permission_profile_ref(),
)
.expect("parent network proxy spec"),
);
@@ -47,6 +47,7 @@ async fn write_rollout_with_user_event(dir: &Path, thread_id: ThreadId) -> io::R
forked_from_id: None,
timestamp: TEST_TIMESTAMP.to_string(),
cwd: std::path::PathBuf::from("."),
workspace_roots: Vec::new(),
originator: "test_originator".to_string(),
cli_version: "test_version".to_string(),
source: SessionSource::Cli,
+4
View File
@@ -37,6 +37,10 @@ impl codex_rollout::RolloutConfigView for Config {
self.cwd.as_path()
}
fn workspace_roots(&self) -> &[codex_utils_absolute_path::AbsolutePathBuf] {
&self.workspace_roots
}
fn model_provider_id(&self) -> &str {
self.model_provider_id.as_str()
}
+8 -13
View File
@@ -33,7 +33,6 @@ fn test_writable_roots_constraint() {
// Policy limited to the workspace only; exclude system temp roots so
// only `cwd` is writable by default.
let policy_workspace_only = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
@@ -51,17 +50,16 @@ fn test_writable_roots_constraint() {
&cwd,
));
// With the parent dir explicitly added as a writable root, the
// outside write should be permitted.
let policy_with_parent = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![parent],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
// With the parent dir explicitly added as a workspace root, the outside
// write should be permitted.
let file_system_policy_with_parent =
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::Path { path: parent },
access: FileSystemAccessMode::Write,
}]);
assert!(is_write_patch_constrained_to_writable_paths(
&add_outside,
&FileSystemSandboxPolicy::from(&policy_with_parent),
&file_system_policy_with_parent,
&cwd,
));
}
@@ -101,7 +99,6 @@ fn granular_with_all_flags_true_matches_on_request_for_out_of_root_patch() {
let outside_path = parent.join("outside.txt");
let add_outside = ApplyPatchAction::new_add_for_test(&outside_path, "".to_string());
let policy_workspace_only = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
@@ -145,7 +142,6 @@ fn granular_sandbox_approval_false_rejects_out_of_root_patch() {
let outside_path = parent.join("outside.txt");
let add_outside = ApplyPatchAction::new_add_for_test(&outside_path, "".to_string());
let policy_workspace_only = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
@@ -295,7 +291,6 @@ fn missing_project_dot_codex_config_requires_approval() {
let config_path = cwd.join(".codex").join("config.toml");
let action = ApplyPatchAction::new_add_for_test(&config_path, "".to_string());
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
+4
View File
@@ -143,6 +143,7 @@ pub(super) async fn user_input_or_turn_inner(
items,
SessionSettingsUpdate {
cwd: Some(cwd),
workspace_roots: None,
approval_policy: Some(approval_policy),
approvals_reviewer,
sandbox_policy: Some(sandbox_policy),
@@ -163,6 +164,7 @@ pub(super) async fn user_input_or_turn_inner(
}
Op::UserInputWithTurnContext {
cwd,
workspace_roots,
approval_policy,
approvals_reviewer,
sandbox_policy,
@@ -195,6 +197,8 @@ pub(super) async fn user_input_or_turn_inner(
items,
SessionSettingsUpdate {
cwd,
workspace_roots: workspace_roots
.map(|roots| roots.into_iter().map(|root| root.to_path_buf()).collect()),
approval_policy,
approvals_reviewer,
sandbox_policy,
+7 -2
View File
@@ -616,10 +616,11 @@ impl Codex {
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(),
permission_profile: config.permissions.permission_profile_constraint().clone(),
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments: environment_selections.to_selections(),
@@ -2582,9 +2583,13 @@ impl Session {
developer_sections.push(model_switch_message);
}
if turn_context.config.include_permissions_instructions {
let permission_profile = turn_context
.permission_profile
.clone()
.materialize_project_roots_with_workspace_roots(&turn_context.workspace_roots);
developer_sections.push(
PermissionsInstructions::from_permission_profile(
&turn_context.permission_profile,
&permission_profile,
turn_context.approval_policy.value(),
turn_context.config.approvals_reviewer,
self.services.exec_policy.current().as_ref(),
+1
View File
@@ -143,6 +143,7 @@ pub(super) async fn spawn_review_thread(
windows_sandbox_level: parent_turn_context.windows_sandbox_level,
shell_environment_policy: parent_turn_context.shell_environment_policy.clone(),
cwd: parent_turn_context.cwd.clone(),
workspace_roots: parent_turn_context.workspace_roots.clone(),
final_output_json_schema: None,
codex_self_exe: parent_turn_context.codex_self_exe.clone(),
codex_linux_sandbox_exe: parent_turn_context.codex_linux_sandbox_exe.clone(),
@@ -61,11 +61,13 @@ async fn record_initial_history_resumed_bare_turn_context_does_not_hydrate_previ
turn_id: Some(turn_context.sub_id.clone()),
trace_id: turn_context.trace_id.clone(),
cwd: turn_context.cwd.to_path_buf(),
workspace_roots: turn_context.workspace_roots.clone(),
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy(),
permission_profile: None,
active_permission_profile: None,
network: None,
file_system_sandbox_policy: None,
model: previous_model.to_string(),
@@ -102,11 +104,13 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif
turn_id: Some(turn_context.sub_id.clone()),
trace_id: turn_context.trace_id.clone(),
cwd: turn_context.cwd.to_path_buf(),
workspace_roots: turn_context.workspace_roots.clone(),
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy(),
permission_profile: None,
active_permission_profile: None,
network: None,
file_system_sandbox_policy: None,
model: previous_model.to_string(),
@@ -912,11 +916,13 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
turn_id: Some(turn_context.sub_id.clone()),
trace_id: turn_context.trace_id.clone(),
cwd: turn_context.cwd.to_path_buf(),
workspace_roots: turn_context.workspace_roots.clone(),
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy(),
permission_profile: None,
active_permission_profile: None,
network: None,
file_system_sandbox_policy: None,
model: previous_model.to_string(),
@@ -990,11 +996,13 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
turn_id: Some(turn_context.sub_id.clone()),
trace_id: turn_context.trace_id.clone(),
cwd: turn_context.cwd.to_path_buf(),
workspace_roots: turn_context.workspace_roots.clone(),
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy(),
permission_profile: None,
active_permission_profile: None,
network: None,
file_system_sandbox_policy: None,
model: previous_model.to_string(),
@@ -1021,11 +1029,13 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
turn_id: Some(turn_context.sub_id.clone()),
trace_id: turn_context.trace_id.clone(),
cwd: turn_context.cwd.to_path_buf(),
workspace_roots: turn_context.workspace_roots.clone(),
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy(),
permission_profile: None,
active_permission_profile: None,
network: None,
file_system_sandbox_policy: None,
model: previous_model.to_string(),
@@ -1136,11 +1146,13 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
turn_id: Some(current_turn_id.clone()),
trace_id: turn_context.trace_id.clone(),
cwd: turn_context.cwd.to_path_buf(),
workspace_roots: turn_context.workspace_roots.clone(),
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy(),
permission_profile: None,
active_permission_profile: None,
network: None,
file_system_sandbox_policy: None,
model: current_model.to_string(),
@@ -1250,11 +1262,13 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
turn_id: Some(turn_context.sub_id.clone()),
trace_id: turn_context.trace_id.clone(),
cwd: turn_context.cwd.to_path_buf(),
workspace_roots: turn_context.workspace_roots.clone(),
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy(),
permission_profile: None,
active_permission_profile: None,
network: None,
file_system_sandbox_policy: None,
model: previous_model.to_string(),
@@ -1402,11 +1416,13 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
turn_id: Some(turn_context.sub_id.clone()),
trace_id: turn_context.trace_id.clone(),
cwd: turn_context.cwd.to_path_buf(),
workspace_roots: turn_context.workspace_roots.clone(),
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy(),
permission_profile: None,
active_permission_profile: None,
network: None,
file_system_sandbox_policy: None,
model: previous_model.to_string(),
+66 -25
View File
@@ -75,6 +75,9 @@ pub(crate) struct SessionConfiguration {
/// execution sandbox are resolved against this directory **instead** of
/// the process-wide current working directory.
pub(super) cwd: AbsolutePathBuf,
/// Workspace roots used to materialize symbolic `:project_roots`
/// permission entries for this thread.
pub(super) workspace_roots: Vec<AbsolutePathBuf>,
/// Directory containing all Codex state for this session.
pub(super) codex_home: AbsolutePathBuf,
/// Optional user-facing name for the thread, updated during the session.
@@ -112,12 +115,13 @@ impl SessionConfiguration {
}
pub(super) fn sandbox_policy(&self) -> SandboxPolicy {
self.permission_profile()
let permission_profile = self.materialized_permission_profile();
permission_profile
.to_legacy_sandbox_policy(&self.cwd)
.unwrap_or_else(|_| {
let file_system_sandbox_policy = self.file_system_sandbox_policy();
codex_sandboxing::compatibility_sandbox_policy_for_permission_profile(
self.permission_profile.get(),
&permission_profile,
&file_system_sandbox_policy,
self.network_sandbox_policy(),
&self.cwd,
@@ -126,13 +130,21 @@ impl SessionConfiguration {
}
pub(super) fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy {
self.permission_profile.get().file_system_sandbox_policy()
self.materialized_permission_profile()
.file_system_sandbox_policy()
}
pub(super) fn network_sandbox_policy(&self) -> NetworkSandboxPolicy {
self.permission_profile.get().network_sandbox_policy()
}
fn materialized_permission_profile(&self) -> PermissionProfile {
self.permission_profile
.get()
.clone()
.materialize_project_roots_with_workspace_roots(&self.workspace_roots)
}
pub(super) fn thread_config_snapshot(&self) -> ThreadConfigSnapshot {
ThreadConfigSnapshot {
model: self.collaboration_mode.model().to_string(),
@@ -143,6 +155,7 @@ impl SessionConfiguration {
permission_profile: self.permission_profile(),
active_permission_profile: self.active_permission_profile(),
cwd: self.cwd.clone(),
workspace_roots: self.workspace_roots.clone(),
ephemeral: self.original_config_do_not_use.ephemeral,
reasoning_effort: self.collaboration_mode.reasoning_effort(),
personality: self.personality,
@@ -223,6 +236,20 @@ impl SessionConfiguration {
let cwd_changed = absolute_cwd.as_path() != self.cwd.as_path();
next_configuration.cwd = absolute_cwd;
if let Some(workspace_roots) = updates.workspace_roots.clone() {
let mut workspace_roots: Vec<AbsolutePathBuf> = workspace_roots
.into_iter()
.map(|path| {
AbsolutePathBuf::resolve_path_against_base(
normalize_for_native_workdir(path.as_path()),
next_configuration.cwd.as_path(),
)
})
.collect();
dedupe_absolute_paths(&mut workspace_roots);
next_configuration.workspace_roots = workspace_roots;
}
if let Some(permission_profile) = updates.permission_profile.clone() {
let active_permission_profile =
updates.active_permission_profile.clone().or_else(|| {
@@ -253,26 +280,31 @@ impl SessionConfiguration {
),
)?;
next_configuration.active_permission_profile = None;
} else if cwd_changed
&& file_system_policy_matches_legacy
&& file_system_policy_has_rebindable_project_root_write
{
// Preserve richer split policies across cwd-only updates; only
// rederive when the session is already using a structurally
// cwd-bound legacy bridge.
let file_system_sandbox_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries(
&current_sandbox_policy,
&next_configuration.cwd,
&current_file_system_sandbox_policy,
);
next_configuration.permission_profile.set(
PermissionProfile::from_runtime_permissions_with_enforcement(
SandboxEnforcement::from_legacy_sandbox_policy(&current_sandbox_policy),
&file_system_sandbox_policy,
current_network_sandbox_policy,
),
)?;
} else {
if cwd_changed
&& file_system_policy_matches_legacy
&& file_system_policy_has_rebindable_project_root_write
{
// Preserve richer split policies across cwd-only updates; only
// rederive when the session is already using a structurally
// cwd-bound legacy bridge.
let file_system_sandbox_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy_preserving_deny_entries(
&current_sandbox_policy,
&next_configuration.cwd,
&current_file_system_sandbox_policy,
);
next_configuration.permission_profile.set(
PermissionProfile::from_runtime_permissions_with_enforcement(
SandboxEnforcement::from_legacy_sandbox_policy(&current_sandbox_policy),
&file_system_sandbox_policy,
current_network_sandbox_policy,
),
)?;
}
if let Some(active_permission_profile) = updates.active_permission_profile.clone() {
next_configuration.active_permission_profile = Some(active_permission_profile);
}
}
if let Some(app_server_client_name) = updates.app_server_client_name.clone() {
next_configuration.app_server_client_name = Some(app_server_client_name);
@@ -306,9 +338,15 @@ impl SessionConfiguration {
}
}
fn dedupe_absolute_paths(paths: &mut Vec<AbsolutePathBuf>) {
let mut seen = std::collections::HashSet::new();
paths.retain(|path| seen.insert(path.clone()));
}
#[derive(Default, Clone)]
pub(crate) struct SessionSettingsUpdate {
pub(crate) cwd: Option<PathBuf>,
pub(crate) workspace_roots: Option<Vec<PathBuf>>,
pub(crate) approval_policy: Option<AskForApproval>,
pub(crate) approvals_reviewer: Option<ApprovalsReviewer>,
pub(crate) sandbox_policy: Option<SandboxPolicy>,
@@ -425,6 +463,7 @@ impl Session {
dynamic_tools: session_configuration.dynamic_tools.clone(),
metadata: ThreadPersistenceMetadata {
cwd: Some(config.cwd.to_path_buf()),
workspace_roots: config.workspace_roots.clone(),
model_provider: config.model_provider_id.clone(),
memory_mode: if config.memories.generate_memories {
ThreadMemoryMode::Enabled
@@ -447,6 +486,7 @@ impl Session {
include_archived: true,
metadata: ThreadPersistenceMetadata {
cwd: Some(config.cwd.to_path_buf()),
workspace_roots: config.workspace_roots.clone(),
model_provider: config.model_provider_id.clone(),
memory_mode: if config.memories.generate_memories {
ThreadMemoryMode::Enabled
@@ -769,7 +809,7 @@ impl Session {
let (network_proxy, session_network_proxy) = Self::start_managed_network_proxy(
spec,
current_exec_policy.as_ref(),
config.permissions.permission_profile.get(),
config.permissions.permission_profile_ref(),
network_policy_decider.as_ref().map(Arc::clone),
blocked_request_observer.as_ref().map(Arc::clone),
managed_network_requirements_configured,
@@ -832,7 +872,7 @@ impl Session {
// setup is straightforward enough and performs well.
mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized(
&config.permissions.approval_policy,
&config.permissions.permission_profile,
config.permissions.permission_profile_constraint(),
))),
mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()),
unified_exec_manager: UnifiedExecProcessManager::new(
@@ -934,6 +974,7 @@ impl Session {
permission_profile: session_configuration.permission_profile(),
active_permission_profile: session_configuration.active_permission_profile(),
cwd: session_configuration.cwd.clone(),
workspace_roots: session_configuration.workspace_roots.clone(),
reasoning_effort: session_configuration.collaboration_mode.reasoning_effort(),
initial_messages,
network_proxy: session_network_proxy.filter(|_| {
+95 -27
View File
@@ -1823,8 +1823,10 @@ async fn session_configured_reports_permission_profile_for_external_sandbox() ->
};
let expected_sandbox_policy = sandbox_policy.clone();
let mut builder = test_codex().with_config(move |config| {
config.permissions.permission_profile = codex_config::Constrained::allow_any(
PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy),
config.permissions.replace_permission_profile_constraint(
codex_config::Constrained::allow_any(PermissionProfile::from_legacy_sandbox_policy(
&sandbox_policy,
)),
);
config
.set_legacy_sandbox_policy(sandbox_policy)
@@ -1977,11 +1979,13 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() {
turn_id: Some(turn_context.sub_id.clone()),
trace_id: turn_context.trace_id.clone(),
cwd: turn_context.cwd.to_path_buf(),
workspace_roots: turn_context.workspace_roots.clone(),
current_date: turn_context.current_date.clone(),
timezone: turn_context.timezone.clone(),
approval_policy: turn_context.approval_policy.value(),
sandbox_policy: turn_context.sandbox_policy(),
permission_profile: None,
active_permission_profile: None,
network: None,
file_system_sandbox_policy: None,
model: previous_model.to_string(),
@@ -2578,10 +2582,11 @@ async fn set_rate_limits_retains_previous_credits() {
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(),
permission_profile: config.permissions.permission_profile_constraint().clone(),
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments: Vec::new(),
@@ -2682,10 +2687,11 @@ async fn set_rate_limits_updates_plan_type_when_present() {
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(),
permission_profile: config.permissions.permission_profile_constraint().clone(),
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments: Vec::new(),
@@ -2952,6 +2958,7 @@ async fn attach_thread_persistence(session: &mut Session) -> PathBuf {
dynamic_tools: Vec::new(),
metadata: ThreadPersistenceMetadata {
cwd: Some(config.cwd.to_path_buf()),
workspace_roots: config.workspace_roots.clone(),
model_provider: config.model_provider_id.clone(),
memory_mode: if config.memories.generate_memories {
ThreadMemoryMode::Enabled
@@ -3159,10 +3166,11 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(),
permission_profile: config.permissions.permission_profile_constraint().clone(),
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments: Vec::new(),
@@ -3205,7 +3213,6 @@ async fn session_configuration_apply_preserves_profile_file_system_policy_on_cwd
session_configuration.cwd = original_cwd.abs();
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
@@ -3239,9 +3246,15 @@ async fn session_configuration_apply_preserves_profile_file_system_policy_on_cwd
.expect("cwd-only update should succeed");
assert_eq!(
updated.file_system_sandbox_policy(),
updated.permission_profile().file_system_sandbox_policy(),
file_system_sandbox_policy
);
let expected_materialized_file_system_policy = file_system_sandbox_policy
.materialize_project_roots_with_workspace_roots(&updated.workspace_roots);
assert_eq!(
updated.file_system_sandbox_policy(),
expected_materialized_file_system_policy
);
}
#[tokio::test]
@@ -3291,9 +3304,16 @@ async fn session_configuration_apply_permission_profile_preserves_existing_deny_
expected_file_system_policy.glob_scan_max_depth = Some(2);
expected_file_system_policy.entries.push(deny_entry);
assert_eq!(
updated.file_system_sandbox_policy(),
updated.permission_profile().file_system_sandbox_policy(),
expected_file_system_policy
);
let expected_materialized_file_system_policy = expected_file_system_policy
.clone()
.materialize_project_roots_with_workspace_roots(&updated.workspace_roots);
assert_eq!(
updated.file_system_sandbox_policy(),
expected_materialized_file_system_policy
);
}
#[tokio::test]
@@ -3310,7 +3330,7 @@ async fn session_configuration_apply_permission_profile_accepts_direct_write_roo
let file_system_sandbox_policy =
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: external_write_path.clone(),
path: external_write_path,
},
access: FileSystemAccessMode::Write,
}]);
@@ -3334,7 +3354,6 @@ async fn session_configuration_apply_permission_profile_accepts_direct_write_roo
assert_eq!(
updated.sandbox_policy(),
SandboxPolicy::WorkspaceWrite {
writable_roots: vec![external_write_path],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
@@ -3429,14 +3448,15 @@ enabled = false
}
#[tokio::test]
async fn session_configuration_apply_rederives_legacy_file_system_policy_on_cwd_update() {
async fn session_configuration_apply_preserves_legacy_workspace_roots_on_cwd_update() {
let mut session_configuration = make_session_configuration_for_tests().await;
let workspace = tempfile::tempdir().expect("create temp dir");
let project_root = workspace.path().join("project");
let original_cwd = project_root.join("subdir");
session_configuration.cwd = original_cwd.abs();
let original_cwd = original_cwd.abs();
session_configuration.cwd = original_cwd.clone();
session_configuration.workspace_roots = vec![original_cwd.clone()];
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: Vec::new(),
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
@@ -3455,20 +3475,61 @@ async fn session_configuration_apply_rederives_legacy_file_system_policy_on_cwd_
let updated = session_configuration
.apply(&SessionSettingsUpdate {
cwd: Some(project_root.clone()),
cwd: Some(project_root),
..Default::default()
})
.expect("cwd-only update should succeed");
let expected_file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
&updated.sandbox_policy(),
&project_root,
);
let expected_file_system_policy = file_system_sandbox_policy
.materialize_project_roots_with_workspace_roots(std::slice::from_ref(&original_cwd));
assert_eq!(updated.workspace_roots, vec![original_cwd.clone()]);
assert!(
updated
.file_system_sandbox_policy()
.is_semantically_equivalent_to(&expected_file_system_policy, &project_root),
"cwd-only update should rederive the legacy filesystem policy for the new cwd"
.is_semantically_equivalent_to(&expected_file_system_policy, original_cwd.as_path()),
"cwd-only update should preserve the existing workspace roots"
);
}
#[tokio::test]
async fn session_configuration_apply_preserves_active_permission_profile_on_legacy_cwd_update() {
let mut session_configuration = make_session_configuration_for_tests().await;
let workspace = tempfile::tempdir().expect("create temp dir");
let project_root = workspace.path().join("project");
let original_cwd = project_root.join("subdir");
let original_cwd = original_cwd.abs();
session_configuration.cwd = original_cwd.clone();
session_configuration.workspace_roots = vec![original_cwd];
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
};
let file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
&sandbox_policy,
&session_configuration.cwd,
);
session_configuration.permission_profile = codex_config::Constrained::allow_any(
PermissionProfile::from_runtime_permissions_with_enforcement(
SandboxEnforcement::from_legacy_sandbox_policy(&sandbox_policy),
&file_system_sandbox_policy,
NetworkSandboxPolicy::from(&sandbox_policy),
),
);
let active_permission_profile =
codex_protocol::models::ActivePermissionProfile::new(":workspace");
let updated = session_configuration
.apply(&SessionSettingsUpdate {
cwd: Some(project_root),
active_permission_profile: Some(active_permission_profile.clone()),
..Default::default()
})
.expect("cwd and active permission profile update should succeed");
assert_eq!(
updated.active_permission_profile(),
Some(active_permission_profile)
);
}
@@ -3685,10 +3746,11 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(),
permission_profile: config.permissions.permission_profile_constraint().clone(),
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments: Vec::new(),
@@ -3794,10 +3856,11 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(),
permission_profile: config.permissions.permission_profile_constraint().clone(),
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments: default_environments,
@@ -3841,7 +3904,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
let services = SessionServices {
mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized(
&config.permissions.approval_policy,
&config.permissions.permission_profile,
config.permissions.permission_profile_constraint(),
))),
mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()),
unified_exec_manager: UnifiedExecProcessManager::new(
@@ -4024,10 +4087,11 @@ async fn make_session_with_config_and_rx(
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(),
permission_profile: config.permissions.permission_profile_constraint().clone(),
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments: default_environments,
@@ -4127,10 +4191,11 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(),
permission_profile: config.permissions.permission_profile_constraint().clone(),
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments: default_environments,
@@ -4778,6 +4843,7 @@ fn op_kind_distinguishes_turn_ops() {
final_output_json_schema: None,
responsesapi_client_metadata: None,
cwd: None,
workspace_roots: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
@@ -5173,6 +5239,7 @@ async fn shutdown_complete_does_not_append_to_thread_store_after_shutdown() {
dynamic_tools: Vec::new(),
metadata: ThreadPersistenceMetadata {
cwd: Some(config.cwd.to_path_buf()),
workspace_roots: config.workspace_roots.clone(),
model_provider: config.model_provider_id.clone(),
memory_mode: if config.memories.generate_memories {
ThreadMemoryMode::Enabled
@@ -5514,10 +5581,11 @@ where
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.permissions.approval_policy.clone(),
approvals_reviewer: config.approvals_reviewer,
permission_profile: config.permissions.permission_profile.clone(),
permission_profile: config.permissions.permission_profile_constraint().clone(),
active_permission_profile: config.permissions.active_permission_profile(),
windows_sandbox_level: WindowsSandboxLevel::from_config(&config),
cwd: config.cwd.clone(),
workspace_roots: config.workspace_roots.clone(),
codex_home: config.codex_home.clone(),
thread_name: None,
environments: default_environments,
@@ -5561,7 +5629,7 @@ where
let services = SessionServices {
mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized(
&config.permissions.approval_policy,
&config.permissions.permission_profile,
config.permissions.permission_profile_constraint(),
))),
mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()),
unified_exec_manager: UnifiedExecProcessManager::new(
+17 -3
View File
@@ -70,6 +70,7 @@ pub struct TurnContext {
/// by the model as well as sandbox policies are resolved against this path
/// instead of `std::env::current_dir()`.
pub(crate) cwd: AbsolutePathBuf,
pub(crate) workspace_roots: Vec<AbsolutePathBuf>,
pub(crate) current_date: Option<String>,
pub(crate) timezone: Option<String>,
pub(crate) app_server_client_name: Option<String>,
@@ -104,7 +105,10 @@ impl TurnContext {
}
pub(crate) fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy {
self.permission_profile.file_system_sandbox_policy()
self.permission_profile
.clone()
.materialize_project_roots_with_workspace_roots(&self.workspace_roots)
.file_system_sandbox_policy()
}
pub(crate) fn network_sandbox_policy(&self) -> NetworkSandboxPolicy {
@@ -253,6 +257,7 @@ impl TurnContext {
thread_source: self.thread_source,
environments: self.environments.clone(),
cwd: self.cwd.clone(),
workspace_roots: self.workspace_roots.clone(),
current_date: self.current_date.clone(),
timezone: self.timezone.clone(),
app_server_client_name: self.app_server_client_name.clone(),
@@ -314,6 +319,7 @@ impl TurnContext {
FileSystemSandboxContext {
permissions,
cwd: Some(self.cwd.clone()),
workspace_roots: self.workspace_roots.clone(),
windows_sandbox_level: self.windows_sandbox_level,
windows_sandbox_private_desktop: self
.config
@@ -349,11 +355,13 @@ impl TurnContext {
turn_id: Some(self.sub_id.clone()),
trace_id: self.trace_id.clone(),
cwd: self.cwd.to_path_buf(),
workspace_roots: self.workspace_roots.clone(),
current_date: self.current_date.clone(),
timezone: self.timezone.clone(),
approval_policy: self.approval_policy.value(),
sandbox_policy: self.sandbox_policy(),
permission_profile: Some(self.permission_profile()),
active_permission_profile: self.config.permissions.active_permission_profile(),
network: self.turn_context_network_item(),
file_system_sandbox_policy: self.non_legacy_file_system_sandbox_policy(),
model: self.model_info.slug.clone(),
@@ -411,14 +419,19 @@ impl Session {
let config = session_configuration.original_config_do_not_use.clone();
let mut per_turn_config = (*config).clone();
per_turn_config.cwd = cwd;
per_turn_config.workspace_roots = session_configuration.workspace_roots.clone();
per_turn_config.model_reasoning_effort =
session_configuration.collaboration_mode.reasoning_effort();
per_turn_config.model_reasoning_summary = session_configuration.model_reasoning_summary;
per_turn_config.service_tier = session_configuration.service_tier.clone();
per_turn_config.personality = session_configuration.personality;
per_turn_config.approvals_reviewer = session_configuration.approvals_reviewer;
per_turn_config.permissions.permission_profile =
session_configuration.permission_profile.clone();
per_turn_config
.permissions
.replace_permission_profile_constraint_with_active_profile(
session_configuration.permission_profile.clone(),
session_configuration.active_permission_profile(),
);
let permission_profile = session_configuration.permission_profile();
let resolved_web_search_mode =
resolve_web_search_mode_for_turn(&per_turn_config.web_search_mode, &permission_profile);
@@ -552,6 +565,7 @@ impl Session {
thread_source: session_configuration.thread_source,
environments,
cwd,
workspace_roots: session_configuration.workspace_roots.clone(),
current_date: Some(current_date),
timezone: Some(timezone),
app_server_client_name: session_configuration.app_server_client_name.clone(),
@@ -252,7 +252,6 @@ fn write_permissions_for_paths_skip_dirs_already_writable_under_workspace_root()
let file_path = AbsolutePathBuf::try_from(nested.join("file.txt"))
.expect("nested file path should be absolute");
let sandbox_policy = FileSystemSandboxPolicy::from(&SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: false,
@@ -274,7 +273,6 @@ fn write_permissions_for_paths_keep_dirs_outside_workspace_root() {
.expect("outside file path should be absolute");
let cwd_abs = cwd.abs();
let sandbox_policy = FileSystemSandboxPolicy::from(&SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
@@ -269,6 +269,7 @@ pub(crate) fn apply_spawn_agent_runtime_overrides(
config.permissions.shell_environment_policy = turn.shell_environment_policy.clone();
config.codex_linux_sandbox_exe = turn.codex_linux_sandbox_exe.clone();
config.cwd = turn.cwd.clone();
config.workspace_roots = turn.workspace_roots.clone();
config
.permissions
.set_permission_profile(turn.permission_profile())
@@ -3402,7 +3402,7 @@ async fn build_agent_spawn_config_uses_turn_context_values() {
turn.cwd = temp_dir.abs();
turn.codex_linux_sandbox_exe = Some(PathBuf::from("/bin/echo"));
let sandbox_policy = pick_allowed_sandbox_policy(
&turn.config.permissions.permission_profile,
turn.config.permissions.permission_profile_constraint(),
turn.config.legacy_sandbox_policy(),
turn.cwd.as_path(),
);
+3
View File
@@ -85,6 +85,7 @@ impl ToolOrchestrator {
enforce_managed_network: attempt.enforce_managed_network,
manager: attempt.manager,
sandbox_cwd: attempt.sandbox_cwd,
workspace_roots: attempt.workspace_roots,
codex_linux_sandbox_exe: attempt.codex_linux_sandbox_exe,
use_legacy_landlock: attempt.use_legacy_landlock,
windows_sandbox_level: attempt.windows_sandbox_level,
@@ -241,6 +242,7 @@ impl ToolOrchestrator {
enforce_managed_network: managed_network_active,
manager: &self.sandbox,
sandbox_cwd,
workspace_roots: &turn_ctx.workspace_roots,
codex_linux_sandbox_exe: turn_ctx.codex_linux_sandbox_exe.as_ref(),
use_legacy_landlock,
windows_sandbox_level: turn_ctx.windows_sandbox_level,
@@ -359,6 +361,7 @@ impl ToolOrchestrator {
enforce_managed_network: managed_network_active,
manager: &self.sandbox,
sandbox_cwd,
workspace_roots: &turn_ctx.workspace_roots,
codex_linux_sandbox_exe: None,
use_legacy_landlock,
windows_sandbox_level: turn_ctx.windows_sandbox_level,
@@ -99,6 +99,7 @@ impl ApplyPatchRuntime {
Some(FileSystemSandboxContext {
permissions,
cwd: Some(attempt.sandbox_cwd.clone()),
workspace_roots: attempt.workspace_roots.to_vec(),
windows_sandbox_level: attempt.windows_sandbox_level,
windows_sandbox_private_desktop: attempt.windows_sandbox_private_desktop,
use_legacy_landlock: attempt.use_legacy_landlock,
@@ -214,6 +214,7 @@ async fn file_system_sandbox_context_uses_active_attempt() {
enforce_managed_network: false,
manager: &manager,
sandbox_cwd: &path,
workspace_roots: std::slice::from_ref(&path),
codex_linux_sandbox_exe: None,
use_legacy_landlock: true,
windows_sandbox_level: WindowsSandboxLevel::RestrictedToken,
@@ -267,6 +268,7 @@ async fn no_sandbox_attempt_has_no_file_system_context() {
enforce_managed_network: false,
manager: &manager,
sandbox_cwd: &path,
workspace_roots: std::slice::from_ref(&path),
codex_linux_sandbox_exe: None,
use_legacy_landlock: false,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
@@ -111,6 +111,7 @@ async fn explicit_escalation_prepares_exec_without_managed_network() -> anyhow::
enforce_managed_network: false,
manager: &manager,
sandbox_cwd: &cwd,
workspace_roots: std::slice::from_ref(&cwd),
codex_linux_sandbox_exe: None,
use_legacy_landlock: false,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
@@ -168,6 +168,7 @@ pub(super) async fn try_run_zsh_fork(
windows_sandbox_level,
arg0,
sandbox_policy_cwd,
workspace_roots: attempt.workspace_roots.to_vec(),
codex_linux_sandbox_exe: ctx.turn.codex_linux_sandbox_exe.clone(),
use_legacy_landlock: ctx.turn.features.use_legacy_landlock(),
};
@@ -232,7 +233,7 @@ pub(super) async fn try_run_zsh_fork(
pub(crate) async fn prepare_unified_exec_zsh_fork(
req: &crate::tools::runtimes::unified_exec::UnifiedExecRequest,
_attempt: &SandboxAttempt<'_>,
attempt: &SandboxAttempt<'_>,
ctx: &ToolCtx,
exec_request: ExecRequest,
shell_zsh_path: &std::path::Path,
@@ -269,6 +270,7 @@ pub(crate) async fn prepare_unified_exec_zsh_fork(
windows_sandbox_level: exec_request.windows_sandbox_level,
arg0: exec_request.arg0.clone(),
sandbox_policy_cwd: exec_request.windows_sandbox_policy_cwd.clone(),
workspace_roots: attempt.workspace_roots.to_vec(),
codex_linux_sandbox_exe: ctx.turn.codex_linux_sandbox_exe.clone(),
use_legacy_landlock: ctx.turn.features.use_legacy_landlock(),
};
@@ -753,6 +755,7 @@ struct CoreShellCommandExecutor {
windows_sandbox_level: WindowsSandboxLevel,
arg0: Option<String>,
sandbox_policy_cwd: AbsolutePathBuf,
workspace_roots: Vec<AbsolutePathBuf>,
codex_linux_sandbox_exe: Option<PathBuf>,
use_legacy_landlock: bool,
}
@@ -923,6 +926,7 @@ impl CoreShellCommandExecutor {
enforce_managed_network: self.network.is_some(),
network: self.network.as_ref(),
sandbox_policy_cwd: &self.sandbox_policy_cwd,
workspace_roots: &self.workspace_roots,
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.as_deref(),
use_legacy_landlock: self.use_legacy_landlock,
windows_sandbox_level: self.windows_sandbox_level,
+2
View File
@@ -385,6 +385,7 @@ pub(crate) struct SandboxAttempt<'a> {
pub enforce_managed_network: bool,
pub(crate) manager: &'a SandboxManager,
pub(crate) sandbox_cwd: &'a AbsolutePathBuf,
pub(crate) workspace_roots: &'a [AbsolutePathBuf],
pub codex_linux_sandbox_exe: Option<&'a std::path::PathBuf>,
pub use_legacy_landlock: bool,
pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel,
@@ -407,6 +408,7 @@ impl<'a> SandboxAttempt<'a> {
enforce_managed_network: self.enforce_managed_network,
network,
sandbox_policy_cwd: self.sandbox_cwd,
workspace_roots: self.workspace_roots,
codex_linux_sandbox_exe: self
.codex_linux_sandbox_exe
.map(std::path::PathBuf::as_path),
+2
View File
@@ -519,6 +519,7 @@ impl TestCodexBuilder {
load_default_config_for_test(home).await
};
config.cwd = cwd_override;
config.workspace_roots = vec![config.cwd.clone()];
config.model_provider = model_provider;
if let Ok(path) = codex_utils_cargo_bin::cargo_bin("codex") {
config.codex_self_exe = Some(path);
@@ -543,6 +544,7 @@ impl TestCodexBuilder {
for mutator in mutators {
mutator(&mut config);
}
config.workspace_roots = vec![config.cwd.clone()];
ensure_test_model_catalog(&mut config)?;
if config.include_apply_patch_tool {
-4
View File
@@ -827,7 +827,6 @@ fn scenarios() -> Vec<ScenarioSpec> {
use AskForApproval::*;
let workspace_write = |network_access| SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
@@ -2047,7 +2046,6 @@ async fn approving_apply_patch_for_session_skips_future_prompts_for_same_file()
let server = start_mock_server().await;
let approval_policy = AskForApproval::OnRequest;
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
@@ -2803,7 +2801,6 @@ allow_local_binding = true
)?;
let approval_policy = AskForApproval::OnFailure;
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: true,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
@@ -3083,7 +3080,6 @@ allow_local_binding = true
)?;
let approval_policy = AskForApproval::OnFailure;
let turn_sandbox_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: true,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
+1
View File
@@ -53,6 +53,7 @@ where
params,
&PermissionProfile::read_only(),
&cwd,
&[],
&None,
/*use_legacy_landlock*/ false,
/*stdout_stream*/ None,
@@ -575,9 +575,13 @@ async fn permissions_message_includes_writable_roots() -> Result<()> {
let permissions = permissions_texts(&req.single_request());
let normalize_line_endings = |s: &str| s.replace("\r\n", "\n");
let exec_policy = load_exec_policy(&test.config.config_layer_stack).await?;
let sandbox_policy = test.config.legacy_sandbox_policy();
let expected = PermissionsInstructions::from_policy(
&sandbox_policy,
let permission_profile = test
.config
.permissions
.permission_profile()
.materialize_project_roots_with_workspace_roots(&test.config.workspace_roots);
let expected = PermissionsInstructions::from_permission_profile(
&permission_profile,
AskForApproval::OnRequest,
test.config.approvals_reviewer,
&exec_policy,
@@ -63,6 +63,7 @@ async fn write_rollout_with_user_event(dir: &Path, thread_id: ThreadId) -> io::R
forked_from_id: None,
timestamp: TEST_TIMESTAMP.to_string(),
cwd: std::path::PathBuf::from("."),
workspace_roots: Vec::new(),
originator: "test_originator".to_string(),
cli_version: "test_version".to_string(),
source: SessionSource::Cli,
@@ -109,6 +110,7 @@ async fn write_rollout_with_meta_only(dir: &Path, thread_id: ThreadId) -> io::Re
forked_from_id: None,
timestamp: TEST_TIMESTAMP.to_string(),
cwd: std::path::PathBuf::from("."),
workspace_roots: Vec::new(),
originator: "test_originator".to_string(),
cli_version: "test_version".to_string(),
source: SessionSource::Cli,
+6 -3
View File
@@ -436,9 +436,12 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ true,
);
let sandbox_policy = permission_profile
.to_legacy_sandbox_policy(config.cwd.as_path())
.expect("workspace profile should have legacy projection");
let sandbox_policy = codex_sandboxing::compatibility_sandbox_policy_for_permission_profile(
&permission_profile,
&permission_profile.file_system_sandbox_policy(),
permission_profile.network_sandbox_policy(),
config.cwd.as_path(),
);
codex
.submit(Op::OverrideTurnContext {
cwd: None,
@@ -285,7 +285,6 @@ async fn expect_request_permissions_event(
fn workspace_write_excluding_tmp() -> SandboxPolicy {
SandboxPolicy::WorkspaceWrite {
writable_roots: vec![],
network_access: false,
exclude_tmpdir_env_var: true,
exclude_slash_tmp: true,
@@ -29,11 +29,13 @@ fn resume_history(
turn_id: Some(turn_id.clone()),
trace_id: None,
cwd: config.cwd.to_path_buf(),
workspace_roots: config.workspace_roots.clone(),
current_date: None,
timezone: None,
approval_policy: config.permissions.approval_policy.value(),
sandbox_policy: config.legacy_sandbox_policy(),
permission_profile: None,
active_permission_profile: None,
network: None,
file_system_sandbox_policy: None,
model: previous_model.to_string(),
@@ -141,6 +141,7 @@ async fn backfill_scans_existing_rollouts() -> Result<()> {
forked_from_id: None,
timestamp: "2026-01-27T12:00:00Z".to_string(),
cwd: codex_home.to_path_buf(),
workspace_roots: Vec::new(),
originator: "test".to_string(),
cli_version: "test".to_string(),
source: SessionSource::default(),
+12 -7
View File
@@ -906,9 +906,11 @@ allow_local_binding = true
.enable(Feature::UnifiedExec)
.expect("test config should allow feature update");
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::Never);
config.permissions.permission_profile = Constrained::allow_any(
PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy_for_config),
);
config
.permissions
.replace_permission_profile_constraint(Constrained::allow_any(
PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy_for_config),
));
});
let test = builder.build_remote_aware(server).await?;
assert!(
@@ -2752,10 +2754,13 @@ async fn unified_exec_enforces_glob_deny_read_policy() -> Result<()> {
},
access: FileSystemAccessMode::None,
});
config.permissions.permission_profile =
Constrained::allow_any(PermissionProfile::from_runtime_permissions(
&file_system_sandbox_policy,
NetworkSandboxPolicy::Restricted,
config
.permissions
.replace_permission_profile_constraint(Constrained::allow_any(
PermissionProfile::from_runtime_permissions(
&file_system_sandbox_policy,
NetworkSandboxPolicy::Restricted,
),
));
});
let TestCodex {
+3 -2
View File
@@ -344,11 +344,12 @@ async fn user_shell_command_does_not_set_network_sandbox_env_var() -> anyhow::Re
let server = responses::start_mock_server().await;
let mut builder = core_test_support::test_codex::test_codex().with_config(|config| {
let file_system_sandbox_policy = config.permissions.file_system_sandbox_policy();
config.permissions.permission_profile =
config.permissions.replace_permission_profile_constraint(
codex_config::Constrained::allow_any(PermissionProfile::from_runtime_permissions(
&file_system_sandbox_policy,
NetworkSandboxPolicy::Restricted,
));
)),
);
});
let test = builder.build(&server).await?;
@@ -127,6 +127,7 @@ async fn windows_restricted_token_rejects_exact_and_glob_deny_read_policy() -> a
},
&permission_profile,
&cwd,
std::slice::from_ref(&cwd),
&None,
/*use_legacy_landlock*/ false,
/*stdout_stream*/ None,
@@ -211,6 +212,7 @@ async fn windows_elevated_enforces_exact_and_glob_deny_read_policy() -> anyhow::
},
&permission_profile,
&cwd,
std::slice::from_ref(&cwd),
&None,
/*use_legacy_landlock*/ false,
/*stdout_stream*/ None,

Some files were not shown because too many files have changed in this diff Show More