mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Separate thread settings from environment configuration (#39597)
## Why Environment-owned permission profiles and workspace roots must remain effective without being persisted or restored as thread-owned settings. ## What changed - Keep thread settings snapshots and restore data separate from the effective configuration supplied by the primary environment. - Use resolved environment permissions, active profiles, workspace roots, and network policy when building turn context and session events. - Preserve environment selections when agents are evicted, resumed, or spawned, and validate owner configuration when it becomes ready. - Start memory initialization only after the primary environment is configured. ## Testing - Extend remote-environment and multi-agent tests to cover settings persistence, owner permission profiles, workspace roots, spawning, eviction, and resume. GitOrigin-RevId: d82f126981cd0190233020bbdd61d96c70558868
This commit is contained in:
@@ -4,7 +4,7 @@ use crate::outgoing_message::ClientRequestResult;
|
||||
use crate::outgoing_message::ThreadScopedOutgoingMessageSender;
|
||||
use crate::request_processors::populate_thread_turns_from_history;
|
||||
use crate::request_processors::thread_from_stored_thread;
|
||||
use crate::request_processors::thread_settings_from_core_snapshot;
|
||||
use crate::request_processors::thread_settings_from_config_snapshot;
|
||||
use crate::server_request_error::is_turn_transition_server_request_error;
|
||||
use crate::thread_state::ThreadState;
|
||||
use crate::thread_state::TurnSummary;
|
||||
@@ -1205,9 +1205,9 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
.await;
|
||||
}
|
||||
EventMsg::ThreadQueueChanged(_) => {}
|
||||
EventMsg::ThreadSettingsApplied(thread_settings_event) => {
|
||||
EventMsg::ThreadSettingsApplied(_) => {
|
||||
let thread_settings =
|
||||
thread_settings_from_core_snapshot(thread_settings_event.thread_settings);
|
||||
thread_settings_from_config_snapshot(&conversation.config_snapshot().await);
|
||||
let changed = {
|
||||
let mut state = thread_state.lock().await;
|
||||
state.note_thread_settings(thread_settings.clone())
|
||||
|
||||
@@ -684,7 +684,6 @@ pub(crate) use self::thread_summary::read_summary_from_rollout;
|
||||
#[cfg(test)]
|
||||
pub(crate) use self::thread_summary::summary_to_thread;
|
||||
pub(crate) use self::thread_summary::thread_settings_from_config_snapshot;
|
||||
pub(crate) use self::thread_summary::thread_settings_from_core_snapshot;
|
||||
|
||||
pub(crate) fn build_legacy_api_turns_from_rollout_items(items: &[RolloutItem]) -> Vec<Turn> {
|
||||
let mut builder = ThreadHistoryBuilder::new();
|
||||
|
||||
@@ -161,10 +161,7 @@ impl ThreadGoalRequestProcessor {
|
||||
Some(path) if codex_rollout::existing_rollout_path(&path).await.is_none() => {
|
||||
// Goal-first threads need their settings captured when the goal creates the
|
||||
// rollout. Once materialized, normal settings updates own this event.
|
||||
let persisted_settings = thread
|
||||
.config_snapshot()
|
||||
.await
|
||||
.into_thread_settings_snapshot();
|
||||
let persisted_settings = thread.thread_settings_snapshot().await;
|
||||
let items = [
|
||||
thread_settings_applied_item(persisted_settings.clone()),
|
||||
outcome.thread_goal_updated_item(),
|
||||
@@ -173,10 +170,7 @@ impl ThreadGoalRequestProcessor {
|
||||
Err(err) => Err(err),
|
||||
Ok(()) => {
|
||||
// Catch up a settings update queued while the rollout materialized.
|
||||
let current_settings = thread
|
||||
.config_snapshot()
|
||||
.await
|
||||
.into_thread_settings_snapshot();
|
||||
let current_settings = thread.thread_settings_snapshot().await;
|
||||
if current_settings == persisted_settings {
|
||||
Ok(())
|
||||
} else {
|
||||
|
||||
@@ -82,7 +82,7 @@ struct ThreadListFilters {
|
||||
|
||||
struct ThreadRevertRuntimeSnapshot {
|
||||
config: Config,
|
||||
settings: ThreadConfigSnapshot,
|
||||
settings: CodexThreadSettingsOverrides,
|
||||
client_mcp_extensions: ClientMcpExtensions,
|
||||
}
|
||||
|
||||
@@ -2019,7 +2019,7 @@ impl ThreadRequestProcessor {
|
||||
}
|
||||
let runtime_snapshot = ThreadRevertRuntimeSnapshot {
|
||||
config: thread.config().await.as_ref().clone(),
|
||||
settings: config_snapshot,
|
||||
settings: thread.restorable_thread_settings().await,
|
||||
client_mcp_extensions: thread.client_mcp_extensions(),
|
||||
};
|
||||
|
||||
@@ -4662,7 +4662,7 @@ impl ThreadRequestProcessor {
|
||||
restore_approval_policy || restore_approvals_reviewer || restore_permission_profile;
|
||||
let loaded_parent_settings = if paginated_source && needs_latest_settings {
|
||||
if let Ok(parent) = self.thread_manager.get_thread(source_thread_id).await {
|
||||
let snapshot = parent.config_snapshot().await;
|
||||
let snapshot = parent.thread_settings_snapshot().await;
|
||||
Some(PersistedResumeSettings {
|
||||
approval_policy: snapshot.approval_policy,
|
||||
approvals_reviewer: Some(snapshot.approvals_reviewer),
|
||||
|
||||
@@ -194,47 +194,6 @@ pub(crate) fn thread_settings_from_config_snapshot(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn thread_settings_from_core_snapshot(
|
||||
snapshot: codex_protocol::protocol::ThreadSettingsSnapshot,
|
||||
) -> ThreadSettings {
|
||||
let codex_protocol::protocol::ThreadSettingsSnapshot {
|
||||
model,
|
||||
model_provider_id,
|
||||
service_tier,
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
permission_profile,
|
||||
active_permission_profile,
|
||||
cwd,
|
||||
reasoning_effort,
|
||||
reasoning_summary,
|
||||
personality,
|
||||
collaboration_mode,
|
||||
} = snapshot;
|
||||
let sandbox_policy = codex_sandboxing::compatibility_sandbox_policy_for_permission_profile(
|
||||
&permission_profile,
|
||||
cwd.as_path(),
|
||||
)
|
||||
.into();
|
||||
ThreadSettings {
|
||||
sandbox_policy,
|
||||
cwd,
|
||||
approval_policy: approval_policy.into(),
|
||||
approvals_reviewer: approvals_reviewer.into(),
|
||||
active_permission_profile: thread_response_active_permission_profile(
|
||||
active_permission_profile,
|
||||
),
|
||||
model,
|
||||
model_provider: model_provider_id,
|
||||
service_tier,
|
||||
effort: reasoning_effort,
|
||||
summary: reasoning_summary,
|
||||
collaboration_mode,
|
||||
multi_agent_mode: MultiAgentMode::ExplicitRequestOnly,
|
||||
personality,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn parse_datetime(timestamp: Option<&str>) -> Option<DateTime<Utc>> {
|
||||
timestamp.and_then(|ts| {
|
||||
|
||||
@@ -5,7 +5,6 @@ use codex_agent_extension::AgentRunner;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::FunctionCallOutputContentItem;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AdditionalContextEntry as CoreAdditionalContextEntry;
|
||||
use codex_protocol::protocol::AdditionalContextKind as CoreAdditionalContextKind;
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
@@ -551,14 +550,6 @@ impl TurnRequestProcessor {
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let parent_permission_profile_override =
|
||||
thread_settings.permission_profile.clone().or_else(|| {
|
||||
thread_settings
|
||||
.sandbox_policy
|
||||
.as_ref()
|
||||
.map(PermissionProfile::from_legacy_sandbox_policy)
|
||||
});
|
||||
|
||||
let submission = thread
|
||||
.start_or_steer_turn(
|
||||
TurnInputRequest::new(TurnInput::UserInput {
|
||||
@@ -592,17 +583,17 @@ impl TurnRequestProcessor {
|
||||
|
||||
if turn_has_input && started {
|
||||
let config_snapshot = thread.config_snapshot().await;
|
||||
let parent_permission_profile =
|
||||
parent_permission_profile_override.unwrap_or(config_snapshot.permission_profile);
|
||||
codex_memories_write::start_memories_startup_task(
|
||||
Arc::clone(&self.thread_manager),
|
||||
Arc::clone(&self.auth_manager),
|
||||
thread_id,
|
||||
Arc::clone(&thread),
|
||||
thread.config().await,
|
||||
parent_permission_profile,
|
||||
&config_snapshot.session_source,
|
||||
);
|
||||
if config_snapshot.is_primary_environment_configured() {
|
||||
codex_memories_write::start_memories_startup_task(
|
||||
Arc::clone(&self.thread_manager),
|
||||
Arc::clone(&self.auth_manager),
|
||||
thread_id,
|
||||
Arc::clone(&thread),
|
||||
thread.config().await,
|
||||
config_snapshot.permission_profile,
|
||||
&config_snapshot.session_source,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
self.outgoing
|
||||
|
||||
@@ -144,6 +144,13 @@ impl V2Residency {
|
||||
self.touch(candidate_thread_id);
|
||||
continue;
|
||||
}
|
||||
let environments = candidate_thread.environment_selections().await;
|
||||
candidate_thread
|
||||
.session
|
||||
.services
|
||||
.agent_control
|
||||
.state
|
||||
.save_evicted_environments(candidate_thread_id, environments);
|
||||
let _ = manager.remove_thread(&candidate_thread_id).await;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -267,6 +267,7 @@ impl AgentControl {
|
||||
if self.state.agent_metadata_for_thread(thread_id).is_none() {
|
||||
return Err(CodexErr::ThreadNotFound(thread_id));
|
||||
}
|
||||
let environment_selections = self.state.evicted_environments(thread_id);
|
||||
|
||||
let stored_thread = state
|
||||
.read_stored_thread(ReadThreadParams {
|
||||
@@ -365,18 +366,21 @@ impl AgentControl {
|
||||
agent_control: self.clone(),
|
||||
session_source,
|
||||
parent_thread_id,
|
||||
environment_selections,
|
||||
inherited_environments,
|
||||
inherited_exec_policy,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(reloaded_thread) => {
|
||||
self.state.clear_evicted_environments(thread_id);
|
||||
residency_slot.commit(reloaded_thread.thread_id);
|
||||
state.notify_thread_created(reloaded_thread.thread_id);
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
if state.get_thread(thread_id).await.is_ok() {
|
||||
self.state.clear_evicted_environments(thread_id);
|
||||
drop(residency_slot);
|
||||
self.touch_loaded_v2_residency(&state, thread_id).await;
|
||||
return Ok(());
|
||||
@@ -1009,6 +1013,7 @@ impl AgentControl {
|
||||
agent_control: self.clone(),
|
||||
session_source,
|
||||
parent_thread_id,
|
||||
environment_selections: None,
|
||||
inherited_environments,
|
||||
inherited_exec_policy,
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::error::Result;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use rand::prelude::IndexedRandom;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
@@ -29,11 +30,25 @@ pub(crate) struct AgentRegistry {
|
||||
#[derive(Default)]
|
||||
struct ActiveAgents {
|
||||
agent_tree: HashMap<String, AgentMetadata>,
|
||||
thread_paths: HashMap<ThreadId, String>,
|
||||
thread_paths: HashMap<ThreadId, RegisteredAgent>,
|
||||
used_agent_nicknames: HashSet<String>,
|
||||
nickname_reset_count: usize,
|
||||
}
|
||||
|
||||
struct RegisteredAgent {
|
||||
path: String,
|
||||
evicted_environments: Option<Vec<TurnEnvironmentSelection>>,
|
||||
}
|
||||
|
||||
impl RegisteredAgent {
|
||||
fn new(path: String) -> Self {
|
||||
Self {
|
||||
path,
|
||||
evicted_environments: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct AgentMetadata {
|
||||
pub(crate) agent_id: Option<ThreadId>,
|
||||
@@ -108,7 +123,7 @@ impl AgentRegistry {
|
||||
active_agents
|
||||
.thread_paths
|
||||
.remove(&thread_id)
|
||||
.and_then(|key| active_agents.agent_tree.remove(key.as_str()))
|
||||
.and_then(|agent| active_agents.agent_tree.remove(agent.path.as_str()))
|
||||
.is_some_and(|metadata| {
|
||||
!metadata.agent_path.as_ref().is_some_and(AgentPath::is_root)
|
||||
})
|
||||
@@ -134,7 +149,9 @@ impl AgentRegistry {
|
||||
})
|
||||
.agent_id;
|
||||
if let Some(root_thread_id) = root_thread_id {
|
||||
active_agents.thread_paths.insert(root_thread_id, root_path);
|
||||
active_agents
|
||||
.thread_paths
|
||||
.insert(root_thread_id, RegisteredAgent::new(root_path));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,10 +172,48 @@ impl AgentRegistry {
|
||||
active_agents
|
||||
.thread_paths
|
||||
.get(&thread_id)
|
||||
.and_then(|path| active_agents.agent_tree.get(path))
|
||||
.and_then(|agent| active_agents.agent_tree.get(&agent.path))
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub(crate) fn save_evicted_environments(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
environments: Vec<TurnEnvironmentSelection>,
|
||||
) {
|
||||
let mut active_agents = self
|
||||
.active_agents
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if let Some(agent) = active_agents.thread_paths.get_mut(&thread_id) {
|
||||
agent.evicted_environments = Some(environments);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn evicted_environments(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
) -> Option<Vec<TurnEnvironmentSelection>> {
|
||||
let active_agents = self
|
||||
.active_agents
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
active_agents
|
||||
.thread_paths
|
||||
.get(&thread_id)
|
||||
.and_then(|agent| agent.evicted_environments.clone())
|
||||
}
|
||||
|
||||
pub(crate) fn clear_evicted_environments(&self, thread_id: ThreadId) {
|
||||
let mut active_agents = self
|
||||
.active_agents
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if let Some(agent) = active_agents.thread_paths.get_mut(&thread_id) {
|
||||
agent.evicted_environments = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn live_agents(&self) -> Vec<AgentMetadata> {
|
||||
self.active_agents
|
||||
.lock()
|
||||
@@ -189,10 +244,14 @@ impl AgentRegistry {
|
||||
if let Some(agent_nickname) = agent_metadata.agent_nickname.clone() {
|
||||
active_agents.used_agent_nicknames.insert(agent_nickname);
|
||||
}
|
||||
if let Some(previous_key) = active_agents.thread_paths.insert(thread_id, key.clone())
|
||||
&& previous_key != key
|
||||
if let Some(previous_agent) = active_agents
|
||||
.thread_paths
|
||||
.insert(thread_id, RegisteredAgent::new(key.clone()))
|
||||
&& previous_agent.path != key
|
||||
{
|
||||
active_agents.agent_tree.remove(previous_key.as_str());
|
||||
active_agents
|
||||
.agent_tree
|
||||
.remove(previous_agent.path.as_str());
|
||||
}
|
||||
if let Some(previous_metadata) = active_agents.agent_tree.insert(key, agent_metadata)
|
||||
&& let Some(previous_thread_id) = previous_metadata.agent_id
|
||||
|
||||
@@ -28,6 +28,7 @@ use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::EnvironmentConfig;
|
||||
use codex_protocol::protocol::EnvironmentConfigState;
|
||||
use codex_protocol::protocol::Event;
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
use codex_protocol::protocol::Op;
|
||||
@@ -102,46 +103,24 @@ impl ThreadConfigSnapshot {
|
||||
&self.environments.environments
|
||||
}
|
||||
|
||||
/// Whether the primary environment has resolved configuration, if one is selected.
|
||||
pub fn is_primary_environment_configured(&self) -> bool {
|
||||
self.environment_selections()
|
||||
.first()
|
||||
.is_none_or(|selection| {
|
||||
matches!(
|
||||
selection.config,
|
||||
EnvironmentConfigState::FromThread | EnvironmentConfigState::Ready(_)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn sandbox_policy(&self) -> SandboxPolicy {
|
||||
codex_sandboxing::compatibility_sandbox_policy_for_permission_profile(
|
||||
&self.permission_profile,
|
||||
self.cwd().as_path(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn into_thread_settings_snapshot(self) -> ThreadSettingsSnapshot {
|
||||
let cwd = self.cwd().clone();
|
||||
ThreadSettingsSnapshot {
|
||||
model: self.model,
|
||||
model_provider_id: self.model_provider_id,
|
||||
service_tier: self.service_tier,
|
||||
approval_policy: self.approval_policy,
|
||||
approvals_reviewer: self.approvals_reviewer,
|
||||
permission_profile: self.permission_profile,
|
||||
active_permission_profile: self.active_permission_profile,
|
||||
cwd,
|
||||
reasoning_effort: self.reasoning_effort,
|
||||
reasoning_summary: self.reasoning_summary,
|
||||
personality: self.personality,
|
||||
collaboration_mode: self.collaboration_mode,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_thread_settings_overrides(self) -> CodexThreadSettingsOverrides {
|
||||
CodexThreadSettingsOverrides {
|
||||
environments: Some(self.environments),
|
||||
profile_workspace_roots: Some(self.profile_workspace_roots),
|
||||
approval_policy: Some(self.approval_policy),
|
||||
approvals_reviewer: Some(self.approvals_reviewer),
|
||||
permission_profile: Some(self.permission_profile),
|
||||
active_permission_profile: self.active_permission_profile,
|
||||
summary: self.reasoning_summary,
|
||||
service_tier: Some(self.service_tier),
|
||||
collaboration_mode: Some(self.collaboration_mode),
|
||||
personality: self.personality,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Thread settings overrides that app-server validates before starting a turn.
|
||||
@@ -423,17 +402,15 @@ impl CodexThread {
|
||||
self.session.preview_settings(&updates).await
|
||||
}
|
||||
|
||||
/// Restores effective mutable settings captured from another loaded runtime.
|
||||
/// Restores thread-owned mutable settings captured from another loaded runtime.
|
||||
///
|
||||
/// Runtime replacement uses this after resume so clients keep their current thread settings
|
||||
/// rather than reverting to the original layer-backed config.
|
||||
pub async fn restore_thread_settings(
|
||||
&self,
|
||||
snapshot: ThreadConfigSnapshot,
|
||||
settings: CodexThreadSettingsOverrides,
|
||||
) -> ConstraintResult<()> {
|
||||
let updates = self
|
||||
.thread_settings_update(snapshot.into_thread_settings_overrides())
|
||||
.await;
|
||||
let updates = self.thread_settings_update(settings).await;
|
||||
self.session.update_settings(updates).await
|
||||
}
|
||||
|
||||
@@ -648,6 +625,16 @@ impl CodexThread {
|
||||
self.session.thread_config_snapshot().await
|
||||
}
|
||||
|
||||
/// Returns thread-owned settings suitable for rollout persistence and resume.
|
||||
pub async fn thread_settings_snapshot(&self) -> ThreadSettingsSnapshot {
|
||||
self.session.thread_settings_snapshot().await
|
||||
}
|
||||
|
||||
/// Captures thread-owned settings and environment selections for runtime restoration.
|
||||
pub async fn restorable_thread_settings(&self) -> CodexThreadSettingsOverrides {
|
||||
self.session.restorable_thread_settings().await
|
||||
}
|
||||
|
||||
/// Returns the MCP extensions declared by the client that created this runtime.
|
||||
pub fn client_mcp_extensions(&self) -> ClientMcpExtensions {
|
||||
self.session.services.client_mcp_extensions.clone()
|
||||
|
||||
@@ -14,6 +14,7 @@ use codex_exec_server::ExecutorFileSystem;
|
||||
use codex_exec_server::SelectedCapabilityRootsStatus;
|
||||
use codex_protocol::capabilities::CapabilityRootLocation;
|
||||
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::EnvironmentConfig;
|
||||
use codex_protocol::protocol::EnvironmentConfigState;
|
||||
use codex_protocol::protocol::EnvironmentConnectionEvent;
|
||||
@@ -360,6 +361,18 @@ impl ThreadEnvironments {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns installed owner configuration without treating pending attachments as ready.
|
||||
pub(crate) fn primary_config_for(
|
||||
selections: &[TurnEnvironmentSelection],
|
||||
) -> Option<&EnvironmentConfig> {
|
||||
match &selections.first()?.config {
|
||||
EnvironmentConfigState::Ready(config) => Some(config),
|
||||
EnvironmentConfigState::FromThread
|
||||
| EnvironmentConfigState::Pending
|
||||
| EnvironmentConfigState::Failed(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn primary_workspace_roots_for(
|
||||
selections: &[TurnEnvironmentSelection],
|
||||
) -> Vec<AbsolutePathBuf> {
|
||||
@@ -713,6 +726,16 @@ impl TurnEnvironmentSnapshot {
|
||||
self.turn_environments().next()
|
||||
}
|
||||
|
||||
/// Returns the primary environment's resolved permissions, or the provided fallback.
|
||||
pub(crate) fn permission_profile_or_else(
|
||||
&self,
|
||||
fallback: impl FnOnce() -> PermissionProfile,
|
||||
) -> PermissionProfile {
|
||||
self.primary()
|
||||
.map(TurnEnvironment::permission_profile_with_workspace_roots)
|
||||
.unwrap_or_else(fallback)
|
||||
}
|
||||
|
||||
pub(crate) fn local(&self) -> Option<&TurnEnvironment> {
|
||||
self.turn_environments()
|
||||
.find(|environment| !environment.environment.is_remote())
|
||||
|
||||
@@ -14,7 +14,6 @@ use crate::context::NodeReplReviewEvidenceMode;
|
||||
use crate::context::node_repl_review_evidence_mode;
|
||||
use crate::event_mapping::is_contextual_user_message_content;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn_context::TurnEnvironment;
|
||||
use codex_utils_output_truncation::TruncationPolicy;
|
||||
use codex_utils_output_truncation::approx_bytes_for_tokens;
|
||||
use codex_utils_output_truncation::approx_token_count;
|
||||
@@ -302,9 +301,9 @@ fn parent_turn_denied_reads_context(context: &GuardianReviewContext) -> Option<S
|
||||
let cwd = environment
|
||||
.and_then(|environment| environment.cwd().to_abs_path().ok())
|
||||
.unwrap_or_else(|| turn.cwd.clone());
|
||||
let permission_profile = environment
|
||||
.map(TurnEnvironment::permission_profile_with_workspace_roots)
|
||||
.unwrap_or_else(|| turn.permission_profile());
|
||||
let permission_profile = context
|
||||
.environments()
|
||||
.permission_profile_or_else(|| turn.permission_profile());
|
||||
let file_system_policy = permission_profile.file_system_sandbox_policy();
|
||||
let mut entries = file_system_policy
|
||||
.get_unreadable_roots_with_cwd(&cwd)
|
||||
|
||||
@@ -88,9 +88,7 @@ fn approval_metadata(
|
||||
}
|
||||
|
||||
fn approval_config(turn_context: &TurnContext) -> codex_mcp::McpConfig {
|
||||
let mut config = (*mcp_config_for_test(&turn_context.config)).clone();
|
||||
config.permission_profile = turn_context.permission_profile();
|
||||
config
|
||||
(*mcp_config_for_test(&turn_context.config)).clone()
|
||||
}
|
||||
|
||||
fn mcp_turn_metadata_context(turn_context: &TurnContext) -> McpTurnMetadataContext<'_> {
|
||||
|
||||
@@ -139,6 +139,12 @@ impl Session {
|
||||
};
|
||||
|
||||
environment.config = config;
|
||||
if matches!(environment.config, EnvironmentConfigState::Ready(_)) {
|
||||
state
|
||||
.session_configuration
|
||||
.validate(&environments)
|
||||
.map_err(|error| CodexErr::InvalidRequest(error.to_string()))?;
|
||||
}
|
||||
|
||||
// Invalidate MCP before installed configuration can wake a waiting turn.
|
||||
self.mark_mcp_runtime_dirty();
|
||||
|
||||
@@ -123,6 +123,7 @@ use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_protocol::protocol::ThreadHistoryMode;
|
||||
use codex_protocol::protocol::ThreadSettingsOverrides;
|
||||
use codex_protocol::protocol::ThreadSettingsSnapshot;
|
||||
use codex_protocol::protocol::ThreadSource;
|
||||
use codex_protocol::protocol::TurnAbortReason;
|
||||
use codex_protocol::protocol::TurnContextItem;
|
||||
@@ -178,6 +179,7 @@ use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::client::ModelClient;
|
||||
use crate::codex_thread::CodexThreadSettingsOverrides;
|
||||
use crate::codex_thread::ThreadConfigSnapshot;
|
||||
#[cfg(test)]
|
||||
use crate::compact::collect_user_messages;
|
||||
@@ -1652,6 +1654,20 @@ impl Session {
|
||||
.thread_config_snapshot(self.services.turn_environments.selections())
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_settings_snapshot(&self) -> ThreadSettingsSnapshot {
|
||||
let state = self.state.lock().await;
|
||||
state
|
||||
.session_configuration
|
||||
.thread_settings_snapshot(&self.services.turn_environments.selections())
|
||||
}
|
||||
|
||||
pub(crate) async fn restorable_thread_settings(&self) -> CodexThreadSettingsOverrides {
|
||||
let state = self.state.lock().await;
|
||||
state
|
||||
.session_configuration
|
||||
.restorable_thread_settings(self.services.turn_environments.selections())
|
||||
}
|
||||
|
||||
pub(crate) async fn set_app_server_client_info(
|
||||
&self,
|
||||
app_server_client_name: Option<String>,
|
||||
@@ -3162,9 +3178,8 @@ impl Session {
|
||||
if !discovery.sandbox_contexts().is_empty() {
|
||||
extension_data.insert(discovery.sandbox_contexts().clone());
|
||||
}
|
||||
} else if !turn_context
|
||||
.config
|
||||
.permissions
|
||||
} else if !environments
|
||||
.permission_profile_or_else(|| turn_context.permission_profile())
|
||||
.file_system_sandbox_policy()
|
||||
.has_full_disk_read_access()
|
||||
{
|
||||
|
||||
@@ -137,10 +137,6 @@ impl SessionConfiguration {
|
||||
&self.codex_home
|
||||
}
|
||||
|
||||
pub(super) fn permission_profile_state(&self) -> &PermissionProfileState {
|
||||
&self.permission_profile_state
|
||||
}
|
||||
|
||||
pub(super) fn inferred_environment_config(&self) -> EnvironmentConfig {
|
||||
EnvironmentConfig {
|
||||
allow_login_shell: self.allow_login_shell,
|
||||
@@ -170,10 +166,6 @@ impl SessionConfiguration {
|
||||
self.permission_profile_state.active_permission_profile()
|
||||
}
|
||||
|
||||
pub(super) fn profile_workspace_roots(&self) -> &[AbsolutePathBuf] {
|
||||
self.permission_profile_state.profile_workspace_roots()
|
||||
}
|
||||
|
||||
pub(super) fn apply_permission_profile_to_permissions(
|
||||
&self,
|
||||
permissions: &mut crate::config::Permissions,
|
||||
@@ -221,20 +213,26 @@ impl SessionConfiguration {
|
||||
) -> ThreadConfigSnapshot {
|
||||
let workspace_roots =
|
||||
ThreadEnvironments::primary_workspace_roots_for(&environment_selections);
|
||||
let permission_profile = ThreadEnvironments::primary_config_for(&environment_selections)
|
||||
.map(|config| config.permission_profile.clone())
|
||||
.unwrap_or_else(|| self.permission_profile_state.snapshot());
|
||||
ThreadConfigSnapshot {
|
||||
model: self.collaboration_mode.model().to_string(),
|
||||
model_provider_id: self.original_config_do_not_use.model_provider_id.clone(),
|
||||
service_tier: self.service_tier.clone(),
|
||||
approval_policy: self.approval_policy.value(),
|
||||
approvals_reviewer: self.approvals_reviewer,
|
||||
permission_profile: self.materialized_permission_profile(&environment_selections),
|
||||
active_permission_profile: self.active_permission_profile(),
|
||||
permission_profile: permission_profile
|
||||
.permission_profile()
|
||||
.clone()
|
||||
.materialize_project_roots_with_workspace_roots(&workspace_roots),
|
||||
active_permission_profile: permission_profile.active_permission_profile(),
|
||||
environments: TurnEnvironmentSelections::new(
|
||||
self.legacy_fallback_cwd.clone(),
|
||||
environment_selections,
|
||||
),
|
||||
workspace_roots,
|
||||
profile_workspace_roots: self.profile_workspace_roots().to_vec(),
|
||||
profile_workspace_roots: permission_profile.profile_workspace_roots().to_vec(),
|
||||
ephemeral: self.original_config_do_not_use.ephemeral,
|
||||
reasoning_effort: self.collaboration_mode.reasoning_effort(),
|
||||
reasoning_summary: self.model_reasoning_summary,
|
||||
@@ -249,6 +247,54 @@ impl SessionConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
/// Captures thread-owned settings for persistence and resume.
|
||||
pub(super) fn thread_settings_snapshot(
|
||||
&self,
|
||||
environment_selections: &[TurnEnvironmentSelection],
|
||||
) -> ThreadSettingsSnapshot {
|
||||
ThreadSettingsSnapshot {
|
||||
model: self.collaboration_mode.model().to_string(),
|
||||
model_provider_id: self.original_config_do_not_use.model_provider_id.clone(),
|
||||
service_tier: self.service_tier.clone(),
|
||||
approval_policy: self.approval_policy.value(),
|
||||
approvals_reviewer: self.approvals_reviewer,
|
||||
permission_profile: self.materialized_permission_profile(environment_selections),
|
||||
active_permission_profile: self.active_permission_profile(),
|
||||
cwd: self.legacy_fallback_cwd.clone(),
|
||||
reasoning_effort: self.collaboration_mode.reasoning_effort(),
|
||||
reasoning_summary: self.model_reasoning_summary,
|
||||
personality: self.personality,
|
||||
collaboration_mode: self.collaboration_mode.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Captures thread-owned settings and their separately owned environments.
|
||||
pub(super) fn restorable_thread_settings(
|
||||
&self,
|
||||
environment_selections: Vec<TurnEnvironmentSelection>,
|
||||
) -> CodexThreadSettingsOverrides {
|
||||
CodexThreadSettingsOverrides {
|
||||
environments: Some(TurnEnvironmentSelections::new(
|
||||
self.legacy_fallback_cwd.clone(),
|
||||
environment_selections,
|
||||
)),
|
||||
profile_workspace_roots: Some(
|
||||
self.permission_profile_state
|
||||
.profile_workspace_roots()
|
||||
.to_vec(),
|
||||
),
|
||||
approval_policy: Some(self.approval_policy.value()),
|
||||
approvals_reviewer: Some(self.approvals_reviewer),
|
||||
permission_profile: Some(self.permission_profile()),
|
||||
active_permission_profile: self.active_permission_profile(),
|
||||
summary: self.model_reasoning_summary,
|
||||
service_tier: Some(self.service_tier.clone()),
|
||||
collaboration_mode: Some(self.collaboration_mode.clone()),
|
||||
personality: self.personality,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn validate(
|
||||
&self,
|
||||
environments: &[TurnEnvironmentSelection],
|
||||
@@ -258,7 +304,7 @@ impl SessionConfiguration {
|
||||
ConstraintError::InvalidValue {
|
||||
field_name: "environments",
|
||||
candidate: "environment configuration".to_string(),
|
||||
allowed: format!("valid selected capability roots ({error})"),
|
||||
allowed: format!("valid environment configuration ({error})"),
|
||||
requirement_source: codex_config::RequirementSource::Unknown,
|
||||
}
|
||||
})
|
||||
@@ -281,9 +327,16 @@ impl SessionConfiguration {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let permission_profile = ThreadEnvironments::primary_config_for(environments)
|
||||
.map(|config| config.permission_profile.permission_profile())
|
||||
.unwrap_or_else(|| self.permission_profile_state.permission_profile())
|
||||
.clone()
|
||||
.materialize_project_roots_with_workspace_roots(
|
||||
&ThreadEnvironments::primary_workspace_roots_for(environments),
|
||||
);
|
||||
if self.approvals_reviewer == ApprovalsReviewer::AutoReview
|
||||
&& !self
|
||||
.file_system_sandbox_policy(environments)
|
||||
&& !permission_profile
|
||||
.file_system_sandbox_policy()
|
||||
.has_full_disk_write_access()
|
||||
&& self
|
||||
.original_config_do_not_use
|
||||
@@ -1392,33 +1445,32 @@ impl Session {
|
||||
// Dispatch the SessionConfiguredEvent first and then report any errors.
|
||||
// If resuming, include converted initial messages in the payload so UIs can render them immediately.
|
||||
let initial_messages = initial_history.get_event_msgs();
|
||||
let thread_config =
|
||||
session_configuration.thread_config_snapshot(turn_environments.selections());
|
||||
let events = std::iter::once(Event {
|
||||
id: INITIAL_SUBMIT_ID.to_owned(),
|
||||
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
|
||||
session_id,
|
||||
thread_id,
|
||||
forked_from_id,
|
||||
parent_thread_id,
|
||||
thread_source: session_configuration.thread_source.clone(),
|
||||
cwd: thread_config.cwd().clone(),
|
||||
forked_from_id: thread_config.forked_from_thread_id,
|
||||
parent_thread_id: thread_config.parent_thread_id,
|
||||
thread_source: thread_config.thread_source,
|
||||
thread_name: session_configuration.thread_name.clone(),
|
||||
model: session_configuration.collaboration_mode.model().to_string(),
|
||||
model_provider_id: config.model_provider_id.clone(),
|
||||
service_tier: session_configuration.service_tier.clone(),
|
||||
approval_policy: session_configuration.approval_policy.value(),
|
||||
approvals_reviewer: session_configuration.approvals_reviewer,
|
||||
permission_profile: session_configuration
|
||||
.materialized_permission_profile(environment_selections),
|
||||
active_permission_profile: session_configuration.active_permission_profile(),
|
||||
cwd: session_configuration.cwd().clone(),
|
||||
reasoning_effort: session_configuration.collaboration_mode.reasoning_effort(),
|
||||
initial_messages,
|
||||
model: thread_config.model,
|
||||
model_provider_id: thread_config.model_provider_id,
|
||||
service_tier: thread_config.service_tier,
|
||||
approval_policy: thread_config.approval_policy,
|
||||
approvals_reviewer: thread_config.approvals_reviewer,
|
||||
network_proxy: session_network_proxy.filter(|_| {
|
||||
Self::managed_network_proxy_active_for_permission_profile(
|
||||
session_configuration
|
||||
.permission_profile_state()
|
||||
.permission_profile(),
|
||||
&thread_config.permission_profile,
|
||||
)
|
||||
}),
|
||||
permission_profile: thread_config.permission_profile,
|
||||
active_permission_profile: thread_config.active_permission_profile,
|
||||
reasoning_effort: thread_config.reasoning_effort,
|
||||
initial_messages,
|
||||
rollout_path,
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -9592,14 +9592,15 @@ async fn turn_context_item_omits_legacy_equivalent_file_system_sandbox_policy()
|
||||
async fn turn_context_item_stores_active_permission_profile() {
|
||||
let (_session, mut turn_context) = make_session_and_context().await;
|
||||
let active_permission_profile = ActivePermissionProfile::read_only();
|
||||
let config = Arc::make_mut(&mut turn_context.config);
|
||||
config
|
||||
.permissions
|
||||
.set_permission_profile_from_session_snapshot(PermissionProfileSnapshot::active(
|
||||
PermissionProfile::read_only(),
|
||||
active_permission_profile.clone(),
|
||||
))
|
||||
.expect("test setup should allow updating permission profile");
|
||||
let TurnEnvironmentState::Ready(environment) = &mut turn_context.environments.environments[0]
|
||||
else {
|
||||
panic!("turn environment should be ready");
|
||||
};
|
||||
environment.config_origin = EnvironmentConfigOrigin::Owner;
|
||||
environment.config_mut().permission_profile = PermissionProfileSnapshot::active(
|
||||
PermissionProfile::read_only(),
|
||||
active_permission_profile.clone(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
turn_context
|
||||
@@ -9618,10 +9619,12 @@ async fn turn_context_item_stores_split_file_system_sandbox_policy_when_differen
|
||||
&file_system_sandbox_policy,
|
||||
turn_context.network_sandbox_policy(),
|
||||
);
|
||||
Arc::make_mut(&mut turn_context.config)
|
||||
.permissions
|
||||
.set_permission_profile(permission_profile)
|
||||
.expect("test setup should allow updating permission profile");
|
||||
let TurnEnvironmentState::Ready(environment) = &mut turn_context.environments.environments[0]
|
||||
else {
|
||||
panic!("turn environment should be ready");
|
||||
};
|
||||
environment.config_mut().permission_profile =
|
||||
PermissionProfileSnapshot::legacy(permission_profile);
|
||||
|
||||
let item = turn_context.to_turn_context_item();
|
||||
|
||||
@@ -9791,10 +9794,12 @@ async fn record_context_updates_and_set_reference_context_item_persists_split_fi
|
||||
&file_system_sandbox_policy,
|
||||
turn_context.network_sandbox_policy(),
|
||||
);
|
||||
Arc::make_mut(&mut turn_context.config)
|
||||
.permissions
|
||||
.set_permission_profile(permission_profile)
|
||||
.expect("test setup should allow updating permission profile");
|
||||
let TurnEnvironmentState::Ready(environment) = &mut turn_context.environments.environments[0]
|
||||
else {
|
||||
panic!("turn environment should be ready");
|
||||
};
|
||||
environment.config_mut().permission_profile =
|
||||
PermissionProfileSnapshot::legacy(permission_profile);
|
||||
let rollout_path = attach_thread_persistence(&mut session).await;
|
||||
|
||||
let turn_context = Arc::new(turn_context);
|
||||
|
||||
@@ -83,7 +83,7 @@ pub(super) async fn prepare_update(
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies persistent settings and emits the resulting effective snapshot.
|
||||
/// Applies persistent settings and emits the resulting thread-owned snapshot.
|
||||
pub(super) async fn apply_update(
|
||||
session: &Session,
|
||||
submission_id: String,
|
||||
@@ -94,7 +94,7 @@ pub(super) async fn apply_update(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Emits the effective thread settings after a successful update.
|
||||
/// Emits the thread-owned settings after a successful update.
|
||||
pub(super) async fn emit_applied(session: &Session, submission_id: String) {
|
||||
let msg = applied_event(session).await;
|
||||
session
|
||||
@@ -105,11 +105,10 @@ pub(super) async fn emit_applied(session: &Session, submission_id: String) {
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Builds the effective thread-settings event used by live updates and
|
||||
/// Builds the thread-owned settings event used by live updates and
|
||||
/// synthesized fork history.
|
||||
pub(super) async fn applied_event(session: &Session) -> EventMsg {
|
||||
let snapshot = session.thread_config_snapshot().await;
|
||||
EventMsg::ThreadSettingsApplied(ThreadSettingsAppliedEvent {
|
||||
thread_settings: snapshot.into_thread_settings_snapshot(),
|
||||
thread_settings: session.thread_settings_snapshot().await,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -278,21 +278,49 @@ impl TurnContext {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the selected environment's permissions, or the thread's permissions when none is ready.
|
||||
pub(crate) fn permission_profile(&self) -> PermissionProfile {
|
||||
self.config.permissions.effective_permission_profile()
|
||||
self.environments
|
||||
.permission_profile_or_else(|| self.config.permissions.effective_permission_profile())
|
||||
}
|
||||
|
||||
pub(crate) fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy {
|
||||
self.config.permissions.file_system_sandbox_policy()
|
||||
self.permission_profile().file_system_sandbox_policy()
|
||||
}
|
||||
|
||||
pub(crate) fn network_sandbox_policy(&self) -> NetworkSandboxPolicy {
|
||||
self.config.permissions.network_sandbox_policy()
|
||||
self.permission_profile().network_sandbox_policy()
|
||||
}
|
||||
|
||||
pub(crate) fn sandbox_policy(&self) -> SandboxPolicy {
|
||||
#[allow(deprecated)]
|
||||
self.config.permissions.legacy_sandbox_policy(&self.cwd)
|
||||
codex_sandboxing::compatibility_sandbox_policy_for_permission_profile(
|
||||
&self.permission_profile(),
|
||||
&self.cwd,
|
||||
)
|
||||
}
|
||||
|
||||
/// Combines the selected environment's workspace roots with its permission profile roots.
|
||||
pub(crate) fn effective_workspace_roots(&self) -> Vec<AbsolutePathBuf> {
|
||||
let Some(environment) = self.environments.primary() else {
|
||||
return self.config.effective_workspace_roots();
|
||||
};
|
||||
|
||||
let mut workspace_roots = environment
|
||||
.workspace_roots()
|
||||
.iter()
|
||||
.filter_map(|root| root.to_abs_path().ok())
|
||||
.collect::<Vec<_>>();
|
||||
for root in environment
|
||||
.config()
|
||||
.permission_profile
|
||||
.profile_workspace_roots()
|
||||
{
|
||||
if !workspace_roots.contains(root) {
|
||||
workspace_roots.push(root.clone());
|
||||
}
|
||||
}
|
||||
workspace_roots
|
||||
}
|
||||
|
||||
pub(crate) fn effective_reasoning_effort(&self) -> Option<ReasoningEffortConfig> {
|
||||
@@ -465,7 +493,7 @@ impl TurnContext {
|
||||
}
|
||||
|
||||
pub(crate) fn to_turn_context_item(&self) -> TurnContextItem {
|
||||
let workspace_roots = self.config.effective_workspace_roots();
|
||||
let workspace_roots = self.effective_workspace_roots();
|
||||
#[allow(deprecated)]
|
||||
let cwd = self.cwd.clone();
|
||||
TurnContextItem {
|
||||
@@ -478,7 +506,10 @@ impl TurnContext {
|
||||
approvals_reviewer: Some(self.config.approvals_reviewer),
|
||||
sandbox_policy: self.sandbox_policy(),
|
||||
permission_profile: Some(self.permission_profile()),
|
||||
active_permission_profile: self.config.permissions.active_permission_profile(),
|
||||
active_permission_profile: self.environments.primary().map_or_else(
|
||||
|| self.config.permissions.active_permission_profile(),
|
||||
TurnEnvironment::active_permission_profile,
|
||||
),
|
||||
network: self.turn_context_network_item(),
|
||||
file_system_sandbox_policy: self.non_legacy_file_system_sandbox_policy(),
|
||||
model: self.model_info.slug.clone(),
|
||||
@@ -629,7 +660,9 @@ impl Session {
|
||||
per_turn_config.features.enabled(Feature::FastMode),
|
||||
&model_info,
|
||||
);
|
||||
let permission_profile = per_turn_config.permissions.effective_permission_profile();
|
||||
let permission_profile = environments.permission_profile_or_else(|| {
|
||||
per_turn_config.permissions.effective_permission_profile()
|
||||
});
|
||||
let auto_review_enabled = crate::guardian::routes_approval_policy_to_guardian(
|
||||
per_turn_config.permissions.approval_policy.value(),
|
||||
per_turn_config.approvals_reviewer,
|
||||
@@ -837,6 +870,10 @@ impl Session {
|
||||
.and_then(|turn_environment| turn_environment.cwd().to_abs_path().ok())
|
||||
.unwrap_or_else(|| session_configuration.cwd().clone());
|
||||
let per_turn_config = self.build_per_turn_config(&session_configuration, cwd.clone());
|
||||
let network_permission_profile = primary_turn_environment
|
||||
.map(TurnEnvironment::permission_profile)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| session_configuration.permission_profile());
|
||||
let model_info = self
|
||||
.services
|
||||
.models_manager
|
||||
@@ -902,7 +939,7 @@ impl Session {
|
||||
.as_ref()
|
||||
.and_then(|started_proxy| {
|
||||
Self::managed_network_proxy_active_for_permission_profile(
|
||||
&session_configuration.permission_profile(),
|
||||
&network_permission_profile,
|
||||
)
|
||||
.then(|| started_proxy.proxy())
|
||||
}),
|
||||
|
||||
@@ -148,19 +148,9 @@ impl Session {
|
||||
.current_for_prefix_rules(turn_context.allow_prefix_rules());
|
||||
if turn_context.config.include_permissions_instructions {
|
||||
let environment = step_context.environments.primary();
|
||||
let permission_profile = environment
|
||||
.map(|environment| {
|
||||
let workspace_roots = environment
|
||||
.workspace_roots()
|
||||
.iter()
|
||||
.filter_map(|workspace_root| workspace_root.to_abs_path().ok())
|
||||
.collect::<Vec<_>>();
|
||||
environment
|
||||
.permission_profile()
|
||||
.clone()
|
||||
.materialize_project_roots_with_workspace_roots(&workspace_roots)
|
||||
})
|
||||
.unwrap_or_else(|| turn_context.permission_profile());
|
||||
let permission_profile = step_context
|
||||
.environments
|
||||
.permission_profile_or_else(|| turn_context.permission_profile());
|
||||
#[allow(deprecated)]
|
||||
let cwd = environment
|
||||
.and_then(|environment| environment.cwd().to_abs_path().ok())
|
||||
|
||||
@@ -217,7 +217,7 @@ pub(crate) async fn execute_user_shell_command(
|
||||
capture_policy: ExecCapturePolicy::ShellTool,
|
||||
sandbox: SandboxType::None,
|
||||
windows_sandbox_policy_cwd: cwd.clone().into(),
|
||||
windows_sandbox_workspace_roots: turn_context.config.effective_workspace_roots(),
|
||||
windows_sandbox_workspace_roots: turn_context.effective_workspace_roots(),
|
||||
windows_sandbox_level: turn_context.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: turn_context
|
||||
.config
|
||||
|
||||
@@ -325,6 +325,7 @@ pub(crate) struct ResumeThreadWithHistoryOptions {
|
||||
pub(crate) agent_control: AgentControl,
|
||||
pub(crate) session_source: SessionSource,
|
||||
pub(crate) parent_thread_id: Option<ThreadId>,
|
||||
pub(crate) environment_selections: Option<Vec<TurnEnvironmentSelection>>,
|
||||
pub(crate) inherited_environments: Option<TurnEnvironmentSnapshot>,
|
||||
pub(crate) inherited_exec_policy: Option<Arc<crate::exec_policy::ExecPolicyManager>>,
|
||||
}
|
||||
@@ -1682,15 +1683,17 @@ impl ThreadManagerState {
|
||||
agent_control,
|
||||
session_source,
|
||||
parent_thread_id,
|
||||
environment_selections,
|
||||
inherited_environments,
|
||||
inherited_exec_policy,
|
||||
} = options;
|
||||
let client_mcp_extensions = self.client_mcp_extensions_for_child(parent_thread_id).await;
|
||||
let thread_source = initial_history.get_resumed_thread_source();
|
||||
let environments = inherited_environments
|
||||
.as_ref()
|
||||
.filter(|_| initial_history.get_multi_agent_version() == Some(MultiAgentVersion::V2))
|
||||
.map(TurnEnvironmentSnapshot::to_selections);
|
||||
let environments = environment_selections.or_else(|| {
|
||||
inherited_environments
|
||||
.as_ref()
|
||||
.map(TurnEnvironmentSnapshot::to_selections)
|
||||
});
|
||||
let options = StartThreadOptions {
|
||||
initial_history,
|
||||
session_source: Some(session_source),
|
||||
|
||||
@@ -2,7 +2,6 @@ use super::*;
|
||||
use crate::agent::next_thread_spawn_depth;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::session::turn_context::TurnEnvironment;
|
||||
use crate::tools::handlers::multi_agents_spec::create_resume_agent_tool;
|
||||
use codex_tools::ToolSpec;
|
||||
use std::sync::Arc;
|
||||
@@ -36,7 +35,6 @@ async fn handle_resume_agent(
|
||||
let ToolInvocation {
|
||||
session,
|
||||
turn,
|
||||
step_context,
|
||||
payload,
|
||||
call_id,
|
||||
..
|
||||
@@ -90,7 +88,6 @@ async fn handle_resume_agent(
|
||||
match Box::pin(try_resume_closed_agent(
|
||||
&session,
|
||||
&turn,
|
||||
step_context.environments.primary(),
|
||||
receiver_thread_id,
|
||||
child_depth,
|
||||
))
|
||||
@@ -191,11 +188,10 @@ impl ToolOutput for ResumeAgentResult {
|
||||
async fn try_resume_closed_agent(
|
||||
session: &Arc<Session>,
|
||||
turn: &Arc<TurnContext>,
|
||||
environment: Option<&TurnEnvironment>,
|
||||
receiver_thread_id: ThreadId,
|
||||
child_depth: i32,
|
||||
) -> Result<(), FunctionCallError> {
|
||||
let config = build_agent_resume_config(turn.as_ref(), environment)?;
|
||||
let config = build_agent_resume_config(turn.as_ref())?;
|
||||
Box::pin(session.services.agent_control.resume_agent_from_rollout(
|
||||
config,
|
||||
receiver_thread_id,
|
||||
|
||||
@@ -34,7 +34,6 @@ impl Handler {
|
||||
let ToolInvocation {
|
||||
session,
|
||||
turn,
|
||||
step_context,
|
||||
payload,
|
||||
call_id,
|
||||
..
|
||||
@@ -49,8 +48,7 @@ impl Handler {
|
||||
.agent_control
|
||||
.get_agent_metadata(receiver_thread_id);
|
||||
if receiver_agent.is_some() {
|
||||
let resume_config =
|
||||
build_agent_resume_config(turn.as_ref(), step_context.environments.primary())?;
|
||||
let resume_config = build_agent_resume_config(turn.as_ref())?;
|
||||
session
|
||||
.services
|
||||
.agent_control
|
||||
|
||||
@@ -86,11 +86,8 @@ async fn handle_spawn_agent(
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let mut config = build_agent_spawn_config(
|
||||
&session.get_base_instructions().await,
|
||||
turn.as_ref(),
|
||||
step_context.environments.primary(),
|
||||
)?;
|
||||
let mut config =
|
||||
build_agent_spawn_config(&session.get_base_instructions().await, turn.as_ref())?;
|
||||
if let Some(service_tier) = args.service_tier.as_ref() {
|
||||
config.service_tier = Some(service_tier.clone());
|
||||
}
|
||||
@@ -115,11 +112,7 @@ async fn handle_spawn_agent(
|
||||
args.service_tier.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
apply_spawn_agent_runtime_overrides(
|
||||
&mut config,
|
||||
turn.as_ref(),
|
||||
step_context.environments.primary(),
|
||||
)?;
|
||||
apply_spawn_agent_runtime_overrides(&mut config, turn.as_ref())?;
|
||||
|
||||
let result = Box::pin(session.services.agent_control.spawn_agent_with_metadata(
|
||||
config,
|
||||
|
||||
@@ -5,7 +5,6 @@ use crate::config::HARD_MAX_MULTI_AGENT_V2_TIMEOUT_MS;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::session::turn_context::TurnEnvironment;
|
||||
use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::context::ToolOutput;
|
||||
use crate::tools::context::ToolPayload;
|
||||
@@ -170,36 +169,29 @@ pub(crate) fn parse_collab_input(
|
||||
/// Builds the base config snapshot for a newly spawned sub-agent.
|
||||
///
|
||||
/// The returned config starts from the parent's effective config and then refreshes the
|
||||
/// runtime-owned fields carried by the turn and selected environment, including model selection,
|
||||
/// reasoning settings, approval policy, sandbox, and cwd. Role-specific overrides are layered
|
||||
/// runtime-owned fields carried by the turn, including model selection, reasoning settings,
|
||||
/// approval policy, sandbox, and cwd. Role-specific overrides are layered
|
||||
/// after this step; skipping this helper and cloning stale config state directly can send the child
|
||||
/// agent out with the wrong provider or runtime policy.
|
||||
pub(crate) fn build_agent_spawn_config(
|
||||
base_instructions: &BaseInstructions,
|
||||
turn: &TurnContext,
|
||||
environment: Option<&TurnEnvironment>,
|
||||
) -> Result<Config, FunctionCallError> {
|
||||
let mut config = build_agent_shared_config(turn, environment)?;
|
||||
let mut config = build_agent_shared_config(turn)?;
|
||||
config.base_instructions = Some(base_instructions.text.clone());
|
||||
config.base_instructions_provenance = base_instructions.provenance.clone();
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub(crate) fn build_agent_resume_config(
|
||||
turn: &TurnContext,
|
||||
environment: Option<&TurnEnvironment>,
|
||||
) -> Result<Config, FunctionCallError> {
|
||||
let mut config = build_agent_shared_config(turn, environment)?;
|
||||
pub(crate) fn build_agent_resume_config(turn: &TurnContext) -> Result<Config, FunctionCallError> {
|
||||
let mut config = build_agent_shared_config(turn)?;
|
||||
// For resume, keep base instructions sourced from rollout/session metadata.
|
||||
config.base_instructions = None;
|
||||
config.base_instructions_provenance = None;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn build_agent_shared_config(
|
||||
turn: &TurnContext,
|
||||
environment: Option<&TurnEnvironment>,
|
||||
) -> Result<Config, FunctionCallError> {
|
||||
fn build_agent_shared_config(turn: &TurnContext) -> Result<Config, FunctionCallError> {
|
||||
let base_config = turn.config.clone();
|
||||
let mut config = (*base_config).clone();
|
||||
config.model = Some(turn.model_info.slug.clone());
|
||||
@@ -219,7 +211,7 @@ fn build_agent_shared_config(
|
||||
{
|
||||
config.developer_instructions = Some(developer_instructions);
|
||||
}
|
||||
apply_spawn_agent_runtime_overrides(&mut config, turn, environment)?;
|
||||
apply_spawn_agent_runtime_overrides(&mut config, turn)?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
@@ -237,13 +229,11 @@ pub(crate) fn reject_full_fork_agent_type_override(
|
||||
|
||||
/// Copies runtime-only turn state onto a child config before it is handed to `AgentControl`.
|
||||
///
|
||||
/// These values are chosen by the live turn and selected environment rather than persisted config,
|
||||
/// so leaving them stale can make a child agent disagree with its parent about approval policy,
|
||||
/// cwd, or sandboxing.
|
||||
/// These values are chosen by the live turn rather than persisted config, so leaving them stale can
|
||||
/// make a child agent disagree with its parent about approval policy, cwd, or sandboxing.
|
||||
pub(crate) fn apply_spawn_agent_runtime_overrides(
|
||||
config: &mut Config,
|
||||
turn: &TurnContext,
|
||||
environment: Option<&TurnEnvironment>,
|
||||
) -> Result<(), FunctionCallError> {
|
||||
config
|
||||
.permissions
|
||||
@@ -256,12 +246,14 @@ pub(crate) fn apply_spawn_agent_runtime_overrides(
|
||||
#[allow(deprecated)]
|
||||
let turn_cwd = turn.cwd.clone();
|
||||
config.cwd = turn_cwd;
|
||||
let permission_profile = environment
|
||||
.map(|environment| environment.permission_profile().clone())
|
||||
.unwrap_or_else(|| turn.permission_profile());
|
||||
config
|
||||
.permissions
|
||||
.set_permission_profile(permission_profile)
|
||||
.set_permission_profile_from_session_snapshot(
|
||||
turn.config
|
||||
.permissions
|
||||
.permission_profile_state()
|
||||
.snapshot(),
|
||||
)
|
||||
.map_err(|err| {
|
||||
FunctionCallError::RespondToModel(format!("permission_profile is invalid: {err}"))
|
||||
})?;
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::ThreadManager;
|
||||
use crate::config::AgentRoleConfig;
|
||||
use crate::config::DEFAULT_AGENT_MAX_DEPTH;
|
||||
use crate::config::PermissionProfileSnapshot;
|
||||
use crate::environment_selection::EnvironmentConfigOrigin;
|
||||
use crate::environment_selection::TurnEnvironmentState;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::init_state_db;
|
||||
@@ -2301,6 +2302,7 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() {
|
||||
};
|
||||
environment.config_mut().permission_profile =
|
||||
PermissionProfileSnapshot::legacy(expected_permission_profile.clone());
|
||||
environment.config_origin = EnvironmentConfigOrigin::Owner;
|
||||
assert_ne!(
|
||||
role_config.permissions.effective_permission_profile(),
|
||||
expected_permission_profile,
|
||||
@@ -2308,7 +2310,7 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() {
|
||||
);
|
||||
assert_ne!(
|
||||
expected_permission_profile,
|
||||
turn.permission_profile(),
|
||||
turn.config.permissions.effective_permission_profile(),
|
||||
"test requires an environment profile that differs from the thread profile"
|
||||
);
|
||||
|
||||
@@ -2360,6 +2362,10 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() {
|
||||
expected_network_sandbox_policy
|
||||
);
|
||||
assert_eq!(child_turn.permission_profile(), expected_permission_profile);
|
||||
assert_ne!(
|
||||
child_turn.config.permissions.permission_profile(),
|
||||
&expected_permission_profile
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -4240,6 +4246,10 @@ async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtr
|
||||
let (_session, turn) = make_session_and_context().await;
|
||||
let mut config = turn.config.as_ref().clone();
|
||||
config.agent_max_depth = 3;
|
||||
config
|
||||
.permissions
|
||||
.set_permission_profile(PermissionProfile::workspace_write())
|
||||
.expect("test config should allow workspace writes");
|
||||
config
|
||||
.features
|
||||
.enable(Feature::Sqlite)
|
||||
@@ -4269,6 +4279,22 @@ async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtr
|
||||
.expect("parent thread should start");
|
||||
let parent_thread_id = parent.thread_id;
|
||||
let parent_session = parent.thread.session.clone();
|
||||
let parent_turn = parent_session.new_default_turn().await;
|
||||
let mut owner_config = parent_turn
|
||||
.environments
|
||||
.primary()
|
||||
.expect("parent should have an environment")
|
||||
.config()
|
||||
.clone();
|
||||
let owner_permission_profile = PermissionProfile::read_only();
|
||||
owner_config.permission_profile =
|
||||
PermissionProfileSnapshot::legacy(owner_permission_profile.clone());
|
||||
let parent_environment = parent.thread.environment_selections().await.remove(0);
|
||||
parent
|
||||
.thread
|
||||
.environment_ready(&parent_environment, owner_config)
|
||||
.await
|
||||
.expect("owner environment should be installed");
|
||||
|
||||
let child_turn = parent_session.new_default_turn().await;
|
||||
let child_spawn_output = SpawnAgentHandler::default()
|
||||
@@ -4360,6 +4386,16 @@ async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtr
|
||||
manager.agent_control().get_status(child_thread_id).await,
|
||||
AgentStatus::NotFound
|
||||
);
|
||||
assert_eq!(
|
||||
manager
|
||||
.get_thread(child_thread_id)
|
||||
.await
|
||||
.expect("resumed child thread should exist")
|
||||
.config_snapshot()
|
||||
.await
|
||||
.permission_profile,
|
||||
owner_permission_profile
|
||||
);
|
||||
assert_ne!(
|
||||
manager
|
||||
.agent_control()
|
||||
@@ -4519,8 +4555,7 @@ async fn build_agent_spawn_config_uses_turn_context_values() {
|
||||
.set(AskForApproval::OnRequest)
|
||||
.expect("approval policy set");
|
||||
|
||||
let config = build_agent_spawn_config(&base_instructions, &turn, turn.environments.primary())
|
||||
.expect("spawn config");
|
||||
let config = build_agent_spawn_config(&base_instructions, &turn).expect("spawn config");
|
||||
let mut expected = (*turn.config).clone();
|
||||
expected.base_instructions_provenance = base_instructions.provenance.clone();
|
||||
expected.base_instructions = Some(base_instructions.text);
|
||||
@@ -4538,10 +4573,6 @@ async fn build_agent_spawn_config_uses_turn_context_values() {
|
||||
.approval_policy
|
||||
.set(AskForApproval::OnRequest)
|
||||
.expect("approval policy set");
|
||||
expected
|
||||
.permissions
|
||||
.set_permission_profile(turn.permission_profile())
|
||||
.expect("permission profile set");
|
||||
assert_eq!(config, expected);
|
||||
}
|
||||
|
||||
@@ -4574,10 +4605,9 @@ async fn build_agent_resume_config_clears_base_instructions() {
|
||||
panic!("parent environment should be ready");
|
||||
};
|
||||
environment.config_mut().permission_profile =
|
||||
PermissionProfileSnapshot::legacy(environment_permission_profile.clone());
|
||||
PermissionProfileSnapshot::legacy(environment_permission_profile);
|
||||
|
||||
let config =
|
||||
build_agent_resume_config(&turn, turn.environments.primary()).expect("resume config");
|
||||
let config = build_agent_resume_config(&turn).expect("resume config");
|
||||
|
||||
let mut expected = (*turn.config).clone();
|
||||
expected.base_instructions = None;
|
||||
@@ -4596,9 +4626,5 @@ async fn build_agent_resume_config_clears_base_instructions() {
|
||||
.approval_policy
|
||||
.set(AskForApproval::OnRequest)
|
||||
.expect("approval policy set");
|
||||
expected
|
||||
.permissions
|
||||
.set_permission_profile(environment_permission_profile)
|
||||
.expect("permission profile set");
|
||||
assert_eq!(config, expected);
|
||||
}
|
||||
|
||||
@@ -59,7 +59,6 @@ pub(crate) async fn handle_message_string_tool(
|
||||
let ToolInvocation {
|
||||
session,
|
||||
turn,
|
||||
step_context,
|
||||
call_id,
|
||||
source,
|
||||
..
|
||||
@@ -83,8 +82,7 @@ pub(crate) async fn handle_message_string_tool(
|
||||
let receiver_agent_path = receiver_agent.agent_path.clone().ok_or_else(|| {
|
||||
FunctionCallError::RespondToModel("target agent is missing an agent_path".to_string())
|
||||
})?;
|
||||
let resume_config =
|
||||
build_agent_resume_config(turn.as_ref(), step_context.environments.primary())?;
|
||||
let resume_config = build_agent_resume_config(turn.as_ref())?;
|
||||
session
|
||||
.services
|
||||
.agent_control
|
||||
|
||||
@@ -62,11 +62,8 @@ async fn handle_spawn_agent(
|
||||
|
||||
let session_source = turn.session_source.clone();
|
||||
let child_depth = next_thread_spawn_depth(&session_source);
|
||||
let mut config = build_agent_spawn_config(
|
||||
&session.get_base_instructions().await,
|
||||
turn.as_ref(),
|
||||
step_context.environments.primary(),
|
||||
)?;
|
||||
let mut config =
|
||||
build_agent_spawn_config(&session.get_base_instructions().await, turn.as_ref())?;
|
||||
if let Some(service_tier) = args.service_tier.as_ref() {
|
||||
config.service_tier = Some(service_tier.clone());
|
||||
}
|
||||
@@ -94,11 +91,7 @@ async fn handle_spawn_agent(
|
||||
args.service_tier.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
apply_spawn_agent_runtime_overrides(
|
||||
&mut config,
|
||||
turn.as_ref(),
|
||||
step_context.environments.primary(),
|
||||
)?;
|
||||
apply_spawn_agent_runtime_overrides(&mut config, turn.as_ref())?;
|
||||
|
||||
let spawn_source = thread_spawn_source(
|
||||
session.thread_id,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
use anyhow::Result;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::PermissionProfileSnapshot;
|
||||
use codex_protocol::protocol::EnvironmentConfig;
|
||||
use codex_protocol::protocol::EnvironmentConfigState;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use core_test_support::responses::ResponseMock;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
@@ -236,12 +240,7 @@ async fn v2_residency_reload_preserves_inherited_environment_and_tools() -> Resu
|
||||
.with_model("gpt-5.6-sol")
|
||||
.with_exec_server_url("none")
|
||||
.with_config(|config| {
|
||||
for feature in [
|
||||
Feature::Collab,
|
||||
Feature::MultiAgentV2,
|
||||
Feature::UnifiedExec,
|
||||
Feature::DeferredExecutor,
|
||||
] {
|
||||
for feature in [Feature::Collab, Feature::MultiAgentV2, Feature::UnifiedExec] {
|
||||
config
|
||||
.features
|
||||
.enable(feature)
|
||||
@@ -249,9 +248,22 @@ async fn v2_residency_reload_preserves_inherited_environment_and_tools() -> Resu
|
||||
}
|
||||
config.use_experimental_unified_exec_tool = true;
|
||||
config.multi_agent_v2.max_concurrent_threads_per_session = 2;
|
||||
config
|
||||
.permissions
|
||||
.set_permission_profile(PermissionProfile::workspace_write())
|
||||
.expect("thread permissions should allow workspace writes");
|
||||
});
|
||||
let test = builder.build_with_remote_and_local_env(&server).await?;
|
||||
let child_environment = test.executor_environment().selection().clone();
|
||||
let mut child_environment = test.executor_environment().selection().clone();
|
||||
child_environment.config = EnvironmentConfigState::Ready(EnvironmentConfig {
|
||||
allow_login_shell: test.config.permissions.allow_login_shell,
|
||||
permission_profile: PermissionProfileSnapshot::legacy(PermissionProfile::read_only()),
|
||||
shell_environment_policy: Default::default(),
|
||||
exec_policy: None,
|
||||
mcp_policy: None,
|
||||
network_policy: None,
|
||||
selected_capability_roots: Vec::new(),
|
||||
});
|
||||
if let Some(exec_server_url) = test.executor_environment().exec_server_url() {
|
||||
test.thread_manager
|
||||
.environment_manager()
|
||||
@@ -272,7 +284,14 @@ async fn v2_residency_reload_preserves_inherited_environment_and_tools() -> Resu
|
||||
})
|
||||
.await;
|
||||
|
||||
test.submit_text_turn(EVICT_PROMPT).await?;
|
||||
let mut sender_environment = child_environment.clone();
|
||||
let EnvironmentConfigState::Ready(sender_config) = &mut sender_environment.config else {
|
||||
unreachable!("child environment config should be ready");
|
||||
};
|
||||
sender_config.permission_profile =
|
||||
PermissionProfileSnapshot::legacy(PermissionProfile::workspace_write());
|
||||
test.submit_turn_with_environments(EVICT_PROMPT, Some(vec![sender_environment]))
|
||||
.await?;
|
||||
let replacement_thread_id = created_threads.recv().await?;
|
||||
let replacement_thread = test
|
||||
.thread_manager
|
||||
@@ -303,6 +322,10 @@ async fn v2_residency_reload_preserves_inherited_environment_and_tools() -> Resu
|
||||
.environments,
|
||||
vec![child_environment]
|
||||
);
|
||||
assert_eq!(
|
||||
reloaded_worker.config_snapshot().await.permission_profile,
|
||||
PermissionProfile::read_only()
|
||||
);
|
||||
|
||||
let worker_tools = |response_mock: &ResponseMock| {
|
||||
response_mock
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::sync::Arc;
|
||||
|
||||
use codex_core::ForkSnapshot;
|
||||
use codex_core::NewThread;
|
||||
use codex_core::ThreadConfigSnapshot;
|
||||
use codex_core::TurnInputRequest;
|
||||
use codex_core::parse_turn_item;
|
||||
use codex_history::InitialHistory;
|
||||
@@ -14,6 +13,7 @@ use codex_protocol::mcp::ClientMcpExtensions;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ThreadHistoryMode;
|
||||
use codex_protocol::protocol::ThreadSettingsAppliedEvent;
|
||||
use codex_protocol::protocol::ThreadSettingsSnapshot;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
@@ -110,7 +110,7 @@ async fn fork_thread_twice_drops_to_first_message() {
|
||||
|
||||
let fork1_path = codex_fork1.rollout_path().expect("rollout path");
|
||||
expected_after_first.push(thread_settings_applied_item(
|
||||
codex_fork1.config_snapshot().await,
|
||||
codex_fork1.thread_settings_snapshot().await,
|
||||
));
|
||||
|
||||
// GetHistory on fork1 flushed; the file is ready.
|
||||
@@ -145,7 +145,7 @@ async fn fork_thread_twice_drops_to_first_message() {
|
||||
.unwrap_or(0);
|
||||
let mut expected_after_second: Vec<RolloutItem> = fork1_items[..cut_last_on_fork1].to_vec();
|
||||
expected_after_second.push(thread_settings_applied_item(
|
||||
codex_fork2.config_snapshot().await,
|
||||
codex_fork2.thread_settings_snapshot().await,
|
||||
));
|
||||
let fork2_items = read_rollout_items(&fork2_path);
|
||||
pretty_assertions::assert_eq!(
|
||||
@@ -154,10 +154,10 @@ async fn fork_thread_twice_drops_to_first_message() {
|
||||
);
|
||||
}
|
||||
|
||||
fn thread_settings_applied_item(snapshot: ThreadConfigSnapshot) -> RolloutItem {
|
||||
fn thread_settings_applied_item(snapshot: ThreadSettingsSnapshot) -> RolloutItem {
|
||||
RolloutItem::EventMsg(EventMsg::ThreadSettingsApplied(
|
||||
ThreadSettingsAppliedEvent {
|
||||
thread_settings: snapshot.into_thread_settings_snapshot(),
|
||||
thread_settings: snapshot,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ use codex_protocol::capabilities::SelectedCapabilityRoot;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Settings;
|
||||
use codex_protocol::models::ActivePermissionProfile;
|
||||
use codex_protocol::models::FileSystemPermissions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::PermissionProfileSnapshot;
|
||||
@@ -464,6 +465,13 @@ async fn environment_permissions_follow_configuration_ownership() -> Result<()>
|
||||
let test = builder.build_with_auto_env(&server).await?;
|
||||
let selection = test.executor_environment().selection().clone();
|
||||
let marker = selection.cwd.join(FILE_NAME)?;
|
||||
let owner_active_profile = ActivePermissionProfile::new("owner-read-only");
|
||||
let owner_profile_workspace_root = test.config.cwd.join("owner-profile-root");
|
||||
let owner_permission_profile = PermissionProfileSnapshot::active_with_profile_workspace_roots(
|
||||
PermissionProfile::read_only(),
|
||||
owner_active_profile.clone(),
|
||||
vec![owner_profile_workspace_root.clone()],
|
||||
);
|
||||
|
||||
let (shell, command) = match test_target_os() {
|
||||
TestTargetOs::Linux => (
|
||||
@@ -549,30 +557,59 @@ async fn environment_permissions_follow_configuration_ownership() -> Result<()>
|
||||
vec![TurnEnvironmentSelection {
|
||||
config: EnvironmentConfigState::Ready(EnvironmentConfig {
|
||||
allow_login_shell: test.config.permissions.allow_login_shell,
|
||||
permission_profile: PermissionProfileSnapshot::legacy(
|
||||
PermissionProfile::read_only(),
|
||||
),
|
||||
permission_profile: owner_permission_profile,
|
||||
shell_environment_policy: Default::default(),
|
||||
exec_policy: None,
|
||||
mcp_policy: None,
|
||||
network_policy: None,
|
||||
selected_capability_roots: Vec::new(),
|
||||
}),
|
||||
..selection
|
||||
..selection.clone()
|
||||
}],
|
||||
)),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
submit_thread_settings(
|
||||
&test.codex,
|
||||
ThreadSettingsOverrides {
|
||||
permission_profile: Some(PermissionProfile::workspace_write()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
test.codex
|
||||
.submit(Op::ThreadSettings {
|
||||
thread_settings: ThreadSettingsOverrides {
|
||||
permission_profile: Some(PermissionProfile::workspace_write()),
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
let persisted_settings = wait_for_event_match(&test.codex, |event| match event {
|
||||
EventMsg::ThreadSettingsApplied(event) => Some(event.thread_settings.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
let snapshot = test.codex.config_snapshot().await;
|
||||
assert_eq!(snapshot.permission_profile, PermissionProfile::read_only());
|
||||
assert_eq!(
|
||||
snapshot.active_permission_profile,
|
||||
Some(owner_active_profile.clone())
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.profile_workspace_roots,
|
||||
vec![owner_profile_workspace_root.clone()]
|
||||
);
|
||||
assert_eq!(
|
||||
persisted_settings,
|
||||
test.codex.thread_settings_snapshot().await
|
||||
);
|
||||
assert_ne!(
|
||||
persisted_settings.active_permission_profile,
|
||||
snapshot.active_permission_profile
|
||||
);
|
||||
test.codex
|
||||
.restore_thread_settings(test.codex.restorable_thread_settings().await)
|
||||
.await?;
|
||||
let (mcp_config, _) = test.codex.current_mcp_config_and_runtime_context().await;
|
||||
assert_eq!(
|
||||
mcp_config.permission_profile,
|
||||
PermissionProfile::workspace_write()
|
||||
);
|
||||
test.submit_text_turn("try to write a file with owner-provided permissions")
|
||||
.await?;
|
||||
|
||||
@@ -593,6 +630,34 @@ async fn environment_permissions_follow_configuration_ownership() -> Result<()>
|
||||
.is_err(),
|
||||
"read-only attachment unexpectedly wrote {FILE_NAME}"
|
||||
);
|
||||
let turn_context = test
|
||||
.codex
|
||||
.load_history(/*include_archived*/ false)
|
||||
.await?
|
||||
.items
|
||||
.into_iter()
|
||||
.rev()
|
||||
.find_map(|item| match item {
|
||||
RolloutItem::TurnContext(context) => Some(context),
|
||||
_ => None,
|
||||
})
|
||||
.context("owner turn context")?;
|
||||
assert!(
|
||||
turn_context
|
||||
.workspace_roots
|
||||
.as_ref()
|
||||
.is_some_and(|roots| roots.contains(&owner_profile_workspace_root))
|
||||
);
|
||||
assert_eq!(
|
||||
(
|
||||
turn_context.permission_profile,
|
||||
turn_context.active_permission_profile
|
||||
),
|
||||
(
|
||||
Some(PermissionProfile::read_only()),
|
||||
Some(owner_active_profile)
|
||||
)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1516,6 +1581,14 @@ async fn pending_attachment_installs_configuration_before_waiting_turn_resumes()
|
||||
.thread
|
||||
.environment_failed(&pending_selection, "configuration unavailable".to_string())
|
||||
.await?;
|
||||
for (thread, configured) in [
|
||||
(&waiting.thread, false),
|
||||
(&independent.thread, true),
|
||||
(&failed.thread, false),
|
||||
] {
|
||||
let snapshot = thread.config_snapshot().await;
|
||||
assert_eq!(snapshot.is_primary_environment_configured(), configured);
|
||||
}
|
||||
assert_eq!(
|
||||
failed.thread.environment_selections().await,
|
||||
vec![TurnEnvironmentSelection {
|
||||
@@ -2006,6 +2079,10 @@ async fn deferred_executor_spawn_agent_inherits_ready_step_environments(
|
||||
.with_exec_server_url(format!("ws://{}", listener.local_addr()?))
|
||||
.with_config(move |config| {
|
||||
config.project_doc_max_bytes = 0;
|
||||
config
|
||||
.permissions
|
||||
.set_permission_profile(PermissionProfile::workspace_write())
|
||||
.expect("thread should allow workspace writes");
|
||||
assert!(config.features.enable(Feature::DeferredExecutor).is_ok());
|
||||
assert!(config.features.enable(Feature::Collab).is_ok());
|
||||
if multi_agent_v2 {
|
||||
@@ -2028,11 +2105,25 @@ async fn deferred_executor_spawn_agent_inherits_ready_step_environments(
|
||||
)
|
||||
.await
|
||||
.context("thread startup should not wait for the remote environment")??;
|
||||
let owner_active_profile = ActivePermissionProfile::new("owner-read-only");
|
||||
let owner_profile_workspace_root = test.config.cwd.join("owner-profile-root");
|
||||
let remote_selection = TurnEnvironmentSelection {
|
||||
environment_id: REMOTE_ENVIRONMENT_ID.to_string(),
|
||||
cwd: PathUri::from_abs_path(&test.config.cwd),
|
||||
workspace_roots: vec![PathUri::from_abs_path(&test.config.cwd)],
|
||||
config: EnvironmentConfigState::FromThread,
|
||||
config: EnvironmentConfigState::Ready(EnvironmentConfig {
|
||||
allow_login_shell: test.config.permissions.allow_login_shell,
|
||||
permission_profile: PermissionProfileSnapshot::active_with_profile_workspace_roots(
|
||||
PermissionProfile::read_only(),
|
||||
owner_active_profile.clone(),
|
||||
vec![owner_profile_workspace_root.clone()],
|
||||
),
|
||||
shell_environment_policy: Default::default(),
|
||||
exec_policy: None,
|
||||
mcp_policy: None,
|
||||
network_policy: None,
|
||||
selected_capability_roots: Vec::new(),
|
||||
}),
|
||||
};
|
||||
let expected_environments = vec![remote_selection, local(test.config.cwd.clone())];
|
||||
let mut created_threads = test.thread_manager.subscribe_thread_created();
|
||||
@@ -2068,6 +2159,24 @@ async fn deferred_executor_spawn_agent_inherits_ready_step_environments(
|
||||
child_thread.environment_selections().await,
|
||||
expected_environments
|
||||
);
|
||||
let child_snapshot = child_thread.config_snapshot().await;
|
||||
let child_settings = child_thread.thread_settings_snapshot().await;
|
||||
assert_ne!(
|
||||
child_settings.permission_profile,
|
||||
child_snapshot.permission_profile
|
||||
);
|
||||
assert_eq!(
|
||||
(
|
||||
child_snapshot.permission_profile,
|
||||
child_snapshot.active_permission_profile,
|
||||
child_snapshot.profile_workspace_roots,
|
||||
),
|
||||
(
|
||||
PermissionProfile::read_only(),
|
||||
Some(owner_active_profile),
|
||||
vec![owner_profile_workspace_root],
|
||||
)
|
||||
);
|
||||
assert!(
|
||||
response_mock.requests()[1]
|
||||
.function_call_output_content_and_success(wait_call_id)
|
||||
|
||||
@@ -307,7 +307,7 @@ async fn shell_command_times_out_with_timeout_ms() -> anyhow::Result<()> {
|
||||
let harness = shell_command_harness_with(|builder| builder.with_model("gpt-5.4")).await?;
|
||||
let call_id = "shell-command-timeout";
|
||||
let command = if cfg!(windows) {
|
||||
"timeout /t 5"
|
||||
"Start-Sleep -Seconds 5"
|
||||
} else {
|
||||
"sleep 5"
|
||||
};
|
||||
|
||||
@@ -267,7 +267,6 @@ impl MemoryStartupContext {
|
||||
|
||||
let mut client_session = model_client.new_session();
|
||||
let window_id = format!("{}:0", self.thread_id);
|
||||
let permission_profile = config.permissions.effective_permission_profile();
|
||||
let responses_metadata = detached_memory_responses_metadata(
|
||||
installation_id,
|
||||
session_id_string,
|
||||
@@ -275,7 +274,7 @@ impl MemoryStartupContext {
|
||||
window_id,
|
||||
&session_source,
|
||||
&config.cwd,
|
||||
&permission_profile,
|
||||
&config_snapshot.permission_profile,
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -452,6 +452,7 @@ async fn memories_startup_phase1_uses_live_thread_service_tier_and_detached_meta
|
||||
&test.codex,
|
||||
codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
service_tier: Some(Some(ServiceTier::Fast.request_value().to_string())),
|
||||
permission_profile: Some(codex_protocol::models::PermissionProfile::workspace_write()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
@@ -507,7 +508,7 @@ async fn memories_startup_phase1_uses_live_thread_service_tier_and_detached_meta
|
||||
let metadata: serde_json::Value =
|
||||
serde_json::from_str(&metadata_header).expect("turn metadata json");
|
||||
assert_eq!(metadata["request_kind"].as_str(), Some("memory"));
|
||||
assert_eq!(metadata["sandbox_mode"].as_str(), Some("read-only"));
|
||||
assert_eq!(metadata["sandbox_mode"].as_str(), Some("workspace-write"));
|
||||
assert!(metadata.get("session_id").is_none());
|
||||
assert!(metadata.get("thread_id").is_none());
|
||||
assert!(metadata.get("turn_id").is_none());
|
||||
|
||||
Reference in New Issue
Block a user