Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 31 additions & 8 deletions codex-rs/app-server/src/codex_message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
};
Expand Down Expand Up @@ -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(),
Expand All @@ -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,
};
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
};
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 1 addition & 3 deletions codex-rs/core/src/session/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down
7 changes: 1 addition & 6 deletions codex-rs/core/src/session/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
Expand Down
5 changes: 3 additions & 2 deletions codex-rs/exec/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 2 additions & 3 deletions codex-rs/exec/tests/event_processor_with_json_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
19 changes: 6 additions & 13 deletions codex-rs/mcp-server/src/outgoing_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
106 changes: 91 additions & 15 deletions codex-rs/protocol/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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 {
Expand Down
Loading
Loading