mirror of
https://github.com/openai/codex.git
synced 2026-10-22 17:58:46 +00:00
## 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
166 lines
4.8 KiB
Rust
166 lines
4.8 KiB
Rust
use std::path::PathBuf;
|
|
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
use super::SkillInterfaceAssetPolicy;
|
|
use super::SkillInterfaceFile;
|
|
use super::resolve_skill_interface;
|
|
use crate::SkillInterface;
|
|
|
|
fn absolute_path(path: &str) -> AbsolutePathBuf {
|
|
AbsolutePathBuf::try_from(
|
|
std::env::current_dir()
|
|
.expect("current directory")
|
|
.join(path),
|
|
)
|
|
.expect("absolute path")
|
|
}
|
|
|
|
#[test]
|
|
fn resolves_local_interface_fields_and_assets() {
|
|
let skill_dir = absolute_path("skill");
|
|
let interface = SkillInterfaceFile {
|
|
display_name: Some(" Demo skill ".to_string()),
|
|
short_description: Some(" Short description ".to_string()),
|
|
icon_small: Some(PathBuf::from("./assets/icon.svg")),
|
|
icon_large: Some(PathBuf::from("assets/icon-large.png")),
|
|
brand_color: Some(" #123ABC ".to_string()),
|
|
default_prompt: Some(" Run the demo ".to_string()),
|
|
};
|
|
|
|
assert_eq!(
|
|
resolve_skill_interface(
|
|
Some(interface),
|
|
&skill_dir,
|
|
SkillInterfaceAssetPolicy::LocalOnly,
|
|
),
|
|
Some(SkillInterface {
|
|
display_name: Some("Demo skill".to_string()),
|
|
short_description: Some("Short description".to_string()),
|
|
icon_small: Some(skill_dir.join("assets/icon.svg")),
|
|
icon_large: Some(skill_dir.join("assets/icon-large.png")),
|
|
brand_color: Some("#123ABC".to_string()),
|
|
default_prompt: Some("Run the demo".to_string()),
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_invalid_local_interface_fields() {
|
|
let skill_dir = absolute_path("skill");
|
|
let interface = SkillInterfaceFile {
|
|
display_name: None,
|
|
short_description: None,
|
|
icon_small: Some(PathBuf::from("../outside.svg")),
|
|
icon_large: Some(PathBuf::from("icon.png")),
|
|
brand_color: Some("blue".to_string()),
|
|
default_prompt: Some("x".repeat(1025)),
|
|
};
|
|
|
|
assert_eq!(
|
|
resolve_skill_interface(
|
|
Some(interface),
|
|
&skill_dir,
|
|
SkillInterfaceAssetPolicy::LocalOnly,
|
|
),
|
|
None
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_absolute_asset_paths() {
|
|
let skill_dir = absolute_path("skill");
|
|
let interface = SkillInterfaceFile {
|
|
icon_small: Some(absolute_path("outside.svg").as_path().to_path_buf()),
|
|
..Default::default()
|
|
};
|
|
|
|
assert_eq!(
|
|
resolve_skill_interface(
|
|
Some(interface),
|
|
&skill_dir,
|
|
SkillInterfaceAssetPolicy::LocalOnly,
|
|
),
|
|
None
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn drops_invalid_fields_without_discarding_valid_interface_fields() {
|
|
let skill_dir = absolute_path("skill");
|
|
let interface = SkillInterfaceFile {
|
|
display_name: Some("Demo skill".to_string()),
|
|
short_description: None,
|
|
icon_small: Some(PathBuf::from("assets/icon.svg")),
|
|
icon_large: Some(PathBuf::from("icon.png")),
|
|
brand_color: Some("blue".to_string()),
|
|
default_prompt: Some("x".repeat(1025)),
|
|
};
|
|
|
|
assert_eq!(
|
|
resolve_skill_interface(
|
|
Some(interface),
|
|
&skill_dir,
|
|
SkillInterfaceAssetPolicy::LocalOnly,
|
|
),
|
|
Some(SkillInterface {
|
|
display_name: Some("Demo skill".to_string()),
|
|
short_description: None,
|
|
icon_small: Some(skill_dir.join("assets/icon.svg")),
|
|
icon_large: None,
|
|
brand_color: None,
|
|
default_prompt: None,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn resolves_plugin_shared_assets() {
|
|
let plugin_root = absolute_path("plugin");
|
|
let skill_dir = plugin_root.join("skills/demo");
|
|
let interface = SkillInterfaceFile {
|
|
icon_small: Some(PathBuf::from("../../assets/icon.svg")),
|
|
..Default::default()
|
|
};
|
|
|
|
assert_eq!(
|
|
resolve_skill_interface(
|
|
Some(interface),
|
|
&skill_dir,
|
|
SkillInterfaceAssetPolicy::PluginShared {
|
|
plugin_root: &plugin_root,
|
|
},
|
|
),
|
|
Some(SkillInterface {
|
|
display_name: None,
|
|
short_description: None,
|
|
icon_small: Some(plugin_root.join("assets/icon.svg")),
|
|
icon_large: None,
|
|
brand_color: None,
|
|
default_prompt: None,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_plugin_assets_outside_shared_assets_root() {
|
|
let plugin_root = absolute_path("plugin");
|
|
let skill_dir = plugin_root.join("skills/demo");
|
|
let interface = SkillInterfaceFile {
|
|
icon_small: Some(PathBuf::from("../../other/icon.svg")),
|
|
..Default::default()
|
|
};
|
|
|
|
assert_eq!(
|
|
resolve_skill_interface(
|
|
Some(interface),
|
|
&skill_dir,
|
|
SkillInterfaceAssetPolicy::PluginShared {
|
|
plugin_root: &plugin_root,
|
|
},
|
|
),
|
|
None
|
|
);
|
|
}
|