Both paths in search_skills/hybrid_search_skills now go through a
shared SkillRanker singleton:
- SkillSearchEngine._bm25_phase: previously instantiated a fresh
SkillRanker per call, reloading the pickle cache each time.
- hybrid_search_skills candidate loop: previously generated
embeddings via generate_embedding on every query, ignoring the
persistent cache entirely.
The persistent pickle at
.openspace/skill_embedding_cache/skill_embeddings_v1.pkl is
reused across invocations and survives process restarts.
Candidates without a stable skill_id are skipped to avoid cache
key collisions.
On a 28-skill local registry with text-embedding-3-small via
OpenRouter, query latency drops from 8-14s to ~300ms after
warm-up. Top-1 match identity is preserved on all test queries
(score drift <0.001).
Cloud candidates that already carry _embedding from the
server-side search endpoint are skipped and unchanged.
- Add resolve() + is_relative_to() check in _extract_zip() to block
nested traversal entries like nested/../../escape.txt
- Sanitize server-provided skill name in import_skill() to prevent
directory escape via malicious record metadata
- Add 6 regression tests covering both attack vectors
Closes#17
Co-authored-by: LeftX <xzq-xu@users.noreply.github.com>