mirror of
https://github.com/openai/codex.git
synced 2026-11-13 22:47:20 +00:00
Load skill interface metadata in the host loader (#36903)
## What changed - Move skill interface validation and asset-path resolution into `codex-skills` so both skill loaders share the same behavior. - Populate host-loaded skills with validated display metadata, prompts, brand colors, and local icons from `agents/openai.yaml`. - Allow plugin skills to reference icons under the plugin's shared `assets` directory while rejecting absolute paths and paths that escape permitted asset roots. ## Testing - Add resolver tests for valid, invalid, local, and plugin-shared interface assets. - Add host loader coverage for interface fields and asset-path restrictions. GitOrigin-RevId: 3f316191752eeebd15d11ac0bee82201574320ec
This commit is contained in:
1 parent
f93109615f
commit
49b0aebd6f
10 files changed
+634
-456
No files matched your search
@@ -847,198 +847,6 @@ policy:
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn accepts_icon_paths_under_assets_dir() {
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
let skill_path = write_skill(&codex_home, "demo", "ui-skill", "from json");
|
||||
let skill_dir = skill_path.parent().expect("skill dir");
|
||||
let normalized_skill_dir = normalized(skill_dir);
|
||||
|
||||
write_skill_interface_at(
|
||||
skill_dir,
|
||||
r#"
|
||||
{
|
||||
"interface": {
|
||||
"display_name": "UI Skill",
|
||||
"icon_small": "assets/icon.png",
|
||||
"icon_large": "./assets/logo.svg"
|
||||
}
|
||||
}
|
||||
"#,
|
||||
);
|
||||
|
||||
let cfg = make_config(&codex_home).await;
|
||||
let outcome = load_skills_for_test(&cfg).await;
|
||||
|
||||
assert!(
|
||||
outcome.errors.is_empty(),
|
||||
"unexpected errors: {:?}",
|
||||
outcome.errors
|
||||
);
|
||||
assert_eq!(
|
||||
outcome.skills,
|
||||
vec![SkillMetadata {
|
||||
name: "ui-skill".to_string(),
|
||||
description: "from json".to_string(),
|
||||
short_description: None,
|
||||
interface: Some(SkillInterface {
|
||||
display_name: Some("UI Skill".to_string()),
|
||||
short_description: None,
|
||||
icon_small: Some(normalized_skill_dir.join("assets/icon.png")),
|
||||
icon_large: Some(normalized_skill_dir.join("assets/logo.svg")),
|
||||
brand_color: None,
|
||||
default_prompt: None,
|
||||
}),
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
plugin_id: None,
|
||||
remote_plugin_id: None,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ignores_invalid_brand_color() {
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
let skill_path = write_skill(&codex_home, "demo", "ui-skill", "from json");
|
||||
let skill_dir = skill_path.parent().expect("skill dir");
|
||||
|
||||
write_skill_interface_at(
|
||||
skill_dir,
|
||||
r#"
|
||||
{
|
||||
"interface": {
|
||||
"brand_color": "blue"
|
||||
}
|
||||
}
|
||||
"#,
|
||||
);
|
||||
|
||||
let cfg = make_config(&codex_home).await;
|
||||
let outcome = load_skills_for_test(&cfg).await;
|
||||
|
||||
assert!(
|
||||
outcome.errors.is_empty(),
|
||||
"unexpected errors: {:?}",
|
||||
outcome.errors
|
||||
);
|
||||
assert_eq!(
|
||||
outcome.skills,
|
||||
vec![SkillMetadata {
|
||||
name: "ui-skill".to_string(),
|
||||
description: "from json".to_string(),
|
||||
short_description: None,
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
plugin_id: None,
|
||||
remote_plugin_id: None,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ignores_default_prompt_over_max_length() {
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
let skill_path = write_skill(&codex_home, "demo", "ui-skill", "from json");
|
||||
let skill_dir = skill_path.parent().expect("skill dir");
|
||||
let normalized_skill_dir = normalized(skill_dir);
|
||||
let too_long = "x".repeat(MAX_DEFAULT_PROMPT_LEN + 1);
|
||||
|
||||
write_skill_interface_at(
|
||||
skill_dir,
|
||||
&format!(
|
||||
r##"
|
||||
{{
|
||||
"interface": {{
|
||||
"display_name": "UI Skill",
|
||||
"icon_small": "./assets/small-400px.png",
|
||||
"default_prompt": "{too_long}"
|
||||
}}
|
||||
}}
|
||||
"##
|
||||
),
|
||||
);
|
||||
|
||||
let cfg = make_config(&codex_home).await;
|
||||
let outcome = load_skills_for_test(&cfg).await;
|
||||
|
||||
assert!(
|
||||
outcome.errors.is_empty(),
|
||||
"unexpected errors: {:?}",
|
||||
outcome.errors
|
||||
);
|
||||
assert_eq!(
|
||||
outcome.skills,
|
||||
vec![SkillMetadata {
|
||||
name: "ui-skill".to_string(),
|
||||
description: "from json".to_string(),
|
||||
short_description: None,
|
||||
interface: Some(SkillInterface {
|
||||
display_name: Some("UI Skill".to_string()),
|
||||
short_description: None,
|
||||
icon_small: Some(normalized_skill_dir.join("assets/small-400px.png")),
|
||||
icon_large: None,
|
||||
brand_color: None,
|
||||
default_prompt: None,
|
||||
}),
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
plugin_id: None,
|
||||
remote_plugin_id: None,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn drops_interface_when_icons_are_invalid() {
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
let skill_path = write_skill(&codex_home, "demo", "ui-skill", "from json");
|
||||
let skill_dir = skill_path.parent().expect("skill dir");
|
||||
|
||||
write_skill_interface_at(
|
||||
skill_dir,
|
||||
r#"
|
||||
{
|
||||
"interface": {
|
||||
"icon_small": "icon.png",
|
||||
"icon_large": "./assets/../logo.svg"
|
||||
}
|
||||
}
|
||||
"#,
|
||||
);
|
||||
|
||||
let cfg = make_config(&codex_home).await;
|
||||
let outcome = load_skills_for_test(&cfg).await;
|
||||
|
||||
assert!(
|
||||
outcome.errors.is_empty(),
|
||||
"unexpected errors: {:?}",
|
||||
outcome.errors
|
||||
);
|
||||
assert_eq!(
|
||||
outcome.skills,
|
||||
vec![SkillMetadata {
|
||||
name: "ui-skill".to_string(),
|
||||
description: "from json".to_string(),
|
||||
short_description: None,
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
plugin_id: None,
|
||||
remote_plugin_id: None,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn loads_plugin_skill_interface_icons_from_shared_plugin_assets() {
|
||||
let root = tempfile::tempdir().expect("tempdir");
|
||||
@@ -1110,65 +918,6 @@ interface:
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn drops_plugin_skill_interface_icons_that_escape_shared_plugin_assets() {
|
||||
let root = tempfile::tempdir().expect("tempdir");
|
||||
let plugin_root = root.path().join("plugins/twilio-developer-kit");
|
||||
let skill_path = write_skill_at(
|
||||
&plugin_root.join("skills"),
|
||||
"twilio-send-message",
|
||||
"send-message",
|
||||
"send messages",
|
||||
);
|
||||
let skill_dir = skill_path.parent().expect("skill dir");
|
||||
write_skill_interface_at(
|
||||
skill_dir,
|
||||
r##"
|
||||
interface:
|
||||
icon_small: "../../other/logo.svg"
|
||||
"##,
|
||||
);
|
||||
|
||||
let outcome = load_skills_from_roots(
|
||||
[SkillRoot {
|
||||
path: plugin_root.join("skills").abs(),
|
||||
scope: SkillScope::User,
|
||||
file_system: Arc::clone(&LOCAL_FS),
|
||||
plugin_identity: Some(PluginIdentity {
|
||||
plugin_id: "twilio-developer-kit@test".to_string(),
|
||||
remote_plugin_id: None,
|
||||
}),
|
||||
plugin_namespace: None,
|
||||
plugin_root: Some(plugin_root.abs()),
|
||||
discovery_mode: SkillDiscoveryMode::Recursive,
|
||||
}],
|
||||
/*plugin_skill_snapshots*/ None,
|
||||
Arc::new(Semaphore::new(MAX_CONCURRENT_ROOT_SCANS)),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
outcome.errors.is_empty(),
|
||||
"unexpected errors: {:?}",
|
||||
outcome.errors
|
||||
);
|
||||
assert_eq!(
|
||||
outcome.skills,
|
||||
vec![SkillMetadata {
|
||||
name: "send-message".to_string(),
|
||||
description: "send messages".to_string(),
|
||||
short_description: None,
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
plugin_id: Some("twilio-developer-kit@test".to_string()),
|
||||
remote_plugin_id: None,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn symlink_dir(target: &Path, link: &Path) {
|
||||
std::os::unix::fs::symlink(target, link).unwrap();
|
||||
@@ -2141,7 +1890,7 @@ async fn preserves_overlong_short_descriptions() {
|
||||
let codex_home = tempfile::tempdir().expect("tempdir");
|
||||
let skill_dir = codex_home.path().join("skills/demo");
|
||||
fs::create_dir_all(&skill_dir).unwrap();
|
||||
let too_long = "x".repeat(MAX_SHORT_DESCRIPTION_LEN + 1);
|
||||
let too_long = "x".repeat(MAX_DESCRIPTION_LEN + 1);
|
||||
let contents = format!(
|
||||
"---\nname: demo-skill\ndescription: long description\nmetadata:\n short-description: {too_long}\n---\n\n# Body\n"
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user