feat: skill discovery — search and install skills from external sources (#111)

* feat: skill discovery — search and install skills from external sources

Add discovery UI and API for finding and installing skills from
skills.sh registries and GitHub repositories with one-click install,
SKILL.md frontmatter parsing, and security scan integration.

Core modules:
- skill_parser.py: ParsedSkill dataclass, parse_skill_md() with YAML
  frontmatter support (Anthropic + Hermes tag formats), name validation
- skill_sources.py: SkillsShClient (async search + resolve),
  fetch_skill_from_github (SKILL.md + bundled resource fetching with
  256KB cap, text extension filter, GitHub API tree traversal)

API:
- GET /v1/api/admin/skills/discover — search with installed annotation
  and scan_status for installed skills
- POST /v1/api/admin/skills/install — fetch, parse, duplicate check,
  create with origin="source" readonly=true, store resources, audit

Also fixes pre-existing bug where _skill_to_response omitted scan_status,
scan_report, scan_version fields — scan tier badges in the installed
skills table were silently empty despite data existing in storage.

Admin UI: pill toggle (Installed/Discover), discovery cards with scan
tier badges, GitHub import modal with proper focus trap/Escape/backdrop,
scoped selectors preventing MCP↔Skills cross-tab state corruption.

SDK: discover_skills() + install_skill() on Python (async+sync) and
TypeScript console clients.

48 new tests across 3 test files. All 2632 tests pass.

* fix: address copilot review — 404 vs 502, O(n) lookups, branch fallback

- SkillNotFoundError subclass: install returns 404 when SKILL.md is
  missing, 502 only for connectivity/upstream errors
- get_skill_by_source_url() + list_installed_skill_urls(): indexed
  storage lookups replace O(n) full-table scans with content blobs
- Default branch fallback: tries main then master when URL doesn't
  specify a branch
- Path normalization: strip trailing slash once, remove redundant
  candidate
- SDK install_skill() returns typed SkillInfo with response_model
- Tree size guard: skip resource tree if response >2MB
This commit is contained in:
Patrick Buckley
2026-03-16 18:42:32 -07:00
committed by GitHub
parent e71ea38953
commit c28bfc1e58
29 changed files with 2601 additions and 12 deletions
+68
View File
@@ -1350,6 +1350,74 @@ version. Requires the `admin.skills` permission.
---
### `GET /v1/api/admin/skills/discover` (Console)
Search external skill registries for available skills. Requires the
`admin.skills` permission.
**Query parameters:**
| Parameter | Type | Default | Description |
|-----------|--------|---------|-------------|
| `q` | string | `""` | Search query |
| `limit` | int | `20` | Max results (1100) |
**Response:**
```json
{
"skills": [
{
"id": "owner/repo/skill-name",
"name": "skill-name",
"description": "A skill description",
"author": "Author Name",
"source": "skills.sh",
"source_url": "https://github.com/owner/repo",
"install_count": 42,
"tags": ["coding", "review"],
"installed": false
}
]
}
```
**Error:** `502` if the registry is unreachable.
---
### `POST /v1/api/admin/skills/install` (Console)
Install a skill from an external source (skills.sh registry or GitHub).
Requires the `admin.skills` permission.
**Request body:**
```json
{
"source": "github",
"url": "https://github.com/owner/skill-repo"
}
```
Or for skills.sh:
```json
{
"source": "skills.sh",
"skill_id": "owner/skill-name"
}
```
**Response:** Same as `GET /v1/api/admin/skills/{skill_id}` — the created
skill object.
**Errors:** `400` invalid source or missing fields, `404` SKILL.md not found,
`409` skill already installed (duplicate source_url or name), `502` source
unreachable.
---
### `GET /v1/api/admin/settings` (Console)
List all settings with their effective values, defaults, and metadata. Requires