mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45: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>
121 lines
4.3 KiB
TypeScript
121 lines
4.3 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import { createSupabaseMCPClient } from '../supabase-mcp'
|
|
import { getMcpTools } from './mcp-tools'
|
|
|
|
vi.mock('../supabase-mcp', () => ({
|
|
createSupabaseMCPClient: vi.fn(),
|
|
}))
|
|
|
|
const BASE_PARAMS = {
|
|
accessToken: 'token',
|
|
projectRef: 'abcdefghijklmnopqrst',
|
|
aiOptInLevel: 'schema_and_log_and_data' as const,
|
|
// A fresh, non-aborted signal by default; lifecycle tests override it
|
|
signal: new AbortController().signal,
|
|
}
|
|
|
|
// A realistic remote tool set: all expected read tools plus the UI-executed ones
|
|
const FULL_REMOTE_TOOLS = {
|
|
search_docs: { description: 'docs' },
|
|
list_tables: { description: 'list tables' },
|
|
list_extensions: { description: 'extensions' },
|
|
list_edge_functions: { description: 'edge functions' },
|
|
list_branches: { description: 'branches' },
|
|
get_advisors: { description: 'advisors' },
|
|
query_logs: { description: 'query logs' },
|
|
execute_sql: { description: 'execute sql' },
|
|
deploy_edge_function: { description: 'deploy' },
|
|
}
|
|
|
|
describe('ai/tools/mcp-tools getMcpTools', () => {
|
|
let close: ReturnType<typeof vi.fn>
|
|
let tools: ReturnType<typeof vi.fn>
|
|
let consoleErrorSpy: ReturnType<typeof vi.spyOn>
|
|
|
|
beforeEach(() => {
|
|
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
close = vi.fn().mockResolvedValue(undefined)
|
|
tools = vi.fn().mockResolvedValue({ ...FULL_REMOTE_TOOLS })
|
|
vi.mocked(createSupabaseMCPClient).mockResolvedValue({ tools, close } as any)
|
|
})
|
|
|
|
afterEach(() => {
|
|
consoleErrorSpy.mockRestore()
|
|
})
|
|
|
|
it('returns MCP tools and strips UI-executed tools handled locally', async () => {
|
|
const result = await getMcpTools(BASE_PARAMS)
|
|
|
|
expect(result).toHaveProperty('list_tables')
|
|
expect(result).toHaveProperty('query_logs')
|
|
expect(result).not.toHaveProperty('execute_sql')
|
|
expect(result).not.toHaveProperty('deploy_edge_function')
|
|
})
|
|
|
|
it('warns when the remote server is missing an expected tool (contract drift)', async () => {
|
|
const { list_branches, ...withoutBranches } = FULL_REMOTE_TOOLS
|
|
tools.mockResolvedValueOnce(withoutBranches)
|
|
|
|
await getMcpTools(BASE_PARAMS)
|
|
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('list_branches'))
|
|
})
|
|
|
|
it('does not warn about drift when all expected tools are present', async () => {
|
|
await getMcpTools(BASE_PARAMS)
|
|
|
|
const driftWarnings = consoleErrorSpy.mock.calls.filter(
|
|
([msg]: unknown[]) => typeof msg === 'string' && msg.includes('missing expected tools')
|
|
)
|
|
expect(driftWarnings).toHaveLength(0)
|
|
})
|
|
|
|
it('keeps the connection open until the request signal aborts', async () => {
|
|
const controller = new AbortController()
|
|
|
|
await getMcpTools({ ...BASE_PARAMS, signal: controller.signal })
|
|
// Tools execute later during streaming, so the client must stay open
|
|
expect(close).not.toHaveBeenCalled()
|
|
|
|
controller.abort()
|
|
await Promise.resolve()
|
|
expect(close).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('closes the client and skips fetching tools when the signal is already aborted', async () => {
|
|
const controller = new AbortController()
|
|
controller.abort()
|
|
|
|
const result = await getMcpTools({ ...BASE_PARAMS, signal: controller.signal })
|
|
|
|
expect(close).toHaveBeenCalledTimes(1)
|
|
expect(tools).not.toHaveBeenCalled()
|
|
expect(result).toEqual({})
|
|
})
|
|
|
|
it('closes the client and rethrows when fetching tools fails, without double-closing on later abort', async () => {
|
|
tools.mockRejectedValueOnce(new Error('network unreachable'))
|
|
const controller = new AbortController()
|
|
|
|
await expect(getMcpTools({ ...BASE_PARAMS, signal: controller.signal })).rejects.toThrow(
|
|
'network unreachable'
|
|
)
|
|
expect(close).toHaveBeenCalledTimes(1)
|
|
|
|
// A subsequent abort must not close the (already closed) client again
|
|
controller.abort()
|
|
await Promise.resolve()
|
|
expect(close).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('closes the client and rethrows when tool validation fails', async () => {
|
|
// A known tool name with a non-object value passes the opt-in filter but
|
|
// fails schema validation
|
|
tools.mockResolvedValueOnce({ ...FULL_REMOTE_TOOLS, list_tables: 'not-an-object' })
|
|
|
|
await expect(getMcpTools(BASE_PARAMS)).rejects.toThrow('MCP tools validation failed')
|
|
expect(close).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|