/** * @fileoverview CanvasBridge — server-side adapter over the framework's * `DataCanvas` primitive. Owns three concerns the framework leaves to the * consumer: (1) default canvas resolution (per-session under * `BRAPI_SESSION_ISOLATION=true` with a present `ctx.sessionId`, per-tenant * otherwise; the agent never passes `canvas_id`, the bridge caches and reuses * it via `ctx.state`), (2) generating SQL-safe `df_` table names for * spilled find_* results, and (3) tracking originating-source provenance so * `brapi_dataframe_describe` can surface it. * * Canvas is mandatory — `core.canvas` must be configured (DuckDB) for the * server to start. There is no on/off toggle; spillover always lands on the * canvas. * * @module services/canvas-bridge/canvas-bridge */ import type { Context } from '@cyanheads/mcp-ts-core'; import type { CanvasInstance, DataCanvas, ExportResult, ExportTarget, QueryResult, RegisterTableResult } from '@cyanheads/mcp-ts-core/canvas'; import type { ServerConfig } from '../../config/server-config.js'; import type { DescribedTable } from './types.js'; /** Input to {@link CanvasBridge.registerDataframe}. */ export interface RegisterDataframeInput { /** Originating BrAPI baseUrl. */ baseUrl: string; /** Cap that fired before the upstream was exhausted. Omit when no cap fired. */ maxRows?: number; /** Original filter map / query — provenance for reproducibility. */ query: unknown; /** Row payload to materialize. */ rows: Record[]; /** Originating tool (e.g. `find_observations`). */ source: string; /** True when the producer hit a row/page cap before exhausting upstream. */ truncated?: boolean; } /** Result of {@link CanvasBridge.registerDataframe} — the dataframe handle. */ export interface RegisterDataframeResult { /** * Maps each sanitized column name back to its original upstream key, for * columns renamed to clear the SQL-safe-identifier gate (e.g. `end` → `end_`). * Present only when at least one column was renamed. */ columnLegend?: Record; columns: string[]; createdAt: string; expiresAt: string; maxRows?: number; rowCount: number; tableName: string; truncated?: boolean; } /** * Bridge for `core.canvas` that adds default-canvas semantics — session-scoped * under `BRAPI_SESSION_ISOLATION=true` with a present `ctx.sessionId`, * tenant-scoped otherwise. */ export declare class CanvasBridge { private readonly canvas; private readonly serverConfig; constructor(canvas: DataCanvas, serverConfig: ServerConfig); /** * Acquire the default canvas for the current request. The canvasId is cached * in `ctx.state` so subsequent calls reuse the same canvas (sliding TTL on * the canvas itself keeps it warm). On stale-id errors (NotFound), clears * the cache and acquires a fresh canvas — matches the spirit of the * framework's "omit on retry" recovery hint without surfacing the token to * the agent. * * Scope follows `BRAPI_SESSION_ISOLATION`: when true (default) and * `ctx.sessionId` is present, the canvas is session-scoped — the cache key * folds the session ID in, so concurrent HTTP sessions in the same tenant * end up on distinct canvases. When isolation is disabled or no session ID * is available (stdio, stateless HTTP without opt-in), falls back to the * tenant-shared canvas. */ getInstance(ctx: Context): Promise; /** * Resolve the cache key for the default-canvas pointer. Session-scoped when * `BRAPI_SESSION_ISOLATION=true` and `ctx.sessionId` is present; per-tenant * otherwise. See `getInstance` for the full scoping rules. */ private defaultCanvasKey; /** * Materialize a spilled `find_*` result as a canvas dataframe. Generates a * `df_` table name, registers the rows with an all-nullable schema, * persists provenance metadata in `ctx.state`, and returns the full * dataframe handle. */ registerDataframe(ctx: Context, input: RegisterDataframeInput): Promise; /** * Run a SQL query against the default canvas (see `getInstance` for the * session-vs-tenant scoping rules). Caps `rowLimit` to * `BRAPI_CANVAS_MAX_ROWS` so a missing/excessive caller value can't bypass * the response-size budget. Cancellation flows through `ctx.signal`; the * timeout wraps the AbortSignal with a wall-clock cap. * * Pre-gate: rejects SQL that reaches into DuckDB's system catalogs * (`information_schema`, `pg_catalog`, `sqlite_master`, `duckdb_*` metadata * functions). Under shared-tenant deployments the canvas hosts every * caller's dataframes; without this gate, `SELECT * FROM information_schema.tables` * leaks the full df_ namespace and bypasses brapi_dataframe_describe's * possession-required policy. */ query(ctx: Context, sql: string, options?: { preview?: number; registerAs?: string; rowLimit?: number; }): Promise; /** * Register an explicitly-named table (used by the rare consumer that wants * to push rows directly without going through the spillover path). */ registerTable(ctx: Context, name: string, rows: Record[]): Promise; /** Drop a single canvas table by name. Returns true when found and removed. */ drop(ctx: Context, name: string): Promise; /** * Export a canvas table to a path target. The framework resolves the * relative path against `CANVAS_EXPORT_PATH` and rejects absolute / * traversal inputs — the bridge only forwards. * * Tracking the resulting path under the *source* dataframe name (so a * subsequent `drop(sourceName)` can unlink it) is the caller's job. The * bridge can't infer "source name" when the export reads from a transient * derived table (e.g. a projection materialized for one export call) — * auto-tracking under whatever `tableName` was passed would unlink the * file the moment the derived table is dropped. See * `pairExportToSourceDataframe` in `brapi-dataframe-export.tool.ts`. */ export(ctx: Context, tableName: string, target: ExportTarget, options?: { signal?: AbortSignal; }): Promise; /** * Track an exported file path under a source dataframe name so that a * later `drop(sourceName)` unlinks it. Persisted in `ctx.state` with the * remaining TTL of the source dataframe; if the dataframe expires via * TTL, the entry disappears and the file is reaped opportunistically by * the export tool's mtime sweep. */ trackExport(ctx: Context, sourceName: string, path: string): Promise; private unlinkTrackedExports; /** * Describe one or all canvas tables, augmenting the framework's `TableInfo` * with originating-source provenance for auto-registered tables. */ describe(ctx: Context, options?: { tableName?: string; }): Promise; private augmentTableInfo; } /** * Reason string set on `validationError.data.reason` when a query is rejected * for reaching into DuckDB's system catalogs. Exported so the dataframe-query * tool can recognize it alongside the framework's `SQL_GATE_REASONS` and route * it through the typed `sql_rejected` contract. */ export declare const SYSTEM_CATALOG_ACCESS_REASON: "system_catalog_access"; /** * Initialize the singleton bridge. The framework canvas is mandatory — the * caller is responsible for ensuring `core.canvas` is defined before this * runs (`createApp` setup hook should fail closed when it isn't). */ export declare function initCanvasBridge(canvas: DataCanvas, serverConfig: ServerConfig): CanvasBridge; export declare function getCanvasBridge(): CanvasBridge; /** Test-only — clear the singleton between suites. */ export declare function resetCanvasBridge(): void; //# sourceMappingURL=canvas-bridge.d.ts.map