Connections

Connections

Point Kaval at your systems of record so verify() can check them live.

Overview

Most beliefs are grounded against the public web. But the authoritative value of your facts — a customer's plan, a subscription's status, an order's amount — lives in your own systems of record. A connection teaches Kaval how to read one of those systems, so a verify() call can fetch the actual row and ground its verdict on it.

Connections are created once, on the dashboard's Connections page, and then used by every agent that calls with your API key. Each account can hold up to 20 connections.

How it works

A connection is a named pointer to one system, reached through one of three doors:

  • MCP — Kaval calls a read tool on an MCP server (Stripe's hosted mcp.stripe.com, Supabase's mcp.supabase.com, or your own).
  • Postgres — Kaval runs a read-only lookup against your database, restricted to an explicit table allowlist.
  • HTTP — Kaval GETs a URL template on your API, e.g. https://api.example.com/orders/$id.

At verify time, a request that carries a record reference is resolved against the calling account's stored connection for that system: Kaval decrypts the credential, fetches the row, and grounds the verdict on what it finds. If the row contradicts the belief you get contradicted; if it confirms it, current; if the record doesn't mention the subject at all, the check falls through to the web oracles rather than guessing.

Creating a connection

On Dashboard → Connections, pick a provider preset (or a custom door) and fill in:

  • System name — the name your verify() calls will use, e.g. stripe or billing-db (lowercase letters, digits, -, _).
  • Door config — endpoint and tool (MCP), table allowlist (Postgres), or URL template (HTTP). Presets prefill this.
  • Credential — an API key, token, or connection string. It is encrypted before it reaches the database and can never be read back out; saving the same system name again replaces it. Where the provider supports it (Stripe, Supabase, and custom MCP servers), you can skip the paste entirely and click Connect with OAuth instead — Kaval runs the authorization in a new tab and stores the resulting tokens encrypted.

After saving — or returning from an OAuth connect — the dashboard immediately runs a live test so you know the connection resolves before an agent depends on it.

Stripe

Uses Stripe's hosted MCP server (https://mcp.stripe.com, tool fetch_stripe_resources). See Stripe's MCP docs.

  1. Easiest: Connect with OAuth. Pick the Stripe preset and click Connect with OAuth — you authorize Kaval in Stripe's consent screen (a read-only grant), and the connection is saved and tested automatically. No key to paste.
  2. Or paste a restricted key. In the Stripe Dashboard, go to Developers → API keys → Create restricted key and grant Read on only the resources you'll verify (customers, subscriptions, invoices, …). Don't use your secret key — a restricted read-only key is all Kaval needs. Paste it as the credential and save.
  3. At verify time pass the Stripe object id as the record id: { system: "stripe", id: "cus_…" }. Stripe resolves the resource from the id's prefix (cus_, sub_, in_, pi_, …), so no table is needed.

GitHub

Uses GitHub's hosted read-only MCP server (https://api.githubcopilot.com/mcp/readonly, tool get_file_contents) to verify beliefs about repository contents — "the config on main still pins version X". See the GitHub MCP server docs.

  1. Create a fine-grained personal access token (Settings → Developer settings) scoped to the repositories you'll verify, with read-only Contents permission. GitHub is credential-only — its authorization server doesn't support the dynamic client registration Kaval's OAuth connect relies on, so a PAT is the path.
  2. Pick the GitHub preset and paste the token as the credential. In the args template, replace your-org / your-repo with the repository the connection reads (one connection per repository).
  3. At verify time the record id is the file path: { system: "github", id: "config/pricing.json" }. The file's body becomes the record — JSON files are parsed into fields; a missing path reads as record-absent and falls through to the web oracles.

Supabase

Uses Supabase's hosted MCP server, project-scoped and in read-only mode (https://mcp.supabase.com/mcp?project_ref=…&read_only=true, tool execute_sql). See the Supabase MCP guide.

  1. Pick the Supabase preset and enter your project ref (the subdomain of your project URL) in the dialog's project-ref field — the rest of the endpoint is fixed. Project scoping is required — without it the tool demands a project_id argument on every call — and it limits reads to that one project.
  2. Authenticate either way: click Connect with OAuth and authorize Kaval in Supabase's consent screen (the connection saves and tests itself), or create a personal access token in your Supabase account settings and paste it as the credential (project keys — anon / service-role — won't authenticate here).
  3. Pass the table and row id on the ref: { system: "supabase", table: "subscriptions", id: "…" }. The preset runs select * from "$table" where id = '$id' limit 1 (substitutions are SQL-escaped).

Neon

