Commit Graph

45 Commits

  • config: own layer provenance types (#29722)
    ## Why
    
    Config layer provenance describes how effective configuration was
    assembled, so it belongs with the config loader rather than in
    app-server's serialized API types.
    
    ## What changed
    
    - Moved `ConfigLayerSource`, `ConfigLayerMetadata`, and `ConfigLayer`
    ownership into `codex-config`.
    - Kept app-server's wire payloads unchanged and added explicit
    conversions at the app boundary.
    - Removed lower-level app-server-protocol dependencies from config
    consumers.
    
    ## Stack
    
    This is PR 3 of 6, stacked on [PR
    #29721](https://github.com/openai/codex/pull/29721). Review only the
    delta from `codex/split-auth-domain-types`. Next: [PR
    #29723](https://github.com/openai/codex/pull/29723).
    
    ## Validation
    
    - `codex-config` coverage passed.
    - App-server config-manager and config RPC coverage passed.
  • [plugins] Enforce marketplace source admission requirements (#29753)
    ## Why
    
    Managed marketplace source requirements only become effective when every
    local marketplace mutation path applies the same admission decision.
    This change centralizes that decision so CLI, app-server, and
    external-agent migration flows cannot add, install from, or refresh a
    disallowed source.
    
    ## What changed
    
    - Match exact normalized Git repository URLs with an optional exact
    `ref`.
    - Match Git hosts with managed regular expressions.
    - Match local marketplaces by exact absolute path.
    - Preserve the expected path/name boundary for managed OpenAI
    marketplaces.
    - Enforce source admission during marketplace add, plugin install, and
    configured Git marketplace upgrade.
    - Continue upgrading independent marketplaces when one source is
    rejected and return a per-marketplace error.
    - Load the effective requirements stack at CLI, app-server, and
    external-agent migration entry points.
    
    This PR does not filter already configured marketplaces at runtime; that
    remains in draft follow-up #29691.
    
    ## Stack
    
    This is PR 2 of 3 and is based on #29690, which introduces the
    requirements data shape and merge behavior.
    
    ## Test plan
    
    - Source matcher coverage for Git URL/ref, host-pattern, local-path, and
    managed marketplace cases.
    - Marketplace add and plugin install coverage for allowed and rejected
    sources.
    - Marketplace upgrade coverage for rejection and per-marketplace
    continuation.
  • auth: move domain mode below app wire types (#29721)
    ## Why
    
    Authentication mode is a domain concept used by login, model selection,
    telemetry, and transports. Keeping the canonical type in app-server
    protocol forces those lower-level crates to depend on an unrelated wire
    API.
    
    ## What changed
    
    - Added canonical `codex_protocol::auth::AuthMode` domain values.
    - Kept the app-server wire DTO unchanged and added an explicit app-side
    conversion.
    - Removed production app-server-protocol dependencies from login,
    model-provider-info, models-manager, and otel call paths.
    
    ## Stack
    
    This is PR 2 of 6, stacked on [PR
    #29714](https://github.com/openai/codex/pull/29714). Review only the
    delta from `codex/split-json-rpc-protocols`. Next: [PR
    #29722](https://github.com/openai/codex/pull/29722).
    
    ## Validation
    
    - Auth and login coverage passed in the focused protocol/domain test
    run.
    - App-server account and auth conversion coverage passed.
  • [codex] Ignore local curated plugins when remote catalog is active (#29765)
    ## Summary
    
    - suppress configured `openai-curated` plugins when the remote plugin
    feature is enabled and auth uses the Codex backend
    - preserve `openai-api-curated` and non-Codex-backend behavior while
    including remote catalog activation in the plugin load cache key
    - add core plugin coverage and an app-server integration test for
    runtime feature enablement
    
    ## Why
    
    The Codex app enables remote plugins through process-local runtime
    feature enablement, which can happen after app-server startup tasks have
    already observed legacy local plugin state. The existing conflict logic
    only preferred a remote plugin when the same plugin was already
    installed remotely, so a configured legacy-only plugin could continue
    exposing skills and other capabilities from `openai-curated`.
    
    ## Impact
    
    When the remote catalog is active, legacy `openai-curated` plugins no
    longer contribute skills, MCP servers, apps, or hooks. Remote installed
    plugins continue to load normally, and `openai-api-curated` remains
    unaffected. This does not change remote fetch, bundle sync, or uninstall
    behavior.
    
    ## Validation
    
    - `just test -p codex-core-plugins
    remote_global_catalog_ignores_local_curated_plugins
    remote_plugin_feature_keeps_local_curated_without_codex_backend`
    - `just test -p codex-app-server
    runtime_remote_plugin_enablement_excludes_local_curated_plugin_skills`
    - `just fmt`
    - `git diff --check`
  • Separate local and remote plugin analytics IDs (#29495)
    ## Why
    
    Plugin analytics overloaded `plugin_id`: most events used the Codex
    `<plugin>@<marketplace>` identity, while remote install events used the
    backend plugin ID. That makes the same field change meaning across event
    types and complicates downstream identity resolution.
    
    This change makes the contract unambiguous:
    
    - `plugin_id`: the local Codex `<plugin>@<marketplace>` identity, when
    resolved
    - `remote_plugin_id`: the backend plugin identity, when available
    
    For a remote install failure that happens before plugin details resolve,
    `plugin_id` is `null` and `remote_plugin_id` remains populated.
    
    ## What changed
    
    All six plugin analytics events use the same identity contract:
    
    - `codex_plugin_installed`
    - `codex_plugin_install_failed`
    - `codex_plugin_uninstalled`
    - `codex_plugin_enabled`
    - `codex_plugin_disabled`
    - `codex_plugin_used`
    
    Remote identity is resolved from the current installed-plugin snapshot
    first, with persisted install metadata as fallback. The telemetry
    metadata type keeps local identity optional for failures that occur
    before remote details are available.
    
    The app-server test client's manual analytics smokes now find remote
    mutation events through `remote_plugin_id` and validate that `plugin_id`
    remains local.
    
    ## Remote uninstall
    
    Resolve and capture telemetry metadata before removing the local plugin
    cache, then emit `codex_plugin_uninstalled` after the backend confirms
    success. The event is also emitted when backend uninstall succeeds but
    local cache cleanup reports `CacheRemove`.
    
    If a concurrent remote-cache refresh removes the local bundle before
    telemetry capture, the already-fetched remote plugin detail supplies
    fallback capability metadata.
    
    ## Validation
    
    - `just test -p codex-analytics` — 82 passed
    - `just test -p codex-core-plugins` — 271 passed
    - `just test -p codex-app-server-test-client` — 5 passed
    - `just test -p codex-plugin` — 3 passed
    - `just test -p codex-app-server plugin_install` — 37 passed
    - `just test -p codex-app-server plugin_uninstall` — 10 passed
    
    The production app-server install/uninstall flow was also exercised
    against `plugins~Plugin_f1b845ac33888191ac156169c58733c2`
    (`build-ios-apps@openai-curated-remote`), and the plugin's original
    uninstalled state was restored.
  • [plugins] Add dark-mode logo metadata (#29488)
    Adds additive dark-mode plugin logo metadata across manifests, remote
    catalogs, and the app-server protocol while keeping uninstalled Git
    listings free of synthetic local paths.
    
    Supersedes #28945. This replacement uses an upstream branch so trusted
    CI can use the repository-provided remote Bazel configuration.
    
    ## Current state
    
    Plugin interfaces expose only the default logo asset. Clients therefore
    cannot select a dedicated dark-mode logo even when a plugin provides
    one.
    
    ## What this PR changes
    
    - Adds nullable `logoDark` and `logoUrlDark` fields to
    `PluginInterface`.
    - Resolves local `interface.logoDark` assets and maps remote
    `logo_url_dark` values.
    - Removes path-backed interface assets, including `logoDark`, from
    uninstalled Git fallback listings until the plugin has a real local
    root.
    - Updates the bundled plugin validator and manifest reference.
    - Regenerates the app-server JSON schemas and TypeScript types.
    
    Local manifests expose `interface.logoDark` as a package-relative asset
    path. Remote catalog responses expose `logo_url_dark`. These values map
    into separate app-server fields so clients can preserve local-path and
    remote-URL handling.
    
    ## Risk
    
    The fields are additive and nullable, so existing clients retain their
    current logo behavior. The main risks are an incomplete mapping path or
    exposing a synthetic local path for an uninstalled Git plugin.
    Local-manifest, remote-catalog, fallback-listing, protocol
    serialization, and app-server integration tests cover those paths.
    
    Spiciness: 2/5
    
    ## Testing
    
    - `just write-app-server-schema`
    - `just fmt`
    - Regression test first failed with `logo_dark` resolved to
    `/assets/logo-dark.png`, then passed after the fallback-listing fix.
    - `just test -p codex-core-plugins` (267 tests passed)
    - `just test -p codex-app-server 'suite::v2::plugin'` (114 tests passed)
    - `just test -p codex-app-server-protocol -p codex-core-plugins -p
    codex-plugin -p codex-skills` (517 tests passed before the follow-up)
    - `just test -p codex-tui plugin` (47 tests passed)
    - Validated a local plugin manifest containing `interface.logoDark` with
    the bundled validator.
    
    ## Manual verification
    
    Create a local plugin with both `interface.logo` and
    `interface.logoDark`, then call `plugin/list` or `plugin/read`. Confirm
    the response contains separate `logo` and `logoDark` paths. For a remote
    catalog entry, confirm `logoUrlDark` is populated from `logo_url_dark`.
    For an uninstalled Git marketplace entry, confirm path-backed interface
    assets remain absent until installation.
    
    Issue: N/A - coordinated maintainer change.
  • [codex] Centralize Plugin Analytics Metadata (#27102)
    This PR moves construction of `PluginTelemetryMetadata` from loader and
    model helpers into `PluginsManager`, which already owns installed plugin
    state and will eventually perform remote identity enrichment. The
    metadata type remains in `codex-plugin`, and serialized analytics events
    remain unchanged.
    
    ## Before
    
    ```mermaid
    flowchart LR
        subgraph Events["Analytics event paths"]
            direction TB
            Lifecycle["Local install / uninstall"]
            Config["Enable / disable"]
            Remote["Remote install"]
            Used["Plugin used"]
        end
    
        subgraph Construction["Metadata construction"]
            direction TB
            Loader["Loader telemetry helpers"]
            Summary["PluginCapabilitySummary::telemetry_metadata"]
            Override["Caller adds remote_plugin_id"]
        end
    
        Metadata["PluginTelemetryMetadata"]
    
        Lifecycle --> Loader
        Config --> Loader
        Remote --> Loader
        Loader -->|"local events"| Metadata
        Loader -->|"remote install"| Override
        Override --> Metadata
        Used --> Summary
        Summary --> Metadata
    ```
    
    Telemetry metadata was constructed through loader helpers, a
    capability-summary method, and a remote-install call-site override.
    
    ## After
    
    ```mermaid
    flowchart LR
        subgraph Events["Analytics event paths"]
            direction TB
            Lifecycle["Local install / uninstall"]
            Config["Enable / disable"]
            Remote["Remote install"]
            Used["Plugin used"]
        end
    
        Manager["PluginsManager — single construction owner"]
        Metadata["PluginTelemetryMetadata"]
    
        Lifecycle --> Manager
        Config --> Manager
        Remote -->|"authoritative remote ID"| Manager
        Used -->|"capability summary"| Manager
        Manager --> Metadata
    ```
    
    Every analytics path delegates metadata construction to
    `PluginsManager`. Remote install still supplies its authoritative
    backend ID explicitly.
    
    ## What Changes
    
    - Make loader code return a focused plugin capability summary instead of
    constructing analytics metadata.
    - Centralize immutable plugin telemetry metadata construction in
    `PluginsManager`.
    - Route local install/uninstall, remote install, enable/disable, and
    plugin-used emitters through the manager.
    - Preserve the current serialized analytics contract exactly.
    
    Normal metadata still has no remote override. Remote install continues
    to provide its authoritative backend ID explicitly, so the existing
    serializer continues reporting that ID through `plugin_id`.
    Snapshot-based enrichment is intentionally deferred to the final PR.
    
    ## Testing
    
    - `just test -p codex-core-plugins` (238 tests passed)
    - `just test -p codex-plugin` (3 tests passed)
    - Scoped Clippy/compile checks passed for `codex-plugin`,
    `codex-core-plugins`, `codex-app-server`, and `codex-core`.
    
    ## Split Overview
    
    ```text
    main
    ├── #27093  Debug analytics capture                 (merged)
    ├── #27099  Non-mutating plugin smoke               (merged)
    ├── #27100  Remote install/uninstall smoke          (merged)
    └── #27102  Plugin telemetry metadata refactor      ← you are here
        └── #27669  Persist remote plugin identity
    
    After #27102 and #27669 merge:
    └── Final PR: add explicit local and remote IDs to plugin analytics
    ```
    
    Review order and dependencies:
    
    1. [#27093 Add debug-only analytics event
    capture](https://github.com/openai/codex/pull/27093) (merged)
    2. [#27099 Add a plugin analytics smoke
    workflow](https://github.com/openai/codex/pull/27099) (merged)
    3. [#27100 Add a remote plugin analytics mutation smoke
    workflow](https://github.com/openai/codex/pull/27100) (merged)
    4. This metadata refactor, independent and based on `main`
    5. [#27669 Persist remote plugin
    identity](https://github.com/openai/codex/pull/27669), stacked on this
    PR
    6. Final remote-ID behavior PR, created after the prerequisites merge
    
    The original [#26281](https://github.com/openai/codex/pull/26281)
    remains open as the aggregate reference until the final replacement PR
    is published.
  • [plugins] Refresh plugin and tool caches after remote install (#28951)
    Summary
    - Refresh the installed remote-plugin snapshot and Codex Apps tools
    after completing a remote JIT install.
    - Gate `completed: true` on every expected `app_connector_id` appearing
    after the uncached `tools/list` refresh, while continuing to skip local
    bundle verification for server-side installs.
    - Keep the cached recommendations response and filter refreshed
    installed remote IDs locally, so this does not add another
    recommendations fetch.
    - Add regression coverage for tools appearing after the hard refresh and
    remaining absent after the refresh. The resumed model request sees the
    refreshed tool router when installation completes.
    
    Root Cause
    - Remote suggestions from `openai-curated-remote` returned `true` before
    taking the existing connector refresh path, leaving the resumed turn
    with the pre-install Apps tool catalog.
    
    Validation
    - `just test -p codex-core request_plugin_install`
    - `just test -p codex-core-plugins
    recommended_plugin_candidates_filter_installed_and_disabled_plugins`
    - `just test -p codex-core-plugins`
    - `just fix -p codex-core-plugins`
    - `just fix -p codex-core`
    - `just fmt`
    - `just test -p codex-core` was not fully clean locally: 2,729 passed,
    26 failed, and 16 skipped. The failures were dominated by local
    Seatbelt/network/timing issues, including plugin-install timeouts under
    full-suite contention; the focused plugin-install runs pass.
  • [codex] Reuse parsed plugin skills during session startup (#28844)
    ## Summary
    
    - Preserve raw plugin skill-root snapshots in the matching loaded-plugin
    cache entry, keyed by the effective plugin root identity including
    namespace.
    - Pass those snapshots through `SkillsLoadInput` as an optional preload,
    so session startup reuses plugin parsing while ordinary skill loads pass
    `None`.
    - Keep plugin skill loading cohesive: the existing loaders accept the
    optional snapshots directly, and uncached or marketplace-detail paths do
    not create a cache.
    
    ## Why
    
    Plugin discovery already parses plugin skills to determine available
    capabilities. Cold session startup then scanned and parsed the same
    roots again while building the skills snapshot.
    
    This solves the same duplicate-work problem as #28623 while keeping
    ownership narrow: `PluginsManager` creates and owns
    `PluginSkillSnapshots` only for its loaded-plugin cache entry;
    `SkillsService` consumes an optional clone. Entry replacement or
    clearing naturally drops the snapshots, with no separate generation,
    capacity policy, or watcher coupling.
    
    ## Validation
    
    - `cargo clippy -p codex-core-skills --all-targets -- -D warnings`
    - `just test -p codex-core-plugins
    skills_service_reuses_skills_parsed_during_plugin_load`
    - `just test -p codex-core-skills
    namespaces_plugin_skills_using_provided_namespace`
    - `just fmt`
  • [codex] Support marketplace plugin manifest fallback (#28789)
    ## Summary
    
    Support marketplace plugins whose source directory does not include a
    discoverable plugin manifest. Metadata-rich `marketplace.json` entries
    now act as fallback plugin manifests for listing, local detail reads,
    install, and non-curated cache refresh.
    
    The fallback preserves marketplace-entry plugin fields wholesale, then
    adds the small Codex-facing compatibility bridge for presentation
    metadata. A real source `plugin.json` always wins when present.
    
    ## Details
    
    - Capture flattened marketplace-entry fields into
    `MarketplacePluginManifestFallback`, preserving fields such as
    `version`, `description`, `skills`, `mcpServers`, `apps`, `hooks`,
    `agents`, `commands`, `strict`, `author`, and future manifest fields
    without a per-field translation list.
    - Bridge Claude-style top-level `displayName`, `author.name`,
    `homepage`, and marketplace `category` into Codex's nested `interface`
    fields only when the nested values are absent.
    - Treat fallback metadata as installable only when the marketplace entry
    contributes metadata beyond bare `name` and `source`; existing
    missing-manifest behavior remains for metadata-free entries.
    - Read local plugin details from the already parsed fallback manifest,
    including fallback-declared app and MCP paths, instead of rereading only
    an on-disk manifest.
    - Pass fallback contents into `PluginStore`, which validates them and
    injects `.codex-plugin/plugin.json` into Store's existing atomic copy.
    Local marketplace source directories are never mutated, and the fallback
    path no longer needs an additional staging directory.
    - Keep Git source materialization unchanged; Git clones still use the
    existing marketplace source staging area before Store installation.
  • [codex] Cache plugin metadata for tool suggestions (#27812)
    ## Why
    
    `built_tools` runs for every sampling request, and local plugin
    discovery was repeatedly rereading plugin manifests, skills, MCP
    configuration, and app declarations to build the same tool-suggest
    metadata.
    
    That source-derived metadata is stable until the existing plugin manager
    reloads its cache. Runtime eligibility still needs to reflect the
    current install, disable, policy, app-overlap, and authentication state.
    
    ## What changed
    
    - Add a bounded, in-memory tool-suggest metadata cache owned by
    `PluginsManager`.
    - Key cached metadata by plugin identity and source, while applying
    authentication routing each time the metadata is projected.
    - Invalidate the metadata alongside the existing loaded-plugin cache,
    including its normal configuration, marketplace refresh, and
    remote-installed-plugin invalidation paths.
    - Guard against an in-flight load repopulating stale metadata after
    invalidation.
    - Keep marketplace membership and all runtime eligibility filtering live
    rather than introducing a separate catalog or revision model.
    
    ## Impact
    
    Repeated sampling requests reuse already-loaded plugin capability
    metadata while retaining the existing plugin-manager lifecycle as the
    single freshness boundary.
    
    ## Validation
    
    - `just test -p codex-core-plugins` — 252 passed
    - Added focused coverage for cache invalidation and authentication
    reprojection.
  • [codex] Pass plugin namespace into skill loading (#28608)
    ## What changed
    
    - retain the parsed plugin manifest namespace on loaded plugins
    - carry that namespace through `PluginSkillRoot` and `SkillRoot`
    - use the provided namespace when qualifying plugin skill names
    - include the namespace in the skills cache key
    
    ## Why
    
    Plugin loading has already parsed `plugin.json`, but skill parsing
    currently walks every `SKILL.md` ancestor and probes/reads the manifest
    again to reconstruct the same namespace. Passing the parsed namespace
    removes those repeated filesystem calls, which are particularly costly
    on remote filesystems.
    
    Context:
    https://openai.slack.com/archives/C0ARA9GF5D4/p1781639496496439?thread_ts=1781202444.891669&cid=C0ARA9GF5D4
    
    ## Impact
    
    Plugin skill names remain unchanged. A regression test uses a
    deliberately different on-disk manifest name to verify that plugin roots
    use the provided parsed namespace.
    
    ## Validation
    
    - `just test -p codex-core-skills -p codex-core-plugins -p codex-plugin
    -p codex-utils-plugins` (352 passed)
    - `just fix -p codex-core-skills -p codex-core-plugins -p codex-plugin
    -p codex-utils-plugins`
    - `just fmt`
  • [codex] Support plugin manifest path lists (#28790)
    ## Summary
    
    Allow plugin manifests to declare `skills` as either a single path
    string or an array of path strings in the core plugin loader.
    
    ## Why
    
    Some plugin packages need to expose skills from more than one directory.
    Before this change, `plugin.json` only accepted a single string for
    `skills`, so manifests like this were ignored as an invalid `skills`
    shape:
    
    ```json
    {
      "skills": ["./skills/abc", "./skills/edk"]
    }
    ```
    
    This keeps the existing single-string form working while adding support
    for the list form. The final scope is intentionally limited to the core
    plugin manifest/load path for `skills`; `apps`, file-backed
    `mcpServers`, and the bundled plugin-creator assets are unchanged in
    this PR.
    
    ## What changed
    
    - Parse `skills` as either a string or an array of strings in
    `plugin.json`.
    - Store resolved skill paths as a list in `PluginManifestPaths`.
    - Load manifest-declared skill roots in addition to the default
    `./skills` root.
    - Deduplicate exact duplicate skill roots before loading.
    - Rely on existing skill-loader dedupe by canonical `SKILL.md` path for
    overlapping roots such as `./skills` plus `./skills/abc`.
    - Update plugin manifest tests to cover:
      - single string `skills`
      - list of string `skills`
      - duplicate skill roots
      - `./skills` as a manifest path
      - explicit child roots like `./skills/abc` and `./skills/edk`
      - overlapping-root dedupe
    
    ## Validation
    
    - `just test -p codex-plugin`
    - `just test -p codex-core-plugins`
    - `just test -p codex-mcp-extension`
    - `git diff --check`
  • [codex] Support object-valued plugin MCP manifests (#28580)
    ## Summary
    This fixes plugin manifest parsing for MCP servers declared as an object
    directly in `plugin.json`.
    
    Before this change, Codex modeled `mcpServers` as only a string path,
    for example:
    
    ```json
    {
      "name": "counter-sample",
      "version": "1.1.1",
      "mcpServers": "./.mcp.json"
    }
    ```
    
    Some migrated plugins instead provide the server map directly in the
    manifest:
    
    ```json
    {
      "name": "counter-sample",
      "version": "1.1.1",
      "description": "Plugin that declares MCP servers in the manifest",
      "mcpServers": {
        "counter": {
          "type": "http",
          "url": "https://sample.example/counter/mcp"
        }
      }
    }
    ```
    
    That object form previously failed during install/load with an error
    like:
    
    ```text
    failed to parse plugin manifest: invalid type: map, expected a string
    ```
    
    ## What changed
    - Add a manifest representation for `mcpServers` as either
    `Path(Resource)` or `Object(map)`.
    - Parse `plugin.json` `mcpServers` as either a string path or an object.
    - Route object-valued MCP server maps through the existing plugin MCP
    config parser instead of adding a second parser.
    - Apply existing per-plugin MCP server policy to object-valued MCP
    servers the same way as file-backed MCP servers.
    - Include object-valued MCP server names in plugin telemetry/capability
    metadata.
    - Support object-valued MCP config for executor plugins without
    requiring a `.mcp.json` filesystem read.
    - Update the bundled plugin-creator validator and `plugin-json-spec.md`
    so generated-plugin validation accepts the same object-valued shape.
    
    ## Compatibility
    Existing plugin manifests that use `"mcpServers": "./.mcp.json"`
    continue to work. Plugins can now also use the object shape shown above.
    
    ## Tests
    Added coverage for the new manifest attribute shape at the install,
    normal load, telemetry, and executor-provider layers:
    
    - `install_accepts_manifest_mcp_server_objects`
    - `load_plugins_loads_manifest_mcp_server_objects`
    - `plugin_telemetry_metadata_uses_manifest_mcp_server_objects`
    - `reads_manifest_object_config_without_executor_file_system_access`
    
    Also smoke-tested the plugin-creator validator against both supported
    forms:
    
    - `mcpServers` as a direct object in `plugin.json`
    - `mcpServers` as `"./.mcp.json"` with a companion `.mcp.json`
    
    ## Validation
    - `just test -p codex-plugin`
    - `just test -p codex-core-plugins`
    - `just test -p codex-mcp-extension`
    - `just bazel-lock-update`
    - `just bazel-lock-check`
    - `just fmt`
    - `git diff --check`
    - Focused rename/object-form rerun: `just test -p codex-core-plugins
    manager::tests::load_plugins_loads_manifest_mcp_server_objects
    manager::tests::plugin_telemetry_metadata_uses_manifest_mcp_server_objects
    store::tests::install_accepts_manifest_mcp_server_objects`
    - Focused executor rerun: `just test -p codex-mcp-extension
    executor_plugin::provider::tests::reads_manifest_object_config_without_executor_file_system_access`
    - `python3
    codex-rs/skills/src/assets/samples/plugin-creator/scripts/validate_plugin.py
    /private/tmp/codex-validator-object`
    - `python3
    codex-rs/skills/src/assets/samples/plugin-creator/scripts/validate_plugin.py
    /private/tmp/codex-validator-path`
  • [codex] [3/4] Activate endpoint plugin recommendations (#27704)
    Summary\n- Await endpoint recommendation selection while constructing
    each authenticated turn, removing the first-turn cache race.\n- Snapshot
    and filter endpoint candidates once per turn, then use that same set for
    the bounded contextual user fragment, tool exposure, and exact install
    validation.\n- Keep recommendation selection ephemeral: do not persist
    recommendation state in or gate resumed threads on prior context.\n-
    Hide the legacy list tool in endpoint mode and preserve legacy discovery
    unchanged when the endpoint is disabled or unavailable.\n- Keep remote
    plugin and connector app identities out of model-visible context and
    attach them only to Codex-owned elicitation metadata.\n\nStack\n- 3/4,
    based on #28400.\n- Endpoint client and cache: #28399.\n- Generalized
    suggestion presentation: #28400.\n- Install-schema follow-up:
    #28403.\n\nValidation\n- \n- \n- \n- \n- Full : 2,649 passed and 88
    environment-dependent tests failed because this sandbox cannot write ,
    nest Seatbelt, or locate auxiliary test binaries.
  • [codex] [1/4] Add recommended plugin endpoint cache (#28399)
    Summary
    - Add authenticated parsing for `/ps/plugins/suggested?scope=GLOBAL`,
    including remote plugin and connector app identities.
    - Validate, deduplicate, sort, and cap endpoint candidates before
    caching them by backend and account identity.
    - Deduplicate concurrent cache misses and warm recommendations from the
    existing remote-installed-plugin refresh path used at startup and after
    account changes.
    - Keep endpoint results model-invisible in this PR; failures and
    responses without `enabled: true` resolve to legacy mode.
    
    Stack
    - 1/3. Follow-up: #28400 generalizes plugin suggestion presentation
    without activating endpoint recommendations.
    - Final activation: #27704.
    
    Validation
    - `just test -p codex-core-plugins recommended_plugins`
    - `just fix -p codex-core-plugins`
    - `just fmt`
    - `git diff --check`
  • [codex] Clarify plugin load and runtime capability stages (#28472)
    ## Summary
    
    Plugin loading and auth projection both previously produced
    `PluginLoadOutcome`. That made an unfiltered load result look like
    runtime-ready capabilities and generated capability summaries before
    auth routing had run.
    
    This change keeps loaded plugin records in the cache, applies the
    current auth policy in `PluginsManager`, and only then builds
    `PluginLoadOutcome` and its summaries. Auth changes still reuse the
    cached disk load and re-resolve apps and MCP servers without reloading
    plugins.
    
    The updated tests cover cached auth changes and verify that capability
    summaries match the effective app/MCP surface.
    
    ## Testing
    
    - `just test -p codex-core-plugins`
    - `just test -p codex-plugin`
    - `just fix -p codex-core-plugins`
  • [codex] Make plugin details capability aware (#27958)
    ## Summary
    
    Makes plugin details/read flows capability-aware so auth-filtered plugin
    surfaces report the same usable app/MCP/skill shape as the marketplace
    and install flows.
    
    ## Validation
    
    Not run; this change was rebased onto the current plugin auth stack and
    pushed as a draft PR.
    
    **Manual test**
    1. set up a local marketplace with a plugin that has both app and mcp
    declarations
    
    ```
    // .app.json
    {
      "apps": {
        "linear": {
          "id": "some_id"
        }
      }
    }
    
    ```
    
    ```
    // .mcp.json
    {
      "mcpServers": {
        "linear": {
          "type": "http",
          "url": "https://mcp.linear.app/mcp",
          "oauth_resource": "https://mcp.linear.app/mcp"
        },
        "linear2": {
          "type": "http",
          "url": "https://mcp.linear2.app/mcp",
          "oauth_resource": "https://mcp.linear2.app/mcp"
        }
      }
    }
    ```
    
    2a. **login in with api key** and observe plugin details page which
    shows no apps (note we don't show "app not available due to api key log
    in as there's no way to differentiate between no apps and app without
    substitute mcp exists" without significantly more code changes, i've
    separated this to a follow up if we want that behaviour.
    <img width="1170" height="279" alt="Screenshot 2026-06-15 at 23 45 40"
    src="https://github.com/user-attachments/assets/d36cb160-fbec-461e-9643-9c761dbae7bb"
    />
    <img width="975" height="640" alt="Screenshot 2026-06-15 at 18 40 30"
    src="https://github.com/user-attachments/assets/90ec0bc8-7506-4b90-bbd3-070720de799e"
    />
    
    
    2b. **log in with chat** and observe intended conflict resolution logic
    <img width="1165" height="224" alt="Screenshot 2026-06-15 at 17 17 30"
    src="https://github.com/user-attachments/assets/80adfbf2-7dac-4f08-8b76-8eeeab6c95e7"
    />
    <img width="968" height="567" alt="Screenshot 2026-06-15 at 18 38 59"
    src="https://github.com/user-attachments/assets/9ea92c5e-535b-4aa4-8ad0-ee513b57bc3c"
    />
  • [codex] Load API curated marketplace by auth (#28383)
    ## Summary
    - choose the local OpenAI curated marketplace manifest based on auth:
    Codex backend auth gets the existing marketplace, direct provider auth
    gets `api_marketplace.json`
    - include Bedrock API key auth in the direct-provider API marketplace
    path
    - safely skip the API marketplace when `api_marketplace.json` is absent
    
    ## Validation
    - `just fmt`
    - `git diff --check origin/main...HEAD`
    - CI should run the full validation
    
    ## Manual Testing
    
    ### - New api marketplace not available for API key sign
    1. Safely not display anything from api marketplace
    <img width="1161" height="289" alt="Screenshot 2026-06-15 at 21 37 43"
    src="https://github.com/user-attachments/assets/a5f16642-8a20-4ac1-a0de-1274a4c7b5b2"
    />
    
    ### - New api marketplace for API key sign in
    1. Setup api_marketplace.json
    ```
    {
      "name": "openai-curated",
      "interface": {
        "displayName": "Codex official"
      },
      "plugins": [
        {
          "name": "linear",
          "source": {
            "source": "local",
            "path": "./plugins/linear"
          },
          "policy": {
            "installation": "AVAILABLE",
            "authentication": "ON_INSTALL"
          },
          "category": "Productivity"
        }
      ]
    }
    ```
    
    2. Log in with API key, observe that only the defined plugin from
    api_marketplace.json is available from "Codex Official" (outside of
    local testing marketplaces)
    <img width="1167" height="446" alt="Screenshot 2026-06-15 at 21 16 53"
    src="https://github.com/user-attachments/assets/7cf61477-d826-4ef6-bc05-0a23ac1c0259"
    />
    
    also checked functionality on codex app
    
    ### - SiWC users 
    Still uses 'default' marketplace.json and renders all plugins
    <img width="1171" height="502" alt="Screenshot 2026-06-15 at 21 40 25"
    src="https://github.com/user-attachments/assets/d212ea9b-0aa5-470b-8ea4-450efe65bb2b"
    />
    
    also checked functionality on codex app
    
    
    ## Notes
    - `just test -p codex-core-plugins` was started locally before splitting
    branches, but I stopped relying on local tests per follow-up and left
    final validation to PR CI.
  • [codex] Skip plugin MCP OAuth for matching app routes (#27461)
    ## Context
    
    This is PR5 in the plugin auth-routing stack. Earlier PRs make plugin
    surface projection auth-aware, narrow App/MCP conflicts by App
    declaration name, and keep connector listings auth-aware. This PR
    applies the same name-based App/MCP conflict rule into plugin MCP
    loading, so install-time MCP OAuth and plugin detail metadata both
    reflect the MCPs available for the current auth route.
    
    ## Stack
    
    - PR1: #27652 seed plugin manager auth at construction.
    - PR2: #27459 route plugin surfaces by auth mode.
    - PR3: #27607 dedupe plugin MCP servers by App declaration name.
    - PR4: #27602 preserve plugin Apps in connector listings.
    - PR5: #27461 skip install-time plugin MCP OAuth for matching App
    routes.
    
    ## Summary
    
    - Make `load_plugin_mcp_servers` auth-aware and let it load App
    declarations before filtering same-name MCP servers for Codex-backend
    auth.
    - Use that filtered MCP list for both install-time MCP OAuth and
    marketplace plugin detail metadata.
    - Preserve API-key/direct auth behavior so plugin MCP servers remain
    visible and can still start OAuth.
    
    ## Validation
    
    ```bash
    cargo fmt --all
    cargo test -p codex-core-plugins read_plugin_for_config_filters_mcp_servers_for_codex_backend_auth
    cargo check -p codex-core-plugins -p codex-app-server
    git diff --check
    git diff --cached --check
    ```
  • [codex] Dedupe plugin MCPs by app declaration name (#27607)
    ## Context
    
    This is the next step in the plugin auth-routing stack. The earlier PRs
    make `PluginsManager` auth-aware and move the broad App/MCP surface
    decision into that layer. This PR narrows the ChatGPT/SIWC behavior so
    we only hide a plugin MCP server when it conflicts with an App
    declaration of the same name.
    
    In product terms: if a plugin exposes both an App route and MCP route
    for `foo`, ChatGPT/SIWC sessions should use the App route for `foo`. If
    the same plugin also exposes a separate MCP server like `foo2`, that MCP
    server should remain available.
    
    ```json
    // .app.json
    {
      "apps": {
        "foo": {
          "id": "connector_abc"
        }
      }
    }
    ```
    
    ```json
    // .mcp.json
    {
      "mcpServers": {
        "foo": {
          "url": "https://mcp.foo.com/mcp"
        },
        "foo2": {
          "url": "https://mcp.foo2.com/mcp"
        }
      }
    }
    ```
    
    ## Stack
    
    - PR1: #27652 seed plugin manager auth at construction.
    - PR2: #27459 route plugin surfaces by auth mode.
    - PR3: #27607 dedupe plugin MCP servers by App declaration name.
    - PR4: #27602 preserve plugin Apps in connector listings.
    - PR5: #27461 skip install-time plugin MCP OAuth for matching App
    routes.
    
    ## Summary
    
    - Preserve App declaration names in loaded plugin metadata.
    - Keep public effective App outputs as deduped connector IDs for
    existing callers.
    - For ChatGPT/SIWC, suppress only plugin MCP servers whose names match
    declared App names.
    
    ## Validation
    
    ```bash
    cargo fmt --all
    cargo test -p codex-core-plugins plugin_auth_projection
    cargo test -p codex-core-plugins effective_apps
    cargo test -p codex-core-plugins read_plugin_for_config_installed_git_source_reads_from_cache_without_cloning
    cargo test -p codex-core explicit_plugin_mentions_use_apps_for_chatgpt_dual_surface_plugins
    cargo test -p codex-core explicit_plugin_mentions_keep_non_conflicting_mcp_for_chatgpt_auth
    cargo test -p codex-app-server --test all plugin_install_filters_disallowed_apps_needing_auth
    git diff --check
    ```
    
    ---------
    
    Co-authored-by: Xin Lin <xl@openai.com>
  • [codex] Gate plugin MCP servers by auth route (#27459)
    ## Context
    
    Some plugins expose both Apps and MCP servers. This PR moves auth-aware
    surface projection into `core-plugins::PluginsManager`, so callers get a
    consistent effective plugin view. Later PRs narrow the conflict rule and
    update listing/install paths.
    
    The high level goal of this PR is to set up the plumbing to
    conditionally filter App/MCP in the plugin manager layer. We start by
    removing MCP servers when using SIWC/Codex-backend auth, and removing
    Apps when using API-key-style auth.
    
    This PR is now stacked on #27652, which contains only the constructor
    plumbing for seeding `PluginsManager` with the current auth mode.
    
    ## Stack
    
    - PR1: #27652 seed plugin manager auth at construction.
    - PR2: #27459 route plugin surfaces by auth mode.
    - PR3: #27607 dedupe plugin MCP servers by App declaration name.
    - PR4: #27602 preserve plugin Apps in connector listings.
    - PR5: #27461 skip install-time plugin MCP OAuth for matching App
    routes.
    
    ## Summary
    
    - API-key/non-ChatGPT routes hide plugin Apps and keep plugin MCPs.
    - ChatGPT/SIWC with Apps enabled keeps plugin Apps and suppresses MCPs
    for dual-surface plugins.
    - MCP-only plugins stay available for ChatGPT/SIWC sessions.
    - Cached plugin load outcomes are re-projected when auth mode changes.
    
    ## Validation
    
    ```bash
    cargo test -p codex-core-plugins plugin_auth_projection
    cargo test -p codex-core list_tool_suggest_discoverable_plugins
    git diff --check
    ```
  • [codex] Add auth mode to plugin manager constructor (#27652)
    ## Context
    
    Plugins can expose more than one way for Codex to use them: App
    connectors for ChatGPT/SIWC-backed sessions and MCP servers for API key
    login sessions. The broader goal is to make `PluginsManager` the place
    that understands which plugin surfaces should be visible for the current
    auth route, so callers do not each have to make that decision
    themselves.
    
    This PR is the small setup step for that work. It lets the plugin
    manager be created with the current `AuthMode`, which gives the followup
    auth routing PRs the information they need without relying on setter
    injection.
    
    ## Stack
    
    - PR1: #27652 seed plugin manager auth at construction.
    - PR2: #27459 route plugin surfaces by auth mode.
    - PR3: #27607 dedupe plugin MCP servers by App declaration name.
    - PR4: #27602 preserve plugin Apps in connector listings.
    - PR5: #27461 skip install-time plugin MCP OAuth for matching App
    routes.
    
    ## Summary
    
    - Let `PluginsManager::new_with_restriction_product` accept an initial
    `AuthMode`.
    - Keep `PluginsManager::new` behavior unchanged for ordinary callers.
    
    ## Validation
    
    ```bash
    cargo test -p codex-core-plugins plugins_manager_tracks_auth_mode
    cargo test -p codex-core list_tool_suggest_discoverable_plugins
    git diff --check
    ```
    
    ---------
    
    Co-authored-by: Xin Lin <xl@openai.com>
  • [codex] Pass auth mode to plugin manager (#27517)
    ## Summary
    - Add auth mode state to `PluginsManager`.
    - Sync the plugin manager auth mode when `ThreadManager` is created and
    when account auth changes.
    - Route plugin load outcomes through an auth-aware projection hook so
    follow-up plugin filtering can stay inside `core-plugins`.
    
    ## Motivation
    This prepares plugin capability loading to be configured by auth mode,
    such as hiding or exposing app/MCP-backed plugin surfaces based on
    whether the user is using ChatGPT auth or API-key auth, without leaking
    those details outside the plugin manager.
    
    ## Tests
    - `just fmt`
    - `just test -p codex-core-plugins`
    - `env -u CODEX_SANDBOX_NETWORK_DISABLED -u CODEX_SANDBOX just test -p
    codex-core thread_manager::tests`
    - `env -u CODEX_SANDBOX_NETWORK_DISABLED -u CODEX_SANDBOX just test -p
    codex-app-server`
  • [codex] Skip local curated discovery for remote plugins (#27311)
    ## Summary
    
    - skip the local `openai-curated` marketplace before marketplace loading
    when tool-suggest discovery uses remote plugins
    - preserve existing marketplace listing behavior for all other callers
    and when remote plugins are disabled
    - add regression coverage proving the curated marketplace is excluded
    before its malformed manifest can be read
    
    ## Why
    
    Tool-suggest discovery previously loaded every local `openai-curated`
    plugin manifest and only discarded that marketplace afterward when
    remote plugins were enabled. The remote catalog is used in that mode, so
    the local scan consumed CPU without contributing discoverable plugins.
    
    ## Impact
    
    Remote-plugin tool suggestion discovery no longer reads the local
    curated marketplace and its plugin manifests. `openai-bundled`,
    configured marketplaces, normal `plugin/list` behavior, and local
    curated discovery when remote plugins are disabled are unchanged.
    
    ## Validation
    
    - `just test -p codex-core-plugins
    list_marketplaces_can_skip_openai_curated_before_loading`
    - `just test -p codex-core
    list_tool_suggest_discoverable_plugins_omits_openai_curated_when_remote_enabled`
    - `just fmt`
    - `git diff --check`
  • [codex] Return workspace directory installed plugins (#27098)
    ## Summary
    
    - return installed `workspace-directory` remote plugins by default in
    `plugin/installed`
    - keep shared-with-me installed plugins gated behind `plugin_sharing`
    - filter remote installed plugin marketplaces by canonical marketplace
    name instead of coarse workspace scope
    
    ## Validation
    
    - `just fmt`
    - `just test -p codex-core-plugins`
    - `just test -p codex-app-server`
    - `just fix -p codex-core-plugins`
    - `just fix -p codex-app-server`
    - `$xin-build` targeted verification:
    - `just test -p codex-core-plugins
    build_remote_installed_plugin_marketplaces_from_cache_filters_by_marketplace_name`
    - `just test -p codex-app-server
    plugin_installed_includes_workspace_directory_without_plugin_sharing`
    - `just test -p codex-app-server
    plugin_installed_includes_remote_shared_with_me_plugins`
    - `just test -p codex-app-server
    plugin_list_omits_shared_with_me_kind_when_plugin_sharing_disabled`
  • [codex] Prune stale curated plugin caches (#26934)
    Curated plugin startup refresh now removes cached plugins whose names no
    longer appear in the raw openai-curated marketplace. This prevents users
    with the old standalone Google Sheets plugin selected locally from
    continuing to load its stale cache after the curated repo drops it.
    
    Existing config is left untouched, and plugins still present in the
    marketplace continue to refresh from local curated sources.
    
    Validation:
    - `just fmt`
    - `just test -p codex-core-plugins`
    - `git diff --check`
  • [codex] Remove legacy remote plugin startup sync (#25936)
    ## Summary
    
    - Remove the legacy startup remote plugin sync path that called
    `/plugins/list` and reconciled curated plugin cache/config.
    - Remove the `sync_plugins_from_remote` API, its result/error types,
    startup marker task, and tests that expected the legacy request.
    - Keep the current remote installed bundle sync and remote catalog flows
    (`/ps/plugins/installed` and `/ps/plugins/list`) intact.
    
    ## Validation
    
    - `just fmt`
    - `git diff --check`
    - `env HOME=/private/tmp/codex-xin-build-home
    USERPROFILE=/private/tmp/codex-xin-build-home just test -p
    codex-core-plugins`
    - Searched for legacy `/plugins/list` sync references; remaining matches
    are `/ps/plugins/list` catalog tests/code.
    
    ## Notes
    
    - `just test -p codex-app-server plugin_list` is currently blocked
    before running filtered tests by an unrelated compile error in
    `app-server/tests/suite/v2/image_generation.rs`:
    `app_test_support::McpProcess` is not exported.
  • Speed up TUI startup by reusing plugin discovery (#26469)
    ## Summary
    
    TUI startup loads related plugin data from `hooks/list`, session MCP
    initialization, and plugin skill warmup. These paths repeated filesystem
    discovery and emitted the same plugin warnings, while `hooks/list` and
    account/model bootstrap ran serially.
    
    This change:
    
    - Reuses one immutable plugin load outcome across startup consumers.
    - Keys the cache only on plugin-relevant configuration.
    - Single-flights concurrent plugin loads and prevents invalidated loads
    from repopulating the cache.
    - Runs hook discovery and account/model bootstrap concurrently.
    - Preserves configuration-migration ordering, hook review behavior, and
    accurate startup telemetry.
    
    In 10 alternating release-build launches in the Ruff repository with the
    existing `~/.codex` configuration, median time to the first editable
    composer decreased from 833ms to 504ms. The branch was faster in 9 of 10
    pairs, with a paired median improvement of 312ms.
  • Load plugin hooks without other plugin capabilities (#26272)
    ## Summary
    
    `hooks/list` only consumes plugin hook declarations, but previously
    loaded every enabled plugin's skills, MCP configuration, apps, and
    capability summary before discarding them.
    
    In a local benchmark, this reduced `hooks/list` latency by over 100ms
    (e.g., from 594 to 467ms on startup, and 168 to 16ms when making a
    `hooks/list` call later in the same TUI session). This is on the
    critical path to rendering the TUI, so every 10s of ms should be eyed
    skeptically (IMO).
    
    This change adds a hook-specific plugin loading path that preserves
    plugin enablement, remote/local conflict resolution, deterministic
    ordering, manifest resolution, and hook-loading warnings while skipping
    unrelated capabilities. (I think there's room for a more general design
    here that allows you to project the capabilities you need at load-time,
    but that seems unnecessary right now.)
  • Handle invalid plugin skills manifest field (#25717)
    ## Summary
    - Treat invalid `plugin.json` `skills` shapes as a field-level warning
    instead of rejecting the whole manifest
    - Keep valid string path behavior unchanged and continue falling back to
    the default `skills/` root
    - Add regression coverage for array-shaped `skills`
    
    ## Tests
    - `just fmt`
    - `cargo test -p codex-core-plugins`
  • fix: Deduplicate installed local and remote curated plugins (#25681)
    ## Summary
    - Deduplicate installed `openai-curated` and `openai-curated-remote`
    plugin conflicts by feature flag.
    - Prefer remote when remote plugins are enabled; otherwise prefer local,
    while preserving one-sided installs.
    
    ## Testing
    - `just fmt`
    - `git diff --check`
    - Targeted `just test` was blocked locally because `cargo-nextest` is
    not installed.
  • Preserve plugin app manifest order (#25491)
    ## Summary
    - Preserve app declaration order when loading plugin .app.json files.
    - Keep plugin connector summaries in plugin app order after connector
    metadata is merged and filtered.
    - Add regression coverage for .app.json order and connector summary
    order.
    
    ## Validation
    - just fmt
    - just test -p codex-chatgpt
    connectors_for_plugin_apps_returns_only_requested_plugin_apps
    - just test -p codex-core-plugins
    effective_apps_preserves_app_config_order
    - just fix -p codex-core-plugins (passes with existing clippy
    large_enum_variant warning in core-plugins/src/manifest.rs)
    - just fix -p codex-chatgpt
    - just bazel-lock-update
    - just bazel-lock-check
  • Remove plugin hooks feature flag (#22552)
    # Why
    
    This is a follow-up stacked on top of the `plugin_hooks` default-on
    change. Once we are comfortable making plugin hooks part of the normal
    plugin behavior, the separate feature flag stops buying us much and
    leaves extra branching/cache state behind.
    
    # What
    
    - remove the `PluginHooks` feature and generated config-schema entries
    - make plugin hook loading/listing follow plugin enablement directly
    - drop plugin-manager cache/state that only existed to distinguish
    hook-flag toggles
    - remove tests and fixtures that modeled `plugin_hooks = true/false`
  • Route MCP servers through explicit environments (#23583)
    ## Summary
    - route each configured MCP server through an explicit per-server
    `environment_id` instead of a manager-wide remote toggle
    - default omitted `environment_id` to `local`, resolve named ids through
    `EnvironmentManager`, and fail only the affected MCP server when an
    explicit id is unknown
    - keep local stdio on the existing local launcher path for now, while
    named-environment stdio uses the selected environment backend and
    requires an absolute `cwd`
    - allow local HTTP MCP servers to keep using the ambient HTTP client
    when no local `Environment` is configured; named-environment HTTP MCPs
    use that environment's HTTP client
    
    ## Validation
    - devbox Bazel build: `bazel build --bes_backend= --bes_results_url=
    //codex-rs/cli:codex //codex-rs/rmcp-client:test_stdio_server
    //codex-rs/rmcp-client:test_streamable_http_server`
    - devbox app-server config matrix with real `config.toml` /
    `environments.toml` files covering omitted local, explicit local,
    omitted local under remote default, explicit remote stdio, local HTTP
    without local env, explicit remote HTTP, local stdio without local env,
    unknown explicit env, and remote stdio without `cwd`
  • feat(plugins): tabulate plugin list output (#23727)
    ## Summary
    - render `codex plugin list` as one table per marketplace with the
    marketplace manifest path shown above each table
    - surface the installed plugin version in the CLI output by threading
    `installed_version` through marketplace listing state
    - narrow the system-root exemption so only known bundled/runtime
    marketplaces skip missing-manifest failures, and keep `VERSION` empty
    for cached-but-unconfigured plugins
    
    ## Rationale
    The plugin list UX was hard to scan as a flat list and did not show
    which installed version was active. This change makes the CLI output
    easier to read in the real multi-marketplace case, keeps the plugin path
    visible, fixes the Sapphire regression where bundled/runtime marketplace
    roots were blocking `plugin list`, and addresses the two review findings
    that came out of the follow-up deep review.
    
    ## Key Decisions
    - kept the CLI output grouped per marketplace instead of one global
    table so the marketplace path can live with the rows it owns
    - kept `VERSION` as the installed version, which means it is empty until
    a plugin is actually installed
    - handled the bundled/runtime regression in the CLI snapshot validation
    path rather than widening app-server protocol or changing marketplace
    loading behavior
    - narrowed the exemption to known system marketplace names plus expected
    system paths, so user-configured marketplaces under those directories
    still fail loudly
    - gated `installed_version` on actual installed state so `VERSION`
    cannot show stale cache state for `not installed` rows
    
    ## Validation
    - `just fmt`
    - Sapphire: `cargo test -p codex-cli --test plugin_cli` (`14 passed; 0
    failed`)
    - Sapphire smoke test: bundled/runtime roots still work
      - `cargo run -q -p codex-cli -- plugin add sample@debug`
      - `cargo run -q -p codex-cli -- plugin list`
    - verified the bundled/runtime-root scenario no longer errors and shows
    the expected marketplace table output
    - Sapphire smoke test: custom marketplace under bundled path still
    errors
    - verified `failed to load configured marketplace snapshot(s)` for
    `custom-marketplace`
    - Sapphire smoke test: cached-but-unconfigured plugin hides version
    - verified `sample@debug not installed` renders with an empty `VERSION`
    column
    
    ## Sample Output
    ```text
    /tmp/custom-marketplace/plugin.json
    NAME          VERSION  STATUS         DESCRIPTION
    sample@debug  1.0.0    enabled        Debug sample plugin
    other@local            not installed  Local development plugin
    ```
  • feat: Add vertical remote plugin collection support (#23584)
    - Adds an explicit vertical marketplace kind for plugin/list that
    fail-open fetches collection=vertical only when full remote plugins are
    disabled.
    
    - Renames the global remote marketplace/cache identity to
    openai-curated-remote and materializes remote installs with backend
    release versions and app manifests.
  • [codex] Add installed-plugin mention API (#22448)
    ## Summary
    - add app-server `plugin/installed` for mention-oriented plugin loading
    - return installed plugins plus explicitly requested install-suggestion
    rows
    - keep remote handling on installed-state data instead of the broad
    catalog listing path
    
    ## Why
    The `@` mention surface only needs plugins that are usable now, plus a
    small product-approved set of install suggestions. It does not need the
    full catalog-shaped `plugin/list` payload that the Plugins page uses.
    
    ## Validation
    - `just write-app-server-schema`
    - `just fmt`
    - `cargo test -p codex-app-server-protocol`
    - `cargo test -p codex-core-plugins`
    - `cargo test -p codex-app-server --test all plugin_installed_`
    
    ## Notes
    - The package-wide `cargo test -p codex-app-server` run still hits an
    existing unrelated stack overflow in
    `in_process::tests::in_process_start_clamps_zero_channel_capacity`.
    - Companion webview PR: https://github.com/openai/openai/pull/915672
  • Support explicit MCP OAuth client IDs (#22575)
    ## Why
    Some MCP OAuth providers require a pre-registered public client ID and
    cannot rely on dynamic client registration. Codex already supports MCP
    OAuth, but it had no way to supply that client ID from config into the
    PKCE flow.
    
    ## What changed
    - add `oauth.client_id` under `[mcp_servers.<server>]` config, including
    config editing and schema generation
    - thread the configured client ID through CLI, app-server, plugin login,
    and MCP skill dependency OAuth entrypoints
    - configure RMCP authorization with the explicit client when present,
    while preserving the existing dynamic-registration path when it is
    absent
    - add focused coverage for config parsing/serialization and OAuth URL
    generation
    
    ## Verification
    - `cargo test -p codex-config -p codex-rmcp-client -p codex-mcp -p
    codex-core-plugins`
    - `cargo test -p codex-core blocking_replace_mcp_servers_round_trips
    --lib`
    - `cargo test -p codex-core
    replace_mcp_servers_streamable_http_serializes_oauth_resource --lib`
    - `cargo test -p codex-core config_schema_matches_fixture --lib`
    
    ## Notes
    Broader local package runs still hit unrelated pre-existing stack
    overflows in:
    - `codex-app-server::in_process_start_clamps_zero_channel_capacity`
    -
    `codex-core::resume_agent_from_rollout_uses_edge_data_when_descendant_metadata_source_is_stale`
  • Relax remote plugin sync gate (#22594)
    ## Summary
    - Allow remote installed-plugin cache refresh to start whenever plugins
    are enabled.
    - Allow remote installed-plugin bundle sync to start whenever plugins
    are enabled.
    - Remove the extra local `remote_plugin_enabled` guard from those
    background sync paths.
    
    ## Context
    Server-side installed plugin state and optional bundle URL behavior are
    owned by plugin-service `/public/plugins/installed`, so these local sync
    paths only need the overall plugin enablement gate.
    
    ## Test plan
    - `just fmt`
    - `cargo test -p codex-core-plugins`
  • feat: Expose plugin versions and gate plugin sharing (#22397)
    - Adds localVersion to plugin summaries and remoteVersion to share
    context, including generated API schemas.
    - Hydrates local and remote plugin versions from manifests and remote
    release metadata.
    - Adds default-on plugin_sharing gate for shared-with-me listing and
    plugin/share/save, with disabled-path errors
        and focused coverage.
  • Read cached metadata for installed Git plugins (#20825)
    ## Summary
    - Populate `plugin/list` interface metadata for installed Git-sourced
    marketplace plugins from the active cached plugin bundle.
    - Preserve marketplace category precedence so list behavior matches
    `plugin/read`.
    - Keep existing fallback behavior when the cache or manifest is missing
    or invalid.
    
    ## Test Plan
    - `cd codex-rs && just fmt`
    - `cd codex-rs && cargo test -p codex-core-plugins
    list_marketplaces_installed_git_source_reads_metadata_from_cache_without_cloning`
    - `cd codex-rs && cargo test -p codex-app-server
    plugin_list_returns_installed_git_source_interface_from_cache`
    - `cd codex-rs && just fix -p codex-core-plugins`
    - `cd codex-rs && just fix -p codex-app-server`
    - `git diff --check`
    
    Server-truth check: OpenAI monorepo app-server generated types already
    expose `PluginSummary.interface`, and the webview consumes it for plugin
    cards. This PR keeps the protocol/schema unchanged and fills the
    existing field from the cached installed bundle for Git-backed
    cross-repo plugins.
  • Show plugin hooks in plugin details (#21447)
    Supersedes the abandoned #19859, rebuilt on latest `main`.
    
    # Why
    
    PR #19705 adds discovery for hooks bundled with plugins, but `/plugins`
    still only shows skills, apps, and MCP servers. This follow-up makes
    bundled hooks visible in the same plugin detail view so users can
    inspect the full plugin surface in one place.
    
    We also need `PluginHookSummary` to populate Plugin Hooks in the app;
    `hooks/list` is not enough there because plugin detail needs to show
    hooks for disabled plugins too.
    
    # What
    
    - extend `plugin/read` with `PluginHookSummary` entries for bundled
    hooks
    - summarize plugin hooks while loading plugin details
    - render a `Hooks` row in the `/plugins` detail popup
    
    <img width="3456" height="848" alt="CleanShot 2026-04-27 at 11 45 34@2x"
    src="https://github.com/user-attachments/assets/fe3a38d6-a260-4351-8513-fb04c93d725b"
    />
  • Expose plugin manifest keywords in app server (#21271)
    ## Summary
    - Add plugin manifest keywords to core plugin marketplace/detail models
    - Expose keywords on app-server v2 PluginSummary and generated
    schema/types
    - Populate keywords in plugin/list and plugin/read responses for local
    plugins
    
    Depends on https://github.com/openai/openai/pull/891087
    
    ## Validation
    - just fmt
    - just write-app-server-schema
    - cargo test -p codex-app-server-protocol
    - cargo test -p codex-core-plugins
    - cargo test -p codex-app-server
    plugin_list_keeps_valid_marketplaces_when_another_marketplace_fails_to_load
    - cargo test -p codex-app-server
    plugin_read_returns_plugin_details_with_bundle_contents