From d0923bb2c403e2f9344ee336d603329d27441a3d Mon Sep 17 00:00:00 2001 From: celia-oai Date: Fri, 20 Feb 2026 14:29:46 -0800 Subject: [PATCH] changes --- codex-rs/core/src/codex.rs | 30 ++++++++++ codex-rs/core/src/skills/permissions.rs | 77 +++++++++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 46a2087f27..e881aebaf3 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -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, pub(crate) dynamic_tools: Vec, pub(crate) turn_metadata_state: Arc, + pub(crate) skill_prefix_permissions: std::sync::RwLock, Permissions>>, } impl TurnContext { pub(crate) fn model_context_window(&self) -> Option { @@ -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, 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) -> 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 diff --git a/codex-rs/core/src/skills/permissions.rs b/codex-rs/core/src/skills/permissions.rs index 1cf18e1052..48b6411f5a 100644 --- a/codex-rs/core/src/skills/permissions.rs +++ b/codex-rs/core/src/skills/permissions.rs @@ -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, 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 { + 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,