Install skills from skills.sh in the Config Explorer #12

Merged
lz merged 24 commits from feat/custom-branch into main 2026-05-15 19:59:51 +02:00
Owner

Summary

Adds a first-class skill installer to the Config Explorer. Users paste a GitHub identifier (or search skills.sh in-app), Nexus clones the repo, scans for SKILL.md files, installs one into ~/.claude/skills/<name>/, and tracks origin + commit SHA in a sidecar .nexus-skill.json manifest. The manifest powers update detection, local-edit warnings, and clean uninstall.

The page becomes a three-pane layout:

  • Installed Assets panel (new, leftmost) — primary lifecycle UI: list, check for updates, update, uninstall.
  • File tree (existing) — repositioned as the "advanced" raw-file view.
  • Editor (existing) — unchanged.

Highlights

  • Search skills.sh from inside Nexus. Reverse-engineered the npx skills find CLI's API; the install modal opens to a search box that proxies https://skills.sh/api/search through a server-side route. Clicking a result auto-installs (no picker needed when the search hit matches a SKILL.md frontmatter name).
  • Manifest-tracked installs. Each install writes .nexus-skill.json with source, installedSha, ISO timestamp, and a SHA-256 of every file. The hashes let us detect when a user has locally edited a managed skill and warn before overwriting on update.
  • Cross-volume safe. Staging lives on nexus-data, skills live on claude-session — two separate Docker volumes. The service uses a moveDir helper that falls back from fs.rename to cp + rm when it hits EXDEV.
  • No DB tables. The disk is the source of truth. Surviving a container rebuild requires only the claude-session volume.
  • Auth-gated. Every new route is behind locals.masterKey, same as the existing Config Explorer.

Architecture

