fix(gateway): allow standard skill frontmatter metadata (#1103)

* fix(gateway): allow standard skill frontmatter metadata

Accept standard optional frontmatter fields during .skill installs so external skills with version, author, or compatibility metadata do not fail validation.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* docs: sync skill installer metadata behavior

Document the skill install allowlist so user-facing and backend contributor docs match the gateway validation contract.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* Apply suggestions from code review

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Ryanba
2026-03-13 21:23:35 +08:00
committed by GitHub
parent 03cafea715
commit cda9fb7bca
4 changed files with 83 additions and 4 deletions

View File

@@ -4,7 +4,9 @@ import re
import shutil
import tempfile
import zipfile
from collections.abc import Mapping
from pathlib import Path
from typing import cast
import yaml
from fastapi import APIRouter, HTTPException
@@ -57,7 +59,20 @@ class SkillInstallResponse(BaseModel):
# Allowed properties in SKILL.md frontmatter
ALLOWED_FRONTMATTER_PROPERTIES = {"name", "description", "license", "allowed-tools", "metadata"}
ALLOWED_FRONTMATTER_PROPERTIES = {
"name",
"description",
"license",
"allowed-tools",
"metadata",
"compatibility",
"version",
"author",
}
def _safe_load_frontmatter(frontmatter_text: str) -> object:
return cast(object, yaml.safe_load(frontmatter_text))
def _validate_skill_frontmatter(skill_dir: Path) -> tuple[bool, str, str | None]:
@@ -86,9 +101,11 @@ def _validate_skill_frontmatter(skill_dir: Path) -> tuple[bool, str, str | None]
# Parse YAML frontmatter
try:
frontmatter = yaml.safe_load(frontmatter_text)
if not isinstance(frontmatter, dict):
parsed_frontmatter = _safe_load_frontmatter(frontmatter_text)
if not isinstance(parsed_frontmatter, Mapping):
return False, "Frontmatter must be a YAML dictionary", None
parsed_frontmatter = cast(Mapping[object, object], parsed_frontmatter)
frontmatter: dict[str, object] = {str(key): value for key, value in parsed_frontmatter.items()}
except yaml.YAMLError as e:
return False, f"Invalid YAML in frontmatter: {e}", None