Integrate standard agent skills

This commit is contained in:
Georg Bauer
2026-08-29 21:12:40 +02:00
parent f2772f20c5
commit c0681dedb9
2 changed files with 244 additions and 1 deletions

View File

@@ -34,6 +34,108 @@ const SHELL_ENV_TIMEOUT: Duration = Duration::from_secs(5);
const SHELL_ENV_SENTINEL: &[u8] = b"\0DS4_ENV\0";
static USER_SHELL_ENVIRONMENT: OnceLock<Vec<(OsString, OsString)>> = OnceLock::new();
#[derive(Debug, Deserialize)]
struct AgentSkillMetadata {
name: String,
description: String,
}
struct AgentSkill {
name: String,
description: String,
path: PathBuf,
}
fn agent_skills_root(home: Option<&Path>) -> Option<PathBuf> {
home.map(|home| home.join(".agents/skills"))
}
fn valid_skill_name(name: &str) -> bool {
(1..=64).contains(&name.len())
&& !name.starts_with('-')
&& !name.ends_with('-')
&& !name.contains("--")
&& name
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
}
fn skill_frontmatter(content: &str) -> Option<&str> {
let content = content.strip_prefix("---")?;
let content = content
.strip_prefix('\n')
.or_else(|| content.strip_prefix("\r\n"))?;
let end = content
.match_indices("\n---")
.find(|(index, _)| {
content.get(index + 4..).is_some_and(|tail| {
tail.is_empty() || tail.starts_with('\n') || tail.starts_with("\r\n")
})
})?
.0;
Some(content[..end].trim_end_matches('\r'))
}
fn discover_agent_skills(root: &Path) -> Vec<AgentSkill> {
let Ok(root) = root.canonicalize() else {
return Vec::new();
};
let Ok(entries) = fs::read_dir(&root) else {
return Vec::new();
};
let mut skills = entries
.filter_map(Result::ok)
.filter_map(|entry| {
let directory_name = entry.file_name().into_string().ok()?;
let path = entry.path().join("SKILL.md").canonicalize().ok()?;
let content = fs::read_to_string(&path).ok()?;
let metadata =
serde_norway::from_str::<AgentSkillMetadata>(skill_frontmatter(&content)?).ok()?;
if metadata.name != directory_name
|| !valid_skill_name(&metadata.name)
|| metadata.description.trim().is_empty()
|| !(1..=1024).contains(&metadata.description.chars().count())
{
return None;
}
Some(AgentSkill {
name: metadata.name,
description: metadata.description,
path,
})
})
.collect::<Vec<_>>();
skills.sort_by(|left, right| left.name.cmp(&right.name));
skills
}
fn agent_skills_prompt_for(root: &Path) -> Option<String> {
let skills = discover_agent_skills(root);
if skills.is_empty() {
return None;
}
let catalog = skills
.iter()
.map(|skill| {
serde_json::json!({
"name": skill.name,
"description": skill.description,
"location": skill.path,
})
.to_string()
})
.collect::<Vec<_>>()
.join("\n");
Some(format!(
"# Available agent skills\n\nWhen a task matches a skill description, use the read tool to read its complete SKILL.md before proceeding. Resolve relative paths from the directory containing SKILL.md and read referenced resources only as needed.\n\n{catalog}"
))
}
pub(crate) fn agent_skills_prompt() -> Option<String> {
let root = agent_skills_root(std::env::var_os("HOME").as_deref().map(Path::new))?;
agent_skills_prompt_for(&root)
}
fn user_shell() -> OsString {
std::env::var_os("SHELL").unwrap_or_else(|| OsString::from("/bin/sh"))
}
@@ -1451,6 +1553,7 @@ impl Drop for RalphRoundDirectory {
pub(crate) struct Tools {
root: PathBuf,
agent_skill_roots: Vec<PathBuf>,
context_tokens: i32,
more: Option<(PathBuf, usize, bool)>,
more_text: Option<(String, usize)>,
@@ -1469,6 +1572,19 @@ impl Tools {
root: root
.canonicalize()
.map_err(|error| format!("Could not open the project directory: {error}"))?,
agent_skill_roots: agent_skills_root(
std::env::var_os("HOME").as_deref().map(Path::new),
)
.map(|root| {
let mut roots = discover_agent_skills(&root)
.into_iter()
.filter_map(|skill| skill.path.parent().map(Path::to_owned))
.collect::<Vec<_>>();
roots.sort();
roots.dedup();
roots
})
.unwrap_or_default(),
context_tokens,
more: None,
more_text: None,
@@ -1993,6 +2109,13 @@ impl Tools {
if path.starts_with(&self.root) {
return Ok(path);
}
if self
.agent_skill_roots
.iter()
.any(|root| path.starts_with(root))
{
return Ok(path);
}
if let Some(brain) = &self.dev_brain
&& let Ok(relative) = path.strip_prefix(brain.folder())
&& !relative
@@ -2002,7 +2125,7 @@ impl Tools {
return Ok(path);
}
Err(format!(
"path is outside the project and managed Dev Brain folder: {original}"
"path is outside the project, agent skills, and managed Dev Brain folder: {original}"
))
}
@@ -3453,6 +3576,117 @@ fn boolean(call: &ToolCall, name: &str, default: bool) -> bool {
mod tests {
use super::*;
#[test]
fn standard_agent_skills_are_discovered_prompted_and_read_only() {
let directory = std::env::temp_dir().join(format!(
"ds4-agent-skills-{}",
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let project = directory.join("project");
let skills_root = directory.join("home/.agents/skills");
fs::create_dir_all(&project).unwrap();
fs::create_dir_all(&skills_root).unwrap();
assert!(agent_skills_prompt_for(&skills_root).is_none());
let allium = skills_root.join("allium");
fs::create_dir(&allium).unwrap();
fs::write(
allium.join("SKILL.md"),
"---\r\nname: allium\r\ndescription: Tend Allium specifications\r\nlicense: MIT\r\n---\r\nSECRET FULL INSTRUCTIONS\r\n",
)
.unwrap();
fs::write(allium.join("reference.md"), "skill reference\n").unwrap();
let invalid = skills_root.join("wrong-directory");
fs::create_dir(&invalid).unwrap();
fs::write(
invalid.join("SKILL.md"),
"---\nname: different-name\ndescription: Invalid mismatch\n---\n",
)
.unwrap();
let linked_source = directory.join("linked-source");
fs::create_dir(&linked_source).unwrap();
fs::write(
linked_source.join("SKILL.md"),
"---\nname: linked\ndescription: Follow a linked skill\n---\n",
)
.unwrap();
fs::write(linked_source.join("reference.md"), "linked reference\n").unwrap();
std::os::unix::fs::symlink(&linked_source, skills_root.join("linked")).unwrap();
let skills = discover_agent_skills(&skills_root);
assert_eq!(skills.len(), 2);
assert_eq!(skills[0].name, "allium");
let prompt = agent_skills_prompt_for(&skills_root).unwrap();
assert!(prompt.contains("Tend Allium specifications"));
assert!(prompt.contains(allium.join("SKILL.md").to_str().unwrap()));
assert!(prompt.contains("read its complete SKILL.md"));
assert!(prompt.contains("Follow a linked skill"));
assert!(!prompt.contains("SECRET FULL INSTRUCTIONS"));
assert!(!prompt.contains("different-name"));
let mut tools = Tools::new(&project, 4096).unwrap();
tools.agent_skill_roots = skills
.iter()
.map(|skill| skill.path.parent().unwrap().to_owned())
.collect();
let cancel = AtomicBool::new(false);
let reference = allium.join("reference.md");
assert!(
tools
.execute(
&call("read", [("path", reference.to_str().unwrap())]),
&cancel,
)
.contains("skill reference")
);
assert!(
tools
.execute(
&call(
"read",
[("path", linked_source.join("reference.md").to_str().unwrap())],
),
&cancel,
)
.contains("linked reference")
);
let created = allium.join("created.md");
assert!(
tools
.execute(
&call(
"write",
[
("path", created.to_str().unwrap()),
("content", "must not be written"),
],
),
&cancel,
)
.contains("not a managed project or Dev Brain file")
);
assert!(!created.exists());
let outside = directory.join("outside.md");
fs::write(&outside, "outside\n").unwrap();
std::os::unix::fs::symlink(&outside, allium.join("escape.md")).unwrap();
assert!(
tools
.execute(
&call(
"read",
[("path", allium.join("escape.md").to_str().unwrap())],
),
&cancel,
)
.contains("outside the project, agent skills")
);
fs::remove_dir_all(directory).unwrap();
}
#[test]
fn prompts_and_parsers_expose_the_reference_tool_set() {
let prompt = system_prompt(ModelChoice::DeepSeekV4Flash, "extra", false);