v0.8.0 Built for agents. Ready for your data. ↗

Your agents.Your data.Your rules.

Give your AI agent a URL. curral provides the skill, schema and business context to query your DuckDB and Iceberg data. You decide what it can read, which rows it sees and which columns stay masked.

docker pull lucasapassos/curral:v0.8.0

Curral is Portuguese for corral: where you keep the herd in and decide who gets through the gate.

curral / agent gateway
CodexClaude CodeYour agent
curral
DuckDBIceberg
One gateway. Your access rules.
Agent session Browser simulation

“What was our revenue last month?”

  1. 01Discover permitted tablesschema
  2. 02Read business contextdocs
  3. 03Validate before executiondry_run
  4. 04Query within your rulesquery

✓ Query complete · row filters and masks applied

Agent-ready by design

A URL is all it takes to get started.

No SDK required. One skill gives your agent a path from a question to a permissioned SQL result.

  1. 01

    Give it a URL

    The server describes its API and provides the skill install command. Your agent can discover how to connect.

  2. 02

    Connect your agent

    Install the skill and sign in from your terminal. Works with Codex, Claude Code and other agents that read SKILL.md.

  3. 03

    Ask your data

    Your agent discovers the schema, searches business context, checks its SQL and queries within your permissions.

Install for Codex · v0.8.0

curl -fsSL https://raw.githubusercontent.com/lucasapassos/curral/v0.8.0/skills/curral/scripts/install.sh -o /tmp/curral-install.sh
CURRAL_SKILL_REF=v0.8.0 sh /tmp/curral-install.sh ~/.agents/skills/curral

# In your own terminal: credentials stay out of the chat.
~/.agents/skills/curral/scripts/curral.sh login

Context to reason. Boundaries to act.

The skill teaches your agent to discover only what it may query, read semantic documents and validate SQL with a dry run. Your policies govern every query.

Codex · Claude Code · Hermes · OpenClaw · omp

Choose your agent and connect →

DuckDB has no users, no permissions and no network protocol.

So sharing a DuckDB file or an Iceberg lake ends one of two ways: you hand out storage credentials and everyone can read everything, or you build and babysit a custom API for every team that asks.

curral is the layer in between. One binary with DuckDB embedded, speaking HTTP and Flight SQL on one side, deciding per statement who may run it, which rows they see and which columns arrive masked.

Six checks between a request and your data

Each stage can stop a request, and every outcome lands in the audit log.

  1. 1

    Authenticate

    Basic with bcrypt, service API keys, JWTs from any OIDC provider, or a trusted service naming the user it acts for. Repeated failures lock the IP or user out.

  2. 2

    Queue

    A global concurrency limit plus per-user quotas, so one heavy user cannot starve everyone else.

  3. 3

    Inspect

    The statement is prepared, not run. DuckDB’s own planner says which base tables it really reads, through views, CTEs and joins.

  4. 4

    Authorize

    An embedded OPA evaluates your Rego policy and returns allow or deny, plus limits and column masks.

  5. 5

    Protect

    Each protected table is replaced by a subquery that filters rows and masks columns before your query sees it.

  6. 6

    Execute

    A fresh connection and one transaction per request. Results stream as JSON, CSV, NDJSON or Arrow, over HTTP or Flight SQL.

Try the gate: see how your rules change a query

Send a query through the gate

Simulated in your browser with an example policy.

