Preserve filesystem authority when loading skills (#59279)

## Why

Skill discovery and metadata loading used raw filesystem handles with unrestricted reads, and cache keys distinguished filesystems without accounting for captured permissions.

## What changed

- Pass borrowed `EnvironmentAccess` accessors through skill root resolution, discovery, canonicalization, and metadata reads.
- Include captured permissions in skill snapshot and empty remote root cache keys.
- Add fixed-path watches to `EnvironmentAccess`, falling back to live discovery when read restrictions prevent using unsandboxed executor watches.
- Keep user and plugin roots under host filesystem authority independently of the selected repository executor.

## Testing

Extend remote root cache coverage to verify that changing captured permissions triggers a fresh scan and that restricted accessors decline unsandboxed watches.

GitOrigin-RevId: 97d4bfdfcba78a93fc8845773c6397d4364494e3
This commit is contained in:
Dmitriy Kovalenko
2026-11-04 01:13:54 +00:00
committed by copyberry
parent afefd0ba95
commit 526f6099f6
25 changed files with 403 additions and 273 deletions
@@ -27,11 +27,21 @@ use std::sync::Weak;
/// Filesystem operations authorized by the environment that supplied this borrowed accessor.
///
/// Callers cannot extract the underlying filesystem or choose another sandbox. They may retain
/// returned data, cache keys, and already-opened read streams after the accessor is gone.
/// returned data, cache keys, opened read streams, and fixed-path watches after the accessor is gone.
pub trait EnvironmentAccess: Send + Sync {
/// Identifies the filesystem and captured permissions without granting access to either.
fn cache_key(&self) -> EnvironmentAccessKey;
/// Registers an owned watch for these fixed paths, or returns `None` when unsupported.
/// Implementations must preserve captured read permissions when checking the observation.
fn watch_missing(
&self,
_missing: Vec<PathUri>,
_present: Option<PathUri>,
) -> ExecutorFileSystemFuture<'static, Option<tokio::sync::oneshot::Receiver<()>>> {
Box::pin(async { Ok(None) })
}
fn canonicalize<'a>(&'a self, path: &'a PathUri) -> ExecutorFileSystemFuture<'a, PathUri>;
fn read_file<'a>(
@@ -201,6 +211,26 @@ impl<'environment> FileSystemEnvironmentAccessor<'environment> {
}
impl EnvironmentAccess for FileSystemEnvironmentAccessor<'_> {
fn watch_missing(
&self,
missing: Vec<PathUri>,
present: Option<PathUri>,
) -> ExecutorFileSystemFuture<'static, Option<tokio::sync::oneshot::Receiver<()>>> {
// Executor watches make unsandboxed observations, so restricted reads use live discovery.
if self
.key
.sandbox
.as_ref()
.is_some_and(|sandbox| sandbox.should_read_from_sandbox())
{
return Box::pin(async { Ok(None) });
}
let Some(file_system) = self.key.file_system.upgrade() else {
return Box::pin(async { Ok(None) });
};
Box::pin(async move { file_system.watch_missing(missing, present).await })
}
fn cache_key(&self) -> EnvironmentAccessKey {
self.key.clone()
}