Repository navigation
design phase 7.4 multi source
Status: implemented in Phase 7.4 (✅). PRD:
prd-phase-7-skills.md§7.4. Prior designs:phase-7.1-plugin-source.md(storage) +phase-7.2-marketplace-fetch.md(single-marketplace browse) +phase-7.3-hub-ui.md(the hub page this extends). Phase 7 complete.
Hermes-style multi-source skill search. A query dispatches to a set of
SkillSource adapters in parallel; results are merged, deduped by identifier,
ranked by trust, and surfaced in the existing /skills/hub page alongside the
7.2/7.3 marketplace browse. This is the last sub-phase of Phase 7.
Hermes ships 10 adapters. This phase ports the 4 worth porting and explicitly skips/defers the rest, with reasons:
| Adapter | Verdict | Why |
|---|---|---|
GitHubSource |
PORT | Foundational. Taps + recursive-tree cache + trust tiers. |
WellKnownSkillSource |
PORT | Open /.well-known/skills/ protocol, no auth, ~220 lines. |
UrlSource |
PORT | Trivial single-file fetch. ~160 lines. |
ClaudeMarketplaceSource |
PORT as MarketplaceSource |
Same .claude-plugin/marketplace.json mechanism as 7.2 — wrapped, not reimplemented. |
SkillsShSource |
DEFER (7.5+) | Thin GitHub-redirect layer with sitemap-walking complexity. If ported, /api/search only. |
BrowseShSource |
DEFER (7.5+) | Clean API but niche (browser-automation skills only). |
HermesIndexSource |
SKIP | Hermes-specific artifact (hermes-agent.nousresearch.com). Borrow the schema later, not the source. |
ClawHubSource |
SKIP | Docstring itself confirms distrust post-ClawHavoc (341 malicious skills, Feb 2026). |
LobeHubSource |
SKIP | System-prompt templates, wrong artifact class. |
OptionalSkillSource |
SKIP | Hermes-bundled official skills (not a remote source). |
GET /api/skills/search?q=<query>
│
▼
SkillSearchRouter.search(q)
│
│ Promise.all(sources.map(s => race(s.search(q), timeout))) ← per-source timeout
│ • marketplace source → SkillCatalogService.getCatalog (7.2, cached)
│ • github source → GitHub trees API (per-tap, tree-cached 1h)
│ • well-known source → GET <host>/.well-known/skills/index.json
│ • url source → no-op (fetch-only; contributes nothing to search)
▼
merge: dedupe by identifier, keep higher trust tier on collision
│
▼
{ results: SkillMeta[], timedOut: string[], errored: string[] }
-
GitHubSource { fetcher, token?, taps, logger }.tapsdefault = the 4TRUSTED_REPOS(openai/skills,anthropics/skills,huggingface/skills,NVIDIA/skills). -
search(q): for each tap,GET /repos/{repo}/git/trees/HEAD?recursive=1(the recursive tree API — one call per tap, vs the naive per-directory walk that blows the rate limit). Collect every dir containing aSKILL.md, filter by name ⊇ q, returnSkillMeta(identifier =owner/repo/dir, trust viaresolveTrustTier). -
Tree cache:
Map<repo, {dirs, expiresAt}>, 1h TTL. Trees change rarely. -
Rate-limit handling: 403/429 → log warn + return
[]for that tap. The source never throws — partial results from other sources still surface. -
Auth: optional
GITHUB_TOKEN(5000/hr authenticated vs 60/hr anonymous). Hermes'sgh auth tokensubprocess + GitHub App fallbacks deliberately not ported — too heavy; a user with a token sets the env. -
inspect/fetchreturn null (7.4 ships search only; install-time fetch is Phase 3.3).
-
WellKnownSource { fetcher, logger }. No auth. -
search(q): only handles URL-form queries (http(s)://host). GETs<origin>/.well-known/skills/index.json→{skills:[{name,description}]}, returnsSkillMeta(identifierwell-known:<origin>/<name>, alwayscommunity). Non-URL queries return empty (no host auto-discovery). -
inspect/fetchreturn null.
-
UrlSource { fetcher, logger }. Single-fileSKILL.mdat a direct URL. -
search(): no-op (Hermes is too; this source is fetch-only — it has no catalog). It contributes when a user pastes a URL into the search box and the hub resolves it viainspect(not yet wired in the UI — Phase 3.3 territory). -
inspect(url): GET url, parse YAML frontmatter for name/description.
-
MarketplaceSource { catalog, allowlist }. Wraps 7.2'sSkillCatalogService— no reimplemented fetch, the catalog cache is reused. -
search(q): for each allowlist entry,catalog.listPlugins(id, url, {q}), mapMarketplacePlugin→SkillMeta(identifier{marketplaceId}:{name}, trust viaresolveTrustTier). Catalog-fetch errors (502) are swallowed — a failed marketplace contributes nothing.
-
SkillSearchRouter { sources, timeoutMs, logger }. -
search(q, limit=50): Promise<{results, timedOut, errored}>:-
Per-source timeout via
Promise.race([s.search(q), timeout])— a slow source can't block the response pasttimeoutMs(default 30s). Timed-out source ids go totimedOut; rejected ones toerrored. The whole search never throws on a source failure (partial-results-first, matching Hermes). -
Merge/dedupe: key =
identifier(NOTname— browse-sh skills across sites share task names). On collision, the higher-trust entry wins (TRUST_RANK = {builtin:2, trusted:1, community:0}); within a tier, insertion order (source priority) is preserved.
-
Per-source timeout via
- Decorated as
app.skillSearch(co-located Fastify augmentation).
-
Timeout must not block. Hermes deliberately avoids Python's
with ThreadPoolExecutor(...)because its__exit__callsshutdown(wait=True), making the overall timeout a no-op. The JS equivalent of itsshutdown(wait=False, cancel_futures=True)isPromise.race+ a per-source timeout promise — a plainawait Promise.all(...)would let a slow source block past the budget. The in-flight promises settle in the background after the timeout; their results are discarded. - Dedupe by identifier, not name. Same skill name from two sources (or two repos) are distinct results. The collision rule keeps the higher-trust copy.
| Env | Default | Purpose |
|---|---|---|
GITHUB_TOKEN |
unset | optional PAT for GitHubSource (5000/hr vs 60/hr) |
SKILL_GITHUB_TAPS |
openai/skills,anthropics/skills,huggingface/skills,NVIDIA/skills |
comma-separated owner/repo taps |
SKILL_SEARCH_TIMEOUT_MS |
30000 |
per-source timeout (overall budget) |
SKILL_DISABLED_SOURCES |
unset | comma-separated source ids to disable (test mode) |
-
GET /api/skills/search?q=<query>&limit=50→{results: SkillMeta[], timedOut: string[], errored: string[]}.qrequired (400 if absent). Browsing without a query stays on 7.2's/api/skills/marketplaces/:id/plugins. - SDK:
api.searchSkills(q, limit?).
The existing hub page (7.3) gains a dual mode:
- Search box empty → 7.2/7.3 marketplace browse (marketplace Select + category Select).
-
Search box non-empty → multi-source search. The marketplace/category Selects
hide; the table shows results from all sources with a per-row
sourceKindbadge (marketplace / github / well-known / url).
Both modes project into a unified HubRow (key/name/description/sourceKind/
category/homepage/tier/pluginSource/hasPin) so the table and the save dialog
render one way. pluginSource is precomputed by each adapter and carried in
SkillMeta.extra.pluginSource — the save dialog stores it verbatim via
skillMetaToResourceSource(meta), no identifier reverse-engineering.
If timedOut.length > 0, a text-muted-foreground notice appears above the
table ("Some sources timed out (…); showing partial results").
The smoke suite verifies the search pipeline with SKILL_DISABLED_SOURCES= github,well-known,url — so only the marketplace source runs against the 7.2
fixture. The [7.4] block asserts: q required (400), the marketplace result
surfaces (jfrog), every result carries identifier + trustLevel,
extra.pluginSource is the plugin variant, and timedOut/errored are
arrays. 133 passed / 0 failed.
The per-adapter fetch logic (GitHub tree parsing, well-known index shape, URL
frontmatter) is covered by typecheck + manual verification (like 7.3's UI):
real GitHub/well-known behavior requires a GITHUB_TOKEN and live network,
which the smoke suite deliberately avoids.
Production (real multi-source search):
JWT_SECRET=… pnpm dev:server
# GITHUB_TOKEN unset → github source runs anonymous (60 req/hr); set it for 5000/hrTest (marketplace-only search against a fixture):
JWT_SECRET=… STORAGE_DRIVER=memory \
MARKETPLACE_FIXTURE_PATH=$PWD/scripts/fixtures/marketplace.json \
SKILL_DISABLED_SOURCES=github,well-known,url \
pnpm dev:serverAdd a custom GitHub tap:
SKILL_GITHUB_TAPS="openai/skills,anthropics/skills,myorg/my-skills"-
fetch()(materialize bytes) — Phase 3.3's install writers. 7.4 adapters implementsearch(+inspectfor url);fetchreturns null. - skills.sh / browse.sh adapters — DEFER (7.5+).
- clawhub / lobehub / hermes-index — SKIP (verified).
- Taps management UI/API — env config is enough for 7.4; a CRUD surface is a follow-on.
- Content scanning — Out of Scope (Harness Nexus stores references; the target tool executes).
-
A pre-built merged index (the Hermes
hermes-indexpattern withresolved_github_idshortcuts). Worth revisiting if multi-source search proves slow against live GitHub; for now the per-tap tree cache suffices.