permissions: make SessionConfigured profile-only (#19774)

## Why

`SessionConfiguredEvent` is the internal event that tells clients what
permissions are active for a session. Emitting both `sandbox_policy` and
`permission_profile` leaves two possible authorities and forces every
consumer to decide which one to honor. At this point in the migration,
the profile is expressive enough to represent managed, disabled, and
external sandbox enforcement, so the internal event can be profile-only.

The wire compatibility concern is older serialized events or rollout
data that only contain `sandbox_policy`; those still need to
deserialize.

## What Changed

- Removes `sandbox_policy` from `SessionConfiguredEvent` and makes
`permission_profile` required.
- Adds custom deserialization so old payloads with only `sandbox_policy`
are upgraded to a cwd-anchored `PermissionProfile`.
- Updates core event emission and TUI session handling to sync
permissions from the profile directly.
- Updates app-server response construction to derive the legacy
`sandbox` response field from the active thread snapshot instead of from
`SessionConfiguredEvent`.
- Updates yolo-mode display logic to treat both
`PermissionProfile::Disabled` and managed unrestricted filesystem plus
enabled network as full-access, while still preserving the distinction
between no sandbox and external sandboxing.

## Verification

- `cargo test -p codex-protocol session_configured_event --lib`
- `cargo test -p codex-protocol serialize_event --lib`
- `cargo test -p codex-exec session_configured --lib`
- `cargo test -p codex-app-server
thread_response_permission_profile_preserves_enforcement --lib`
- `cargo test -p codex-core
session_configured_reports_permission_profile_for_external_sandbox
--lib`
- `cargo test -p codex-tui session_configured --lib`
- `cargo test -p codex-tui
yolo_mode_includes_managed_full_access_profiles --lib`


































---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19774).
* #19900
* #19899
* #19776
* #19775
* __->__ #19774
This commit is contained in:
Michael Bolin
2026-04-27 22:06:47 -07:00
committed by GitHub
Unverified
parent 5ba908d179
commit bf38def44e
17 changed files with 227 additions and 161 deletions
@@ -4235,9 +4235,13 @@ impl CodexMessageProcessor {
thread_status,
/*has_live_in_progress_turn*/ false,
);
let permission_profile = thread_response_permission_profile(
codex_thread.config_snapshot().await.permission_profile,
let config_snapshot = codex_thread.config_snapshot().await;
let sandbox = thread_response_sandbox_policy(
&config_snapshot.permission_profile,
config_snapshot.cwd.as_path(),
);
let permission_profile =
thread_response_permission_profile(config_snapshot.permission_profile.clone());
let response = ThreadResumeResponse {
thread,
@@ -4248,7 +4252,7 @@ impl CodexMessageProcessor {
instruction_sources,
approval_policy: session_configured.approval_policy.into(),
approvals_reviewer: session_configured.approvals_reviewer.into(),
sandbox: session_configured.sandbox_policy.into(),
sandbox,
permission_profile,
reasoning_effort: session_configured.reasoning_effort,
};
@@ -4831,9 +4835,13 @@ impl CodexMessageProcessor {
.await,
/*has_in_progress_turn*/ false,
);
let permission_profile = thread_response_permission_profile(
forked_thread.config_snapshot().await.permission_profile,
let config_snapshot = forked_thread.config_snapshot().await;
let sandbox = thread_response_sandbox_policy(
&config_snapshot.permission_profile,
config_snapshot.cwd.as_path(),
);
let permission_profile =
thread_response_permission_profile(config_snapshot.permission_profile);
let response = ThreadForkResponse {
thread: thread.clone(),
@@ -4844,7 +4852,7 @@ impl CodexMessageProcessor {
instruction_sources,
approval_policy: session_configured.approval_policy.into(),
approvals_reviewer: session_configured.approvals_reviewer.into(),
sandbox: session_configured.sandbox_policy.into(),
sandbox,
permission_profile,
reasoning_effort: session_configured.reasoning_effort,
};
@@ -8091,13 +8099,14 @@ async fn handle_pending_thread_resume_request(
service_tier,
approval_policy,
approvals_reviewer,
sandbox_policy,
sandbox_policy: _,
permission_profile,
cwd,
reasoning_effort,
..
} = pending.config_snapshot;
let instruction_sources = pending.instruction_sources;
let sandbox = thread_response_sandbox_policy(&permission_profile, cwd.as_path());
let permission_profile = thread_response_permission_profile(permission_profile);
let response = ThreadResumeResponse {
@@ -8109,7 +8118,7 @@ async fn handle_pending_thread_resume_request(
instruction_sources,
approval_policy: approval_policy.into(),
approvals_reviewer: approvals_reviewer.into(),
sandbox: sandbox_policy.into(),
sandbox,
permission_profile,
reasoning_effort,
};
@@ -9240,6 +9249,20 @@ fn thread_response_permission_profile(
Some(permission_profile.into())
}
fn thread_response_sandbox_policy(
permission_profile: &codex_protocol::models::PermissionProfile,
cwd: &Path,
) -> codex_app_server_protocol::SandboxPolicy {
let file_system_policy = permission_profile.file_system_sandbox_policy();
let sandbox_policy = codex_sandboxing::compatibility_sandbox_policy_for_permission_profile(
permission_profile,
&file_system_policy,
permission_profile.network_sandbox_policy(),
cwd,
);
sandbox_policy.into()
}
fn requested_permissions_trust_project(overrides: &ConfigOverrides, cwd: &Path) -> bool {
if matches!(
overrides.sandbox_mode,
+1 -3
View File
@@ -867,7 +867,6 @@ impl Session {
// Dispatch the SessionConfiguredEvent first and then report any errors.
// If resuming, include converted initial messages in the payload so UIs can render them immediately.
let initial_messages = initial_history.get_event_msgs();
let session_sandbox_policy = session_configuration.sandbox_policy();
let events = std::iter::once(Event {
id: INITIAL_SUBMIT_ID.to_owned(),
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
@@ -879,8 +878,7 @@ impl Session {
service_tier: session_configuration.service_tier,
approval_policy: session_configuration.approval_policy.value(),
approvals_reviewer: session_configuration.approvals_reviewer,
sandbox_policy: session_sandbox_policy.clone(),
permission_profile: Some(session_configuration.permission_profile()),
permission_profile: session_configuration.permission_profile(),
cwd: session_configuration.cwd.clone(),
reasoning_effort: session_configuration.collaboration_mode.reasoning_effort(),
history_log_id,
+1 -6
View File
@@ -1527,17 +1527,12 @@ async fn session_configured_reports_permission_profile_for_external_sandbox() ->
let test = builder.build(&server).await?;
assert_eq!(
test.session_configured.sandbox_policy,
expected_sandbox_policy
);
let expected_permission_profile =
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
&expected_sandbox_policy,
);
assert_eq!(
test.session_configured.permission_profile,
Some(expected_permission_profile),
test.session_configured.permission_profile, expected_permission_profile,
"ExternalSandbox is represented explicitly instead of as a lossy root-write profile"
);
Ok(())
+3 -2
View File
@@ -1046,8 +1046,9 @@ fn session_configured_from_thread_response(
service_tier,
approval_policy,
approvals_reviewer,
sandbox_policy,
permission_profile,
permission_profile: permission_profile.unwrap_or_else(|| {
PermissionProfile::from_legacy_sandbox_policy_for_cwd(&sandbox_policy, cwd.as_path())
}),
cwd,
reasoning_effort,
history_log_id: 0,
@@ -28,9 +28,9 @@ use codex_app_server_protocol::TurnStartedNotification;
use codex_app_server_protocol::TurnStatus;
use codex_app_server_protocol::WebSearchAction as ApiWebSearchAction;
use codex_protocol::ThreadId;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::WebSearchAction;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionConfiguredEvent;
use codex_utils_absolute_path::test_support::PathBufExt;
use codex_utils_absolute_path::test_support::test_path_buf;
@@ -114,8 +114,7 @@ fn session_configured_produces_thread_started_event() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/tmp/project").abs(),
reasoning_effort: None,
history_log_id: 0,
+6 -13
View File
@@ -231,10 +231,10 @@ mod tests {
use anyhow::Result;
use codex_protocol::ThreadId;
use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionConfiguredEvent;
use codex_utils_absolute_path::test_support::PathBufExt;
use codex_utils_absolute_path::test_support::test_path_buf;
@@ -304,8 +304,7 @@ mod tests {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffort::default()),
history_log_id: 1,
@@ -349,8 +348,7 @@ mod tests {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffort::default()),
history_log_id: 1,
@@ -389,9 +387,7 @@ mod tests {
"model_provider_id": "test-provider",
"approval_policy": "never",
"approvals_reviewer": "user",
"sandbox_policy": {
"type": "read-only"
},
"permission_profile": session_configured_event.permission_profile,
"cwd": test_path_buf("/home/user/project"),
"reasoning_effort": session_configured_event.reasoning_effort,
"history_log_id": session_configured_event.history_log_id,
@@ -419,8 +415,7 @@ mod tests {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffort::default()),
history_log_id: 1,
@@ -460,9 +455,7 @@ mod tests {
"model_provider_id": "test-provider",
"approval_policy": "never",
"approvals_reviewer": "user",
"sandbox_policy": {
"type": "read-only"
},
"permission_profile": session_configured_event.permission_profile,
"cwd": test_path_buf("/home/user/project"),
"reasoning_effort": session_configured_event.reasoning_effort,
"history_log_id": session_configured_event.history_log_id,
+91 -15
View File
@@ -3541,7 +3541,7 @@ pub struct SessionNetworkProxyRuntime {
pub socks_addr: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
#[derive(Debug, Clone, Serialize, JsonSchema, TS)]
pub struct SessionConfiguredEvent {
pub session_id: ThreadId,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -3569,16 +3569,8 @@ pub struct SessionConfiguredEvent {
#[serde(default)]
pub approvals_reviewer: ApprovalsReviewer,
/// Legacy sandbox projection for commands executed in the system.
///
/// Consumers should prefer `permission_profile` when it is present. This
/// field remains available as a compatibility fallback for older emitters
/// and sessions that only expose legacy sandbox state.
pub sandbox_policy: SandboxPolicy,
/// Canonical effective permissions for commands executed in the session.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub permission_profile: Option<PermissionProfile>,
pub permission_profile: PermissionProfile,
/// Working directory that should be treated as the *root* of the
/// session.
@@ -3609,6 +3601,70 @@ pub struct SessionConfiguredEvent {
pub rollout_path: Option<PathBuf>,
}
impl<'de> Deserialize<'de> for SessionConfiguredEvent {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct Wire {
session_id: ThreadId,
forked_from_id: Option<ThreadId>,
#[serde(default)]
thread_name: Option<String>,
model: String,
model_provider_id: String,
service_tier: Option<ServiceTier>,
approval_policy: AskForApproval,
#[serde(default)]
approvals_reviewer: ApprovalsReviewer,
// `SessionConfiguredEvent` is persisted into rollout history. Older
// rollouts only have `sandbox_policy`, so accept it on deserialize
// and immediately project it into the canonical `permission_profile`.
sandbox_policy: Option<SandboxPolicy>,
permission_profile: Option<PermissionProfile>,
cwd: AbsolutePathBuf,
reasoning_effort: Option<ReasoningEffortConfig>,
history_log_id: u64,
history_entry_count: usize,
initial_messages: Option<Vec<EventMsg>>,
network_proxy: Option<SessionNetworkProxyRuntime>,
rollout_path: Option<PathBuf>,
}
let wire = Wire::deserialize(deserializer)?;
let permission_profile = match (wire.permission_profile, wire.sandbox_policy) {
(Some(permission_profile), _) => permission_profile,
(None, Some(sandbox_policy)) => PermissionProfile::from_legacy_sandbox_policy_for_cwd(
&sandbox_policy,
wire.cwd.as_path(),
),
(None, None) => {
return Err(serde::de::Error::missing_field("permission_profile"));
}
};
Ok(Self {
session_id: wire.session_id,
forked_from_id: wire.forked_from_id,
thread_name: wire.thread_name,
model: wire.model,
model_provider_id: wire.model_provider_id,
service_tier: wire.service_tier,
approval_policy: wire.approval_policy,
approvals_reviewer: wire.approvals_reviewer,
permission_profile,
cwd: wire.cwd,
reasoning_effort: wire.reasoning_effort,
history_log_id: wire.history_log_id,
history_entry_count: wire.history_entry_count,
initial_messages: wire.initial_messages,
network_proxy: wire.network_proxy,
rollout_path: wire.rollout_path,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ThreadNameUpdatedEvent {
pub thread_id: ThreadId,
@@ -5088,6 +5144,7 @@ mod tests {
fn serialize_event() -> Result<()> {
let conversation_id = ThreadId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?;
let rollout_file = NamedTempFile::new()?;
let permission_profile = PermissionProfile::read_only();
let event = Event {
id: "1234".to_string(),
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
@@ -5099,8 +5156,7 @@ mod tests {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: permission_profile.clone(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -5120,9 +5176,7 @@ mod tests {
"model_provider_id": "openai",
"approval_policy": "never",
"approvals_reviewer": "user",
"sandbox_policy": {
"type": "read-only"
},
"permission_profile": permission_profile,
"cwd": test_path_buf("/home/user/project"),
"reasoning_effort": "medium",
"history_log_id": 0,
@@ -5134,6 +5188,28 @@ mod tests {
Ok(())
}
#[test]
fn deserialize_legacy_session_configured_event_uses_sandbox_policy() -> Result<()> {
let cwd = test_path_buf("/home/user/project");
let value = json!({
"session_id": "67e55044-10b1-426f-9247-bb680e5fe0c8",
"model": "codex-mini-latest",
"model_provider_id": "openai",
"approval_policy": "never",
"approvals_reviewer": "user",
"sandbox_policy": {
"type": "read-only"
},
"cwd": cwd,
"history_log_id": 0,
"history_entry_count": 0,
});
let event: SessionConfiguredEvent = serde_json::from_value(value)?;
assert_eq!(event.permission_profile, PermissionProfile::read_only());
Ok(())
}
#[test]
fn vec_u8_as_base64_serialization_and_deserialization() -> Result<()> {
let event = ExecCommandOutputDeltaEvent {
+2 -2
View File
@@ -556,6 +556,7 @@ mod tests {
use crate::app::test_support::app_enabled_in_effective_config;
use crate::app::test_support::make_test_app;
use crate::test_support::PathBufExt;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::SessionConfiguredEvent;
@@ -653,8 +654,7 @@ mod tests {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: next_cwd.clone().abs(),
reasoning_effort: None,
history_log_id: 0,
+6 -12
View File
@@ -3566,8 +3566,7 @@ async fn render_clear_ui_header_after_long_transcript_for_snapshot() -> String {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/tmp/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::High),
history_log_id: 0,
@@ -4317,8 +4316,7 @@ async fn backtrack_selection_with_duplicate_history_targets_unique_turn() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: None,
history_log_id: 0,
@@ -4381,8 +4379,7 @@ async fn backtrack_selection_with_duplicate_history_targets_unique_turn() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: None,
history_log_id: 0,
@@ -4475,8 +4472,7 @@ async fn backtrack_resubmit_preserves_data_image_urls_in_user_turn() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: None,
history_log_id: 0,
@@ -4859,8 +4855,7 @@ async fn new_session_requests_shutdown_for_previous_conversation() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: None,
history_log_id: 0,
@@ -4972,8 +4967,7 @@ async fn clear_only_ui_reset_preserves_chat_session_state() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/tmp/project").abs(),
reasoning_effort: None,
history_log_id: 0,
+6 -29
View File
@@ -142,12 +142,8 @@ use codex_protocol::items::AgentMessageContent;
use codex_protocol::items::AgentMessageItem;
use codex_protocol::items::UserMessageItem;
use codex_protocol::models::MessagePhase;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::SandboxEnforcement;
use codex_protocol::models::local_image_label_text;
use codex_protocol::parse_command::ParsedCommand;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::plan_tool::PlanItemArg as UpdatePlanItemArg;
use codex_protocol::plan_tool::StepStatus as UpdatePlanItemStatus;
#[cfg(test)]
@@ -1620,8 +1616,7 @@ fn thread_session_state_to_legacy_event(
service_tier: session.service_tier,
approval_policy: session.approval_policy,
approvals_reviewer: session.approvals_reviewer,
sandbox_policy: session.sandbox_policy,
permission_profile: Some(session.permission_profile),
permission_profile: session.permission_profile,
cwd: session.cwd,
reasoning_effort: session.reasoning_effort,
history_log_id: session.history_log_id,
@@ -2349,32 +2344,14 @@ impl ChatWidget {
self.config.permissions.approval_policy =
Constrained::allow_only(event.approval_policy);
}
let permission_sync = match event.permission_profile.clone() {
Some(permission_profile) => self
.config
.permissions
.set_permission_profile(permission_profile),
None => self
.config
.permissions
.set_legacy_sandbox_policy(event.sandbox_policy.clone(), event.cwd.as_path()),
};
let permission_sync = self
.config
.permissions
.set_permission_profile(event.permission_profile.clone());
if let Err(err) = permission_sync {
tracing::warn!(%err, "failed to sync permissions from SessionConfigured");
let permission_profile = event.permission_profile.clone().unwrap_or_else(|| {
let file_system_sandbox_policy =
FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
&event.sandbox_policy,
event.cwd.as_path(),
);
PermissionProfile::from_runtime_permissions_with_enforcement(
SandboxEnforcement::from_legacy_sandbox_policy(&event.sandbox_policy),
&file_system_sandbox_policy,
NetworkSandboxPolicy::from(&event.sandbox_policy),
)
});
self.config.permissions.permission_profile =
Constrained::allow_only(permission_profile);
Constrained::allow_only(event.permission_profile.clone());
}
self.config.approvals_reviewer = event.approvals_reviewer;
self.status_line_project_root_name_cache = None;
@@ -16,8 +16,7 @@ async fn submission_preserves_text_elements_and_local_images() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -121,8 +120,7 @@ async fn submission_includes_configured_permission_profile() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: Some(expected_permission_profile.clone()),
permission_profile: expected_permission_profile.clone(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -172,10 +170,7 @@ async fn submission_keeps_profile_when_legacy_projection_is_external() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::ExternalSandbox {
network_access: codex_protocol::protocol::NetworkAccess::Restricted,
},
permission_profile: Some(expected_permission_profile.clone()),
permission_profile: expected_permission_profile.clone(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -218,8 +213,7 @@ async fn submission_with_remote_and_local_images_keeps_local_placeholder_numberi
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -314,8 +308,7 @@ async fn enter_with_only_remote_images_submits_user_turn() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -380,8 +373,7 @@ async fn shift_enter_with_only_remote_images_does_not_submit_user_turn() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -421,8 +413,7 @@ async fn enter_with_only_remote_images_does_not_submit_when_modal_is_active() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -462,8 +453,7 @@ async fn enter_with_only_remote_images_does_not_submit_when_input_disabled() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -506,8 +496,7 @@ async fn submission_prefers_selected_duplicate_skill_path() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -1004,8 +1004,7 @@ async fn bang_shell_enter_while_task_running_submits_run_user_shell_command() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -24,8 +24,7 @@ async fn resumed_initial_messages_render_history() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -138,8 +137,7 @@ async fn replayed_user_message_preserves_text_elements_and_local_images() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -200,8 +198,7 @@ async fn replayed_user_message_preserves_remote_image_urls() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -256,7 +253,6 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() {
.expect("set sandbox policy");
chat.config.cwd = test_path_buf("/home/user/main").abs();
let legacy_fallback_sandbox = SandboxPolicy::new_read_only_policy();
let expected_cwd = test_path_buf("/home/user/sub-agent").abs();
let expected_file_system_policy = FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
@@ -289,8 +285,7 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: legacy_fallback_sandbox,
permission_profile: Some(expected_permission_profile.clone()),
permission_profile: expected_permission_profile.clone(),
cwd: expected_cwd.clone(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -355,8 +350,9 @@ async fn session_configured_external_sandbox_keeps_external_runtime_policy() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: expected_sandbox.clone(),
permission_profile: None,
permission_profile: codex_protocol::models::PermissionProfile::External {
network: NetworkSandboxPolicy::Restricted,
},
cwd: test_path_buf("/home/user/external").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -405,8 +401,7 @@ async fn replayed_user_message_with_only_remote_images_renders_history_cell() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -459,8 +454,7 @@ async fn replayed_user_message_with_only_local_images_does_not_render_history_ce
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -781,8 +775,7 @@ async fn replayed_reasoning_item_hides_raw_reasoning_when_disabled() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
cwd: test_project_path().abs(),
reasoning_effort: None,
history_log_id: 0,
@@ -829,8 +822,7 @@ async fn replayed_reasoning_item_shows_raw_reasoning_when_enabled() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: codex_protocol::models::PermissionProfile::read_only(),
cwd: test_project_path().abs(),
reasoning_effort: None,
history_log_id: 0,
@@ -483,8 +483,7 @@ async fn permissions_selection_marks_auto_review_current_after_session_configure
service_tier: None,
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::AutoReview,
sandbox_policy: SandboxPolicy::new_workspace_write_policy(),
permission_profile: None,
permission_profile: PermissionProfile::workspace_write(),
cwd: test_project_path().abs(),
reasoning_effort: None,
history_log_id: 0,
@@ -519,6 +518,13 @@ async fn permissions_selection_marks_auto_review_current_with_custom_workspace_w
.set_enabled(Feature::GuardianApproval, /*enabled*/ true);
let extra_root = test_path_buf("/tmp/guardian-approvals-extra").abs();
let cwd = test_project_path().abs();
let permission_profile = PermissionProfile::workspace_write_with(
&[extra_root],
codex_protocol::protocol::NetworkSandboxPolicy::Restricted,
/*exclude_tmpdir_env_var*/ false,
/*exclude_slash_tmp*/ false,
);
chat.handle_codex_event(Event {
id: "session-configured-custom-workspace".to_string(),
@@ -531,14 +537,8 @@ async fn permissions_selection_marks_auto_review_current_with_custom_workspace_w
service_tier: None,
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: ApprovalsReviewer::AutoReview,
sandbox_policy: SandboxPolicy::WorkspaceWrite {
writable_roots: vec![extra_root],
network_access: false,
exclude_tmpdir_env_var: false,
exclude_slash_tmp: false,
},
permission_profile: None,
cwd: test_project_path().abs(),
permission_profile,
cwd,
reasoning_effort: None,
history_log_id: 0,
history_entry_count: 0,
@@ -1059,8 +1059,7 @@ async fn submit_user_message_emits_structured_plugin_mentions_from_bindings() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -1305,8 +1304,7 @@ async fn plan_slash_command_with_args_submits_prompt_in_plan_mode() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
@@ -1741,8 +1741,7 @@ async fn session_configured_clears_goal_status_footer() {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/home/user/project").abs(),
reasoning_effort: Some(ReasoningEffortConfig::default()),
history_log_id: 0,
+44 -11
View File
@@ -54,6 +54,8 @@ use codex_protocol::account::PlanType;
use codex_protocol::mcp::Resource;
#[cfg(test)]
use codex_protocol::mcp::ResourceTemplate;
use codex_protocol::models::ManagedFileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::WebSearchAction;
use codex_protocol::models::local_image_label_text;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
@@ -64,7 +66,6 @@ use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::FileChange;
use codex_protocol::protocol::McpAuthStatus;
use codex_protocol::protocol::McpInvocation;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionConfiguredEvent;
use codex_protocol::request_user_input::RequestUserInputAnswer;
use codex_protocol::request_user_input::RequestUserInputQuestion;
@@ -1240,7 +1241,7 @@ pub(crate) fn new_session_info(
model,
reasoning_effort,
approval_policy,
sandbox_policy,
permission_profile,
..
} = event;
// Header box rendered as history (so it appears at the very top)
@@ -1251,7 +1252,7 @@ pub(crate) fn new_session_info(
config.cwd.to_path_buf(),
CODEX_CLI_VERSION,
)
.with_yolo_mode(has_yolo_permissions(approval_policy, &sandbox_policy));
.with_yolo_mode(has_yolo_permissions(approval_policy, &permission_profile));
let mut parts: Vec<Box<dyn HistoryCell>> = vec![Box::new(header)];
if is_first_event {
@@ -1313,14 +1314,23 @@ pub(crate) fn new_session_info(
pub(crate) fn is_yolo_mode(config: &Config) -> bool {
has_yolo_permissions(
config.permissions.approval_policy.value(),
&config
.permissions
.legacy_sandbox_policy(config.cwd.as_path()),
&config.permissions.permission_profile(),
)
}
fn has_yolo_permissions(approval_policy: AskForApproval, sandbox_policy: &SandboxPolicy) -> bool {
approval_policy == AskForApproval::Never && *sandbox_policy == SandboxPolicy::DangerFullAccess
fn has_yolo_permissions(
approval_policy: AskForApproval,
permission_profile: &PermissionProfile,
) -> bool {
approval_policy == AskForApproval::Never
&& matches!(
permission_profile,
PermissionProfile::Disabled
| PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Unrestricted,
network: codex_protocol::protocol::NetworkSandboxPolicy::Enabled,
}
)
}
pub(crate) fn new_user_prompt(
@@ -2993,7 +3003,6 @@ mod tests {
use codex_protocol::parse_command::ParsedCommand;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::McpAuthStatus;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionConfiguredEvent;
use dirs::home_dir;
use pretty_assertions::assert_eq;
@@ -3184,8 +3193,7 @@ mod tests {
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
permission_profile: None,
permission_profile: PermissionProfile::read_only(),
cwd: test_path_buf("/tmp/project").abs(),
reasoning_effort: None,
history_log_id: 0,
@@ -4205,6 +4213,31 @@ mod tests {
insta::assert_snapshot!(rendered);
}
#[test]
fn yolo_mode_includes_managed_full_access_profiles() {
let permission_profile = PermissionProfile::Managed {
file_system: ManagedFileSystemPermissions::Unrestricted,
network: codex_protocol::protocol::NetworkSandboxPolicy::Enabled,
};
assert!(has_yolo_permissions(
AskForApproval::Never,
&permission_profile
));
}
#[test]
fn yolo_mode_excludes_external_sandbox_profiles() {
let permission_profile = PermissionProfile::External {
network: codex_protocol::protocol::NetworkSandboxPolicy::Enabled,
};
assert!(!has_yolo_permissions(
AskForApproval::Never,
&permission_profile
));
}
#[test]
fn session_header_directory_center_truncates() {
let mut dir = home_dir().expect("home directory");