mirror of
https://github.com/openai/codex.git
synced 2026-11-13 06:45:07 +00:00
Harden internal Guardian session isolation (#40497)
## Why Internal Guardian reviews must remain independent of parent-session customization while still honoring managed execution and environment restrictions. ## What changed - Apply the restricted Guardian session path to both internal and subagent review sources. - Start internal reviews without inherited user instructions, extensions, MCP servers, or multi-agent behavior. - Preserve managed execution policy and selected environment constraints while exposing only the allowed review tools. Honor shell feature restrictions across every selected environment and retain code mode when enabled. ## Testing - Cover internal review isolation from parent state and managed instructions. - Cover optional tool filtering, managed shell restrictions, code mode, and secondary environment profiles. GitOrigin-RevId: edbf7d8a6fe3ee8213a1c75bc8ec331a1cf49006
This commit is contained in:
1 parent
be3a666143
commit
8476434a97
11 files changed
+475
-62
No files matched your search
@@ -80,7 +80,7 @@ pub(crate) async fn run_codex_thread_interactive(
|
|||||||
warnings: Vec::new(),
|
warnings: Vec::new(),
|
||||||
};
|
};
|
||||||
let session_source = SessionSource::SubAgent(subagent_source.clone());
|
let session_source = SessionSource::SubAgent(subagent_source.clone());
|
||||||
let is_guardian_reviewer = crate::guardian::is_guardian_reviewer_source(&session_source);
|
let is_guardian_reviewer = crate::guardian::is_basic_session_source(&session_source);
|
||||||
let extensions = if is_guardian_reviewer {
|
let extensions = if is_guardian_reviewer {
|
||||||
codex_extension_api::empty_extension_registry()
|
codex_extension_api::empty_extension_registry()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ pub(crate) use prompt::BUNDLED_GUARDIAN_POLICY;
|
|||||||
pub(crate) use prompt::guardian_truncate_text;
|
pub(crate) use prompt::guardian_truncate_text;
|
||||||
pub(crate) use review::GuardianReviewOptions;
|
pub(crate) use review::GuardianReviewOptions;
|
||||||
pub(crate) use review::guardian_timeout_message;
|
pub(crate) use review::guardian_timeout_message;
|
||||||
pub(crate) use review::is_guardian_reviewer_source;
|
pub(crate) use review::is_basic_session_source;
|
||||||
pub(crate) use review::new_guardian_review_id;
|
pub(crate) use review::new_guardian_review_id;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) use review::record_guardian_denial_for_test;
|
pub(crate) use review::record_guardian_denial_for_test;
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ use codex_protocol::protocol::GuardianAssessmentEvent;
|
|||||||
use codex_protocol::protocol::GuardianAssessmentStatus;
|
use codex_protocol::protocol::GuardianAssessmentStatus;
|
||||||
use codex_protocol::protocol::GuardianRiskLevel;
|
use codex_protocol::protocol::GuardianRiskLevel;
|
||||||
use codex_protocol::protocol::GuardianUserAuthorization;
|
use codex_protocol::protocol::GuardianUserAuthorization;
|
||||||
|
use codex_protocol::protocol::InternalSessionSource;
|
||||||
use codex_protocol::protocol::ReviewDecision;
|
use codex_protocol::protocol::ReviewDecision;
|
||||||
|
use codex_protocol::protocol::SessionSource;
|
||||||
use codex_protocol::protocol::SubAgentSource;
|
use codex_protocol::protocol::SubAgentSource;
|
||||||
use codex_protocol::protocol::TurnAbortReason;
|
use codex_protocol::protocol::TurnAbortReason;
|
||||||
use codex_protocol::protocol::WarningEvent;
|
use codex_protocol::protocol::WarningEvent;
|
||||||
@@ -209,14 +211,12 @@ pub(crate) fn routes_approval_policy_to_guardian(
|
|||||||
) && approvals_reviewer == ApprovalsReviewer::AutoReview
|
) && approvals_reviewer == ApprovalsReviewer::AutoReview
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_guardian_reviewer_source(
|
pub(crate) fn is_basic_session_source(session_source: &SessionSource) -> bool {
|
||||||
session_source: &codex_protocol::protocol::SessionSource,
|
match session_source {
|
||||||
) -> bool {
|
SessionSource::SubAgent(SubAgentSource::Other(label)) => label == GUARDIAN_REVIEWER_NAME,
|
||||||
matches!(
|
SessionSource::Internal(InternalSessionSource::Guardian) => true,
|
||||||
session_source,
|
_ => false,
|
||||||
codex_protocol::protocol::SessionSource::SubAgent(SubAgentSource::Other(label))
|
}
|
||||||
if label == GUARDIAN_REVIEWER_NAME
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn track_guardian_review(
|
fn track_guardian_review(
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ impl Session {
|
|||||||
mut projection: McpRuntimeProjection,
|
mut projection: McpRuntimeProjection,
|
||||||
) -> BoxFuture<'a, McpRuntimeProjection> {
|
) -> BoxFuture<'a, McpRuntimeProjection> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
if crate::guardian::is_guardian_reviewer_source(session_source) {
|
if crate::guardian::is_basic_session_source(session_source) {
|
||||||
return projection;
|
return projection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -547,11 +547,16 @@ impl Session {
|
|||||||
config
|
config
|
||||||
.startup_warnings
|
.startup_warnings
|
||||||
.extend(user_instruction_provider_warnings);
|
.extend(user_instruction_provider_warnings);
|
||||||
let exec_policy = if crate::guardian::is_guardian_reviewer_source(&session_source) {
|
let exec_policy = if crate::guardian::is_basic_session_source(&session_source) {
|
||||||
// Guardian review should rely on the built-in shell safety checks,
|
let managed_policy = config
|
||||||
// not on caller-provided exec-policy rules that could shape the
|
.config_layer_stack
|
||||||
// reviewer or silently auto-approve commands.
|
.requirements()
|
||||||
Arc::new(ExecPolicyManager::default())
|
.exec_policy
|
||||||
|
.as_deref()
|
||||||
|
.map_or_else(codex_execpolicy::Policy::empty, |policy| {
|
||||||
|
policy.as_ref().clone()
|
||||||
|
});
|
||||||
|
Arc::new(ExecPolicyManager::new(Arc::new(managed_policy)))
|
||||||
} else if let Some(exec_policy) = &inherited_exec_policy {
|
} else if let Some(exec_policy) = &inherited_exec_policy {
|
||||||
Arc::clone(exec_policy)
|
Arc::clone(exec_policy)
|
||||||
} else {
|
} else {
|
||||||
@@ -602,9 +607,8 @@ impl Session {
|
|||||||
config.http_client_factory(),
|
config.http_client_factory(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let trusted_guardian_reviewer =
|
let trusted_guardian_reviewer = crate::guardian::is_basic_session_source(&session_source)
|
||||||
crate::guardian::is_guardian_reviewer_source(&session_source)
|
&& !matches!(conversation_history, InitialHistory::Resumed(_));
|
||||||
&& !matches!(conversation_history, InitialHistory::Resumed(_));
|
|
||||||
if config
|
if config
|
||||||
.config_layer_stack
|
.config_layer_stack
|
||||||
.requirements()
|
.requirements()
|
||||||
@@ -3592,7 +3596,7 @@ impl Session {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
let separate_guardian_developer_message =
|
let separate_guardian_developer_message =
|
||||||
crate::guardian::is_guardian_reviewer_source(&session_source);
|
crate::guardian::is_basic_session_source(&session_source);
|
||||||
// Keep the guardian policy prompt out of the aggregated developer bundle so it
|
// Keep the guardian policy prompt out of the aggregated developer bundle so it
|
||||||
// stays isolated as its own top-level developer message for guardian subagents.
|
// stays isolated as its own top-level developer message for guardian subagents.
|
||||||
if !separate_guardian_developer_message
|
if !separate_guardian_developer_message
|
||||||
|
|||||||
@@ -659,7 +659,7 @@ async fn required_mcp_servers_for_input(
|
|||||||
turn_context: &TurnContext,
|
turn_context: &TurnContext,
|
||||||
user_input: &[UserInput],
|
user_input: &[UserInput],
|
||||||
) -> (Vec<String>, Vec<crate::plugins::PluginCapabilitySummary>) {
|
) -> (Vec<String>, Vec<crate::plugins::PluginCapabilitySummary>) {
|
||||||
if crate::guardian::is_guardian_reviewer_source(&turn_context.session_source) {
|
if crate::guardian::is_basic_session_source(&turn_context.session_source) {
|
||||||
return (Vec::new(), Vec::new());
|
return (Vec::new(), Vec::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -765,7 +765,7 @@ async fn build_skills_and_plugins(
|
|||||||
let turn_context = step_context.turn.as_ref();
|
let turn_context = step_context.turn.as_ref();
|
||||||
// Guardian input embeds the parent transcript as untrusted evidence. Do not interpret skill or
|
// Guardian input embeds the parent transcript as untrusted evidence. Do not interpret skill or
|
||||||
// plugin mentions from that generated prompt as requests to inject additional instructions.
|
// plugin mentions from that generated prompt as requests to inject additional instructions.
|
||||||
if crate::guardian::is_guardian_reviewer_source(&turn_context.session_source) {
|
if crate::guardian::is_basic_session_source(&turn_context.session_source) {
|
||||||
return Some((Vec::new(), HashSet::new()));
|
return Some((Vec::new(), HashSet::new()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1322,7 +1322,7 @@ pub(crate) fn build_prompt(
|
|||||||
parallel_tool_calls: true,
|
parallel_tool_calls: true,
|
||||||
base_instructions,
|
base_instructions,
|
||||||
output_schema: turn_context.final_output_json_schema.clone(),
|
output_schema: turn_context.final_output_json_schema.clone(),
|
||||||
output_schema_strict: !crate::guardian::is_guardian_reviewer_source(
|
output_schema_strict: !crate::guardian::is_basic_session_source(
|
||||||
&turn_context.session_source,
|
&turn_context.session_source,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -285,14 +285,16 @@ impl Session {
|
|||||||
world_state.add_section(usage_hint);
|
world_state.add_section(usage_hint);
|
||||||
}
|
}
|
||||||
world_state.add_section(multi_agent_mode);
|
world_state.add_section(multi_agent_mode);
|
||||||
world_state.add_section(ManagedDeveloperInstructionsState::new(
|
if !crate::guardian::is_basic_session_source(&turn_context.session_source) {
|
||||||
turn_context
|
world_state.add_section(ManagedDeveloperInstructionsState::new(
|
||||||
.config
|
turn_context
|
||||||
.config_layer_stack
|
.config
|
||||||
.requirements()
|
.config_layer_stack
|
||||||
.additional_developer_instructions
|
.requirements()
|
||||||
.as_ref(),
|
.additional_developer_instructions
|
||||||
));
|
.as_ref(),
|
||||||
|
));
|
||||||
|
}
|
||||||
Ok(world_state)
|
Ok(world_state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1882,21 +1882,44 @@ impl ThreadManagerState {
|
|||||||
threads.remove(&resumed.conversation_id);
|
threads.remove(&resumed.conversation_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let user_instructions = self
|
let (
|
||||||
.user_instructions_for_spawn(&session_source, parent_thread_id, forked_from_thread_id)
|
user_instructions,
|
||||||
.await;
|
inherited_exec_policy,
|
||||||
|
extensions,
|
||||||
|
mcp_manager,
|
||||||
|
multi_agent_version,
|
||||||
|
) = if crate::guardian::is_basic_session_source(&session_source) {
|
||||||
|
(
|
||||||
|
LoadedUserInstructions::default(),
|
||||||
|
None,
|
||||||
|
empty_extension_registry(),
|
||||||
|
Arc::new(McpManager::new(Arc::clone(&self.plugins_manager))),
|
||||||
|
Some(MultiAgentVersion::Disabled),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
self.user_instructions_for_spawn(
|
||||||
|
&session_source,
|
||||||
|
parent_thread_id,
|
||||||
|
forked_from_thread_id,
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
inherited_exec_policy,
|
||||||
|
Arc::clone(&self.extensions),
|
||||||
|
Arc::clone(&self.mcp_manager),
|
||||||
|
self.initial_multi_agent_version_for_spawn(
|
||||||
|
&initial_history,
|
||||||
|
Some(&session_source),
|
||||||
|
parent_thread_id,
|
||||||
|
forked_from_thread_id,
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
};
|
||||||
let parent_rollout_thread_trace = self
|
let parent_rollout_thread_trace = self
|
||||||
.parent_rollout_thread_trace_for_source(&session_source, &initial_history)
|
.parent_rollout_thread_trace_for_source(&session_source, &initial_history)
|
||||||
.await;
|
.await;
|
||||||
let tracked_session_source = session_source.clone();
|
let tracked_session_source = session_source.clone();
|
||||||
let multi_agent_version = self
|
|
||||||
.initial_multi_agent_version_for_spawn(
|
|
||||||
&initial_history,
|
|
||||||
Some(&session_source),
|
|
||||||
parent_thread_id,
|
|
||||||
forked_from_thread_id,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
let originator = self
|
let originator = self
|
||||||
.effective_originator(
|
.effective_originator(
|
||||||
&initial_history,
|
&initial_history,
|
||||||
@@ -1925,9 +1948,9 @@ impl ThreadManagerState {
|
|||||||
environment_manager: Arc::clone(&self.environment_manager),
|
environment_manager: Arc::clone(&self.environment_manager),
|
||||||
skills_service: Arc::clone(&self.skills_service),
|
skills_service: Arc::clone(&self.skills_service),
|
||||||
plugins_manager: Arc::clone(&self.plugins_manager),
|
plugins_manager: Arc::clone(&self.plugins_manager),
|
||||||
mcp_manager: Arc::clone(&self.mcp_manager),
|
mcp_manager,
|
||||||
code_mode_session_provider: Arc::clone(&self.code_mode_session_provider),
|
code_mode_session_provider: Arc::clone(&self.code_mode_session_provider),
|
||||||
extensions: Arc::clone(&self.extensions),
|
extensions,
|
||||||
conversation_history: initial_history,
|
conversation_history: initial_history,
|
||||||
requested_history_mode: history_mode,
|
requested_history_mode: history_mode,
|
||||||
fork_persistence,
|
fork_persistence,
|
||||||
|
|||||||
@@ -878,18 +878,106 @@ async fn start_thread_keeps_internal_threads_hidden_from_normal_lookups() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn spawn_internal_session_preserves_parent_lineage_without_forking_history() {
|
async fn spawn_internal_session_preserves_parent_lineage_without_forking_history() {
|
||||||
|
struct ParentLifecycleContributor {
|
||||||
|
observed_mcp_sources: Arc<std::sync::Mutex<Vec<SessionSource>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl codex_extension_api::ThreadLifecycleContributor<Config> for ParentLifecycleContributor {}
|
||||||
|
|
||||||
|
impl codex_extension_api::McpServerContributor<Config> for ParentLifecycleContributor {
|
||||||
|
fn id(&self) -> &'static str {
|
||||||
|
"parent_mcp_contributor"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contribute<'a>(
|
||||||
|
&'a self,
|
||||||
|
context: codex_extension_api::McpServerContributionContext<'a, Config>,
|
||||||
|
) -> codex_extension_api::ExtensionFuture<'a, Vec<codex_extension_api::McpServerContribution>>
|
||||||
|
{
|
||||||
|
Box::pin(async move {
|
||||||
|
if let Some(session_source) = context.session_source() {
|
||||||
|
self.observed_mcp_sources
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||||
|
.push(session_source.clone());
|
||||||
|
}
|
||||||
|
Vec::new()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ParentInstructionsProvider(codex_extension_api::UserInstructions);
|
||||||
|
|
||||||
|
impl codex_extension_api::UserInstructionsProvider for ParentInstructionsProvider {
|
||||||
|
fn load_user_instructions(&self) -> codex_extension_api::LoadUserInstructionsFuture<'_> {
|
||||||
|
Box::pin(async move {
|
||||||
|
codex_extension_api::LoadedUserInstructions {
|
||||||
|
instructions: Some(self.0.clone()),
|
||||||
|
warnings: Vec::new(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let temp_dir = tempdir().expect("tempdir");
|
let temp_dir = tempdir().expect("tempdir");
|
||||||
let mut config = test_config().await;
|
let mut config = test_config().await;
|
||||||
config.codex_home = temp_dir.path().join("codex-home").abs();
|
config.codex_home = temp_dir.path().join("codex-home").abs();
|
||||||
config.cwd = config.codex_home.abs();
|
config.cwd = config.codex_home.abs();
|
||||||
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
|
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
|
||||||
|
|
||||||
let manager = ThreadManager::with_models_provider_and_home_for_tests(
|
let mut managed_exec_policy = codex_execpolicy::Policy::empty();
|
||||||
|
managed_exec_policy
|
||||||
|
.add_prefix_rule(&["rm".to_string()], codex_execpolicy::Decision::Forbidden)
|
||||||
|
.expect("add managed execution restriction");
|
||||||
|
let mut requirements = config.config_layer_stack.requirements().clone();
|
||||||
|
requirements.exec_policy = Some(codex_config::Sourced::new(
|
||||||
|
codex_execpolicy::RequirementsExecPolicy::new(managed_exec_policy),
|
||||||
|
codex_config::RequirementSource::Unknown,
|
||||||
|
));
|
||||||
|
requirements.additional_developer_instructions = Some(codex_config::Sourced::new(
|
||||||
|
"managed instructions must not shape the reviewer".to_string(),
|
||||||
|
codex_config::RequirementSource::Unknown,
|
||||||
|
));
|
||||||
|
let mut requirements_toml = config.config_layer_stack.requirements_toml().clone();
|
||||||
|
requirements_toml.additional_developer_instructions =
|
||||||
|
Some("managed instructions must not shape the reviewer".to_string());
|
||||||
|
config.config_layer_stack = codex_config::ConfigLayerStack::new(
|
||||||
|
config
|
||||||
|
.config_layer_stack
|
||||||
|
.all_layers_low_to_high()
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
|
requirements,
|
||||||
|
requirements_toml,
|
||||||
|
)
|
||||||
|
.expect("managed requirements stack");
|
||||||
|
|
||||||
|
let parent_instructions = codex_extension_api::UserInstructions {
|
||||||
|
text: "parent user instructions must not be inherited".to_string(),
|
||||||
|
source: config.codex_home.join("AGENTS.md"),
|
||||||
|
};
|
||||||
|
let mut manager = ThreadManager::with_models_provider_and_home_for_tests(
|
||||||
CodexAuth::from_api_key("dummy"),
|
CodexAuth::from_api_key("dummy"),
|
||||||
config.model_provider.clone(),
|
config.model_provider.clone(),
|
||||||
config.codex_home.to_path_buf(),
|
config.codex_home.to_path_buf(),
|
||||||
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||||
);
|
);
|
||||||
|
let observed_mcp_sources = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||||
|
let parent_contributor = Arc::new(ParentLifecycleContributor {
|
||||||
|
observed_mcp_sources: Arc::clone(&observed_mcp_sources),
|
||||||
|
});
|
||||||
|
let mut extensions = codex_extension_api::ExtensionRegistryBuilder::new();
|
||||||
|
extensions.thread_lifecycle_contributor(parent_contributor.clone());
|
||||||
|
extensions.mcp_server_contributor(parent_contributor);
|
||||||
|
let manager_state = Arc::get_mut(&mut manager.state).expect("unshared thread manager state");
|
||||||
|
manager_state.extensions = Arc::new(extensions.build());
|
||||||
|
manager_state.mcp_manager = Arc::new(McpManager::new_with_extensions(
|
||||||
|
Arc::clone(&manager_state.plugins_manager),
|
||||||
|
Arc::clone(&manager_state.extensions),
|
||||||
|
manager_state.mcp_manager.codex_apps_tools_cache(),
|
||||||
|
));
|
||||||
|
manager_state.user_instructions_provider =
|
||||||
|
Arc::new(ParentInstructionsProvider(parent_instructions.clone()));
|
||||||
let parent = manager
|
let parent = manager
|
||||||
.start_thread(StartThreadOptions {
|
.start_thread(StartThreadOptions {
|
||||||
metrics_service_name: Some("codex_work_desktop".to_string()),
|
metrics_service_name: Some("codex_work_desktop".to_string()),
|
||||||
@@ -897,6 +985,45 @@ async fn spawn_internal_session_preserves_parent_lineage_without_forking_history
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.expect("start parent thread");
|
.expect("start parent thread");
|
||||||
|
parent
|
||||||
|
.thread
|
||||||
|
.session
|
||||||
|
.set_multi_agent_version_if_unset(MultiAgentVersion::V2);
|
||||||
|
assert_eq!(
|
||||||
|
parent.thread.session.user_instructions().await,
|
||||||
|
Some(parent_instructions)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parent
|
||||||
|
.thread
|
||||||
|
.session
|
||||||
|
.services
|
||||||
|
.extensions
|
||||||
|
.thread_lifecycle_contributors()
|
||||||
|
.len(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
let mut reviewer_environments = parent
|
||||||
|
.thread
|
||||||
|
.session
|
||||||
|
.services
|
||||||
|
.turn_environments
|
||||||
|
.selections();
|
||||||
|
let reviewer_environment = reviewer_environments
|
||||||
|
.first_mut()
|
||||||
|
.expect("parent should have an environment");
|
||||||
|
reviewer_environment.config =
|
||||||
|
EnvironmentConfigState::Ready(codex_protocol::protocol::EnvironmentConfig {
|
||||||
|
allow_login_shell: true,
|
||||||
|
permission_profile: config.permissions.permission_profile_state().snapshot(),
|
||||||
|
shell_environment_policy: Default::default(),
|
||||||
|
exec_policy: Some(codex_execpolicy::RequirementsExecPolicy::new(
|
||||||
|
codex_execpolicy::Policy::empty(),
|
||||||
|
)),
|
||||||
|
mcp_policy: None,
|
||||||
|
network_policy: None,
|
||||||
|
selected_capability_roots: Vec::new(),
|
||||||
|
});
|
||||||
let reviewer = manager
|
let reviewer = manager
|
||||||
.spawn_internal_session(
|
.spawn_internal_session(
|
||||||
parent.thread_id,
|
parent.thread_id,
|
||||||
@@ -905,7 +1032,7 @@ async fn spawn_internal_session_preserves_parent_lineage_without_forking_history
|
|||||||
initial_history: InitialHistory::Forked(vec![RolloutItem::ResponseItem(
|
initial_history: InitialHistory::Forked(vec![RolloutItem::ResponseItem(
|
||||||
user_msg("parent history must not be inherited").into(),
|
user_msg("parent history must not be inherited").into(),
|
||||||
)]),
|
)]),
|
||||||
environments: Some(Vec::new()),
|
environments: Some(reviewer_environments),
|
||||||
..StartThreadOptions::new(config)
|
..StartThreadOptions::new(config)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -934,11 +1061,70 @@ async fn spawn_internal_session_preserves_parent_lineage_without_forking_history
|
|||||||
assert_eq!(reviewer_config.parent_thread_id, Some(parent.thread_id));
|
assert_eq!(reviewer_config.parent_thread_id, Some(parent.thread_id));
|
||||||
assert_eq!(reviewer_config.forked_from_thread_id, None);
|
assert_eq!(reviewer_config.forked_from_thread_id, None);
|
||||||
assert_eq!(reviewer_config.originator, "codex_work_desktop");
|
assert_eq!(reviewer_config.originator, "codex_work_desktop");
|
||||||
|
assert_eq!(
|
||||||
|
reviewer.thread.multi_agent_version(),
|
||||||
|
Some(MultiAgentVersion::Disabled)
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
reviewer.session_configured.parent_thread_id,
|
reviewer.session_configured.parent_thread_id,
|
||||||
Some(parent.thread_id)
|
Some(parent.thread_id)
|
||||||
);
|
);
|
||||||
assert_eq!(reviewer.session_configured.forked_from_id, None);
|
assert_eq!(reviewer.session_configured.forked_from_id, None);
|
||||||
|
assert!(reviewer.thread.session.user_instructions().await.is_none());
|
||||||
|
assert!(
|
||||||
|
reviewer
|
||||||
|
.thread
|
||||||
|
.session
|
||||||
|
.services
|
||||||
|
.exec_policy
|
||||||
|
.current()
|
||||||
|
.rules()
|
||||||
|
.contains_key("rm")
|
||||||
|
);
|
||||||
|
let reviewer_turn = reviewer.thread.session.new_default_turn().await;
|
||||||
|
let reviewer_world_state =
|
||||||
|
build_world_state_from_turn_context(&reviewer.thread.session, &reviewer_turn).await;
|
||||||
|
let reviewer_context = reviewer
|
||||||
|
.thread
|
||||||
|
.session
|
||||||
|
.build_initial_context_with_world_state(&reviewer_turn, &reviewer_world_state)
|
||||||
|
.await;
|
||||||
|
assert!(
|
||||||
|
!serde_json::to_string(&reviewer_context)
|
||||||
|
.expect("reviewer context should serialize")
|
||||||
|
.contains("managed instructions must not shape the reviewer")
|
||||||
|
);
|
||||||
|
let reviewer_environment = reviewer
|
||||||
|
.thread
|
||||||
|
.environment_selections()
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.expect("reviewer should retain its selected environment");
|
||||||
|
assert!(matches!(
|
||||||
|
reviewer_environment.config,
|
||||||
|
EnvironmentConfigState::Ready(config) if config.exec_policy.is_some()
|
||||||
|
));
|
||||||
|
assert!(
|
||||||
|
reviewer
|
||||||
|
.thread
|
||||||
|
.session
|
||||||
|
.services
|
||||||
|
.extensions
|
||||||
|
.thread_lifecycle_contributors()
|
||||||
|
.is_empty()
|
||||||
|
);
|
||||||
|
{
|
||||||
|
let observed_mcp_sources = observed_mcp_sources
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
|
assert!(!observed_mcp_sources.is_empty());
|
||||||
|
assert!(
|
||||||
|
observed_mcp_sources
|
||||||
|
.iter()
|
||||||
|
.all(|source| !source.is_internal())
|
||||||
|
);
|
||||||
|
}
|
||||||
assert_eq!(manager.list_thread_ids().await, vec![parent.thread_id]);
|
assert_eq!(manager.list_thread_ids().await, vec![parent.thread_id]);
|
||||||
assert!(manager.get_thread(reviewer.thread_id).await.is_err());
|
assert!(manager.get_thread(reviewer.thread_id).await.is_err());
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
@@ -141,8 +141,7 @@ pub(crate) fn build_tool_router(
|
|||||||
let mut registry = ToolRegistry::default();
|
let mut registry = ToolRegistry::default();
|
||||||
add_core_tool_sources(&context, &mut registry);
|
add_core_tool_sources(&context, &mut registry);
|
||||||
|
|
||||||
let hosted_specs = if crate::guardian::is_guardian_reviewer_source(&turn_context.session_source)
|
let hosted_specs = if crate::guardian::is_basic_session_source(&turn_context.session_source) {
|
||||||
{
|
|
||||||
Vec::new()
|
Vec::new()
|
||||||
} else {
|
} else {
|
||||||
let registered_mcp_tools = session.services.mcp_handler_cache.append_mcp_tools(
|
let registered_mcp_tools = session.services.mcp_handler_cache.append_mcp_tools(
|
||||||
@@ -278,7 +277,7 @@ pub(crate) fn append_source_tools(
|
|||||||
extension_tool_executors: impl IntoIterator<Item = Arc<dyn ToolExecutor<ExtensionToolCall>>>,
|
extension_tool_executors: impl IntoIterator<Item = Arc<dyn ToolExecutor<ExtensionToolCall>>>,
|
||||||
dynamic_tools: &[DynamicToolSpec],
|
dynamic_tools: &[DynamicToolSpec],
|
||||||
) -> Vec<ToolSpec> {
|
) -> Vec<ToolSpec> {
|
||||||
if crate::guardian::is_guardian_reviewer_source(&turn_context.session_source) {
|
if crate::guardian::is_basic_session_source(&turn_context.session_source) {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,7 +548,7 @@ fn hosted_model_tool_specs(
|
|||||||
) -> Vec<ToolSpec> {
|
) -> Vec<ToolSpec> {
|
||||||
// Responses Lite accepts schemas for client-executed tools, not hosted Responses tools.
|
// Responses Lite accepts schemas for client-executed tools, not hosted Responses tools.
|
||||||
if turn_context.model_info.use_responses_lite
|
if turn_context.model_info.use_responses_lite
|
||||||
|| crate::guardian::is_guardian_reviewer_source(&turn_context.session_source)
|
|| crate::guardian::is_basic_session_source(&turn_context.session_source)
|
||||||
{
|
{
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
@@ -890,27 +889,40 @@ fn add_core_tool_sources(context: &CoreToolPlanContext<'_>, registry: &mut ToolR
|
|||||||
// Guardian reviewers receive only `exec_command`, `write_stdin`, and `view_image`
|
// Guardian reviewers receive only `exec_command`, `write_stdin`, and `view_image`
|
||||||
// when a managed sandbox can enforce the parent's filesystem restrictions;
|
// when a managed sandbox can enforce the parent's filesystem restrictions;
|
||||||
// all general tool sources stay excluded.
|
// all general tool sources stay excluded.
|
||||||
if crate::guardian::is_guardian_reviewer_source(&context.turn_context.session_source) {
|
if crate::guardian::is_basic_session_source(&context.turn_context.session_source) {
|
||||||
let turn_context = context.turn_context;
|
let turn_context = context.turn_context;
|
||||||
if !matches!(
|
if !matches!(
|
||||||
turn_context.permission_profile(),
|
turn_context.permission_profile(),
|
||||||
PermissionProfile::Managed { .. }
|
PermissionProfile::Managed { .. }
|
||||||
) {
|
) || context.environments.turn_environments().any(|environment| {
|
||||||
|
!matches!(
|
||||||
|
environment.permission_profile(),
|
||||||
|
PermissionProfile::Managed { .. }
|
||||||
|
)
|
||||||
|
}) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let environment_mode = tool_environment_mode(context.environments);
|
let environment_mode = tool_environment_mode(context.environments);
|
||||||
if environment_mode.has_environment() {
|
if environment_mode.has_environment() {
|
||||||
let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple);
|
let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple);
|
||||||
registry.add(ExecCommandHandler::new(ExecCommandHandlerOptions {
|
if turn_context.config.features.enabled(Feature::ShellTool)
|
||||||
allow_login_shell: any_environment_allows_login_shell(context.environments),
|
&& turn_context.config.features.enabled(Feature::UnifiedExec)
|
||||||
exec_permission_approvals_enabled: false,
|
&& !matches!(
|
||||||
include_environment_id,
|
turn_context.model_info.shell_type,
|
||||||
include_shell_parameter: unified_exec_should_include_shell_parameter(
|
ConfigShellToolType::Disabled
|
||||||
turn_context,
|
)
|
||||||
context.environments,
|
{
|
||||||
),
|
registry.add(ExecCommandHandler::new(ExecCommandHandlerOptions {
|
||||||
}));
|
allow_login_shell: any_environment_allows_login_shell(context.environments),
|
||||||
registry.add(WriteStdinHandler);
|
exec_permission_approvals_enabled: false,
|
||||||
|
include_environment_id,
|
||||||
|
include_shell_parameter: unified_exec_should_include_shell_parameter(
|
||||||
|
turn_context,
|
||||||
|
context.environments,
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
registry.add(WriteStdinHandler);
|
||||||
|
}
|
||||||
if turn_context.config.features.enabled(Feature::ViewImage) {
|
if turn_context.config.features.enabled(Feature::ViewImage) {
|
||||||
registry.add(ViewImageHandler::new(ViewImageToolOptions {
|
registry.add(ViewImageHandler::new(ViewImageToolOptions {
|
||||||
can_request_original_image_detail: can_request_original_image_detail(
|
can_request_original_image_detail: can_request_original_image_detail(
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ use codex_protocol::openai_models::InputModality;
|
|||||||
use codex_protocol::openai_models::ToolMode;
|
use codex_protocol::openai_models::ToolMode;
|
||||||
use codex_protocol::openai_models::WebSearchToolType;
|
use codex_protocol::openai_models::WebSearchToolType;
|
||||||
use codex_protocol::protocol::EnvironmentConfigState;
|
use codex_protocol::protocol::EnvironmentConfigState;
|
||||||
|
use codex_protocol::protocol::InternalSessionSource;
|
||||||
use codex_protocol::protocol::MultiAgentVersion;
|
use codex_protocol::protocol::MultiAgentVersion;
|
||||||
use codex_protocol::protocol::SessionSource;
|
use codex_protocol::protocol::SessionSource;
|
||||||
use codex_protocol::protocol::SubAgentSource;
|
use codex_protocol::protocol::SubAgentSource;
|
||||||
@@ -476,6 +477,191 @@ fn apply_patch_accepts_environment_id(spec: &ToolSpec) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn internal_guardian_sessions_exclude_optional_core_tools() {
|
||||||
|
let (session, mut turn) = make_session_and_context().await;
|
||||||
|
turn.session_source = SessionSource::Internal(InternalSessionSource::Guardian);
|
||||||
|
set_feature(&mut turn, Feature::ViewImage, /*enabled*/ true);
|
||||||
|
Arc::make_mut(&mut turn.config).update_plan_enabled = true;
|
||||||
|
turn.multi_agent_version = MultiAgentVersion::V2;
|
||||||
|
let turn = Arc::new(turn);
|
||||||
|
let step_context = StepContext::for_test(Arc::clone(&turn));
|
||||||
|
|
||||||
|
let router = super::build_tool_router(
|
||||||
|
&session,
|
||||||
|
step_context.turn.as_ref(),
|
||||||
|
&step_context.environments,
|
||||||
|
&step_context.mcp,
|
||||||
|
/*apps_enabled*/ false,
|
||||||
|
&turn.extension_data,
|
||||||
|
/*tool_suggest_candidates*/ None,
|
||||||
|
)
|
||||||
|
.expect("build internal Guardian tool router");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
router
|
||||||
|
.model_visible_specs()
|
||||||
|
.iter()
|
||||||
|
.map(codex_tools::ToolSpec::name)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec!["exec_command", "write_stdin", "view_image"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn internal_guardian_sessions_respect_managed_shell_restrictions() {
|
||||||
|
for (disabled_feature, shell_type) in [
|
||||||
|
(Some(Feature::ShellTool), ConfigShellToolType::UnifiedExec),
|
||||||
|
(Some(Feature::UnifiedExec), ConfigShellToolType::UnifiedExec),
|
||||||
|
(None, ConfigShellToolType::Disabled),
|
||||||
|
] {
|
||||||
|
let (session, mut turn) = make_session_and_context().await;
|
||||||
|
turn.session_source = SessionSource::Internal(InternalSessionSource::Guardian);
|
||||||
|
set_feature(&mut turn, Feature::ViewImage, /*enabled*/ true);
|
||||||
|
set_feature(&mut turn, Feature::CodeMode, /*enabled*/ true);
|
||||||
|
if let Some(feature) = disabled_feature {
|
||||||
|
let config = Arc::make_mut(&mut turn.config);
|
||||||
|
config.features = crate::config::ManagedFeatures::from_configured(
|
||||||
|
config.features.get().clone(),
|
||||||
|
Some(codex_config::Sourced::new(
|
||||||
|
codex_config::FeatureRequirementsToml {
|
||||||
|
entries: BTreeMap::from([(feature.key().to_string(), false)]),
|
||||||
|
},
|
||||||
|
codex_config::RequirementSource::Unknown,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.expect("managed shell restriction should be valid");
|
||||||
|
}
|
||||||
|
Arc::make_mut(&mut turn.model_info).shell_type = shell_type;
|
||||||
|
let turn = Arc::new(turn);
|
||||||
|
let step_context = StepContext::for_test(Arc::clone(&turn));
|
||||||
|
|
||||||
|
let router = super::build_tool_router(
|
||||||
|
&session,
|
||||||
|
step_context.turn.as_ref(),
|
||||||
|
&step_context.environments,
|
||||||
|
&step_context.mcp,
|
||||||
|
/*apps_enabled*/ false,
|
||||||
|
&turn.extension_data,
|
||||||
|
/*tool_suggest_candidates*/ None,
|
||||||
|
)
|
||||||
|
.expect("build internal Guardian tool router");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
router
|
||||||
|
.model_visible_specs()
|
||||||
|
.iter()
|
||||||
|
.map(codex_tools::ToolSpec::name)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec![
|
||||||
|
codex_code_mode::PUBLIC_TOOL_NAME,
|
||||||
|
codex_code_mode::WAIT_TOOL_NAME,
|
||||||
|
"view_image",
|
||||||
|
],
|
||||||
|
"disabled feature: {disabled_feature:?}, shell type: {shell_type:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn internal_guardian_sessions_preserve_code_mode() {
|
||||||
|
let (session, mut turn) = make_session_and_context().await;
|
||||||
|
turn.session_source = SessionSource::Internal(InternalSessionSource::Guardian);
|
||||||
|
set_feature(&mut turn, Feature::CodeMode, /*enabled*/ true);
|
||||||
|
let turn = Arc::new(turn);
|
||||||
|
let step_context = StepContext::for_test(Arc::clone(&turn));
|
||||||
|
|
||||||
|
let router = super::build_tool_router(
|
||||||
|
&session,
|
||||||
|
step_context.turn.as_ref(),
|
||||||
|
&step_context.environments,
|
||||||
|
&step_context.mcp,
|
||||||
|
/*apps_enabled*/ false,
|
||||||
|
&turn.extension_data,
|
||||||
|
/*tool_suggest_candidates*/ None,
|
||||||
|
)
|
||||||
|
.expect("build internal Guardian tool router");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
router
|
||||||
|
.model_visible_specs()
|
||||||
|
.iter()
|
||||||
|
.any(|tool| tool.name() == codex_code_mode::PUBLIC_TOOL_NAME)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
router
|
||||||
|
.model_visible_specs()
|
||||||
|
.iter()
|
||||||
|
.any(|tool| tool.name() == codex_code_mode::WAIT_TOOL_NAME)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn internal_guardian_sessions_require_managed_secondary_environments() {
|
||||||
|
for (secondary_profile, expected_tools) in [
|
||||||
|
(
|
||||||
|
codex_protocol::models::PermissionProfile::workspace_write(),
|
||||||
|
vec!["exec_command", "write_stdin", "view_image"],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
codex_protocol::models::PermissionProfile::Disabled,
|
||||||
|
Vec::new(),
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let (session, mut turn) = make_session_and_context().await;
|
||||||
|
turn.session_source = SessionSource::Internal(InternalSessionSource::Guardian);
|
||||||
|
set_feature(&mut turn, Feature::ViewImage, /*enabled*/ true);
|
||||||
|
let TurnEnvironmentState::Ready(primary) = turn
|
||||||
|
.environments
|
||||||
|
.environments
|
||||||
|
.first_mut()
|
||||||
|
.expect("primary environment")
|
||||||
|
else {
|
||||||
|
panic!("primary environment should be ready");
|
||||||
|
};
|
||||||
|
primary.config_mut().permission_profile =
|
||||||
|
codex_protocol::models::PermissionProfileSnapshot::legacy(
|
||||||
|
codex_protocol::models::PermissionProfile::workspace_write(),
|
||||||
|
);
|
||||||
|
duplicate_primary_environment(&mut turn);
|
||||||
|
let secondary_workspace_root =
|
||||||
|
codex_utils_path_uri::PathUri::from_abs_path(&turn.config.cwd.join("secondary"));
|
||||||
|
let TurnEnvironmentState::Ready(secondary) = turn
|
||||||
|
.environments
|
||||||
|
.environments
|
||||||
|
.get_mut(1)
|
||||||
|
.expect("secondary environment")
|
||||||
|
else {
|
||||||
|
panic!("secondary environment should be ready");
|
||||||
|
};
|
||||||
|
secondary.selection.workspace_roots = vec![secondary_workspace_root];
|
||||||
|
secondary.config_mut().permission_profile =
|
||||||
|
codex_protocol::models::PermissionProfileSnapshot::legacy(secondary_profile);
|
||||||
|
let turn = Arc::new(turn);
|
||||||
|
let step_context = StepContext::for_test(Arc::clone(&turn));
|
||||||
|
|
||||||
|
let router = super::build_tool_router(
|
||||||
|
&session,
|
||||||
|
step_context.turn.as_ref(),
|
||||||
|
&step_context.environments,
|
||||||
|
&step_context.mcp,
|
||||||
|
/*apps_enabled*/ false,
|
||||||
|
&turn.extension_data,
|
||||||
|
/*tool_suggest_candidates*/ None,
|
||||||
|
)
|
||||||
|
.expect("build internal Guardian tool router");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
router
|
||||||
|
.model_visible_specs()
|
||||||
|
.iter()
|
||||||
|
.map(codex_tools::ToolSpec::name)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
expected_tools
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn wait_for_environment_requires_feature_and_uses_host_config_when_present() {
|
async fn wait_for_environment_requires_feature_and_uses_host_config_when_present() {
|
||||||
const TOOL_DESCRIPTION: &str = "Host-provided wait tool description";
|
const TOOL_DESCRIPTION: &str = "Host-provided wait tool description";
|
||||||
|
|||||||
Reference in new issue
Block a user