Dev #11

Merged
cpfarhood merged 86 commits from dev into local 2026-05-12 00:02:32 +00:00
Showing only changes of commit 27db0d3c67 - Show all commits
+34 -9
View File
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
import { promises as fs } from "node:fs"; import { promises as fs } from "node:fs";
import path from "node:path"; import path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { and, asc, eq } from "drizzle-orm"; import { and, asc, eq, sql } from "drizzle-orm";
import type { Db } from "@paperclipai/db"; import type { Db } from "@paperclipai/db";
import { companies, companySkills } from "@paperclipai/db"; import { companies, companySkills } from "@paperclipai/db";
import { readPaperclipSkillSyncPreference } from "@paperclipai/adapter-utils/server-utils"; import { readPaperclipSkillSyncPreference } from "@paperclipai/adapter-utils/server-utils";
@@ -2372,21 +2372,27 @@ export function companySkillService(db: Db) {
const incomingOwner = asString(incomingMeta.owner); const incomingOwner = asString(incomingMeta.owner);
const incomingRepo = asString(incomingMeta.repo); const incomingRepo = asString(incomingMeta.repo);
const incomingKind = asString(incomingMeta.sourceKind); const incomingKind = asString(incomingMeta.sourceKind);
// Bundled skills are sourced from the server image and re-upserted by
// ensureBundledSkills only. Never let a non-bundled import overwrite a
// bundled row, regardless of which org/repo it claims to be from.
if ( if (
existing existing
&& existingMeta.sourceKind === "paperclip_bundled" && existingMeta.sourceKind === "paperclip_bundled"
&& incomingKind === "github" && incomingKind !== "paperclip_bundled"
&& incomingOwner === "paperclipai"
&& incomingRepo === "paperclip"
) { ) {
out.push(existing); out.push(existing);
continue; continue;
} }
// Preserve sourceAuthSecretId across re-imports of the same skill. Skip
// bundled rows: they should never carry a PAT reference, and preserving
// one across a bundled re-upsert would re-attach stale data.
const metadata = { const metadata = {
...(skill.metadata ?? {}), ...(skill.metadata ?? {}),
skillKey: skill.key, skillKey: skill.key,
...(existing?.metadata && typeof (existing.metadata as Record<string, unknown>).sourceAuthSecretId === "string" ...(existing?.metadata
&& existingMeta.sourceKind !== "paperclip_bundled"
&& typeof (existing.metadata as Record<string, unknown>).sourceAuthSecretId === "string"
? { sourceAuthSecretId: (existing.metadata as Record<string, unknown>).sourceAuthSecretId } ? { sourceAuthSecretId: (existing.metadata as Record<string, unknown>).sourceAuthSecretId }
: {}), : {}),
}; };
@@ -2464,6 +2470,8 @@ export function companySkillService(db: Db) {
if (authToken && imported.length > 0) { if (authToken && imported.length > 0) {
for (const skill of imported) { for (const skill of imported) {
const skillMeta = skill.metadata as Record<string, unknown> | null;
if (skillMeta?.sourceKind === "paperclip_bundled") continue;
const secretName = `skill-pat:${skill.id}`; const secretName = `skill-pat:${skill.id}`;
let secretId: string; let secretId: string;
const existing = await secretsSvc.getByName(companyId, secretName); const existing = await secretsSvc.getByName(companyId, secretName);
@@ -2530,10 +2538,24 @@ export function companySkillService(db: Db) {
const meta = skill.metadata as Record<string, unknown> | null; const meta = skill.metadata as Record<string, unknown> | null;
const secretId = typeof meta?.sourceAuthSecretId === "string" ? meta.sourceAuthSecretId : null; const secretId = typeof meta?.sourceAuthSecretId === "string" ? meta.sourceAuthSecretId : null;
if (secretId) { if (secretId) {
try { // Skip cleanup if another skill in the same company still references this
await secretsSvc.remove(secretId); // secret. The deleted row is already gone from the table, so any result
} catch { // here is a sibling skill we shouldn't orphan.
// Best-effort: don't fail the skill deletion if secret cleanup fails const otherSkillRefs = await db
.select({ id: companySkills.id })
.from(companySkills)
.where(and(
eq(companySkills.companyId, companyId),
sql`${companySkills.metadata} ->> 'sourceAuthSecretId' = ${secretId}`,
))
.limit(1);
if (otherSkillRefs.length === 0) {
try {
await secretsSvc.remove(secretId);
} catch {
// Best-effort: don't fail the skill deletion if secret cleanup fails
// (typically blocked by an agent env binding still referencing it).
}
} }
} }
@@ -2549,6 +2571,9 @@ export function companySkillService(db: Db) {
if (!skill) return null; if (!skill) return null;
const meta = (skill.metadata ?? {}) as Record<string, unknown>; const meta = (skill.metadata ?? {}) as Record<string, unknown>;
if (meta.sourceKind === "paperclip_bundled") {
throw unprocessable("Cannot configure auth for bundled paperclip skills");
}
const existingSecretId = typeof meta.sourceAuthSecretId === "string" ? meta.sourceAuthSecretId : null; const existingSecretId = typeof meta.sourceAuthSecretId === "string" ? meta.sourceAuthSecretId : null;
if (authToken) { if (authToken) {