Uses Neon's hosted MCP server in read-only, project-scoped mode (https://mcp.neon.tech/mcp?readonly=true&projectId=…, tool run_sql). See Neon's MCP docs. Create a Neon API key, pick the Neon preset, enter your Neon project id in the dialog's project-id field (the rest of the endpoint is fixed), and paste the key as the credential. The preset runs select * from "$table" where id = '$id' limit 1 (substitutions are SQL-escaped), so pass both table and id on the ref.

Postgres

Connects directly to any Postgres database (Supabase, Neon, RDS, self-hosted) with a connection string. The config maps the table names your refs will use onto real tables — an allowlist: Kaval refuses to read any table not listed.

json
{
  "tables": {
    "customers": { "table": "customers", "idColumn": "id", "updatedAtColumn": "updated_at" },
    "orders":    { "table": "orders" }
  }
}

idColumn and updatedAtColumn are optional (defaulting to id / updated_at); all identifiers must be plain SQL names. The credential is the connection string itself — create a dedicated role with SELECT on just the allowlisted tables:

sql
create role kaval_reader with login password '…';
grant select on customers, orders to kaval_reader;
-- credential: postgres://kaval_reader:…@db.example.com:5432/prod

Custom MCP server

Point Kaval at any MCP server that exposes a read tool. Configure the endpoint URL, the tool name to call, and optionally an args template that maps the ref onto the tool's arguments — string values may carry $id and $table placeholders:

json
{
  "url": "https://mcp.example.com/mcp",
  "tool": "get_record",
  "argsTemplate": { "collection": "$table", "key": "$id" }
}

Without a template, the tool receives the plain structured ref ({ id, table }). Authentication is either a pasted token — sent as Authorization: Bearer <token> by default, or through a custom header via authHeader / verbatim via rawAuth — or OAuth: the dashboard's "Connect with OAuth" runs an OAuth 2.1 authorization-code + PKCE flow against your server, stores the tokens encrypted, and refreshes them automatically as they expire.

Custom HTTP API

The simplest door: Kaval GETs a URL template, substituting $id / $table (URL-encoded), and reads the JSON response as the record.

json
{
  "urlTemplate": "https://api.example.com/v1/$table/$id",
  "updatedAtField": "modified_at"
}

updatedAtField names the response field carrying the row's modified timestamp (default updated_at) — it feeds freshness checks like freshness_sla. The credential is sent as a Bearer token by default, with the same authHeader / rawAuth overrides as the MCP door.

Using connections in verify()

Pass a record pointer naming the connected system and the row id your agent already holds:

const verdict = await rg.verify({
  fact_type: "billing.plan",
  subject: { id: "cus_9XyZ", name: "Acme Corp" },
  claim: "Acme Corp is on the Enterprise plan",
  record: { system: "stripe", id: "cus_9XyZ" },
});

if (verdict.act) proceed();

Auto-routing

You don't have to name the system on every call. If the request carries an id but no record.system — via record_id, or an id on the subject — Kaval fills the system in from your connected set:

typescript
// with one connection (or an `answers` match), this routes itself:
const verdict = await rg.verify({
  fact_type: "billing.plan",
  subject: "Acme Corp",
  claim: "Acme Corp is on the Enterprise plan",
  record_id: "cus_9XyZ",
});
  • Each connection can declare answers: up to 32 fact-type globs (*, an exact billing.plan, or a prefix billing.*) naming what it's authoritative for. Unset means it answers everything — so with a single connection, every id-bearing belief routes to it with zero configuration. When several match, the oldest connection wins.
  • Optionally, an LLM tier (off by default, per-account opt-in) picks the best-fitting system for beliefs the globs couldn't route. It chooses only among your actually-connected systems and abstains below a confidence threshold.

Testing a connection

Every save — and every completed OAuth connect — triggers an automatic live test, and each row on the Connections page keeps a Test action with its last result (OK · tested 3m ago or the failure message). The test opens the door for real — it calls the MCP endpoint, connects to the database, or fetches the HTTP template — so a green check means verify() will resolve against it. A failed test leaves the connection saved; fix the credential or config and re-test.

Security

  • Credentials are write-only. They are encrypted (AES-256-GCM) before touching the database, are never returned by any API, and are decrypted only in memory for the moment a lookup runs.
  • Use least-privilege credentials. Kaval only ever reads, so give it read-only material: a restricted Stripe key, a SELECT-only Postgres role, a scoped token.
  • Egress is guarded. Every outbound connection — MCP, HTTP, Postgres, and each OAuth leg — is checked against an SSRF policy that refuses private, loopback, and link-local destinations, including on redirects.
  • Reads are bounded. The Postgres door only touches allowlisted tables, and template substitutions are SQL- and URL-escaped so a hostile id can't break out of its position.
  • OAuth tokens rotate. Tokens obtained through the OAuth flow are stored encrypted and refreshed automatically; a rotated refresh token is re-persisted on the spot.