mirror of
https://github.com/openai/codex.git
synced 2026-11-09 20:50:33 +00:00
permissions: move workspace roots onto thread state
This commit is contained in:
@@ -658,6 +658,7 @@ mod tests {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: next_cwd.clone().abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
message_history: None,
|
||||
|
||||
@@ -67,7 +67,7 @@ pub(super) fn emit_project_config_warnings(app_event_tx: &AppEventSender, config
|
||||
|
||||
pub(super) fn emit_system_bwrap_warning(app_event_tx: &AppEventSender, config: &Config) {
|
||||
let Some(message) = crate::legacy_core::config::system_bwrap_warning(
|
||||
config.permissions.permission_profile.get(),
|
||||
config.permissions.permission_profile_ref(),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -3845,6 +3845,7 @@ async fn render_clear_ui_header_after_long_transcript_for_snapshot() -> String {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/tmp/project").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::High),
|
||||
message_history: None,
|
||||
@@ -4093,6 +4094,7 @@ fn test_thread_session(thread_id: ThreadId, cwd: PathBuf) -> ThreadSessionState
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: cwd.abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
message_history: None,
|
||||
@@ -4668,6 +4670,7 @@ async fn backtrack_selection_with_duplicate_history_targets_unique_turn() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
message_history: None,
|
||||
@@ -4731,6 +4734,7 @@ async fn backtrack_selection_with_duplicate_history_targets_unique_turn() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
message_history: None,
|
||||
@@ -4823,6 +4827,7 @@ async fn backtrack_resubmit_preserves_data_image_urls_in_user_turn() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
message_history: None,
|
||||
@@ -5058,6 +5063,7 @@ async fn refreshed_snapshot_session_persists_resumed_turns() {
|
||||
)];
|
||||
let resumed_session = ThreadSessionState {
|
||||
cwd: test_path_buf("/tmp/refreshed").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
..initial_session.clone()
|
||||
};
|
||||
@@ -5222,6 +5228,7 @@ async fn new_session_requests_shutdown_for_previous_conversation() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
message_history: None,
|
||||
@@ -5343,6 +5350,7 @@ async fn clear_only_ui_reset_preserves_chat_session_state() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/tmp/project").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
message_history: None,
|
||||
|
||||
@@ -352,6 +352,7 @@ mod tests {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: cwd.abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
message_history: None,
|
||||
|
||||
@@ -603,6 +603,7 @@ impl App {
|
||||
approvals_reviewer,
|
||||
permission_profile.clone(),
|
||||
active_permission_profile,
|
||||
config.workspace_roots.clone(),
|
||||
model.to_string(),
|
||||
*effort,
|
||||
*summary,
|
||||
|
||||
@@ -25,11 +25,13 @@ impl App {
|
||||
.config_ref()
|
||||
.permissions
|
||||
.active_permission_profile();
|
||||
let workspace_roots = self.chat_widget.config_ref().workspace_roots.clone();
|
||||
let update_session = |session: &mut ThreadSessionState| {
|
||||
session.approval_policy = approval_policy;
|
||||
session.approvals_reviewer = approvals_reviewer;
|
||||
session.permission_profile = permission_profile.clone();
|
||||
session.active_permission_profile = active_permission_profile.clone();
|
||||
session.workspace_roots = workspace_roots.clone();
|
||||
};
|
||||
|
||||
if self.primary_thread_id == Some(active_thread_id)
|
||||
@@ -53,6 +55,7 @@ impl App {
|
||||
) -> ThreadSessionState {
|
||||
let permission_profile = self.current_permission_profile();
|
||||
let active_permission_profile = self.current_active_permission_profile();
|
||||
let workspace_roots = self.chat_widget.config_ref().workspace_roots.clone();
|
||||
let mut session = self
|
||||
.primary_session_configured
|
||||
.clone()
|
||||
@@ -71,6 +74,7 @@ impl App {
|
||||
permission_profile: permission_profile.clone(),
|
||||
active_permission_profile: active_permission_profile.clone(),
|
||||
cwd: thread.cwd.clone(),
|
||||
workspace_roots: workspace_roots.clone(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: self.chat_widget.current_reasoning_effort(),
|
||||
message_history: None,
|
||||
@@ -81,6 +85,7 @@ impl App {
|
||||
session.thread_name = thread.name.clone();
|
||||
session.model_provider_id = thread.model_provider.clone();
|
||||
session.cwd = thread.cwd.clone();
|
||||
session.workspace_roots = workspace_roots;
|
||||
session.permission_profile = permission_profile;
|
||||
session.active_permission_profile = active_permission_profile;
|
||||
session.instruction_source_paths = Vec::new();
|
||||
@@ -146,6 +151,7 @@ mod tests {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: cwd.abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
message_history: None,
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
use crate::bottom_pane::FeedbackAudience;
|
||||
use crate::legacy_core::config::Config;
|
||||
use crate::permission_compat::legacy_compatible_permission_profile;
|
||||
use crate::session_state::MessageHistoryMetadata;
|
||||
use crate::session_state::ThreadSessionState;
|
||||
use crate::status::StatusAccountDisplay;
|
||||
@@ -34,8 +33,6 @@ use codex_app_server_protocol::MemoryResetResponse;
|
||||
use codex_app_server_protocol::Model as ApiModel;
|
||||
use codex_app_server_protocol::ModelListParams;
|
||||
use codex_app_server_protocol::ModelListResponse;
|
||||
use codex_app_server_protocol::PermissionProfileModificationParams;
|
||||
use codex_app_server_protocol::PermissionProfileSelectionParams;
|
||||
use codex_app_server_protocol::RateLimitSnapshot;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ReviewDelivery;
|
||||
@@ -108,7 +105,6 @@ use codex_otel::TelemetryAuthMode;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::approvals::GuardianAssessmentEvent;
|
||||
use codex_protocol::models::ActivePermissionProfile;
|
||||
use codex_protocol::models::ActivePermissionProfileModification;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::openai_models::ModelAvailabilityNux;
|
||||
@@ -116,6 +112,7 @@ use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::openai_models::ModelServiceTier;
|
||||
use codex_protocol::openai_models::ModelUpgrade;
|
||||
use codex_protocol::openai_models::ReasoningEffortPreset;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use color_eyre::eyre::ContextCompat;
|
||||
use color_eyre::eyre::Result;
|
||||
@@ -552,6 +549,7 @@ impl AppServerSession {
|
||||
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer,
|
||||
permission_profile: PermissionProfile,
|
||||
active_permission_profile: Option<ActivePermissionProfile>,
|
||||
workspace_roots: Vec<AbsolutePathBuf>,
|
||||
model: String,
|
||||
effort: Option<codex_protocol::openai_models::ReasoningEffort>,
|
||||
summary: Option<codex_protocol::config_types::ReasoningSummary>,
|
||||
@@ -561,12 +559,15 @@ impl AppServerSession {
|
||||
output_schema: Option<serde_json::Value>,
|
||||
) -> Result<TurnStartResponse> {
|
||||
let request_id = self.next_request_id();
|
||||
let (sandbox_policy, permissions) = turn_permissions_overrides(
|
||||
let thread_params_mode = self.thread_params_mode();
|
||||
let permissions = turn_permissions_selection(active_permission_profile, thread_params_mode);
|
||||
let sandbox_policy = turn_sandbox_policy_selection(
|
||||
&permission_profile,
|
||||
active_permission_profile,
|
||||
cwd.as_path(),
|
||||
self.thread_params_mode(),
|
||||
thread_params_mode,
|
||||
permissions.as_deref(),
|
||||
);
|
||||
let workspace_roots = turn_workspace_roots_selection(workspace_roots, thread_params_mode);
|
||||
self.client
|
||||
.request_typed(ClientRequest::TurnStart {
|
||||
request_id,
|
||||
@@ -576,6 +577,7 @@ impl AppServerSession {
|
||||
responsesapi_client_metadata: None,
|
||||
environments: None,
|
||||
cwd: Some(cwd),
|
||||
workspace_roots,
|
||||
approval_policy: Some(approval_policy),
|
||||
approvals_reviewer: Some(approvals_reviewer.into()),
|
||||
sandbox_policy,
|
||||
@@ -1172,56 +1174,73 @@ fn sandbox_mode_from_permission_profile(
|
||||
}
|
||||
}
|
||||
|
||||
fn permissions_selection_from_active_profile(
|
||||
active: ActivePermissionProfile,
|
||||
) -> PermissionProfileSelectionParams {
|
||||
let modifications = active
|
||||
.modifications
|
||||
.into_iter()
|
||||
.map(|modification| match modification {
|
||||
ActivePermissionProfileModification::AdditionalWritableRoot { path } => {
|
||||
PermissionProfileModificationParams::AdditionalWritableRoot { path }
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
PermissionProfileSelectionParams::Profile {
|
||||
id: active.id,
|
||||
modifications: (!modifications.is_empty()).then_some(modifications),
|
||||
fn permissions_selection_from_active_profile(active: ActivePermissionProfile) -> String {
|
||||
active.id
|
||||
}
|
||||
|
||||
fn turn_permissions_selection(
|
||||
active_permission_profile: Option<ActivePermissionProfile>,
|
||||
thread_params_mode: ThreadParamsMode,
|
||||
) -> Option<String> {
|
||||
if matches!(thread_params_mode, ThreadParamsMode::Remote) {
|
||||
return None;
|
||||
}
|
||||
|
||||
active_permission_profile.map(permissions_selection_from_active_profile)
|
||||
}
|
||||
|
||||
fn turn_sandbox_policy_selection(
|
||||
permission_profile: &PermissionProfile,
|
||||
cwd: &std::path::Path,
|
||||
thread_params_mode: ThreadParamsMode,
|
||||
permissions: Option<&str>,
|
||||
) -> Option<codex_app_server_protocol::SandboxPolicy> {
|
||||
(matches!(thread_params_mode, ThreadParamsMode::Remote) || permissions.is_none())
|
||||
.then(|| sandbox_policy_from_permission_profile(permission_profile, cwd))
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn sandbox_policy_from_permission_profile(
|
||||
permission_profile: &PermissionProfile,
|
||||
cwd: &std::path::Path,
|
||||
) -> Option<codex_app_server_protocol::SandboxPolicy> {
|
||||
if let Ok(policy) = permission_profile.to_legacy_sandbox_policy(cwd) {
|
||||
return Some(policy.into());
|
||||
}
|
||||
|
||||
let network_access = permission_profile.network_sandbox_policy().is_enabled();
|
||||
match sandbox_mode_from_permission_profile(permission_profile, cwd)? {
|
||||
codex_app_server_protocol::SandboxMode::DangerFullAccess => {
|
||||
Some(codex_app_server_protocol::SandboxPolicy::DangerFullAccess)
|
||||
}
|
||||
codex_app_server_protocol::SandboxMode::ReadOnly => {
|
||||
Some(codex_app_server_protocol::SandboxPolicy::ReadOnly { network_access })
|
||||
}
|
||||
codex_app_server_protocol::SandboxMode::WorkspaceWrite => {
|
||||
Some(codex_app_server_protocol::SandboxPolicy::WorkspaceWrite {
|
||||
network_access,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
legacy_writable_roots: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn turn_permissions_overrides(
|
||||
permission_profile: &PermissionProfile,
|
||||
active_permission_profile: Option<ActivePermissionProfile>,
|
||||
cwd: &std::path::Path,
|
||||
fn turn_workspace_roots_selection(
|
||||
workspace_roots: Vec<AbsolutePathBuf>,
|
||||
thread_params_mode: ThreadParamsMode,
|
||||
) -> (
|
||||
Option<codex_app_server_protocol::SandboxPolicy>,
|
||||
Option<PermissionProfileSelectionParams>,
|
||||
) {
|
||||
let permissions = if matches!(thread_params_mode, ThreadParamsMode::Embedded) {
|
||||
active_permission_profile.map(permissions_selection_from_active_profile)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let sandbox_policy = (matches!(thread_params_mode, ThreadParamsMode::Remote)
|
||||
|| permissions.is_none())
|
||||
.then(|| {
|
||||
let legacy_profile = legacy_compatible_permission_profile(permission_profile, cwd);
|
||||
let policy = legacy_profile
|
||||
.to_legacy_sandbox_policy(cwd)
|
||||
.unwrap_or_else(|err| {
|
||||
unreachable!("legacy-compatible permissions must project to legacy policy: {err}")
|
||||
});
|
||||
policy.into()
|
||||
});
|
||||
(sandbox_policy, permissions)
|
||||
) -> Option<Vec<AbsolutePathBuf>> {
|
||||
match thread_params_mode {
|
||||
ThreadParamsMode::Embedded => Some(workspace_roots),
|
||||
ThreadParamsMode::Remote => Some(workspace_roots),
|
||||
}
|
||||
}
|
||||
|
||||
fn permissions_selection_from_config(
|
||||
config: &Config,
|
||||
thread_params_mode: ThreadParamsMode,
|
||||
) -> Option<PermissionProfileSelectionParams> {
|
||||
) -> Option<String> {
|
||||
if matches!(thread_params_mode, ThreadParamsMode::Remote) {
|
||||
return None;
|
||||
}
|
||||
@@ -1243,7 +1262,7 @@ fn thread_start_params_from_config(
|
||||
.is_none()
|
||||
.then(|| {
|
||||
sandbox_mode_from_permission_profile(
|
||||
&config.permissions.permission_profile(),
|
||||
config.permissions.permission_profile_ref(),
|
||||
config.cwd.as_path(),
|
||||
)
|
||||
})
|
||||
@@ -1253,6 +1272,7 @@ fn thread_start_params_from_config(
|
||||
model_provider: thread_params_mode.model_provider_from_config(config),
|
||||
service_tier: service_tier_override_from_config(config),
|
||||
cwd: thread_cwd_from_config(config, thread_params_mode, remote_cwd_override),
|
||||
workspace_roots: thread_start_workspace_roots_from_config(config, thread_params_mode),
|
||||
approval_policy: Some(config.permissions.approval_policy.value().into()),
|
||||
approvals_reviewer: approvals_reviewer_override_from_config(config),
|
||||
sandbox,
|
||||
@@ -1273,24 +1293,16 @@ fn thread_resume_params_from_config(
|
||||
remote_cwd_override: Option<&std::path::Path>,
|
||||
) -> ThreadResumeParams {
|
||||
let permissions = permissions_selection_from_config(&config, thread_params_mode);
|
||||
let sandbox = permissions
|
||||
.is_none()
|
||||
.then(|| {
|
||||
sandbox_mode_from_permission_profile(
|
||||
&config.permissions.permission_profile(),
|
||||
config.cwd.as_path(),
|
||||
)
|
||||
})
|
||||
.flatten();
|
||||
ThreadResumeParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
model: config.model.clone(),
|
||||
model_provider: thread_params_mode.model_provider_from_config(&config),
|
||||
service_tier: service_tier_override_from_config(&config),
|
||||
cwd: thread_cwd_from_config(&config, thread_params_mode, remote_cwd_override),
|
||||
workspace_roots: None,
|
||||
approval_policy: Some(config.permissions.approval_policy.value().into()),
|
||||
approvals_reviewer: approvals_reviewer_override_from_config(&config),
|
||||
sandbox,
|
||||
sandbox: None,
|
||||
permissions,
|
||||
config: config_request_overrides_from_config(&config),
|
||||
persist_extended_history: false,
|
||||
@@ -1305,15 +1317,6 @@ fn thread_fork_params_from_config(
|
||||
remote_cwd_override: Option<&std::path::Path>,
|
||||
) -> ThreadForkParams {
|
||||
let permissions = permissions_selection_from_config(&config, thread_params_mode);
|
||||
let sandbox = permissions
|
||||
.is_none()
|
||||
.then(|| {
|
||||
sandbox_mode_from_permission_profile(
|
||||
&config.permissions.permission_profile(),
|
||||
config.cwd.as_path(),
|
||||
)
|
||||
})
|
||||
.flatten();
|
||||
ThreadForkParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
model: config.model.clone(),
|
||||
@@ -1322,7 +1325,7 @@ fn thread_fork_params_from_config(
|
||||
cwd: thread_cwd_from_config(&config, thread_params_mode, remote_cwd_override),
|
||||
approval_policy: Some(config.permissions.approval_policy.value().into()),
|
||||
approvals_reviewer: approvals_reviewer_override_from_config(&config),
|
||||
sandbox,
|
||||
sandbox: None,
|
||||
permissions,
|
||||
config: config_request_overrides_from_config(&config),
|
||||
base_instructions: config.base_instructions.clone(),
|
||||
@@ -1334,6 +1337,16 @@ fn thread_fork_params_from_config(
|
||||
}
|
||||
}
|
||||
|
||||
fn thread_start_workspace_roots_from_config(
|
||||
config: &Config,
|
||||
thread_params_mode: ThreadParamsMode,
|
||||
) -> Option<Vec<AbsolutePathBuf>> {
|
||||
match thread_params_mode {
|
||||
ThreadParamsMode::Embedded => Some(config.workspace_roots.clone()),
|
||||
ThreadParamsMode::Remote => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn thread_cwd_from_config(
|
||||
config: &Config,
|
||||
thread_params_mode: ThreadParamsMode,
|
||||
@@ -1399,11 +1412,15 @@ async fn thread_session_state_from_thread_start_response(
|
||||
) -> Result<ThreadSessionState, String> {
|
||||
let permission_profile = permission_profile_from_thread_response(
|
||||
&response.sandbox,
|
||||
response.permission_profile.as_ref(),
|
||||
response.cwd.as_path(),
|
||||
config,
|
||||
thread_params_mode,
|
||||
);
|
||||
let workspace_roots = workspace_roots_from_thread_response(
|
||||
&response.sandbox,
|
||||
&response.cwd,
|
||||
&response.workspace_roots,
|
||||
);
|
||||
thread_session_state_from_thread_response(
|
||||
&response.thread.id,
|
||||
response.thread.forked_from_id.clone(),
|
||||
@@ -1417,6 +1434,7 @@ async fn thread_session_state_from_thread_start_response(
|
||||
permission_profile,
|
||||
response.active_permission_profile.clone().map(Into::into),
|
||||
response.cwd.clone(),
|
||||
workspace_roots,
|
||||
response.instruction_sources.clone(),
|
||||
response.reasoning_effort,
|
||||
config,
|
||||
@@ -1431,11 +1449,15 @@ async fn thread_session_state_from_thread_resume_response(
|
||||
) -> Result<ThreadSessionState, String> {
|
||||
let permission_profile = permission_profile_from_thread_response(
|
||||
&response.sandbox,
|
||||
response.permission_profile.as_ref(),
|
||||
response.cwd.as_path(),
|
||||
config,
|
||||
thread_params_mode,
|
||||
);
|
||||
let workspace_roots = workspace_roots_from_thread_response(
|
||||
&response.sandbox,
|
||||
&response.cwd,
|
||||
&response.workspace_roots,
|
||||
);
|
||||
thread_session_state_from_thread_response(
|
||||
&response.thread.id,
|
||||
response.thread.forked_from_id.clone(),
|
||||
@@ -1449,6 +1471,7 @@ async fn thread_session_state_from_thread_resume_response(
|
||||
permission_profile,
|
||||
response.active_permission_profile.clone().map(Into::into),
|
||||
response.cwd.clone(),
|
||||
workspace_roots,
|
||||
response.instruction_sources.clone(),
|
||||
response.reasoning_effort,
|
||||
config,
|
||||
@@ -1463,11 +1486,15 @@ async fn thread_session_state_from_thread_fork_response(
|
||||
) -> Result<ThreadSessionState, String> {
|
||||
let permission_profile = permission_profile_from_thread_response(
|
||||
&response.sandbox,
|
||||
response.permission_profile.as_ref(),
|
||||
response.cwd.as_path(),
|
||||
config,
|
||||
thread_params_mode,
|
||||
);
|
||||
let workspace_roots = workspace_roots_from_thread_response(
|
||||
&response.sandbox,
|
||||
&response.cwd,
|
||||
&response.workspace_roots,
|
||||
);
|
||||
thread_session_state_from_thread_response(
|
||||
&response.thread.id,
|
||||
response.thread.forked_from_id.clone(),
|
||||
@@ -1481,6 +1508,7 @@ async fn thread_session_state_from_thread_fork_response(
|
||||
permission_profile,
|
||||
response.active_permission_profile.clone().map(Into::into),
|
||||
response.cwd.clone(),
|
||||
workspace_roots,
|
||||
response.instruction_sources.clone(),
|
||||
response.reasoning_effort,
|
||||
config,
|
||||
@@ -1490,22 +1518,74 @@ async fn thread_session_state_from_thread_fork_response(
|
||||
|
||||
fn permission_profile_from_thread_response(
|
||||
sandbox: &codex_app_server_protocol::SandboxPolicy,
|
||||
permission_profile: Option<&codex_app_server_protocol::PermissionProfile>,
|
||||
cwd: &std::path::Path,
|
||||
config: &Config,
|
||||
thread_params_mode: ThreadParamsMode,
|
||||
) -> PermissionProfile {
|
||||
if let Some(permission_profile) = permission_profile {
|
||||
return permission_profile.clone().into();
|
||||
}
|
||||
match thread_params_mode {
|
||||
ThreadParamsMode::Embedded => config.permissions.permission_profile(),
|
||||
ThreadParamsMode::Remote => {
|
||||
ThreadParamsMode::Remote => permission_profile_from_response_sandbox(sandbox, cwd),
|
||||
}
|
||||
}
|
||||
|
||||
fn permission_profile_from_response_sandbox(
|
||||
sandbox: &codex_app_server_protocol::SandboxPolicy,
|
||||
cwd: &std::path::Path,
|
||||
) -> PermissionProfile {
|
||||
match sandbox {
|
||||
codex_app_server_protocol::SandboxPolicy::WorkspaceWrite {
|
||||
network_access,
|
||||
exclude_tmpdir_env_var,
|
||||
exclude_slash_tmp,
|
||||
legacy_writable_roots: _,
|
||||
} => PermissionProfile::workspace_write_with(
|
||||
&[],
|
||||
if *network_access {
|
||||
NetworkSandboxPolicy::Enabled
|
||||
} else {
|
||||
NetworkSandboxPolicy::Restricted
|
||||
},
|
||||
*exclude_tmpdir_env_var,
|
||||
*exclude_slash_tmp,
|
||||
),
|
||||
codex_app_server_protocol::SandboxPolicy::DangerFullAccess
|
||||
| codex_app_server_protocol::SandboxPolicy::ReadOnly { .. }
|
||||
| codex_app_server_protocol::SandboxPolicy::ExternalSandbox { .. } => {
|
||||
PermissionProfile::from_legacy_sandbox_policy_for_cwd(&sandbox.to_core(), cwd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn workspace_roots_from_thread_response(
|
||||
sandbox: &codex_app_server_protocol::SandboxPolicy,
|
||||
cwd: &AbsolutePathBuf,
|
||||
workspace_roots: &[AbsolutePathBuf],
|
||||
) -> Vec<AbsolutePathBuf> {
|
||||
if !workspace_roots.is_empty() || sandbox.legacy_writable_roots().is_empty() {
|
||||
return workspace_roots.to_vec();
|
||||
}
|
||||
|
||||
match sandbox {
|
||||
codex_app_server_protocol::SandboxPolicy::WorkspaceWrite { .. } => {
|
||||
let mut roots = Vec::with_capacity(1 + sandbox.legacy_writable_roots().len());
|
||||
push_unique_workspace_root(&mut roots, cwd.clone());
|
||||
for root in sandbox.legacy_writable_roots() {
|
||||
push_unique_workspace_root(&mut roots, root.clone());
|
||||
}
|
||||
roots
|
||||
}
|
||||
codex_app_server_protocol::SandboxPolicy::DangerFullAccess
|
||||
| codex_app_server_protocol::SandboxPolicy::ReadOnly { .. }
|
||||
| codex_app_server_protocol::SandboxPolicy::ExternalSandbox { .. } => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn push_unique_workspace_root(roots: &mut Vec<AbsolutePathBuf>, root: AbsolutePathBuf) {
|
||||
if !roots.iter().any(|existing| existing == &root) {
|
||||
roots.push(root);
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "session mapping keeps explicit fields"
|
||||
@@ -1523,6 +1603,7 @@ async fn thread_session_state_from_thread_response(
|
||||
permission_profile: PermissionProfile,
|
||||
active_permission_profile: Option<ActivePermissionProfile>,
|
||||
cwd: AbsolutePathBuf,
|
||||
workspace_roots: Vec<AbsolutePathBuf>,
|
||||
instruction_source_paths: Vec<AbsolutePathBuf>,
|
||||
reasoning_effort: Option<codex_protocol::openai_models::ReasoningEffort>,
|
||||
config: &Config,
|
||||
@@ -1550,6 +1631,7 @@ async fn thread_session_state_from_thread_response(
|
||||
permission_profile,
|
||||
active_permission_profile,
|
||||
cwd,
|
||||
workspace_roots,
|
||||
instruction_source_paths,
|
||||
reasoning_effort,
|
||||
message_history: Some(MessageHistoryMetadata {
|
||||
@@ -1627,6 +1709,7 @@ mod tests {
|
||||
);
|
||||
|
||||
assert_eq!(params.cwd, Some(config.cwd.to_string_lossy().to_string()));
|
||||
assert_eq!(params.workspace_roots, Some(config.workspace_roots.clone()));
|
||||
assert_eq!(params.sandbox, None);
|
||||
assert_eq!(
|
||||
params.permissions,
|
||||
@@ -1656,60 +1739,88 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn embedded_turn_permissions_use_active_profile_selection() {
|
||||
let cwd = test_path_buf("/workspace/project").abs();
|
||||
let active_permission_profile = ActivePermissionProfile::new(":workspace");
|
||||
let expected_permissions =
|
||||
permissions_selection_from_active_profile(active_permission_profile.clone());
|
||||
|
||||
let (sandbox_policy, permissions) = turn_permissions_overrides(
|
||||
&PermissionProfile::workspace_write(),
|
||||
Some(active_permission_profile),
|
||||
cwd.as_path(),
|
||||
ThreadParamsMode::Embedded,
|
||||
);
|
||||
let permissions =
|
||||
turn_permissions_selection(Some(active_permission_profile), ThreadParamsMode::Embedded);
|
||||
|
||||
assert_eq!(sandbox_policy, None);
|
||||
assert_eq!(permissions, Some(expected_permissions));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_turn_permissions_fall_back_to_sandbox_without_active_profile() {
|
||||
let cwd = test_path_buf("/workspace/project").abs();
|
||||
fn embedded_turn_workspace_roots_forward_current_roots() {
|
||||
let workspace_root = test_path_buf("/workspace/project").abs();
|
||||
let workspace_roots = vec![workspace_root];
|
||||
|
||||
let (sandbox_policy, permissions) = turn_permissions_overrides(
|
||||
&PermissionProfile::read_only(),
|
||||
let roots =
|
||||
turn_workspace_roots_selection(workspace_roots.clone(), ThreadParamsMode::Embedded);
|
||||
|
||||
assert_eq!(roots, Some(workspace_roots));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_turn_workspace_roots_forward_current_roots() {
|
||||
let workspace_roots = vec![test_path_buf("/workspace/project").abs()];
|
||||
|
||||
let roots =
|
||||
turn_workspace_roots_selection(workspace_roots.clone(), ThreadParamsMode::Remote);
|
||||
|
||||
assert_eq!(roots, Some(workspace_roots));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_turn_permissions_omit_overrides_without_active_profile() {
|
||||
let permissions = turn_permissions_selection(
|
||||
/*active_permission_profile*/ None,
|
||||
cwd.as_path(),
|
||||
ThreadParamsMode::Embedded,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
sandbox_policy,
|
||||
Some(codex_app_server_protocol::SandboxPolicy::ReadOnly {
|
||||
network_access: false
|
||||
})
|
||||
);
|
||||
assert_eq!(permissions, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_turn_permissions_use_sandbox_even_with_active_profile() {
|
||||
let cwd = test_path_buf("/workspace/project").abs();
|
||||
|
||||
let (sandbox_policy, permissions) = turn_permissions_overrides(
|
||||
&PermissionProfile::read_only(),
|
||||
fn remote_turn_permissions_omit_overrides_even_with_active_profile() {
|
||||
let permissions = turn_permissions_selection(
|
||||
Some(ActivePermissionProfile::new(":read-only")),
|
||||
cwd.as_path(),
|
||||
ThreadParamsMode::Remote,
|
||||
);
|
||||
|
||||
assert_eq!(permissions, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_turn_sandbox_projects_current_permission_profile() {
|
||||
let cwd = test_path_buf("/workspace/project").abs();
|
||||
|
||||
let sandbox = turn_sandbox_policy_selection(
|
||||
&PermissionProfile::read_only(),
|
||||
cwd.as_path(),
|
||||
ThreadParamsMode::Remote,
|
||||
/*permissions*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
sandbox_policy,
|
||||
sandbox,
|
||||
Some(codex_app_server_protocol::SandboxPolicy::ReadOnly {
|
||||
network_access: false
|
||||
})
|
||||
);
|
||||
assert_eq!(permissions, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_turn_sandbox_omits_when_named_permissions_are_sent() {
|
||||
let cwd = test_path_buf("/workspace/project").abs();
|
||||
|
||||
let sandbox = turn_sandbox_policy_selection(
|
||||
&PermissionProfile::read_only(),
|
||||
cwd.as_path(),
|
||||
ThreadParamsMode::Embedded,
|
||||
Some(":read-only"),
|
||||
);
|
||||
|
||||
assert_eq!(sandbox, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1718,7 +1829,7 @@ mod tests {
|
||||
let config = build_config(&temp_dir).await;
|
||||
let thread_id = ThreadId::new();
|
||||
let expected_sandbox = sandbox_mode_from_permission_profile(
|
||||
&config.permissions.permission_profile(),
|
||||
config.permissions.permission_profile_ref(),
|
||||
config.cwd.as_path(),
|
||||
);
|
||||
|
||||
@@ -1744,12 +1855,15 @@ mod tests {
|
||||
assert_eq!(start.cwd, None);
|
||||
assert_eq!(resume.cwd, None);
|
||||
assert_eq!(fork.cwd, None);
|
||||
assert_eq!(start.workspace_roots, None);
|
||||
assert_eq!(resume.workspace_roots, None);
|
||||
assert_eq!(fork.workspace_roots, None);
|
||||
assert_eq!(start.model_provider, None);
|
||||
assert_eq!(resume.model_provider, None);
|
||||
assert_eq!(fork.model_provider, None);
|
||||
assert_eq!(start.sandbox, expected_sandbox);
|
||||
assert_eq!(resume.sandbox, expected_sandbox);
|
||||
assert_eq!(fork.sandbox, expected_sandbox);
|
||||
assert_eq!(resume.sandbox, None);
|
||||
assert_eq!(fork.sandbox, None);
|
||||
assert_eq!(start.permissions, None);
|
||||
assert_eq!(resume.permissions, None);
|
||||
assert_eq!(fork.permissions, None);
|
||||
@@ -1825,7 +1939,7 @@ mod tests {
|
||||
let thread_id = ThreadId::new();
|
||||
let remote_cwd = PathBuf::from("repo/on/server");
|
||||
let expected_sandbox = sandbox_mode_from_permission_profile(
|
||||
&config.permissions.permission_profile(),
|
||||
config.permissions.permission_profile_ref(),
|
||||
config.cwd.as_path(),
|
||||
);
|
||||
|
||||
@@ -1851,12 +1965,15 @@ mod tests {
|
||||
assert_eq!(start.cwd.as_deref(), Some("repo/on/server"));
|
||||
assert_eq!(resume.cwd.as_deref(), Some("repo/on/server"));
|
||||
assert_eq!(fork.cwd.as_deref(), Some("repo/on/server"));
|
||||
assert_eq!(start.workspace_roots, None);
|
||||
assert_eq!(resume.workspace_roots, None);
|
||||
assert_eq!(fork.workspace_roots, None);
|
||||
assert_eq!(start.model_provider, None);
|
||||
assert_eq!(resume.model_provider, None);
|
||||
assert_eq!(fork.model_provider, None);
|
||||
assert_eq!(start.sandbox, expected_sandbox);
|
||||
assert_eq!(resume.sandbox, expected_sandbox);
|
||||
assert_eq!(fork.sandbox, expected_sandbox);
|
||||
assert_eq!(resume.sandbox, None);
|
||||
assert_eq!(fork.sandbox, None);
|
||||
assert_eq!(start.permissions, None);
|
||||
assert_eq!(resume.permissions, None);
|
||||
assert_eq!(fork.permissions, None);
|
||||
@@ -2014,6 +2131,7 @@ mod tests {
|
||||
model_provider: "openai".to_string(),
|
||||
service_tier: None,
|
||||
cwd: test_path_buf("/tmp/project").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_sources: vec![test_path_buf("/tmp/project/AGENTS.md").abs()],
|
||||
approval_policy: codex_app_server_protocol::AskForApproval::Never,
|
||||
approvals_reviewer: codex_app_server_protocol::ApprovalsReviewer::User,
|
||||
@@ -2021,7 +2139,6 @@ mod tests {
|
||||
.to_legacy_sandbox_policy(test_path_buf("/tmp/project").as_path())
|
||||
.expect("read-only profile must be legacy-compatible")
|
||||
.into(),
|
||||
permission_profile: Some(read_only_profile.clone().into()),
|
||||
active_permission_profile: None,
|
||||
reasoning_effort: None,
|
||||
};
|
||||
@@ -2044,7 +2161,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_thread_response_prefers_permission_profile_over_legacy_sandbox() {
|
||||
async fn remote_thread_response_uses_legacy_sandbox_projection() {
|
||||
let temp_dir = tempfile::tempdir().expect("tempdir");
|
||||
let config = build_config(&temp_dir).await;
|
||||
let cwd = test_path_buf("/tmp/project").abs();
|
||||
@@ -2052,58 +2169,101 @@ mod tests {
|
||||
.to_legacy_sandbox_policy(cwd.as_path())
|
||||
.expect("read-only profile must be legacy-compatible")
|
||||
.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!(
|
||||
permission_profile_from_thread_response(
|
||||
&fallback_sandbox,
|
||||
Some(&response_profile),
|
||||
cwd.as_path(),
|
||||
&config,
|
||||
ThreadParamsMode::Remote,
|
||||
),
|
||||
split_profile
|
||||
PermissionProfile::read_only()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn embedded_thread_response_prefers_permission_profile_when_present() {
|
||||
async fn remote_thread_response_keeps_workspace_roots_out_of_permission_profile() {
|
||||
let temp_dir = tempfile::tempdir().expect("tempdir");
|
||||
let config = build_config(&temp_dir).await;
|
||||
let cwd = test_path_buf("/tmp/project").abs();
|
||||
let response_profile = PermissionProfile::read_only().into();
|
||||
let extra_root = test_path_buf("/workspace/cache").abs();
|
||||
let expected = PermissionProfile::workspace_write_with(
|
||||
&[],
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
/*exclude_tmpdir_env_var*/ false,
|
||||
/*exclude_slash_tmp*/ false,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
permission_profile_from_thread_response(
|
||||
&codex_app_server_protocol::SandboxPolicy::WorkspaceWrite {
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
legacy_writable_roots: Vec::new(),
|
||||
},
|
||||
cwd.as_path(),
|
||||
&config,
|
||||
ThreadParamsMode::Remote,
|
||||
),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
workspace_roots_from_thread_response(
|
||||
&codex_app_server_protocol::SandboxPolicy::WorkspaceWrite {
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
legacy_writable_roots: Vec::new(),
|
||||
},
|
||||
&cwd,
|
||||
std::slice::from_ref(&extra_root),
|
||||
),
|
||||
vec![extra_root.clone()]
|
||||
);
|
||||
assert_eq!(
|
||||
workspace_roots_from_thread_response(
|
||||
&codex_app_server_protocol::SandboxPolicy::WorkspaceWrite {
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
legacy_writable_roots: Vec::new(),
|
||||
},
|
||||
&cwd,
|
||||
&[],
|
||||
),
|
||||
Vec::<AbsolutePathBuf>::new()
|
||||
);
|
||||
assert_eq!(
|
||||
workspace_roots_from_thread_response(
|
||||
&codex_app_server_protocol::SandboxPolicy::WorkspaceWrite {
|
||||
network_access: false,
|
||||
exclude_tmpdir_env_var: false,
|
||||
exclude_slash_tmp: false,
|
||||
legacy_writable_roots: vec![extra_root.clone()],
|
||||
},
|
||||
&cwd,
|
||||
&[],
|
||||
),
|
||||
vec![cwd, extra_root]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn embedded_thread_response_uses_local_config_permission_profile() {
|
||||
let temp_dir = tempfile::tempdir().expect("tempdir");
|
||||
let config = build_config(&temp_dir).await;
|
||||
let cwd = test_path_buf("/tmp/project").abs();
|
||||
let expected = config.permissions.permission_profile();
|
||||
|
||||
assert_eq!(
|
||||
permission_profile_from_thread_response(
|
||||
&codex_app_server_protocol::SandboxPolicy::DangerFullAccess,
|
||||
Some(&response_profile),
|
||||
cwd.as_path(),
|
||||
&config,
|
||||
ThreadParamsMode::Embedded,
|
||||
),
|
||||
PermissionProfile::read_only()
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2137,6 +2297,7 @@ mod tests {
|
||||
/*active_permission_profile*/ None,
|
||||
test_path_buf("/tmp/project").abs(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
/*reasoning_effort*/ None,
|
||||
&config,
|
||||
)
|
||||
@@ -2171,6 +2332,7 @@ mod tests {
|
||||
/*active_permission_profile*/ None,
|
||||
test_path_buf("/tmp/project").abs(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
/*reasoning_effort*/ None,
|
||||
&config,
|
||||
)
|
||||
|
||||
@@ -1246,6 +1246,7 @@ impl ChatWidget {
|
||||
self.current_rollout_path = session.rollout_path.clone();
|
||||
self.current_cwd = Some(session.cwd.to_path_buf());
|
||||
self.config.cwd = session.cwd.clone();
|
||||
self.config.workspace_roots = session.workspace_roots.clone();
|
||||
self.effective_service_tier = session.service_tier.clone();
|
||||
if let Err(err) = self
|
||||
.config
|
||||
@@ -1266,10 +1267,12 @@ impl ChatWidget {
|
||||
);
|
||||
if let Err(err) = permission_sync {
|
||||
tracing::warn!(%err, "failed to sync permissions from SessionConfigured");
|
||||
self.config.permissions.permission_profile =
|
||||
Constrained::allow_only(session.permission_profile.clone());
|
||||
self.config.permissions.active_permission_profile =
|
||||
session.active_permission_profile.clone();
|
||||
self.config
|
||||
.permissions
|
||||
.replace_permission_profile_constraint_with_active_profile(
|
||||
Constrained::allow_only(session.permission_profile.clone()),
|
||||
session.active_permission_profile.clone(),
|
||||
);
|
||||
}
|
||||
self.config.approvals_reviewer = session.approvals_reviewer;
|
||||
self.status_line_project_root_name_cache = None;
|
||||
|
||||
@@ -12,7 +12,7 @@ use codex_app_server_protocol::AskForApproval;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_utils_sandbox_summary::summarize_permission_profile;
|
||||
use codex_utils_sandbox_summary::summarize_config_permission_profile;
|
||||
|
||||
use super::status_state::TerminalTitleStatusKind;
|
||||
|
||||
@@ -902,7 +902,7 @@ fn permissions_display(config: &Config) -> String {
|
||||
}
|
||||
|
||||
let permission_profile = config.permissions.permission_profile();
|
||||
let summary = summarize_permission_profile(&permission_profile, config.cwd.as_path());
|
||||
let summary = summarize_config_permission_profile(config);
|
||||
if let Some(details) = summary.strip_prefix("read-only")
|
||||
&& !details.contains("(network access enabled)")
|
||||
{
|
||||
|
||||
@@ -28,6 +28,7 @@ async fn submission_preserves_text_elements_and_local_images() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -131,6 +132,7 @@ async fn submission_includes_configured_permission_profile() {
|
||||
permission_profile: expected_permission_profile.clone(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -180,6 +182,7 @@ async fn submission_keeps_profile_when_legacy_projection_is_external() {
|
||||
permission_profile: expected_permission_profile.clone(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -221,6 +224,7 @@ async fn submission_with_remote_and_local_images_keeps_local_placeholder_numberi
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -314,6 +318,7 @@ async fn enter_with_only_remote_images_submits_user_turn() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -377,6 +382,7 @@ async fn shift_enter_with_only_remote_images_does_not_submit_user_turn() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -415,6 +421,7 @@ async fn enter_with_only_remote_images_does_not_submit_when_modal_is_active() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -453,6 +460,7 @@ async fn enter_with_only_remote_images_does_not_submit_when_input_disabled() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -494,6 +502,7 @@ async fn submission_prefers_selected_duplicate_skill_path() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
|
||||
@@ -957,6 +957,7 @@ async fn bang_shell_enter_while_task_running_submits_run_user_shell_command() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
|
||||
@@ -29,6 +29,7 @@ async fn resumed_initial_messages_render_history() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -99,6 +100,7 @@ async fn replayed_user_message_preserves_text_elements_and_local_images() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -167,6 +169,7 @@ async fn replayed_user_message_preserves_remote_image_urls() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -266,6 +269,7 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() {
|
||||
permission_profile: expected_permission_profile,
|
||||
active_permission_profile: None,
|
||||
cwd: expected_cwd.clone(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -322,6 +326,7 @@ async fn session_configured_external_sandbox_keeps_external_runtime_policy() {
|
||||
permission_profile: expected_permission_profile,
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/external").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -360,6 +365,7 @@ async fn replayed_user_message_with_only_remote_images_renders_history_cell() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -414,6 +420,7 @@ async fn replayed_user_message_with_only_local_images_renders_history_cell() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -684,6 +691,7 @@ async fn replayed_reasoning_item_hides_raw_reasoning_when_disabled() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_project_path().abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
message_history: None,
|
||||
@@ -729,6 +737,7 @@ async fn replayed_reasoning_item_shows_raw_reasoning_when_enabled() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_project_path().abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
message_history: None,
|
||||
|
||||
@@ -584,6 +584,7 @@ async fn permissions_selection_marks_auto_review_current_after_session_configure
|
||||
permission_profile: PermissionProfile::workspace_write(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_project_path().abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
message_history: None,
|
||||
@@ -631,6 +632,7 @@ async fn permissions_selection_marks_auto_review_current_with_custom_workspace_w
|
||||
permission_profile,
|
||||
active_permission_profile: None,
|
||||
cwd,
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
message_history: None,
|
||||
|
||||
@@ -1217,6 +1217,7 @@ async fn submit_user_message_emits_structured_plugin_mentions_from_bindings() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -1460,6 +1461,7 @@ async fn plan_slash_command_with_args_submits_prompt_in_plan_mode() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
|
||||
@@ -2229,6 +2229,7 @@ async fn session_configured_clears_goal_status_footer() {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/home/user/project").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
@@ -2240,6 +2241,36 @@ async fn session_configured_clears_goal_status_footer() {
|
||||
assert!(chat.turn_lifecycle.budget_limited_turn_ids.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_configured_syncs_workspace_roots_into_config() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
|
||||
let cwd = test_path_buf("/home/user/project").abs();
|
||||
let extra_root = test_path_buf("/home/user/extra").abs();
|
||||
|
||||
chat.handle_thread_session(crate::session_state::ThreadSessionState {
|
||||
thread_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
thread_name: None,
|
||||
model: "gpt-5.4".to_string(),
|
||||
model_provider_id: "test-provider".to_string(),
|
||||
service_tier: None,
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
permission_profile: PermissionProfile::workspace_write(),
|
||||
active_permission_profile: None,
|
||||
cwd: cwd.clone(),
|
||||
workspace_roots: vec![cwd.clone(), extra_root.clone()],
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::default()),
|
||||
message_history: None,
|
||||
network_proxy: None,
|
||||
rollout_path: None,
|
||||
});
|
||||
|
||||
assert_eq!(chat.config_ref().workspace_roots, vec![cwd, extra_root]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_goal_update_for_other_thread_is_ignored() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
|
||||
|
||||
@@ -1574,7 +1574,7 @@ pub(crate) fn new_session_info(
|
||||
pub(crate) fn is_yolo_mode(config: &Config) -> bool {
|
||||
has_yolo_permissions(
|
||||
AskForApproval::from(config.permissions.approval_policy.value()),
|
||||
&config.permissions.permission_profile(),
|
||||
config.permissions.permission_profile_ref(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3959,6 +3959,7 @@ mod tests {
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
cwd: test_path_buf("/tmp/project").abs(),
|
||||
workspace_roots: Vec::new(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
message_history: None,
|
||||
|
||||
@@ -154,7 +154,6 @@ mod npm_registry;
|
||||
pub(crate) mod onboarding;
|
||||
mod oss_selection;
|
||||
mod pager_overlay;
|
||||
mod permission_compat;
|
||||
pub(crate) mod public_widgets;
|
||||
mod render;
|
||||
mod resize_reflow_cap;
|
||||
@@ -981,7 +980,7 @@ pub async fn run_main(
|
||||
|
||||
if let Some(warning) = add_dir_warning_message(
|
||||
&cli.add_dir,
|
||||
&config.permissions.permission_profile(),
|
||||
config.permissions.permission_profile_ref(),
|
||||
config.cwd.as_path(),
|
||||
) {
|
||||
#[allow(clippy::print_stderr)]
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
//! Compatibility projections from the canonical permission profile model into
|
||||
//! legacy shapes still required by older or remote app-server APIs.
|
||||
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use std::path::Path;
|
||||
|
||||
pub(crate) fn legacy_compatible_permission_profile(
|
||||
permission_profile: &PermissionProfile,
|
||||
cwd: &Path,
|
||||
) -> PermissionProfile {
|
||||
if permission_profile.to_legacy_sandbox_policy(cwd).is_ok() {
|
||||
return permission_profile.clone();
|
||||
}
|
||||
|
||||
let file_system_policy = permission_profile.file_system_sandbox_policy();
|
||||
let network_policy = permission_profile.network_sandbox_policy();
|
||||
let cwd_abs = AbsolutePathBuf::from_absolute_path(cwd).ok();
|
||||
let writable_roots = file_system_policy
|
||||
.get_writable_roots_with_cwd(cwd)
|
||||
.into_iter()
|
||||
.map(|root| root.root)
|
||||
.filter(|root| cwd_abs.as_ref() != Some(root))
|
||||
.collect::<Vec<_>>();
|
||||
let tmpdir_writable = std::env::var_os("TMPDIR")
|
||||
.filter(|tmpdir| !tmpdir.is_empty())
|
||||
.and_then(|tmpdir| {
|
||||
AbsolutePathBuf::from_absolute_path(std::path::PathBuf::from(tmpdir)).ok()
|
||||
})
|
||||
.is_some_and(|tmpdir| file_system_policy.can_write_path_with_cwd(tmpdir.as_path(), cwd));
|
||||
let slash_tmp = Path::new("/tmp");
|
||||
let slash_tmp_writable = slash_tmp.is_absolute()
|
||||
&& slash_tmp.is_dir()
|
||||
&& file_system_policy.can_write_path_with_cwd(slash_tmp, cwd);
|
||||
|
||||
PermissionProfile::workspace_write_with(
|
||||
&writable_roots,
|
||||
network_policy,
|
||||
!tmpdir_writable,
|
||||
!slash_tmp_writable,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_app_server_protocol::FileSystemAccessMode;
|
||||
use codex_app_server_protocol::FileSystemPath;
|
||||
use codex_app_server_protocol::FileSystemSandboxEntry;
|
||||
use codex_app_server_protocol::FileSystemSpecialPath;
|
||||
use codex_app_server_protocol::PermissionProfile as AppServerPermissionProfile;
|
||||
use codex_app_server_protocol::PermissionProfileFileSystemPermissions;
|
||||
use codex_app_server_protocol::PermissionProfileNetworkPermissions;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn compatibility_profile_preserves_unbridgeable_write_roots() {
|
||||
let cwd = AbsolutePathBuf::try_from("/workspace/project").expect("absolute cwd");
|
||||
let extra_root = AbsolutePathBuf::try_from("/workspace/extra").expect("absolute root");
|
||||
let permission_profile: PermissionProfile = AppServerPermissionProfile::Managed {
|
||||
network: PermissionProfileNetworkPermissions { enabled: false },
|
||||
file_system: PermissionProfileFileSystemPermissions::Restricted {
|
||||
entries: vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: extra_root.clone(),
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
],
|
||||
glob_scan_max_depth: None,
|
||||
},
|
||||
}
|
||||
.into();
|
||||
|
||||
let compatibility_profile =
|
||||
legacy_compatible_permission_profile(&permission_profile, cwd.as_path());
|
||||
let policy = compatibility_profile
|
||||
.to_legacy_sandbox_policy(cwd.as_path())
|
||||
.expect("compatibility profile should project to legacy policy");
|
||||
let roots = policy
|
||||
.get_writable_roots_with_cwd(cwd.as_path())
|
||||
.into_iter()
|
||||
.map(|root| root.root)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(roots, vec![extra_root, cwd]);
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,9 @@ pub(crate) struct ThreadSessionState {
|
||||
/// when the server knows it.
|
||||
pub(crate) active_permission_profile: Option<ActivePermissionProfile>,
|
||||
pub(crate) cwd: AbsolutePathBuf,
|
||||
/// Workspace roots used to materialize symbolic project-root grants in
|
||||
/// `permission_profile`.
|
||||
pub(crate) workspace_roots: Vec<AbsolutePathBuf>,
|
||||
pub(crate) instruction_source_paths: Vec<AbsolutePathBuf>,
|
||||
pub(crate) reasoning_effort: Option<codex_protocol::openai_models::ReasoningEffort>,
|
||||
pub(crate) message_history: Option<MessageHistoryMetadata>,
|
||||
|
||||
@@ -15,14 +15,12 @@ use codex_protocol::ThreadId;
|
||||
use codex_protocol::account::PlanType;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::models::ActivePermissionProfile;
|
||||
use codex_protocol::models::ActivePermissionProfileModification;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_utils_sandbox_summary::summarize_permission_profile;
|
||||
use codex_utils_sandbox_summary::summarize_config_permission_profile;
|
||||
use ratatui::prelude::*;
|
||||
use ratatui::style::Stylize;
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use url::Url;
|
||||
|
||||
@@ -254,6 +252,7 @@ impl StatusHistoryCell {
|
||||
) -> (Self, StatusHistoryHandle) {
|
||||
let approval_policy = AskForApproval::from(config.permissions.approval_policy.value());
|
||||
let permission_profile = config.permissions.permission_profile();
|
||||
let sandbox_summary = summarize_config_permission_profile(config);
|
||||
let mut config_entries = vec![
|
||||
("workdir", config.cwd.display().to_string()),
|
||||
("model", model_name.to_string()),
|
||||
@@ -262,10 +261,7 @@ impl StatusHistoryCell {
|
||||
"approval",
|
||||
config.permissions.approval_policy.value().to_string(),
|
||||
),
|
||||
(
|
||||
"sandbox",
|
||||
summarize_permission_profile(&permission_profile, config.cwd.as_path()),
|
||||
),
|
||||
("sandbox", sandbox_summary.clone()),
|
||||
];
|
||||
if config.model_provider.wire_api == WireApi::Responses {
|
||||
let effort_value = reasoning_effort_override
|
||||
@@ -288,7 +284,7 @@ impl StatusHistoryCell {
|
||||
.map(|(_, v)| v.clone())
|
||||
.unwrap_or_else(|| "<unknown>".to_string());
|
||||
let active_permission_profile = config.permissions.active_permission_profile();
|
||||
let sandbox = status_permission_summary(&permission_profile, config.cwd.as_path());
|
||||
let sandbox = status_permission_summary(&sandbox_summary);
|
||||
let approval = status_approval_label(approval_policy, config.approvals_reviewer, &approval);
|
||||
let permissions = status_permissions_label(
|
||||
active_permission_profile.as_ref(),
|
||||
@@ -539,8 +535,7 @@ impl StatusHistoryCell {
|
||||
}
|
||||
}
|
||||
|
||||
fn status_permission_summary(permission_profile: &PermissionProfile, cwd: &Path) -> String {
|
||||
let summary = summarize_permission_profile(permission_profile, cwd);
|
||||
fn status_permission_summary(summary: &str) -> String {
|
||||
if let Some(details) = summary.strip_prefix("read-only") {
|
||||
if details.contains("(network access enabled)") {
|
||||
return "read-only with network access".to_string();
|
||||
@@ -548,15 +543,20 @@ fn status_permission_summary(permission_profile: &PermissionProfile, cwd: &Path)
|
||||
return "read-only".to_string();
|
||||
}
|
||||
if let Some(details) = summary.strip_prefix("workspace-write") {
|
||||
if details.contains("(network access enabled)") {
|
||||
return "workspace with network access".to_string();
|
||||
let (details, has_network) =
|
||||
details_without_network_suffix(details, "(network access enabled)");
|
||||
let mut summary = workspace_write_extra_details(details)
|
||||
.map(|details| format!("workspace{details}"))
|
||||
.unwrap_or_else(|| "workspace".to_string());
|
||||
if has_network {
|
||||
summary.push_str(" with network access");
|
||||
}
|
||||
return "workspace".to_string();
|
||||
return summary;
|
||||
}
|
||||
if summary == "custom permissions (network access enabled)" {
|
||||
return "custom permissions with network access".to_string();
|
||||
}
|
||||
summary
|
||||
summary.to_string()
|
||||
}
|
||||
|
||||
fn status_permissions_label(
|
||||
@@ -567,25 +567,6 @@ fn status_permissions_label(
|
||||
approval: &str,
|
||||
) -> String {
|
||||
let active_id = active_permission_profile.map(|active| active.id.as_str());
|
||||
let writable_root_modifications = active_permission_profile
|
||||
.map(|active| {
|
||||
active
|
||||
.modifications
|
||||
.iter()
|
||||
.filter(|modification| {
|
||||
matches!(
|
||||
modification,
|
||||
ActivePermissionProfileModification::AdditionalWritableRoot { .. }
|
||||
)
|
||||
})
|
||||
.count()
|
||||
})
|
||||
.unwrap_or(0);
|
||||
let modification_suffix = match writable_root_modifications {
|
||||
0 => String::new(),
|
||||
1 => " + 1 writable root".to_string(),
|
||||
count => format!(" + {count} writable roots"),
|
||||
};
|
||||
match active_id {
|
||||
Some(":read-only") => {
|
||||
let label = if sandbox == "read-only with network access" {
|
||||
@@ -593,15 +574,16 @@ fn status_permissions_label(
|
||||
} else {
|
||||
"Read Only"
|
||||
};
|
||||
return format!("{label}{modification_suffix} ({approval})");
|
||||
return format!("{label} ({approval})");
|
||||
}
|
||||
Some(":workspace") => {
|
||||
let label = if let Some(details) = sandbox.strip_prefix("workspace") {
|
||||
format!("Workspace{details}")
|
||||
} else {
|
||||
"Workspace".to_string()
|
||||
};
|
||||
return format!("{label} ({approval})");
|
||||
}
|
||||
Some(":workspace") => match sandbox {
|
||||
"workspace" => return format!("Workspace{modification_suffix} ({approval})"),
|
||||
"workspace with network access" => {
|
||||
return format!("Workspace with network access{modification_suffix} ({approval})");
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Some(":danger-no-sandbox") if permission_profile == &PermissionProfile::Disabled => {
|
||||
return if approval_policy == AskForApproval::Never {
|
||||
"Full Access".to_string()
|
||||
@@ -609,7 +591,7 @@ fn status_permissions_label(
|
||||
format!("No Sandbox ({approval})")
|
||||
};
|
||||
}
|
||||
Some(id) => return format!("Profile {id}{modification_suffix} ({sandbox}, {approval})"),
|
||||
Some(id) => return format!("Profile {id} ({sandbox}, {approval})"),
|
||||
None => {}
|
||||
}
|
||||
|
||||
@@ -627,6 +609,28 @@ fn status_permissions_label(
|
||||
format!("Custom ({sandbox}, {approval})")
|
||||
}
|
||||
|
||||
fn details_without_network_suffix<'a>(details: &'a str, network_suffix: &str) -> (&'a str, bool) {
|
||||
details
|
||||
.strip_suffix(network_suffix)
|
||||
.map(|details| (details.trim_end(), true))
|
||||
.unwrap_or((details, false))
|
||||
}
|
||||
|
||||
fn workspace_write_extra_details(details: &str) -> Option<String> {
|
||||
let entries = details.trim().strip_prefix('[')?;
|
||||
let entries = entries.strip_suffix(']')?;
|
||||
let extras = entries
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|entry| !matches!(*entry, "workdir" | "/tmp" | "$TMPDIR"))
|
||||
.collect::<Vec<_>>();
|
||||
if extras.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(format!(" [{}]", extras.join(", ")))
|
||||
}
|
||||
}
|
||||
|
||||
fn status_approval_label(
|
||||
approval_policy: AskForApproval,
|
||||
approvals_reviewer: ApprovalsReviewer,
|
||||
|
||||
@@ -30,7 +30,6 @@ use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::models::ActivePermissionProfile;
|
||||
use codex_protocol::models::ActivePermissionProfileModification;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
@@ -97,6 +96,12 @@ fn test_status_account_display() -> Option<StatusAccountDisplay> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_cwd_as_workspace_root(config: &mut Config, cwd: &str) {
|
||||
let cwd = test_path_buf(cwd).abs();
|
||||
config.cwd = cwd.clone();
|
||||
config.workspace_roots = vec![cwd];
|
||||
}
|
||||
|
||||
fn token_info_for(model_slug: &str, config: &Config, usage: &TokenUsage) -> TokenUsageInfo {
|
||||
let context_window =
|
||||
crate::legacy_core::test_support::construct_model_info_offline(model_slug, config)
|
||||
@@ -191,7 +196,7 @@ async fn status_snapshot_includes_reasoning_details() {
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.model_provider_id = "openai".to_string();
|
||||
config.model_reasoning_summary = Some(ReasoningSummary::Detailed);
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_cwd_as_workspace_root(&mut config, "/workspace/tests");
|
||||
config
|
||||
.permissions
|
||||
.set_permission_profile(PermissionProfile::workspace_write())
|
||||
@@ -269,7 +274,7 @@ async fn status_permissions_non_default_workspace_write_uses_workspace_label() {
|
||||
.approval_policy
|
||||
.set(AskForApproval::OnRequest.to_core())
|
||||
.expect("set approval policy");
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_cwd_as_workspace_root(&mut config, "/workspace/tests");
|
||||
config
|
||||
.permissions
|
||||
.set_permission_profile(app_server_workspace_write_profile(
|
||||
@@ -307,7 +312,7 @@ async fn status_permissions_named_read_only_profile_shows_builtin_label() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn status_permissions_read_only_profile_shows_additional_writable_roots() {
|
||||
async fn status_permissions_read_only_profile_ignores_workspace_root_metadata() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config
|
||||
@@ -326,19 +331,13 @@ async fn status_permissions_read_only_profile_shows_additional_writable_roots()
|
||||
&file_system_policy,
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
),
|
||||
Some(
|
||||
ActivePermissionProfile::new(":read-only").with_modifications(vec![
|
||||
ActivePermissionProfileModification::AdditionalWritableRoot {
|
||||
path: extra_root,
|
||||
},
|
||||
]),
|
||||
),
|
||||
Some(ActivePermissionProfile::new(":read-only")),
|
||||
)
|
||||
.expect("set permission profile");
|
||||
|
||||
assert_eq!(
|
||||
permissions_text_for(&config).as_deref(),
|
||||
Some("Read Only + 1 writable root (on-request)")
|
||||
Some("Read Only (on-request)")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -390,7 +389,7 @@ async fn status_permissions_workspace_auto_review_shows_reviewer_label() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn status_permissions_named_profile_shows_additional_writable_roots() {
|
||||
async fn status_permissions_named_workspace_profile_shows_workspace_roots() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config
|
||||
@@ -398,29 +397,79 @@ async fn status_permissions_named_profile_shows_additional_writable_roots() {
|
||||
.approval_policy
|
||||
.set(AskForApproval::OnRequest.to_core())
|
||||
.expect("set approval policy");
|
||||
set_cwd_as_workspace_root(&mut config, "/workspace/project");
|
||||
let extra_root = test_path_buf("/workspace/extra").abs();
|
||||
config.workspace_roots = vec![config.cwd.clone(), extra_root.clone()];
|
||||
config
|
||||
.permissions
|
||||
.set_permission_profile_with_active_profile(
|
||||
PermissionProfile::workspace_write_with(
|
||||
std::slice::from_ref(&extra_root),
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
/*exclude_tmpdir_env_var*/ false,
|
||||
/*exclude_slash_tmp*/ false,
|
||||
),
|
||||
Some(
|
||||
ActivePermissionProfile::new(":workspace").with_modifications(vec![
|
||||
ActivePermissionProfileModification::AdditionalWritableRoot {
|
||||
path: extra_root,
|
||||
},
|
||||
]),
|
||||
),
|
||||
PermissionProfile::workspace_write(),
|
||||
Some(ActivePermissionProfile::new(":workspace")),
|
||||
)
|
||||
.expect("set permission profile");
|
||||
|
||||
assert_eq!(
|
||||
permissions_text_for(&config),
|
||||
Some(format!("Workspace [{}] (on-request)", extra_root.display()))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn status_permissions_workspace_profile_hides_internal_memories_root() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config
|
||||
.permissions
|
||||
.approval_policy
|
||||
.set(AskForApproval::OnRequest.to_core())
|
||||
.expect("set approval policy");
|
||||
set_cwd_as_workspace_root(&mut config, "/workspace/project");
|
||||
let memories_root = config.codex_home.join("memories");
|
||||
let (file_system_policy, network_policy) =
|
||||
PermissionProfile::workspace_write().to_runtime_permissions();
|
||||
let file_system_policy = file_system_policy
|
||||
.with_additional_legacy_workspace_writable_roots(std::slice::from_ref(&memories_root));
|
||||
config
|
||||
.permissions
|
||||
.set_permission_profile_with_active_profile(
|
||||
PermissionProfile::from_runtime_permissions(&file_system_policy, network_policy),
|
||||
Some(ActivePermissionProfile::new(":workspace")),
|
||||
)
|
||||
.expect("set permission profile");
|
||||
|
||||
assert_eq!(
|
||||
permissions_text_for(&config).as_deref(),
|
||||
Some("Workspace + 1 writable root (on-request)")
|
||||
Some("Workspace (on-request)")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn status_permissions_hides_internal_memories_root_when_it_is_the_only_writable_root() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config
|
||||
.permissions
|
||||
.approval_policy
|
||||
.set(AskForApproval::OnRequest.to_core())
|
||||
.expect("set approval policy");
|
||||
set_cwd_as_workspace_root(&mut config, "/workspace/project");
|
||||
config.workspace_roots = Vec::new();
|
||||
let memories_root = config.codex_home.join("memories");
|
||||
let (file_system_policy, network_policy) =
|
||||
PermissionProfile::workspace_write().to_runtime_permissions();
|
||||
let file_system_policy = file_system_policy
|
||||
.with_additional_legacy_workspace_writable_roots(std::slice::from_ref(&memories_root));
|
||||
config
|
||||
.permissions
|
||||
.set_permission_profile_with_active_profile(
|
||||
PermissionProfile::from_runtime_permissions(&file_system_policy, network_policy),
|
||||
Some(ActivePermissionProfile::new(":workspace")),
|
||||
)
|
||||
.expect("set permission profile");
|
||||
|
||||
assert_eq!(
|
||||
permissions_text_for(&config).as_deref(),
|
||||
Some("Workspace (on-request)")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -475,7 +524,7 @@ async fn status_snapshot_shows_active_user_defined_profile() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_cwd_as_workspace_root(&mut config, "/workspace/tests");
|
||||
config
|
||||
.permissions
|
||||
.set_permission_profile_with_active_profile(
|
||||
@@ -572,7 +621,7 @@ async fn status_snapshot_shows_auto_review_permissions() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_cwd_as_workspace_root(&mut config, "/workspace/tests");
|
||||
config.approvals_reviewer = ApprovalsReviewer::AutoReview;
|
||||
config
|
||||
.permissions
|
||||
@@ -673,7 +722,7 @@ async fn status_snapshot_includes_forked_from() {
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.model_provider_id = "openai".to_string();
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_cwd_as_workspace_root(&mut config, "/workspace/tests");
|
||||
|
||||
let account_display = test_status_account_display();
|
||||
let usage = TokenUsage {
|
||||
@@ -727,7 +776,7 @@ async fn status_snapshot_includes_monthly_limit() {
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.model_provider_id = "openai".to_string();
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_cwd_as_workspace_root(&mut config, "/workspace/tests");
|
||||
|
||||
let account_display = test_status_account_display();
|
||||
let usage = TokenUsage {
|
||||
@@ -985,7 +1034,7 @@ async fn status_card_token_usage_excludes_cached_tokens() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_cwd_as_workspace_root(&mut config, "/workspace/tests");
|
||||
|
||||
let account_display = test_status_account_display();
|
||||
let usage = TokenUsage {
|
||||
@@ -1033,7 +1082,7 @@ async fn status_snapshot_truncates_in_narrow_terminal() {
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.model_provider_id = "openai".to_string();
|
||||
config.model_reasoning_summary = Some(ReasoningSummary::Detailed);
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_cwd_as_workspace_root(&mut config, "/workspace/tests");
|
||||
|
||||
let account_display = test_status_account_display();
|
||||
let usage = TokenUsage {
|
||||
@@ -1097,7 +1146,7 @@ async fn status_snapshot_shows_missing_limits_message() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_cwd_as_workspace_root(&mut config, "/workspace/tests");
|
||||
|
||||
let account_display = test_status_account_display();
|
||||
let usage = TokenUsage {
|
||||
@@ -1145,7 +1194,7 @@ async fn status_snapshot_uses_default_reasoning_when_config_empty() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_cwd_as_workspace_root(&mut config, "/workspace/tests");
|
||||
|
||||
let account_display = test_status_account_display();
|
||||
let usage = TokenUsage {
|
||||
@@ -1196,7 +1245,7 @@ async fn status_snapshot_shows_refreshing_limits_notice() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_cwd_as_workspace_root(&mut config, "/workspace/tests");
|
||||
|
||||
let usage = TokenUsage {
|
||||
input_tokens: 500,
|
||||
@@ -1261,7 +1310,7 @@ async fn status_snapshot_includes_credits_and_limits() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex".to_string());
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_cwd_as_workspace_root(&mut config, "/workspace/tests");
|
||||
|
||||
let account_display = test_status_account_display();
|
||||
let usage = TokenUsage {
|
||||
@@ -1331,7 +1380,7 @@ async fn status_snapshot_shows_unavailable_limits_message() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_cwd_as_workspace_root(&mut config, "/workspace/tests");
|
||||
|
||||
let account_display = test_status_account_display();
|
||||
let usage = TokenUsage {
|
||||
@@ -1389,7 +1438,7 @@ async fn status_snapshot_treats_refreshing_empty_limits_as_unavailable() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_cwd_as_workspace_root(&mut config, "/workspace/tests");
|
||||
|
||||
let usage = TokenUsage {
|
||||
input_tokens: 500,
|
||||
@@ -1447,7 +1496,7 @@ async fn status_snapshot_shows_stale_limits_message() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_cwd_as_workspace_root(&mut config, "/workspace/tests");
|
||||
|
||||
let account_display = test_status_account_display();
|
||||
let usage = TokenUsage {
|
||||
@@ -1514,7 +1563,7 @@ async fn status_snapshot_cached_limits_hide_credits_without_flag() {
|
||||
let temp_home = TempDir::new().expect("temp home");
|
||||
let mut config = test_config(&temp_home).await;
|
||||
config.model = Some("gpt-5.1-codex".to_string());
|
||||
config.cwd = test_path_buf("/workspace/tests").abs();
|
||||
set_cwd_as_workspace_root(&mut config, "/workspace/tests");
|
||||
|
||||
let account_display = test_status_account_display();
|
||||
let usage = TokenUsage {
|
||||
|
||||
Reference in New Issue
Block a user