class Skills(Capability):
"""Mount skills into a Codex auto-discovery root inside the sandbox."""
type: Literal["skills"] = "skills"
skills: list[Skill] = Field(default_factory=list)
from_: BaseEntry | None = Field(default=None)
lazy_from: LazySkillSource | None = Field(default=None)
skills_path: str = Field(default=".agents")
_skills_metadata: list[SkillMetadata] | None = PrivateAttr(default=None)
@field_validator("skills", mode="before")
@classmethod
def _coerce_skills(
cls,
value: Sequence[Skill | Mapping[str, object]] | None,
) -> list[Skill]:
if value is None:
return []
return [
skill if isinstance(skill, Skill) else Skill.model_validate(dict(skill))
for skill in value
]
@field_validator("from_", mode="before")
@classmethod
def _coerce_entry(
cls,
entry: BaseEntry | Mapping[str, object] | None,
) -> BaseEntry | None:
if entry is None or isinstance(entry, BaseEntry):
return entry
return BaseEntry.parse(entry)
def model_post_init(self, context: Any, /) -> None:
_ = context
skills_root = _validate_relative_path(self.skills_path, field_name="skills_path")
self.skills_path = str(skills_root)
if not self.skills and self.from_ is None and self.lazy_from is None:
raise SkillsConfigError(
message="skills capability requires `skills`, `from_`, or `lazy_from`",
context={"field": "skills"},
)
configured_sources = sum(
1
for has_source in (
bool(self.skills),
self.from_ is not None,
self.lazy_from is not None,
)
if has_source
)
if configured_sources > 1:
raise SkillsConfigError(
message="skills capability accepts only one of `skills`, `from_`, or `lazy_from`",
context={"field": "skills", "has_from": self.from_ is not None},
)
if self.from_ is not None and not self.from_.is_dir:
raise SkillsConfigError(
message="`from_` must be a directory-like artifact",
context={"field": "from_", "artifact_type": self.from_.type},
)
seen_names: set[Path] = set()
for skill in self.skills:
rel = _validate_relative_path(
skill.name,
field_name="skills[].name",
context={"skill_name": skill.name},
)
if rel in seen_names:
raise SkillsConfigError(
message=f"duplicate skill name: {skill.name}",
context={"field": "skills[].name", "skill_name": skill.name},
)
seen_names.add(rel)
def process_manifest(self, manifest: Manifest) -> Manifest:
skills_root = Path(self.skills_path)
existing_paths = _manifest_entry_paths(manifest)
if self.lazy_from:
# Lazy sources do not claim `skills_root` in the manifest up front, so reserve the
# whole namespace here and fail fast if any existing manifest entry is equal to,
# above, or below that path.
overlaps = sorted(
str(path)
for path in existing_paths
if path == skills_root or path in skills_root.parents or skills_root in path.parents
)
if overlaps:
raise SkillsConfigError(
message="skills lazy_from path overlaps existing manifest entries",
context={
"path": str(skills_root),
"source": "lazy_from",
"overlaps": overlaps,
},
)
return manifest
if self.from_:
if skills_root in existing_paths:
existing_entry = _get_manifest_entry_by_path(manifest, skills_root)
if existing_entry is None:
raise SkillsConfigError(
message="skills root path lookup failed",
context={"path": str(skills_root), "source": "from_"},
)
if existing_entry.is_dir:
return manifest
raise SkillsConfigError(
message="skills root path already exists in manifest",
context={
"path": str(skills_root),
"source": "from_",
"existing_type": existing_entry.type,
},
)
manifest.entries[skills_root] = self.from_
existing_paths.add(skills_root)
for skill in self.skills:
relative_path = skills_root / Path(skill.name)
rendered_skill = skill.as_dir_entry()
if relative_path in existing_paths:
existing_entry = _get_manifest_entry_by_path(manifest, relative_path)
if existing_entry is None:
raise SkillsConfigError(
message="skill path lookup failed",
context={"path": str(relative_path), "skill_name": skill.name},
)
if existing_entry == rendered_skill:
continue
raise SkillsConfigError(
message="skill path already exists in manifest",
context={"path": str(relative_path), "skill_name": skill.name},
)
manifest.entries[relative_path] = rendered_skill
existing_paths.add(relative_path)
return manifest
def bind(self, session: BaseSandboxSession) -> None:
super().bind(session)
self._skills_metadata = None
def tools(self) -> list[Tool]:
if self.lazy_from is None:
return []
if self.session is None:
raise ValueError(f"{type(self).__name__} is not bound to a SandboxSession")
return [_LoadSkillTool(skills=self)]
async def load_skill(self, skill_name: str) -> dict[str, str]:
if self.lazy_from is None:
raise SkillsConfigError(
message="load_skill is only available when lazy_from is configured",
context={"skill_name": skill_name},
)
if self.session is None:
raise ValueError(f"{type(self).__name__} is not bound to a SandboxSession")
return await self.lazy_from.load_skill(
skill_name=skill_name,
session=self.session,
skills_path=self.skills_path,
user=self.run_as,
)
async def _resolve_runtime_metadata(self, manifest: Manifest) -> list[SkillMetadata]:
if self.session is None:
return []
skills_root = Path(manifest.root) / Path(self.skills_path)
try:
entries = await self.session.ls(skills_root, user=self.run_as)
except Exception:
return []
metadata: list[SkillMetadata] = []
for entry in entries:
if not entry.is_dir():
continue
skill_dir = Path(entry.path)
skill_name = skill_dir.name
skill_path = Path(self.skills_path) / skill_name
skill_md_path = skill_dir / "SKILL.md"
try:
handle = await self.session.read(skill_md_path, user=self.run_as)
except Exception:
continue
try:
markdown = _read_text(handle)
finally:
handle.close()
frontmatter = _parse_frontmatter(markdown)
metadata.append(
SkillMetadata(
name=frontmatter.get("name", skill_name),
description=frontmatter.get("description", "No description provided."),
path=skill_path,
)
)
return metadata
async def _skill_metadata(self, manifest: Manifest) -> list[SkillMetadata]:
if self._skills_metadata is not None:
return self._skills_metadata
metadata: list[SkillMetadata] = []
for skill in self.skills:
metadata.append(
SkillMetadata(
name=skill.name,
description=skill.description,
path=Path(self.skills_path) / skill.name,
)
)
if self.lazy_from is not None:
metadata.extend(self.lazy_from.list_skill_metadata(skills_path=self.skills_path))
elif self.from_ is not None:
metadata.extend(await self._resolve_runtime_metadata(manifest))
if isinstance(self.from_, Dir) and not metadata:
for key, entry in self.from_.children.items():
if not isinstance(entry, Dir):
continue
skill_name = str(key if isinstance(key, Path) else Path(key))
metadata.append(
SkillMetadata(
name=skill_name,
description=entry.description or "No description provided.",
path=Path(self.skills_path) / skill_name,
)
)
deduped: dict[tuple[str, str], SkillMetadata] = {}
for item in metadata:
deduped[(item.name, str(item.path))] = item
self._skills_metadata = sorted(deduped.values(), key=lambda item: item.name)
return self._skills_metadata
async def instructions(self, manifest: Manifest) -> str | None:
skills = await self._skill_metadata(manifest)
if not skills:
return None
available_skill_lines: list[str] = []
for skill in skills:
path_str = str(skill.path).replace("\\", "/")
available_skill_lines.append(f"- {skill.name}: {skill.description} (file: {path_str})")
how_to_use_section = (
_HOW_TO_USE_LAZY_SKILLS_SECTION
if self.lazy_from is not None
else _HOW_TO_USE_SKILLS_SECTION
)
return "\n".join(
[
"## Skills",
_SKILLS_SECTION_INTRO,
"### Available skills",
*available_skill_lines,
*(
[
"### Lazy loading",
"- These skills are indexed for planning, but they are not materialized "
"in the workspace yet.",
"- Call `load_skill` with a single skill name from the list before "
"reading its `SKILL.md` or other files from the workspace.",
"- `load_skill` stages exactly one skill under the listed path. "
"If you need more than one skill, call it multiple times.",
]
if self.lazy_from is not None
else []
),
how_to_use_section,
]
)