Namespace company skill identities

Persist canonical namespaced skill keys, split adapter runtime names from skill keys, and update portability/import flows to carry the canonical identity end-to-end.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta
2026-03-16 18:27:20 -05:00
parent bb46423969
commit 5890b318c4
39 changed files with 9902 additions and 309 deletions

View File

@@ -99,7 +99,7 @@ async function isLikelyPaperclipRuntimeSkillSource(candidate: string, skillName:
type EnsureCodexSkillsInjectedOptions = {
skillsHome?: string;
skillsEntries?: Array<{ name: string; source: string }>;
skillsEntries?: Array<{ key: string; runtimeName: string; source: string }>;
desiredSkillNames?: string[];
linkSkill?: (source: string, target: string) => Promise<void>;
};
@@ -110,16 +110,16 @@ export async function ensureCodexSkillsInjected(
) {
const allSkillsEntries = options.skillsEntries ?? await readPaperclipRuntimeSkillEntries({}, __moduleDir);
const desiredSkillNames =
options.desiredSkillNames ?? allSkillsEntries.map((entry) => entry.name);
options.desiredSkillNames ?? allSkillsEntries.map((entry) => entry.key);
const desiredSet = new Set(desiredSkillNames);
const skillsEntries = allSkillsEntries.filter((entry) => desiredSet.has(entry.name));
const skillsEntries = allSkillsEntries.filter((entry) => desiredSet.has(entry.key));
if (skillsEntries.length === 0) return;
const skillsHome = options.skillsHome ?? path.join(resolveCodexHomeDir(process.env), "skills");
await fs.mkdir(skillsHome, { recursive: true });
const removedSkills = await removeMaintainerOnlySkillSymlinks(
skillsHome,
skillsEntries.map((entry) => entry.name),
skillsEntries.map((entry) => entry.runtimeName),
);
for (const skillName of removedSkills) {
await onLog(
@@ -129,7 +129,7 @@ export async function ensureCodexSkillsInjected(
}
const linkSkill = options.linkSkill;
for (const entry of skillsEntries) {
const target = path.join(skillsHome, entry.name);
const target = path.join(skillsHome, entry.runtimeName);
try {
const existing = await fs.lstat(target).catch(() => null);
@@ -141,7 +141,7 @@ export async function ensureCodexSkillsInjected(
if (
resolvedLinkedPath &&
resolvedLinkedPath !== entry.source &&
(await isLikelyPaperclipRuntimeSkillSource(resolvedLinkedPath, entry.name))
(await isLikelyPaperclipRuntimeSkillSource(resolvedLinkedPath, entry.runtimeName))
) {
await fs.unlink(target);
if (linkSkill) {
@@ -151,7 +151,7 @@ export async function ensureCodexSkillsInjected(
}
await onLog(
"stderr",
`[paperclip] Repaired Codex skill "${entry.name}" into ${skillsHome}\n`,
`[paperclip] Repaired Codex skill "${entry.key}" into ${skillsHome}\n`,
);
continue;
}
@@ -162,12 +162,12 @@ export async function ensureCodexSkillsInjected(
await onLog(
"stderr",
`[paperclip] ${result === "repaired" ? "Repaired" : "Injected"} Codex skill "${entry.name}" into ${skillsHome}\n`,
`[paperclip] ${result === "repaired" ? "Repaired" : "Injected"} Codex skill "${entry.key}" into ${skillsHome}\n`,
);
} catch (err) {
await onLog(
"stderr",
`[paperclip] Failed to inject Codex skill "${entry.name}" into ${skillsHome}: ${err instanceof Error ? err.message : String(err)}\n`,
`[paperclip] Failed to inject Codex skill "${entry.key}" into ${skillsHome}: ${err instanceof Error ? err.message : String(err)}\n`,
);
}
}

View File

@@ -53,7 +53,7 @@ async function readInstalledSkillTargets(skillsHome: string) {
async function buildCodexSkillSnapshot(config: Record<string, unknown>): Promise<AdapterSkillSnapshot> {
const availableEntries = await readPaperclipRuntimeSkillEntries(config, __moduleDir);
const availableByName = new Map(availableEntries.map((entry) => [entry.name, entry]));
const availableByKey = new Map(availableEntries.map((entry) => [entry.key, entry]));
const desiredSkills = resolvePaperclipDesiredSkillNames(config, availableEntries);
const desiredSet = new Set(desiredSkills);
const skillsHome = resolveCodexSkillsHome(config);
@@ -62,8 +62,8 @@ async function buildCodexSkillSnapshot(config: Record<string, unknown>): Promise
const warnings: string[] = [];
for (const available of availableEntries) {
const installedEntry = installed.get(available.name) ?? null;
const desired = desiredSet.has(available.name);
const installedEntry = installed.get(available.runtimeName) ?? null;
const desired = desiredSet.has(available.key);
let state: AdapterSkillEntry["state"] = "available";
let managed = false;
let detail: string | null = null;
@@ -82,12 +82,13 @@ async function buildCodexSkillSnapshot(config: Record<string, unknown>): Promise
}
entries.push({
name: available.name,
key: available.key,
runtimeName: available.runtimeName,
desired,
managed,
state,
sourcePath: available.source,
targetPath: path.join(skillsHome, available.name),
targetPath: path.join(skillsHome, available.runtimeName),
detail,
required: Boolean(available.required),
requiredReason: available.requiredReason ?? null,
@@ -95,23 +96,25 @@ async function buildCodexSkillSnapshot(config: Record<string, unknown>): Promise
}
for (const desiredSkill of desiredSkills) {
if (availableByName.has(desiredSkill)) continue;
if (availableByKey.has(desiredSkill)) continue;
warnings.push(`Desired skill "${desiredSkill}" is not available from the Paperclip skills directory.`);
entries.push({
name: desiredSkill,
key: desiredSkill,
runtimeName: null,
desired: true,
managed: true,
state: "missing",
sourcePath: null,
targetPath: path.join(skillsHome, desiredSkill),
targetPath: null,
detail: "Paperclip cannot find this skill in the local runtime skills directory.",
});
}
for (const [name, installedEntry] of installed.entries()) {
if (availableByName.has(name)) continue;
if (availableEntries.some((entry) => entry.runtimeName === name)) continue;
entries.push({
name,
key: name,
runtimeName: name,
desired: false,
managed: false,
state: "external",
@@ -121,7 +124,7 @@ async function buildCodexSkillSnapshot(config: Record<string, unknown>): Promise
});
}
entries.sort((left, right) => left.name.localeCompare(right.name));
entries.sort((left, right) => left.key.localeCompare(right.key));
return {
adapterType: "codex_local",
@@ -144,23 +147,23 @@ export async function syncCodexSkills(
const availableEntries = await readPaperclipRuntimeSkillEntries(ctx.config, __moduleDir);
const desiredSet = new Set([
...desiredSkills,
...availableEntries.filter((entry) => entry.required).map((entry) => entry.name),
...availableEntries.filter((entry) => entry.required).map((entry) => entry.key),
]);
const skillsHome = resolveCodexSkillsHome(ctx.config);
await fs.mkdir(skillsHome, { recursive: true });
const installed = await readInstalledSkillTargets(skillsHome);
const availableByName = new Map(availableEntries.map((entry) => [entry.name, entry]));
const availableByRuntimeName = new Map(availableEntries.map((entry) => [entry.runtimeName, entry]));
for (const available of availableEntries) {
if (!desiredSet.has(available.name)) continue;
const target = path.join(skillsHome, available.name);
if (!desiredSet.has(available.key)) continue;
const target = path.join(skillsHome, available.runtimeName);
await ensurePaperclipSkillSymlink(available.source, target);
}
for (const [name, installedEntry] of installed.entries()) {
const available = availableByName.get(name);
const available = availableByRuntimeName.get(name);
if (!available) continue;
if (desiredSet.has(name)) continue;
if (desiredSet.has(available.key)) continue;
if (installedEntry.targetPath !== available.source) continue;
await fs.unlink(path.join(skillsHome, name)).catch(() => {});
}
@@ -170,7 +173,7 @@ export async function syncCodexSkills(
export function resolveCodexDesiredSkillNames(
config: Record<string, unknown>,
availableEntries: Array<{ name: string; required?: boolean }>,
availableEntries: Array<{ key: string; required?: boolean }>,
) {
return resolvePaperclipDesiredSkillNames(config, availableEntries);
}