mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-22 07:02:16 +03:00
fix(skills): make marketplace installs available to API keys (#10854)
Dashboard-installed SkillsMP and skills.sh skills now store under the canonical global skill scope and merge into every API-key-scoped lookup, so marketplace installs actually reach API keys instead of staying invisible outside the installing session. Tenant-owned skill overrides stay isolated; existing skillsmp/skillssh rows are recognized without a migration, with canonical rows preferred on identity overlap. Closes #9716. Validated in an isolated worktree boarded onto origin/release/v3.8.50 (0 conflicts, 9 files): - 94/94 skills-*.test.ts tests pass, including the tenant-isolation regression coverage in skills-injection.test.ts (global skills reach a different API key without leaking another tenant's skills). - check-file-size, check-changelog-integrity: OK. - typecheck:core: clean. - check-complexity / check-cognitive-complexity: OK, both under baseline. Co-authored-by: kriptoburak <kriptoburak@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { skillRegistry } from "@/lib/skills/registry";
|
||||
import { GLOBAL_SKILL_OWNER_ID, skillRegistry } from "@/lib/skills/registry";
|
||||
import { validateBody, isValidationFailure } from "@/shared/validation/helpers";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
@@ -39,7 +39,7 @@ export async function POST(request: Request) {
|
||||
description,
|
||||
schema: { input: schema.input, output: schema.output },
|
||||
handler: handlerCode,
|
||||
apiKeyId: apiKeyId || "system",
|
||||
apiKeyId: apiKeyId || GLOBAL_SKILL_OWNER_ID,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { validateBody, isValidationFailure } from "@/shared/validation/helpers";
|
||||
import { skillRegistry } from "@/lib/skills/registry";
|
||||
import { GLOBAL_SKILL_OWNER_ID, skillRegistry } from "@/lib/skills/registry";
|
||||
import { getSkillsProviderSetting } from "@/lib/skills/providerSettings";
|
||||
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
@@ -44,7 +44,7 @@ export async function POST(request: Request) {
|
||||
description,
|
||||
schema: { input: { content: "string" }, output: { result: "string" } },
|
||||
handler: `// Installed from SkillsMP\n// SKILL.md content:\n${skillMdContent}`,
|
||||
apiKeyId: provider,
|
||||
apiKeyId: GLOBAL_SKILL_OWNER_ID,
|
||||
enabled: true,
|
||||
mode: "auto",
|
||||
sourceProvider: "skillsmp",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { validateBody, isValidationFailure } from "@/shared/validation/helpers";
|
||||
import { skillRegistry } from "@/lib/skills/registry";
|
||||
import { GLOBAL_SKILL_OWNER_ID, skillRegistry } from "@/lib/skills/registry";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import { fetchSkillMd } from "@/lib/skills/skillssh";
|
||||
import { getSkillsProviderSetting } from "@/lib/skills/providerSettings";
|
||||
@@ -46,7 +46,7 @@ export async function POST(request: Request) {
|
||||
description,
|
||||
schema: { input: { content: "string" }, output: { result: "string" } },
|
||||
handler: `// Installed from skills.sh\n// Source: ${source}/${skillId}\n// SKILL.md content:\n${skillMdContent}`,
|
||||
apiKeyId: provider,
|
||||
apiKeyId: GLOBAL_SKILL_OWNER_ID,
|
||||
enabled: true,
|
||||
mode: "auto",
|
||||
sourceProvider: "skillssh",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Skill, SkillSchema } from "./types";
|
||||
import type { Skill, SkillSchema } from "./types";
|
||||
import { SkillCreateInputSchema } from "./schemas";
|
||||
import { getDbInstance } from "../db/core";
|
||||
import { randomUUID } from "crypto";
|
||||
@@ -6,6 +6,10 @@ import { logger } from "../../../open-sse/utils/logger.ts";
|
||||
|
||||
const log = logger("SKILLS");
|
||||
|
||||
export const GLOBAL_SKILL_OWNER_ID = "system";
|
||||
const GLOBAL_SKILL_OWNER_IDS = [GLOBAL_SKILL_OWNER_ID, "skillsmp", "skillssh"] as const;
|
||||
const GLOBAL_SKILL_OWNER_ID_SET = new Set<string>(GLOBAL_SKILL_OWNER_IDS);
|
||||
|
||||
class SkillRegistry {
|
||||
private static instance: SkillRegistry;
|
||||
private registeredSkills: Map<string, Skill> = new Map();
|
||||
@@ -39,6 +43,36 @@ class SkillRegistry {
|
||||
return `${skill.apiKeyId}:${skill.name}@${skill.version}`;
|
||||
}
|
||||
|
||||
private skillIdentity(skill: Pick<Skill, "name" | "version">): string {
|
||||
return `${skill.name}@${skill.version}`;
|
||||
}
|
||||
|
||||
private isGlobalOwner(apiKeyId: string): boolean {
|
||||
return GLOBAL_SKILL_OWNER_ID_SET.has(apiKeyId);
|
||||
}
|
||||
|
||||
private scopedSkills(apiKeyId?: string): Skill[] {
|
||||
const skills = Array.from(this.registeredSkills.values());
|
||||
if (!apiKeyId) return skills;
|
||||
|
||||
const owned = skills.filter((skill) => skill.apiKeyId === apiKeyId);
|
||||
const visibleIdentities = new Set(owned.map((skill) => this.skillIdentity(skill)));
|
||||
const global = [
|
||||
...skills.filter((skill) => skill.apiKeyId === GLOBAL_SKILL_OWNER_ID),
|
||||
...skills.filter(
|
||||
(skill) => skill.apiKeyId !== GLOBAL_SKILL_OWNER_ID && this.isGlobalOwner(skill.apiKeyId)
|
||||
),
|
||||
];
|
||||
for (const skill of global) {
|
||||
const identity = this.skillIdentity(skill);
|
||||
if (!visibleIdentities.has(identity)) {
|
||||
owned.push(skill);
|
||||
visibleIdentities.add(identity);
|
||||
}
|
||||
}
|
||||
return owned;
|
||||
}
|
||||
|
||||
private cacheSkill(skill: Skill): void {
|
||||
this.registeredSkills.set(this.cacheKey(skill), skill);
|
||||
this.updateVersionCache(skill);
|
||||
@@ -180,16 +214,11 @@ class SkillRegistry {
|
||||
|
||||
list(apiKeyId?: string): Skill[] {
|
||||
log.debug("skills.registry.list", { apiKeyId, cached: !this.isCacheStale() });
|
||||
if (apiKeyId) {
|
||||
return Array.from(this.registeredSkills.values()).filter((s) => s.apiKeyId === apiKeyId);
|
||||
}
|
||||
return Array.from(this.registeredSkills.values());
|
||||
return this.scopedSkills(apiKeyId);
|
||||
}
|
||||
|
||||
getSkill(identifier: string, apiKeyId?: string): Skill | undefined {
|
||||
const matchesScope = (skill: Skill) => !apiKeyId || skill.apiKeyId === apiKeyId;
|
||||
const skills = Array.from(this.registeredSkills.values()).filter(matchesScope);
|
||||
|
||||
const skills = this.scopedSkills(apiKeyId);
|
||||
const byId = skills.find((skill) => skill.id === identifier);
|
||||
if (byId) return byId;
|
||||
|
||||
@@ -206,8 +235,8 @@ class SkillRegistry {
|
||||
}
|
||||
|
||||
getSkillVersions(name: string, apiKeyId?: string): Skill[] {
|
||||
return Array.from(this.registeredSkills.values())
|
||||
.filter((skill) => skill.name === name && (!apiKeyId || skill.apiKeyId === apiKeyId))
|
||||
return this.scopedSkills(apiKeyId)
|
||||
.filter((skill) => skill.name === name)
|
||||
.sort((a, b) => this.compareVersions(b.version, a.version));
|
||||
}
|
||||
|
||||
@@ -304,12 +333,23 @@ class SkillRegistry {
|
||||
try {
|
||||
log.debug("skills.registry.loadFromDatabase", { cached: false });
|
||||
const db = getDbInstance();
|
||||
const rows = apiKeyId
|
||||
? db.prepare("SELECT * FROM skills WHERE api_key_id = ?").all(apiKeyId)
|
||||
: db.prepare("SELECT * FROM skills").all();
|
||||
let rows: unknown[];
|
||||
if (!apiKeyId) {
|
||||
rows = db.prepare("SELECT * FROM skills").all();
|
||||
} else if (this.isGlobalOwner(apiKeyId)) {
|
||||
rows = db
|
||||
.prepare("SELECT * FROM skills WHERE api_key_id IN (?, ?, ?)")
|
||||
.all(...GLOBAL_SKILL_OWNER_IDS);
|
||||
} else {
|
||||
rows = db
|
||||
.prepare("SELECT * FROM skills WHERE api_key_id IN (?, ?, ?, ?)")
|
||||
.all(apiKeyId, ...GLOBAL_SKILL_OWNER_IDS);
|
||||
}
|
||||
|
||||
if (apiKeyId) {
|
||||
this.removeCachedSkills((skill) => skill.apiKeyId === apiKeyId);
|
||||
this.removeCachedSkills(
|
||||
(skill) => skill.apiKeyId === apiKeyId || this.isGlobalOwner(skill.apiKeyId)
|
||||
);
|
||||
} else {
|
||||
this.registeredSkills.clear();
|
||||
this.versionCache.clear();
|
||||
|
||||
Reference in New Issue
Block a user