9 Commits

  • Project selected plugin runtime by environment availability (#30093)
    ## Why
    
    Selected plugin metadata is stable, but MCP processes are live runtime
    state. They need different lifetimes:
    
    - the MCP extension caches manifest, MCP, and connector declarations for
    each stable selected root;
    - each model step projects that cached metadata through the roots that
    resolved as ready for that exact step;
    - the MCP manager is rebuilt only when that availability projection
    changes.
    
    This matches executor skills: both features consume the same resolved
    step roots instead of inferring readiness from the turn's selected
    environments.
    
    ## Behavior
    
    ```text
    E1 not ready for this step
      -> no E1 MCP servers or connectors
      -> cached plugin metadata stays in ext/mcp
    
    E1 becomes ready
      -> reuse cached metadata
      -> publish one MCP runtime containing E1 capabilities
    
    same ready roots on the next step
      -> reuse the exact runtime; no rediscovery and no MCP restart
    
    resume
      -> create new extension thread state and a new MCP runtime
    ```
    
    All model-facing consumers use the same step snapshot:
    
    ```text
    resolved selected roots
            |
            v
    extension MCP/connector projection
            |
            v
    { MCP config, connector snapshot, MCP manager }
            |
            +-> advertise model tools
            +-> build app/connector tools
            +-> execute MCP calls
    ```
    
    ## Cache contract
    
    The existing MCP extension owns a cache keyed by the full
    `SelectedCapabilityRoot`:
    
    ```rust
    let state = thread_store.get_or_init(SelectedExecutorPluginMcpState::default);
    ```
    
    The cache lives with extension thread state. Environment availability
    filters projection but does not invalidate metadata. Resume creates new
    thread state. There is no file watcher or executor generation because
    contents behind a stable environment/root are assumed stable.
    
    ## What changes
    
    - Keeps executor plugin discovery and cached metadata in `ext/mcp`.
    - Caches MCP and connector declarations together per selected root.
    - Uses the step's already-resolved capability roots, including lazy
    environments that are not turn environments.
    - Reuses the current MCP runtime when the ready-root projection is
    unchanged.
    - Uses the same step MCP manager and connector snapshot for
    model-visible tools and execution.
    - Resolves direct thread-scoped MCP requests from the current
    selected-root projection.
    
    ## Deliberately out of scope
    
    - `app/list` remains based on the latest global host-plugin state; this
    PR does not make its response or notifications thread-specific.
    - `required = true` startup semantics do not apply to delayed executor
    MCP activation.
    - No filesystem/content invalidation.
    - No transport-disconnect watcher.
    - No executor generations or environment replacement semantics.
    - No client sharing across complete manager replacements.
    
    ## Stack
    
    1. Extension-owned World State sections.
    2. Project executor skills through World State.
    3. Pin one MCP runtime to each model step.
    4. **This PR:** project selected MCP and connector state from
    extension-owned metadata.
    5. Integration coverage for selected capability availability and resume.
    
    ## Verification
    
    -
    `selected_plugin_servers_use_managed_requirements_for_the_selected_root_id`
    - The stacked integration PR covers unavailable to ready activation,
    unchanged-runtime reuse, skills, MCP tools, connector attribution, and
    cold resume.
  • Make selected plugin roots URI-native (#28918)
    ## Why
    
    Selected capability roots belong to the executor filesystem, not the
    app-server host. Converting their path strings into the host's native
    `Path` breaks whenever the two machines use different path conventions,
    such as a Windows executor behind a Unix app-server.
    
    This PR establishes `PathUri` as the selected-plugin boundary so the
    executor remains authoritative for its paths.
    
    ## What changed
    
    - Require `selectedCapabilityRoots[].location.path` to be a canonical
    `file:` URI and deserialize it directly as `PathUri`; native path
    strings are rejected.
    - Update the app-server schema, generated TypeScript, examples, and
    request coverage for the URI contract.
    - Keep selected roots, resolved plugin locations, manifest paths, and
    manifest resources as `PathUri`.
    - Inspect and read plugin roots and manifests only through the selected
    environment's `ExecutorFileSystem`.
    - Parse executor manifests with the shared URI-native parser from #29620
    instead of projecting them onto the host filesystem.
    - Enforce resource containment lexically and preserve the root URI's
    POSIX or Windows path convention.
    - Cover foreign Windows plugin roots and URI-native manifest resources.
    
    ```text
    thread/start
      selectedCapabilityRoots[].location.path = "file:///C:/plugins/demo"
                                  | PathUri
                                  v
                        ExecutorFileSystem
                                  |
                                  +--> plugin.json
                                  +--> manifest resources
    ```
    
    This PR stops at the shared selected-plugin representation. The next two
    PRs remove the remaining host-path projections in the skill and MCP
    consumers.
    
    ## Stack
    
    1. #29614 — add lexical `PathUri` containment.
    2. #29620 — share URI-native manifest path resolution.
    3. **This PR** — keep selected plugin roots and resources URI-native.
    4. #29626 — load executor skills without host path conversion.
    5. #29628 — resolve executor MCP working directories without host path
    conversion.
  • Discover stdio MCP servers from selected executor plugins (#27870)
    ## Why
    
    **In short:** this PR discovers MCP registrations by reading a selected
    plugin's `.mcp.json` on its executor. #27884 then resolves those
    registrations in the shared catalog.
    
    `thread/start.selectedCapabilityRoots` can select a plugin root owned by
    an executor, and Codex can resolve that package through the executor
    filesystem. MCP declarations inside the selected plugin are still
    ignored.
    
    This PR adds the source-specific discovery layer on top of the
    selected-plugin catalog boundary in #27884:
    
    ```text
    selected capability root
            |
            v
    resolve the plugin through its executor filesystem
            |
            v
    read and normalize its MCP config through the same filesystem
            |
            v
    contribute stdio registrations bound to that environment ID
    ```
    
    The existing MCP launcher and connection manager remain unchanged. MCP
    config parsing is shared with local plugins through #27863.
    
    ## What changed
    
    - Added an executor plugin MCP provider in the MCP extension.
    - Retained only the exact filesystem capability used for package
    resolution and reused it for the selected plugin's MCP config, with no
    host-filesystem fallback or unrelated process/HTTP authority.
    - Read either the manifest-declared MCP config or the default
    `.mcp.json`; a missing default file means the plugin has no MCP servers.
    - Accepted stdio servers only for this first vertical. Executor-owned
    HTTP declarations are skipped with a warning until their placement
    semantics are defined.
    - Normalized stdio registrations with the owning environment's stable
    logical ID and plugin-root working directory.
    - Resolved environment-variable names on the owning executor and
    rejected explicit local forwarding for non-local plugins.
    - Froze discovered declarations once per active thread runtime, then
    applied current managed plugin and MCP requirements when contributing
    them.
    - Carried the selected root ID, display name, and selection order into
    the catalog contribution defined by #27884.
    
    ## Behavior and scope
    
    There is intentionally no production behavior change yet. This PR
    provides the executor provider and contribution boundary, but app-server
    does not install it in this change. Existing local plugin MCP loading is
    unchanged, and no MCP process is launched by this PR alone.
    
    ## Assumptions
    
    - The selected root ID is the plugin policy identity; the manifest
    display name is presentation metadata.
    - An environment ID is a stable logical authority. Reconnection or
    replacement under the same ID does not change ownership.
    - Selected plugin packages and their manifests are trusted inputs.
    - The selected package and MCP discovery snapshot remain frozen for the
    active thread runtime.
    
    ## Follow-up
    
    The next PR installs this contributor in app-server and adds an
    end-to-end test proving that a selected plugin MCP tool launches on its
    owning executor, can be called by the model, survives an explicit MCP
    refresh, and is invisible when its root was not selected.
    
    Resume, fork, environment removal or ID changes, dynamic catalog reload,
    and executor-owned HTTP MCP placement remain separate lifecycle
    decisions.
    
    ## Verification
    
    Focused tests cover executor-only filesystem reads, missing and
    malformed config, stdio filtering and normalization, managed
    requirements, package attribution, and selection order. CI owns
    execution of the test suite.
  • Make MCP server contributions thread-scoped (#27670)
    ## Why
    
    `selectedCapabilityRoots` belongs to one thread, but MCP contributors
    previously received only the global Codex config. That left no clean way
    for a selected executor capability to contribute MCP servers to its own
    thread.
    
    ## What this PR does
    
    - Gives MCP contributors a small context containing the config and, for
    a running thread, its frozen host-seeded inputs.
    - Uses the same thread inputs during startup, status queries, refreshes,
    and skill dependency checks.
    - Keeps threadless MCP operations and the existing hosted Apps behavior
    unchanged.
    - Adds coverage showing that two threads resolve independent
    registrations and that later lifecycle mutations do not change the
    frozen MCP inputs.
    
    This PR does not discover plugin manifests, add MCP servers, or launch
    anything new. It only establishes the thread-scoped registration
    boundary.
    
    ## Follow-ups
    
    - Resolve selected executor plugin roots through their owning
    environment filesystem.
    - Convert their stdio MCP declarations into environment-bound
    registrations and add an executor MCP end-to-end test.
    
    ## Verification
    
    - `just fmt`
    - `cargo check --tests -p codex-protocol -p codex-extension-api -p
    codex-mcp-extension -p codex-core -p codex-app-server`
    
    Tests and Clippy were not run.
  • Resolve MCP server registrations through a catalog (#27634)
    ## Why
    
    MCP servers currently come from user config, local plugins,
    compatibility Apps synthesis, and host extensions. Those sources were
    composed by mutating a shared map, leaving registration identity,
    precedence, removal, and provenance implicit in assembly order.
    
    Before adding executor-owned MCPs, Codex needs one durable resolution
    boundary above `McpConnectionManager`. This PR introduces that boundary
    while preserving current server configuration, policy, and runtime
    behavior. Executor-scoped registrations and explicit policy layers
    remain follow-ups.
    
    ## What changed
    
    - Add typed `McpServerRegistration` inputs and an immutable
    `ResolvedMcpCatalog` in `codex-mcp`.
    - Retain each registration's complete `McpServerConfig`, including its
    environment binding, while recording its source and provenance.
    - Preserve the existing structural precedence between plugin, config,
    compatibility, and ordered extension sources.
    - Resolve equal-precedence actions by contribution order; provenance IDs
    are used only for diagnostics and cannot affect the winner.
    - Preserve extension removals and the existing name-scoped `enabled =
    false` veto.
    - Report same-tier conflicts with every contender and the final catalog
    outcome, including whether the winning action registers or removes the
    server.
    - Require MCP contributors to provide a stable diagnostic identity.
    - Derive materialized server maps and plugin ownership from the resolved
    catalog.
    
    `McpConnectionManager`, transport startup, tool calls, and resource
    routing continue to consume the same effective `McpServerConfig` values.
    
    ## Scope
    
    This PR does not add new MCP capabilities or change user-visible
    behavior. It does not add executor plugin discovery, thread-scoped
    registrations, dynamic refresh generations, or new user/managed policy
    semantics.
    
    ## Verification
    
    - Added focused catalog coverage for source precedence, complete
    configuration preservation, disabled vetoes, plugin ownership,
    contribution-order tie breaking, removal outcomes, and conflict
    diagnostics.
    - Extended hosted Apps coverage for ordered extension removal and
    Apps-disabled hosts with and without the hosted extension installed.
    - `cargo check -p codex-mcp --tests -p codex-extension-api -p
    codex-core`
  • [codex] Preserve disabled MCP servers across runtime overlays (#27414)
    ## Why
    
    Recent MCP runtime overlay changes replace same-name configured server
    entries with compatibility or extension-provided configs. Those
    replacement configs default to enabled, so an MCP server explicitly
    configured with `enabled = false` could be initialized anyway.
    
    The connection manager still filters disabled servers correctly, but the
    configured disabled state was lost before initialization reached that
    filter.
    
    ## What changed
    
    - Remember MCP servers that are disabled in the configured view before
    applying runtime fallbacks and extension overlays.
    - Restore `enabled = false` for those servers after overlays, while
    leaving all other overlay fields and `Remove` precedence unchanged.
    - Add focused extension-backed regression coverage for a disabled
    `codex_apps` server.
    
    ## Testing
    
    - `just fmt`
    - `just test -p codex-mcp-extension`
    - `just fix -p codex-core`
    - `just fix -p codex-mcp-extension`
    
    The full workspace `just test` suite was not run.
  • Remove async-trait from extension contributors (#27383)
    ## Why
    
    Extension contributors are registered behind `dyn Trait` objects, so
    native `async fn`/RPITIT methods would make these traits
    non-object-safe. Spell out the boxed, `Send` future contract directly so
    `extension-api` no longer needs `async-trait` while retaining the
    existing runtime model.
    
    ## What changed
    
    - add a shared `ExtensionFuture` alias and use it for asynchronous
    contributor methods
    - migrate production and test implementations to return `Box::pin(async
    move { ... })`
    - remove `async-trait` dependencies where they are no longer used,
    keeping it dev-only where unrelated test executors still require it
    
    ## Behavior
    
    No behavior change is intended. Contributor futures remain boxed,
    `Send`, dynamically dispatched, and lazily executed; cancellation and
    callback ordering stay unchanged.
    
    ## Testing
    
    - `just test -p codex-extension-api` (11 passed)
    - affected extension crates (64 passed)
    - targeted `codex-core` contributor tests (14 passed)
    - `just fmt`
    - `just bazel-lock-update`
    - `just bazel-lock-check`
    
    A broad local `codex-core` run compiled successfully but encountered
    unrelated sandbox and missing test-binary fixture failures; CI will run
    the full checks.
  • Use plugin-service MCP as the hosted plugin runtime (#27198)
    ## Stack
    
    - Base: #27191
    - This PR is the third vertical and should be reviewed against
    `jif/external-plugins-2`, not `main`.
    
    ## Why
    
    #27191 moves the host-owned Apps MCP registration behind an extension
    contributor, but deliberately preserves the existing endpoint-selection
    feature while that contribution contract lands. App-server can therefore
    resolve the server through extensions, yet the hosted plugin endpoint is
    still selected through temporary `apps_mcp_path_override` plumbing.
    
    That is not the long-term plugin model. A plugin can bundle skills,
    connectors, MCP servers, and hooks, and those components do not all need
    the same source or execution environment. In particular, an
    authenticated HTTP MCP server can expose plugin capabilities directly
    from a backend without an executor or an orchestrator filesystem.
    
    This PR completes that hosted vertical. App-server's MCP extension now
    owns the aggregate hosted plugin runtime at `/ps/mcp`. Connector actions
    continue to arrive as MCP tools, while backend-provided skills arrive as
    MCP resources and use Codex's existing resource list/read paths. No
    second backend client, skill filesystem, or generic plugin activation
    framework is introduced.
    
    The backend route remains the hosted implementation. This change
    replaces Codex's temporary endpoint-selection mechanism, not the service
    behind the endpoint.
    
    ## What changed
    
    ### Hosted plugin runtime
    
    The MCP extension now contributes `codex_apps` as the hosted plugin
    runtime rather than as a configurable Apps endpoint:
    
    - `https://chatgpt.com` resolves to
    `https://chatgpt.com/backend-api/ps/mcp`;
    - a bare custom ChatGPT base resolves to `/api/codex/ps/mcp`;
    - the existing product-SKU header and ChatGPT authentication behavior
    are preserved;
    - executor availability is never consulted for this streamable HTTP
    transport.
    
    The same MCP connection carries both component shapes supported by the
    hosted endpoint:
    
    - connector actions are discovered and invoked as MCP tools;
    - hosted skills are enumerated and read as MCP resources through the
    existing `list_mcp_resources` and `read_mcp_resource` paths.
    
    This keeps component access in the subsystem that already owns the
    protocol instead of downloading backend skills into an orchestrator
    filesystem or inventing a parallel hosted-skill client.
    
    ### Explicit runtime ordering
    
    `McpManager` now resolves the reserved `codex_apps` entry in three
    ordered phases:
    
    1. install the legacy Apps fallback for compatibility;
    2. apply ordered extension `Set` or `Remove` overlays;
    3. apply the final ChatGPT-auth gate without synthesizing the server
    again.
    
    This ordering is important:
    
    - an ordinary configured or plugin MCP server cannot claim the
    auth-bearing `codex_apps` name;
    - an extension-contributed hosted runtime wins over the fallback;
    - an extension `Remove` remains authoritative;
    - a host without the MCP extension retains the legacy Apps endpoint and
    current local-only behavior.
    
    The temporary `legacy_apps_mcp_loader_enabled` coordination flag is no
    longer needed.
    
    ### Remove the path override
    
    The `apps_mcp_path_override` feature and its runtime plumbing are
    removed, including:
    
    - the feature registry entry and structured feature config;
    - `Config` and `McpConfig` fields;
    - config schema output;
    - config-lock materialization;
    - URL override handling in `codex-mcp`.
    
    Existing boolean and structured forms still deserialize as ignored
    compatibility input. They are omitted from new serialized config, and
    config-lock comparison normalizes the removed input so older locks
    remain replayable.
    
    ### App-server coverage
    
    App-server MCP fixtures now serve the hosted route at
    `/api/codex/ps/mcp`. Existing resource-read and tool/elicitation flows
    therefore exercise the extension-owned endpoint rather than succeeding
    through the legacy fallback.
    
    The stack also adds the missing `codex_chatgpt::connectors` re-export
    for the manager-backed connector helper introduced in #27191.
    
    ## Compatibility
    
    - App-server installs the extension and uses `/ps/mcp` for the hosted
    runtime.
    - CLI and other hosts that do not install the extension retain the
    legacy Apps endpoint.
    - Apps disabled or non-ChatGPT authentication removes `codex_apps` from
    the effective runtime view.
    - Existing local plugins, local skills, executor-selected skills,
    configured MCP servers, and MCP OAuth behavior are otherwise unchanged.
    - Backend plugin enablement remains account/workspace state owned by the
    hosted endpoint; this PR does not add thread-local backend plugin
    selection.
    
    ## Architectural fit
    
    The stack now proves two independent runtime shapes:
    
    1. #27184 resolves filesystem-backed skills through the executor that
    owns a selected root.
    2. #27191 and this PR resolve a backend-hosted HTTP MCP through an
    extension with no executor.
    
    Together they preserve the intended separation:
    
    - selection identifies a plugin/root when explicit selection is needed;
    - each component's owning extension resolves its concrete access
    mechanism;
    - execution stays with the runtime required by that component;
    - existing skills, MCP, connector, and hook subsystems remain the
    downstream consumers.
    
    ## Planned follow-ups
    
    1. **Executor stdio MCP:** selecting an executor plugin registers a
    manifest-declared stdio MCP server and executes it in the environment
    that owns the plugin.
    2. **Optional backend selection:** only if CCA needs thread-local
    selection distinct from backend account/workspace enablement, add a
    concrete backend-owned capability location and surface those selected
    skills through the skills catalog.
    3. **Connector metadata and hooks:** activate those plugin components
    through their existing owning subsystems, with executor hooks remaining
    environment-bound.
    4. **Propagation and persistence:** define explicit resume, fork,
    subagent, refresh, and environment-removal semantics once selected roots
    have multiple real consumers.
    5. **Local convergence:** migrate legacy local skill, MCP, connector,
    and hook paths behind their owning extensions one vertical at a time,
    then remove duplicate core managers and compatibility plumbing after
    parity.
    
    ## Verification
    
    Coverage in this change exercises:
    
    - extension-owned `/backend-api/ps/mcp` registration without an
    executor;
    - preservation of the legacy endpoint in hosts without the extension;
    - extension `Set` and `Remove` precedence over the legacy fallback;
    - ChatGPT-auth gating for the reserved server;
    - hosted MCP resource reads with and without an active thread;
    - connector tool invocation and MCP elicitation through the hosted
    route;
    - ignored boolean and structured forms of the removed path override;
    - config-lock replay compatibility for the removed feature.
    
    `cargo check -p codex-features -p codex-mcp-extension -p
    codex-app-server` passes. Tests and Clippy were not run locally under
    the current development instruction; CI provides the full validation
    pass.
  • Route hosted Apps MCP through extensions (#27191)
    ## 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.