mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
* fix(skills): gate skill-collector CLI detection behind management auth + loopback PR #6294 fork-main bundled genuinely new skill-collector CLI-detection routes (GET /api/skills/collect/detect, POST /api/skills/collect/install) on top of content already shipped via #6186. This reconstructs the PR against the current release tip, keeping only the new detect/install routes and their SKILL.md, and drops the 3 already-merged commits so two post-merge quality fixes on /api/github-skills (Zod validation + sanitizeErrorMessage) are not reverted. - GET /api/skills/collect/detect spawned a child process per CLI_TOOL_IDS entry via getCliRuntimeStatus(), unauthenticated and reachable over any tunnel. All 3 routes (github-skills GET/POST, skills/collect/detect, skills/collect/install) now require requireManagementAuth(), matching every sibling /api/skills/* route. - Classified /api/skills/collect/ in LOCAL_ONLY_API_PREFIXES and SPAWN_CAPABLE_PREFIXES (routeGuard.ts / spawnCapablePrefixes.ts) and added src/app/api/skills/collect to SPAWN_CAPABLE_ROUTE_ROOTS in check-route-guard-membership.ts so the automated gate actually scans it (Hard Rules #15 + #17). - omniroute_github_skills_install MCP tool now reports the honest action: "planned" instead of "installed", matching the REST route. - Dropped docker-compose.drive-d.yml, start.sh, and the unrelated @types/node/settings.ts changes (personal dev-machine / out-of-scope). - Added route-level tests for all 3 routes + the 3 MCP tools (auth-required and no-stack-trace-leak assertions) and a route-guard regression test. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(quality): register new routeGuard covering test in stryker.conf.json check:mutation-test-coverage --strict (Fast Quality Gates) flagged tests/unit/authz/route-guard-skills-collect.test.ts as a covering unit test for src/server/authz/routeGuard.ts that was missing from tap.testFiles, so its mutant kills would silently not count toward the mutation-test baseline. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * chore: resync CHANGELOG after merging release/v3.8.47 Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: Moseyuh333 <Moseyuh333@users.noreply.github.com>
144 lines
5.0 KiB
TypeScript
144 lines
5.0 KiB
TypeScript
/**
|
||
* githubSkillTools.ts — MCP tools for GitHub agent skill discovery and import.
|
||
*
|
||
* Provides tools to:
|
||
* - Search GitHub for repos with SKILL.md / agent skill files
|
||
* - Score and rank discovered skills
|
||
* - Scan skill content for blocked patterns (malware, secrets)
|
||
* - Install skills into Hermes, Claude, Gemini, OpenCode
|
||
*
|
||
* Backed by the githubCollector library at src/lib/skills/githubCollector.ts.
|
||
*/
|
||
|
||
import { z } from "zod";
|
||
import { sanitizeErrorMessage } from "../../utils/error.ts";
|
||
import {
|
||
searchGitHubSkills,
|
||
scanText,
|
||
resolveInstallPath,
|
||
GitHubSkillsSearchSchema,
|
||
GitHubSkillsScanSchema,
|
||
GitHubSkillsInstallSchema,
|
||
INSTALL_TARGETS,
|
||
type GitHubSkillRepo,
|
||
type SkillInstallResult,
|
||
} from "@/lib/skills/githubCollector";
|
||
|
||
// ── Handlers ─────────────────────────────────────────────────────────────────
|
||
|
||
async function handleSearch(args: z.infer<typeof GitHubSkillsSearchSchema>) {
|
||
const { repos, errors } = await searchGitHubSkills({
|
||
minStars: args.minStars,
|
||
maxResults: args.maxResults,
|
||
});
|
||
|
||
let filtered = repos;
|
||
if (args.minScore > 0) filtered = filtered.filter((r) => r.score >= args.minScore);
|
||
if (args.query) {
|
||
const q = args.query.toLowerCase();
|
||
filtered = filtered.filter(
|
||
(r) => r.fullName.toLowerCase().includes(q) || r.description.toLowerCase().includes(q)
|
||
);
|
||
}
|
||
|
||
return {
|
||
skills: filtered.map((r: GitHubSkillRepo) => ({
|
||
fullName: r.fullName,
|
||
stars: r.stars,
|
||
score: r.score,
|
||
description: r.description.slice(0, 200),
|
||
topics: r.topics,
|
||
htmlUrl: r.htmlUrl,
|
||
hasSkillFile: r.hasSkillFile,
|
||
license: r.license,
|
||
})),
|
||
total: filtered.length,
|
||
errors: errors.length > 0 ? errors : undefined,
|
||
};
|
||
}
|
||
|
||
async function handleScan(args: z.infer<typeof GitHubSkillsScanSchema>) {
|
||
const findings = scanText(args.content, args.repoName);
|
||
return {
|
||
repoName: args.repoName,
|
||
clean: findings.length === 0,
|
||
findings: findings.map((f) => ({
|
||
pattern: f.pattern,
|
||
context: f.context,
|
||
})),
|
||
};
|
||
}
|
||
|
||
async function handleInstall(args: z.infer<typeof GitHubSkillsInstallSchema>) {
|
||
const results: SkillInstallResult[] = [];
|
||
const skillName = args.repoName.split("/").pop() || args.repoName;
|
||
|
||
for (const target of args.targets) {
|
||
try {
|
||
const dest = resolveInstallPath(target, skillName, args.description);
|
||
// In a real implementation, this would clone the repo and copy files.
|
||
// For now, we return the planned install path as a dry-run result — matches
|
||
// the honest `action: "planned"` the REST route (/api/github-skills POST)
|
||
// reports for the same operation.
|
||
results.push({
|
||
target,
|
||
ok: true,
|
||
action: "planned",
|
||
destDir: dest,
|
||
});
|
||
} catch (err) {
|
||
results.push({
|
||
target,
|
||
ok: false,
|
||
action: "error",
|
||
error: sanitizeErrorMessage((err as Error).message),
|
||
});
|
||
}
|
||
}
|
||
|
||
return {
|
||
repoName: args.repoName,
|
||
skillName,
|
||
results,
|
||
allOk: results.every((r) => r.ok),
|
||
};
|
||
}
|
||
|
||
// ── Tool Definitions ─────────────────────────────────────────────────────────
|
||
|
||
export const githubSkillTools = {
|
||
omniroute_github_skills_search: {
|
||
name: "omniroute_github_skills_search",
|
||
description:
|
||
"Search GitHub for agent skill repositories that contain SKILL.md, CLAUDE.md, .cursorrules, or similar agent configuration files. " +
|
||
"Returns scored results sorted by relevance. Scores are 0.0–1.0 based on stars, name signals, description keywords, and topic tags. " +
|
||
"Ideal for discovering community agent skills from GitHub.",
|
||
inputSchema: GitHubSkillsSearchSchema,
|
||
scopes: ["read:skills"],
|
||
handler: handleSearch,
|
||
},
|
||
|
||
omniroute_github_skills_scan: {
|
||
name: "omniroute_github_skills_scan",
|
||
description:
|
||
"Scan SKILL.md or README content from a GitHub repo for blocked patterns including eval(base64), " +
|
||
"hardcoded secrets (API keys, passwords, private keys), dangerous function calls (os.system, subprocess.Popen), " +
|
||
"and other malware indicators. Returns findings with context or 'clean' status.",
|
||
inputSchema: GitHubSkillsScanSchema,
|
||
scopes: ["read:skills"],
|
||
handler: handleScan,
|
||
},
|
||
|
||
omniroute_github_skills_install: {
|
||
name: "omniroute_github_skills_install",
|
||
description:
|
||
"Preview or plan the installation of a discovered GitHub skill into one or more agent directories " +
|
||
"(Hermes: ~/AppData/Local/hermes/skills/, Claude: ~/.claude/skills/, Gemini: ~/.gemini/skills/, " +
|
||
"OpenCode: ~/.opencode/skills/). Categorizes the skill based on its name and description. " +
|
||
"Returns the target paths where the skill would be installed.",
|
||
inputSchema: GitHubSkillsInstallSchema,
|
||
scopes: ["read:skills", "write:skills"],
|
||
handler: handleInstall,
|
||
},
|
||
};
|