Add unmanaged skill provenance to agent skills

Expose adapter-discovered user-installed skills with provenance metadata, share persistent skill snapshot classification across local adapters, and render unmanaged skills as a read-only section in the agent skills UI.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta
2026-03-18 14:21:50 -05:00
parent 58d7f59477
commit cfc53bf96b
19 changed files with 497 additions and 501 deletions

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { applyAgentSkillSnapshot } from "./agent-skills-state";
import { applyAgentSkillSnapshot, isReadOnlyUnmanagedSkillEntry } from "./agent-skills-state";
describe("applyAgentSkillSnapshot", () => {
it("hydrates the initial snapshot without arming autosave", () => {
@@ -55,4 +55,36 @@ describe("applyAgentSkillSnapshot", () => {
shouldSkipAutosave: true,
});
});
it("treats user-installed entries outside the company library as read-only unmanaged skills", () => {
expect(isReadOnlyUnmanagedSkillEntry({
key: "crack-python",
runtimeName: "crack-python",
desired: false,
managed: false,
state: "external",
origin: "user_installed",
}, new Set(["paperclip"]))).toBe(true);
});
it("keeps company-library entries in the managed section even when the adapter reports an external conflict", () => {
expect(isReadOnlyUnmanagedSkillEntry({
key: "paperclip",
runtimeName: "paperclip",
desired: true,
managed: false,
state: "external",
origin: "company_managed",
}, new Set(["paperclip"]))).toBe(false);
});
it("falls back to legacy snapshots that only mark unmanaged external entries", () => {
expect(isReadOnlyUnmanagedSkillEntry({
key: "legacy-external",
runtimeName: "legacy-external",
desired: false,
managed: false,
state: "external",
}, new Set())).toBe(true);
});
});

View File

@@ -1,3 +1,5 @@
import type { AgentSkillEntry } from "@paperclipai/shared";
export interface AgentSkillDraftState {
draft: string[];
lastSaved: string[];
@@ -27,3 +29,12 @@ export function applyAgentSkillSnapshot(
shouldSkipAutosave: shouldReplaceDraft,
};
}
export function isReadOnlyUnmanagedSkillEntry(
entry: AgentSkillEntry,
companySkillKeys: Set<string>,
): boolean {
if (companySkillKeys.has(entry.key)) return false;
if (entry.origin === "user_installed" || entry.origin === "external_unknown") return true;
return entry.managed === false && entry.state === "external";
}