Conduit
Data connectors & domain components
The data layer starts with a connector — a scoped, read-only external data source — and is consumed through domain components: named, parameterized queries that humans author and the platform governs. Together they are the only path from a question to an external system. Connectors come in three kinds — postgres, mysql, and snowflake — behind one adapter contract, so governance, provenance, and citations are identical regardless of engine. Concepts first: Semantic ↔ data layer.
Connectors
A connector separates what is secret from what is not:
Connector fields
Credentials are write-only
enc:v1 ciphertext, and every read returns a masked placeholder. Decryption happens exclusively inside the executor at call time, with short-lived connections per call. Use a read-only database role for the connector user — read-only is enforced in the platform, but defense in depth is free.Test connection. Each connector has a test action that opens a connection, runs a trivial probe, and stamps lastCheckedAt / lastStatus — so the Data page shows at a glance which sources are healthy before an agent ever depends on them.
Engines
Each kind maps to one engine adapter — the only code that touches a driver SDK. Adapters share one interface (test connection, discover schema, execute under caps), so everything downstream of the connector is engine-agnostic. What differs per engine is the non-secret config shape and how read-only is enforced at the session level.
postgres config
mysql config
snowflake config
Read-only enforcement differs by engine — know your floor. Every execution on every engine passes the shared statement gate first: single-statement SELECT/WITH only, validated at save time and again at execution time. On top of that:
- Postgres and MySQL additionally set the session read-only mode on every short-lived connection before the statement runs — a second, engine-level wall: even a statement that slipped the gate could not write.
- Snowflakehas no session read-only mode, so enforcement there is the statement gate plus your role grants. This is exactly the case where the "read-only role" recommendation stops being defense in depth and becomes the second wall itself — grant the connector role
SELECTonly.
One template dialect. Domain component templates are always written with positional $1..$nplaceholders, whatever the engine. The adapter rewrites them to the driver's native style ($n for Postgres, ? binds for MySQL and Snowflake) with a pure, unit-tested rewriter — values always travel as bound parameters, never interpolated into SQL text.
Schema discovery
Authoring a good query requires knowing what is there. Each connector has a discoveraction that reads the engine's information_schema (through the same read-only, capped execution path) and stores a schema snapshot on the connector: every visible table with its schema, columns (name, engine-native type, nullability), and a cheap row-count estimate where the engine offers one. The snapshot is stamped discoveredAt, so you always know how fresh your view of the source is — re-run discovery any time the upstream schema changes.
The snapshot powers three things: the schema browser on the Data page, snapshot-validated table previews, and the context handed to LLM query generation — all below.
Safe table previews
From the schema browser you can preview any discovered table — a bounded SELECTof its first rows. Previews never build SQL from free text: the table and column identifiers must exist in the stored snapshot, are quoted with the engine's own identifier quoting, and everything else about the statement is fixed. A table name that is not in the snapshot is refused, which closes the identifier-injection path entirely. Previews run under the connector's maxRows and timeoutMs and write a data_calls provenance row like any other execution.
Domain components (named queries)
A domain component is a governed query: a human writes the SQL once, declares its parameters, and describes it for the model. Consumers — the kb_data tool, the structured query API, MCP clients — supply validated parameters only.
Domain component fields
Save-time SQL validation. A component that is not a single SELECT/WITH statement is rejected when you save it — not discovered at 2am when an agent calls it. The same check re-runs at execution time (not-read-only), so a template can never drift into a write.
LLM query generation
Writing a component by hand requires knowing SQL and the source schema. Generation removes the first requirement without loosening any rule: describe what you want in plain language, and the model drafts the SQL from the connector's schema snapshot — real tables, real columns, real types, not guesses. The draft lands in the same editor a hand-written component uses, for a human to review, edit, test-run, and only then save.
Generated SQL never auto-saves
$1..$n params, typed param specs), with the same re-check at execution time. Generation changes who types the first draft; it changes nothing about governance.Once saved, a generated component is indistinguishable from a hand-written one: it is discoverable by kb_data, callable through POST /api/v1/query and the MCP server, and bindable to entities.
Governance at execution
Every execution — from any surface, on any engine — passes the same gauntlet:
- Connector and component must exist and be
enabled. - Params are validated against the
DataParamSpeclist — types coerced, required params present, defaults applied (invalid-paramsotherwise). - Single-statement SELECT/WITH re-verified (
not-read-only). - Credentials decrypt, a short-lived connection opens (session read-only on Postgres/MySQL), the adapter rewrites
$nplaceholders to the driver's bind style, the statement runs undertimeoutMs, and results cap atmaxRowswith atruncatedflag. Rows come back strictly JSON-safe — dates and engine-native types are normalized (e.g.Date→ ISO string) identically on every engine. - A
data_callsprovenance row is appended — on success and on failure (ok·error·timeout·blocked) — recording the exact SQL, params used, row count, duration, and executor.
The allowRawSql escape hatch
allowRawSql enabled on a connector, tools and the API may submit ad-hoc SQL for exploration. The same guards still apply — read-only single-statement enforcement, maxRows, timeoutMs, and a full provenance row with the raw statement text. Without the flag, raw SQL is refused with raw-sql-forbidden. Named components remain the recommended surface: they are the difference between a governed data layer and a SQL prompt-injection target.The Data page
Everything above is managed in the workspace Data page (/w/:workspaceId/data):
- Connectors — the create/edit dialog opens with an Engineselector (PostgreSQL · MySQL · Snowflake) that switches the config form to that engine's fields and default port; the engine is fixed once the connector is created. Secrets are entered once and masked thereafter; enable/disable, test connection with live status, and the governance caps live in the same dialog.
- Schema browser — the discovered snapshot per connector: tables, columns, types, row estimates, the
discoveredAtstamp, a refresh action, and snapshot-validated previews. - Domain components — author the SQL template and param specs with save-time validation (or start from an LLM draft), and preview-run a component with sample params to see real rows before exposing it.
- Call history — the recent
data_callsprovenance trail: status, duration, row count, and which run/conversation triggered each call.
Mutations are owner/admin-gated; all of it is workspace-scoped under FORCE-RLS like every other tenant surface.
Next