skills: make backend plugin skills invocable without an executor (#27387)

## Why

#27198 made the extension-owned `codex_apps` MCP connection the hosted
plugin runtime, but its `mcp/skill` resources still bypassed the skills
extension. App-server could list and read those resources through
generic MCP APIs, but a thread with no selected environment did not
expose them in the model's skills catalog or load their `SKILL.md`
through `$skill`.

Hosted skills should stay remote while using the same typed catalog,
source authority, deduplication, bounded contextual catalog, and
selected-skill prompt injection as host and executor skills. They should
not be downloaded or exposed as ambient filesystem paths.

## What changed

- Add a session-scoped `McpResourceClient` over the replaceable MCP
connection manager so resource list/read calls follow startup and
refresh replacements.
- Add a `BackendSkillProvider` that pages `codex_apps` resources,
accepts bounded and validated `mcp/skill` entries, and reads a selected
skill's `SKILL.md` through the same MCP connection.
- Register the remote provider in app-server and include it in the
skills catalog even when a thread has no selected capability roots or
executor.
- Contribute hosted skill metadata through the bounded
`AvailableSkillsInstructions` developer-context path, exclude remote
entries from per-turn catalog injection, and classify `<skills>`
messages as contextual developer content so rollback can trim and
rebuild them correctly.

## Testing

- Extend the app-server MCP resource integration test with
`environments: []` to exercise two-page discovery, filter a
non-`mcp/skill` resource, verify the escaped developer catalog entry and
user-role `<skill>` fragment containing the fetched `SKILL.md`, and
preserve generic MCP resource reads.
- Add core event-mapping coverage that classifies `<skills>` developer
messages as contextual history.
This commit is contained in:
jif authored and GitHub committed 2026-06-11 11:28:16 +02:00
1 parent 53b5019745
commit a287c5dffd
26 files changed
+877 -116

No files matched your search

+15
View File
@@ -1,9 +1,12 @@
use codex_core::config::Config;
use codex_protocol::capabilities::SelectedCapabilityRoot;
use std::future::Future;
use std::sync::Mutex;
use tokio::sync::OnceCell;
use crate::catalog::SkillCatalog;
use crate::catalog::SkillCatalogEntry;
use crate::catalog::SkillProviderError;
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct SkillsExtensionConfig {
@@ -24,6 +27,7 @@ impl SkillsExtensionConfig {
pub(crate) struct SkillsThreadState {
config: Mutex<SkillsExtensionConfig>,
selected_roots: Vec<SelectedCapabilityRoot>,
orchestrator_catalog: OnceCell<SkillCatalog>,
}
impl SkillsThreadState {
@@ -34,6 +38,7 @@ impl SkillsThreadState {
Self {
config: Mutex::new(config),
selected_roots,
orchestrator_catalog: OnceCell::new(),
}
}
@@ -54,6 +59,16 @@ impl SkillsThreadState {
pub(crate) fn selected_roots(&self) -> &[SelectedCapabilityRoot] {
&self.selected_roots
}
pub(crate) async fn orchestrator_catalog_snapshot(
&self,
initialize: impl Future<Output = Result<SkillCatalog, SkillProviderError>> + Send,
) -> Result<SkillCatalog, SkillProviderError> {
self.orchestrator_catalog
.get_or_try_init(|| initialize)
.await
.cloned()
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]