mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
## Why The transcript supplied to Guardian may omit earlier user instructions, restrictions, or revoked permissions. History retrieval lets the reviewer check relevant authorization before approving actions with side effects. ## What changed - Add the disabled-by-default `guardian_conversation_history_tools` feature. With Apps enabled, expose `user_message.search_messages` and `user_message.read_messages` through the parent's live Apps connection and conversation identity. - Recheck the parent's current app and tool policy on each call, rejecting disabled tools and calls requiring approval. - Add retrieval instructions that distinguish user authorization from assistant context and account for later revocations and incomplete results. Allow overrides through `auto_review.experimental_conversation_history_prompt`. - Limit history responses to an estimated 4,000 tokens by default, configurable through `auto_review.conversation_history_max_output_tokens`, while preserving stricter parent and reviewer limits. - Allow explicit extension registries for isolated sessions and prevent those sessions from inheriting the implicit Apps connection. ## Testing Add scenarios covering history tool exposure, parent connection and identity reuse, custom prompts, output truncation, stricter reviewer budgets, and live permission changes on a reused reviewer. Extend configuration coverage for blank prompt overrides. GitOrigin-RevId: 055a69b113c8548bd58e7d5618ea026ba6d1ffb1
431 lines
17 KiB
Rust
431 lines
17 KiB
Rust
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
|
|
use crate::config::Config;
|
|
use codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID;
|
|
use codex_config::McpServerConfig;
|
|
use codex_connectors::ConnectorRuntimeManager;
|
|
use codex_connectors::PluginConnectorSource;
|
|
use codex_core_plugins::PluginsManager;
|
|
use codex_exec_server::ExecutorCapabilityDiscoverySnapshot;
|
|
use codex_extension_api::ExtensionData;
|
|
use codex_extension_api::ExtensionDataInit;
|
|
use codex_extension_api::ExtensionRegistry;
|
|
use codex_extension_api::McpServerContribution;
|
|
use codex_extension_api::McpServerContributionContext;
|
|
use codex_extension_api::SelectedPlugin;
|
|
use codex_extension_api::SelectedPluginIdentity;
|
|
use codex_extension_api::SelectedPluginSnapshot;
|
|
use codex_extension_api::SessionIsolation;
|
|
use codex_features::Feature;
|
|
use codex_login::CodexAuth;
|
|
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
|
|
use codex_mcp::EffectiveMcpServer;
|
|
use codex_mcp::McpConfig;
|
|
use codex_mcp::McpEnvironmentAuthority;
|
|
use codex_mcp::McpPluginAttribution;
|
|
use codex_mcp::McpServerRegistration;
|
|
use codex_mcp::McpToolCatalogCache;
|
|
use codex_mcp::ToolInfo;
|
|
use codex_mcp::codex_apps_mcp_server_config;
|
|
use codex_mcp::configured_mcp_servers;
|
|
use codex_mcp::effective_mcp_servers;
|
|
use codex_plugin::AppConnectorId;
|
|
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
|
use codex_protocol::protocol::EnvironmentConfigState;
|
|
use codex_protocol::protocol::SessionSource;
|
|
use codex_protocol::protocol::TurnEnvironmentSelection;
|
|
|
|
const LEGACY_CODEX_APPS_REGISTRATION_ID: &str = "legacy_codex_apps";
|
|
|
|
/// MCP configuration and capability availability derived from the same inputs.
|
|
#[derive(Clone)]
|
|
pub(crate) struct McpRuntimeProjection {
|
|
pub(crate) config: McpConfig,
|
|
pub(crate) plugins_available: bool,
|
|
pub(crate) selected_plugins: SelectedPluginSnapshot,
|
|
}
|
|
|
|
pub(crate) enum McpEnvironmentScope<'a> {
|
|
/// Controller-level operations without an associated thread.
|
|
HostOnly,
|
|
/// Every thread selection captured for this operation, including unavailable configurations.
|
|
Selected(&'a [TurnEnvironmentSelection]),
|
|
}
|
|
|
|
impl<'a> McpEnvironmentScope<'a> {
|
|
pub(crate) fn authority_for(&self, environment_id: &str) -> McpEnvironmentAuthority<'a> {
|
|
let Self::Selected(selections) = *self else {
|
|
return McpEnvironmentAuthority::Unrestricted;
|
|
};
|
|
let Some(selection) = selections
|
|
.iter()
|
|
.find(|selection| selection.environment_id == environment_id)
|
|
else {
|
|
return if environment_id == DEFAULT_MCP_SERVER_ENVIRONMENT_ID {
|
|
McpEnvironmentAuthority::Unrestricted
|
|
} else {
|
|
McpEnvironmentAuthority::SelectedPluginsOnly
|
|
};
|
|
};
|
|
match &selection.config {
|
|
EnvironmentConfigState::FromThread => McpEnvironmentAuthority::Unrestricted,
|
|
EnvironmentConfigState::Pending | EnvironmentConfigState::Failed(_) => {
|
|
McpEnvironmentAuthority::Unavailable
|
|
}
|
|
EnvironmentConfigState::Ready(config) => config
|
|
.mcp_policy
|
|
.as_ref()
|
|
.map_or(McpEnvironmentAuthority::Unrestricted, |policy| {
|
|
McpEnvironmentAuthority::Restricted(policy)
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) struct McpThreadIdentity<'a> {
|
|
pub(crate) auth_changed: bool,
|
|
pub(crate) session_source: &'a SessionSource,
|
|
pub(crate) originator: &'a str,
|
|
pub(crate) environments: McpEnvironmentScope<'a>,
|
|
pub(crate) disabled_plugin_ids: &'a [String],
|
|
}
|
|
|
|
enum OrderedMcpOverlay {
|
|
Set(Box<McpServerRegistration>),
|
|
Remove {
|
|
contributor_id: &'static str,
|
|
contribution_order: usize,
|
|
name: String,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct McpManager {
|
|
plugins_manager: Arc<PluginsManager>,
|
|
extensions: Arc<ExtensionRegistry<Config>>,
|
|
codex_apps_tools_cache: ConnectorRuntimeManager<ToolInfo>,
|
|
tool_catalog_cache: McpToolCatalogCache,
|
|
}
|
|
|
|
impl McpManager {
|
|
pub fn new(plugins_manager: Arc<PluginsManager>) -> Self {
|
|
Self::new_with_extensions(
|
|
plugins_manager,
|
|
codex_extension_api::empty_extension_registry(),
|
|
ConnectorRuntimeManager::default(),
|
|
)
|
|
}
|
|
|
|
/// Creates a manager that resolves host-installed MCP contributions.
|
|
pub fn new_with_extensions(
|
|
plugins_manager: Arc<PluginsManager>,
|
|
extensions: Arc<ExtensionRegistry<Config>>,
|
|
codex_apps_tools_cache: ConnectorRuntimeManager<ToolInfo>,
|
|
) -> Self {
|
|
Self {
|
|
plugins_manager,
|
|
extensions,
|
|
codex_apps_tools_cache,
|
|
tool_catalog_cache: McpToolCatalogCache::default(),
|
|
}
|
|
}
|
|
|
|
pub fn codex_apps_tools_cache(&self) -> ConnectorRuntimeManager<ToolInfo> {
|
|
self.codex_apps_tools_cache.clone()
|
|
}
|
|
|
|
pub fn tool_catalog_cache(&self) -> McpToolCatalogCache {
|
|
self.tool_catalog_cache.clone()
|
|
}
|
|
|
|
/// Returns the MCP config after applying compatibility built-ins and
|
|
/// runtime-only extension overlays.
|
|
pub async fn runtime_config(&self, config: &Config) -> McpConfig {
|
|
self.runtime_config_with_context(
|
|
McpServerContributionContext::global(config),
|
|
// Threadless discovery and control-plane paths have no effective thread
|
|
// originator; active-thread tool calls use runtime_config_for_step below.
|
|
/*originator*/
|
|
None,
|
|
McpEnvironmentScope::HostOnly,
|
|
&[],
|
|
)
|
|
.await
|
|
.config
|
|
}
|
|
|
|
#[tracing::instrument(name = "mcp.runtime_config.project_for_step", skip_all)]
|
|
pub(crate) async fn runtime_config_for_step(
|
|
&self,
|
|
config: &Config,
|
|
thread_init: &ExtensionDataInit,
|
|
thread_store: &ExtensionData,
|
|
identity: McpThreadIdentity<'_>,
|
|
ready_selected_capability_roots: &[SelectedCapabilityRoot],
|
|
executor_capability_discovery: Option<&ExecutorCapabilityDiscoverySnapshot>,
|
|
) -> McpRuntimeProjection {
|
|
self.runtime_config_with_context(
|
|
McpServerContributionContext::for_step(
|
|
config,
|
|
thread_init,
|
|
thread_store,
|
|
identity.originator,
|
|
ready_selected_capability_roots,
|
|
executor_capability_discovery,
|
|
)
|
|
.with_session_source(identity.session_source)
|
|
.with_auth_changed(identity.auth_changed),
|
|
Some(identity.originator),
|
|
identity.environments,
|
|
identity.disabled_plugin_ids,
|
|
)
|
|
.await
|
|
}
|
|
|
|
async fn runtime_config_with_context(
|
|
&self,
|
|
context: McpServerContributionContext<'_, Config>,
|
|
originator: Option<&str>,
|
|
environment_scope: McpEnvironmentScope<'_>,
|
|
disabled_plugin_ids: &[String],
|
|
) -> McpRuntimeProjection {
|
|
let config = context.config();
|
|
let mut selected_plugin_connector_sources = Vec::new();
|
|
let mut selected_plugin_registrations = Vec::new();
|
|
let mut selected_plugins = Vec::new();
|
|
let mut disabled_plugin_roots = Vec::new();
|
|
let mut overlays = Vec::new();
|
|
// A contributor can emit multiple ordered actions, so order each action globally rather
|
|
// than enumerating contributors.
|
|
let mut contribution_order = 0;
|
|
let mut plugin_selection_order = 0;
|
|
for contributor in self.extensions.mcp_server_contributors() {
|
|
// Finish executor plugins before asking for hosted contributions, which may change
|
|
// accounts while an executor is still loading.
|
|
for SelectedPlugin {
|
|
selected_root_id,
|
|
plugin_id,
|
|
mcp,
|
|
} in contributor.selected_plugins(context).await
|
|
{
|
|
let selection_order = plugin_selection_order;
|
|
plugin_selection_order += 1;
|
|
let disabled = disabled_plugin_ids.contains(&plugin_id);
|
|
if !config.features.enabled(Feature::Plugins) || disabled {
|
|
disabled_plugin_roots.push(selected_root_id);
|
|
} else {
|
|
selected_plugins.push(SelectedPluginIdentity {
|
|
selected_root_id: Some(selected_root_id),
|
|
plugin_id: plugin_id.clone(),
|
|
});
|
|
}
|
|
let contribution = mcp.await;
|
|
if !disabled {
|
|
for (name, server) in contribution.servers {
|
|
selected_plugin_registrations.push(
|
|
McpServerRegistration::from_selected_plugin(
|
|
name,
|
|
McpPluginAttribution::new(
|
|
plugin_id.clone(),
|
|
contribution.plugin_display_name.clone(),
|
|
),
|
|
selection_order,
|
|
&contribution.source_environment_id,
|
|
server,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
// Disabled plugins still tell Apps which connectors to hide.
|
|
if config.features.enabled(Feature::Plugins)
|
|
&& !contribution.connector_ids.is_empty()
|
|
{
|
|
selected_plugin_connector_sources.push(
|
|
PluginConnectorSource::from_connector_ids(
|
|
plugin_id,
|
|
contribution.plugin_display_name,
|
|
contribution.connector_ids.into_iter().map(AppConnectorId),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
for contribution in contributor.contribute(context).await {
|
|
match contribution {
|
|
McpServerContribution::Set { name, config } => {
|
|
overlays.push(OrderedMcpOverlay::Set(Box::new(
|
|
McpServerRegistration::from_extension(
|
|
name,
|
|
contributor.id(),
|
|
contribution_order,
|
|
*config,
|
|
),
|
|
)));
|
|
}
|
|
McpServerContribution::SetWithProtocolMode {
|
|
name,
|
|
config,
|
|
protocol_mode,
|
|
} => {
|
|
overlays.push(OrderedMcpOverlay::Set(Box::new(
|
|
McpServerRegistration::from_extension(
|
|
name,
|
|
contributor.id(),
|
|
contribution_order,
|
|
*config,
|
|
)
|
|
.with_protocol_mode(protocol_mode),
|
|
)));
|
|
}
|
|
McpServerContribution::HostedApps {
|
|
config,
|
|
protocol_mode,
|
|
} => {
|
|
let mut registration = McpServerRegistration::from_hosted_apps(
|
|
contributor.id(),
|
|
contribution_order,
|
|
*config,
|
|
);
|
|
if let Some(protocol_mode) = protocol_mode {
|
|
registration = registration.with_protocol_mode(protocol_mode);
|
|
}
|
|
overlays.push(OrderedMcpOverlay::Set(Box::new(registration)));
|
|
}
|
|
McpServerContribution::HostedPluginConnectors {
|
|
plugin_id,
|
|
plugin_display_name,
|
|
connector_ids,
|
|
} => {
|
|
if config.features.enabled(Feature::Plugins) {
|
|
if !disabled_plugin_ids.contains(&plugin_id) {
|
|
selected_plugins.push(SelectedPluginIdentity {
|
|
selected_root_id: None,
|
|
plugin_id: plugin_id.clone(),
|
|
});
|
|
}
|
|
if !connector_ids.is_empty() {
|
|
selected_plugin_connector_sources.push(
|
|
PluginConnectorSource::from_connector_ids(
|
|
plugin_id,
|
|
plugin_display_name,
|
|
connector_ids.into_iter().map(AppConnectorId),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
McpServerContribution::Remove { name } => {
|
|
overlays.push(OrderedMcpOverlay::Remove {
|
|
contributor_id: contributor.id(),
|
|
contribution_order,
|
|
name,
|
|
});
|
|
}
|
|
}
|
|
contribution_order += 1;
|
|
}
|
|
}
|
|
|
|
let loaded_plugins = self
|
|
.plugins_manager
|
|
.plugins_for_config(&config.plugins_config_input())
|
|
.await;
|
|
let host_plugin_connector_sources = loaded_plugins
|
|
.capability_summaries()
|
|
.iter()
|
|
.map(PluginConnectorSource::from);
|
|
let connector_snapshot = if config.features.enabled(Feature::Plugins) {
|
|
self.plugins_manager.connector_snapshot(
|
|
host_plugin_connector_sources.chain(selected_plugin_connector_sources),
|
|
disabled_plugin_ids,
|
|
)
|
|
} else {
|
|
Default::default()
|
|
};
|
|
let loaded_plugins = loaded_plugins.without_plugins(disabled_plugin_ids);
|
|
let plugins_available =
|
|
!selected_plugins.is_empty() || !loaded_plugins.capability_summaries().is_empty();
|
|
let mut mcp_config = config
|
|
.to_mcp_config_with_loaded_plugins(&loaded_plugins, selected_plugin_registrations);
|
|
let mut catalog = mcp_config.mcp_server_catalog.to_builder();
|
|
// Isolated sessions do not inherit the implicit Apps connection.
|
|
if mcp_config.apps_enabled
|
|
&& context
|
|
.thread_init()
|
|
.and_then(codex_extension_api::ExtensionDataInit::get::<SessionIsolation>)
|
|
.as_deref()
|
|
!= Some(&SessionIsolation::Isolated)
|
|
{
|
|
catalog.register(McpServerRegistration::from_compatibility(
|
|
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
|
LEGACY_CODEX_APPS_REGISTRATION_ID,
|
|
codex_apps_mcp_server_config(
|
|
&mcp_config.chatgpt_base_url,
|
|
mcp_config.apps_mcp_product_sku.as_deref(),
|
|
originator,
|
|
),
|
|
));
|
|
} else {
|
|
catalog.remove_compatibility(
|
|
CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
|
LEGACY_CODEX_APPS_REGISTRATION_ID,
|
|
);
|
|
}
|
|
|
|
for overlay in overlays {
|
|
match overlay {
|
|
OrderedMcpOverlay::Set(registration) => catalog.register(*registration),
|
|
OrderedMcpOverlay::Remove {
|
|
contributor_id,
|
|
contribution_order,
|
|
name,
|
|
} => catalog.remove_extension(name, contributor_id, contribution_order),
|
|
}
|
|
}
|
|
let catalog = catalog.build_with_environment_authority(|environment_id| {
|
|
environment_scope.authority_for(environment_id)
|
|
});
|
|
for conflict in catalog.conflicts() {
|
|
tracing::warn!(
|
|
server = conflict.name,
|
|
outcome = ?conflict.outcome,
|
|
contenders = ?conflict.contenders,
|
|
"conflicting MCP server actions; using resolved catalog outcome"
|
|
);
|
|
}
|
|
mcp_config.mcp_server_catalog = catalog;
|
|
mcp_config.connector_snapshot = connector_snapshot;
|
|
McpRuntimeProjection {
|
|
config: mcp_config,
|
|
plugins_available,
|
|
selected_plugins: SelectedPluginSnapshot {
|
|
plugins: selected_plugins,
|
|
disabled_plugin_roots,
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Returns config- and plugin-backed servers without runtime contributions.
|
|
pub async fn configured_servers(&self, config: &Config) -> HashMap<String, McpServerConfig> {
|
|
let mcp_config = config.to_mcp_config(self.plugins_manager.as_ref()).await;
|
|
configured_mcp_servers(&mcp_config)
|
|
}
|
|
|
|
/// Returns configured and host-contributed servers before auth gating.
|
|
pub async fn runtime_servers(&self, config: &Config) -> HashMap<String, McpServerConfig> {
|
|
let mcp_config = self.runtime_config(config).await;
|
|
configured_mcp_servers(&mcp_config)
|
|
}
|
|
|
|
/// Returns runtime servers after auth gating and compatibility built-ins.
|
|
pub async fn effective_servers(
|
|
&self,
|
|
config: &Config,
|
|
auth: Option<&CodexAuth>,
|
|
) -> HashMap<String, EffectiveMcpServer> {
|
|
let mcp_config = self.runtime_config(config).await;
|
|
effective_mcp_servers(&mcp_config, auth)
|
|
}
|
|
}
|