nexus/src/lib/server/skills-installer/
  resolve.ts       — owner/repo[/sub][#ref] → { cloneUrl, ref, subpath, source }
  git-client.ts    — GitClient interface + execGitClient (spawn 'git', no shell)
  scan.ts          — walk staging for SKILL.md, parse js-yaml frontmatter
  manifest.ts      — atomic read/write + hashDirectory + countLocalEdits
  service.ts       — orchestrator (preview, install, list, check, update, uninstall, sweep)
  http.ts          — SkillsInstallerError → HTTP status mapping
  types.ts, index.ts

nexus/src/routes/api/config-explorer/assets/
  preview/         POST    paste identifier → clone + scan + return picker
  install/         POST    install a staged skill into ~/.claude/skills/<name>
  installed/       GET     list managed skills + update status
  installed/[name] DELETE  uninstall (refuses on unmanaged dirs)
  check-updates/   POST    git ls-remote vs installedSha
  update/          POST    re-clone + atomic swap + refresh manifest
  staging/[id]     DELETE  cancel an in-flight install
  search/          POST    proxy skills.sh /api/search

nexus/src/lib/components/config-explorer/
  InstallSkillModal.svelte     — search → loading → picker → install
  InstalledAssetsPanel.svelte  — primary lifecycle UI

worker side: Dockerfile gains `git` in the runtime image; nexus side: `js-yaml` added.

Security

Multiple supply-chain attacks ruled out during review:

  • Resolver rejects ssh://, file://, git://, and plain http:// — HTTPS only.
  • git is spawned via spawn('git', args, ...) (args array, no shell interpolation).
  • Skill names parsed from upstream SKILL.md frontmatter pass through assertSafeSkillName — no slashes, no .., no leading dot, max 255 chars. Without this guard a malicious public repo could ship a name: "../agents/evil" and write outside skills/.

Testing

171 vitest cases pass (44 new). Coverage:

  • Identifier resolver — 16 cases (shorthand, URLs, refs, subpaths, blocked schemes).
  • SKILL.md scanner — 12 cases (single-skill, multi-skill, invalid frontmatter, depth, .git skip).
  • Manifest — 8 cases (round-trip, hashing, local-edit detection of mod/add/delete).
  • Service — 22 cases (preview, install, list, update, uninstall, sweep, lock concurrency, path-traversal rejection, EXDEV cross-volume, staging TTL).

Fake GitClient materializes files on disk per test so manifest + hashing logic runs against a real fs. Suite stays under 5s offline, no real network.

What's intentionally out of scope (v1)

  • Browse/UI for agents, commands, hooks, output-styles — only skills for now. The route prefix is /assets/ so adding more asset types later is purely "add another scanner".
  • Private GitHub repos (no auth flow).
  • Auto-update (operator must click).
  • Merging upstream changes with local edits (current model: warn and overwrite).

Known follow-ups (deferred from final review, not blockers)

  • listInstalled re-hashes every file on every call — fine until ~100 skills.
  • checkUpdates swallows per-asset errors silently — add logger.warn.
  • updateAsset rollback failure path is silent if both move + rollback fail.
  • Modal Cancel during in-flight preview doesn't abort the request (request continues; toast fires after modal closes).

Test plan

  • pnpm test in nexus/ — all 171 tests pass.
  • docker compose up -d — container boots; agent-nexus ready logged.
  • Open Config Explorer → Installed Assets panel renders empty on left.
  • Click + Install → search "react" → click a result → toast confirms install.
  • Inspect on disk: docker compose exec nexus ls /shared-config/dot-claude/skills/<name>/ shows SKILL.md + .nexus-skill.json.
  • Click Check Updates → status pill becomes "up to date".
  • Click Update → confirm → succeeds.
  • Manually edit SKILL.md via file tree → click Update → confirm dialog warns about local edits.
  • Click Uninstall → row + directory both gone.
  • Spawn a worker → ls /root/.claude/skills/ shows the installed skills (cross-worker volume sanity).
## Summary Adds a first-class **skill installer** to the Config Explorer. Users paste a GitHub identifier (or search skills.sh in-app), Nexus clones the repo, scans for `SKILL.md` files, installs one into `~/.claude/skills/<name>/`, and tracks origin + commit SHA in a sidecar `.nexus-skill.json` manifest. The manifest powers update detection, local-edit warnings, and clean uninstall. The page becomes a **three-pane layout**: - **Installed Assets panel** (new, leftmost) — primary lifecycle UI: list, check for updates, update, uninstall. - **File tree** (existing) — repositioned as the "advanced" raw-file view. - **Editor** (existing) — unchanged. ## Highlights - **Search skills.sh from inside Nexus.** Reverse-engineered the `npx skills find` CLI's API; the install modal opens to a search box that proxies `https://skills.sh/api/search` through a server-side route. Clicking a result auto-installs (no picker needed when the search hit matches a SKILL.md frontmatter name). - **Manifest-tracked installs.** Each install writes `.nexus-skill.json` with `source`, `installedSha`, ISO timestamp, and a SHA-256 of every file. The hashes let us detect when a user has locally edited a managed skill and warn before overwriting on update. - **Cross-volume safe.** Staging lives on `nexus-data`, skills live on `claude-session` — two separate Docker volumes. The service uses a `moveDir` helper that falls back from `fs.rename` to `cp + rm` when it hits `EXDEV`. - **No DB tables.** The disk is the source of truth. Surviving a container rebuild requires only the `claude-session` volume. - **Auth-gated.** Every new route is behind `locals.masterKey`, same as the existing Config Explorer. ## Architecture ``` nexus/src/lib/server/skills-installer/ resolve.ts — owner/repo[/sub][#ref] → { cloneUrl, ref, subpath, source } git-client.ts — GitClient interface + execGitClient (spawn 'git', no shell) scan.ts — walk staging for SKILL.md, parse js-yaml frontmatter manifest.ts — atomic read/write + hashDirectory + countLocalEdits service.ts — orchestrator (preview, install, list, check, update, uninstall, sweep) http.ts — SkillsInstallerError → HTTP status mapping types.ts, index.ts nexus/src/routes/api/config-explorer/assets/ preview/ POST paste identifier → clone + scan + return picker install/ POST install a staged skill into ~/.claude/skills/<name> installed/ GET list managed skills + update status installed/[name] DELETE uninstall (refuses on unmanaged dirs) check-updates/ POST git ls-remote vs installedSha update/ POST re-clone + atomic swap + refresh manifest staging/[id] DELETE cancel an in-flight install search/ POST proxy skills.sh /api/search nexus/src/lib/components/config-explorer/ InstallSkillModal.svelte — search → loading → picker → install InstalledAssetsPanel.svelte — primary lifecycle UI worker side: Dockerfile gains `git` in the runtime image; nexus side: `js-yaml` added. ``` ## Security Multiple supply-chain attacks ruled out during review: - Resolver **rejects** `ssh://`, `file://`, `git://`, and plain `http://` — HTTPS only. - `git` is spawned via `spawn('git', args, ...)` (args array, no shell interpolation). - Skill names parsed from upstream `SKILL.md` frontmatter pass through `assertSafeSkillName` — no slashes, no `..`, no leading dot, max 255 chars. Without this guard a malicious public repo could ship a `name: "../agents/evil"` and write outside `skills/`. ## Testing 171 vitest cases pass (44 new). Coverage: - **Identifier resolver** — 16 cases (shorthand, URLs, refs, subpaths, blocked schemes). - **SKILL.md scanner** — 12 cases (single-skill, multi-skill, invalid frontmatter, depth, .git skip). - **Manifest** — 8 cases (round-trip, hashing, local-edit detection of mod/add/delete). - **Service** — 22 cases (preview, install, list, update, uninstall, sweep, lock concurrency, path-traversal rejection, EXDEV cross-volume, staging TTL). Fake `GitClient` materializes files on disk per test so manifest + hashing logic runs against a real fs. Suite stays under 5s offline, no real network. ## What's intentionally out of scope (v1) - Browse/UI for agents, commands, hooks, output-styles — only skills for now. The route prefix is `/assets/` so adding more asset types later is purely "add another scanner". - Private GitHub repos (no auth flow). - Auto-update (operator must click). - Merging upstream changes with local edits (current model: warn and overwrite). ## Known follow-ups (deferred from final review, not blockers) - `listInstalled` re-hashes every file on every call — fine until ~100 skills. - `checkUpdates` swallows per-asset errors silently — add `logger.warn`. - `updateAsset` rollback failure path is silent if both move + rollback fail. - Modal Cancel during in-flight preview doesn't abort the request (request continues; toast fires after modal closes). ## Test plan - [ ] `pnpm test` in `nexus/` — all 171 tests pass. - [ ] `docker compose up -d` — container boots; `agent-nexus ready` logged. - [ ] Open Config Explorer → Installed Assets panel renders empty on left. - [ ] Click `+ Install` → search "react" → click a result → toast confirms install. - [ ] Inspect on disk: `docker compose exec nexus ls /shared-config/dot-claude/skills/<name>/` shows `SKILL.md` + `.nexus-skill.json`. - [ ] Click `Check Updates` → status pill becomes "up to date". - [ ] Click `Update` → confirm → succeeds. - [ ] Manually edit `SKILL.md` via file tree → click `Update` → confirm dialog warns about local edits. - [ ] Click `Uninstall` → row + directory both gone. - [ ] Spawn a worker → `ls /root/.claude/skills/` shows the installed skills (cross-worker volume sanity).
lz added 24 commits 2026-05-15 19:42:00 +02:00
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(skills-installer): search-first browse UI in install modal
All checks were successful
ci / nexus (pull_request) Successful in 1m57s
ci / images (./nexus, agent-nexus) (pull_request) Successful in 3m51s
ci / images (./worker, nexus-worker) (pull_request) Successful in 47s
c92115ecb5
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lz merged commit c2cd231d5b into main 2026-05-15 19:59:51 +02:00
lz deleted branch feat/custom-branch 2026-05-15 19:59:57 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lz/agent-nexus!12
No description provided.