mirror of
https://github.com/openai/codex.git
synced 2026-11-02 19:43:24 +00:00
## Stack - Base: #27184 - This PR is the second vertical and should be reviewed against `jif/external-plugins-1`, not `main`. ## Why CCA is moving toward a split runtime where the orchestrator may have no filesystem or executor, but it still needs to activate remotely hosted plugin components. HTTP MCP servers are the simplest complete example: they need configuration and host authentication, but they do not need an executor process. The Apps MCP endpoint is currently synthesized by a special-purpose loader inside the MCP runtime. That works locally, but it leaves hosted MCP activation outside the extension model being established in #27184. It also makes the Apps path a poor foundation for plugins whose skills, MCP servers, connectors, and hooks may come from different sources or execute in different places. This PR moves that one behavior behind an extension-owned contribution while preserving the existing local fallback. It deliberately does not introduce a generic plugin activation framework. ## What changed ### MCP extension contribution `codex-extension-api` gains an ordered `McpServerContributor` contract. A contributor returns typed `Set` or `Remove` overlays for MCP server configuration; later contributors win for the names they own. The contract stays at the existing MCP configuration boundary. Extensions do not create a second connection manager or transport abstraction. ### Hosted Apps MCP extension A new `codex-mcp-extension` contributes the reserved `codex_apps` server from the existing Apps feature, ChatGPT base URL, path override, and product SKU configuration. When `apps_mcp_path_override` is enabled for `https://chatgpt.com`, the resulting streamable HTTP endpoint is `https://chatgpt.com/backend-api/ps/mcp`. The existing ChatGPT-auth gate remains authoritative, so this server can run in an orchestrator-only process without being exposed for API-key sessions. ### One resolved runtime view `McpManager` now distinguishes three views: - **configured:** config- and plugin-backed servers before extension overlays; - **runtime:** configured servers plus host-installed extension contributions; - **effective:** runtime servers after auth gating and compatibility built-ins. App-server installs the hosted MCP extension and uses the runtime view for thread startup, refresh, status, threadless resource reads, connector discovery, and MCP OAuth lookup. This keeps `mcpServer/oauth/login` consistent with the servers exposed by the other MCP APIs. The hosted Apps server itself continues to use existing ChatGPT host authentication rather than MCP OAuth. ## Compatibility Hosts that do not install the MCP extension retain the existing Apps MCP synthesis path. This preserves current local-only, CLI, and standalone-host behavior while app-server exercises the extension path. Disabling Apps removes the reserved `codex_apps` entry, and losing ChatGPT auth removes it from the effective runtime view. Executor availability is not consulted for this HTTP transport. ## Follow-ups The next vertical will resolve a manifest-declared stdio MCP server from an executor-selected plugin root and execute it in the environment that owns that root. Later verticals can add backend-owned skills, connector metadata, hooks, durable selection semantics, and incremental local convergence without changing the component-specific runtime boundaries introduced here. ## Verification Focused coverage was added for: - contributing the hosted Apps MCP at `/backend-api/ps/mcp` without an executor; - requiring ChatGPT auth in the effective runtime view; - removing a reserved configured Apps server when the Apps feature is disabled. `cargo check -p codex-app-server -p codex-mcp-extension -p codex-extension-api -p codex-mcp` passed. Tests and Clippy were not run locally under the current development instruction; CI provides the full validation pass.
210 lines
7.8 KiB
Rust
210 lines
7.8 KiB
Rust
use std::future::Future;
|
|
use std::sync::Arc;
|
|
|
|
use codex_context_fragments::ContextualUserFragment;
|
|
use codex_protocol::items::TurnItem;
|
|
use codex_protocol::protocol::ReviewDecision;
|
|
use codex_protocol::protocol::TokenUsageInfo;
|
|
use codex_tools::ToolCall;
|
|
use codex_tools::ToolExecutor;
|
|
|
|
use crate::ExtensionData;
|
|
|
|
mod mcp;
|
|
mod prompt;
|
|
mod thread_lifecycle;
|
|
mod tool_lifecycle;
|
|
mod turn_input;
|
|
mod turn_lifecycle;
|
|
|
|
pub use mcp::McpServerContribution;
|
|
pub use prompt::PromptFragment;
|
|
pub use prompt::PromptSlot;
|
|
pub use thread_lifecycle::ThreadIdleInput;
|
|
pub use thread_lifecycle::ThreadResumeInput;
|
|
pub use thread_lifecycle::ThreadStartInput;
|
|
pub use thread_lifecycle::ThreadStopInput;
|
|
pub use tool_lifecycle::ToolCallOutcome;
|
|
pub use tool_lifecycle::ToolCallSource;
|
|
pub use tool_lifecycle::ToolFinishInput;
|
|
pub use tool_lifecycle::ToolLifecycleFuture;
|
|
pub use tool_lifecycle::ToolStartInput;
|
|
pub use turn_input::TurnInputContext;
|
|
pub use turn_input::TurnInputEnvironment;
|
|
pub use turn_lifecycle::TurnAbortInput;
|
|
pub use turn_lifecycle::TurnErrorInput;
|
|
pub use turn_lifecycle::TurnStartInput;
|
|
pub use turn_lifecycle::TurnStopInput;
|
|
|
|
/// Extension contribution that resolves runtime MCP servers from host config.
|
|
///
|
|
/// Contributors run in registration order. Later contributions for the same
|
|
/// name replace earlier ones. Implementations must contribute only names they
|
|
/// own and must apply any source-specific policy before returning a server.
|
|
/// Plugin-owned servers and their provenance continue to be resolved by the
|
|
/// plugin manager until that ownership moves into an extension explicitly.
|
|
#[async_trait::async_trait]
|
|
pub trait McpServerContributor<C: Sync>: Send + Sync {
|
|
async fn contribute(&self, config: &C) -> Vec<McpServerContribution>;
|
|
}
|
|
|
|
/// Extension contribution that adds prompt fragments during prompt assembly.
|
|
pub trait ContextContributor: Send + Sync {
|
|
fn contribute<'a>(
|
|
&'a self,
|
|
session_store: &'a ExtensionData,
|
|
thread_store: &'a ExtensionData,
|
|
) -> std::pin::Pin<Box<dyn Future<Output = Vec<PromptFragment>> + Send + 'a>>;
|
|
}
|
|
|
|
/// Contributor for host-owned thread lifecycle gates.
|
|
///
|
|
/// Implementations should use these callbacks to seed, rehydrate, or flush
|
|
/// extension-private thread state. Heavy dependencies belong on the extension
|
|
/// value created by the host, not in these inputs.
|
|
#[async_trait::async_trait]
|
|
pub trait ThreadLifecycleContributor<C: Sync>: Send + Sync {
|
|
/// Called after thread-scoped extension stores are created, before later
|
|
/// contributors can read from them.
|
|
async fn on_thread_start(&self, _input: ThreadStartInput<'_, C>) {}
|
|
|
|
/// Called after the host constructs a runtime from persisted history.
|
|
async fn on_thread_resume(&self, _input: ThreadResumeInput<'_>) {}
|
|
|
|
/// Called after the host has drained immediately pending thread work.
|
|
///
|
|
/// Implementations may use host capabilities captured by the extension to
|
|
/// submit follow-up input. The host remains responsible for deciding
|
|
/// whether that input starts a turn, is queued, or is ignored.
|
|
async fn on_thread_idle(&self, _input: ThreadIdleInput<'_>) {}
|
|
|
|
/// Called before the host drops the thread runtime and thread-scoped store.
|
|
async fn on_thread_stop(&self, _input: ThreadStopInput<'_>) {}
|
|
}
|
|
|
|
/// Contributor for host-owned turn lifecycle gates.
|
|
///
|
|
/// Implementations should use these callbacks to seed, observe, or clear
|
|
/// extension-private turn state. The host exposes stable identifiers and
|
|
/// extension stores instead of core runtime objects.
|
|
#[async_trait::async_trait]
|
|
pub trait TurnLifecycleContributor: Send + Sync {
|
|
/// Called after turn-scoped extension stores are created, before the task
|
|
/// for the turn starts running.
|
|
async fn on_turn_start(&self, _input: TurnStartInput<'_>) {}
|
|
|
|
/// Called before the host drops the completed turn runtime and turn store.
|
|
async fn on_turn_stop(&self, _input: TurnStopInput<'_>) {}
|
|
|
|
/// Called after the host aborts a running turn.
|
|
async fn on_turn_abort(&self, _input: TurnAbortInput<'_>) {}
|
|
|
|
/// Called when the host observes an error for a running turn.
|
|
async fn on_turn_error(&self, _input: TurnErrorInput<'_>) {}
|
|
}
|
|
|
|
/// Extension contribution that can add turn-local model input.
|
|
///
|
|
/// Implementations should resolve only the model-visible input they own and
|
|
/// must preserve authority boundaries for external resources. Expensive or
|
|
/// host-specific dependencies belong on the extension value installed by the
|
|
/// host, not in this input.
|
|
#[async_trait::async_trait]
|
|
pub trait TurnInputContributor: Send + Sync {
|
|
/// Returns additional contextual fragments for one submitted turn.
|
|
async fn contribute(
|
|
&self,
|
|
input: TurnInputContext,
|
|
session_store: &ExtensionData,
|
|
thread_store: &ExtensionData,
|
|
turn_store: &ExtensionData,
|
|
) -> Vec<Box<dyn ContextualUserFragment + Send>>;
|
|
}
|
|
|
|
/// Contributor for host-owned configuration changes.
|
|
///
|
|
/// Implementations should treat the supplied values as immutable before/after
|
|
/// snapshots of the effective thread configuration.
|
|
pub trait ConfigContributor<C>: Send + Sync {
|
|
/// Called after the host commits a changed thread configuration.
|
|
fn on_config_changed(
|
|
&self,
|
|
_session_store: &ExtensionData,
|
|
_thread_store: &ExtensionData,
|
|
_previous_config: &C,
|
|
_new_config: &C,
|
|
) {
|
|
}
|
|
}
|
|
|
|
/// Contributor for token usage checkpoints reported by the model provider.
|
|
///
|
|
/// Implementations should keep this callback cheap. The host calls it after
|
|
/// updating cached token usage and before emitting the corresponding client
|
|
/// token-count notification.
|
|
#[async_trait::async_trait]
|
|
pub trait TokenUsageContributor: Send + Sync {
|
|
/// Called each time the host records token usage from a model response.
|
|
async fn on_token_usage(
|
|
&self,
|
|
_session_store: &ExtensionData,
|
|
_thread_store: &ExtensionData,
|
|
_turn_store: &ExtensionData,
|
|
_token_usage: &TokenUsageInfo,
|
|
) {
|
|
}
|
|
}
|
|
|
|
/// Extension contribution that exposes native tools owned by a feature.
|
|
pub trait ToolContributor: Send + Sync {
|
|
/// Returns the native tools visible for the supplied extension stores.
|
|
fn tools(
|
|
&self,
|
|
session_store: &ExtensionData,
|
|
thread_store: &ExtensionData,
|
|
) -> Vec<Arc<dyn ToolExecutor<ToolCall>>>;
|
|
}
|
|
|
|
/// Contributor for host-owned tool lifecycle gates.
|
|
///
|
|
/// Implementations should use these callbacks to observe tool execution without
|
|
/// inspecting or rewriting tool input/output. Use `ToolContributor` for owning a
|
|
/// tool implementation and hooks for policy that needs tool payloads.
|
|
pub trait ToolLifecycleContributor: Send + Sync {
|
|
/// Called once the host has accepted a tool call for execution.
|
|
fn on_tool_start<'a>(&'a self, _input: ToolStartInput<'a>) -> ToolLifecycleFuture<'a> {
|
|
Box::pin(std::future::ready(()))
|
|
}
|
|
|
|
/// Called after the tool call returns, is blocked, fails, or is cancelled.
|
|
fn on_tool_finish<'a>(&'a self, _input: ToolFinishInput<'a>) -> ToolLifecycleFuture<'a> {
|
|
Box::pin(std::future::ready(()))
|
|
}
|
|
}
|
|
|
|
/// Extension contribution that can claim rendered approval-review prompts.
|
|
#[async_trait::async_trait]
|
|
pub trait ApprovalReviewContributor: Send + Sync {
|
|
async fn contribute(
|
|
&self,
|
|
session_store: &ExtensionData,
|
|
thread_store: &ExtensionData,
|
|
prompt: &str,
|
|
) -> Option<ReviewDecision>;
|
|
}
|
|
|
|
/// Ordered post-processing contribution for one parsed turn item.
|
|
///
|
|
/// Implementations may mutate the item before it is emitted and may use the
|
|
/// explicitly exposed thread- and turn-lifetime stores when they need durable
|
|
/// extension-private state.
|
|
#[async_trait::async_trait]
|
|
pub trait TurnItemContributor: Send + Sync {
|
|
async fn contribute(
|
|
&self,
|
|
thread_store: &ExtensionData,
|
|
turn_store: &ExtensionData,
|
|
item: &mut TurnItem,
|
|
) -> Result<(), String>;
|
|
}
|