This commit is contained in:
celia-oai
2026-02-20 17:58:35 -08:00
parent 7c0af91bd2
commit 8f6050aafe
2 changed files with 69 additions and 2 deletions

View File

@@ -975,7 +975,11 @@ fn parse_skill_selection(
) -> Result<Option<SkillSelection>> {
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"),
}

View File

@@ -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 <skill-script>`, 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::<Vec<_>>();
(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<String> {
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);
}
}