diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md index 7b07159f3b3..ea0ce2dad48 100644 --- a/doc/DEVELOPING.md +++ b/doc/DEVELOPING.md @@ -1895,6 +1895,10 @@ automatically alongside it; see [Agent commentary](agent-commentary.md). Other runtime skills remain controlled by `paperclipSkillSync.desiredSkills`. The native `paperclip_runner` does not use this legacy default because its protocol supplies the control-plane contract. +An empty native runtime selection returns before company skill inventory work. +An explicit empty stored key set does the same. Nonempty selections still use +the current inventory, version and access checks. Legacy automatic selections +still resolve their operational and feedback skills. Validate the catalog without writing the manifest: diff --git a/server/src/__tests__/company-skills-empty-runtime.test.ts b/server/src/__tests__/company-skills-empty-runtime.test.ts new file mode 100644 index 00000000000..369546e4e40 --- /dev/null +++ b/server/src/__tests__/company-skills-empty-runtime.test.ts @@ -0,0 +1,27 @@ +import { randomUUID } from "node:crypto"; +import { expect, it } from "vitest"; +import type { Db } from "@paperclipai/db"; +import { companySkillService } from "../services/company-skills.js"; + +const databaseAccess = new Error("Runtime selection touched the inventory database"); +const db = new Proxy({} as Db, { get() { throw databaseAccess; } }); + +it.each([ + { onlyKeys: new Set() }, + { onlyKeys: new Set(), includeLegacy: true, selectionConfig: { paperclipSkillSync: { desiredSkills: ["selected"] } } }, + { selectionConfig: {} }, + { selectionConfig: { paperclipSkillSync: { desiredSkills: [] } } }, + { selectionConfig: { paperclipSkillSync: { desiredSkills: [" ", null, {}] } } }, +])("does no inventory work for an empty runtime selection: %o", async options => { + await expect(companySkillService(db).listRuntimeSkillEntries(randomUUID(), options)).resolves.toEqual([]); +}); + +it.each([ + {}, + { includeLegacy: true, selectionConfig: {} }, + { includeLegacy: true, selectionConfig: { paperclipSkillSync: { desiredSkills: [] } } }, + { selectionConfig: { paperclipSkillSync: { desiredSkills: ["selected"] } } }, + { onlyKeys: new Set(["selected"]), selectionConfig: {} }, +])("retains inventory resolution for a requested or legacy skill set: %o", async options => { + await expect(companySkillService(db).listRuntimeSkillEntries(randomUUID(), options)).rejects.toBe(databaseAccess); +}); diff --git a/server/src/services/company-skills.ts b/server/src/services/company-skills.ts index c6da9f8bb86..0c4f5df645d 100644 --- a/server/src/services/company-skills.ts +++ b/server/src/services/company-skills.ts @@ -6143,6 +6143,8 @@ export function companySkillService(db: Db, options: { actor?: AuthorizationActo companyId: string, options: RuntimeSkillEntryOptions = {}, ): Promise { + if (options.onlyKeys?.size === 0 || (!options.onlyKeys && options.selectionConfig && !options.includeLegacy + && readPaperclipSkillSyncPreference(options.selectionConfig).desiredSkills.length === 0)) return []; const skills = await listFull(companyId); const selectedKeys = options.onlyKeys ?? (options.selectionConfig ? new Set( (options.includeLegacy ? resolveLegacyPaperclipDesiredSkillNames : resolvePaperclipDesiredSkillNames)(