mirror of
https://github.com/supabase/supabase.git
synced 2026-10-19 08:09:11 +03:00
## Summary Migrates the create, delete, and run notebook assistant tools to the tool contract + server pattern used by `update_notebook`. Each tool now defines its schema once in a shared contract, with server-only execution split into a `.server.ts` module. This removes duplicated client-side schemas and gives the three tools one shared notebook mutation output schema. ## Changes - Add `create-notebook`, `delete-notebook`, and `run-notebook` tool contracts, each with a `.server.ts` implementation. - Remove the duplicated client-side schemas for these tools from `notebook-tools.ts` and the registry. - Add `notebook-mutation-output.ts` as the single output schema shared by the notebook mutation tools. - Add `lib/ai/opt-in-level.ts` with the opt-in level helpers, extracted from the existing module to break an import cycle. - Add optional `ToolContract.outputOnInvalid` so `run_notebook` history replay can reuse a stored output when input fails validation. - Update the AI assistant panel renderers (proposal, run, opt-in modal and request) and the tool filter, sanitizer, and cache invalidation to use the new contracts. ## Deviations worth reviewing - **`outputOnInvalid` on `ToolContract`.** This is a new optional hook. It exists so `run_notebook` history replay can return a valid output when stored input no longer passes validation. Reviewers should confirm this is the right level for the hook rather than handling it inside the tool. - **`opt-in-level.ts` extraction.** The helpers were moved into their own module to break a circular import between the tool modules and the opt-in components. This adds one file, but no behavior change is intended. - **`create-notebook` contract imports the data-layer `notebook-schema`.** This matches `update-notebook`. It should be moved behind the contract before the planned ESLint rule step, which restricts contract modules from importing data-layer code. ## Testing - `vitest` for `lib/ai`, `components/ui/AIAssistantPanel`, and `state`: 920 tests pass across 71 files. - `tsc`: one error remains in `packages/ui-patterns` (`InstructionBlocks.tsx`), which is unrelated to this change. I have not verified whether it also fails on the base branch. - ESLint reports no errors on the changed files (9 `no-explicit-any` warnings in `tool-filter.ts` and its test); Prettier is clean. - Not run: knip and the lint ratchet.
62 lines
2.3 KiB
TypeScript
62 lines
2.3 KiB
TypeScript
import { untrustedSql } from '@supabase/pg-meta'
|
|
|
|
import { getAuthHeaders } from './auth-headers'
|
|
import { contract } from './create-notebook'
|
|
import { assertValidDatabaseIdentifiers } from './notebook-tool-error'
|
|
import { approvalGatedToolFromContract } from './tool-from-contract'
|
|
import type { ApprovalGatedToolFactory } from './types'
|
|
import type { WritableCell, WritableNotebook } from '@/data/content/notebooks/notebook-schema'
|
|
import { createNotebook } from '@/data/content/notebooks/notebook-upsert-mutation'
|
|
import { untrustedLogSql } from '@/data/logs/safe-analytics-sql'
|
|
import { getReadReplicas } from '@/data/read-replicas/replicas-query'
|
|
|
|
export const createTool: ApprovalGatedToolFactory<typeof contract> = (ctx) => {
|
|
const { projectRef } = ctx
|
|
const authHeaders = getAuthHeaders(ctx.authorization)
|
|
|
|
return approvalGatedToolFromContract(
|
|
contract,
|
|
ctx,
|
|
async ({ name, description, content }, { abortSignal }, gate) => {
|
|
if (
|
|
content.cells.some(
|
|
(cell) => cell._tag === 'database_cell' && cell.database_identifier !== undefined
|
|
)
|
|
) {
|
|
const databases = await getReadReplicas({ projectRef }, abortSignal, authHeaders)
|
|
assertValidDatabaseIdentifiers(
|
|
content.cells,
|
|
new Set((databases ?? []).map((database) => database.identifier))
|
|
)
|
|
}
|
|
|
|
// `gate` is only handed to an `execute` that runs after the user approves, so that
|
|
// approval is the user gesture that promotes each cell's SQL from untrusted to safe.
|
|
// Keep the promotion here, not in a shared helper, so it's auditable directly.
|
|
const cells: WritableNotebook['cells'] = content.cells.map((cell): WritableCell => {
|
|
switch (cell._tag) {
|
|
case 'markdown_cell':
|
|
return cell
|
|
case 'database_cell':
|
|
return { ...cell, sql: gate.acceptUntrustedSql(untrustedSql(cell.sql)) }
|
|
case 'log_cell':
|
|
return { ...cell, sql: gate.acceptUntrustedLogsSql(untrustedLogSql(cell.sql)) }
|
|
}
|
|
})
|
|
|
|
const result = await createNotebook(
|
|
{
|
|
projectRef: projectRef ?? '',
|
|
name,
|
|
description,
|
|
content: { schema_version: content.schema_version, cells },
|
|
},
|
|
abortSignal,
|
|
authHeaders
|
|
)
|
|
|
|
return { id: result.id, name }
|
|
}
|
|
)
|
|
}
|