Codebase map¶
Where everything lives and where to make a change. Written to be read in one sitting by a new contributor — or a coding agent — before touching the code.
It describes stable structure, not progress: for what the system is and why, see Architecture; for roadmap and milestone status see the README and the issue tracker. Invariant references below (I1, I2, …) are numbered as in Architectural invariants.
1. Repository Structure¶
DuckHaven is a polyglot monorepo: a uv Python workspace (api, agent,
shared) plus an npm React app (web).
duckhaven/
├── api/ FastAPI control plane (the "brain")
├── agent/ DuckDB compute agent (the "muscle")
├── shared/ duckhaven-shared: the control↔agent wire contract
├── web/ React SPA (SQL worksheets, catalog, admin)
├── deploy/ Docker Compose stack, entrypoints, secrets bootstrap
├── scripts/ Operator helpers (pg backup, token generation)
├── docs/ This file + development, runbook, self-hosting guides
├── Makefile Canonical dev/test/lint/migrate/compose commands
└── pyproject.toml uv workspace root (members: api, agent, shared)
Each Python package owns its own pyproject.toml. api and agent both
declare duckhaven-shared = { workspace = true }. The dependency direction
is strict: api and agent depend on shared; nothing depends on api
or agent (Invariant I6).
2. Code Map¶
This is the navigation core. For each package: what it is for, how its internals are organized, and the files to open first.
2.1 shared/ — duckhaven_shared (the contract)¶
Purpose. The single source of truth for the control-plane ↔ agent wire
format. Tiny by design (only depends on pydantic).
| Module | Defines |
|---|---|
protocol.py |
FrameType (string enum) and Frame ({type, payload}) — every message on the control WebSocket |
schemas.py |
AgentCapabilities — the document an agent advertises (DuckDB version, loaded extensions, memory ceiling, cores, host) |
FrameType values: auth, auth_ok, dispatch_query, query_progress,
query_done, cancel_query, heartbeat, agent_status, metrics_sample,
set_concurrency, and the SQL-session frames open_session, session_opened,
exec_statement, close_session, session_closed. Adding or changing a
frame means editing this package — both sides pick it up by import, which is
how the contract stays in sync (Invariant I5).
2.2 api/ — duckhaven-api (control plane)¶
Purpose. Everything the browser talks to, plus the agent-facing WebSocket. A FastAPI app backed by async SQLAlchemy + Postgres.
App composition (src/api/main.py) — important to understand the URL
surface:
- An outer ASGI app mounts three things:
- the agent WebSocket at
/agents/connect(root level — agents dial here), - the REST API sub-app under
/api(shares an origin with the SPA), - the built SPA as static files at
/(only present in the image; a catch-all servesindex.htmlso client-side routes deep-link). - The REST sub-app owns a lifespan that constructs the shared
PolarisClient(held onapp.state).
Internal layout:
| Directory | Responsibility |
|---|---|
routers/ |
HTTP/WS endpoints. One module per resource: auth, workspaces, catalogs (attach/detach + storage-backend migration control), schemas (catalog DDL + table sample), queries, agents, grants (scoped access-grant admin API), assistant (SSE chat + write-approval), health, setup, plus agents_ws (the agent WebSocket) and admin/ (agents, storage, users, maintenance, service_accounts). |
services/ |
Business logic, framework-free. The interesting code lives here (see below). |
models/ |
SQLAlchemy ORM models = the Postgres schema. |
schemas/ |
Pydantic request/response DTOs (distinct from ORM models). |
db/ |
Engine/session setup (session.py) and the declarative Base. |
deps.py |
FastAPI dependencies: get_db, get_current_user (session cookie or PAT bearer token), get_polaris_client. |
config.py |
pydantic-settings config (DB URL, Polaris URL + client credentials, cookie/secret settings). |
alembic/ |
Database migrations. Applied automatically by the container entrypoint in production. |
Key services to know:
| Service | What it does | Interacts with |
|---|---|---|
services/query.py |
Dispatches a query to an agent over the registry socket; handles query_progress/query_done frames coming back; fetches the result Parquet from the agent and decodes it to JSON rows (decode_parquet_page); also drives the synchronous table-sample preview and persists agent-reported table stats. The heart of the system. |
agent_registry, the Query/TableMetadata models |
services/agent_registry.py |
In-memory ConnectionManager (registry) mapping agent_id → live WebSocket. The only place that knows which agents are connected right now. |
routers/agents_ws.py, services/query.py |
services/polaris.py |
Async client for Polaris: catalogs (management API) + namespaces/tables (Iceberg REST), with OAuth2 client-credentials auth. Speaks REST directly via httpx. |
Polaris container |
services/sql_guard.py |
The SQL allowlist for the one-shot query path. Uses duckdb.extract_statements to parse only and allow data + catalog DDL statements (SELECT/INSERT/UPDATE/DELETE/MERGE/CREATE/ALTER/DROP), rejecting sandbox-escaping ones (ATTACH, COPY, LOAD, SET, …). No connection, no execution. |
routers/queries.py |
services/statement_policy.py |
The capability-scoped policy for the SQL session path (relaxes the allowlist). A sqlglot AST classifier that admits a safe SET subset, COPY/read_* confined to the session's staging prefix, and ATTACH of the managed catalog, rejecting everything else fail-closed. Runs at the API, per statement. |
routers/sql_sessions.py |
services/sql_sessions/ |
SQL session brokering (package): dispatches open_session/exec_statement/close_session frames, applies the agent's lifecycle acks, reconciles sessions on agent disconnect, and runs a leader-elected idle/max-lifetime reaper. |
routers/sql_sessions.py, routers/agents_ws.py, the SqlSession/Query models |
services/session_credentials.py |
The per-session credential-vending seam: supplies the Polaris connection block placed in open_session (the API is the vendor, not the agent's static config) and computes a session's scoped staging_uri. |
services/sql_sessions/, routers/sql_sessions.py |
services/agent_capabilities.py |
Maps a backend kind to its required DuckDB extension and checks an agent's advertised capabilities at dispatch time. | routers/queries.py |
services/workspace.py |
Membership/role checks (assert_workspace_member), workspace lookup, lazy Polaris catalog creation (ensure_polaris_catalog), backend→storage mapping (polaris_storage). |
Polaris, the Workspace/WorkspaceMember models |
services/auth.py |
bcrypt password hashing/verification and session-cookie handling. | routers/auth.py, routers/setup.py |
services/grants.py |
Scoped-access grant resolution: walks the table → schema → catalog hierarchy (inherits downward, including future tables), additive/no-deny, effective tier capped at the workspace role. Extracts referenced objects from SQL via sqlglot. |
routers/schemas.py (browsing filter/404), services/query.py::dispatch_query (per-table enforcement), routers/grants.py (admin API) |
services/assistant/ |
The governed AI data assistant (package, not a single module). Thin tools that are authenticated loopback calls to DuckHaven's own REST API (httpx + ASGITransport) so authorization never leaves the normal API boundary; wraps them in a governance/audit capability; mints an ephemeral per-turn PAT as the assistant's identity; persists conversation state to Postgres; runs turns via Pydantic AI with human-in-the-loop write approval. |
routers/assistant.py, the loopback REST API, Pydantic AI, services/grants.py/sql_guard.py (via the loopback) |
services/mcp/ |
The Model Context Protocol server mounted at /mcp (package). Reuses the assistant's loopback Gateway, but authenticated as the caller: an ASGI middleware validates the presented access token by asking the REST API GET /me over the loopback, then hands the authenticated client to the tools on the ASGI scope (not a ContextVar — the SDK dispatches handlers into a task group started at boot). Read-only unless MCP_ALLOW_WRITES. Also re-exposes the assistant's knowledge/ documentation tools, gated on the same ASSISTANT_DOCS_ENABLED switch. |
main.py (the /mcp route), services/assistant/gateway.py, services/assistant/knowledge/, services/sql_guard.py, the mcp SDK |
services/migration/ |
The catalog storage-backend migration engine (package): a pending → copying → verifying → cutover → completed state machine run by a leader-elected background loop (Postgres advisory lock). Provisions a shadow Polaris catalog at the target backend, copies/rewrites Iceberg files, verifies snapshot counts, then atomically re-points the catalog row at cutover; all progress is checkpointed so a crash resumes from the last completed table. |
routers/catalogs.py, Polaris, the storage backends |
2.3 agent/ — duckhaven-agent (compute)¶
Purpose. Embed a DuckDB engine, execute dispatched queries, and serve
result files. An agent is a single Python process running three concurrent
tasks (src/agent/main.py gathers them):
- Control channel (
control/channel.py) — opens the outbound WebSocket, authenticates, advertisesAgentCapabilities, then loops handling frames (dispatch_query,cancel_query,heartbeat,set_concurrency, and the SQL-session framesopen_session/exec_statement/close_session). Reconnects with backoff if the socket drops. On each heartbeat it re-advertises capabilities; eachmetrics_samplealso carries the live running/queued query counts, active concurrency profile, and held-session count. Held SQL-session connections live incontrol/session.py(asession_id → DuckDBPyConnectionregistry, cleared on every reconnect), each holding an admission slot for its lifetime; a statement runs on its held connection viarun_statement_sync. - Result server (
results/server.py) — an HTTP server bound toRESULTS_HTTP_HOST(0.0.0.0in compose) that advertises its port to the control plane, servingresults/{query_id}.parquetwith HTTPRangesupport, gated by a Bearer token (the agent's session token, held inauth.py:TokenHolder). - Retention sweep (
results/retention.py) — periodically deletes result Parquet files older than the retention window.
Query execution is split for testability and cancellation:
| Module | Responsibility |
|---|---|
executor/admission.py |
Admission: memory-budget admission control with a FIFO queue. The cgroup-aware budget (effective_memory_bytes() × (1 − headroom)) is split into a weighted slot ladder (profiles in duckhaven_shared.concurrency); a new query takes the largest free slot, the rest queue. In the default auto profile it instead admits a per-query requested reservation (sized from the estimate) clamped to [floor, ceiling×budget]. Enforces the invariant Σ running memory_limit ≤ budget so the agent never OOM-kills. Exposes live running/queued counts and the active profile, switchable at runtime via the set_concurrency frame. |
executor/plan.py |
Shared DuckDB plan/profile tree-walker: one recursive walker, two extractors. parse_explain reads estimated cardinality from EXPLAIN (FORMAT json) (pre-execution); parse_profile reads actual per-operator metrics + a query-level summary from the JSON profile (post-execution). Consumed by both the estimator and the profiler. |
executor/estimator.py |
estimate_memory_bytes: runs EXPLAIN on the attached connection and approximates peak memory as Σ over blocking operators (joins, group-bys, sorts) of estimated_cardinality × row_width × safety; bucket_for snaps it to a T-shirt bucket of the budget. Best-effort — returns None (caller uses the fallback bucket) on any failure. |
executor/runner.py |
run_query_sync: the synchronous DuckDB path. Sets memory_limit/threads to the slice the admission manager granted, loads iceberg (+ httpfs/azure for cloud), creates a connection-scoped iceberg OAuth2 SECRET from agent config and ATTACHes the workspace's Polaris catalog (warehouse = workspace slug; vended_credentials for every backend, since all are object storage), then COPY (sql) TO '<uuid>.parquet'. Around execution it enables DuckDB JSON profiling and normalizes the result into stats["profile"] (best-effort; the auto path reuses the connection it ran EXPLAIN on). When the dispatch carries stats_for, it also returns the target table's COUNT(*) for the catalog sidecar. |
executor/supervisor.py |
run_query: runs run_query_sync on a thread executor with a wall-clock timeout. Uses DuckDB's thread-safe conn.interrupt() (via loop.call_later and on cancel) to actually stop a running query. |
config.py holds the operator timeout ceiling (max_timeout_s, per-query
requests clamp to it), the admission knobs (max_concurrency_profile — default
auto — memory_headroom_fraction, max_queue_depth, queued_timeout_s), the
estimator knobs (estimate_safety_multiplier, estimate_floor_bytes,
estimate_ceiling_fraction, explain_timeout_s, estimate_fallback_bucket),
and the profiling kill-switch (profiling_enabled, default on).
2.4 web/ — the React SPA¶
Purpose. The worksheet UI and admin console. React 19 + Vite, TanStack Router/Query/Table, Monaco editor, Radix + shadcn/ui + Tailwind.
Layered by responsibility (open in this order to trace a feature):
| Directory | Responsibility |
|---|---|
src/api/ |
Thin fetch wrappers. client.ts prefixes every call with /api and sends cookies; one module per resource. |
src/queries/ |
TanStack Query hooks (useX query/mutation) wrapping src/api/. The data-fetching seam components consume. |
src/features/ |
Page-level features: worksheet/, catalog/, saved-queries/, history/, auth/, admin/. |
src/components/app/ |
App-shell chrome (AppShell, TopBar, LeftRail, AgentPicker, WorkspaceSwitcher, CommandPalette). |
src/components/ui/ |
shadcn/ui primitives. |
src/router.tsx |
TanStack route tree. Authed routes nest under /$ws/... (worksheets, catalog, saved-queries, history, queries/$queryId (the dedicated query-profile graph), admin/*). |
src/types/ |
Shared TypeScript types mirroring the API DTOs. |
src/mock/ |
MSW handlers + fixtures used in dev (and tests) when the real API is absent. |
The frontend's AgentPicker runs the same backend-compatibility check the
control plane enforces server-side, so incompatible agents are visibly
disabled before a query is even sent.
2.5 deploy/ & scripts/¶
deploy/docker-compose.yml defines the all-in-one stack:
postgres → polaris-bootstrap (one-shot schema + root principal) →
polaris → api → agent, plus objectstore for object storage.
The API's own entrypoint (api/src/api/entrypoint.py) generates the first-boot
secrets (including the one-shot admin setup token), runs Alembic migrations,
then starts uvicorn; the API
also seeds the agent bootstrap token on startup. The objectstore-bootstrap
one-shot creates the warehouse bucket. Remote agents can still be deployed
per host against the same control plane. scripts/ holds operator helpers
(pg-backup.sh, assistant-mine-feedback.py, wait-for-stack.sh).
3. Data Model¶
DuckHaven splits its state across two stores, and the split is itself an invariant (I3): DuckHaven's own entities live in Postgres; catalog metadata lives in Apache Polaris.
Postgres (owned by api/src/api/models/)¶
erDiagram
users ||--o{ credentials : has
users ||--o{ workspace_members : joins
workspaces ||--o{ workspace_members : has
workspaces ||--o{ queries : runs
workspaces ||--o{ saved_queries : stores
workspaces ||--o{ workspace_catalogs : attaches
catalogs ||--o{ workspace_catalogs : "bound to (M:N)"
catalogs ||--o{ table_metadata : "stats + ownership"
catalogs ||--o{ catalog_grants : "scoped access"
users ||--o{ catalog_grants : "granted to"
catalogs }o--|| storage_backends : "pinned to (1)"
catalogs ||--o{ catalog_migrations : migrates
agents ||--o{ credentials : "session token"
agents ||--o{ queries : executes
workspaces ||--o{ sql_sessions : hosts
agents ||--o{ sql_sessions : holds
sql_sessions ||--o{ queries : "statements"
workspaces ||--o{ assistant_conversations : has
users ||--o{ assistant_conversations : owns
assistant_conversations ||--o{ assistant_messages : contains
assistant_messages ||--o{ assistant_tool_calls : logs
users {
uuid id
string email
string password_hash
string role
}
credentials {
uuid id
uuid user_id
uuid agent_id
string kind
string token
datetime expires_at
}
workspaces {
uuid id
string slug
}
catalogs {
uuid id
string slug
string polaris_name
uuid storage_backend_id
}
workspace_catalogs {
uuid workspace_id
uuid catalog_id
bool is_default
string access_mode
}
catalog_grants {
uuid id
uuid user_id
uuid catalog_id
string schema_name
string table_name
string tier
}
workspace_members {
uuid workspace_id
uuid user_id
string role
}
storage_backends {
uuid id
string kind
string name
string root_uri
}
agents {
uuid id
string name
string status
json capabilities
string result_host
int result_port
}
queries {
uuid id
uuid workspace_id
uuid agent_id
uuid user_id
uuid session_id
text sql
string status
string origin
int row_count
json progress
string result_path
jsonb profile
}
sql_sessions {
uuid id
uuid workspace_id
uuid agent_id
uuid user_id
string status
string active_catalog
string staging_uri
datetime last_active_at
}
saved_queries {
uuid id
uuid workspace_id
text sql
uuid default_agent_id
}
table_metadata {
uuid id
uuid catalog_id
string schema_name
string table_name
uuid owner_id
bigint row_count
bigint size_bytes
datetime last_write_at
}
catalog_migrations {
uuid id
uuid catalog_id
uuid source_backend_id
uuid target_backend_id
string status
jsonb progress
}
assistant_conversations {
uuid id
uuid workspace_id
uuid user_id
string title
}
assistant_messages {
uuid id
uuid conversation_id
jsonb model_messages
}
assistant_tool_calls {
uuid id
uuid message_id
string tool_name
jsonb args
jsonb result
}
Notes that matter for changes:
credentialsis polymorphic bykind: usersession, userpat(a bearer-token Personal Access Token, hashed with SHA-256 for O(1) lookup-by-hash, operator-chosen expiry), agentagent_bootstrap(single-use), and agentagent_session(long-lived).workspace_catalogs.access_modeisopen(today's default — the workspace role applies uniformly) orscoped(per-principalcatalog_grantsnarrow access below the workspace role; see Permissions).catalog_migrationstracks a storage-backend migration's state machine (pending → copying → verifying → cutover → completed) plus per-table and event-log detail tables (checkpointed so a crash resumes from the last completed table); seeservices/migration/.assistant_conversations/assistant_messages/assistant_tool_callspersist the AI assistant's chat history: one conversation per worksheet session, its messages serialized via Pydantic AI'sModelMessagesTypeAdapter, and each tool invocation logged for audit; seeservices/assistant/.agents.capabilitiesis the last advertisedAgentCapabilitiesJSON;result_host/result_porttell the API where to fetch the result Parquet.queriesis also the audit log — there is no separate audit table.GET /workspaces/{workspace}/queriesreads rows straight fromqueries, excluding internal rows (origin = "sample", used by the table-sample preview). It is workspace-scoped for members; an admin may passall_workspaces(and theuser_id/agent_id/since/untilfilters) for the cross-workspace audit view.sql_sessionsis the state-of-record for the SQL session layer (I9) — the agent's held connection is ephemeral socket state. A row pins oneagent_id(every statement routes there), records the requestinguser_idfor per-statement authorization, the scopedstaging_uri, and the timestamps the idle-reaper uses. Session statements are ordinaryqueriesrows tagged withsession_id(origin = "session"), so they reuse the poll/fetch/audit pipeline.- Catalogs are decoupled (M:N).
catalogsis a first-class entity (own Polaris catalog + storage backend);workspace_catalogsbinds catalogs to workspaces many-to-many, with exactly oneis_defaultper workspace. Storage is catalog-scoped — a backend cannot be deleted while any catalog references it. table_metadatais the catalog sidecar — keyed bycatalog_id(the table's true home; a shared catalog has one ownership/stats row). It holds what Polaris does not track:owner_id,last_write_*, and agent-computedrow_count/size_bytes. Populated on table create and on sample/stats completion; merged intoTableOutbyrouters/schemas.py. Polaris remains the source of truth for catalog structure. (Maintenance health rows keep a denormalizedworkspace_idalongsidecatalog_idso the workspace health page reads without a join.)
Apache Polaris (owned by Polaris, addressed via services/polaris.py)¶
One Polaris catalog per DuckHaven catalog (named by catalogs.polaris_name;
migrated catalogs keep the originating workspace slug, so no Polaris catalog is
renamed), containing namespaces (schemas) and tables. A workspace's query attaches
all of its bound catalogs (multi-attach), each under its slug alias, and USEs
the active one. The default namespace is analytics, not main: main is
DuckDB's built-in default schema in an attached catalog and shadows the Iceberg
namespace, so catalog.main.table won't resolve. Every DuckHaven-created table is
Apache Iceberg format and catalog-managed by definition; its location sits under
the catalog's base location, derived from the catalog backend's root_uri. Catalogs
are created fully DuckHaven-owned:
ensure_catalog_access grants the service principal the full catalog-management
set (CATALOG_MANAGE_CONTENT + CATALOG_MANAGE_METADATA + CATALOG_MANAGE_ACCESS),
and create_catalog enables polaris.config.drop-with-purge.enabled so DROP
reclaims data files — without the content grant Polaris returns 403 on table data
access. REST-created tables record a table_metadata owner sidecar at create time;
tables created via SQL DDL get one lazily (stats on sample). A table's Iceberg
snapshot history (the catalog History tab) is read live off the
loadTable metadata via list_snapshots — never persisted to Postgres — and a
"query at this snapshot" worksheet pins the read with DuckDB's AT (VERSION => …)
/ AT (TIMESTAMP => …) time-travel clause (there is no BEFORE; the UI says
"as of"). The agent's DuckDB
sets the working catalog with USE <catalog>.<schema>
(a bare USE <catalog> does not resolve the attached REST catalog). Polaris
vends short-lived, scoped storage credentials to DuckDB on attach via access
delegation (ACCESS_DELEGATION_MODE 'vended_credentials').
Storage policy — object storage only (one bundled). Every workspace
catalog is backed by S3-compatible object storage; there is no Polaris FILE
storage. This is forced by DuckHaven's control-plane/compute-split topology:
DuckDB can only read and write Iceberg tables through the REST catalog when
storage is S3-compatible, because Polaris must vend scoped credentials the
remote agent uses. FILE storage cannot support writes across the
Polaris-container / remote-agent boundary (Polaris creates table directories as
its container user; the agent gets permission denied), so it was removed. The
compose stack therefore bundles an S3-compatible store (RustFS), and the
object_store backend kind is physically backed by a bucket in it: its catalogs
use storageType = S3 pointed at that store (with the catalog's vended endpoint set to
an externally-reachable URL the agent can reach, and an internal endpoint for
Polaris itself). Per-catalog isolation comes from a /{polaris_name} prefix under
the shared bucket, so catalogs sharing a backend never collide. The
s3/adls_gen2 kinds remain operator-owned external object stores.
4. Common Change Scenarios¶
A quick "if you want to do X, start here" index for contributors and agents.
| You want to… | Start in | Also touch |
|---|---|---|
| Add a control↔agent message | shared/.../protocol.py (new FrameType) |
sender + handler in api/.../routers/agents_ws.py / services/query.py and agent/.../control/channel.py |
| Add a REST endpoint | api/.../routers/<resource>.py |
a schemas/ DTO, register in main.py, a test under api/tests/unit/routers/, and the web src/api/ + src/queries/ |
| Add/alter a Postgres table | api/.../models/ |
a new migration in api/alembic/versions/ (make migrate-new name=...) |
| Change query execution (extensions, pragmas, attach) | agent/.../executor/runner.py |
supervisor.py if it affects timeout/cancel |
| Widen/narrow the SQL allowlist | api/.../services/sql_guard.py |
its test in tests/unit/services/test_sql_guard.py; the runner's single-SELECT branch in agent/.../executor/runner.py |
| Change the SQL-session statement policy | api/.../services/statement_policy.py |
its test in tests/unit/services/test_statement_policy.py; routers/sql_sessions.py (where it is enforced) |
| Add a catalog DDL UI action | api/.../routers/schemas.py (REST endpoint) + services/polaris.py |
web src/features/catalog/CatalogNodeMenu.tsx (+ dialogs), src/api/schemas.ts, src/queries/schemas.mutations.ts, MSW handler, tests |
| Add a storage backend kind | api/.../services/agent_capabilities.py (required extension) + services/workspace.py (polaris_storage) |
agent/.../executor/runner.py (iceberg attach), StorageBackend validation, web StorageIcon/wizard |
| Change catalog credentials | agent/.../config.py + api/.../config.py (Polaris client id/secret) |
agent/.../executor/runner.py (iceberg SECRET) |
| Add a UI screen | web/src/features/<feature>/ |
src/router.tsx, src/api/ + src/queries/, MSW handler in src/mock/handlers/, a test under web/tests/ |
| Change scoped-access grant resolution or enforcement | api/.../services/grants.py |
routers/schemas.py (browsing), services/query.py::dispatch_query (SQL), routers/grants.py (admin API), tests under api/tests/unit/ |
| Change the agent handshake/auth | api/.../routers/agents_ws.py + api/.../routers/admin/agents.py |
agent/.../control/channel.py, agent/.../auth.py |
Per project convention, every change ships with tests (pytest for
api/agent, Vitest + RTL + MSW for web) and passes
make test && pre-commit run --all-files.
5. Known Technical Debt¶
Honest, stable-enough caveats. The live, itemized list lives in the issue tracker and the README roadmap; this section names the categories a contributor should be aware of.
- External cloud-backend storage config. The
s3/adls_gen2kinds (operator-owned external object stores) still need their PolarisstorageConfigInfocredential wiring (role ARN / tenant) completed inservices/workspace.polaris_storage. Their write paths are validated behind opt-in/env-gated integration tests. The bundledobject_storepath is fully wired (see §7). - DuckDB-iceberg DDL coverage.
DROPnow purges (catalogs enable drop-with-purge and grant full ownership), but the breadth ofCREATE/ALTERsupport against the Polaris REST catalog is bounded by the DuckDBicebergextension version on the agent. The UI Alter flow generates SQL run through the agent, so any unsupported op surfaces as a query error rather than a silent no-op. - Control-plane HA is opt-in. The default deploy is single-node (Postgres + Polaris + API on one host) for simplicity, and that is what most installs run. A highly-available topology — HA Postgres plus multiple API replicas behind a load balancer — is now supported via a separate compose file; see High availability. It is not the default, so a single-node install carries the usual single-box failure domain until you opt in.
- Result durability. Results live only on the executing agent until the retention sweep removes them; they are not replicated. The recovery story is "re-run from saved SQL," not "fetch the old result."
- Progress reporting is coarse.
query_progressframes are persisted toqueries.progress, but the UI surfaces a binary running/done rather than streaming progress.