diff --git a/codex-rs/app-server-test-client/src/lib.rs b/codex-rs/app-server-test-client/src/lib.rs index 552513ab27..89fe0cb8cb 100644 --- a/codex-rs/app-server-test-client/src/lib.rs +++ b/codex-rs/app-server-test-client/src/lib.rs @@ -975,7 +975,11 @@ fn parse_skill_selection( ) -> Result> { match (skill_name, skill_path) { (None, None) => Ok(None), - (Some(name), Some(path)) => Ok(Some(SkillSelection { name, path })), + (Some(name), Some(path)) => { + let path = fs::canonicalize(&path) + .with_context(|| format!("canonicalize --skill-path {}", path.display()))?; + Ok(Some(SkillSelection { name, path })) + } (Some(_), None) => bail!("--skill-name requires --skill-path"), (None, Some(_)) => bail!("--skill-path requires --skill-name"), } diff --git a/codex-rs/core/src/zsh_exec_bridge/mod.rs b/codex-rs/core/src/zsh_exec_bridge/mod.rs index 24f84d4412..fce3d095e3 100644 --- a/codex-rs/core/src/zsh_exec_bridge/mod.rs +++ b/codex-rs/core/src/zsh_exec_bridge/mod.rs @@ -4,6 +4,8 @@ use std::path::PathBuf; use tokio::sync::Mutex; use uuid::Uuid; +#[cfg(unix)] +use crate::bash::parse_shell_lc_plain_commands; #[cfg(unix)] use crate::error::CodexErr; #[cfg(unix)] @@ -369,6 +371,11 @@ impl ZshExecBridge { argv.clone() }; let normalized_exec_program = normalize_wrapper_executable_for_skill_matching(&file, &cwd); + let normalized_wrapped_exec_program = + normalize_wrapped_shell_inner_executable_for_skill_matching( + &command_for_approval, + &cwd, + ); // Check exec policy against the resolved executable path, not the original argv[0]. let mut command_for_execpolicy = command_for_approval.clone(); @@ -384,7 +391,15 @@ impl ZshExecBridge { .unwrap_or_else(std::sync::PoisonError::into_inner); let matched_skill_permissions = registry .get(std::slice::from_ref(&normalized_exec_program)) - .cloned(); + .cloned() + .or_else(|| { + // Wrapper requests often execute `zsh -c/-lc `, so the + // wrapper executable (`zsh`) won't match skill script prefixes. Fall back + // to the parsed inner executable path to match skill permissions. + normalized_wrapped_exec_program + .as_ref() + .and_then(|program| registry.get(std::slice::from_ref(program)).cloned()) + }); let overlay_prompt_prefixes = registry.keys().cloned().collect::>(); (overlay_prompt_prefixes, matched_skill_permissions) }; @@ -640,6 +655,19 @@ fn normalize_wrapper_executable_for_skill_matching(file: &str, cwd: &str) -> Str canonicalized.to_string_lossy().to_string() } +#[cfg(unix)] +fn normalize_wrapped_shell_inner_executable_for_skill_matching( + command: &[String], + cwd: &str, +) -> Option { + let commands = parse_shell_lc_plain_commands(command)?; + let first_command = commands.first()?; + let program = first_command.first()?; + Some(normalize_wrapper_executable_for_skill_matching( + program, cwd, + )) +} + #[cfg(unix)] fn normalize_lexically(path: &Path) -> PathBuf { let mut normalized = PathBuf::new(); @@ -724,4 +752,39 @@ mod tests { let normalized = normalize_wrapper_executable_for_skill_matching("bash", "/tmp"); assert_eq!(normalized, "bash"); } + + #[test] + fn normalize_wrapped_shell_inner_executable_for_skill_matching_resolves_relative_script() { + let tempdir = tempfile::tempdir().expect("tempdir"); + let repo_root = tempdir.path().join("repo"); + let cwd = repo_root.join("codex-rs"); + let skill_script = + repo_root.join(".agents/skills/sandbox-approval-demo/scripts/skill_action.sh"); + fs::create_dir_all( + skill_script + .parent() + .expect("skill script should have a parent directory"), + ) + .expect("create skill scripts dir"); + fs::create_dir_all(&cwd).expect("create cwd"); + fs::write(&skill_script, "#!/bin/sh\n").expect("write skill script"); + + let command = vec![ + "/bin/zsh".to_string(), + "-c".to_string(), + "../.agents/skills/sandbox-approval-demo/scripts/./skill_action.sh".to_string(), + ]; + + let normalized = normalize_wrapped_shell_inner_executable_for_skill_matching( + &command, + &cwd.to_string_lossy(), + ) + .expect("should parse wrapped shell command"); + + let expected = dunce::canonicalize(&skill_script) + .expect("canonicalize skill script") + .to_string_lossy() + .to_string(); + assert_eq!(normalized, expected); + } }