Skip to content

Questions and answers

Where another page says it better, this one links rather than restates.

Why not just give the agent a database connection and let it write SQL?

Two things go wrong at once, and only one of them is obvious. An agent that invents SQL invents a definition with it, so nobody can say whether "revenue" means what finance means by it. The rows come back according to what the service may read rather than what the caller may read, which is how a row-level security policy becomes decorative.

sutura fixes the first by construction today: definitions arrive certified and pinned, the tool surface has no field an invented query could arrive in, and every value a question carries binds as a parameter rather than reaching the statement as text.

The second is half built, and the missing half is the one that matters. "Every query runs as the caller" needs a credential minted per request. That now exists: a request context reaches the query path, a credential broker mints once per answer for every source a plan reads, and the execution port has no signature that runs without the result - so a subject with no credential at a source is refused rather than answered as this process. What is absent is a data system that evaluates the asking subject: no adapter in this build can carry a per-subject credential. Against a local file the property is trivially true and worth nothing, because a file has no login. Against a warehouse it is still a target - what changed is that no question has a code path for a warehouse to be read as this process through. The boot path does, by design: it re-executes every anchor before a listener is bound, there is no caller then, and Warehouse::verify_anchor takes no credential.

What bounds that path is placement made checkable, not its input type - a correction a second review forced. verify_anchor takes an AnchorPlan, a plan the pinned bundle itself agrees is a declared anchor's own: the metric must be defined and anchored, the range the one the bundle certifies, the grain the coarsest that metric declares, and there may be no group-by key and no predicate a question asked for. Every one of those facts is read off the bundle, so the check catches a boot path that compiled the wrong question. It does not stop code that wants to reach the method: the constructor is public, every value it reads is publicly constructible, and Rust has no cross-crate friend visibility. So the mechanism that makes the credential-free path boot-only is a lint - clippy.toml bans the method and the boot path holds the single expectation, so a second call site is a build error until somebody writes a second one a reviewer sees. A lint reaches this workspace and not a crate outside it; that is the limit, and it is stated on the type as well.

Why is a refusal not an error?

An error invites a retry. A refusal is a variant of the result type with a typed reason, so a caller cannot mistake "you may not ask this" for a transport hiccup and loop until something answers. That part is enforced today, and the golden suite provokes every variant a question can reach.

Recording each refusal with the whole principal chain is built: an AuditSink takes every outcome, refusal and answer alike, before it is returned, and sutura-runtime ships the structured writer a deployment that attaches nothing else gets. Two limits, both deliberate. sutura retains nothing - what a record is worth is what the deployment's sink is worth. And the subject in the chain is only as strong as what established it: behind the shared bearer token alone the record names the deployment, because that is who asked as far as anything can tell; a deployment that declares security.inbound names the caller, from a signature. Still a design target is a refusal attributable to a subject whose own access decided it - the record can say which identity each leg ran under, and on this build that is never the asking subject.

Why does the tool surface take no table name?

Because a refusal can be retried, reworded and eventually satisfied, and an absent field cannot. Query carries no SQL, no table, no filter expression and no row ids, so an uncertified question does not compile. That is enforced today, and deny_unknown_fields means a question carrying sql: is an error naming the field rather than one silently dropped.

The intended backstop - widening the surface changes a dumped schema, so the widening lands in the diff of the review that did it - is a design target. The schema dump is not written yet, so today the only thing catching a widened surface is review.

Why is there no result cache?

Under row-level security, two callers asking the same question are entitled to different rows. A cache keyed on the query text serves the first caller's rows to the second: a cross-user leak with a hit rate. The same reasoning rules out a materialised copy refreshed on a schedule, which is read under whoever refreshed it.

No mechanism can prove an absence, so this one is written down as a decision. Adding any cache of rows is an architecture change, keyed on subject first or not at all.

Why can a question not span two data systems?

A second data system is a second identity to satisfy, not a bigger version of the same query. The one-source rule is enforced today: the plan stage collects every source the plan reaches into a set and refuses unless exactly one name is in it, and a golden builds a two-source catalogue to provoke the refusal.

The identity half of that reasoning is a design target. There is no per-leg credential, so nothing asserts that two subjects get different rows, and nothing can until one exists.

Federation is wanted, in this order: per-leg identity first, then federation. A predicate pushed into ClickHouse or Postgres is evaluated there, under the caller's own grants, so excluded rows never enter this process. Where the parts come from names the projects that already do that part well.

Why are definitions not editable here?

Editing a certified statement forks the definition from the number it certifies, which was the only thing certifying it bought. Definitions are authored in the semantic layer that renders them and arrive pinned and hashed. A wrong definition is wrong upstream.

What happens if the agent asking is manipulated?

For a system whose input is natural language from wherever the user found it, a manipulated agent is the expected case rather than the disaster case, and the defence is not detecting it. The most an attacker can make the agent emit is a different certified question over the same pinned definitions - that much is enforced today by the shape of Query. The blast radius of a fully manipulated agent is the set of questions its caller could already ask.

The clause "asked as the same caller, against the same authorization" is half built. The credential broker exists and Warehouse::execute has no signature that runs without what it minted, so there is no code path a question reaches a data system through as an unnamed identity. What is still absent is an adapter that can carry a per-subject credential, so nothing today makes a statement about whose rows come back: the bound on a manipulated agent is the tool surface plus, where a deployment declares security.inbound, the scopes that caller was granted - which decide which operations it may invoke and not which rows an answer contains.

Why do the musl builds swap the allocator?

musl's mallocng serialises the whole process on one lock word. With one binary and only threading toggled, a 48-core run takes 4.45s on glibc and 92.16s on musl, slower than musl's own single-core run; linking mimalloc brings it to 3.83s. What ships has the details, including the cost of MI_SECURE=4.

Can I use it today?

For single-player work over local files, yes. sutura compile renders the statement for a question and sutura query answers it, over a catalogue of documents in git and the CSV or Parquet files in a directory you name. That is the honest description: a governed single-player semantic compiler and executor over local files, served either from the command line or over HTTP.

Not yet as the identity-aware runtime this site describes, and the gap is narrower and more specific than it used to be. There is a request context, a credential broker port with a static-credential implementor, an audit sink, an MCP surface, and - where a deployment declares security.inbound - a verified caller identity from a signature, with OAuth scopes deciding which operations that caller may invoke. What there is not is leg 2: no adapter in this build has anywhere for a per-subject credential to arrive, both declare so, and the broker that ships mints what an operator configured. So a deployment can know exactly who is asking, record it, refuse a subject it holds no credential for - and still read every row as one identity. There is no Arrow result envelope. The one data system the shipped binary opens is the in-process engine over those files - sutura-exec-duckdb renders and pushes down, and is a development dependency rather than something the binary links. So the governance that decides which rows is still the narrow tool surface and the pinned bundle, not identity. What exists today is the inventory.

The environment, the gates and the release pipeline do work, because a mechanism is cheaper to build before there is code to retrofit it onto.

How do I work on it?

Contributing covers the three routes to an environment, the two toolchains and which gates run when. On a network with no direct egress, read Building without direct egress first.