mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
Orgs with the HIPAA add-on had the Assistant's opt-in level forced to `disabled` on any project marked High Compliance, regardless of what the org picked in its AI settings. The restriction predated our AI provider BAAs. The consequence is those users see the Assistant failing to answer questions about their data w/ no clear path how to fix it, even though the LLM provider supports this use case. This PR removes these Assistant restrictions on the server and client so those projects honor the org's chosen level. Braintrust conversation tracing is unchanged and still blocked for these projects, see [this test case](https://github.com/supabase/supabase/blob/b9800ccf16/apps/studio/lib/ai/braintrust-logger.test.ts#L16-L20). See [comments](https://linear.app/supabase/issue/AI-1153/allow-hipaa-orgs-to-opt-in-to-assistant-data-access-for-high#comment-485a0d46) for legal approval and conditions. The client-side changes enable features like "Debug with AI" on SQL query failures, “Generate/Rename with AI” for snippet titles, and generated Assistant chat titles for these customers. The AI opt-in copy now adds a reminder to obtain consent from data subjects, linking the [shared responsibility model](https://supabase.com/docs/guides/deployment/shared-responsibility-model) based also on [this comment](https://linear.app/supabase/issue/AI-1153/allow-hipaa-orgs-to-opt-in-to-assistant-data-access-for-high#comment-f81ee610). <img width="400" alt="CleanShot 2026-09-17 at 5 01 02 PM@2x" src="https://github.com/user-attachments/assets/d02123f2-3e32-4d83-9f98-7d15e59222ef" /> To test with a HIPAA-enabled project in staging, you can use this [Plan Change [Staging]](https://app.hex.tech/supabase/app/Plan-Change-Staging-032BD32jo1EaisCS85qunf/latest) Hex to add the HIPAA add-on. Once the add-on is present, you can turn on High Compliance from a project's settings. Also in org settings, crank up the Assistant data opt-in level and verify the Assistant is able to answer questions about the project's data. My results testing with opt-in level "Schema, Logs & Database Data": | High compliance setting | Data opt-in working | |--------|--------| | <img width="1302" height="422" alt="CleanShot 2026-09-17 at 5 03 36 PM@2x" src="https://github.com/user-attachments/assets/c416371b-2eb8-49df-9c07-6d8eababb443" /> | <img width="1566" height="1516" alt="CleanShot 2026-09-17 at 5 05 14 PM@2x" src="https://github.com/user-attachments/assets/39624355-7f8f-46ce-9f08-a8acfb9da830" /> | Closes AI-1153 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## New Features - AI-assisted query renaming, snippet title generation, debugging, and tools now follow organization AI opt-in settings rather than project HIPAA status. - Debugging assistance and AI actions remain available for eligible users without additional HIPAA-based blocking. - AI metadata warnings consistently show standard opt-in messaging and permission settings. - AI settings remind users to obtain consent before entering personal data and link to shared responsibility guidance. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
124 lines
5.0 KiB
TypeScript
124 lines
5.0 KiB
TypeScript
// Type-only import (erased at build time — pulls no runtime code into this route).
|
|
import type * as SupabaseMcp from '@supabase/mcp-server-supabase'
|
|
import type { ToolSet } from 'ai'
|
|
|
|
import { createSupabaseMCPClient } from '../supabase-mcp'
|
|
import { filterToolsByOptInLevel, toolSetValidationSchema } from '../tool-filter'
|
|
import type { AiOptInLevel } from '@/hooks/misc/useOrgOptedIntoAi'
|
|
|
|
/**
|
|
* Union of the tool names exposed by the pinned `@supabase/mcp-server-supabase`
|
|
* version. Studio's dependency is bumped in lockstep with the remote MCP server
|
|
* (via automated bump PRs), so typing our tool-name lists against this makes an
|
|
* upstream rename/removal a **compile-time** failure (`pnpm typecheck`) in that
|
|
* PR, instead of a silent capability loss at runtime.
|
|
*/
|
|
type SupabaseMcpToolName = keyof typeof SupabaseMcp.supabaseMcpToolSchemas
|
|
|
|
// UI-executed tools handled locally by Studio (see getStudioTools); the remote
|
|
// MCP server's versions are removed so the UI-controlled Studio versions win.
|
|
const UI_EXECUTED_TOOLS = [
|
|
'execute_sql',
|
|
'deploy_edge_function',
|
|
] as const satisfies readonly SupabaseMcpToolName[]
|
|
|
|
// Read-only tools the assistant relies on from the remote MCP server — the
|
|
// MCP-sourced subset of the allowlist in tool-filter.ts (TOOL_CATEGORY_MAP).
|
|
// `satisfies` gives the compile-time drift guard; the runtime check below also
|
|
// catches a deployed server that returns fewer tools (feature flags / version
|
|
// skew). The allowlist remains the source of truth for what is allowed.
|
|
const EXPECTED_MCP_TOOLS = [
|
|
'search_docs',
|
|
'list_tables',
|
|
'list_extensions',
|
|
'list_edge_functions',
|
|
'list_branches',
|
|
'get_advisors',
|
|
'query_logs',
|
|
] as const satisfies readonly SupabaseMcpToolName[]
|
|
|
|
export const getMcpTools = async ({
|
|
accessToken,
|
|
projectRef,
|
|
aiOptInLevel,
|
|
signal,
|
|
}: {
|
|
accessToken: string
|
|
projectRef: string
|
|
aiOptInLevel: AiOptInLevel
|
|
// Required: the remote client holds an HTTP connection that must be torn down
|
|
// when the request ends. The caller owns that lifecycle via this signal.
|
|
signal: AbortSignal
|
|
}) => {
|
|
// Connect to the remote MCP server over HTTP and fetch its tools, replacing
|
|
// the local tools. A remote failure (outage, timeout, auth) degrades to the
|
|
// remaining tools in `getTools` rather than breaking the assistant.
|
|
const mcpClient = await createSupabaseMCPClient({
|
|
accessToken,
|
|
projectRef,
|
|
})
|
|
|
|
// The remote client keeps an HTTP connection open. The tools' `execute`
|
|
// functions are invoked later, while the response is streaming, so the
|
|
// connection must stay open until the request ends. Close it exactly once when
|
|
// the request is done (normal completion or abort) to avoid leaking a
|
|
// connection per request.
|
|
let closed = false
|
|
const closeClient = () => {
|
|
if (closed) return
|
|
closed = true
|
|
void mcpClient.close().catch(() => {})
|
|
}
|
|
|
|
// The request already ended before we could fetch tools; don't bother.
|
|
if (signal.aborted) {
|
|
closeClient()
|
|
return {} as ToolSet
|
|
}
|
|
signal.addEventListener('abort', closeClient, { once: true })
|
|
|
|
try {
|
|
const availableMcpTools = (await mcpClient.tools()) as ToolSet
|
|
|
|
// Runtime drift detection: `EXPECTED_MCP_TOOLS` is compile-time-checked
|
|
// against the pinned package (see its declaration), but the *deployed* remote
|
|
// server can still return fewer tools than the pinned types — feature flags,
|
|
// killswitches, or version skew during a bump. `filterToolsByOptInLevel`
|
|
// drops missing tools silently, so warn to make that observable.
|
|
const missingExpectedTools = EXPECTED_MCP_TOOLS.filter((name) => !(name in availableMcpTools))
|
|
if (missingExpectedTools.length > 0) {
|
|
console.error(
|
|
`Remote MCP server is missing expected tools: ${missingExpectedTools.join(', ')}. ` +
|
|
'The tool contract may have drifted; the assistant will operate without them.'
|
|
)
|
|
}
|
|
|
|
// Safety gate: `filterToolsByOptInLevel` keeps only tools in the allowlist
|
|
// (tool-filter.ts TOOL_CATEGORY_MAP) and drops everything else. This — not
|
|
// the `read_only` query param — is what prevents the remote server's
|
|
// write/destructive tools (apply_migration, create_branch, ...) from reaching
|
|
// the assistant. `read_only` is defense-in-depth (those tools throw at
|
|
// runtime). Do not remove this filter on the assumption `read_only` suffices.
|
|
const allowedMcpTools = filterToolsByOptInLevel(availableMcpTools, aiOptInLevel)
|
|
|
|
// Remove UI-executed tools handled locally
|
|
const filteredMcpTools: ToolSet = { ...allowedMcpTools }
|
|
UI_EXECUTED_TOOLS.forEach((toolName) => {
|
|
delete filteredMcpTools[toolName]
|
|
})
|
|
|
|
// Validate that only known tools are provided
|
|
const validation = toolSetValidationSchema.safeParse(filteredMcpTools)
|
|
if (!validation.success) {
|
|
console.error('MCP tools validation error:', validation.error)
|
|
throw new Error('Internal error: MCP tools validation failed')
|
|
}
|
|
|
|
return validation.data
|
|
} catch (error) {
|
|
// Don't leak the connection if fetching or validating tools fails
|
|
closeClient()
|
|
throw error
|
|
}
|
|
}
|