app-server: stop returning thread permission profiles (#22792)

## Why

The app-server thread lifecycle API should no longer expose the full
`PermissionProfile` value. After the permissions-profile migration,
clients should round-trip only the active profile identity through
`activePermissionProfile` and `permissions` when that identity is known.

The full profile is server-side config. Treating a response-derived
legacy sandbox projection as a new local profile can lose named-profile
restrictions and accidentally widen permissions on the next turn. The
legacy `sandbox` response field remains only as the
compatibility/display fallback.

## What Changed

- Removed `permissionProfile` from `ThreadStartResponse`,
`ThreadResumeResponse`, and `ThreadForkResponse`.
- Stopped populating that field in app-server thread start/resume/fork
responses.
- Updated embedded exec/TUI response mapping to derive display
permission state from local config or the legacy sandbox fallback
instead of a response profile value.
- Added a TUI turn override shape that distinguishes preserving server
permissions, selecting an active profile id, and sending a legacy
sandbox for an explicit local override.
- Preserved remote app-server permissions across turns by sending
`permissions` only when an `activePermissionProfile` id is known, and
otherwise sending no sandbox override unless the user selected a local
override.
- Kept embedded `thread/resume` hydration server-authored when
`activePermissionProfile` is absent, which matches the live-thread
attach path where the server ignores requested overrides.
- Updated the app-server README to remove the obsolete lifecycle
response `permissionProfile` reference. The remaining
`permissionProfile` README references are request-side permission
overrides.
- Regenerated app-server JSON schema and TypeScript fixtures.
- Kept the generated typed response enum exempt from
`large_enum_variant`, matching the existing payload enum exemption after
the lifecycle response variants shrank.

## How To Review

Start with `codex-rs/app-server-protocol/src/protocol/v2/thread.rs` to
confirm the response shape, then check the response construction in
`codex-rs/app-server/src/request_processors`. The generated schema and
TypeScript fixture changes are mechanical follow-through from the
protocol removal.

The TUI behavior is the delicate part: review
`codex-rs/tui/src/app_server_session.rs` for response hydration and
turn-start override projection, then
`codex-rs/tui/src/app/thread_routing.rs` for the decision about whether
the next turn should preserve the server snapshot, send an active
profile id, or send a legacy sandbox for an explicit local override.

## Verification

- `just write-app-server-schema`
- `cargo test -p codex-app-server-protocol
thread_lifecycle_responses_default_missing_optional_fields`
- `cargo test -p codex-exec
session_configured_from_thread_response_uses_permission_profile_from_config`
- `cargo test -p codex-tui --lib thread_response`
- `cargo test -p codex-tui turn_permissions_`
- `cargo test -p codex-tui
resume_response_restores_turns_from_thread_items`
- `cargo test -p codex-analytics
track_response_only_enqueues_analytics_relevant_responses`
- `just fix -p codex-analytics`
- `just fix -p codex-app-server-protocol`
- `just fix -p codex-tui`
- `just argument-comment-lint`

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/22792).
* #22795
* __->__ #22792
This commit is contained in:
Michael Bolin authored and GitHub committed 2026-05-15 12:45:48 -07:00
1 parent 6afe00efda
commit 83bbb4f326
22 files changed
+249 -1170

No files matched your search

