Skip to content

Commit b31f5c2

Browse files
committed
changes
1 parent 776b091 commit b31f5c2

File tree

3 files changed

+49
-173
lines changed

3 files changed

+49
-173
lines changed
Lines changed: 35 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,32 @@
1-
use std::collections::HashSet;
21
use std::path::Component;
32
use std::path::Path;
43
use std::path::PathBuf;
54

65
use codex_protocol::protocol::SandboxPolicy;
7-
use dirs::home_dir;
86
use dunce::canonicalize as canonicalize_path;
97

10-
use crate::bash::parse_shell_lc_plain_commands;
11-
use crate::bash::parse_shell_lc_single_command_prefix;
128
use crate::config::Permissions;
139
use crate::path_utils::normalize_for_path_comparison;
1410
use crate::skills::SkillLoadOutcome;
15-
use crate::skills::SkillMetadata;
1611

12+
/// Resolves the sandbox policy extension contributed by the first matching
13+
/// skill for a command invocation.
14+
///
15+
/// Assumptions:
16+
/// 1. `command_cwd` reflects the effective command target location.
17+
/// 2. If `command_cwd` is contained by multiple skill directories, the first
18+
/// enabled skill in `skills_outcome.skills` wins.
19+
/// 3. Command tokens are not used for matching.
20+
///
21+
/// Returns `None` when no enabled skill with permissions matches
22+
/// `command_cwd`.
1723
pub(crate) fn resolve_skill_sandbox_extension_for_command(
1824
skills_outcome: &SkillLoadOutcome,
19-
command: &[String],
2025
command_cwd: &Path,
2126
) -> Option<SandboxPolicy> {
22-
let segments = command_segments_for_matching(command);
23-
let mut best_match: Option<(usize, SandboxPolicy)> = None;
24-
25-
for segment in segments {
26-
for candidate in candidate_paths_for_segment(&segment, command_cwd) {
27-
let Some((depth, _, permissions)) =
28-
match_skill_for_candidate(skills_outcome, candidate.as_path())
29-
else {
30-
continue;
31-
};
32-
33-
let should_replace = match &best_match {
34-
Some((best_depth, _)) => depth > *best_depth,
35-
None => true,
36-
};
37-
if should_replace {
38-
best_match = Some((depth, permissions.sandbox_policy.get().clone()));
39-
}
40-
}
41-
}
42-
43-
best_match.map(|(_, sandbox_policy)| sandbox_policy)
44-
}
45-
46-
fn command_segments_for_matching(command: &[String]) -> Vec<Vec<String>> {
47-
if let Some(commands) = parse_shell_lc_plain_commands(command)
48-
&& !commands.is_empty()
49-
{
50-
return commands;
51-
}
52-
53-
if let Some(command) = parse_shell_lc_single_command_prefix(command) {
54-
return vec![command];
55-
}
56-
57-
vec![command.to_vec()]
58-
}
59-
60-
fn candidate_paths_for_segment(segment: &[String], command_cwd: &Path) -> Vec<PathBuf> {
61-
let mut candidates = Vec::new();
62-
let mut seen = HashSet::new();
63-
64-
if let Some(path) = normalize_candidate_path(command_cwd)
65-
&& seen.insert(path.clone())
66-
{
67-
candidates.push(path);
68-
}
69-
70-
for token in segment {
71-
let Some(path) = candidate_path_from_token(token, command_cwd) else {
72-
continue;
73-
};
74-
if seen.insert(path.clone()) {
75-
candidates.push(path);
76-
}
77-
}
78-
79-
candidates
80-
}
81-
82-
fn candidate_path_from_token(token: &str, command_cwd: &Path) -> Option<PathBuf> {
83-
if token.is_empty() || token.contains("://") || token.starts_with('-') {
84-
return None;
85-
}
86-
87-
let is_path_like = token == "~"
88-
|| token.starts_with("~/")
89-
|| token.starts_with("./")
90-
|| token.starts_with("../")
91-
|| token.contains('/')
92-
|| token.contains('\\')
93-
|| Path::new(token).is_absolute()
94-
|| command_cwd.join(token).exists();
95-
if !is_path_like {
96-
return None;
97-
}
98-
99-
let expanded = expand_home(token);
100-
let path = PathBuf::from(expanded);
101-
let absolute = if path.is_absolute() {
102-
path
103-
} else {
104-
command_cwd.join(path)
105-
};
106-
normalize_candidate_path(&absolute)
27+
let candidate = normalize_candidate_path(command_cwd)?;
28+
let permissions = match_skill_for_candidate(skills_outcome, candidate.as_path())?;
29+
Some(permissions.sandbox_policy.get().clone())
10730
}
10831

