This commit is contained in:
celia-oai
2026-02-20 14:29:46 -08:00
parent 1f54496c48
commit d0923bb2c4
2 changed files with 107 additions and 0 deletions

View File

@@ -120,6 +120,7 @@ use crate::config::Config;
use crate::config::Constrained;
use crate::config::ConstraintResult;
use crate::config::GhostSnapshotConfig;
use crate::config::Permissions;
use crate::config::StartedNetworkProxy;
use crate::config::resolve_web_search_mode_for_turn;
use crate::config::types::McpServerConfig;
@@ -216,6 +217,7 @@ use crate::skills::collect_explicit_skill_mentions;
use crate::skills::injection::ToolMentionKind;
use crate::skills::injection::app_id_from_path;
use crate::skills::injection::tool_kind_for_path;
use crate::skills::permissions::build_skill_script_prefix_permissions;
use crate::skills::resolve_skill_dependencies_for_turn;
use crate::state::ActiveTurn;
use crate::state::SessionServices;
@@ -564,6 +566,7 @@ pub(crate) struct TurnContext {
pub(crate) js_repl: Arc<JsReplHandle>,
pub(crate) dynamic_tools: Vec<DynamicToolSpec>,
pub(crate) turn_metadata_state: Arc<TurnMetadataState>,
pub(crate) skill_prefix_permissions: std::sync::RwLock<HashMap<Vec<String>, Permissions>>,
}
impl TurnContext {
pub(crate) fn model_context_window(&self) -> Option<i64> {
@@ -603,6 +606,11 @@ impl TurnContext {
let collaboration_mode =
self.collaboration_mode
.with_updates(Some(model.clone()), Some(reasoning_effort), None);
let skill_prefix_permissions = self
.skill_prefix_permissions
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
let features = self.features.clone();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
@@ -645,9 +653,21 @@ impl TurnContext {
js_repl: Arc::clone(&self.js_repl),
dynamic_tools: self.dynamic_tools.clone(),
turn_metadata_state: self.turn_metadata_state.clone(),
skill_prefix_permissions: std::sync::RwLock::new(skill_prefix_permissions),
}
}
pub(crate) fn set_skill_prefix_permissions(
&self,
skill_prefix_permissions: HashMap<Vec<String>, Permissions>,
) {
let mut registry = self
.skill_prefix_permissions
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*registry = skill_prefix_permissions;
}
pub(crate) fn resolve_path(&self, path: Option<String>) -> PathBuf {
path.as_ref()
.map(PathBuf::from)
@@ -987,6 +1007,7 @@ impl Session {
js_repl,
dynamic_tools: session_configuration.dynamic_tools.clone(),
turn_metadata_state,
skill_prefix_permissions: std::sync::RwLock::new(HashMap::new()),
}
}
@@ -4177,6 +4198,13 @@ async fn spawn_review_thread(
dynamic_tools: parent_turn_context.dynamic_tools.clone(),
truncation_policy: model_info.truncation_policy.into(),
turn_metadata_state,
skill_prefix_permissions: std::sync::RwLock::new(
parent_turn_context
.skill_prefix_permissions
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone(),
),
};
// Seed the child task with the review prompt as the initial user message.
@@ -4335,6 +4363,8 @@ pub(crate) async fn run_turn(
&connector_slug_counts,
)
});
turn_context
.set_skill_prefix_permissions(build_skill_script_prefix_permissions(&mentioned_skills));
let config = turn_context.config.clone();
if config
.features

View File

@@ -1,4 +1,6 @@
use std::collections::HashMap;
use std::collections::HashSet;
use std::io::ErrorKind;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
@@ -17,6 +19,7 @@ use crate::protocol::ReadOnlyAccess;
use crate::protocol::SandboxPolicy;
#[cfg(target_os = "macos")]
use crate::seatbelt_permissions::MacOsSeatbeltProfileExtensions;
use crate::skills::SkillMetadata;
#[cfg(not(target_os = "macos"))]
type MacOsSeatbeltProfileExtensions = ();
@@ -118,6 +121,31 @@ pub(crate) fn compile_permission_profile(
})
}
pub(crate) fn build_skill_script_prefix_permissions(
skills: &[SkillMetadata],
) -> HashMap<Vec<String>, Permissions> {
let mut entries = HashMap::new();
for skill in skills {
let Some(skill_permissions) = skill.permissions.clone() else {
continue;
};
let Some(skill_dir) = skill.path.parent() else {
warn!(
"ignoring skill script prefix permissions for {}: SKILL.md has no parent directory",
skill.path.display()
);
continue;
};
for script_path in normalized_skill_script_paths(skill_dir) {
entries.insert(vec![script_path], skill_permissions.clone());
}
}
entries
}
fn normalize_permission_paths(
skill_dir: &Path,
values: &[String],
@@ -138,6 +166,55 @@ fn normalize_permission_paths(
paths
}
/// Returns unique script file paths for a skill's top-level `scripts/` directory.
///
/// The returned paths are normalized lexically, canonicalized when possible, and
/// rendered as strings. Non-file entries are ignored. If the directory does not
/// exist (or can't be read), an empty list is returned after logging a warning
/// for non-`NotFound` errors.
fn normalized_skill_script_paths(skill_dir: &Path) -> Vec<String> {
let scripts_dir = skill_dir.join("scripts");
let entries = match std::fs::read_dir(&scripts_dir) {
Ok(entries) => entries,
Err(err) if err.kind() == ErrorKind::NotFound => return Vec::new(),
Err(err) => {
warn!(
"ignoring skill scripts directory {}: {err}",
scripts_dir.display()
);
return Vec::new();
}
};
let mut normalized_paths = Vec::new();
let mut seen = HashSet::new();
for entry in entries {
let entry = match entry {
Ok(entry) => entry,
Err(err) => {
warn!(
"ignoring entry in skill scripts directory {}: {err}",
scripts_dir.display()
);
continue;
}
};
let path = entry.path();
let is_file = entry.file_type().is_ok_and(|file_type| file_type.is_file());
if !is_file {
continue;
}
let normalized = normalize_lexically(&path);
let canonicalized = canonicalize_path(&normalized).unwrap_or(normalized);
let rendered = canonicalized.to_string_lossy().to_string();
if seen.insert(rendered.clone()) {
normalized_paths.push(rendered);
}
}
normalized_paths
}
fn normalize_permission_path(
skill_dir: &Path,
value: &str,