Add adapter skill sync for codex and claude

This commit is contained in:
Dotta 2026-03-13 22:49:42 -05:00
parent 271c2b9018
commit 56a34a8f8a
22 changed files with 907 additions and 26 deletions

View file

@ -5,6 +5,7 @@ import type { Db } from "@paperclipai/db";
import { agents as agentsTable, companies, heartbeatRuns } from "@paperclipai/db";
import { and, desc, eq, inArray, not, sql } from "drizzle-orm";
import {
agentSkillSyncSchema,
createAgentKeySchema,
createAgentHireSchema,
createAgentSchema,
@ -12,12 +13,17 @@ import {
isUuidLike,
resetAgentSessionSchema,
testAdapterEnvironmentSchema,
type AgentSkillSnapshot,
type InstanceSchedulerHeartbeatAgent,
updateAgentPermissionsSchema,
updateAgentInstructionsPathSchema,
wakeAgentSchema,
updateAgentSchema,
} from "@paperclipai/shared";
import {
readPaperclipSkillSyncPreference,
writePaperclipSkillSyncPreference,
} from "@paperclipai/adapter-utils/server-utils";
import { validate } from "../middleware/validate.js";
import {
agentService,
@ -334,6 +340,20 @@ export function agentRoutes(db: Db) {
return details;
}
function buildUnsupportedSkillSnapshot(
adapterType: string,
desiredSkills: string[] = [],
): AgentSkillSnapshot {
return {
adapterType,
supported: false,
mode: "unsupported",
desiredSkills,
entries: [],
warnings: ["This adapter does not implement skill sync yet."],
};
}
function redactForRestrictedAgentView(agent: Awaited<ReturnType<typeof svc.getById>>) {
if (!agent) return null;
return {
@ -459,6 +479,119 @@ export function agentRoutes(db: Db) {
},
);
router.get("/agents/:id/skills", async (req, res) => {
const id = req.params.id as string;
const agent = await svc.getById(id);
if (!agent) {
res.status(404).json({ error: "Agent not found" });
return;
}
await assertCanReadConfigurations(req, agent.companyId);
const adapter = findServerAdapter(agent.adapterType);
if (!adapter?.listSkills) {
const preference = readPaperclipSkillSyncPreference(
agent.adapterConfig as Record<string, unknown>,
);
res.json(buildUnsupportedSkillSnapshot(agent.adapterType, preference.desiredSkills));
return;
}
const { config: runtimeConfig } = await secretsSvc.resolveAdapterConfigForRuntime(
agent.companyId,
agent.adapterConfig,
);
const snapshot = await adapter.listSkills({
agentId: agent.id,
companyId: agent.companyId,
adapterType: agent.adapterType,
config: runtimeConfig,
});
res.json(snapshot);
});
router.post(
"/agents/:id/skills/sync",
validate(agentSkillSyncSchema),
async (req, res) => {
const id = req.params.id as string;
const agent = await svc.getById(id);
if (!agent) {
res.status(404).json({ error: "Agent not found" });
return;
}
await assertCanUpdateAgent(req, agent);
const desiredSkills = Array.from(
new Set(
(req.body.desiredSkills as string[])
.map((value) => value.trim())
.filter(Boolean),
),
);
const nextAdapterConfig = writePaperclipSkillSyncPreference(
agent.adapterConfig as Record<string, unknown>,
desiredSkills,
);
const actor = getActorInfo(req);
const updated = await svc.update(agent.id, {
adapterConfig: nextAdapterConfig,
}, {
recordRevision: {
createdByAgentId: actor.agentId,
createdByUserId: actor.actorType === "user" ? actor.actorId : null,
source: "skill-sync",
},
});
if (!updated) {
res.status(404).json({ error: "Agent not found" });
return;
}
const adapter = findServerAdapter(updated.adapterType);
const { config: runtimeConfig } = await secretsSvc.resolveAdapterConfigForRuntime(
updated.companyId,
updated.adapterConfig,
);
const snapshot = adapter?.syncSkills
? await adapter.syncSkills({
agentId: updated.id,
companyId: updated.companyId,
adapterType: updated.adapterType,
config: runtimeConfig,
}, desiredSkills)
: adapter?.listSkills
? await adapter.listSkills({
agentId: updated.id,
companyId: updated.companyId,
adapterType: updated.adapterType,
config: runtimeConfig,
})
: buildUnsupportedSkillSnapshot(updated.adapterType, desiredSkills);
await logActivity(db, {
companyId: updated.companyId,
actorType: actor.actorType,
actorId: actor.actorId,
action: "agent.skills_synced",
entityType: "agent",
entityId: updated.id,
agentId: actor.agentId,
runId: actor.runId,
details: {
adapterType: updated.adapterType,
desiredSkills,
mode: snapshot.mode,
supported: snapshot.supported,
entryCount: snapshot.entries.length,
warningCount: snapshot.warnings.length,
},
});
res.json(snapshot);
},
);
router.get("/companies/:companyId/agents", async (req, res) => {
const companyId = req.params.companyId as string;
assertCompanyAccess(req, companyId);