Files
supabase/apps/studio/lib/ai/tools/studio-tools.ts
Saxon Fletcher 80da153450 Fix for AI Assistant query and deploy confirmation (#46052)
When Assistant requests confirmation to run a query or deploy an edge
function if the user doesn't skip or run and instead sends a follow-up
message it errors out. This allows follow-up messages and treats them as
"skips" which means adjusting confirmation message state as part of the
follow-up. This also uses toModelOutput to cleanse data based on
permissions.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Enhanced tool approval workflow: pending approvals are now
automatically resolved as denied when submitting new messages
* Improved chat input state management with better handling of approval
states
  * Customizable loading messages for tool operations

* **Bug Fixes**
  * Fixed chat input availability during pending tool approval states
  * Improved tool execution feedback during approval workflows

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46052?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-20 09:09:28 +10:00

128 lines
4.8 KiB
TypeScript

import { acceptUntrustedSql, untrustedSql } from '@supabase/pg-meta'
import { tool } from 'ai'
import { z } from 'zod'
import { deployEdgeFunction } from '@/data/edge-functions/edge-functions-deploy-mutation'
import { executeSql } from '@/data/sql/execute-sql-query'
import type { AiOptInLevel } from '@/hooks/misc/useOrgOptedIntoAi'
import {
EDGE_FUNCTION_PROMPT,
PG_BEST_PRACTICES,
REALTIME_PROMPT,
RLS_PROMPT,
} from '@/lib/ai/prompts'
import { NO_DATA_PERMISSIONS } from '@/lib/ai/tools/tool-sanitizer'
import { fixSqlBackslashEscapes } from '@/lib/ai/util'
const KNOWLEDGE = {
pg_best_practices: PG_BEST_PRACTICES,
rls: RLS_PROMPT,
edge_functions: EDGE_FUNCTION_PROMPT,
realtime: REALTIME_PROMPT,
} as const
type KnowledgeName = keyof typeof KNOWLEDGE
export const executeSqlInputSchema = z.object({
// Transform at parse time so the corrected SQL is what gets stored in
// toolCall.input — ensuring evals and logs reflect what actually runs.
sql: z.string().describe('The SQL statement to execute.').transform(fixSqlBackslashEscapes),
label: z.string().describe('A short 2-4 word label for the SQL statement.'),
chartConfig: z
.object({
view: z.enum(['table', 'chart']).describe('How to render the results after execution'),
xAxis: z.string().optional().describe('The column to use for the x-axis of the chart.'),
yAxis: z.string().optional().describe('The column to use for the y-axis of the chart.'),
})
.describe('Chart configuration for rendering the results'),
isWriteQuery: z
.boolean()
.default(false)
.describe(
'Whether the SQL statement performs a write operation or has side effects. Set true for INSERT/UPDATE/DELETE/DDL and for SELECT statements that call side-effecting functions, such as select cron.schedule(...), cron.unschedule(...), or functions that create, modify, schedule, enqueue, notify, or trigger work.'
),
})
export const loadKnowledgeInputSchema = z.object({
name: z
.enum(Object.keys(KNOWLEDGE) as [KnowledgeName, ...KnowledgeName[]])
.describe('The knowledge to load'),
})
export type StudioToolsContext = {
projectRef?: string
connectionString?: string
authorization?: string
aiOptInLevel?: AiOptInLevel
}
export const getStudioTools = (ctx: StudioToolsContext = {}) => {
const { projectRef, connectionString, authorization, aiOptInLevel = 'schema' } = ctx
const authHeaders = authorization
? { 'Content-Type': 'application/json', Authorization: authorization }
: undefined
return {
execute_sql: tool({
description:
'Asks the user to execute a SQL statement and return the results. Requires user approval before executing.',
inputSchema: executeSqlInputSchema,
needsApproval: true,
execute: async ({ sql }) => {
// The `needsApproval: true` gate on this tool means the user has
// explicitly approved this AI-generated SQL before execute runs —
// that approval is the user gesture that promotes untrusted to safe.
const { result } = await executeSql(
{ projectRef, connectionString, sql: acceptUntrustedSql(untrustedSql(sql)) },
undefined,
authHeaders
)
return result
},
toModelOutput: ({ output }) => {
return aiOptInLevel === 'schema_and_log_and_data'
? { type: 'json', value: output }
: { type: 'text', value: NO_DATA_PERMISSIONS }
},
}),
deploy_edge_function: tool({
description:
'Asks the user to deploy a Supabase Edge Function from provided code. Requires user approval before deploying.',
inputSchema: z.object({
name: z.string().describe('The URL-friendly name/slug of the Edge Function.'),
code: z.string().describe('The TypeScript code for the Edge Function.'),
}),
needsApproval: true,
execute: async ({ name, code }) => {
await deployEdgeFunction({
projectRef: projectRef ?? '',
slug: name,
metadata: {
entrypoint_path: 'index.ts',
name,
verify_jwt: true,
},
files: [{ name: 'index.ts', content: code }],
authorization,
})
return { success: true }
},
}),
rename_chat: tool({
description: `Rename the current chat session when the current chat name doesn't describe the conversation topic.`,
inputSchema: z.object({
newName: z.string().describe('The new name for the chat session. Five words or less.'),
}),
execute: async () => {
return { status: 'Chat request sent to client' }
},
}),
load_knowledge: tool({
description:
'Load detailed knowledge about a Supabase topic before answering questions about it.',
inputSchema: loadKnowledgeInputSchema,
execute: ({ name }) => KNOWLEDGE[name],
}),
}
}