10932
fn normalize_candidate_path(path: &Path) -> Option<PathBuf> {
@@ -121,9 +44,7 @@ fn normalize_candidate_path(path: &Path) -> Option<PathBuf> {
12144
fn match_skill_for_candidate<'a>(
12245
skills_outcome: &'a SkillLoadOutcome,
12346
candidate: &Path,
124-
) -> Option<(usize, &'a SkillMetadata, &'a Permissions)> {
125-
let mut best_match: Option<(usize, &SkillMetadata, &Permissions)> = None;
126-
47+
) -> Option<&'a Permissions> {
12748
for skill in &skills_outcome.skills {
12849
if skills_outcome.disabled_paths.contains(&skill.path) {
12950
continue;
@@ -140,33 +61,10 @@ fn match_skill_for_candidate<'a>(
14061
if !candidate.starts_with(&skill_dir) {
14162
continue;
14263
}
143-
144-
let depth = skill_dir.components().count();
145-
let should_replace = match &best_match {
146-
Some((best_depth, _, _)) => depth > *best_depth,
147-
None => true,
148-
};
149-
if should_replace {
150-
best_match = Some((depth, skill, permissions));
151-
}
64+
return Some(permissions);
15265
}
15366

154-
best_match
155-
}
156-
157-
fn expand_home(path: &str) -> String {
158-
if path == "~" {
159-
if let Some(home) = home_dir() {
160-
return home.to_string_lossy().to_string();
161-
}
162-
return path.to_string();
163-
}
164-
if let Some(rest) = path.strip_prefix("~/")
165-
&& let Some(home) = home_dir()
166-
{
167-
return home.join(rest).to_string_lossy().to_string();
168-
}
169-
path.to_string()
67+
None
17068
}
17169

17270
fn normalize_lexically(path: &Path) -> PathBuf {
@@ -237,13 +135,11 @@ mod tests {
237135
}
238136

239137
#[test]
240-
fn resolves_skill_policy_for_executable_inside_skill_directory() {
138+
fn resolves_skill_policy_when_cwd_is_inside_skill_directory() {
241139
let tempdir = tempfile::tempdir().expect("tempdir");
242140
let skill_dir = tempdir.path().join("skills/demo");
243141
let scripts_dir = skill_dir.join("scripts");
244142
std::fs::create_dir_all(&scripts_dir).expect("create scripts");
245-
let executable = scripts_dir.join("run.sh");
246-
std::fs::write(&executable, "#!/bin/sh\necho ok\n").expect("write script");
247143
let skill_path = skill_dir.join("SKILL.md");
248144
std::fs::write(&skill_path, "skill").expect("write SKILL.md");
249145

@@ -260,19 +156,17 @@ mod tests {
260156
skill_policy.clone(),
261157
)]);
262158

263-
let resolved = resolve_skill_sandbox_extension_for_command(
264-
&outcome,
265-
&[canonical(&executable).to_string_lossy().to_string()],
266-
tempdir.path(),
267-
);
159+
let resolved = resolve_skill_sandbox_extension_for_command(&outcome, &scripts_dir);
268160

269161
assert_eq!(resolved, Some(skill_policy));
270162
}
271163

272164
#[test]
273-
fn resolves_skill_policy_for_shell_wrapped_relative_script_command() {
165+
fn does_not_resolve_policy_when_only_command_path_matches() {
274166
let tempdir = tempfile::tempdir().expect("tempdir");
275167
let skill_dir = tempdir.path().join("skills/demo");
168+
let outside_dir = tempdir.path().join("outside");
169+
std::fs::create_dir_all(&outside_dir).expect("create outside");
276170
let scripts_dir = skill_dir.join("scripts");
277171
std::fs::create_dir_all(&scripts_dir).expect("create scripts");
278172
std::fs::write(scripts_dir.join("run.sh"), "#!/bin/sh\necho ok\n").expect("write script");
@@ -292,26 +186,17 @@ mod tests {
292186
skill_policy.clone(),
293187
)]);
294188

295-
let resolved = resolve_skill_sandbox_extension_for_command(
296-
&outcome,
297-
&[
298-
"bash".to_string(),
299-
"-lc".to_string(),
300-
"./scripts/run.sh --flag".to_string(),
301-
],
302-
&skill_dir,
303-
);
189+
let resolved = resolve_skill_sandbox_extension_for_command(&outcome, &outside_dir);
304190

305-
assert_eq!(resolved, Some(skill_policy));
191+
assert_eq!(resolved, None);
306192
}
307193

308194
#[test]
309195
fn ignores_disabled_skill_when_resolving_command_policy() {
310196
let tempdir = tempfile::tempdir().expect("tempdir");
311197
let skill_dir = tempdir.path().join("skills/demo");
312198
std::fs::create_dir_all(&skill_dir).expect("create skill dir");
313-
let executable = skill_dir.join("tool.sh");
314-
std::fs::write(&executable, "#!/bin/sh\necho ok\n").expect("write script");
199+
std::fs::write(skill_dir.join("tool.sh"), "#!/bin/sh\necho ok\n").expect("write script");
315200
let skill_path = skill_dir.join("SKILL.md");
316201
std::fs::write(&skill_path, "skill").expect("write SKILL.md");
317202
let skill_path = canonical(&skill_path);
@@ -322,24 +207,23 @@ mod tests {
322207
)]);
323208
outcome.disabled_paths.insert(skill_path);
324209

