Respect filesystem permissions during capability discovery (#36124)

## Why

Executor capability discovery can traverse plugin and skill roots, including
symlinks. Under restricted filesystem permissions, discovery must not expose
files outside the permitted paths.

## What changed

- Pass each environment's filesystem sandbox context through capability root
  discovery and apply it to metadata, directory walks, and file reads.
- Enable discovery for restricted sessions so permitted executor skills remain
  available while inaccessible roots and symlink targets are omitted.
- Key discovery caches by sandbox context and reject sandboxed discovery on
  executors that do not advertise support for it.
- Split requests with more than 128 roots into supported-size batches.

## Testing

- Cover permitted and denied external symlink targets.
- Verify restricted skill listing excludes inaccessible skills.
- Verify cache separation across permission contexts and discovery of 129 roots.

GitOrigin-RevId: 44d16468ca003403bdb8b71a04ae8c9ff94ed494
This commit is contained in:
jif authored and copyberry committed 2026-07-30 10:01:06 +00:00
1 parent 3d805abdf0
commit 5decb399ae
14 files changed
+510 -70

No files matched your search

+4 -1
View File
@@ -4,6 +4,7 @@ use std::sync::Arc;
use codex_core_skills::HostSkillsSnapshot;
use codex_core_skills::injection::HostSkillsCatalogInWorldState;
use codex_core_skills::injection::InjectedHostSkillPrompts;
use codex_exec_server::ExecutorCapabilityDiscoverySnapshot;
use codex_exec_server::FileSystemSandboxContext;
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
use codex_exec_server::ResolvedSelectedCapabilityRoot;
@@ -457,7 +458,9 @@ where
include_bundled_skills: false,
include_orchestrator_skills: false,
mcp_resources: None,
executor_capability_discovery: None,
executor_capability_discovery: step_store
.get::<ExecutorCapabilityDiscoverySnapshot>()
.map(|discovery| discovery.as_ref().clone()),
});
self.build_skill_tools(
session_store,
+15 -2
View File
@@ -4,6 +4,7 @@ use std::future::Future;
use std::sync::Arc;
use std::sync::Mutex;
use codex_exec_server::FileSystemSandboxContext;
use codex_extension_api::ExtensionMetrics;
use codex_mcp::McpResourceClient;
use codex_mcp::McpResourceClientCacheKey;
@@ -138,12 +139,19 @@ impl SkillsThreadState {
providers: &SkillProviders,
query: SkillListQuery,
) -> SkillCatalog {
let sandbox_contexts = query
.executor_capability_discovery
.as_ref()
.map(|discovery| discovery.sandbox_contexts().clone())
.unwrap_or_default();
if let Some(cached) = self
.executor_discovery_cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_ref()
.filter(|cached| cached.roots == query.executor_roots)
.filter(|cached| {
cached.roots == query.executor_roots && cached.sandbox_contexts == sandbox_contexts
})
{
return cached.catalog.clone();
}
@@ -153,11 +161,15 @@ impl SkillsThreadState {
.executor_discovery_cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(cached) = cache.as_ref().filter(|cached| cached.roots == roots) {
if let Some(cached) = cache
.as_ref()
.filter(|cached| cached.roots == roots && cached.sandbox_contexts == sandbox_contexts)
{
return cached.catalog.clone();
}
*cache = Some(CachedExecutorDiscoveryCatalog {
roots,
sandbox_contexts,
catalog: discovered.clone(),
});
discovered
@@ -282,6 +294,7 @@ struct CachedExecutorCatalog {
struct CachedExecutorDiscoveryCatalog {
roots: Vec<SelectedCapabilityRoot>,
sandbox_contexts: HashMap<String, FileSystemSandboxContext>,
catalog: SkillCatalog,
}