5.9 KiB
M3.5.5 — GET /skills and /skills/{name}: loadable skills catalog
| Field | Value |
|---|---|
| Phase | M3.5 — Distributed API Layer |
| Size | M — 1–3 days |
| Status | ✅ Done |
| Flags | — |
| Spec | inlined below |
| Blocks | M3.5.8 |
| Depends | M3.5.1, M4.1 (skill drafts exist locally) |
Goal
Read-only endpoints for skill catalog. List all promoted skills (exclude _drafts/), fetch individual skill metadata and body. Skills are Obsidian notes; expose them over HTTP for agent discovery.
Design
List all loadable skills:
GET /memory/skills?loadable=true
→ 200 {
"skills": [
{
"name": "infra-root-causes",
"description": "Identify root causes of infrastructure failures",
"when_to_use": "When troubleshooting cluster or service outages",
"argument_hint": "--project <name>",
"promoted_at": "2026-08-20T10:30:00Z",
"generated_from": null
},
...
]
}
Get one skill (metadata only):
GET /memory/skills/infra-root-causes
→ 200 {
"name": "infra-root-causes",
"description": "...",
"when_to_use": "...",
"argument_hint": "...",
"promoted_at": "2026-08-20T...",
"generated_from": null
}
Get skill with body (full content):
GET /memory/skills/infra-root-causes?include_body=true
→ 200 {
"name": "infra-root-causes",
"description": "...",
"body": "# Infra root causes\n\n..."
}
Filters:
loadable=true(default): exclude_drafts/, return only promoted skillsloadable=false: include everything (admin only — must have special apikey, documented in code)
Technical
Source: Vault at vault/skills/ contains skill markdown files. Each skill is a directory:
vault/skills/
infra-root-causes/
SKILL.md <- frontmatter + body
Frontmatter (YAML in SKILL.md):
---
name: infra-root-causes
description: Identify root causes of infrastructure failures
when_to_use: When troubleshooting cluster or service outages
argument_hint: --project <name>
generated_from: null | <L2 sha256>
---
Drafts are in vault/skills/_drafts/:
vault/skills/
_drafts/
new-skill/
SKILL.md
Only load from vault/skills/*/SKILL.md (not _drafts), unless loadable=false is passed with an admin key.
Steps
-
GET /memory/skillshandler:- List
vault/skills/directory (skip_drafts/) - For each
*/SKILL.md, parse frontmatter - Extract:
name,description,when_to_use,argument_hint,promoted_at(file mtime) - Parse
generated_fromfield to show provenance - Return array
- List
-
GET /memory/skills/{name}handler:- Load
vault/skills/{name}/SKILL.md - Parse frontmatter and body
- If
include_body=false(default), return metadata only - If
include_body=true, include markdown body
- Load
-
loadablequery param (admin-only feature):- Default: exclude
_drafts/ loadable=falsewith admin apikey: include_drafts/in listing- Non-admin key requesting
loadable=false→ 403 Forbidden
- Default: exclude
-
Error handling:
- Skill not found → 404 with
{"error":"not_found","reason":"skill 'xyz' not promoted"} - Malformed SKILL.md (frontmatter parse fails) → 500 with error (admin debug only)
- Admin check: apikey must be in a whitelist (env var
MEM_ADMIN_APIKEYSor config)
- Skill not found → 404 with
Acceptance
- List endpoint returns all promoted skills
- Individual skill fetch works
- Drafts are excluded by default
- Admin with
loadable=falsesees drafts - Skill body is optional (include_body param)
- Promoted_at field reflects file mtime
Verify
Harness: Integration tests + filesystem fixtures.
Setup: Create test vault/skills/ with:
vault/skills/test-skill-1/SKILL.md(promoted)vault/skills/test-skill-2/SKILL.md(promoted)vault/skills/_drafts/draft-skill/SKILL.md(unpromoted)
Integration test — tests/it_skills_endpoint.rs:
a1_list_skills_returns_promoted— GET /skills returns array with test-skill-1 and test-skill-2.a2_drafts_excluded_by_default— GET /skills does not include draft-skill.a3_drafts_included_with_admin_key— GET /skills?loadable=false with admin apikey includes draft-skill.a4_non_admin_denied_drafts— GET /skills?loadable=false with regular apikey returns 403.a5_get_single_skill_metadata— GET /skills/test-skill-1 returns 200 with frontmatter fields.a6_include_body_true— GET /skills/test-skill-1?include_body=true returns body field with markdown.a7_include_body_false— GET /skills/test-skill-1?include_body=false (or omitted) does not include body field.a8_skill_not_found— GET /skills/nonexistent returns 404.a9_promoted_at_is_file_mtime— GET /skills/test-skill-1, assert promoted_at is a valid ISO timestamp close to SKILL.md's modification time.a10_generated_from_field— SKILL.md withgenerated_from: sha256xyzis parsed and returned as-is.
Command: cargo test -p mem-cli skills_endpoint
False pass:
- Drafts never created in test fixtures. The default exclude-drafts logic is untestable without a draft.
- Admin key never tested. Non-admin path and admin path can be identical in code.
- Promoted_at never validated. Can return a fake date; file mtime is the only source.
- Frontmatter parsing doesn't validate required fields (name, description). A malformed SKILL.md is silently returned with null values.
Traps
- Vault directory may not exist locally (only in deployed cluster). Start with a default empty list if vault/ is missing.
- YAML frontmatter parsing is fussy. A tab instead of spaces breaks YAML. Use a YAML parser (serde_yaml) and validate on load.
- File mtime precision: Unix mtime is seconds; SKILL.md edits may not increment it if done within the same second. Use actual write timestamp if available.
- Admin key stored in env var. If unset, default to deny (safer than default allow).
Background: DESIGN.md § Skills — the procedural projection