mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
app-server: stop returning thread permission profiles (#22792)
## Why The app-server thread lifecycle API should no longer expose the full `PermissionProfile` value. After the permissions-profile migration, clients should round-trip only the active profile identity through `activePermissionProfile` and `permissions` when that identity is known. The full profile is server-side config. Treating a response-derived legacy sandbox projection as a new local profile can lose named-profile restrictions and accidentally widen permissions on the next turn. The legacy `sandbox` response field remains only as the compatibility/display fallback. ## What Changed - Removed `permissionProfile` from `ThreadStartResponse`, `ThreadResumeResponse`, and `ThreadForkResponse`. - Stopped populating that field in app-server thread start/resume/fork responses. - Updated embedded exec/TUI response mapping to derive display permission state from local config or the legacy sandbox fallback instead of a response profile value. - Added a TUI turn override shape that distinguishes preserving server permissions, selecting an active profile id, and sending a legacy sandbox for an explicit local override. - Preserved remote app-server permissions across turns by sending `permissions` only when an `activePermissionProfile` id is known, and otherwise sending no sandbox override unless the user selected a local override. - Kept embedded `thread/resume` hydration server-authored when `activePermissionProfile` is absent, which matches the live-thread attach path where the server ignores requested overrides. - Updated the app-server README to remove the obsolete lifecycle response `permissionProfile` reference. The remaining `permissionProfile` README references are request-side permission overrides. - Regenerated app-server JSON schema and TypeScript fixtures. - Kept the generated typed response enum exempt from `large_enum_variant`, matching the existing payload enum exemption after the lifecycle response variants shrank. ## How To Review Start with `codex-rs/app-server-protocol/src/protocol/v2/thread.rs` to confirm the response shape, then check the response construction in `codex-rs/app-server/src/request_processors`. The generated schema and TypeScript fixture changes are mechanical follow-through from the protocol removal. The TUI behavior is the delicate part: review `codex-rs/tui/src/app_server_session.rs` for response hydration and turn-start override projection, then `codex-rs/tui/src/app/thread_routing.rs` for the decision about whether the next turn should preserve the server snapshot, send an active profile id, or send a legacy sandbox for an explicit local override. ## Verification - `just write-app-server-schema` - `cargo test -p codex-app-server-protocol thread_lifecycle_responses_default_missing_optional_fields` - `cargo test -p codex-exec session_configured_from_thread_response_uses_permission_profile_from_config` - `cargo test -p codex-tui --lib thread_response` - `cargo test -p codex-tui turn_permissions_` - `cargo test -p codex-tui resume_response_restores_turns_from_thread_items` - `cargo test -p codex-analytics track_response_only_enqueues_analytics_relevant_responses` - `just fix -p codex-analytics` - `just fix -p codex-app-server-protocol` - `just fix -p codex-tui` - `just argument-comment-lint` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/22792). * #22795 * __->__ #22792
This commit is contained in:
@@ -16,6 +16,7 @@ use crate::app_event::WindowsSandboxEnableMode;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::app_server_session::AppServerSession;
|
||||
use crate::app_server_session::AppServerStartedThread;
|
||||
use crate::app_server_session::TurnPermissionsOverride;
|
||||
use crate::app_server_session::app_server_rate_limit_snapshots;
|
||||
use crate::bottom_pane::AppLinkViewParams;
|
||||
use crate::bottom_pane::ApprovalRequest;
|
||||
|
||||
@@ -588,14 +588,11 @@ impl App {
|
||||
let config = self.chat_widget.config_ref();
|
||||
let approvals_reviewer =
|
||||
approvals_reviewer.unwrap_or(config.approvals_reviewer);
|
||||
let active_permission_profile =
|
||||
if config.permissions.effective_permission_profile()
|
||||
== permission_profile.clone()
|
||||
{
|
||||
config.permissions.active_permission_profile()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let permissions_override = Self::turn_permissions_override_from_config(
|
||||
config,
|
||||
permission_profile,
|
||||
self.runtime_permission_profile_override.as_ref(),
|
||||
);
|
||||
app_server
|
||||
.turn_start(
|
||||
thread_id,
|
||||
@@ -603,8 +600,7 @@ impl App {
|
||||
cwd.clone(),
|
||||
*approval_policy,
|
||||
approvals_reviewer,
|
||||
permission_profile.clone(),
|
||||
active_permission_profile,
|
||||
permissions_override,
|
||||
config.permissions.user_visible_workspace_roots(),
|
||||
model.to_string(),
|
||||
*effort,
|
||||
@@ -700,6 +696,36 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn turn_permissions_override_from_config(
|
||||
config: &Config,
|
||||
permission_profile: &PermissionProfile,
|
||||
runtime_permission_profile_override: Option<&PermissionProfile>,
|
||||
) -> TurnPermissionsOverride {
|
||||
let effective_permission_profile = config.permissions.effective_permission_profile();
|
||||
if &effective_permission_profile == permission_profile
|
||||
&& let Some(active_permission_profile) = config.permissions.active_permission_profile()
|
||||
{
|
||||
return TurnPermissionsOverride::ActiveProfile(active_permission_profile);
|
||||
}
|
||||
|
||||
let runtime_permission_profile_override =
|
||||
runtime_permission_profile_override.map(|profile| {
|
||||
profile
|
||||
.clone()
|
||||
.materialize_project_roots_with_workspace_roots(
|
||||
&config.effective_workspace_roots(),
|
||||
)
|
||||
});
|
||||
if runtime_permission_profile_override
|
||||
.as_ref()
|
||||
.is_some_and(|profile| profile == permission_profile)
|
||||
{
|
||||
return TurnPermissionsOverride::LegacySandbox(permission_profile.clone());
|
||||
}
|
||||
|
||||
TurnPermissionsOverride::Preserve
|
||||
}
|
||||
|
||||
pub(super) fn handle_skills_list_result(
|
||||
&mut self,
|
||||
result: Result<SkillsListResponse>,
|
||||
@@ -1457,3 +1483,79 @@ impl App {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_protocol::models::ActivePermissionProfile;
|
||||
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
|
||||
|
||||
async fn config_with_workspace_profile() -> Config {
|
||||
let temp_dir = tempfile::tempdir().expect("tempdir");
|
||||
ConfigBuilder::default()
|
||||
.codex_home(temp_dir.path().to_path_buf())
|
||||
.harness_overrides(ConfigOverrides {
|
||||
default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string()),
|
||||
..ConfigOverrides::default()
|
||||
})
|
||||
.build()
|
||||
.await
|
||||
.expect("config should build")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_permissions_use_active_profile_when_available() {
|
||||
let config = config_with_workspace_profile().await;
|
||||
let permission_profile = config.permissions.effective_permission_profile();
|
||||
|
||||
assert_eq!(
|
||||
App::turn_permissions_override_from_config(
|
||||
&config,
|
||||
&permission_profile,
|
||||
/*runtime_permission_profile_override*/ None,
|
||||
),
|
||||
TurnPermissionsOverride::ActiveProfile(ActivePermissionProfile::new(
|
||||
BUILT_IN_PERMISSION_PROFILE_WORKSPACE
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_permissions_preserve_server_snapshot_without_local_override() {
|
||||
let mut config = config_with_workspace_profile().await;
|
||||
config
|
||||
.permissions
|
||||
.set_permission_profile(PermissionProfile::read_only())
|
||||
.expect("read-only profile should be allowed");
|
||||
let permission_profile = config.permissions.effective_permission_profile();
|
||||
|
||||
assert_eq!(
|
||||
App::turn_permissions_override_from_config(
|
||||
&config,
|
||||
&permission_profile,
|
||||
/*runtime_permission_profile_override*/ None,
|
||||
),
|
||||
TurnPermissionsOverride::Preserve
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_permissions_send_legacy_sandbox_for_local_override() {
|
||||
let mut config = config_with_workspace_profile().await;
|
||||
let permission_profile = PermissionProfile::workspace_write();
|
||||
config
|
||||
.permissions
|
||||
.set_permission_profile(permission_profile.clone())
|
||||
.expect("workspace profile should be allowed");
|
||||
let effective_permission_profile = config.permissions.effective_permission_profile();
|
||||
|
||||
assert_eq!(
|
||||
App::turn_permissions_override_from_config(
|
||||
&config,
|
||||
&effective_permission_profile,
|
||||
Some(&permission_profile),
|
||||
),
|
||||
TurnPermissionsOverride::LegacySandbox(effective_permission_profile)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +172,16 @@ pub(crate) struct AppServerStartedThread {
|
||||
pub(crate) turns: Vec<Turn>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum TurnPermissionsOverride {
|
||||
/// Leave the app-server thread's sticky permission profile unchanged.
|
||||
Preserve,
|
||||
/// Select a named or built-in profile by id.
|
||||
ActiveProfile(ActivePermissionProfile),
|
||||
/// Apply a user-selected legacy/custom permission profile.
|
||||
LegacySandbox(PermissionProfile),
|
||||
}
|
||||
|
||||
impl AppServerSession {
|
||||
pub(crate) fn new(client: AppServerClient) -> Self {
|
||||
Self {
|
||||
@@ -548,8 +558,7 @@ impl AppServerSession {
|
||||
cwd: PathBuf,
|
||||
approval_policy: AskForApproval,
|
||||
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer,
|
||||
permission_profile: PermissionProfile,
|
||||
active_permission_profile: Option<ActivePermissionProfile>,
|
||||
permissions_override: TurnPermissionsOverride,
|
||||
workspace_roots: &[AbsolutePathBuf],
|
||||
model: String,
|
||||
effort: Option<codex_protocol::openai_models::ReasoningEffort>,
|
||||
@@ -560,13 +569,8 @@ impl AppServerSession {
|
||||
output_schema: Option<serde_json::Value>,
|
||||
) -> Result<TurnStartResponse> {
|
||||
let request_id = self.next_request_id();
|
||||
let (sandbox_policy, permissions) = turn_permissions_overrides(
|
||||
&permission_profile,
|
||||
active_permission_profile,
|
||||
cwd.as_path(),
|
||||
workspace_roots,
|
||||
self.thread_params_mode(),
|
||||
);
|
||||
let (sandbox_policy, permissions) =
|
||||
turn_permissions_overrides(permissions_override, cwd.as_path());
|
||||
self.client
|
||||
.request_typed(ClientRequest::TurnStart {
|
||||
request_id,
|
||||
@@ -1185,32 +1189,32 @@ fn permissions_selection_from_active_profile(
|
||||
}
|
||||
|
||||
fn turn_permissions_overrides(
|
||||
permission_profile: &PermissionProfile,
|
||||
active_permission_profile: Option<ActivePermissionProfile>,
|
||||
permissions_override: TurnPermissionsOverride,
|
||||
cwd: &std::path::Path,
|
||||
_workspace_roots: &[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)
|
||||
match permissions_override {
|
||||
TurnPermissionsOverride::Preserve => (None, None),
|
||||
TurnPermissionsOverride::ActiveProfile(active_permission_profile) => (
|
||||
None,
|
||||
Some(permissions_selection_from_active_profile(
|
||||
active_permission_profile,
|
||||
)),
|
||||
),
|
||||
TurnPermissionsOverride::LegacySandbox(permission_profile) => {
|
||||
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}"
|
||||
)
|
||||
});
|
||||
(Some(policy.into()), None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn permissions_selection_from_config(
|
||||
@@ -1413,9 +1417,8 @@ async fn thread_session_state_from_thread_start_response(
|
||||
config: &Config,
|
||||
thread_params_mode: ThreadParamsMode,
|
||||
) -> Result<ThreadSessionState, String> {
|
||||
let permission_profile = permission_profile_from_thread_response(
|
||||
let permission_profile = display_permission_profile_from_thread_response(
|
||||
&response.sandbox,
|
||||
response.permission_profile.as_ref(),
|
||||
response.cwd.as_path(),
|
||||
config,
|
||||
thread_params_mode,
|
||||
@@ -1446,13 +1449,21 @@ async fn thread_session_state_from_thread_resume_response(
|
||||
config: &Config,
|
||||
thread_params_mode: ThreadParamsMode,
|
||||
) -> 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 permission_profile = if matches!(thread_params_mode, ThreadParamsMode::Embedded)
|
||||
&& response.active_permission_profile.is_none()
|
||||
{
|
||||
PermissionProfile::from_legacy_sandbox_policy_for_cwd(
|
||||
&response.sandbox.to_core(),
|
||||
response.cwd.as_path(),
|
||||
)
|
||||
} else {
|
||||
display_permission_profile_from_thread_response(
|
||||
&response.sandbox,
|
||||
response.cwd.as_path(),
|
||||
config,
|
||||
thread_params_mode,
|
||||
)
|
||||
};
|
||||
thread_session_state_from_thread_response(
|
||||
&response.thread.id,
|
||||
response.thread.forked_from_id.clone(),
|
||||
@@ -1479,9 +1490,8 @@ async fn thread_session_state_from_thread_fork_response(
|
||||
config: &Config,
|
||||
thread_params_mode: ThreadParamsMode,
|
||||
) -> Result<ThreadSessionState, String> {
|
||||
let permission_profile = permission_profile_from_thread_response(
|
||||
let permission_profile = display_permission_profile_from_thread_response(
|
||||
&response.sandbox,
|
||||
response.permission_profile.as_ref(),
|
||||
response.cwd.as_path(),
|
||||
config,
|
||||
thread_params_mode,
|
||||
@@ -1507,16 +1517,12 @@ async fn thread_session_state_from_thread_fork_response(
|
||||
.await
|
||||
}
|
||||
|
||||
fn permission_profile_from_thread_response(
|
||||
fn display_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.effective_permission_profile(),
|
||||
ThreadParamsMode::Remote => {
|
||||
@@ -1692,16 +1698,12 @@ mod tests {
|
||||
let cwd = test_path_buf("/workspace/project").abs();
|
||||
let active_permission_profile =
|
||||
ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_WORKSPACE);
|
||||
let workspace_roots = vec![cwd.clone()];
|
||||
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),
|
||||
TurnPermissionsOverride::ActiveProfile(active_permission_profile),
|
||||
cwd.as_path(),
|
||||
&workspace_roots,
|
||||
ThreadParamsMode::Embedded,
|
||||
);
|
||||
|
||||
assert_eq!(sandbox_policy, None);
|
||||
@@ -1711,17 +1713,12 @@ mod tests {
|
||||
#[test]
|
||||
fn embedded_turn_permissions_select_profile_id_only() {
|
||||
let cwd = test_path_buf("/workspace/project").abs();
|
||||
let extra_root = test_path_buf("/workspace/cache").abs();
|
||||
let active_permission_profile =
|
||||
ActivePermissionProfile::new(BUILT_IN_PERMISSION_PROFILE_WORKSPACE);
|
||||
let workspace_roots = vec![cwd.clone(), extra_root];
|
||||
|
||||
let (sandbox_policy, permissions) = turn_permissions_overrides(
|
||||
&PermissionProfile::workspace_write(),
|
||||
Some(active_permission_profile),
|
||||
TurnPermissionsOverride::ActiveProfile(active_permission_profile),
|
||||
cwd.as_path(),
|
||||
&workspace_roots,
|
||||
ThreadParamsMode::Embedded,
|
||||
);
|
||||
|
||||
assert_eq!(sandbox_policy, None);
|
||||
@@ -1734,15 +1731,23 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_turn_permissions_fall_back_to_sandbox_without_active_profile() {
|
||||
fn turn_permissions_preserve_thread_permissions_without_override() {
|
||||
let cwd = test_path_buf("/workspace/project").abs();
|
||||
|
||||
let (sandbox_policy, permissions) =
|
||||
turn_permissions_overrides(TurnPermissionsOverride::Preserve, cwd.as_path());
|
||||
|
||||
assert_eq!(sandbox_policy, None);
|
||||
assert_eq!(permissions, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_turn_permissions_project_to_sandbox_when_explicitly_overridden() {
|
||||
let cwd = test_path_buf("/workspace/project").abs();
|
||||
|
||||
let (sandbox_policy, permissions) = turn_permissions_overrides(
|
||||
&PermissionProfile::read_only(),
|
||||
/*active_permission_profile*/ None,
|
||||
TurnPermissionsOverride::LegacySandbox(PermissionProfile::read_only()),
|
||||
cwd.as_path(),
|
||||
std::slice::from_ref(&cwd),
|
||||
ThreadParamsMode::Embedded,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -1755,26 +1760,19 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_turn_permissions_use_sandbox_even_with_active_profile() {
|
||||
fn remote_turn_permissions_preserve_active_profile_selection() {
|
||||
let cwd = test_path_buf("/workspace/project").abs();
|
||||
let active_permission_profile = ActivePermissionProfile::new("strict");
|
||||
let expected_permissions =
|
||||
permissions_selection_from_active_profile(active_permission_profile.clone());
|
||||
|
||||
let (sandbox_policy, permissions) = turn_permissions_overrides(
|
||||
&PermissionProfile::read_only(),
|
||||
Some(ActivePermissionProfile::new(
|
||||
BUILT_IN_PERMISSION_PROFILE_READ_ONLY,
|
||||
)),
|
||||
TurnPermissionsOverride::ActiveProfile(active_permission_profile),
|
||||
cwd.as_path(),
|
||||
std::slice::from_ref(&cwd),
|
||||
ThreadParamsMode::Remote,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
sandbox_policy,
|
||||
Some(codex_app_server_protocol::SandboxPolicy::ReadOnly {
|
||||
network_access: false
|
||||
})
|
||||
);
|
||||
assert_eq!(permissions, None);
|
||||
assert_eq!(sandbox_policy, None);
|
||||
assert_eq!(permissions, Some(expected_permissions));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -2109,7 +2107,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,
|
||||
};
|
||||
@@ -2134,6 +2131,24 @@ mod tests {
|
||||
assert_eq!(started.turns.len(), 1);
|
||||
assert_eq!(started.turns[0], response.thread.turns[0]);
|
||||
|
||||
let embedded_config = ConfigBuilder::default()
|
||||
.codex_home(temp_dir.path().join("embedded-codex-home"))
|
||||
.harness_overrides(ConfigOverrides {
|
||||
default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_WORKSPACE.to_string()),
|
||||
..ConfigOverrides::default()
|
||||
})
|
||||
.build()
|
||||
.await
|
||||
.expect("config should build");
|
||||
let started = started_thread_from_resume_response(
|
||||
response.clone(),
|
||||
&embedded_config,
|
||||
ThreadParamsMode::Embedded,
|
||||
)
|
||||
.await
|
||||
.expect("embedded resume response should map");
|
||||
assert_eq!(started.session.permission_profile, read_only_profile);
|
||||
|
||||
let mut empty_roots_response = response;
|
||||
empty_roots_response.runtime_workspace_roots = Vec::new();
|
||||
let started = started_thread_from_resume_response(
|
||||
@@ -2147,61 +2162,43 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_thread_response_prefers_permission_profile_over_legacy_sandbox() {
|
||||
async fn remote_thread_response_uses_legacy_sandbox_fallback() {
|
||||
let temp_dir = tempfile::tempdir().expect("tempdir");
|
||||
let config = build_config(&temp_dir).await;
|
||||
let cwd = test_path_buf("/tmp/project").abs();
|
||||
let fallback_sandbox = PermissionProfile::read_only()
|
||||
let sandbox = PermissionProfile::read_only()
|
||||
.to_legacy_sandbox_policy(cwd.as_path())
|
||||
.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),
|
||||
display_permission_profile_from_thread_response(
|
||||
&sandbox,
|
||||
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 embedded_thread_response_uses_local_config_profile() {
|
||||
let temp_dir = tempfile::tempdir().expect("tempdir");
|
||||
let config = build_config(&temp_dir).await;
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(temp_dir.path().to_path_buf())
|
||||
.harness_overrides(ConfigOverrides {
|
||||
default_permissions: Some(BUILT_IN_PERMISSION_PROFILE_READ_ONLY.to_string()),
|
||||
..ConfigOverrides::default()
|
||||
})
|
||||
.build()
|
||||
.await
|
||||
.expect("config should build");
|
||||
let cwd = test_path_buf("/tmp/project").abs();
|
||||
let response_profile = PermissionProfile::read_only().into();
|
||||
|
||||
assert_eq!(
|
||||
permission_profile_from_thread_response(
|
||||
display_permission_profile_from_thread_response(
|
||||
&codex_app_server_protocol::SandboxPolicy::DangerFullAccess,
|
||||
Some(&response_profile),
|
||||
cwd.as_path(),
|
||||
&config,
|
||||
ThreadParamsMode::Embedded,
|
||||
|
||||
@@ -34,9 +34,11 @@ pub(crate) struct ThreadSessionState {
|
||||
pub(crate) service_tier: Option<String>,
|
||||
pub(crate) approval_policy: AskForApproval,
|
||||
pub(crate) approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer,
|
||||
/// Canonical active permissions for this session. Legacy app-server
|
||||
/// Permission snapshot used by TUI display surfaces. Legacy app-server
|
||||
/// responses are converted to a profile at ingestion time using the
|
||||
/// response cwd so cached sessions do not reinterpret cwd-bound grants.
|
||||
/// Turn requests must not treat this snapshot as a local permission
|
||||
/// override unless the user explicitly changed permissions in the TUI.
|
||||
pub(crate) permission_profile: PermissionProfile,
|
||||
/// Named or implicit built-in profile that produced `permission_profile`,
|
||||
/// when the server knows it.
|
||||
|
||||
Reference in New Issue
Block a user