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