apps/web/src/lib/aiCapabilities.server.ts

1import {
2	AI_TOOL_DEFINITIONS,
3	type AiCapabilities,
4	aiProductionVisible,
5	aiSubmissionsAllowed,
6} from '@local/config/ai-capabilities'
7import { getPublishedSanityClient } from '@local/sanity/client'
8import { sanityFetch } from '@local/sanity/utils/fetch'
9import { z } from 'zod'
10import { executeAiSubmission } from './aiCapabilityPolicy'
11
12const schema = z
13	.object({
14		productionEnabled: z.boolean().nullish(),
15		allowEmailSubmissions: z.boolean().nullish(),
16		stagingEmailSubmissions: z.boolean().nullish(),
17		tools: z.record(z.string(), z.boolean().nullish()).nullish(),
18	})
19	.nullish()
20/** No stale cache or CDN: launch controls always use published settings, including in draft preview. */
21export async function getAiCapabilities(): Promise<AiCapabilities> {
22	try {
23		const doc = schema.parse(
24			await sanityFetch(
25				getPublishedSanityClient().withConfig({
26					useCdn: false,
27					// Settings IDs are private in Sanity; retain published-only reads.
28					token: process.env.SANITY_TOKEN,
29				}),
30				'*[_type == "settings.ai"][0]{productionEnabled, allowEmailSubmissions, stagingEmailSubmissions, tools}',
31				{},
32				{ tag: 'ai-controls' },
33			),
34		)
35		const tools: AiCapabilities['tools'] = {}
36		for (const { name } of AI_TOOL_DEFINITIONS) {
37			const enabled = doc?.tools?.[name]
38			if (enabled != null) tools[name] = enabled
39		}
40		return {
41			productionEnabled: doc?.productionEnabled === true,
42			allowEmailSubmissions: aiSubmissionsAllowed(
43				doc ?? {},
44				process.env.VERCEL_ENV,
45				process.env.VERCEL_GIT_COMMIT_REF,
46			),
47			tools,
48		}
49	} catch (error) {
50		console.error(
51			'[ai-controls] Settings unavailable; disabling AI capabilities',
52			error,
53		)
54		return {
55			productionEnabled: false,
56			allowEmailSubmissions: false,
57			tools: Object.fromEntries(
58				AI_TOOL_DEFINITIONS.map(({ name }) => [name, false]),
59			),
60		}
61	}
62}
63export function isAiVisible(config: AiCapabilities) {
64	return aiProductionVisible(
65		config,
66		process.env.VERCEL_ENV,
67		process.env.AI_AGENT_ENABLED,
68	)
69}
70/** Recheck immediately before a side effect, even if settings changed during generation. */
71export function runAllowedAiSubmission<T>(
72	name: 'submitLead' | 'sendAgendaByEmail',
73	submit: () => Promise<T>,
74) {
75	return executeAiSubmission(name, getAiCapabilities, isAiVisible, submit)
76}
77