mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
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:
26 files changed
+877
-116
No files matched your search
@@ -17,11 +17,13 @@ use codex_extension_api::ThreadLifecycleContributor;
|
||||
use codex_extension_api::ThreadStartInput;
|
||||
use codex_extension_api::TurnInputContext;
|
||||
use codex_extension_api::TurnInputContributor;
|
||||
use codex_mcp::McpResourceClient;
|
||||
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
||||
use codex_protocol::protocol::Event;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::WarningEvent;
|
||||
|
||||
use crate::catalog::SkillCatalog;
|
||||
use crate::catalog::SkillCatalogEntry;
|
||||
use crate::catalog::SkillReadResult;
|
||||
use crate::catalog::SkillSourceKind;
|
||||
@@ -84,7 +86,7 @@ impl ConfigContributor<Config> for SkillsExtension {
|
||||
impl ContextContributor for SkillsExtension {
|
||||
fn contribute<'a>(
|
||||
&'a self,
|
||||
_session_store: &'a ExtensionData,
|
||||
session_store: &'a ExtensionData,
|
||||
thread_store: &'a ExtensionData,
|
||||
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Vec<PromptFragment>> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
@@ -92,19 +94,22 @@ impl ContextContributor for SkillsExtension {
|
||||
return Vec::new();
|
||||
};
|
||||
let config = thread_state.config();
|
||||
if !config.include_instructions || thread_state.selected_roots().is_empty() {
|
||||
if !config.include_instructions {
|
||||
return Vec::new();
|
||||
}
|
||||
let catalog = self
|
||||
.providers
|
||||
.list_for_turn(SkillListQuery {
|
||||
turn_id: thread_store.level_id().to_string(),
|
||||
executor_roots: thread_state.selected_roots().to_vec(),
|
||||
host: None,
|
||||
include_host_skills: false,
|
||||
include_bundled_skills: config.bundled_skills_enabled,
|
||||
include_remote_skills: false,
|
||||
})
|
||||
.list_skills(
|
||||
SkillListQuery {
|
||||
turn_id: thread_store.level_id().to_string(),
|
||||
executor_roots: thread_state.selected_roots().to_vec(),
|
||||
host: None,
|
||||
include_host_skills: false,
|
||||
include_bundled_skills: config.bundled_skills_enabled,
|
||||
include_orchestrator_skills: true,
|
||||
mcp_resources: session_store.get::<McpResourceClient>(),
|
||||
},
|
||||
&thread_state,
|
||||
)
|
||||
.await;
|
||||
for warning in &catalog.warnings {
|
||||
self.emit_warning(thread_store.level_id(), warning.clone());
|
||||
@@ -121,7 +126,7 @@ impl TurnInputContributor for SkillsExtension {
|
||||
fn contribute<'a>(
|
||||
&'a self,
|
||||
input: TurnInputContext,
|
||||
_session_store: &'a ExtensionData,
|
||||
session_store: &'a ExtensionData,
|
||||
thread_store: &'a ExtensionData,
|
||||
turn_store: &'a ExtensionData,
|
||||
) -> ExtensionFuture<'a, Vec<Box<dyn ContextualUserFragment + Send>>> {
|
||||
@@ -138,9 +143,10 @@ impl TurnInputContributor for SkillsExtension {
|
||||
host: host_loaded_skills.clone(),
|
||||
include_host_skills: true,
|
||||
include_bundled_skills: config.bundled_skills_enabled,
|
||||
include_remote_skills: true,
|
||||
include_orchestrator_skills: true,
|
||||
mcp_resources: session_store.get::<McpResourceClient>(),
|
||||
};
|
||||
let catalog = self.providers.list_for_turn(query).await;
|
||||
let catalog = self.list_skills(query, &thread_state).await;
|
||||
for warning in &catalog.warnings {
|
||||
self.emit_warning(&input.turn_id, warning.clone());
|
||||
}
|
||||
@@ -149,9 +155,10 @@ impl TurnInputContributor for SkillsExtension {
|
||||
let mut fragments: Vec<Box<dyn ContextualUserFragment + Send>> = Vec::new();
|
||||
if config.include_instructions {
|
||||
let mut turn_catalog = catalog.clone();
|
||||
turn_catalog
|
||||
.entries
|
||||
.retain(|entry| entry.authority.kind != SkillSourceKind::Executor);
|
||||
turn_catalog.entries.retain(|entry| {
|
||||
entry.authority.kind != SkillSourceKind::Executor
|
||||
&& entry.authority.kind != SkillSourceKind::Orchestrator
|
||||
});
|
||||
if let Some(fragment) = available_skills_fragment(&turn_catalog) {
|
||||
fragments.push(Box::new(fragment));
|
||||
}
|
||||
@@ -162,7 +169,7 @@ impl TurnInputContributor for SkillsExtension {
|
||||
let mut injected_host_skill_prompts = InjectedHostSkillPrompts::default();
|
||||
for entry in &selected_entries {
|
||||
match self
|
||||
.read_main_prompt(entry, host_loaded_skills.clone())
|
||||
.read_main_prompt(entry, host_loaded_skills.clone(), session_store)
|
||||
.await
|
||||
{
|
||||
Ok(read_result) => {
|
||||
@@ -232,10 +239,36 @@ impl TurnInputContributor for SkillsExtension {
|
||||
}
|
||||
|
||||
impl SkillsExtension {
|
||||
async fn list_skills(
|
||||
&self,
|
||||
mut query: SkillListQuery,
|
||||
thread_state: &SkillsThreadState,
|
||||
) -> SkillCatalog {
|
||||
let include_orchestrator_skills = query.include_orchestrator_skills;
|
||||
let orchestrator_query = query.clone();
|
||||
query.include_orchestrator_skills = false;
|
||||
|
||||
let mut catalog = self.providers.list_for_turn(query).await;
|
||||
if include_orchestrator_skills {
|
||||
match thread_state
|
||||
.orchestrator_catalog_snapshot(
|
||||
self.providers
|
||||
.list_orchestrator_for_turn(orchestrator_query),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(orchestrator_catalog) => catalog.extend(orchestrator_catalog),
|
||||
Err(err) => catalog.warnings.push(err.message),
|
||||
}
|
||||
}
|
||||
catalog
|
||||
}
|
||||
|
||||
async fn read_main_prompt(
|
||||
&self,
|
||||
entry: &SkillCatalogEntry,
|
||||
host_loaded_skills: Option<Arc<HostLoadedSkills>>,
|
||||
session_store: &ExtensionData,
|
||||
) -> Result<SkillReadResult, String> {
|
||||
self.providers
|
||||
.read(SkillReadRequest {
|
||||
@@ -243,6 +276,7 @@ impl SkillsExtension {
|
||||
package: entry.id.clone(),
|
||||
resource: entry.main_prompt.clone(),
|
||||
host: host_loaded_skills,
|
||||
mcp_resources: session_store.get::<McpResourceClient>(),
|
||||
})
|
||||
.await
|
||||
.map_err(|err| err.message)
|
||||
|
||||
Reference in new issue
Block a user