325-
let resolved = resolve_skill_sandbox_extension_for_command(
326-
&outcome,
327-
&[canonical(&executable).to_string_lossy().to_string()],
328-
tempdir.path(),
329-
);
210+
let resolved = resolve_skill_sandbox_extension_for_command(&outcome, &skill_dir);
330211

331212
assert_eq!(resolved, None);
332213
}
333214

334215
#[test]
335-
fn prefers_most_specific_skill_directory_for_nested_match() {
216+
fn resolves_first_matching_skill_directory_for_nested_match() {
336217
let tempdir = tempfile::tempdir().expect("tempdir");
337218
let parent_skill_dir = tempdir.path().join("skills/parent");
338219
let nested_skill_dir = parent_skill_dir.join("nested");
339220
std::fs::create_dir_all(nested_skill_dir.join("scripts")).expect("create scripts");
340221

341-
let executable = nested_skill_dir.join("scripts/run.sh");
342-
std::fs::write(&executable, "#!/bin/sh\necho ok\n").expect("write script");
222+
std::fs::write(
223+
nested_skill_dir.join("scripts/run.sh"),
224+
"#!/bin/sh\necho ok\n",
225+
)
226+
.expect("write script");
343227

344228
let parent_skill_path = parent_skill_dir.join("SKILL.md");
345229
let nested_skill_path = nested_skill_dir.join("SKILL.md");
@@ -364,16 +248,12 @@ mod tests {
364248
exclude_slash_tmp: false,
365249
};
366250
let outcome = outcome_with_skills(vec![
367-
skill_with_policy(canonical(&parent_skill_path), parent_policy),
251+
skill_with_policy(canonical(&parent_skill_path), parent_policy.clone()),
368252
skill_with_policy(canonical(&nested_skill_path), nested_policy.clone()),
369253
]);
370254

371-
let resolved = resolve_skill_sandbox_extension_for_command(
372-
&outcome,
373-
&[canonical(&executable).to_string_lossy().to_string()],
374-
tempdir.path(),
375-
);
255+
let resolved = resolve_skill_sandbox_extension_for_command(&outcome, &nested_skill_dir);
376256

377-
assert_eq!(resolved, Some(nested_policy));
257+
assert_eq!(resolved, Some(parent_policy));
378258
}
379259
}

codex-rs/core/src/tools/handlers/shell.rs

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -304,17 +304,14 @@ impl ShellHandler {
304304
.skills_manager
305305
.skills_for_cwd(&turn.cwd, false)
306306
.await;
307-
let effective_sandbox_policy = resolve_skill_sandbox_extension_for_command(
308-
&skills_outcome,
309-
&exec_params.command,
310-
&exec_params.cwd,
311-
)
312-
.map_or_else(
313-
|| turn.sandbox_policy.clone(),
314-
|skill_sandbox_policy| {
315-
extend_sandbox_policy(&turn.sandbox_policy, &skill_sandbox_policy)
316-
},
317-
);
307+
let effective_sandbox_policy =
308+
resolve_skill_sandbox_extension_for_command(&skills_outcome, &exec_params.cwd)
309+
.map_or_else(
310+
|| turn.sandbox_policy.clone(),
311+
|skill_sandbox_policy| {
312+
extend_sandbox_policy(&turn.sandbox_policy, &skill_sandbox_policy)
313+
},
314+
);
318315

319316
let exec_approval_requirement = session
320317
.services

codex-rs/core/src/unified_exec/process_manager.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -615,13 +615,12 @@ impl UnifiedExecProcessManager {
615615
.skills_for_cwd(&context.turn.cwd, false)
616616
.await;
617617
let effective_sandbox_policy =
618-
resolve_skill_sandbox_extension_for_command(&skills_outcome, &request.command, &cwd)
619-
.map_or_else(
620-
|| context.turn.sandbox_policy.clone(),
621-
|skill_sandbox_policy| {
622-
extend_sandbox_policy(&context.turn.sandbox_policy, &skill_sandbox_policy)
623-
},
624-
);
618+
resolve_skill_sandbox_extension_for_command(&skills_outcome, &cwd).map_or_else(
619+
|| context.turn.sandbox_policy.clone(),
620+
|skill_sandbox_policy| {
621+
extend_sandbox_policy(&context.turn.sandbox_policy, &skill_sandbox_policy)
622+
},
623+
);
625624
let exec_approval_requirement = context
626625
.session
627626
.services

0 commit comments

Comments
 (0)