23 Commits

  • 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.
  • Decouple plugin manifest path resolution (#29620)
    ## Why
    
    Plugin manifests use the same schema whether the package lives on the
    host or in an executor. Only the path representation differs: host
    callers need native `Path` inputs and `AbsolutePathBuf` outputs, while
    executor callers need `PathUri` throughout.
    
    Maintaining separate parsing or resolver implementations would duplicate
    the manifest rules and allow them to drift. This PR instead makes
    URI-native resolution the single parsing path and keeps host conversion
    at the boundary.
    
    ## What changed
    
    - Make `parse_plugin_manifest_uri` the shared manifest parser and
    resolve every path-bearing field as `PathUri`.
    - Keep the existing host entrypoint as a thin adapter: convert its
    native root and manifest path to `PathUri`, run the shared parser, then
    map resources back to `AbsolutePathBuf`.
    - Expose `PluginManifest::try_map_resources` so callers can convert the
    generic resource type without duplicating manifest construction.
    - Resolve relative manifest paths using the root URI's convention:
    backslashes are separators for Windows roots and ordinary filename
    characters for POSIX roots.
    - Apply lexical containment after URI resolution, rejecting absolute
    paths and parent traversal outside the plugin root.
    - Make encoded backslashes fail containment only for Windows URIs;
    encoded `/` remains unsafe for every convention.
    - Use a host-native synthetic root for marketplace fallback manifests so
    the host adapter also works on Windows.
    
    ```text
    host Path --------> PathUri --\
                                  +--> one manifest parser --> PluginManifest<PathUri>
    executor PathUri -------------/
    
    host result: PluginManifest<PathUri> --> PluginManifest<AbsolutePathBuf>
    ```
    
    Existing host manifest behavior is preserved; #28918 is the first
    executor consumer.
    
    ## Verification
    
    - `just test -p codex-utils-path-uri`
    - `just test -p codex-plugin`
    - `just test -p codex-core-plugins`
    
    ## Stack
    
    1. #29614 — add lexical `PathUri` containment.
    2. **This PR** — share URI-native manifest path resolution.
    3. #28918 — 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.
  • 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.
  • [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] 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`
  • 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.
  • Add selected-plugin precedence and attribution to the MCP catalog (#27884)
    ## Why
    
    **In short:** this PR resolves already-discovered MCP registrations. It
    does not read selected plugins or discover their MCP servers.
    
    The resolved MCP catalog currently builds config and auto-discovered
    plugin registrations before runtime contributors are applied. A
    thread-selected plugin needs a distinct precedence tier in that same
    initial resolution pass: otherwise a disabled lower-precedence winner
    can leave stale name-level state behind, and the winning MCP tools
    cannot be attributed to the selected package reliably.
    
    This PR adds that catalog boundary before executor discovery is
    connected.
    
    ## What changed
    
    - Added an explicit selected-plugin registration tier between
    auto-discovered plugins and explicit config.
    - Collected selected-plugin contributions before the initial catalog
    build, while leaving compatibility and generic extension overlays in
    their existing runtime phase.
    - Retained the winning plugin ID and display name directly on
    plugin-owned catalog registrations.
    - Derived MCP tool provenance from the winning catalog entry instead of
    joining against local-only plugin summaries.
    - Retained the winning selected server's tool approval policy in the
    running connection manager, so a selected registration cannot inherit
    approval behavior from a losing local plugin.
    - Kept remembered approval session-scoped for selected plugins until
    there is an authority-aware persistence contract; Codex will not write
    approval back to an unrelated local plugin.
    - Preserved existing name-level disabled vetoes for discovered plugins
    and config, while keeping a selected package's own disabled registration
    scoped to that registration.
    - Preserved deterministic selection order and existing config,
    compatibility, and extension precedence.
    
    The resulting order is:
    
    ```text
    auto-discovered plugin
      < selected plugin
      < explicit config
      < compatibility registration
      < extension overlay
    ```
    
    ## Behavior and scope
    
    This is a catalog and provenance change only. No production host
    contributes selected-plugin MCP registrations yet, so existing local MCP
    behavior remains unchanged.
    
    The stacked follow-up, #27870, installs the executor plugin provider
    that produces these registrations. App-server activation remains a
    separate final step.
    
    ## Verification
    
    Focused tests cover precedence, deterministic selected-plugin conflicts,
    disabled-veto behavior across catalog phases, managed requirements
    before selected-plugin resolution, winning-server approval policy, and
    attribution when local and selected packages share an ID or server name.
    CI owns execution of the test suite.
  • build: run buildifier from just fmt (#28125)
    ## Intent
    
    Keep Bazel and Starlark files consistently formatted without requiring
    contributors to install or version buildifier themselves.
    
    ## Implementation
    
    - Add a SHA-256-pinned, cross-platform DotSlash manifest for buildifier
    v8.5.1.
    - Run buildifier from the shared `just fmt` and `just fmt-check` driver,
    with Windows-safe explicit DotSlash invocation.
    - Provision DotSlash in formatting CI and contributor devcontainers, and
    document the source-build prerequisite.
    - Apply the initial mechanical buildifier formatting baseline.
  • [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>
  • Add executor-owned plugin resolution (#27692)
    ## Why
    
    CCA can select a capability root that lives in an executor environment,
    but
    Codex only had a host-filesystem plugin loader. Before selected executor
    plugins can contribute MCP servers, we need a small package boundary
    that can
    answer:
    
    > Does this selected root contain a plugin, and if so, what does its
    manifest
    > declare?
    
    The answer must come from the selected environment's filesystem. A
    failed
    executor lookup must never fall back to the orchestrator filesystem.
    
    ## What this changes
    
    This PR introduces:
    
    ```rust
    PluginProvider::resolve(root)
        -> Result<Option<ResolvedPlugin>, Error>
    ```
    
    `ExecutorPluginProvider` resolves one `SelectedCapabilityRoot` through
    its
    exact `environment_id`. It checks the recognized manifest locations,
    reads the
    manifest through that environment's `ExecutorFileSystem`, and returns an
    inert
    `ResolvedPlugin` containing:
    
    - the opaque selected-root ID;
    - the environment-bound plugin root;
    - the authority-bound manifest resource;
    - parsed metadata and authority-bound component locators.
    
    Descriptor construction rejects manifest or component paths outside the
    selected package root, so consumers cannot accidentally lose the package
    boundary when they receive a resolved plugin.
    
    If the root has no plugin manifest, resolution returns `None`, allowing
    the
    caller to treat it as a standalone capability such as a skill.
    
    ```text
    selected root: repo -> env-1:/workspace/repo
                             |
                             | env-1 filesystem only
                             v
                 .codex-plugin/plugin.json
                             |
                             v
            ResolvedPlugin { authority, root, manifest }
    ```
    
    The existing host loader and the new executor provider now share the
    same
    manifest parser. Existing `codex-core-plugins::manifest` type paths
    remain
    available through re-exports, so host behavior and callers are
    unchanged.
    
    ## Scope
    
    This is intentionally a non-user-visible package-resolution PR. It does
    not:
    
    - parse or register plugin MCP server configurations;
    - activate skills, connectors, hooks, or MCP servers;
    - change app-server wiring;
    - introduce host fallback, caching, or lifecycle behavior.
    
    #27670 has merged, and this PR is now based directly on `main`. Together
    with
    the resolved MCP catalog from #27634, it establishes the inputs needed
    for the
    executor stdio MCP vertical without changing the existing MCP runtime.
    
    ## Follow-up
    
    The next PR will consume `ResolvedPlugin`, read its declared/default MCP
    config
    through the same executor filesystem, bind supported stdio servers to
    that
    environment, and feed those registrations into the resolved MCP catalog.
    An
    app-server E2E will prove that selecting an executor plugin exposes and
    invokes
    its tool on the owning executor.
    
    Resume/fork semantics, dynamic environment replacement, and non-stdio
    placement remain separate lifecycle decisions.
    
    ## Validation
    
    - `just fmt`
    - `cargo check --tests -p codex-plugin -p codex-core-plugins`
    - `just bazel-lock-check`
    - `git diff --check`
    
    Test targets were compiled but not executed locally; CI will run the
    test and
    Clippy suites.
  • fix: Allow plugin skills to share plugin-level icon assets (#23776)
    Thread the plugin root through plugin skill loading so skill interface
    icons can reference shared plugin assets, such as ../../assets/logo.svg.
  • Enable --deny-warnings for cargo shear (#21616)
    ## Summary
    
    In https://github.com/openai/codex/pull/21584, we disabled doctests for
    crates that lack any doctests. We can enforce that property via `cargo
    shear --deny-warnings`: crates that lack doctests will be flagged if
    doctests are enabled, and crates with doctests will be flagged if
    doctests are disabled.
    
    A few additional notes:
    
    - By adding `--deny-warnings`, `cargo shear` also flagged a number of
    modules that were not reachable at all. Some of those have been removed.
    - This PR removes a usage of `windows_modules!` (since `cargo shear` and
    `rustfmt` couldn't see through it) in favor of simple `#[cfg(target_os =
    "windows")]` macros. As a consequence, many of these files exhibit churn
    in this PR, since they weren't being formatted by `rustfmt` at all on
    main.
    - Again, to make the code more analyzable, this PR also removes some
    usages of `#[path = "cwd_junction.rs"]` in favor of a more standard
    module structure. The bin sidecar structure is still retained, but,
    e.g., `windows-sandbox-rs/src/bin/command_runner.rs‎` was moved to
    `windows-sandbox-rs/src/bin/command_runner/main.rs`, and so on.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Add plugin ID to skill analytics (#20923)
    ## Summary
    - thread plugin skill roots through the skills loader with their plugin
    ID
    - store plugin ID on loaded skill metadata for plugin-provided skills
    - include plugin ID on skill invocation analytics events
    
    ## Test plan
    - cargo check -p codex-core-skills
    - cargo check -p codex-core -p codex-core-plugins -p codex-analytics
    - cargo check -p codex-tui
    - cargo check -p codex-plugin -p codex-core -p codex-core-plugins -p
    codex-analytics
    - cargo check -p codex-app-server
    - cargo test -p codex-analytics
    - HOME=/private/tmp/codex-empty-home cargo test -p codex-core-skills
    - just fix -p codex-core-skills
    - just fix -p codex-analytics
    - just fix -p codex-core-plugins
    - just fix -p codex-core
    - just fmt
    - git diff --check
  • Emit analytics for remote plugin installs (#20267)
    ## Summary
    
    - emit `codex_plugin_installed` after a remote plugin install succeeds
    - keep local installs unchanged, but let remote installs override the
    analytics `plugin_id` with the backend remote plugin id
    (`plugins~Plugin_...`)
    - preserve the local/display identity in `plugin_name` and
    `marketplace_name`, plus capability metadata from the installed bundle
    - add regression coverage for local install analytics, remote install
    analytics, and analytics id override serialization
    
    ## Testing
    
    - `just fmt`
    - `cargo test -p codex-analytics`
    - `cargo test -p codex-app-server`
  • Discover hooks bundled with plugins (#19705)
    ## Why
    
    Plugins can bundle lifecycle hooks, but Codex previously only discovered
    hooks from user, project, and managed config layers. This adds the
    plugin discovery and runtime plumbing needed for plugin-bundled hooks
    while keeping execution behind the `plugin_hooks` feature flag.
    
    ## What
    
    - Discovers plugin hook sources from each plugin's default
    `hooks/hooks.json`.
    - Supports `plugin.json` manifest `hooks` entries as either relative
    paths or inline hook objects.
    - Plumbs discovered plugin hook sources through plugin loading into the
    hook runtime when `plugin_hooks` is enabled.
    - Marks plugin-originated hook runs as `HookSource::Plugin`.
    - Injects `PLUGIN_ROOT` and `CLAUDE_PLUGIN_ROOT` into plugin hook
    command environments.
    - Updates generated schemas and hook source metadata for the plugin hook
    source.
    
    ## Stack
    
    1. This PR - openai/codex#19705
    2. openai/codex#19778
    3. openai/codex#19840
    4. openai/codex#19882
    
    ## Reviewer Notes
    
    - Core logic is in `codex-rs/core-plugins/src/loader.rs` and
    `codex-rs/hooks/src/engine/discovery.rs`
    - Moved existing / adding new tests to
    `codex-rs/core-plugins/src/loader_tests.rs` hence the large diff there
    - Otherwise mostly plumbing and minor schema updates
    
    ### Core Changes
    
    The `codex-rs/core` changes are limited to wiring plugin hook support
    into existing core flows:
    
    - `core/src/session/session.rs` conditionally pulls effective plugin
    hook sources and plugin hook load warnings from `PluginsManager` when
    `plugin_hooks` is enabled, then passes them into `HooksConfig`.
    - `core/src/hook_runtime.rs` adds the `plugin` metric tag for
    `HookSource::Plugin`.
    - `core/config.schema.json` picks up the new `plugin_hooks` feature
    flag, and `core/src/plugins/manager_tests.rs` updates fixtures for the
    added plugin hook fields.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • feat: Handle alternate plugin manifest paths (#18182)
    Load plugin manifests through a shared discoverable-path helper so
    manifest reads, installs, and skill names all see the same alternate
    manifest location.
  • Extract codex-core-skills crate (#15749)
    ## Summary
    - move skill loading and management into codex-core-skills
    - leave codex-core with the thin integration layer and shared wiring
    
    ## Testing
    - CI
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Extract codex-plugin crate (#15747)
    ## Summary
    - extract plugin identifiers and load-outcome types into codex-plugin
    - update codex-core to consume the new plugin crate
    
    ## Testing
    - CI
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>