For agents
Persistent analytical data for agents.
An agent that analyses data needs somewhere to keep it once the session ends. LakehouseBox gives your agents a shared home for the data they create and use: Apache Iceberg tables in a catalog that outlives the conversation, queried with DuckDB wherever the agent runs. Your agent brings the compute. LakehouseBox stores and maintains the data.
What your agents get
The session ends. The data remains.
A table an agent writes today is an Iceberg table in a managed catalog, not a file in a sandbox that is gone at the next start. Compaction, snapshot expiry, orphan cleanup and the off-host copies (state every 5 minutes, metadata hourly, data files nightly) run on it whether or not anyone is connected.
Another agent opens the same table.
A later session, a second agent, a colleague with PyIceberg or Spark: everyone points an engine at the same catalog and reads the same rows. Access is a grant per catalog, read or write, per person and per agent key.
The engine is the agent's own DuckDB.
The agent asks LakehouseBox for a connection recipe, then talks to the catalog and the object store directly. No row travels through LakehouseBox's compute; there is no query endpoint to be slow, metered or in the way.
01Who does what
The agent brings the compute. LakehouseBox stores and maintains the data.
LakehouseBox is an Iceberg REST catalog and S3-compatible object storage run as one system in Nuremberg, Germany. It hands out credentials, keeps the tables healthy and records who did what. Everything that touches rows happens in the engine your agent runs, on the machine your agent runs on.
DuckDB 1.5.5 or newer reads and writes the tables; so do PyIceberg 0.12 and Spark 3.5 with Iceberg 1.11. The recipes for all three come from one command, lhbox connect, and the same table is readable from each. Snowflake connects to the catalog today; its data path is not available yet.
| Your agent | LakehouseBox |
|---|---|
| Runs DuckDB | Runs the Iceberg catalog and the object store |
| Reads and writes rows | Vends per-table storage credentials that expire |
| Decides what to keep | Compacts to 128 MB files, expires snapshots, removes orphans, hourly |
| Holds one agent key | Confines the key to the catalogs you chose; revokes it on its own |
| Reads its limits first | Answers GET /v1/usage with every limit and its headroom |
| Asks the human when it must | Keeps signup, Terms and key recovery as a person's actions |
02The lifecycle
Create, ingest, discover, query, write back, share, recover.
Nine steps, then the relay for devices that cannot speak Iceberg. Each step is a real command or a real API call. The person's part is approving once in the browser, creating the account there if needed; from the third step on, the agent works alone with its own connection. Placeholders in angle brackets stand for values the previous step returned; no credential on this page is real.
The agent installs the CLI and runs lhbox login. The CLI prints a link with a code, opens the browser when there is one, and waits up to fifteen minutes; the agent shows the person the link. --level read asks for read only, --name names the connection, --catalog picks one; the default is the organisation's default catalog with read and write.
$ curl -fsSL https://lakehousebox.com/install.sh | sh
$ lhbox login
# Open https://lakehousebox.com/app/device?user_code=XXXX-XXXX
# and approve the connection in your browser (code XXXX-XXXX).
# … waiting (up to 15 minutes)
The person opens the link. With no account yet, the same page offers to create one: name, email, organisation, the link in the mail, a password, and the Terms; the code survives. Then a consent screen names the machine, the catalog and the access asked for, which they may lower to read. On approval the CLI receives the key of a new agent named after the machine, saves it to ~/.config/lhbox/credentials.json and never shows it. Revoke it any time under Connections & tokens. A key can also be minted by hand for a machine without a browser.
# the CLI, once approved: Connected to demo_data (read and write) as laptop. Saved to ~/.config/lhbox/credentials.json. # by hand, from a person's terminal, key shown once: $ lhbox agent create --name reporter \ --grant <warehouse_id>:write
Before touching anything, the agent reads its identity, its limits and the tables that exist. Output is JSON whenever stdout is not a terminal, so a script parses the same thing a person reads. GET /v1/usage lists every limit, the current value and the headroom: a limit is never met by failing. The first catalog is demo_data; another is lhbox catalog create --name <name>, where a name matches ^[a-z][a-z0-9_]{0,62}$ (SQL-friendly; anything else is 400 invalid_name with a suggestion).
$ lhbox whoami # who am I, my limits, what I hold on each catalog $ lhbox usage # enforced: storage 5 GB, catalogs 3/project, snapshot history 20 · 7 days # guidelines (limits_detail.enforced = false): tables 50, namespaces 10, # objects 50,000, commits 20,000/month, 10 s between commits $ lhbox table list --catalog demo_data $ lhbox table get --catalog demo_data --namespace demo --name cities # schema, format version, snapshots, rows, data_files
One command returns the paste-ready SQL for the agent's DuckDB (1.5.5 or newer), with the catalog's credential inside. The engine exchanges it for catalog tokens (900 s) and per-table storage sessions itself. ATTACH the bare bucket name, <handle>--<catalog> with underscores as hyphens: 'acme--demo-data' is read-write, the s3:// form attaches read-only. The recipe's fourth statement, an S3 secret for inspecting the bucket, is not needed for tables. Shorter: lhbox duckdb opens the DuckDB shell already attached, the recipe passing through a private file it deletes a second later; --persist writes DuckDB persistent secrets instead, so any later session needs only the ATTACH line. Single-stream throughput depends on your network path: inside Hetzner's network about 470 MB/s per stream, from a laptop in Madrid 39 MB/s on one stream (measured 2026-09-21), and DuckDB reads Parquet row groups in parallel by itself.
$ lhbox connect --catalog demo_data --engine duckdb INSTALL iceberg; LOAD iceberg; INSTALL httpfs; LOAD httpfs; CREATE SECRET lhbox (TYPE ICEBERG, CLIENT_ID '<client_id>', CLIENT_SECRET '<client_secret>', OAUTH2_SERVER_URI 'https://catalog.lakehousebox.com/v1/oauth/tokens'); ATTACH 'acme--demo-data' AS demo_data (TYPE ICEBERG, ENDPOINT 'https://catalog.lakehousebox.com', SECRET lhbox);
Importing Parquet is one statement, from a local file or a URL; namespaces are schemas. Measured against the service: a million rows, 18 MB of Parquet, in about 2.3 s from a laptop, three data files, one commit. Many files at once: lhbox table import --namespace demo --name events data/*.parquet makes one commit however many files (44 files one by one were 44 commits and 44 waits), keeps GeoParquet metadata as table properties, and creates a format-version 3 table with typed geometry when the catalog's default asks for it.
-- a new table, from a file or a URL CREATE TABLE demo_data.demo.events AS SELECT * FROM read_parquet('events.parquet'); -- into a table that exists INSERT INTO demo_data.demo.events SELECT * FROM read_parquet('more-events.parquet'); COPY demo_data.demo.events FROM 'more-events.parquet' (FORMAT PARQUET); -- namespaces are schemas CREATE SCHEMA demo_data.staging;
From here on it is DuckDB. The engine lists the table through the catalog, receives storage credentials scoped to that table, and reads the Parquet files from s3.lakehousebox.com itself. LakehouseBox sees the catalog call and the object reads; it never sees a result set.
SELECT date_trunc('day', event_time) AS day, count(*) AS events, avg(value) AS mean_value FROM demo_data.demo.events GROUP BY 1 ORDER BY 1;
A saved result is a table another session can open. One CTAS and the summary lives next to the source, versioned and maintained like it. CTAS makes every column optional; when identifier fields, required columns or partitioning matter, create the table first with lhbox table create --column name:type[:required][:identifier], then INSERT.
CREATE TABLE demo_data.demo.daily_summary AS SELECT date_trunc('day', event_time) AS day, count(*) AS events, avg(value) AS mean_value FROM demo_data.demo.events GROUP BY 1; -- a later session, another agent, a colleague: SELECT * FROM demo_data.demo.daily_summary ORDER BY day DESC LIMIT 7;
Access is a grant per catalog: read or write, to a member or to an agent key. A read holder's connect carries the read-only identity; the catalog refuses its commits and the store its writes. Colleagues join by invitation, or automatically once your organisation has claimed its email domain.
$ lhbox org invite --email colleague@company.com --role member # the invitation token is returned once and mailed $ lhbox catalog grant --catalog demo_data \ --principal <principal_id> --level read $ lhbox agent create --name analyst --grant <warehouse_id>:read $ lhbox catalog grants --catalog demo_data # who holds read or write, and how: admin, membership, creator, grant
A person's lost key is replaced by a code to their email, never recovered; an agent's lost key is revoked and a new agent created. The audit log answers who did what, for 400 days. Snapshot history is bounded: maintenance keeps 20 snapshots and 7 days per table, so time travel reaches back that far and no further.
$ lhbox recover --email you@company.com $ lhbox recover --email you@company.com --code 123456 --save # a NEW key; earlier keys keep working until revoked $ lhbox agent revoke --agent-id <agent_id> # every key of that agent stops at once $ lhbox audit --since 7d --action credentials.vend $ lhbox catalog rotate --catalog demo_data # new catalog credentials; old tokens refused now, vended # storage sessions run out within about an hour; fetch a fresh recipe
02bBring your own relay
Devices and scripts that cannot speak Iceberg.
A write-only uploader for the blob bucket
Every catalog has a second bucket for files that are not tables: photos, exports, raw drops. A person or an agent with write on the catalog mints an uploader: an S3 credential that can only PutObject under one prefix of that bucket (multipart included), optionally list that prefix, and nothing else. It cannot read, HEAD or delete even its own uploads, cannot touch the table bucket, and is revoked on its own. Hand it to a device, a Cloudflare Worker, a cron job; if it leaks, the worst case is unwanted files under one prefix.
$ lhbox catalog uploader create --catalog demo_data \
--name camera --prefix photos/ --save camera.env
# access key + secret shown once; endpoint https://s3.lakehousebox.com, the blob bucket, region us-east-1
# a boto3 and an aws-cli recipe come with the answer
$ lhbox catalog uploader list --catalog demo_data
$ lhbox catalog uploader revoke --catalog demo_data --uploader camera
Creating an uploader takes about 20 s and revoking about 30 s today (three identity writes at the store). The relay never needs the catalog credential.
Turning uploads into tables
The catalog's own credential reads the blob bucket. From the agent's DuckDB: read_blob('s3://<blob bucket>/photos/**') for files, read_parquet or read_csv for data drops, and lhbox table import --namespace raw --name drops 's3://<blob bucket>/drops/*.parquet' makes one Iceberg commit however many files arrived. Index the files you keep as blobs in a table (key, size, taken_at) so queries can find them. The blob bucket's name is in the connection recipe.
Expiring uploads. Lifecycle rules on the blob bucket delete files by age or date under a prefix and abort stale multipart uploads: lhbox catalog lifecycle set demo_data --prefix camera/ --expire-days 90 --abort-multipart-days 7 (get, clear; boto3 against s3.lakehousebox.com works too). The store applies them in a daily pass, so a deletion can be up to a day late, and the storage meter follows at the next hourly sample or lhbox catalog measure. Table buckets never take lifecycle rules: snapshot expiry and orphan cleanup manage those files.
Routes: POST/GET /v1/warehouses/{id}/uploaders, DELETE …/uploaders/{uploader_id}, PUT/GET/DELETE /v1/warehouses/{id}/lifecycle, in the API reference.
03Guard rails
What makes it safe to hand to an agent.
Designed for a caller that cannot read a dashboard
- Every error is typed. The body is
{"error": {"code", "message", …}}with, where it helps,remedy,limit,current,retry_after_seconds, and always arequest_id. The code says whether to change the request or the plan; the remedy says how. - The exit code carries the class. The CLI maps every failure to one of six codes (table on the right) and prints the server's typed body to stderr verbatim.
- Limits are readable before you hit them.
GET /v1/usage(lhbox usage) returns every limit with the current value and the headroom, andlimits_detailsays which are enforced (storage, catalogs per project, snapshot history) and which are guidelines for now (tables, namespaces, objects, commit rate). Read it before provisioning. - Mutations are idempotent.
POST /v1/projects,/v1/warehousesand/v1/tablestake anIdempotency-Keyheader (--idempotency-keyin the CLI). A replay returns the original response; the same key on a different endpoint is409 idempotency_key_reused. - Storage credentials expire. Engines receive per-table credentials that live about an hour; catalog tokens live 900 s. A leaked credential is a bounded problem.
- Agent keys are confined and revocable. A key holds exactly the grants it was given, on the catalogs you chose, and
lhbox agent revokestops every key of that agent at once. Ten wrong secrets for one key id within ten minutes answer429withRetry-After. - Over quota, reads keep working. Storage is measured hourly, about two minutes after a catalog is created, and on demand with
lhbox catalog measure; every figure carriesmeasured_at, and enforcement follows the measurement. Past 5 GB, creating catalogs or tables and vending write credentials answer409 quota_exceededuntil space is freed; reading through the catalog credential continues. - The catalog accepts only its own tokens. A LakehouseBox API key is 401 at
catalog.lakehousebox.comby design; engines mint catalog tokens from the catalog's own credential and refresh them themselves.
Exit codes, one meaning each
| Code | Meaning | What the agent does |
|---|---|---|
| 0 | ok | continue |
| 1 | invalid input | fix the values; never retry unchanged |
| 2 | limit or conflict | a real quota or a real 409; change the plan |
| 3 | auth | key missing, wrong or revoked; ask the human |
| 4 | not found | no such project, catalog, namespace or table; list them |
| 5 | server or unreachable | safe to retry with backoff |
| 6 | usage error | the command line is malformed; fix it, never retry |
The HTTP API and the CLI are the same calls; the exit code is the CLI's rendering of the response class. Full list of routes, errors and shapes: API reference, CLI reference.
04MCP
The same account as tools, for hosts that speak the protocol.
The LakehouseBox MCP server gives Claude Code, Claude Desktop, Cursor or any MCP host the account as tools: catalogs, connection recipes, short-lived table credentials, grants, tokens, members, the organisation, usage, the audit log and the Terms status. It is the third face of the same REST API as the CLI and the account page; it adds no permission of its own, caches nothing and sends no telemetry.
There is deliberately no query tool. Rows in a tool response are tokens in a context window and compute on our side; both are the wrong place for them. The agent calls connection and runs the recipe in its own DuckDB. Signup, login and key recovery are not tools either: those remain a person's actions.
The server is installable from a checkout today. The package name lakehousebox-mcp is reserved and not yet published on PyPI. Everything else, including the schema of every tool: /docs/mcp/.
$ lhbox mcp install # Claude Code: `claude mcp add-json`, user scope; the saved key # travels into the host's configuration and is never printed $ lhbox mcp install --client claude-desktop $ lhbox mcp install --print # the mcpServers snippet for any other host, key masked $ lhbox mcp run -- --list-tools # every tool with its JSON schema
| Tools | Names |
|---|---|
| Identity | whoami, usage, terms_status, terms_accept, audit |
| Catalogs | list_warehouses, create_warehouse, delete_warehouse, connection, table_credentials |
| Access | grants_list, grants_set, grants_revoke, tokens_list, tokens_create, tokens_update, tokens_revoke |
| Organisation | members_list, members_invite, members_set_role, members_remove, org_get, org_rename |
Every read carries readOnlyHint; delete_warehouse, grants_revoke, tokens_revoke and members_remove carry destructiveHint, so hosts ask before running them. Today the account-management tools want a human member's key, which is what lhbox mcp install uses by default; a confined agent key serves the data-side tools (connection, table_credentials, usage, its own audit).
05Start
Paste this to your agent.
A task in plain words, with the human step kept
What the agent will find there
- /docs/agent-setup/ is the page written for the agent: install, connect, the recipe, the first task. /llms.txt is the index; /llms-full.txt is the whole documentation as one plain-text file.
- The CLI is one Python file, standard library only, verified against a published SHA-256: /docs/cli/.
- The API is JSON in, JSON out, and every reply names the next call: /docs/api/.
- The engines, with every measured gotcha: /docs/engines/.
- The human's part is small: approve the connection once in the browser, creating the account and accepting the Terms there if needed, revoke it from the account page when done, and recover their own key if it is lost.
NewCo SL is a company in formation in Spain; the service runs in Nuremberg, Germany. No SLA yet; we publish what we measure. Paid plans are to be announced; the free plan, 5 GB stored and no card, is the offer today.
Free to start: 5 GB, no card. Data in Nuremberg, Germany.