import type { ToolProfileBindingTargetType } from "@paperclipai/shared"; type BindingLike = { profileId: string; targetType: ToolProfileBindingTargetType; targetId: string; priority: number; createdAt: Date | string; }; type ProfileLike = { id: string; profileKey: string; metadata: unknown; }; const TOOL_PROFILE_SCOPE_PRECEDENCE: Record = { // Named gateways bind one concrete MCP endpoint instance, so they should // override broader run, agent, and company defaults when both match. gateway: 0, issue: 1, routine: 2, agent: 3, project: 4, company: 5, }; function createdAtMillis(value: Date | string): number { return value instanceof Date ? value.getTime() : new Date(value).getTime(); } export function toolProfileBindingScopePrecedence(targetType: ToolProfileBindingTargetType): number { return TOOL_PROFILE_SCOPE_PRECEDENCE[targetType]; } export function narrowestScopeBindings(bindings: T[]): T[] { if (bindings.length === 0) return []; const winningScope = Math.min(...bindings.map((binding) => toolProfileBindingScopePrecedence(binding.targetType))); return bindings .filter((binding) => toolProfileBindingScopePrecedence(binding.targetType) === winningScope) .sort((a, b) => a.priority - b.priority || createdAtMillis(a.createdAt) - createdAtMillis(b.createdAt) || a.profileId.localeCompare(b.profileId) ); } export function profileIdsInBindingOrder>(bindings: T[]): string[] { const seen = new Set(); const ordered: string[] = []; for (const binding of bindings) { if (seen.has(binding.profileId)) continue; seen.add(binding.profileId); ordered.push(binding.profileId); } return ordered; } function isWizardAppProfile(profile: ProfileLike, connectionId?: string | null): boolean { if (!profile.metadata || typeof profile.metadata !== "object" || Array.isArray(profile.metadata)) return false; const metadata = profile.metadata as Record; if (metadata.source === "connection_intent" && typeof metadata.connectionId === "string" && typeof metadata.agentId === "string") { return profile.profileKey === `connection-intent:${metadata.connectionId}:${metadata.agentId}` && (connectionId == null || metadata.connectionId === connectionId); } if (metadata.source !== "app_gallery_finish" || typeof metadata.connectionId !== "string") return false; if (profile.profileKey !== `app:${metadata.connectionId}`) return false; return connectionId === undefined || connectionId === null || metadata.connectionId === connectionId; } /** * App-wizard assignments are additive capabilities: choosing an app for all * agents (or for one agent) must not disappear merely because that agent also * has a narrower general-purpose profile. Ordinary profiles still use the * narrowest-scope rule; only the profile managed by the app wizard is carried * alongside that winning tier. */ export function effectiveToolProfileBindings( bindings: T[], profiles: ProfileLike[], connectionId?: string | null, options?: { includeAdditiveAppProfiles?: boolean }, ): T[] { if (options?.includeAdditiveAppProfiles === false) { return narrowestScopeBindings(bindings); } const appProfileIds = new Set( profiles.filter((profile) => isWizardAppProfile(profile, connectionId)).map((profile) => profile.id), ); const accessProfileIds = new Set(profiles.filter(profile => { const metadata = profile.metadata as Record | null; return metadata?.source === "connection_intent"; }).map(profile => profile.id)); const selected = [ ...narrowestScopeBindings(bindings.filter(binding => !accessProfileIds.has(binding.profileId))), ...bindings.filter((binding) => appProfileIds.has(binding.profileId)), ]; const seen = new Set(); return selected.filter((binding) => { const key = `${binding.targetType}:${binding.targetId}:${binding.profileId}`; if (seen.has(key)) return false; seen.add(key); return true; }); }