## 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.
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.
## 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`
## 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`
## 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.
## 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
## 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>
Load plugin manifests through a shared discoverable-path helper so
manifest reads, installs, and skill names all see the same alternate
manifest location.
Split plugin loading, marketplace, and related infrastructure out of
core into codex-core-plugins, while keeping the core-facing
configuration and orchestration flow in codex-core.
---------
Co-authored-by: Codex <noreply@openai.com>