mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
feat(studio): let High Compliance projects opt-in to Assistant data access (#50548)
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>
This commit is contained in:
1 parent
ef527f447a
commit
4bb36b944f
26 files changed
+86
-374
No files matched your search
@@ -36,7 +36,6 @@ export const ExplorerChatToolbar = ({
|
||||
isChatLoading,
|
||||
showMetadataWarning,
|
||||
updatedOptInSinceMCP,
|
||||
isHipaaProjectDisallowed,
|
||||
aiOptInLevel,
|
||||
}: ExplorerChatToolbarProps) => {
|
||||
const snap = useAiAssistantStateSnapshot()
|
||||
@@ -120,7 +119,6 @@ export const ExplorerChatToolbar = ({
|
||||
onVisibleChange={setIsOptInModalOpen}
|
||||
showMetadataWarning={showMetadataWarning}
|
||||
updatedOptInSinceMCP={updatedOptInSinceMCP}
|
||||
isHipaaProjectDisallowed={isHipaaProjectDisallowed}
|
||||
aiOptInLevel={aiOptInLevel}
|
||||
/>
|
||||
|
||||
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
import { screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { QueryResultError } from './QueryResultError'
|
||||
import { customRender } from '@/tests/lib/custom-render'
|
||||
import { addAPIMock } from '@/tests/lib/msw'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
createChat: vi.fn(),
|
||||
useParams: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/constants', async () => {
|
||||
const actual = await vi.importActual<Record<string, unknown>>('@/lib/constants')
|
||||
return { ...actual, IS_PLATFORM: false }
|
||||
})
|
||||
|
||||
vi.mock('common', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('common')>()
|
||||
return { ...actual, useParams: () => mocks.useParams() }
|
||||
})
|
||||
|
||||
vi.mock('../hooks', () => ({
|
||||
useCreateChat: () => ({ createChat: mocks.createChat, isCreating: false }),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/misc/useSelectedOrganization', () => ({
|
||||
useSelectedOrganizationQuery: () => ({ data: undefined }),
|
||||
}))
|
||||
|
||||
// Self-hosted has no orgs/billing, so these eligibility queries are expected to never
|
||||
// resolve (disabled or failing) - the dropdown must not stay hidden waiting on them.
|
||||
vi.mock('@/data/subscriptions/org-subscription-query', () => ({
|
||||
useOrgSubscriptionQuery: () => ({ data: undefined, isSuccess: false }),
|
||||
}))
|
||||
|
||||
vi.mock('@/data/config/project-settings-v2-query', () => ({
|
||||
useProjectSettingsV2Query: () => ({ data: undefined, isSuccess: false }),
|
||||
}))
|
||||
|
||||
describe('QueryResultError (self-hosted)', () => {
|
||||
beforeEach(() => {
|
||||
mocks.useParams.mockReturnValue({ ref: 'default' })
|
||||
addAPIMock({
|
||||
method: 'get',
|
||||
path: '/platform/projects/:ref',
|
||||
response: {
|
||||
id: 1,
|
||||
ref: 'default',
|
||||
organization_id: 1,
|
||||
name: 'Test Project',
|
||||
status: 'ACTIVE_HEALTHY',
|
||||
cloud_provider: 'AWS',
|
||||
region: 'us-east-1',
|
||||
db_host: 'db.default.supabase.co',
|
||||
restUrl: 'https://default.supabase.co/rest/v1/',
|
||||
inserted_at: '2024-01-01T00:00:00Z',
|
||||
updated_at: '2024-01-01T00:00:00Z',
|
||||
subscription_id: 'sub_123',
|
||||
is_branch_enabled: false,
|
||||
is_physical_backups_enabled: false,
|
||||
high_availability: false,
|
||||
integration_source: null,
|
||||
connectionString: 'postgresql://postgres@localhost:5432/postgres',
|
||||
is_hibernating: false,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('renders the assistant dropdown without waiting on HIPAA eligibility queries', () => {
|
||||
customRender(
|
||||
<QueryResultError
|
||||
error={{ message: 'relation "foo" does not exist' }}
|
||||
sql="select * from foo;"
|
||||
source="database"
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Debug with Assistant' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -10,8 +10,6 @@ const mocks = vi.hoisted(() => ({
|
||||
createChat: vi.fn(),
|
||||
useParams: vi.fn(),
|
||||
mockCopyToClipboard: vi.fn(),
|
||||
useOrgSubscriptionQuery: vi.fn(),
|
||||
useProjectSettingsV2Query: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('common', async (importOriginal) => {
|
||||
@@ -19,13 +17,6 @@ vi.mock('common', async (importOriginal) => {
|
||||
return { ...actual, useParams: () => mocks.useParams() }
|
||||
})
|
||||
|
||||
// This file covers the platform-mode HIPAA eligibility gate; the self-hosted bypass is
|
||||
// covered separately in QueryResultError.selfhosted.test.tsx.
|
||||
vi.mock('@/lib/constants', async () => {
|
||||
const actual = await vi.importActual<Record<string, unknown>>('@/lib/constants')
|
||||
return { ...actual, IS_PLATFORM: true }
|
||||
})
|
||||
|
||||
// CopyButton and AiAssistantDropdown write via copyToClipboard from 'ui'. Stub just that
|
||||
// export so we can assert the value handed to the clipboard without depending on jsdom's
|
||||
// document.hasFocus() / navigator.clipboard. Everything else in 'ui' stays real.
|
||||
@@ -42,19 +33,9 @@ vi.mock('@/hooks/misc/useSelectedOrganization', () => ({
|
||||
useSelectedOrganizationQuery: () => ({ data: { slug: 'default-org' } }),
|
||||
}))
|
||||
|
||||
vi.mock('@/data/subscriptions/org-subscription-query', () => ({
|
||||
useOrgSubscriptionQuery: () => mocks.useOrgSubscriptionQuery(),
|
||||
}))
|
||||
|
||||
vi.mock('@/data/config/project-settings-v2-query', () => ({
|
||||
useProjectSettingsV2Query: () => mocks.useProjectSettingsV2Query(),
|
||||
}))
|
||||
|
||||
describe('QueryResultError', () => {
|
||||
beforeEach(() => {
|
||||
mocks.useParams.mockReturnValue({ ref: 'default' })
|
||||
mocks.useOrgSubscriptionQuery.mockReturnValue({ data: undefined, isSuccess: true })
|
||||
mocks.useProjectSettingsV2Query.mockReturnValue({ data: undefined, isSuccess: true })
|
||||
// useTrack() (invoked by AiAssistantDropdown) reads the selected project to attach
|
||||
// telemetry context, so the platform project fetch needs a handler even though this
|
||||
// component doesn't read project data itself.
|
||||
@@ -145,34 +126,4 @@ describe('QueryResultError', () => {
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Debug with Assistant' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not render the assistant dropdown while HIPAA eligibility is still resolving', () => {
|
||||
mocks.useOrgSubscriptionQuery.mockReturnValue({ data: undefined, isSuccess: false })
|
||||
|
||||
customRender(
|
||||
<QueryResultError
|
||||
error={{ message: 'relation "foo" does not exist' }}
|
||||
sql="select * from foo;"
|
||||
source="database"
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Debug with Assistant' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not render the assistant dropdown when an eligibility query is disabled or failed', () => {
|
||||
// A disabled or failed query also settles with isSuccess: false forever - same as
|
||||
// still-loading from this component's point of view, so it stays denied.
|
||||
mocks.useProjectSettingsV2Query.mockReturnValue({ data: undefined, isSuccess: false })
|
||||
|
||||
customRender(
|
||||
<QueryResultError
|
||||
error={{ message: 'relation "foo" does not exist' }}
|
||||
sql="select * from foo;"
|
||||
source="database"
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Debug with Assistant' })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,8 @@
|
||||
import { useParams } from 'common'
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
import { parseAsBoolean, useQueryState } from 'nuqs'
|
||||
import { useCallback } from 'react'
|
||||
import { Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
||||
|
||||
import { subscriptionHasHipaaAddon } from '../../Billing/Subscription/Subscription.utils'
|
||||
import { type SqlSnippetSource } from '../../SQLEditor/querySource'
|
||||
import { buildDebugPromptText } from '../../SQLEditor/SQLEditor.utils'
|
||||
import { useCreateChat } from '../hooks'
|
||||
@@ -12,11 +10,8 @@ import { type QueryResult } from '../types'
|
||||
import { AiAssistantDropdown } from '@/components/ui/AiAssistantDropdown'
|
||||
import CopyButton from '@/components/ui/CopyButton'
|
||||
import { InlineLink, InlineLinkClassName } from '@/components/ui/InlineLink'
|
||||
import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
|
||||
import { getSqlErrorLines } from '@/data/sql/utils'
|
||||
import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
import { DOCS_URL, IS_PLATFORM } from '@/lib/constants'
|
||||
import { DOCS_URL } from '@/lib/constants'
|
||||
|
||||
export const QueryResultError = ({
|
||||
error,
|
||||
@@ -34,26 +29,6 @@ export const QueryResultError = ({
|
||||
* into that conversation's composer instead of abandoning it for a new chat. */
|
||||
onDebug?: (prompt: string) => void
|
||||
}) => {
|
||||
const { ref } = useParams()
|
||||
|
||||
const { data: org } = useSelectedOrganizationQuery()
|
||||
const { data: subscription, isSuccess: isSubscriptionResolved } = useOrgSubscriptionQuery({
|
||||
orgSlug: org?.slug,
|
||||
})
|
||||
const { data: projectSettings, isSuccess: isProjectSettingsResolved } = useProjectSettingsV2Query(
|
||||
{
|
||||
projectRef: ref,
|
||||
}
|
||||
)
|
||||
const hasHipaaAddon = subscriptionHasHipaaAddon(subscription) && projectSettings?.is_sensitive
|
||||
// Default deny until both eligibility queries have actually succeeded - a disabled or
|
||||
// failed query also reports isLoading: false, so isLoading can't tell "confirmed no
|
||||
// addon" apart from "don't know yet", and the assistant sends the SQL and error to an
|
||||
// LLM. Self-hosted has no HIPAA concept at all (subscriptionHasHipaaAddon short-circuits
|
||||
// to false there), so there's nothing to wait on outside of platform.
|
||||
const isCheckingHipaaEligibility =
|
||||
IS_PLATFORM && (!isSubscriptionResolved || !isProjectSettingsResolved)
|
||||
|
||||
const { createChat, isCreating } = useCreateChat()
|
||||
|
||||
const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false))
|
||||
@@ -175,7 +150,7 @@ export const QueryResultError = ({
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!hasHipaaAddon && !isCheckingHipaaEligibility && canDebug && (
|
||||
{canDebug && (
|
||||
<AiAssistantDropdown
|
||||
telemetrySource="sql_debug"
|
||||
label="Debug with Assistant"
|
||||
|
||||
+8
-1
@@ -4,8 +4,10 @@ import { FormField, RadioGroup, RadioGroupItem } from 'ui'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
|
||||
import { OptInToOpenAIToggle } from './OptInToOpenAIToggle'
|
||||
import { InlineLink } from '@/components/ui/InlineLink'
|
||||
import { AIOptInFormValues } from '@/hooks/forms/useAIOptInForm'
|
||||
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
|
||||
import { DOCS_URL } from '@/lib/constants'
|
||||
|
||||
interface AIOptInLevelSelectorProps {
|
||||
control: Control<AIOptInFormValues>
|
||||
@@ -89,7 +91,12 @@ export const AIOptInLevelSelector = ({
|
||||
<p>
|
||||
For organizations with HIPAA compliance enabled in their Supabase configuration, any
|
||||
consented information will only be shared with third-party AI providers with whom
|
||||
Supabase has established a Business Associate Agreement (BAA).
|
||||
Supabase has established a Business Associate Agreement (BAA). Don't input personal data
|
||||
unless you've{' '}
|
||||
<InlineLink href={`${DOCS_URL}/guides/deployment/shared-responsibility-model`}>
|
||||
obtained consent
|
||||
</InlineLink>{' '}
|
||||
from the individuals it relates to.
|
||||
</p>
|
||||
<OptInToOpenAIToggle />
|
||||
</div>
|
||||
|
||||
+7
-1
@@ -9,6 +9,7 @@ import {
|
||||
} from 'ui'
|
||||
|
||||
import { InlineLink } from '@/components/ui/InlineLink'
|
||||
import { DOCS_URL } from '@/lib/constants'
|
||||
|
||||
export const OptInToOpenAIToggle = () => {
|
||||
return (
|
||||
@@ -42,7 +43,12 @@ export const OptInToOpenAIToggle = () => {
|
||||
<p>
|
||||
For organizations with HIPAA compliance enabled in their Supabase configuration, any
|
||||
consented information will only be shared with third-party AI providers with whom
|
||||
Supabase has established a Business Associate Agreement (BAA).
|
||||
Supabase has established a Business Associate Agreement (BAA). Don't input personal data
|
||||
unless you've{' '}
|
||||
<InlineLink href={`${DOCS_URL}/guides/deployment/shared-responsibility-model`}>
|
||||
obtained consent
|
||||
</InlineLink>{' '}
|
||||
from the individuals it relates to.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
|
||||
@@ -63,8 +63,7 @@ const RenameQueryForm = ({ snippet, onCancel, onComplete }: RenameQueryFormProps
|
||||
const tabsSnap = useTabsStateSnapshot()
|
||||
const isSQLSnippet = snippet.type === 'sql'
|
||||
|
||||
// Orgs on HIPAA plans or that have disabled AI should not have access to Supabase AI
|
||||
const { aiOptInLevel, isHipaaProjectDisallowed } = useOrgAiOptInLevel()
|
||||
const { aiOptInLevel } = useOrgAiOptInLevel()
|
||||
const isAiOptedOut = aiOptInLevel === 'disabled'
|
||||
|
||||
const { id, name, description } = snippet
|
||||
@@ -180,19 +179,15 @@ const RenameQueryForm = ({ snippet, onCancel, onComplete }: RenameQueryFormProps
|
||||
<ButtonTooltip
|
||||
onClick={() => generateTitle()}
|
||||
size="tiny"
|
||||
disabled={
|
||||
isTitleGenerationLoading || !isApiKeySet || isHipaaProjectDisallowed || isAiOptedOut
|
||||
}
|
||||
disabled={isTitleGenerationLoading || !isApiKeySet || isAiOptedOut}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text: isHipaaProjectDisallowed
|
||||
? 'This feature is not available for HIPAA projects.'
|
||||
: isAiOptedOut
|
||||
? 'Your organization has opted out of AI features.'
|
||||
: isApiKeySet
|
||||
? undefined
|
||||
: 'Add your "OPENAI_API_KEY" to your environment variables to use this feature.',
|
||||
text: isAiOptedOut
|
||||
? 'Your organization has opted out of AI features.'
|
||||
: isApiKeySet
|
||||
? undefined
|
||||
: 'Add your "OPENAI_API_KEY" to your environment variables to use this feature.',
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -263,10 +263,9 @@ export function resolveConnectionString(
|
||||
|
||||
/**
|
||||
* Whether a query run should lazily kick off AI title generation for the
|
||||
* snippet: only when the org has AI enabled (not disabled/HIPAA — which would
|
||||
* silently forward the query to the AI provider without consent), the
|
||||
* snippet still has its placeholder name, and we're running on the hosted
|
||||
* platform.
|
||||
* snippet: only when the org has AI enabled (a disabled org would silently
|
||||
* forward the query to the AI provider without consent), the snippet still
|
||||
* has its placeholder name, and we're running on the hosted platform.
|
||||
*/
|
||||
export function shouldAutoGenerateTitle({
|
||||
aiOptInLevel,
|
||||
|
||||
@@ -4,15 +4,11 @@ import { parseAsBoolean, useQueryState } from 'nuqs'
|
||||
import { forwardRef } from 'react'
|
||||
import { Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
||||
|
||||
import { subscriptionHasHipaaAddon } from '@/components/interfaces/Billing/Subscription/Subscription.utils'
|
||||
import { AiAssistantDropdown } from '@/components/ui/AiAssistantDropdown'
|
||||
import CopyButton from '@/components/ui/CopyButton'
|
||||
import { DataGridResults } from '@/components/ui/DataGridResults'
|
||||
import { InlineLink, InlineLinkClassName } from '@/components/ui/InlineLink'
|
||||
import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
|
||||
import { getSqlErrorLines } from '@/data/sql/utils'
|
||||
import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
import { DOCS_URL } from '@/lib/constants'
|
||||
import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
|
||||
import { useSqlEditorSessionSnapshot } from '@/state/sql-editor/sql-editor-session-state'
|
||||
@@ -30,16 +26,10 @@ export const UtilityTabResults = forwardRef<HTMLDivElement, UtilityTabResultsPro
|
||||
({ id, isExecuting, isDisabled, isDebugging, onDebug, buildDebugPrompt }) => {
|
||||
const { ref } = useParams()
|
||||
const state = useDatabaseSelectorStateSnapshot()
|
||||
const { data: organization } = useSelectedOrganizationQuery()
|
||||
const sessionSnap = useSqlEditorSessionSnapshot()
|
||||
const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false))
|
||||
|
||||
const result = sessionSnap.results[id]?.[0]
|
||||
const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug })
|
||||
|
||||
// Customers on HIPAA plans should not have access to Supabase AI
|
||||
const { data: projectSettings } = useProjectSettingsV2Query({ projectRef: ref })
|
||||
const hasHipaaAddon = subscriptionHasHipaaAddon(subscription) && projectSettings?.is_sensitive
|
||||
|
||||
const isTimeout =
|
||||
result?.error?.message?.includes('canceling statement due to statement timeout') ||
|
||||
@@ -154,16 +144,14 @@ export const UtilityTabResults = forwardRef<HTMLDivElement, UtilityTabResultsPro
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!hasHipaaAddon && (
|
||||
<AiAssistantDropdown
|
||||
label="Debug with Assistant"
|
||||
buildPrompt={buildDebugPrompt}
|
||||
onOpenAssistant={onDebug}
|
||||
telemetrySource="sql_debug"
|
||||
disabled={!!isDisabled || isDebugging}
|
||||
loading={isDebugging}
|
||||
/>
|
||||
)}
|
||||
<AiAssistantDropdown
|
||||
label="Debug with Assistant"
|
||||
buildPrompt={buildDebugPrompt}
|
||||
onOpenAssistant={onDebug}
|
||||
telemetrySource="sql_debug"
|
||||
disabled={!!isDisabled || isDebugging}
|
||||
loading={isDebugging}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ export const ComplianceConfig = () => {
|
||||
return (
|
||||
<PageSection id="compliance-configuration">
|
||||
<PageSectionMeta>
|
||||
<div className="flex flex-col gap-3 @lg:flex-row @lg:items-center @lg:justify-between">
|
||||
<div className="w-full flex flex-col gap-3 @lg:flex-row @lg:items-center @lg:justify-between">
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>High Compliance Configuration</PageSectionTitle>
|
||||
<PageSectionDescription>
|
||||
|
||||
@@ -50,7 +50,6 @@ const defaultProps = {
|
||||
onCloseAssistant: vi.fn(),
|
||||
showMetadataWarning: false,
|
||||
updatedOptInSinceMCP: true,
|
||||
isHipaaProjectDisallowed: false,
|
||||
aiOptInLevel: 'full',
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ interface AIAssistantHeaderProps {
|
||||
onCloseAssistant: () => void
|
||||
showMetadataWarning: boolean
|
||||
updatedOptInSinceMCP: boolean
|
||||
isHipaaProjectDisallowed: boolean
|
||||
aiOptInLevel: 'disabled' | 'schema' | 'full' | string | undefined
|
||||
}
|
||||
|
||||
@@ -50,7 +49,6 @@ export const AIAssistantHeader = ({
|
||||
onCloseAssistant,
|
||||
showMetadataWarning,
|
||||
updatedOptInSinceMCP,
|
||||
isHipaaProjectDisallowed,
|
||||
aiOptInLevel,
|
||||
}: AIAssistantHeaderProps) => {
|
||||
const { openChat } = useCreateChat()
|
||||
@@ -244,7 +242,6 @@ export const AIAssistantHeader = ({
|
||||
onVisibleChange={setIsOptInModalOpen}
|
||||
showMetadataWarning={showMetadataWarning}
|
||||
updatedOptInSinceMCP={updatedOptInSinceMCP}
|
||||
isHipaaProjectDisallowed={isHipaaProjectDisallowed}
|
||||
aiOptInLevel={aiOptInLevel}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,6 @@ interface AIAssistantMetadataWarningProps {
|
||||
onVisibleChange: (visible: boolean) => void
|
||||
showMetadataWarning: boolean
|
||||
updatedOptInSinceMCP: boolean
|
||||
isHipaaProjectDisallowed: boolean
|
||||
aiOptInLevel: 'disabled' | 'schema' | 'full' | string | undefined
|
||||
}
|
||||
|
||||
@@ -17,7 +16,6 @@ export const AIAssistantMetadataWarning = ({
|
||||
onVisibleChange,
|
||||
showMetadataWarning,
|
||||
updatedOptInSinceMCP,
|
||||
isHipaaProjectDisallowed,
|
||||
aiOptInLevel,
|
||||
}: AIAssistantMetadataWarningProps) => (
|
||||
<>
|
||||
@@ -27,30 +25,24 @@ export const AIAssistantMetadataWarning = ({
|
||||
title={
|
||||
!updatedOptInSinceMCP
|
||||
? 'The Assistant has just been updated to help you better!'
|
||||
: isHipaaProjectDisallowed
|
||||
? 'Project metadata is not shared due to HIPAA'
|
||||
: aiOptInLevel === 'disabled'
|
||||
? 'Project metadata is currently not shared'
|
||||
: 'Limited metadata is shared to the Assistant'
|
||||
: aiOptInLevel === 'disabled'
|
||||
? 'Project metadata is currently not shared'
|
||||
: 'Limited metadata is shared to the Assistant'
|
||||
}
|
||||
description={
|
||||
!updatedOptInSinceMCP
|
||||
? 'You may now opt-in to share schema metadata and even logs for better results'
|
||||
: isHipaaProjectDisallowed
|
||||
? 'Your organization has the HIPAA addon and will not send project metadata with your prompts for projects marked as HIPAA.'
|
||||
: aiOptInLevel === 'disabled'
|
||||
? 'The Assistant can provide better answers if you opt-in to share schema metadata.'
|
||||
: aiOptInLevel === 'schema'
|
||||
? 'Sharing query data in addition to schema can further improve responses. Update AI settings to enable this.'
|
||||
: ''
|
||||
: aiOptInLevel === 'disabled'
|
||||
? 'The Assistant can provide better answers if you opt-in to share schema metadata.'
|
||||
: aiOptInLevel === 'schema'
|
||||
? 'Sharing query data in addition to schema can further improve responses. Update AI settings to enable this.'
|
||||
: ''
|
||||
}
|
||||
className="border-0 border-b rounded-none bg-background"
|
||||
>
|
||||
{!isHipaaProjectDisallowed && (
|
||||
<Button className="w-fit mt-4" onClick={() => onVisibleChange(true)}>
|
||||
Permission settings
|
||||
</Button>
|
||||
)}
|
||||
<Button className="w-fit mt-4" onClick={() => onVisibleChange(true)}>
|
||||
Permission settings
|
||||
</Button>
|
||||
</Admonition>
|
||||
)}
|
||||
<AIOptInModal visible={visible} onCancel={() => onVisibleChange(false)} />
|
||||
|
||||
@@ -51,7 +51,6 @@ export interface AssistantChatHeaderProps {
|
||||
isChatLoading: boolean
|
||||
showMetadataWarning: boolean
|
||||
updatedOptInSinceMCP: boolean
|
||||
isHipaaProjectDisallowed: boolean
|
||||
aiOptInLevel: 'disabled' | 'schema' | 'full' | string | undefined
|
||||
}
|
||||
|
||||
@@ -110,7 +109,7 @@ export const AssistantChat = ({
|
||||
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
const { aiOptInLevel, isHipaaProjectDisallowed } = useOrgAiOptInLevel()
|
||||
const { aiOptInLevel } = useOrgAiOptInLevel()
|
||||
// Whether attached queries are sent at all. One definition, shared by the chat form
|
||||
// (which folds them into the message text) and the message metadata (which states
|
||||
// whether any of them was a logs query), so the two can't disagree.
|
||||
@@ -559,7 +558,6 @@ export const AssistantChat = ({
|
||||
isChatLoading,
|
||||
showMetadataWarning,
|
||||
updatedOptInSinceMCP,
|
||||
isHipaaProjectDisallowed,
|
||||
aiOptInLevel,
|
||||
})}
|
||||
{hasMessages ? (
|
||||
|
||||
@@ -34,8 +34,7 @@ export const SaveSnippetDialog = ({ open, sql, onOpenChange, onSave }: SaveSnipp
|
||||
|
||||
const isApiKeySet = !!check?.hasKey
|
||||
|
||||
// Orgs on HIPAA plans or that have disabled AI should not have access to Supabase AI
|
||||
const { aiOptInLevel, isHipaaProjectDisallowed } = useOrgAiOptInLevel()
|
||||
const { aiOptInLevel } = useOrgAiOptInLevel()
|
||||
const isAiOptedOut = aiOptInLevel === 'disabled'
|
||||
|
||||
const { mutate: generateTitle, isPending: isGenerating } = useSqlTitleGenerateMutation({
|
||||
@@ -78,18 +77,16 @@ export const SaveSnippetDialog = ({ open, sql, onOpenChange, onSave }: SaveSnipp
|
||||
<div className="flex justify-end">
|
||||
<ButtonTooltip
|
||||
size="tiny"
|
||||
disabled={isGenerating || !isApiKeySet || isHipaaProjectDisallowed || isAiOptedOut}
|
||||
disabled={isGenerating || !isApiKeySet || isAiOptedOut}
|
||||
onClick={() => generateTitle({ sql })}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text: isHipaaProjectDisallowed
|
||||
? 'This feature is not available for HIPAA projects.'
|
||||
: isAiOptedOut
|
||||
? 'Your organization has opted out of AI features.'
|
||||
: isApiKeySet
|
||||
? undefined
|
||||
: 'Add your "OPENAI_API_KEY" to your environment variables to use this feature.',
|
||||
text: isAiOptedOut
|
||||
? 'Your organization has opted out of AI features.'
|
||||
: isApiKeySet
|
||||
? undefined
|
||||
: 'Add your "OPENAI_API_KEY" to your environment variables to use this feature.',
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
import { subscriptionHasHipaaAddon } from '@/components/interfaces/Billing/Subscription/Subscription.utils'
|
||||
import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
|
||||
import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { IS_PLATFORM, OPT_IN_TAGS } from '@/lib/constants'
|
||||
|
||||
export const aiOptInLevelSchema = z.enum([
|
||||
@@ -49,9 +45,7 @@ export function useOrgOptedIntoAi(): boolean {
|
||||
export function useOrgAiOptInLevel(): {
|
||||
aiOptInLevel: AiOptInLevel
|
||||
includeSchemaMetadata: boolean
|
||||
isHipaaProjectDisallowed: boolean
|
||||
} {
|
||||
const { data: selectedProject } = useSelectedProjectQuery()
|
||||
const { data: selectedOrganization } = useSelectedOrganizationQuery()
|
||||
|
||||
// [Joshen] Default to disabled until migration to clean up existing opt in tags are completed
|
||||
@@ -60,25 +54,9 @@ export function useOrgAiOptInLevel(): {
|
||||
const level = getAiOptInLevel(optInTags)
|
||||
const isOptedIntoAI = level !== 'disabled'
|
||||
|
||||
const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: selectedOrganization?.slug })
|
||||
const hasHipaaAddon = subscriptionHasHipaaAddon(subscription)
|
||||
|
||||
const { data: projectSettings } = useProjectSettingsV2Query({ projectRef: selectedProject?.ref })
|
||||
const isProjectSensitive = !!projectSettings?.is_sensitive
|
||||
|
||||
const preventProjectFromUsingAI = hasHipaaAddon && isProjectSensitive
|
||||
|
||||
// [Joshen] For CLI / self-host, we'd default to 'schema' as opt in level
|
||||
const aiOptInLevel = !IS_PLATFORM
|
||||
? 'schema'
|
||||
: (isOptedIntoAI && !selectedProject) || (isOptedIntoAI && !preventProjectFromUsingAI)
|
||||
? level
|
||||
: 'disabled'
|
||||
const aiOptInLevel = !IS_PLATFORM ? 'schema' : isOptedIntoAI ? level : 'disabled'
|
||||
const includeSchemaMetadata = !IS_PLATFORM || aiOptInLevel !== 'disabled'
|
||||
|
||||
return {
|
||||
aiOptInLevel,
|
||||
includeSchemaMetadata,
|
||||
isHipaaProjectDisallowed: preventProjectFromUsingAI,
|
||||
}
|
||||
return { aiOptInLevel, includeSchemaMetadata }
|
||||
}
|
||||
@@ -93,7 +93,6 @@ describe('getAIDetails', () => {
|
||||
planId: 'pro',
|
||||
region: 'us-east-1',
|
||||
isSensitive: false,
|
||||
isRestrictedByHipaa: false,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -233,49 +232,20 @@ describe('getAIDetails', () => {
|
||||
expect(result.region).toBeUndefined()
|
||||
})
|
||||
|
||||
describe('HIPAA organizations', () => {
|
||||
beforeEach(() => {
|
||||
mockSubscriptionHasHipaaAddon.mockReturnValue(true)
|
||||
mockGetAiOptInLevel.mockReturnValue('schema_and_log_and_data')
|
||||
it('keeps the opt-in level for a sensitive project in a HIPAA org', async () => {
|
||||
mockSubscriptionHasHipaaAddon.mockReturnValue(true)
|
||||
mockGetAiOptInLevel.mockReturnValue('schema_and_log_and_data')
|
||||
mockGetProjectSettings.mockResolvedValue({ is_sensitive: true })
|
||||
|
||||
const result = await getAIDetails({
|
||||
orgSlug: ORG_SLUG,
|
||||
projectRef: PROJECT_REF,
|
||||
authorization: AUTH,
|
||||
})
|
||||
|
||||
it('disables the opt-in level for a sensitive project', async () => {
|
||||
mockGetProjectSettings.mockResolvedValue({ is_sensitive: true })
|
||||
|
||||
const result = await getAIDetails({
|
||||
orgSlug: ORG_SLUG,
|
||||
projectRef: PROJECT_REF,
|
||||
authorization: AUTH,
|
||||
})
|
||||
|
||||
expect(result.aiOptInLevel).toBe('disabled')
|
||||
expect(result.hasHipaaAddon).toBe(true)
|
||||
expect(result.isRestrictedByHipaa).toBe(true)
|
||||
})
|
||||
|
||||
it('disables the opt-in level when project sensitivity is unknown', async () => {
|
||||
mockGetProjectSettings.mockResolvedValue(undefined)
|
||||
|
||||
const result = await getAIDetails({
|
||||
orgSlug: ORG_SLUG,
|
||||
projectRef: PROJECT_REF,
|
||||
authorization: AUTH,
|
||||
})
|
||||
|
||||
expect(result.aiOptInLevel).toBe('disabled')
|
||||
})
|
||||
|
||||
it('keeps the opt-in level for a project explicitly marked not sensitive', async () => {
|
||||
mockGetProjectSettings.mockResolvedValue({ is_sensitive: false })
|
||||
|
||||
const result = await getAIDetails({
|
||||
orgSlug: ORG_SLUG,
|
||||
projectRef: PROJECT_REF,
|
||||
authorization: AUTH,
|
||||
})
|
||||
|
||||
expect(result.aiOptInLevel).toBe('schema_and_log_and_data')
|
||||
})
|
||||
expect(result.aiOptInLevel).toBe('schema_and_log_and_data')
|
||||
expect(result.hasHipaaAddon).toBe(true)
|
||||
expect(result.isSensitive).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps the opt-in level for a sensitive project outside a HIPAA org', async () => {
|
||||
|
||||
@@ -15,8 +15,6 @@ export type AIDetails = {
|
||||
planId: string | undefined
|
||||
region: string | undefined
|
||||
isSensitive: boolean | null | undefined
|
||||
// True when HIPAA forced the opt-in level to `disabled`, rather than the org choosing it.
|
||||
isRestrictedByHipaa: boolean
|
||||
}
|
||||
|
||||
// Resolves the AI opt-in level, model access and tracing inputs for one org/project pair.
|
||||
@@ -63,17 +61,13 @@ export const getAIDetails = async ({
|
||||
planId: undefined,
|
||||
region,
|
||||
isSensitive,
|
||||
isRestrictedByHipaa: false,
|
||||
}
|
||||
}
|
||||
|
||||
const hasHipaaAddon = subscriptionHasHipaaAddon(subscription)
|
||||
|
||||
// Mirrors the client-side gate in useOrgAiOptInLevel, which had no server-side equivalent
|
||||
const isRestrictedByHipaa = hasHipaaAddon && isSensitive !== false
|
||||
|
||||
return {
|
||||
aiOptInLevel: isRestrictedByHipaa ? 'disabled' : getAiOptInLevel(selectedOrg.opt_in_tags),
|
||||
aiOptInLevel: getAiOptInLevel(selectedOrg.opt_in_tags),
|
||||
hasAccessToAdvanceModel: advanceModelAccess.hasAccess,
|
||||
hasHipaaAddon,
|
||||
orgId: selectedOrg.id,
|
||||
@@ -81,6 +75,5 @@ export const getAIDetails = async ({
|
||||
planId: selectedOrg.plan.id,
|
||||
region,
|
||||
isSensitive,
|
||||
isRestrictedByHipaa,
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ describe('tool allowance by opt-in level', () => {
|
||||
query_logs: { execute: vitest.fn().mockResolvedValue({ status: 'success' }) },
|
||||
} as unknown as ToolSet
|
||||
|
||||
const filtered = filterToolsByOptInLevel(mockTools, optInLevel as any, false)
|
||||
const filtered = filterToolsByOptInLevel(mockTools, optInLevel as any)
|
||||
const allowedTools: string[] = []
|
||||
|
||||
Object.entries(filtered).forEach(([toolName, tool]) => {
|
||||
@@ -163,14 +163,14 @@ describe('filterToolsByOptInLevel', () => {
|
||||
}
|
||||
|
||||
it('should filter out unknown tools entirely', async () => {
|
||||
const tools = filterToolsByOptInLevel(mockTools, 'disabled', false)
|
||||
const tools = filterToolsByOptInLevel(mockTools, 'disabled')
|
||||
|
||||
// Unknown tools should be completely filtered out (not present in result)
|
||||
expect(tools).not.toHaveProperty('some_other_tool')
|
||||
})
|
||||
|
||||
it('should always allow UI tools regardless of opt-in level', async () => {
|
||||
const tools = filterToolsByOptInLevel(mockTools, 'disabled', false)
|
||||
const tools = filterToolsByOptInLevel(mockTools, 'disabled')
|
||||
|
||||
expect(tools).toHaveProperty('execute_sql')
|
||||
expect(tools).toHaveProperty('deploy_edge_function')
|
||||
@@ -189,7 +189,7 @@ describe('filterToolsByOptInLevel', () => {
|
||||
})
|
||||
|
||||
it('should stub all managed tools for disabled opt-in level', async () => {
|
||||
const tools = filterToolsByOptInLevel(mockTools, 'disabled', false)
|
||||
const tools = filterToolsByOptInLevel(mockTools, 'disabled')
|
||||
|
||||
await expectStubsFor(tools, [
|
||||
'list_tables',
|
||||
@@ -203,7 +203,7 @@ describe('filterToolsByOptInLevel', () => {
|
||||
})
|
||||
|
||||
it('should stub log tools for schema opt-in level', async () => {
|
||||
const tools = filterToolsByOptInLevel(mockTools, 'schema', false)
|
||||
const tools = filterToolsByOptInLevel(mockTools, 'schema')
|
||||
|
||||
await expectStubsFor(tools, ['get_advisors', 'query_logs'])
|
||||
})
|
||||
@@ -211,7 +211,7 @@ describe('filterToolsByOptInLevel', () => {
|
||||
// No execute_sql tool, so nothing additional to stub for schema_and_log opt-in level
|
||||
|
||||
it('should not stub any tools for schema_and_log_and_data opt-in level', async () => {
|
||||
const tools = filterToolsByOptInLevel(mockTools, 'schema_and_log_and_data', false)
|
||||
const tools = filterToolsByOptInLevel(mockTools, 'schema_and_log_and_data')
|
||||
|
||||
await expectStubsFor(tools, [])
|
||||
})
|
||||
@@ -226,7 +226,7 @@ describe('createPrivacyMessageTool', () => {
|
||||
toModelOutput: vitest.fn(),
|
||||
}
|
||||
|
||||
const privacyTool = createPrivacyMessageTool(originalTool, false)
|
||||
const privacyTool = createPrivacyMessageTool(originalTool)
|
||||
|
||||
expect(privacyTool.description).toContain('Original description')
|
||||
expect(privacyTool.description).toContain('Requires opting in')
|
||||
@@ -236,24 +236,6 @@ describe('createPrivacyMessageTool', () => {
|
||||
expect(result.status).toContain("You don't have permission to use this tool")
|
||||
expect(result.status).toContain('third-party AI providers')
|
||||
})
|
||||
|
||||
it('uses HIPAA copy when the project is HIPAA-restricted', async () => {
|
||||
const originalTool = {
|
||||
description: 'Original description',
|
||||
inputSchema: z.object({}),
|
||||
execute: vitest.fn(),
|
||||
}
|
||||
|
||||
const hipaaTool = createPrivacyMessageTool(originalTool, true)
|
||||
const optInTool = createPrivacyMessageTool(originalTool, false)
|
||||
|
||||
const hipaaStatus = (await hipaaTool.execute({}, {})).status
|
||||
const optInStatus = (await optInTool.execute({}, {})).status
|
||||
|
||||
expect(hipaaStatus).toContain('HIPAA')
|
||||
expect(hipaaStatus).not.toEqual(optInStatus)
|
||||
expect(hipaaTool.description).not.toEqual(optInTool.description)
|
||||
})
|
||||
})
|
||||
|
||||
describe('toolSetValidationSchema', () => {
|
||||
|
||||
@@ -167,16 +167,11 @@ function isToolAllowed(toolName: string, aiOptInLevel: AiOptInLevel): boolean {
|
||||
* Names no provider. Studio switches inference providers, and a stale name here becomes a
|
||||
* false privacy claim shown to users.
|
||||
*/
|
||||
export function createPrivacyMessageTool(
|
||||
toolInstance: Tool<any, any>,
|
||||
isRestrictedByHipaa: boolean
|
||||
) {
|
||||
const privacyMessage = isRestrictedByHipaa
|
||||
? "You don't have permission to use this tool. This project is configured as High Compliance and your organization has the HIPAA add-on, so Supabase does not send project metadata with your prompts. The user cannot change this in their organization's AI settings, so continue without project metadata rather than asking them to opt in."
|
||||
: "You don't have permission to use this tool. This is an organization-wide setting requiring you to opt-in. Please choose your preferred data sharing level in your organization's settings. By default, no data is shared. Granting permission allows Supabase to send information (like schema, logs, or data, depending on your chosen level) to third-party AI providers solely to generate responses."
|
||||
const condensedPrivacyMessage = isRestrictedByHipaa
|
||||
? 'Unavailable because this project is configured as High Compliance, which prevents project metadata from being shared. This cannot be changed in organization settings.'
|
||||
: 'Requires opting in to sharing data with third-party AI providers. You can opt in via organization settings.'
|
||||
export function createPrivacyMessageTool(toolInstance: Tool<any, any>) {
|
||||
const privacyMessage =
|
||||
"You don't have permission to use this tool. This is an organization-wide setting requiring you to opt-in. Please choose your preferred data sharing level in your organization's settings. By default, no data is shared. Granting permission allows Supabase to send information (like schema, logs, or data, depending on your chosen level) to third-party AI providers solely to generate responses."
|
||||
const condensedPrivacyMessage =
|
||||
'Requires opting in to sharing data with third-party AI providers. You can opt in via organization settings.'
|
||||
const toolDescription = toolInstance.description
|
||||
const description =
|
||||
typeof toolDescription === 'function'
|
||||
@@ -197,11 +192,7 @@ export function createPrivacyMessageTool(
|
||||
/**
|
||||
* Filter tools based on the AI opt-in level
|
||||
*/
|
||||
export function filterToolsByOptInLevel(
|
||||
tools: ToolSet,
|
||||
aiOptInLevel: AiOptInLevel,
|
||||
isRestrictedByHipaa: boolean
|
||||
): ToolSet {
|
||||
export function filterToolsByOptInLevel(tools: ToolSet, aiOptInLevel: AiOptInLevel): ToolSet {
|
||||
return Object.fromEntries(
|
||||
Object.entries(tools)
|
||||
.filter(([toolName]) => TOOL_CATEGORY_MAP[toolName] !== undefined)
|
||||
@@ -211,7 +202,7 @@ export function filterToolsByOptInLevel(
|
||||
}
|
||||
|
||||
// If the tool is not allowed, provide a stub that returns a privacy message
|
||||
return [toolName, createPrivacyMessageTool(toolInstance, isRestrictedByHipaa)]
|
||||
return [toolName, createPrivacyMessageTool(toolInstance)]
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -18,7 +18,6 @@ const BASE_PARAMS = {
|
||||
connectionString: 'postgresql://localhost',
|
||||
authorization: 'Bearer token',
|
||||
aiOptInLevel: 'schema_and_log_and_data' as const,
|
||||
isRestrictedByHipaa: false,
|
||||
accessToken: 'access-token',
|
||||
baseUrl: 'https://supabase.com/dashboard',
|
||||
signal: new AbortController().signal,
|
||||
@@ -40,7 +39,6 @@ describe('ai/tools getTools', () => {
|
||||
accessToken: BASE_PARAMS.accessToken,
|
||||
projectRef: BASE_PARAMS.projectRef,
|
||||
aiOptInLevel: BASE_PARAMS.aiOptInLevel,
|
||||
isRestrictedByHipaa: BASE_PARAMS.isRestrictedByHipaa,
|
||||
signal: BASE_PARAMS.signal,
|
||||
})
|
||||
expect(tools).toHaveProperty('studio_tool')
|
||||
|
||||
@@ -17,7 +17,6 @@ export const getTools = async ({
|
||||
connectionString,
|
||||
authorization,
|
||||
aiOptInLevel,
|
||||
isRestrictedByHipaa,
|
||||
accessToken,
|
||||
baseUrl,
|
||||
supportMode,
|
||||
@@ -28,8 +27,6 @@ export const getTools = async ({
|
||||
connectionString: string
|
||||
authorization?: string
|
||||
aiOptInLevel: AiOptInLevel
|
||||
// Only changes the blocked-tool wording.
|
||||
isRestrictedByHipaa: boolean
|
||||
accessToken?: string
|
||||
baseUrl?: string
|
||||
supportMode?: boolean
|
||||
@@ -66,7 +63,6 @@ export const getTools = async ({
|
||||
accessToken,
|
||||
projectRef,
|
||||
aiOptInLevel,
|
||||
isRestrictedByHipaa,
|
||||
signal,
|
||||
})
|
||||
} catch (error) {
|
||||
@@ -95,11 +91,7 @@ export const getTools = async ({
|
||||
|
||||
// Filter all tools based on the (potentially modified) AI opt-in level
|
||||
const toolsWithSupport = supportMode ? { ...tools, ...getSupportLifecycleTools() } : tools
|
||||
const filteredTools: ToolSet = filterToolsByOptInLevel(
|
||||
toolsWithSupport,
|
||||
aiOptInLevel,
|
||||
isRestrictedByHipaa
|
||||
)
|
||||
const filteredTools: ToolSet = filterToolsByOptInLevel(toolsWithSupport, aiOptInLevel)
|
||||
|
||||
return filteredTools
|
||||
}
|
||||
@@ -11,7 +11,6 @@ const BASE_PARAMS = {
|
||||
accessToken: 'token',
|
||||
projectRef: 'abcdefghijklmnopqrst',
|
||||
aiOptInLevel: 'schema_and_log_and_data' as const,
|
||||
isRestrictedByHipaa: false,
|
||||
// A fresh, non-aborted signal by default; lifecycle tests override it
|
||||
signal: new AbortController().signal,
|
||||
}
|
||||
|
||||
@@ -41,13 +41,11 @@ export const getMcpTools = async ({
|
||||
accessToken,
|
||||
projectRef,
|
||||
aiOptInLevel,
|
||||
isRestrictedByHipaa,
|
||||
signal,
|
||||
}: {
|
||||
accessToken: string
|
||||
projectRef: string
|
||||
aiOptInLevel: AiOptInLevel
|
||||
isRestrictedByHipaa: boolean
|
||||
// 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
|
||||
@@ -101,11 +99,7 @@ export const getMcpTools = async ({
|
||||
// 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,
|
||||
isRestrictedByHipaa
|
||||
)
|
||||
const allowedMcpTools = filterToolsByOptInLevel(availableMcpTools, aiOptInLevel)
|
||||
|
||||
// Remove UI-executed tools handled locally
|
||||
const filteredMcpTools: ToolSet = { ...allowedMcpTools }
|
||||
|
||||
@@ -120,7 +120,6 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse, claims?: Jw
|
||||
const includesLogsSnippets = messagesIncludeLogsSnippets(messages)
|
||||
|
||||
let aiOptInLevel: AiOptInLevel = 'disabled'
|
||||
let isRestrictedByHipaa = false
|
||||
let hasAccessToAdvanceModel = false
|
||||
let orgHasHipaaAddon: boolean | undefined
|
||||
let projectIsSensitive: boolean | null | undefined
|
||||
@@ -139,7 +138,6 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse, claims?: Jw
|
||||
const aiDetails = await getAIDetails({ orgSlug: rawOrgSlug, projectRef, authorization })
|
||||
|
||||
aiOptInLevel = aiDetails.aiOptInLevel
|
||||
isRestrictedByHipaa = aiDetails.isRestrictedByHipaa
|
||||
hasAccessToAdvanceModel = aiDetails.hasAccessToAdvanceModel
|
||||
orgHasHipaaAddon = aiDetails.hasHipaaAddon
|
||||
orgId = aiDetails.orgId
|
||||
@@ -189,7 +187,6 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse, claims?: Jw
|
||||
connectionString,
|
||||
authorization,
|
||||
aiOptInLevel,
|
||||
isRestrictedByHipaa,
|
||||
accessToken,
|
||||
baseUrl: getURL(),
|
||||
supportMode,
|
||||
|
||||
@@ -73,7 +73,6 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse, clai
|
||||
const { tableName, schema, columns = [], projectRef, connectionString, orgSlug, message } = data
|
||||
|
||||
let aiOptInLevel: AiOptInLevel = 'disabled'
|
||||
let isRestrictedByHipaa = false
|
||||
|
||||
if (!IS_PLATFORM) {
|
||||
aiOptInLevel = 'schema'
|
||||
@@ -84,7 +83,6 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse, clai
|
||||
const aiDetails = await getAIDetails({ orgSlug, projectRef, authorization })
|
||||
|
||||
aiOptInLevel = aiDetails.aiOptInLevel
|
||||
isRestrictedByHipaa = aiDetails.isRestrictedByHipaa
|
||||
} catch (error) {
|
||||
return res.status(400).json({
|
||||
error: 'There was an error fetching your organization details',
|
||||
@@ -119,7 +117,6 @@ export async function handlePost(req: NextApiRequest, res: NextApiResponse, clai
|
||||
connectionString,
|
||||
authorization,
|
||||
aiOptInLevel,
|
||||
isRestrictedByHipaa,
|
||||
accessToken,
|
||||
isExplorerEnabled: explorerEnabled,
|
||||
signal: toolsAbortController.signal,
|
||||
|
||||
Reference in new issue
Block a user