Who is asking
Statement
  1. Authenticate
  2. Queue
  3. Inspect
  4. Authorize
  5. Protect
  6. Execute

    Pick a user and a statement, then send it.

    Access rules you can review in a pull request

    Policies are Rego, evaluated by an embedded Open Policy Agent. No sidecar, no extra service. The policy sees the base tables a statement really reads, taken from DuckDB’s planner rather than a regex, so a view or a CTE cannot smuggle a read past it.

    • Row-level security per role or user, driven by config or by an ACL table.
    • Column masks computed before your query runs, so a WHERE ssn = … cannot guess the hidden value.
    • Dry runs that explain a decision without executing anything.
    • Per-role limits for timeout, row count and concurrency.
    Read the policy guide
    package curral
    import rego.v1
    
    default allow := false
    
    allow if "admin" in input.roles
    
    allow if {
    	"analyst" in input.roles
    	input.resolved
    	input.statement_type == "SELECT"
    	every t in input.tables { startswith(t, "lake.") }
    }
    
    masks := {"lake.analytics.customers": {
    	"ssn": "last:4", "email": "redact",
    }} if not "pii_reader" in input.roles

    Anything DuckDB can attach

    Declare databases in a YAML file and curral attaches them at startup, then locks the engine. Credentials come from the environment and are scrubbed from the process once loaded.

    • DuckDB files
    • Iceberg REST catalogs
    • Cloudflare R2 Data Catalog
    • Postgres
    • S3 and Parquet

    273 ms to 8 ms

    Median query time against Cloudflare R2 once the built-in catalog metadata cache is on. Throughput with 8 clients went from 3 to 338 requests per second.

    catalog.yaml

    extensions: [httpfs, iceberg]
    secrets:
      - name: r2
        type: iceberg
        params: { TOKEN: "${R2_TOKEN}" }
    databases:
      - name: lake
        path: ${R2_WAREHOUSE}
        schema: analytics
        cache_ttl: 30s
        options: { TYPE: iceberg, SECRET: r2, ENDPOINT: "${R2_CATALOG_URI}" }
      - name: sales
        path: /data/sales.duckdb

    Query it from anywhere

    One statement per request, parameters bound server-side. GET /v1/schema lists only what the caller may query, which makes curral a safe front door for notebooks, BI tools, database IDEs and AI agents.

    Streaming one million rows over the full HTTP path

    1M rows as Arrow140 ms
    1M rows as CSV570 ms
    1M rows as JSON580 ms
    curl -u analyst https://curral.example.com/v1/query \
      -d '{"sql": "SELECT * FROM orders WHERE id = $1", "params": [42]}'

    Database IDEs

    Open it in DataGrip or DBeaver

    Start curral with --flight-listen and it also speaks Arrow Flight SQL. Database IDEs connect through the standard JDBC driver, ADBC clients in Python, Go and R connect directly. Same users, same policy, same audit log: nothing to configure twice.

    • A tree with only what you may query, from the same listing as /v1/schema. Hidden tables never show up.
    • Masks and row filters on every result, the data preview included.
    • Refused before it runs: statements are authorized when the IDE prepares them.
    • Passwords, API keys or SSO tokens, with the same lockouts and TLS certificate.
    Set up DataGrip or DBeaver
    analyst@curral:31337
    • lake
      • analytics
        • customers
        • orders
    • sales empty
    SELECT name, email, ssn, region
    FROM customers;
    nameemailssnregion
    Ana Souza******6789north
    Carla Dias******3456north
    Elisa Nunes******9999north

    3 rows

    The analyst from the example policy: only the lake to browse, only the north region, and PII masked before the query ran.
    jdbc:arrow-flight-sql://localhost:31337?useEncryption=false

    Execution delegation

    Query as your app’s users, not as its key

    A portal or internal app has its own users. Give it a delegating key and it names, on every request, the user it is acting for. curral runs the query as that identity: your policy, row filters, masks, limits and audit apply to them, and the key’s own roles are never used.

    • The policy decides what a service may assert, with the identity in input.identity and the key in input.actor.
    • Read-only, and no fallback: a missing or refused identity is a 403, never the key’s access.
    • Row filters from the policy, ANDed with the file, reading the identity through curral_identity.
    • Over HTTP and Flight SQL, with the effective subject confirmed on every response and recorded in the audit log.
    Read about execution delegation
    $ ctx=$(printf '%s' '{"subject":"portal:tenant-a:user:42","kind":"user","tenant":"tenant-a"}' \
        | base64 | tr '+/' '-_' | tr -d '=\n')
    
    $ curl -si localhost:8080/v1/query \
        -H "Authorization: Bearer $PORTAL_KEY" \
        -H "X-Curral-Execution-Context: $ctx" \
        -d '{"sql": "SELECT count(*) AS n FROM orders"}'
    
    HTTP/1.1 200 OK
    X-Curral-Effective-Subject: portal:tenant-a:user:42
    
    {"columns":[{"name":"n","type":"BIGINT"}],"data":[{"n":118}],"row_count":1}

    Semantic layer

    Context for the agents writing your SQL

    The schema says a column is called amount. It does not say it is in cents, or that cancelled orders never count. Semantic documents do: short notes from the people who know the data, with sample SQL, that an agent searches with the user’s own question before it writes a query.

    1. Filter

      table and schema narrow the candidates to the documents about them.

    2. BM25

      Words of the question, title weighted most; accents, identifiers and plurals folded. Runs in memory, offline.

    3. Jev

      TypeSafe’s model judges whether the SQL would really use each document. Only those that pass come back, ranked by probability.

    With --docs-scope schema, documents follow query permissions. The agent can see which tables in a document it may query. Writes go through your policy and carry an ETag. Without a Jev key, search stays local.

    Read about semantic documents

    Built to run unattended

    Fail-closed audit log
    One JSON line per request: who, what, which tables, which policy version decided. If the log cannot be written, queries stop.
    TLS and lockouts
    Native TLS 1.2+ or Caddy in front. Failed logins are counted per IP and per user, checked before bcrypt.
    Hot reload
    Send SIGHUP to swap users, keys, policies, row filters and semantic documents. Invalid files are rejected; in-flight queries keep their version.
    Prometheus metrics
    Stage latencies, decisions by role, queue depth, audit health and cache hits, on a separate port.
    Hardened image
    Distroless, non-root, no shell, about 190 MB. Extensions are baked in, so it boots without internet.
    Fuzzed inspection
    A differential fuzzer runs every generated statement against DuckDB and fails if a write escapes the inspection.

    Open the gate in two minutes.

    Clone the repository and start curral with the example users and policy.

    git clone --branch v0.8.0 https://github.com/lucasapassos/curral && cd curral
    docker run --rm -p 127.0.0.1:8080:8080 \
      -v "$PWD/examples:/etc/curral:ro" -e CURRAL_DATA=/var/lib/curral \
      lucasapassos/curral:v0.8.0 serve \
        --catalog /etc/curral/catalog.yaml --users /etc/curral/users.yaml \
        --policy /etc/curral/policy.rego --policy /etc/curral/roles.json