@@ -206,7 +206,6 @@ fn sample_thread_start_response(
approval_policy: AppServerAskForApproval::OnFailure, approval_policy: AppServerAskForApproval::OnFailure,
approvals_reviewer: AppServerApprovalsReviewer::User, approvals_reviewer: AppServerApprovalsReviewer::User,
sandbox: AppServerSandboxPolicy::DangerFullAccess, sandbox: AppServerSandboxPolicy::DangerFullAccess,
permission_profile: None,
active_permission_profile: None, active_permission_profile: None,
reasoning_effort: None, reasoning_effort: None,
}) })
@@ -263,7 +262,6 @@ fn sample_thread_resume_response_with_source(
approval_policy: AppServerAskForApproval::OnFailure, approval_policy: AppServerAskForApproval::OnFailure,
approvals_reviewer: AppServerApprovalsReviewer::User, approvals_reviewer: AppServerApprovalsReviewer::User,
sandbox: AppServerSandboxPolicy::DangerFullAccess, sandbox: AppServerSandboxPolicy::DangerFullAccess,
permission_profile: None,
active_permission_profile: None, active_permission_profile: None,
reasoning_effort: None, reasoning_effort: None,
}) })
-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::AskForApproval as AppServerAskForApproval;
use codex_app_server_protocol::ClientRequest; use codex_app_server_protocol::ClientRequest;
use codex_app_server_protocol::ClientResponsePayload; 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::RequestId;
use codex_app_server_protocol::SandboxPolicy as AppServerSandboxPolicy; use codex_app_server_protocol::SandboxPolicy as AppServerSandboxPolicy;
use codex_app_server_protocol::SessionSource as AppServerSessionSource; 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::TurnStatus as AppServerTurnStatus;
use codex_app_server_protocol::TurnSteerParams; use codex_app_server_protocol::TurnSteerParams;
use codex_app_server_protocol::TurnSteerResponse; 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::PathBufExt;
use codex_utils_absolute_path::test_support::test_path_buf; use codex_utils_absolute_path::test_support::test_path_buf;
use std::collections::HashSet; 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 { fn sample_thread_start_response() -> ClientResponsePayload {
ClientResponsePayload::ThreadStart(ThreadStartResponse { ClientResponsePayload::ThreadStart(ThreadStartResponse {
thread: sample_thread("thread-1"), thread: sample_thread("thread-1"),
@@ -158,7 +152,6 @@ fn sample_thread_start_response() -> ClientResponsePayload {
approval_policy: AppServerAskForApproval::OnFailure, approval_policy: AppServerAskForApproval::OnFailure,
approvals_reviewer: AppServerApprovalsReviewer::User, approvals_reviewer: AppServerApprovalsReviewer::User,
sandbox: AppServerSandboxPolicy::DangerFullAccess, sandbox: AppServerSandboxPolicy::DangerFullAccess,
permission_profile: Some(sample_permission_profile()),
active_permission_profile: None, active_permission_profile: None,
reasoning_effort: None, reasoning_effort: None,
}) })
@@ -176,7 +169,6 @@ fn sample_thread_resume_response() -> ClientResponsePayload {
approval_policy: AppServerAskForApproval::OnFailure, approval_policy: AppServerAskForApproval::OnFailure,
approvals_reviewer: AppServerApprovalsReviewer::User, approvals_reviewer: AppServerApprovalsReviewer::User,
sandbox: AppServerSandboxPolicy::DangerFullAccess, sandbox: AppServerSandboxPolicy::DangerFullAccess,
permission_profile: Some(sample_permission_profile()),
active_permission_profile: None, active_permission_profile: None,
reasoning_effort: None, reasoning_effort: None,
}) })
@@ -194,7 +186,6 @@ fn sample_thread_fork_response() -> ClientResponsePayload {
approval_policy: AppServerAskForApproval::OnFailure, approval_policy: AppServerAskForApproval::OnFailure,
approvals_reviewer: AppServerApprovalsReviewer::User, approvals_reviewer: AppServerApprovalsReviewer::User,
sandbox: AppServerSandboxPolicy::DangerFullAccess, sandbox: AppServerSandboxPolicy::DangerFullAccess,
permission_profile: Some(sample_permission_profile()),
active_permission_profile: None, active_permission_profile: None,
reasoning_effort: None, reasoning_effort: None,
}) })
@@ -15527,7 +15527,7 @@
"$ref": "#/definitions/v2/SandboxPolicy" "$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` for profile provenance."
}, },
"serviceTier": { "serviceTier": {
"type": [ "type": [
@@ -17019,7 +17019,7 @@
"$ref": "#/definitions/v2/SandboxPolicy" "$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` for profile provenance."
}, },
"serviceTier": { "serviceTier": {
"type": [ "type": [
@@ -17327,7 +17327,7 @@
"$ref": "#/definitions/v2/SandboxPolicy" "$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` for profile provenance."
}, },
"serviceTier": { "serviceTier": {
"type": [ "type": [
@@ -13351,7 +13351,7 @@
"$ref": "#/definitions/SandboxPolicy" "$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` for profile provenance."
}, },
"serviceTier": { "serviceTier": {
"type": [ "type": [
@@ -14843,7 +14843,7 @@
"$ref": "#/definitions/SandboxPolicy" "$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` for profile provenance."
}, },
"serviceTier": { "serviceTier": {
"type": [ "type": [
@@ -15151,7 +15151,7 @@
"$ref": "#/definitions/SandboxPolicy" "$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` for profile provenance."
}, },
"serviceTier": { "serviceTier": {
"type": [ "type": [
@@ -470,202 +470,6 @@
], ],
"type": "string" "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": { "FileUpdateChange": {
"properties": { "properties": {
"diff": { "diff": {
@@ -904,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": { "ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [ "enum": [
@@ -2572,7 +2247,7 @@
"$ref": "#/definitions/SandboxPolicy" "$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` for profile provenance."
}, },
"serviceTier": { "serviceTier": {
"type": [ "type": [
@@ -470,202 +470,6 @@
], ],
"type": "string" "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": { "FileUpdateChange": {
"properties": { "properties": {
"diff": { "diff": {
@@ -904,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": { "ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [ "enum": [
@@ -2572,7 +2247,7 @@
"$ref": "#/definitions/SandboxPolicy" "$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` for profile provenance."
}, },
"serviceTier": { "serviceTier": {
"type": [ "type": [
@@ -470,202 +470,6 @@
], ],
"type": "string" "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": { "FileUpdateChange": {
"properties": { "properties": {
"diff": { "diff": {
@@ -904,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": { "ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [ "enum": [
@@ -2572,7 +2247,7 @@
"$ref": "#/definitions/SandboxPolicy" "$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` for profile provenance."
}, },
"serviceTier": { "serviceTier": {
"type": [ "type": [
@@ -16,7 +16,6 @@ instructionSources: Array<AbsolutePathBuf>, approvalPolicy: AskForApproval, /**
*/ */
approvalsReviewer: ApprovalsReviewer, /** approvalsReviewer: ApprovalsReviewer, /**
* Legacy sandbox policy retained for compatibility. Experimental clients * Legacy sandbox policy retained for compatibility. Experimental clients
* should prefer `permissionProfile` when they need exact runtime * should prefer `activePermissionProfile` for profile provenance.
* permissions.
*/ */
sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null}; sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null};
@@ -16,7 +16,6 @@ instructionSources: Array<AbsolutePathBuf>, approvalPolicy: AskForApproval, /**
*/ */
approvalsReviewer: ApprovalsReviewer, /** approvalsReviewer: ApprovalsReviewer, /**
* Legacy sandbox policy retained for compatibility. Experimental clients * Legacy sandbox policy retained for compatibility. Experimental clients
* should prefer `permissionProfile` when they need exact runtime * should prefer `activePermissionProfile` for profile provenance.
* permissions.
*/ */
sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null}; sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null};
@@ -16,7 +16,6 @@ instructionSources: Array<AbsolutePathBuf>, approvalPolicy: AskForApproval, /**
*/ */
approvalsReviewer: ApprovalsReviewer, /** approvalsReviewer: ApprovalsReviewer, /**
* Legacy sandbox policy retained for compatibility. Experimental clients * Legacy sandbox policy retained for compatibility. Experimental clients
* should prefer `permissionProfile` when they need exact runtime * should prefer `activePermissionProfile` for profile provenance.
* permissions.
*/ */
sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null}; sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null};
@@ -223,6 +223,7 @@ macro_rules! client_request_definitions {
/// Typed response from the server to the client. /// Typed response from the server to the client.
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
#[allow(clippy::large_enum_variant)]
#[serde(tag = "method", rename_all = "camelCase")] #[serde(tag = "method", rename_all = "camelCase")]
pub enum ClientResponse { pub enum ClientResponse {
$( $(
@@ -2301,7 +2302,6 @@ mod tests {
approval_policy: v2::AskForApproval::OnFailure, approval_policy: v2::AskForApproval::OnFailure,
approvals_reviewer: v2::ApprovalsReviewer::User, approvals_reviewer: v2::ApprovalsReviewer::User,
sandbox: v2::SandboxPolicy::DangerFullAccess, sandbox: v2::SandboxPolicy::DangerFullAccess,
permission_profile: None,
active_permission_profile: None, active_permission_profile: None,
reasoning_effort: None, reasoning_effort: None,
}, },
@@ -2348,7 +2348,6 @@ mod tests {
"sandbox": { "sandbox": {
"type": "dangerFullAccess" "type": "dangerFullAccess"
}, },
"permissionProfile": null,
"activePermissionProfile": null, "activePermissionProfile": null,
"reasoningEffort": null "reasoningEffort": null
} }
@@ -3498,9 +3498,6 @@ fn thread_lifecycle_responses_default_missing_optional_fields() {
assert_eq!(start.instruction_sources, Vec::<AbsolutePathBuf>::new()); assert_eq!(start.instruction_sources, Vec::<AbsolutePathBuf>::new());
assert_eq!(resume.instruction_sources, Vec::<AbsolutePathBuf>::new()); assert_eq!(resume.instruction_sources, Vec::<AbsolutePathBuf>::new());
assert_eq!(fork.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!(start.active_permission_profile, None);
assert_eq!(resume.active_permission_profile, None); assert_eq!(resume.active_permission_profile, None);
assert_eq!(fork.active_permission_profile, None); assert_eq!(fork.active_permission_profile, None);
@@ -1,7 +1,6 @@
use super::ActivePermissionProfile; use super::ActivePermissionProfile;
use super::ApprovalsReviewer; use super::ApprovalsReviewer;
use super::AskForApproval; use super::AskForApproval;
use super::PermissionProfile;
use super::PermissionProfileSelectionParams; use super::PermissionProfileSelectionParams;
use super::SandboxMode; use super::SandboxMode;
use super::SandboxPolicy; use super::SandboxPolicy;
@@ -213,14 +212,8 @@ pub struct ThreadStartResponse {
/// Reviewer currently used for approval requests on this thread. /// Reviewer currently used for approval requests on this thread.
pub approvals_reviewer: ApprovalsReviewer, pub approvals_reviewer: ApprovalsReviewer,
/// Legacy sandbox policy retained for compatibility. Experimental clients /// Legacy sandbox policy retained for compatibility. Experimental clients
/// should prefer `permissionProfile` when they need exact runtime /// should prefer `activePermissionProfile` for profile provenance.
/// permissions.
pub sandbox: SandboxPolicy, 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 /// Named or implicit built-in profile that produced the active
/// permissions, when known. /// permissions, when known.
#[experimental("thread/start.activePermissionProfile")] #[experimental("thread/start.activePermissionProfile")]
@@ -339,14 +332,8 @@ pub struct ThreadResumeResponse {
/// Reviewer currently used for approval requests on this thread. /// Reviewer currently used for approval requests on this thread.
pub approvals_reviewer: ApprovalsReviewer, pub approvals_reviewer: ApprovalsReviewer,
/// Legacy sandbox policy retained for compatibility. Experimental clients /// Legacy sandbox policy retained for compatibility. Experimental clients
/// should prefer `permissionProfile` when they need exact runtime /// should prefer `activePermissionProfile` for profile provenance.
/// permissions.
pub sandbox: SandboxPolicy, 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 /// Named or implicit built-in profile that produced the active
/// permissions, when known. /// permissions, when known.
#[experimental("thread/resume.activePermissionProfile")] #[experimental("thread/resume.activePermissionProfile")]
@@ -459,14 +446,8 @@ pub struct ThreadForkResponse {
/// Reviewer currently used for approval requests on this thread. /// Reviewer currently used for approval requests on this thread.
pub approvals_reviewer: ApprovalsReviewer, pub approvals_reviewer: ApprovalsReviewer,
/// Legacy sandbox policy retained for compatibility. Experimental clients /// Legacy sandbox policy retained for compatibility. Experimental clients
/// should prefer `permissionProfile` when they need exact runtime /// should prefer `activePermissionProfile` for profile provenance.
/// permissions.
pub sandbox: SandboxPolicy, 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 /// Named or implicit built-in profile that produced the active
/// permissions, when known. /// permissions, when known.
#[experimental("thread/fork.activePermissionProfile")] #[experimental("thread/fork.activePermissionProfile")]
+1 -1
View File
@@ -133,7 +133,7 @@ Example with notification opt-out:
- `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; relative paths resolve against the effective thread cwd. For permissions, prefer experimental `permissions` profile selection by id; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissions`. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd`. - `thread/start` — create a new thread; emits `thread/started` (including the current `thread.status`) and auto-subscribes you to turn/item events for that thread. When the request includes a `cwd` and the resolved sandbox is `workspace-write` or full access, app-server also marks that project as trusted in the user `config.toml`. Pass `sessionStartSource: "clear"` when starting a replacement thread after clearing the current session so `SessionStart` hooks receive `source: "clear"` instead of the default `"startup"`. Experimental `runtimeWorkspaceRoots` replaces the thread-scoped runtime workspace roots used to materialize `:workspace_roots`; relative paths resolve against the effective thread cwd. For permissions, prefer experimental `permissions` profile selection by id; the legacy `sandbox` shorthand is still accepted but cannot be combined with `permissions`. Experimental `environments` selects the sticky execution environments for turns on the thread; omit it to use the server default, pass `[]` to disable environments, or pass explicit environment ids with per-environment `cwd`.
- `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`. - `thread/resume` — reopen an existing thread by id so subsequent `turn/start` calls append to it. Accepts the same permission override rules as `thread/start`.
- `thread/fork` — fork an existing thread into a new thread id by copying the stored history; if the source thread is currently mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Experimental clients can pass `excludeTurns: true` when they plan to page fork history via `thread/turns/list` instead of receiving the full turn array immediately. Accepts the same permission override rules as `thread/start`. - `thread/fork` — fork an existing thread into a new thread id by copying the stored history; if the source thread is currently mid-turn, the fork records the same interruption marker as `turn/interrupt` instead of inheriting an unmarked partial turn suffix. The returned `thread.forkedFromId` points at the source thread when known. Accepts `ephemeral: true` for an in-memory temporary fork, emits `thread/started` (including the current `thread.status`), and auto-subscribes you to turn/item events for the new thread. Experimental clients can pass `excludeTurns: true` when they plan to page fork history via `thread/turns/list` instead of receiving the full turn array immediately. Accepts the same permission override rules as `thread/start`.
- `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. Experimental clients can read `runtimeWorkspaceRoots` for the thread-scoped runtime roots, `permissionProfile` for the exact active runtime permissions, and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known. - `thread/start`, `thread/resume`, and `thread/fork` responses include the legacy `sandbox` compatibility projection. Experimental clients can read `runtimeWorkspaceRoots` for the thread-scoped runtime roots and `activePermissionProfile` for the named or implicit built-in profile identity/provenance when known.
- `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. - `thread/list` — page through stored rollouts; supports cursor-based pagination and optional `modelProviders`, `sourceKinds`, `archived`, `cwd`, and `searchTerm` filters. Each returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded.
- `thread/loaded/list` — list the thread ids currently loaded in memory. - `thread/loaded/list` — list the thread ids currently loaded in memory.
- `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded. - `thread/read` — read a stored thread by id without resuming it; optionally include turns via `includeTurns`. The returned `thread` includes `status` (`ThreadStatus`), defaulting to `notLoaded` when the thread is not currently loaded.
@@ -626,7 +626,6 @@ pub(super) async fn handle_pending_thread_resume_request(
approval_policy: approval_policy.into(), approval_policy: approval_policy.into(),
approvals_reviewer: approvals_reviewer.into(), approvals_reviewer: approvals_reviewer.into(),
sandbox, sandbox,
permission_profile: Some(permission_profile.into()),
active_permission_profile, active_permission_profile,
reasoning_effort, reasoning_effort,
}; };
@@ -1199,7 +1199,6 @@ impl ThreadRequestProcessor {
approval_policy: config_snapshot.approval_policy.into(), approval_policy: config_snapshot.approval_policy.into(),
approvals_reviewer: config_snapshot.approvals_reviewer.into(), approvals_reviewer: config_snapshot.approvals_reviewer.into(),
sandbox, sandbox,
permission_profile: Some(config_snapshot.permission_profile.into()),
active_permission_profile, active_permission_profile,
reasoning_effort: config_snapshot.reasoning_effort, reasoning_effort: config_snapshot.reasoning_effort,
}; };
@@ -2554,7 +2553,6 @@ impl ThreadRequestProcessor {
approval_policy: session_configured.approval_policy.into(), approval_policy: session_configured.approval_policy.into(),
approvals_reviewer: session_configured.approvals_reviewer.into(), approvals_reviewer: session_configured.approvals_reviewer.into(),
sandbox, sandbox,
permission_profile: Some(config_snapshot.permission_profile.into()),
active_permission_profile, active_permission_profile,
reasoning_effort: session_configured.reasoning_effort, reasoning_effort: session_configured.reasoning_effort,
}; };
@@ -3215,7 +3213,6 @@ impl ThreadRequestProcessor {
approval_policy: session_configured.approval_policy.into(), approval_policy: session_configured.approval_policy.into(),
approvals_reviewer: session_configured.approvals_reviewer.into(), approvals_reviewer: session_configured.approvals_reviewer.into(),
sandbox, sandbox,
permission_profile: Some(config_snapshot.permission_profile.into()),
active_permission_profile, active_permission_profile,
reasoning_effort: session_configured.reasoning_effort, reasoning_effort: session_configured.reasoning_effort,
}; };
+2 -10
View File
@@ -1089,11 +1089,7 @@ fn session_configured_from_thread_start_response(
response.service_tier.clone(), response.service_tier.clone(),
response.approval_policy.to_core(), response.approval_policy.to_core(),
response.approvals_reviewer.to_core(), response.approvals_reviewer.to_core(),
response config.permissions.effective_permission_profile(),
.permission_profile
.clone()
.map(Into::into)
.unwrap_or_else(|| config.permissions.effective_permission_profile()),
response.active_permission_profile.clone().map(Into::into), response.active_permission_profile.clone().map(Into::into),
response.cwd.clone(), response.cwd.clone(),
response.reasoning_effort, response.reasoning_effort,
@@ -1114,11 +1110,7 @@ fn session_configured_from_thread_resume_response(
response.service_tier.clone(), response.service_tier.clone(),
response.approval_policy.to_core(), response.approval_policy.to_core(),
response.approvals_reviewer.to_core(), response.approvals_reviewer.to_core(),
response config.permissions.effective_permission_profile(),
.permission_profile
.clone()
.map(Into::into)
.unwrap_or_else(|| config.permissions.effective_permission_profile()),
response.active_permission_profile.clone().map(Into::into), response.active_permission_profile.clone().map(Into::into),
response.cwd.clone(), response.cwd.clone(),
response.reasoning_effort, response.reasoning_effort,
+6 -5
View File
@@ -528,7 +528,7 @@ async fn session_configured_from_thread_response_uses_review_policy_from_respons
} }
#[tokio::test] #[tokio::test]
async fn session_configured_from_thread_response_uses_permission_profile_from_response() { async fn session_configured_from_thread_response_uses_permission_profile_from_config() {
let codex_home = tempdir().expect("create temp codex home"); let codex_home = tempdir().expect("create temp codex home");
let cwd = tempdir().expect("create temp cwd"); let cwd = tempdir().expect("create temp cwd");
let config = ConfigBuilder::default() let config = ConfigBuilder::default()
@@ -537,13 +537,15 @@ async fn session_configured_from_thread_response_uses_permission_profile_from_re
.build() .build()
.await .await
.expect("build config"); .expect("build config");
let mut response = sample_thread_start_response(); let response = sample_thread_start_response();
response.permission_profile = Some(PermissionProfile::Disabled.into());
let event = session_configured_from_thread_start_response(&response, &config) let event = session_configured_from_thread_start_response(&response, &config)
.expect("build bootstrap session configured event"); .expect("build bootstrap session configured event");
assert_eq!(event.permission_profile, PermissionProfile::Disabled); assert_eq!(
event.permission_profile,
config.permissions.effective_permission_profile()
);
} }
fn sample_thread_start_response() -> ThreadStartResponse { fn sample_thread_start_response() -> ThreadStartResponse {
@@ -583,7 +585,6 @@ fn sample_thread_start_response() -> ThreadStartResponse {
exclude_tmpdir_env_var: false, exclude_tmpdir_env_var: false,
exclude_slash_tmp: false, exclude_slash_tmp: false,
}, },
permission_profile: None,
active_permission_profile: None, active_permission_profile: None,
reasoning_effort: None, reasoning_effort: None,
} }
+1
View File
@@ -16,6 +16,7 @@ use crate::app_event::WindowsSandboxEnableMode;
use crate::app_event_sender::AppEventSender; use crate::app_event_sender::AppEventSender;
use crate::app_server_session::AppServerSession; use crate::app_server_session::AppServerSession;
use crate::app_server_session::AppServerStartedThread; use crate::app_server_session::AppServerStartedThread;
use crate::app_server_session::TurnPermissionsOverride;
use crate::app_server_session::app_server_rate_limit_snapshots; use crate::app_server_session::app_server_rate_limit_snapshots;
use crate::bottom_pane::AppLinkViewParams; use crate::bottom_pane::AppLinkViewParams;
use crate::bottom_pane::ApprovalRequest; use crate::bottom_pane::ApprovalRequest;
+112 -10
View File
@@ -588,14 +588,11 @@ impl App {
let config = self.chat_widget.config_ref(); let config = self.chat_widget.config_ref();
let approvals_reviewer = let approvals_reviewer =
approvals_reviewer.unwrap_or(config.approvals_reviewer); approvals_reviewer.unwrap_or(config.approvals_reviewer);
let active_permission_profile = let permissions_override = Self::turn_permissions_override_from_config(
if config.permissions.effective_permission_profile() config,
== permission_profile.clone() permission_profile,
{ self.runtime_permission_profile_override.as_ref(),
config.permissions.active_permission_profile() );
} else {
None
};
app_server app_server
.turn_start( .turn_start(
thread_id, thread_id,
@@ -603,8 +600,7 @@ impl App {
cwd.clone(), cwd.clone(),
*approval_policy, *approval_policy,
approvals_reviewer, approvals_reviewer,
permission_profile.clone(), permissions_override,
active_permission_profile,
config.permissions.user_visible_workspace_roots(), config.permissions.user_visible_workspace_roots(),
model.to_string(), model.to_string(),
*effort, *effort,
@@ -700,6 +696,36 @@ impl App {
} }
} }
fn turn_permissions_override_from_config(
config: &Config,
permission_profile: &PermissionProfile,
runtime_permission_profile_override: Option<&PermissionProfile>,
) -> TurnPermissionsOverride {
let effective_permission_profile = config.permissions.effective_permission_profile();
if &effective_permission_profile == permission_profile
&& let Some(active_permission_profile) = config.permissions.active_permission_profile()
{
return TurnPermissionsOverride::ActiveProfile(active_permission_profile);
}
let runtime_permission_profile_override =
runtime_permission_profile_override.map(|profile| {
profile
.clone()
.materialize_project_roots_with_workspace_roots(
&config.effective_workspace_roots(),
)
});
if runtime_permission_profile_override
.as_ref()
.is_some_and(|profile| profile == permission_profile)
{
return TurnPermissionsOverride::LegacySandbox(permission_profile.clone());
}
TurnPermissionsOverride::Preserve
}
pub(super) fn handle_skills_list_result( pub(super) fn handle_skills_list_result(
&mut self, &mut self,
result: Result<SkillsListResponse>, result: Result<SkillsListResponse>,
@@ -1457,3 +1483,79 @@ impl App {
Ok(()) Ok(())
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
async fn config_with_workspace_profile() -> Config {
let temp_dir = tempfile::tempdir().expect("tempdir");
ConfigBuilder::default()
.codex_home(temp_dir.path().to_path_buf())
.harness_overrides(ConfigOverrides {
default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string()),
..ConfigOverrides::default()
})
.build()
.await
.expect("config should build")
}
#[tokio::test]
async fn turn_permissions_use_active_profile_when_available() {
let config = config_with_workspace_profile().await;
let permission_profile = config.permissions.effective_permission_profile();
assert_eq!(
App::turn_permissions_override_from_config(
&config,
&permission_profile,
/*runtime_permission_profile_override*/ None,
),
TurnPermissionsOverride::ActiveProfile(ActivePermissionProfile::new(
BUILT_IN_PERMISSION_PROFILE_WORKSPACE
))
);
}
#[tokio::test]
async fn turn_permissions_preserve_server_snapshot_without_local_override() {
let mut config = config_with_workspace_profile().await;
config
.permissions
.set_permission_profile(PermissionProfile::read_only())
.expect("read-only profile should be allowed");
let permission_profile = config.permissions.effective_permission_profile();
assert_eq!(
App::turn_permissions_override_from_config(
&config,
&permission_profile,
/*runtime_permission_profile_override*/ None,
),
TurnPermissionsOverride::Preserve
);
}
#[tokio::test]
async fn turn_permissions_send_legacy_sandbox_for_local_override() {
let mut config = config_with_workspace_profile().await;
let permission_profile = PermissionProfile::workspace_write();
config
.permissions
.set_permission_profile(permission_profile.clone())
.expect("workspace profile should be allowed");
let effective_permission_profile = config.permissions.effective_permission_profile();
assert_eq!(
App::turn_permissions_override_from_config(
&config,
&effective_permission_profile,
Some(&permission_profile),
),
TurnPermissionsOverride::LegacySandbox(effective_permission_profile)
);
}
}
+108 -111
View File
@@ -172,6 +172,16 @@ pub(crate) struct AppServerStartedThread {
pub(crate) turns: Vec<Turn>, pub(crate) turns: Vec<Turn>,
} }
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum TurnPermissionsOverride {
/// Leave the app-server thread's sticky permission profile unchanged.
Preserve,
/// Select a named or built-in profile by id.
ActiveProfile(ActivePermissionProfile),
/// Apply a user-selected legacy/custom permission profile.
LegacySandbox(PermissionProfile),
}
impl AppServerSession { impl AppServerSession {
pub(crate) fn new(client: AppServerClient) -> Self { pub(crate) fn new(client: AppServerClient) -> Self {
Self { Self {
@@ -548,8 +558,7 @@ impl AppServerSession {
cwd: PathBuf, cwd: PathBuf,
approval_policy: AskForApproval, approval_policy: AskForApproval,
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer, approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer,
permission_profile: PermissionProfile, permissions_override: TurnPermissionsOverride,
active_permission_profile: Option<ActivePermissionProfile>,
workspace_roots: &[AbsolutePathBuf], workspace_roots: &[AbsolutePathBuf],
model: String, model: String,
effort: Option<codex_protocol::openai_models::ReasoningEffort>, effort: Option<codex_protocol::openai_models::ReasoningEffort>,
@@ -560,13 +569,8 @@ impl AppServerSession {
output_schema: Option<serde_json::Value>, output_schema: Option<serde_json::Value>,
) -> Result<TurnStartResponse> { ) -> Result<TurnStartResponse> {
let request_id = self.next_request_id(); let request_id = self.next_request_id();
let (sandbox_policy, permissions) = turn_permissions_overrides( let (sandbox_policy, permissions) =
&permission_profile, turn_permissions_overrides(permissions_override, cwd.as_path());
active_permission_profile,
cwd.as_path(),
workspace_roots,
self.thread_params_mode(),
);
self.client self.client
.request_typed(ClientRequest::TurnStart { .request_typed(ClientRequest::TurnStart {
request_id, request_id,
@@ -1185,32 +1189,32 @@ fn permissions_selection_from_active_profile(
} }
fn turn_permissions_overrides( fn turn_permissions_overrides(
permission_profile: &PermissionProfile, permissions_override: TurnPermissionsOverride,
active_permission_profile: Option<ActivePermissionProfile>,
cwd: &std::path::Path, cwd: &std::path::Path,
_workspace_roots: &[AbsolutePathBuf],
thread_params_mode: ThreadParamsMode,
) -> ( ) -> (
Option<codex_app_server_protocol::SandboxPolicy>, Option<codex_app_server_protocol::SandboxPolicy>,
Option<PermissionProfileSelectionParams>, Option<PermissionProfileSelectionParams>,
) { ) {
let permissions = if matches!(thread_params_mode, ThreadParamsMode::Embedded) { match permissions_override {
active_permission_profile.map(permissions_selection_from_active_profile) TurnPermissionsOverride::Preserve => (None, None),
} else { TurnPermissionsOverride::ActiveProfile(active_permission_profile) => (
None None,
}; Some(permissions_selection_from_active_profile(
let sandbox_policy = (matches!(thread_params_mode, ThreadParamsMode::Remote) active_permission_profile,
|| permissions.is_none()) )),
.then(|| { ),
let legacy_profile = legacy_compatible_permission_profile(permission_profile, cwd); TurnPermissionsOverride::LegacySandbox(permission_profile) => {
let policy = legacy_profile let legacy_profile = legacy_compatible_permission_profile(&permission_profile, cwd);
.to_legacy_sandbox_policy(cwd) let policy = legacy_profile
.unwrap_or_else(|err| { .to_legacy_sandbox_policy(cwd)
unreachable!("legacy-compatible permissions must project to legacy policy: {err}") .unwrap_or_else(|err| {
}); unreachable!(
policy.into() "legacy-compatible permissions must project to legacy policy: {err}"
}); )
(sandbox_policy, permissions) });
(Some(policy.into()), None)
}
}
} }
fn permissions_selection_from_config( fn permissions_selection_from_config(
@@ -1413,9 +1417,8 @@ async fn thread_session_state_from_thread_start_response(
config: &Config, config: &Config,
thread_params_mode: ThreadParamsMode, thread_params_mode: ThreadParamsMode,
) -> Result<ThreadSessionState, String> { ) -> Result<ThreadSessionState, String> {
let permission_profile = permission_profile_from_thread_response( let permission_profile = display_permission_profile_from_thread_response(
&response.sandbox, &response.sandbox,
response.permission_profile.as_ref(),
response.cwd.as_path(), response.cwd.as_path(),
config, config,
thread_params_mode, thread_params_mode,
@@ -1446,13 +1449,21 @@ async fn thread_session_state_from_thread_resume_response(
config: &Config, config: &Config,
thread_params_mode: ThreadParamsMode, thread_params_mode: ThreadParamsMode,
) -> Result<ThreadSessionState, String> { ) -> Result<ThreadSessionState, String> {
let permission_profile = permission_profile_from_thread_response( let permission_profile = if matches!(thread_params_mode, ThreadParamsMode::Embedded)
&response.sandbox, && response.active_permission_profile.is_none()
response.permission_profile.as_ref(), {
response.cwd.as_path(), PermissionProfile::from_legacy_sandbox_policy_for_cwd(
config, &response.sandbox.to_core(),
thread_params_mode, response.cwd.as_path(),
); )
} else {
display_permission_profile_from_thread_response(
&response.sandbox,
response.cwd.as_path(),
config,
thread_params_mode,
)
};
thread_session_state_from_thread_response( thread_session_state_from_thread_response(
&response.thread.id, &response.thread.id,
response.thread.forked_from_id.clone(), response.thread.forked_from_id.clone(),
@@ -1479,9 +1490,8 @@ async fn thread_session_state_from_thread_fork_response(
config: &Config, config: &Config,
thread_params_mode: ThreadParamsMode, thread_params_mode: ThreadParamsMode,
) -> Result<ThreadSessionState, String> { ) -> Result<ThreadSessionState, String> {
let permission_profile = permission_profile_from_thread_response( let permission_profile = display_permission_profile_from_thread_response(
&response.sandbox, &response.sandbox,
response.permission_profile.as_ref(),
response.cwd.as_path(), response.cwd.as_path(),
config, config,
thread_params_mode, thread_params_mode,
@@ -1507,16 +1517,12 @@ async fn thread_session_state_from_thread_fork_response(
.await .await
} }
fn permission_profile_from_thread_response( fn display_permission_profile_from_thread_response(
sandbox: &codex_app_server_protocol::SandboxPolicy, sandbox: &codex_app_server_protocol::SandboxPolicy,
permission_profile: Option<&codex_app_server_protocol::PermissionProfile>,
cwd: &std::path::Path, cwd: &std::path::Path,
config: &Config, config: &Config,
thread_params_mode: ThreadParamsMode, thread_params_mode: ThreadParamsMode,
) -> PermissionProfile { ) -> PermissionProfile {
if let Some(permission_profile) = permission_profile {
return permission_profile.clone().into();
}
match thread_params_mode { match thread_params_mode {
ThreadParamsMode::Embedded => config.permissions.effective_permission_profile(), ThreadParamsMode::Embedded => config.permissions.effective_permission_profile(),
ThreadParamsMode::Remote => { ThreadParamsMode::Remote => {
@@ -1692,16 +1698,12 @@ mod tests {
let cwd = test_path_buf("/workspace/project").abs(); let cwd = test_path_buf("/workspace/project").abs();
let active_permission_profile = let active_permission_profile =
ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_WORKSPACE); ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_WORKSPACE);
let workspace_roots = vec![cwd.clone()];
let expected_permissions = let expected_permissions =
permissions_selection_from_active_profile(active_permission_profile.clone()); permissions_selection_from_active_profile(active_permission_profile.clone());
let (sandbox_policy, permissions) = turn_permissions_overrides( let (sandbox_policy, permissions) = turn_permissions_overrides(
&PermissionProfile::workspace_write(), TurnPermissionsOverride::ActiveProfile(active_permission_profile),
Some(active_permission_profile),
cwd.as_path(), cwd.as_path(),
&workspace_roots,
ThreadParamsMode::Embedded,
); );
assert_eq!(sandbox_policy, None); assert_eq!(sandbox_policy, None);
@@ -1711,17 +1713,12 @@ mod tests {
#[test] #[test]
fn embedded_turn_permissions_select_profile_id_only() { fn embedded_turn_permissions_select_profile_id_only() {
let cwd = test_path_buf("/workspace/project").abs(); let cwd = test_path_buf("/workspace/project").abs();
let extra_root = test_path_buf("/workspace/cache").abs();
let active_permission_profile = let active_permission_profile =
ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_WORKSPACE); ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_WORKSPACE);
let workspace_roots = vec![cwd.clone(), extra_root];
let (sandbox_policy, permissions) = turn_permissions_overrides( let (sandbox_policy, permissions) = turn_permissions_overrides(
&PermissionProfile::workspace_write(), TurnPermissionsOverride::ActiveProfile(active_permission_profile),
Some(active_permission_profile),
cwd.as_path(), cwd.as_path(),
&workspace_roots,
ThreadParamsMode::Embedded,
); );
assert_eq!(sandbox_policy, None); assert_eq!(sandbox_policy, None);
@@ -1734,15 +1731,23 @@ mod tests {
} }
#[test] #[test]
fn embedded_turn_permissions_fall_back_to_sandbox_without_active_profile() { fn turn_permissions_preserve_thread_permissions_without_override() {
let cwd = test_path_buf("/workspace/project").abs();
let (sandbox_policy, permissions) =
turn_permissions_overrides(TurnPermissionsOverride::Preserve, cwd.as_path());
assert_eq!(sandbox_policy, None);
assert_eq!(permissions, None);
}
#[test]
fn legacy_turn_permissions_project_to_sandbox_when_explicitly_overridden() {
let cwd = test_path_buf("/workspace/project").abs(); let cwd = test_path_buf("/workspace/project").abs();
let (sandbox_policy, permissions) = turn_permissions_overrides( let (sandbox_policy, permissions) = turn_permissions_overrides(
&PermissionProfile::read_only(), TurnPermissionsOverride::LegacySandbox(PermissionProfile::read_only()),
/*active_permission_profile*/ None,
cwd.as_path(), cwd.as_path(),
std::slice::from_ref(&cwd),
ThreadParamsMode::Embedded,
); );
assert_eq!( assert_eq!(
@@ -1755,26 +1760,19 @@ mod tests {
} }
#[test] #[test]
fn remote_turn_permissions_use_sandbox_even_with_active_profile() { fn remote_turn_permissions_preserve_active_profile_selection() {
let cwd = test_path_buf("/workspace/project").abs(); let cwd = test_path_buf("/workspace/project").abs();
let active_permission_profile = ActivePermissionProfile::new("strict");
let expected_permissions =
permissions_selection_from_active_profile(active_permission_profile.clone());
let (sandbox_policy, permissions) = turn_permissions_overrides( let (sandbox_policy, permissions) = turn_permissions_overrides(
&PermissionProfile::read_only(), TurnPermissionsOverride::ActiveProfile(active_permission_profile),
Some(ActivePermissionProfile::new(
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
)),
cwd.as_path(), cwd.as_path(),
std::slice::from_ref(&cwd),
ThreadParamsMode::Remote,
); );
assert_eq!( assert_eq!(sandbox_policy, None);
sandbox_policy, assert_eq!(permissions, Some(expected_permissions));
Some(codex_app_server_protocol::SandboxPolicy::ReadOnly {
network_access: false
})
);
assert_eq!(permissions, None);
} }
#[tokio::test] #[tokio::test]
@@ -2109,7 +2107,6 @@ mod tests {
.to_legacy_sandbox_policy(test_path_buf("/tmp/project").as_path()) .to_legacy_sandbox_policy(test_path_buf("/tmp/project").as_path())
.expect("read-only profile must be legacy-compatible") .expect("read-only profile must be legacy-compatible")
.into(), .into(),
permission_profile: Some(read_only_profile.clone().into()),
active_permission_profile: None, active_permission_profile: None,
reasoning_effort: None, reasoning_effort: None,
}; };
@@ -2134,6 +2131,24 @@ mod tests {
assert_eq!(started.turns.len(), 1); assert_eq!(started.turns.len(), 1);
assert_eq!(started.turns[0], response.thread.turns[0]); assert_eq!(started.turns[0], response.thread.turns[0]);
let embedded_config = ConfigBuilder::default()
.codex_home(temp_dir.path().join("embedded-codex-home"))
.harness_overrides(ConfigOverrides {
default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string()),
..ConfigOverrides::default()
})
.build()
.await
.expect("config should build");
let started = started_thread_from_resume_response(
response.clone(),
&embedded_config,
ThreadParamsMode::Embedded,
)
.await
.expect("embedded resume response should map");
assert_eq!(started.session.permission_profile, read_only_profile);
let mut empty_roots_response = response; let mut empty_roots_response = response;
empty_roots_response.runtime_workspace_roots = Vec::new(); empty_roots_response.runtime_workspace_roots = Vec::new();
let started = started_thread_from_resume_response( let started = started_thread_from_resume_response(
@@ -2147,61 +2162,43 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
async fn remote_thread_response_prefers_permission_profile_over_legacy_sandbox() { async fn remote_thread_response_uses_legacy_sandbox_fallback() {
let temp_dir = tempfile::tempdir().expect("tempdir"); let temp_dir = tempfile::tempdir().expect("tempdir");
let config = build_config(&temp_dir).await; let config = build_config(&temp_dir).await;
let cwd = test_path_buf("/tmp/project").abs(); let cwd = test_path_buf("/tmp/project").abs();
let fallback_sandbox = PermissionProfile::read_only() let sandbox = PermissionProfile::read_only()
.to_legacy_sandbox_policy(cwd.as_path()) .to_legacy_sandbox_policy(cwd.as_path())
.expect("read-only profile must be legacy-compatible") .expect("read-only profile must be legacy-compatible")
.into(); .into();
let response_profile = AppServerPermissionProfile::Managed {
file_system: PermissionProfileFileSystemPermissions::Restricted {
entries: vec![
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::Read,
},
FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::ProjectRoots {
subpath: Some(".env".into()),
},
},
access: FileSystemAccessMode::None,
},
],
glob_scan_max_depth: None,
},
network: PermissionProfileNetworkPermissions { enabled: false },
};
let split_profile: PermissionProfile = response_profile.clone().into();
assert_eq!( assert_eq!(
permission_profile_from_thread_response( display_permission_profile_from_thread_response(
&fallback_sandbox, &sandbox,
Some(&response_profile),
cwd.as_path(), cwd.as_path(),
&config, &config,
ThreadParamsMode::Remote, ThreadParamsMode::Remote,
), ),
split_profile PermissionProfile::read_only()
); );
} }
#[tokio::test] #[tokio::test]
async fn embedded_thread_response_prefers_permission_profile_when_present() { async fn embedded_thread_response_uses_local_config_profile() {
let temp_dir = tempfile::tempdir().expect("tempdir"); let temp_dir = tempfile::tempdir().expect("tempdir");
let config = build_config(&temp_dir).await; let config = ConfigBuilder::default()
.codex_home(temp_dir.path().to_path_buf())
.harness_overrides(ConfigOverrides {
default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string()),
..ConfigOverrides::default()
})
.build()
.await
.expect("config should build");
let cwd = test_path_buf("/tmp/project").abs(); let cwd = test_path_buf("/tmp/project").abs();
let response_profile = PermissionProfile::read_only().into();
assert_eq!( assert_eq!(
permission_profile_from_thread_response( display_permission_profile_from_thread_response(
&codex_app_server_protocol::SandboxPolicy::DangerFullAccess, &codex_app_server_protocol::SandboxPolicy::DangerFullAccess,
Some(&response_profile),
cwd.as_path(), cwd.as_path(),
&config, &config,
ThreadParamsMode::Embedded, ThreadParamsMode::Embedded,
+3 -1
View File
@@ -34,9 +34,11 @@ pub(crate) struct ThreadSessionState {
pub(crate) service_tier: Option<String>, pub(crate) service_tier: Option<String>,
pub(crate) approval_policy: AskForApproval, pub(crate) approval_policy: AskForApproval,
pub(crate) approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer, pub(crate) approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer,
/// Canonical active permissions for this session. Legacy app-server /// Permission snapshot used by TUI display surfaces. Legacy app-server
/// responses are converted to a profile at ingestion time using the /// responses are converted to a profile at ingestion time using the
/// response cwd so cached sessions do not reinterpret cwd-bound grants. /// response cwd so cached sessions do not reinterpret cwd-bound grants.
/// Turn requests must not treat this snapshot as a local permission
/// override unless the user explicitly changed permissions in the TUI.
pub(crate) permission_profile: PermissionProfile, pub(crate) permission_profile: PermissionProfile,
/// Named or implicit built-in profile that produced `permission_profile`, /// Named or implicit built-in profile that produced `permission_profile`,
/// when the server knows it. /// when the server knows it.