Data-Layer Guards
Hold an agent's database work to named tables, collections, and a cost ceiling, and strip denied columns from what it reads.
On this page
An agent can query your databases while the kernel holds it to the tables and vector collections you name and to a cost ceiling per warehouse query, and the agent reads only the columns its grant allows.
Four guards in the chio-data-guards crate cover the database path. Three run inside admission and refuse a query before the kernel dispatches it. The fourth runs on the tool's response and reshapes the rows before the agent reads them.
A database call meets two checkpoints
The kernel checks the submitted query on the way in and the returned rows on the way out. Each guard reads a ToolAction::DatabaseQuery that the tool host builds from the call.
An admitted query reaches the tool, and its rows return through the hook
The statement parses, its operation and tables are on the allowlists, and the cost estimate sits under the ceiling. The kernel dispatches it, and QueryResultGuard truncates the rows to MaxRowsReturned before the agent reads them. Govern database queries.
SqlQueryGuard, VectorDbGuard, and WarehouseCostGuard on the rail, dispatch to the database tool, and QueryResultGuard on the return path. The refused statement and the ssn column are illustrative; each guard's internal check order and the receipt fields are left out.A refusal at the first checkpoint leaves the database untouched. The receipt names the guard, and the guard's reason code goes to its log. The second checkpoint shapes what the agent reads after the database has answered. An UPDATE that passed admission has already changed the table by then, so test a query's effect as well as the data delivered to the agent.
SqlQueryGuard parses the statement before it runs
The guard parses the SQL with sqlparser in the configured dialect and walks the syntax tree. It refuses at the first check that fails, in this order:
- Parse. A statement that does not parse is refused.
- Operation. The operation must appear in
operation_allowlist. - Tables. Every referenced table must appear in
table_allowlist. - Columns. When a per-table column allowlist is configured, every projected column must be on it, and
SELECT *is refused for that table. - Predicates. The canonicalized
WHEREclause must match no regex indenylisted_predicates. - Mutations. With
require_where_for_mutationsset, anUPDATEorDELETEwithout aWHEREclause is refused.
Each refusal carries a stable SqlGuardDenyReason code, such as table_not_allowed or missing_where_clause. The kernel returns Verdict::Deny and writes the code to the receipt and the structured log line.
With the configuration below, an agent may read orders and skus through the orders server's sql tool and never reaches customers. SELECT * FROM customers stops at step 3 with table_not_allowed, before the database sees it, and DELETE FROM orders stops at step 2, because only SELECT is allowed.
Computed projections fail closed. With a column allowlist in force, the ? the parser substitutes for an opaque expression, as in SELECT lower(email) FROM customers, is refused, because the guard would have to evaluate the expression to show it stays inside the allowed columns.
Read more in Govern database queries.
Vector queries stay inside named collections and a row ceiling
VectorDbGuard inspects database queries whose database identifier or tool name matches a vendor marker (Pinecone, Weaviate, Qdrant, Chroma, Milvus, or the generic vector), plus memory-shaped calls. It holds them on four axes:
- Collection. The collection, index, class, or store must be named and on the allowlist.
- Namespace. When a namespace allowlist is set, the call must declare a namespace on it. An empty allowlist on a namespaced grant refuses every request.
- Operation class. The guard reuses
SqlOperationClass, so one constraint covers SQL and vector grants.ReadOnlyrefuses every verb inmutating_operations. - Row ceiling. When the grant sets
Constraint::MaxRowsReturned, the call must declaretop_k,topK,k, orlimit.
A call that omits its verb fails closed. Under SqlOperationClass::ReadOnly or ReadWrite, a MemoryWrite-shaped call with no operation field is refused; every class stricter than Admin requires the operation key.
Read more in Memory governance.
A warehouse query stops at its dry-run cost ceiling
WarehouseCostGuard caps what a query on BigQuery, Snowflake, Redshift, Athena, Databricks, Presto, or Trino may scan and cost. It reads a dry-run estimate that the tool server, or an upstream dry-run gate, attaches to the arguments:
{
"query": "SELECT sku, sum(total) FROM orders GROUP BY sku",
"dry_run": {
"bytes_scanned": 53687091200,
"estimated_cost_usd": "0.25"
}
}The guard compares bytes_scanned and estimated_cost_usd against the configured ceilings and refuses a query over either one, a query with no estimate at the configured path, or an estimate that does not parse. The ceiling bounds the warehouse charge only as well as the estimate does, so the host obtains it from a trusted provider or dry-run service and binds it to the submitted query.
Guards evaluate without writing to the receipt. On an admitted call, the integration calls record_cost, which builds a CostDimension::WarehouseQuery record with the reported bytes and cost, so the mapping lives in one place.
The request gates and the result hook register separately
The four guards are built in Rust from their own config structs. The three request gates join a GuardPipeline, which is itself a Guard and registers on the kernel as one unit. QueryResultGuard registers as a post-invocation hook.
use chio_guards::GuardPipeline;
use chio_data_guards::{
SqlDialect, SqlOperation, SqlGuardConfig, SqlQueryGuard,
VectorGuardConfig, VectorDbGuard,
WarehouseCostGuardConfig, WarehouseCostGuard,
QueryResultGuardConfig, QueryResultGuard,
};
// Pre-invocation gates compose onto a GuardPipeline.
let mut pipeline = GuardPipeline::default_pipeline();
pipeline.add(Box::new(SqlQueryGuard::new(SqlGuardConfig {
dialect: SqlDialect::Postgres,
operation_allowlist: vec![SqlOperation::Select],
table_allowlist: vec!["orders".into(), "skus".into()],
denylisted_predicates: vec![r"(?i)\bor\s+1\s*=\s*1\b".into()],
require_where_for_mutations: true,
..Default::default()
})));
pipeline.add(Box::new(VectorDbGuard::new(VectorGuardConfig {
collection_allowlist: vec!["skus".into()],
namespace_allowlist: Some(vec!["harbor-goods".into()]),
denied_operations: vec!["drop_index".into(), "delete_collection".into()],
..Default::default()
})));
pipeline.add(Box::new(WarehouseCostGuard::new(WarehouseCostGuardConfig {
max_bytes_scanned: Some(53_687_091_200), // 50 GiB
max_cost_per_query_usd: Some("1.00".into()),
..Default::default()
})));
// QueryResultGuard runs after the tool response. new() returns Result
// because an invalid PII regex rejects construction.
let result_guard = QueryResultGuard::new(QueryResultGuardConfig {
redact_pii_patterns: vec![
r"[\w.+-]+@[\w-]+\.[\w.-]+".into(),
r"\b\d{3}-\d{2}-\d{4}\b".into(),
],
..Default::default()
})?;
// Register on an existing mutable kernel. Request context supplies the
// scope; the owned hook retains this scope as its fallback.
kernel.add_guard(Box::new(pipeline));
kernel.add_post_invocation_hook(Box::new(
result_guard.into_owned_hook(scope.clone()),
));After the tool returns, the hook truncates row arrays to the scope's Constraint::MaxRowsReturned, replaces the values of columns named in Constraint::ColumnDenylist with the redaction marker, and applies any redact_pii_patterns to every string value. A bare column name matches any table; table.column matches only that table.
The hook reads the scope and matched grant from the post-invocation request context and falls back to the scope it was built with, so a shared host supplies that context for calls with different result constraints. Appending the hook keeps every sanitization hook already registered. QueryResultGuard also implements Guard, and its pre-invocation evaluate always allows, so a result guard added only to the request pipeline redacts nothing.
Configuration stays in Rust: the HushSpec rules map has no data_layer, sql, or vector key, and the guards: section of chio.yaml carries only allow_advisory_promotion and required.
Read more in Guard pipelines.
Guard configuration and limits
Search for dialect, top_k, redaction_marker, or max_bytes_scanned.
SqlQueryGuard
Source: crates/guards/chio-data-guards/src/sql_guard.rs. Listens for ToolAction::DatabaseQuery. Dialects: Generic, Postgres, MySql, Sqlite, MsSql, Snowflake, BigQuery.
SqlQueryGuard configuration
| Knob | Type | Default | Purpose |
|---|---|---|---|
dialect | SqlDialect | Generic | Generic, Postgres, MySql, Sqlite, MsSql, Snowflake, or BigQuery. |
operation_ | Vec<SqlOperation> | empty (refuses) | Select, Insert, Update, Delete, Ddl, Other. |
table_allowlist | Vec<String> | empty (refuses) | Case- |
column_ | Option<HashMap<String, Vec<String>>> | None | Per-table projection allowlist. * as a column entry allows any column on that table. |
denylisted_ | Vec<String> | empty | Case- |
require_ | bool | true | Refuse UPDATE or DELETE without a WHERE. |
allow_all | bool | false | Escape hatch. Logs a warning on construction; parse errors still refuse. |
The predicate compiler caps the denylist at 64 patterns, 512 characters per pattern, a complexity score of 96, and a DFA size of 1 MiB per regex. A pattern over any cap is rejected at construction. An invalid regex collapses the guard into a refuse-all fallback with a tracing::warn! record, so a bad configuration keeps refusing.
SqlQueryGuard deny reasons
SqlGuardDenyReason codes: parse_error, operation_not_allowed, table_not_allowed, select_star_denied, column_not_allowed, predicate_denylisted, missing_where_clause, no_config.
VectorDbGuard
Source: crates/guards/chio-data-guards/src/vector_guard.rs. The mutating_operations that ReadOnly refuses: upsert, insert, update, delete, write, index, reindex, drop, drop_index, create_collection, delete_collection. With a configured row ceiling, a call that declares none of top_k, topK, k, or limit is refused.
VectorDbGuard configuration
| Knob | Type | Default | Purpose |
|---|---|---|---|
vendor_markers | Vec<String> | [vector, pinecone, weaviate, qdrant, chroma, milvus] | Substrings that mark a database or tool name as vector-shaped. |
collection_ | Vec<String> | empty (refuses) | Case- |
namespace_ | Option<Vec<String>> | None | Optional namespace gate. |
denied_ | Vec<String> | empty | Hard denylist that wins over the operation class. |
field_paths | Vector | collection, namespace, operation, top_k | Per-vendor argument key overrides. |
allow_all | bool | false | Escape hatch. |
WarehouseCostGuard
Source: crates/guards/chio-data-guards/src/warehouse_cost_guard.rs. Reads the dry-run estimate at dry_run.bytes_scanned and dry_run.estimated_cost_usd unless field_paths overrides them.
WarehouseCostGuard configuration
| Knob | Type | Default | Purpose |
|---|---|---|---|
max_ | Option<u64> | None | Hard upper bound on reported scan volume. |
max_ | Option<String> | None | Decimal-string cap on dry-run cost. |
warehouse_ | Vec<String> | [bigquery, snowflake, redshift, athena, databricks, presto, trino] | Substrings that mark a request as warehouse- |
field_paths | Warehouse | dry_, dry_ | Dot-paths to the dry-run fields. |
allow_all | bool | false | Escape hatch. |
WarehouseCostGuard refusals
- Dry-run metadata missing at the configured path while a ceiling is set.
- A non-decimal
estimated_cost_usdor a non-integerbytes_scanned. - A negative byte or cost value; the parser rejects the minus sign.
- No ceiling configured while
allow_all = false.
QueryResultGuard
Source: crates/guards/chio-data-guards/src/result_guard.rs. Redaction runs in its PostInvocationHook implementation. A response that does not look like a row list comes back with every value in its data field replaced by the marker, and an unknown column structure inside a row collapses to the marker. A PII regex that fails to compile rejects guard construction.
QueryResultGuard configuration
| Knob | Type | Default | Purpose |
|---|---|---|---|
redaction_ | String | "[REDACTED]" | Replacement string for redacted values. |
redact_ | Vec<String> | empty | Case- |
rows_keys | Vec<String> | [rows, results, records, data] | Keys the guard treats as the row array on the response. |
Performance class
| Guard | Hot path | Class |
|---|---|---|
SqlQueryGuard | SQL parse, AST walk, N regex sweeps on WHERE | O(query length) |
VectorDbGuard | Set lookups and a grant constraint scan | O(C) |
Query | Row truncation and per-cell regex over the response body | O(rows · cells · patterns) |
Warehouse | Two JSON-pointer reads and a decimal compare | O(1) |