feat(mosaic): add secure skill registration CLI (#826)
All checks were successful
ci/woodpecker/push/ci-image Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful

This commit was merged in pull request #826.
This commit is contained in:
2026-07-17 23:45:35 +00:00
parent d3bf52898b
commit d801d6c4c8
18 changed files with 1240 additions and 26 deletions

View File

@@ -0,0 +1,112 @@
# Issue #824 — Mosaic skill CLI and Claude bridge auto-sync
## Objective
Deliver `mosaic skill register|unregister|list` plus install/upgrade reconciliation of every canonical `~/.config/mosaic/skills/*` entry into `~/.claude/skills/`, without clobbering runtime-owned files or directories.
## Scope and constraints
- Issue: mosaicstack/stack#824
- Branch: `feat/824-mosaic-skill-cli`
- M1 runtime: Claude Code only.
- Pi/Codex parity is documentation-only; no non-Claude bridge implementation.
- Do not author the downstream `mosaic-context-refresh` skill.
- Workers do not modify `docs/TASKS.md`, merge, close #824, or touch `main`.
- TDD is mandatory and red-first; filesystem tests use temporary directories only.
- Budget: no explicit token cap supplied; use a focused single-worker implementation with no new dependencies.
## Requirements mapping
1. Register creates the canonical Claude symlink and is idempotent.
2. Names are untrusted: reject empty/escaping/absolute/separator/`..`/leading-dash names before filesystem mutation, with clear CLI stderr and nonzero status.
3. Register repairs only Mosaic-owned dangling symlinks and refuses foreign files, directories, and symlinks.
4. Unregister removes only symlinks pointing inside the canonical Mosaic skills root and is idempotent when absent.
5. List reports registered, dangling, foreign, and canonical-but-unregistered skills.
6. Install and upgrade generically reconcile all canonical skills after framework sync/re-seed, continuing past foreign conflicts without clobbering them.
7. User/developer documentation describes commands, status meanings, security boundaries, and Claude-only M1 scope.
## Plan
1. Add co-located failing Vitest coverage for all filesystem behaviors and auto-sync.
2. Run the focused spec and record the expected RED failure.
3. Commit the red contract as `test(#824): ...`.
4. Implement the skill bridge and Commander command registration.
5. Wire reconciliation into wizard finalize and `mosaic update` re-seed, preserving non-clobber behavior.
6. Update canonical docs and sitemap if navigation changes.
7. Run focused tests, package tests, typecheck, lint, and formatting.
8. Commit implementation/docs as `feat(#824): ...`, queue-guard, push, open PR with `Closes #824.`, fire completion event, and notify the coordinator.
## Progress
- 2026-07-17: Loaded mission/delivery/TDD/documentation rails, issue #824, active mission state, and relevant installer/update paths.
- 2026-07-17: Confirmed `mosaic update` invokes `framework/install.sh` with `MOSAIC_SYNC_ONLY=1`; that path exits before existing post-install skill linking, leaving newly present canonical skills unregistered.
- 2026-07-17: Coordinator addendum classified the user-supplied skill name and runtime symlink target as a path-traversal/symlink-injection surface. Expanded the initial red contract to reject traversal before mutation, preserve every foreign entry, and unregister Mosaic-owned links only.
- 2026-07-17: Implemented the Commander command group and secure generic bridge; wired wizard finalize and successful framework re-seed reconciliation; updated user/developer/installed/root docs and sitemap.
- 2026-07-17: Focused, package-wide, repository baseline, temp-home situational, and independent review gates completed. Ready for scoped feature commit, queue guard, push, and PR handoff.
## Tests and evidence
### TDD evidence
- RED environment attempt: `pnpm --filter @mosaicstack/mosaic exec vitest run src/commands/skill.spec.ts` initially could not locate Vitest because this fresh worktree had no dependencies.
- Dependency setup: `pnpm install --frozen-lockfile --store-dir /home/hermes/.local/share/pnpm/store` succeeded. The explicit store was required because machine pnpm config incorrectly resolves the default store under `/root`.
- RED behavior: focused Vitest failed with `Failed to load url ./skill.js ... Does the file exist?`, proving the bridge API was absent.
- RED integration: finalize/update specs failed because no Claude links or `skillSync` result existed.
- RED symlink injection: symlinked Claude/canonical root tests failed because the initial implementation followed ancestor links.
- GREEN after review remediation: `skill.spec.ts` 36/36, `finalize-skills.spec.ts` 6/6, and `update-checker.reseed.spec.ts` 30/30.
### Baseline gates
- `pnpm --filter '@mosaicstack/mosaic...' run build` — pass (fresh-worktree dependency outputs built).
- `pnpm --filter @mosaicstack/mosaic run typecheck` — pass.
- `pnpm --filter @mosaicstack/mosaic run lint` — pass.
- `pnpm --filter @mosaicstack/mosaic test` — pass: 69 files, 1,325 Vitest tests plus framework shell suite.
- `pnpm typecheck` — pass: 42/42 Turbo tasks.
- `pnpm lint` — pass: 23/23 Turbo tasks.
- `pnpm format:check` — pass.
### Situational evidence
A built-CLI temp-home smoke test (no real `~/.claude` or Mosaic config touched) proved:
- register creates the exact link and a second run reports `already registered`;
- list reports registered and unregistered canonical skills;
- `../../etc` exits 1 with `Invalid skill name` and creates no escaped path;
- unregister removes the managed link and a second run reports `already unregistered`;
- a fake successful framework re-seed generically registered both `added-after-setup` and `second-skill` from runtime directory enumeration.
### Review evidence
- Initial uncommitted Codex code/security review described name validation/clobber protection as strong; its only finding was the harness-owned, unrelated `.mosaic/orchestrator/session.lock`, which is excluded from all commits and the PR.
- Exact branch review then identified two remediations: preserve successful framework re-seed status when bridge-wide reconciliation fails, and reject/escape control-character names to prevent terminal/log injection.
- Both findings were reproduced red-first and remediated. A subsequent exact review identified one finalize failure-isolation blocker; a root-wide bridge error now warns and allows wizard doctor/summary/next-steps completion, with a red-first regression.
- All remediations passed the full package and repository gates. Final exact-head review is rerun after amending the feature commit.
### Acceptance mapping
| Acceptance criterion | Evidence |
| --- | --- |
| register/unregister/list, idempotent | `skill.spec.ts` and built-CLI temp-home smoke |
| traversal/symlink-injection protection | invalid-name matrix, foreign file/dir/link tests, symlinked-root tests |
| list flags dangling and foreign entries | deterministic list status test |
| install and upgrade auto-sync every canonical directory | finalize + framework re-seed integration specs; two-skill built-module smoke |
| newly added skill becomes discoverable without manual link | `added-after-setup` auto-sync creates exact Claude link; Claude can rescan with `/reload-skills` or a new session |
| Pi/Codex parity captured as scope note | user guide, developer guide, installed framework README |
| documentation gate | root README, user guide, developer guide, framework README, sitemap |
## Risks
- Symlink replacement uses `lstat` semantics so dangling links are detectable without following them.
- Link ownership is determined lexically against the canonical skills root, and existing symlink ancestors in either managed root are rejected before mutation.
- Auto-sync continues across per-skill conflicts while never deleting real files/directories or foreign symlinks.
- Claude Code discovers filesystem skills at session launch/reload boundaries; bridge creation makes a later `/reload-skills` or new session able to discover the skill, but cannot mutate an already-cached in-process registry by itself.
- Pi does not need this Claude bridge because its Mosaic launcher can consume the canonical root. Codex lifecycle parity remains explicitly deferred.
- No deployment surface is affected.
## PR #826 review remediation
- 2026-07-17: Exact-head RoR requested changes for two ownership bugs: installer pruning deleted foreign-name links under `MOSAIC_HOME` outside canonical skills, and unregister deleted a same-root link targeting a different skill. It also requested trailing-dot rejection and executable coverage support.
- RED evidence: focused regression run failed 4 tests: register/unregister accepted `safe.`, misdirected unregister did not throw, and the install linker deleted the foreign-name link.
- GREEN evidence: `skill.spec.ts` passes 43/43, including live and dangling foreign-name links in a temp HOME/MOSAIC_HOME and the misdirected unregister invariant.
- Coverage: `vitest run src/commands/skill.spec.ts --coverage` passes configured 85% thresholds for `skill.ts`: 91.05% statements/lines, 86.27% branches, 95.23% functions.
- Full gates: package build passed; package tests passed 69 files / 1,332 tests plus framework shell suite; repository typecheck 42/42, lint 23/23, and format check passed.