Agent workflows
New in 0.4.0
Everything on this page shipped in v0.4.0. If ducklocal check --help doesn't list --restricted, your copy is older; update to use it.
An agent can already query files, build a dashboard and read your comments with the CLI. These additions cover what happens around that work: limiting SQL it did not write, knowing what the window did with a request, letting you approve a change, and recording how an answer was reached.
Limit SQL you did not write
--restricted is for SQL copied from data, and for a dashboard someone else wrote. Read-only mode alone doesn't cover this: it protects the database file, while COPY can still write anywhere you can and FROM 'https://...' can still send a request.
ducklocal query --restricted --allow-read ./data --sql "SELECT * FROM 'data/sales.csv'"
ducklocal check report.dash --restricted --allow-read ./data| The SQL tries to | Result |
|---|---|
Read a file under --allow-read | Allowed |
Read any other file, by path, glob or .. | Denied |
Write a file, even under --allow-read | Denied |
ATTACH a database, or create one | Denied |
Reach http, https, s3 or any other URL | Denied |
INSTALL or LOAD an extension | Denied |
| Change a setting, including these limits | Denied |
Run longer than --timeout (default 60 seconds) | Interrupted, kind timeout |
Use more than --memory-limit (default 2GB) | Fails, kind resource_limit |
With no --allow-read, no files are readable, and that includes a dashboard's own folder. A denied statement exits 1 with kind policy_denied and never falls back to running unrestricted. EXPLAIN, PRAGMA and a top-level PIVOT are refused too, because the statement must be one SELECT.
This is not a sandbox. The limits are DuckDB's own, and DuckLocal applies no operating-system isolation on any platform. They bind the SQL, not whoever chooses the flags: an agent with a shell can drop --restricted. To contain an agent you don't trust, run the agent itself in a container or VM. schema, profile, project check and open --run don't run restricted, and neither does the window when it opens a dashboard.
What limits agent-run SQL has the full threat model and what was tested on each platform.
Stay in step with the window
ducklocal open used to report delivery and nothing more. Two flags now report how a request ended, and guard against acting on a window that has changed.
ducklocal open --state --active # note `revision`
ducklocal open --if-revision REV --run --wait \
--title "Revenue by channel" --sql "SELECT ..."--waitwaits up to 5 minutes and addsstatus,tab_id,result_status,errorandrevisionto the output.--if-revision REVopens nothing if the window no longer shows that revision, and exits 1 with kindstale. Read the state again before trying again. A person typing in another tab also makes it stale, on purpose.
--state now also reports:
| Field | Meaning |
|---|---|
schema_version, revision | The contract's version, and a token for what the window shows |
actions | What became of recent requests: stale, queued, running, opened, succeeded or failed |
limits | The bounds on previews. A read never returns a whole result |
connections | S3's endpoint and region, and each attached database's name, kind, location and address. Never a key |
result.reach | local, remote or unknown, read from the SQL alone, and whether it used your S3 key |
sql_withheld | true when a tab's SQL may hold a credential, and the text is replaced |
scope, on each plot | How much of its query the plot draws |
reach can only see what the SQL names, so a local view over remote data reads as local. Reading any of this makes no network request, so location: "remote" says where a database is, not that it still answers.
An agent can see that S3 is configured without seeing the key, but SQL it runs with open --run can still use that key.
The handoff contract lists every field and error kind, and the JSON schema describes the state.
Propose a dashboard change
When you want to approve a change, an agent can propose it instead of editing the file:
ducklocal propose sales.dash --status # note current_hash
ducklocal propose sales.dash --from new.dash --note "Split revenue by region" --based-on HASH
ducklocal propose sales.dash --status # pending, applied or rejectedThe spec stays as it is, and the dashboard tab shows that a change is waiting. Click Review to run the proposed SQL and see the diff and checks, then click Apply or Reject.
- Only you can apply a proposal, in the window. There is no command for it.
- Applying keeps the old version as
sales.dash.bak, and is refused if the spec changed since the proposal. - Rejecting leaves the spec untouched.
- Clicking Review runs the proposed SQL, so a proposal you don't trust can reach whatever the dashboard's SQL can. Run
check --restrictedon it first.
Record how an analysis got here
Set DUCKLOCAL_SESSION, and query, check, propose and open each record what they did:
export DUCKLOCAL_SESSION=analysis.session.jsonl
ducklocal session note --text "EU revenue fell 12%" --refs 3,4
ducklocal session show --last 20Nothing is recorded without that variable. The file holds SQL and spec text, each source's size and modification time, and each result's shape, never its rows. Errors are kept up to 500 characters and can quote values, so keep the file out of shared folders unless you mean to share them.
Each step and conclusion has doubts, worked out when you read the file: what failed, was cut off or has changed since. An empty list means nothing is known against it, not that it is right.
SQL that may hold a credential is left out. That check goes by words such as secret, token and access_key, so a column with one of those names hides its query too.
Keep an analysis movable
ducklocal project init # write ducklocal-project.json
ducklocal project check # what a move would break
ducklocal project relink --source orders --to data/2026/orders.csvA ducklocal-project.json is optional. It lists a folder's dashboards and what they need; data is referenced, never copied. Opening it in the window opens those dashboards.
project check reports ok, portable, problems (each with a hint) and limitations, and exits 0 whatever it finds, so read the output. It never reaches the network or installs anything. Passing doesn't prove the data is the same. project relink changes your dashboards and keeps each old one as NAME.dash.bak.
A worked example
A leaked key, investigated by an agent and a person walks a made-up week of CloudTrail from the first query to a reviewed dashboard. A test runs the agent's half on every change. Nobody has yet clicked through the window's half of this example, and its README says which steps those are.
