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
2026-06-11 10:28:16 +01:00
committed by GitHub
Unverified
parent 53b5019745
commit a287c5dffd
26 changed files with 877 additions and 116 deletions
+1
View File
@@ -65,6 +65,7 @@ use codex_login::CodexAuth;
use codex_login::auth_env_telemetry::collect_auth_env_telemetry;
use codex_login::default_client::originator;
use codex_mcp::McpConnectionManager;
use codex_mcp::McpResourceClient;
use codex_mcp::McpRuntimeContext;
use codex_mcp::codex_apps_tools_cache_key;
use codex_models_manager::manager::RefreshStrategy;
+13 -7
View File
@@ -946,8 +946,20 @@ impl Session {
.effective_agent_max_threads(MultiAgentVersion::V2)
.unwrap_or(usize::MAX),
);
// Keep one stable manager handle for the session so extension resource clients
// automatically observe the manager installed at startup and on later refreshes.
let mcp_connection_manager = Arc::new(arc_swap::ArcSwap::from_pointee(
McpConnectionManager::new_uninitialized_with_permission_profile(
&config.permissions.approval_policy,
config.permissions.permission_profile(),
config.prefix_mcp_tool_names(),
),
));
let session_extension_data =
codex_extension_api::ExtensionData::new(session_id.to_string());
session_extension_data.insert(McpResourceClient::new(Arc::clone(
&mcp_connection_manager,
)));
let thread_extension_data = codex_extension_api::ExtensionData::new_with_init(
thread_id.to_string(),
thread_extension_init,
@@ -970,13 +982,7 @@ impl Session {
// before any MCP-related events. It is reasonable to consider
// changing this to use Option or OnceCell, though the current
// setup is straightforward enough and performs well.
mcp_connection_manager: arc_swap::ArcSwap::from_pointee(
McpConnectionManager::new_uninitialized_with_permission_profile(
&config.permissions.approval_policy,
config.permissions.permission_profile(),
config.prefix_mcp_tool_names(),
),
),
mcp_connection_manager,
mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()),
unified_exec_manager: UnifiedExecProcessManager::new(
config.background_terminal_max_timeout,
+4 -4
View File
@@ -4940,13 +4940,13 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
);
let services = SessionServices {
mcp_connection_manager: arc_swap::ArcSwap::from_pointee(
mcp_connection_manager: Arc::new(arc_swap::ArcSwap::from_pointee(
McpConnectionManager::new_uninitialized_with_permission_profile(
&config.permissions.approval_policy,
config.permissions.permission_profile(),
config.prefix_mcp_tool_names(),
),
),
)),
mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()),
unified_exec_manager: UnifiedExecProcessManager::new(
config.background_terminal_max_timeout,
@@ -7018,13 +7018,13 @@ where
);
let services = SessionServices {
mcp_connection_manager: arc_swap::ArcSwap::from_pointee(
mcp_connection_manager: Arc::new(arc_swap::ArcSwap::from_pointee(
McpConnectionManager::new_uninitialized_with_permission_profile(
&config.permissions.approval_policy,
config.permissions.permission_profile(),
config.prefix_mcp_tool_names(),
),
),
)),
mcp_startup_cancellation_token: Mutex::new(CancellationToken::new()),
unified_exec_manager: UnifiedExecProcessManager::new(
config.background_terminal_max_timeout,