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