extension: wire extension registries into sessions (#21737)

## Why

[#21736](https://github.com/openai/codex/pull/21736) introduces the
typed extension API, but the runtime does not yet carry a registry
through thread/session startup or give contributors host-owned stores to
read from. This PR wires that host-side path so later feature migrations
can move product-specific behavior behind typed contributions without
adding another bespoke seam directly to `codex-core`.

## What changed

- Thread `ExtensionRegistry<Config>` through `ThreadManager`,
`CodexSpawnArgs`, `Session`, and sub-agent spawn paths.
- Wire `ThreadStartContributor` and `ContextContributor`
- Expose the small supporting surface needed by non-core callers that
construct threads directly, including `empty_extension_registry()`
through `codex-core-api`.

This PR lands the host plumbing only: the app-server registry is still
empty, and concrete feature migrations are intended to follow
separately.
This commit is contained in:
jif-oai
2026-05-11 11:38:18 +02:00
committed by GitHub
parent d2c3ebac1f
commit 436c0df658
28 changed files with 188 additions and 4 deletions
+30
View File
@@ -53,6 +53,7 @@ use codex_config::types::OAuthCredentialsStoreMode;
use codex_exec_server::Environment;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::FileSystemSandboxContext;
use codex_extension_api::PromptSlot;
use codex_features::FEATURES;
use codex_features::Feature;
use codex_features::unstable_features_warning_event;
@@ -392,6 +393,7 @@ pub(crate) struct CodexSpawnArgs {
pub(crate) skills_manager: Arc<SkillsManager>,
pub(crate) plugins_manager: Arc<PluginsManager>,
pub(crate) mcp_manager: Arc<McpManager>,
pub(crate) extensions: Arc<codex_extension_api::ExtensionRegistry<crate::config::Config>>,
pub(crate) conversation_history: InitialHistory,
pub(crate) session_source: SessionSource,
pub(crate) thread_source: Option<ThreadSource>,
@@ -455,6 +457,7 @@ impl Codex {
skills_manager,
plugins_manager,
mcp_manager,
extensions,
conversation_history,
session_source,
thread_source,
@@ -650,6 +653,7 @@ impl Codex {
skills_manager,
plugins_manager,
mcp_manager.clone(),
extensions,
agent_control,
environment_manager,
analytics_events_client,
@@ -2570,6 +2574,7 @@ impl Session {
) -> Vec<ResponseItem> {
let mut developer_sections = Vec::<String>::with_capacity(8);
let mut contextual_user_sections = Vec::<String>::with_capacity(2);
let mut separate_developer_sections = Vec::<String>::new();
let (
reference_context_item,
previous_turn_settings,
@@ -2714,6 +2719,24 @@ impl Session {
{
developer_sections.push(commit_message_instruction);
}
for contributor in self.services.extensions.context_contributors() {
for fragment in contributor.contribute(
&self.services.session_extension_data,
&self.services.thread_extension_data,
) {
match fragment.slot() {
PromptSlot::DeveloperPolicy | PromptSlot::DeveloperCapabilities => {
developer_sections.push(fragment.text().to_string());
}
PromptSlot::ContextualUser => {
contextual_user_sections.push(fragment.text().to_string());
}
PromptSlot::SeparateDeveloper => {
separate_developer_sections.push(fragment.text().to_string());
}
}
}
}
if let Some(user_instructions) = turn_context.user_instructions.as_deref() {
contextual_user_sections.push(
UserInstructions {
@@ -2746,6 +2769,13 @@ impl Session {
{
items.push(developer_message);
}
for section in separate_developer_sections {
if let Some(developer_message) =
crate::context_manager::updates::build_developer_update_item(vec![section])
{
items.push(developer_message);
}
}
if let Some(usage_hint_text) = multi_agent_v2_usage_hint_text
&& let Some(usage_hint_message) =
crate::context_manager::updates::build_developer_update_item(vec![
+15
View File
@@ -364,6 +364,7 @@ impl Session {
skills_manager: Arc<SkillsManager>,
plugins_manager: Arc<PluginsManager>,
mcp_manager: Arc<McpManager>,
extensions: Arc<codex_extension_api::ExtensionRegistry<crate::config::Config>>,
agent_control: AgentControl,
environment_manager: Arc<EnvironmentManager>,
analytics_events_client: Option<AnalyticsEventsClient>,
@@ -810,6 +811,16 @@ impl Session {
SessionId::from(thread_id)
};
let agent_control = agent_control.with_session_id(session_id);
let session_extension_data = codex_extension_api::ExtensionData::new();
let thread_extension_data = codex_extension_api::ExtensionData::new();
for contributor in extensions.thread_start_contributors() {
contributor.contribute(
config.as_ref(),
&session_extension_data,
&thread_extension_data,
);
}
let services = SessionServices {
// Initialize the MCP connection manager with an uninitialized
// instance. It will be replaced with one created via
@@ -845,6 +856,10 @@ impl Session {
skills_manager,
plugins_manager: Arc::clone(&plugins_manager),
mcp_manager: Arc::clone(&mcp_manager),
extensions,
// TODO(jif): extract session to share between sub-agents
session_extension_data,
thread_extension_data,
agent_control,
network_proxy,
network_approval: Arc::clone(&network_approval),
+76
View File
@@ -3725,6 +3725,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
skills_manager,
plugins_manager,
mcp_manager,
Arc::new(codex_extension_api::ExtensionRegistryBuilder::new().build()),
AgentControl::default(),
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
@@ -3871,6 +3872,9 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
skills_manager,
plugins_manager,
mcp_manager,
extensions: Arc::new(codex_extension_api::ExtensionRegistryBuilder::new().build()),
session_extension_data: codex_extension_api::ExtensionData::new(),
thread_extension_data: codex_extension_api::ExtensionData::new(),
agent_control,
network_proxy: None,
network_approval: Arc::clone(&network_approval),
@@ -4061,6 +4065,7 @@ async fn make_session_with_config_and_rx(
skills_manager,
plugins_manager,
mcp_manager,
Arc::new(codex_extension_api::ExtensionRegistryBuilder::new().build()),
AgentControl::default(),
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
@@ -4163,6 +4168,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
skills_manager,
plugins_manager,
mcp_manager,
Arc::new(codex_extension_api::ExtensionRegistryBuilder::new().build()),
agent_control,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
/*analytics_events_client*/ None,
@@ -5586,6 +5592,9 @@ where
skills_manager,
plugins_manager,
mcp_manager,
extensions: Arc::new(codex_extension_api::ExtensionRegistryBuilder::new().build()),
session_extension_data: codex_extension_api::ExtensionData::new(),
thread_extension_data: codex_extension_api::ExtensionData::new(),
agent_control,
network_proxy: None,
network_approval: Arc::clone(&network_approval),
@@ -6133,6 +6142,73 @@ async fn make_multi_agent_v2_usage_hint_test_session(
(session, turn_context)
}
struct GitAttributionTestContributor;
struct GitAttributionTestState;
impl codex_extension_api::ContextContributor for GitAttributionTestContributor {
fn contribute(
&self,
_session_store: &codex_extension_api::ExtensionData,
thread_store: &codex_extension_api::ExtensionData,
) -> Vec<codex_extension_api::PromptFragment> {
thread_store
.get::<GitAttributionTestState>()
.is_some()
.then(|| {
codex_extension_api::PromptFragment::developer_policy(
"git attribution extension enabled",
)
})
.into_iter()
.collect()
}
}
fn git_attribution_test_registry()
-> Arc<codex_extension_api::ExtensionRegistry<crate::config::Config>> {
let mut builder = codex_extension_api::ExtensionRegistryBuilder::new();
builder.prompt_contributor(Arc::new(GitAttributionTestContributor));
Arc::new(builder.build())
}
#[tokio::test]
async fn build_initial_context_includes_git_attribution_from_extensions() {
let (mut session, turn_context) = make_session_and_context().await;
session.services.extensions = git_attribution_test_registry();
session
.services
.thread_extension_data
.insert(GitAttributionTestState);
let initial_context = session.build_initial_context(&turn_context).await;
let developer_messages = developer_message_texts(&initial_context);
assert!(
developer_messages
.iter()
.flatten()
.any(|text| *text == "git attribution extension enabled"),
"expected git attribution developer text, got {developer_messages:?}"
);
}
#[tokio::test]
async fn build_initial_context_omits_git_attribution_when_feature_is_disabled() {
let (mut session, turn_context) = make_session_and_context().await;
session.services.extensions = git_attribution_test_registry();
let initial_context = session.build_initial_context(&turn_context).await;
let developer_messages = developer_message_texts(&initial_context);
assert!(
!developer_messages
.iter()
.flatten()
.any(|text| *text == "git attribution extension enabled"),
"did not expect git attribution developer text, got {developer_messages:?}"
);
}
#[tokio::test]
async fn build_initial_context_adds_multi_agent_v2_root_usage_hint_as_developer_message() {
let (session, turn_context) =
@@ -742,6 +742,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
skills_manager,
plugins_manager,
mcp_manager,
extensions: codex_extension_api::empty_extension_registry(),
conversation_history: InitialHistory::New,
session_source: SessionSource::SubAgent(SubAgentSource::Other(
GUARDIAN_REVIEWER_NAME.to_string(),
+3 -3
View File
@@ -52,11 +52,11 @@ impl TurnEnvironment {
/// The context needed for a single turn of the thread.
#[derive(Debug)]
pub(crate) struct TurnContext {
pub struct TurnContext {
pub(crate) sub_id: String,
pub(crate) trace_id: Option<String>,
pub(crate) realtime_active: bool,
pub(crate) config: Arc<Config>,
pub config: Arc<Config>,
pub(crate) auth_manager: Option<Arc<AuthManager>>,
pub(crate) model_info: ModelInfo,
pub(crate) session_telemetry: SessionTelemetry,
@@ -84,7 +84,7 @@ pub(crate) struct TurnContext {
pub(crate) windows_sandbox_level: WindowsSandboxLevel,
pub(crate) shell_environment_policy: ShellEnvironmentPolicy,
pub(crate) tools_config: ToolsConfig,
pub(crate) features: ManagedFeatures,
pub features: ManagedFeatures,
pub(crate) ghost_snapshot: GhostSnapshotConfig,
pub(crate) final_output_json_schema: Option<Value>,
pub(crate) codex_self_exe: Option<PathBuf>,