Agent data gateway: governed data for AI agents
Overview
Section titled “Overview”AI agents increasingly need business data: a support assistant looks up a customer, an analytics copilot totals orders. Handing an agent a database connection gives it every column of every table. The agent data gateway puts a governed layer in between, built on four ideas familiar from enterprise data platforms:
| Idea | What it does in Shield360 |
|---|---|
| Data dictionary | The tables of your connected database that agents may use, each column tagged with a data class, each table with an owner. |
| Authorization objects | Which agent may read which data classes, for which purposes, from which tables, with denied fields, an optional row filter and a row cap. Anything not allowed is denied. |
| Masking | Fields of a masked class are masked before the result leaves Shield360: j•••@acme.com, ****1234, a salary as a band. |
| Change documents | Every read, allowed or refused, leaves an access record: who asked, for what purpose, which fields came back clear, masked or denied, and how many rows. Never a value. |
Agents reach the data through one tool, data.query, on the Shield360 MCP gateway (/api/mcp, set up under Govern → Agent access). A query is a fixed shape (table, fields, filters, limit, purpose), never SQL. What an agent sends and gets back is versioned: see Contract v1.
Prerequisites
Section titled “Prerequisites”- An environment whose data source is a connected ClickHouse database (the environment’s own database).
- An agent created under Govern → Agent access, with its token configured in the agent’s MCP client.
- The Manage the data dictionary permission to import tables and write authorizations (Administrators and Compliance officers have it). View the data dictionary lets a person see the dictionary, the access log and run a test query.
Build the data dictionary
Section titled “Build the data dictionary”Open Govern → Data dictionary and select Import from data source. Shield360 lists the tables of the environment’s connected database, read-only, at that moment. Nothing is read from the database until you ask. Tables whose names look like credential stores (vault, secrets, tokens, sessions) are never offered.
Each imported column carries the class its name suggests, marked Suggested: email, phone, ssn and address suggest Personal (PII); card, salary and amount suggest Financial; password and token suggest Secret. Columns whose names suggest nothing are Internal. Change any class, then select Confirm suggestions or Save.
Give each table an owner and a description. Agents see both through data.describe.
Re-importing a table keeps every class and description you set, adds new columns as suggestions and drops columns the database no longer has.
Data classes
Section titled “Data classes”| Class | Default | Notes |
|---|---|---|
| Public | Released | |
| Internal | Released | The default for columns whose names suggest nothing. |
| Customer | Denied | Business facts about customers: company, region, tier. |
| Personal (PII) | Denied | Names, emails, phone numbers, addresses, national ids. |
| Financial | Denied | Card numbers, salaries, amounts. |
| Secret | Never released | No authorization can release it, clear or masked. |
Columns of compound types (Array, Map, Tuple, JSON) are listed but never returned.
Write authorization objects
Section titled “Write authorization objects”Under Authorizations, select New authorization and choose:
- Agent: the agent it applies to.
- Purposes: the reasons the agent may give, such as
customer support. A query must state one of them. - Tables: the dictionary tables it covers.
- Returned as is and Returned masked: the data classes released clear or masked. Everything else is denied.
- Denied fields:
table.columnentries never returned, whatever their class. - Row filter (optional): only rows where a column equals a value, applied to every query on that table.
- Max rows: at most 200.
- Aggregates only: the agent may ask for totals (
count,sum,avg,min,max) but never rows.
Rules the gateway enforces
Section titled “Rules the gateway enforces”- Filtering or grouping is allowed only on fields returned clear, so a filter cannot be used to guess a masked or denied value.
minandmaxof a masked field are refused: each is one record’s value. Usesumoravg.- Aggregate groups with fewer than 3 rows are left out of the answer, and the receipt says how many.
- Every value is a bound query parameter. Table and column names must be dictionary entries and plain identifiers.
- Queries run read-only (
readonly=1), on the environment’s read-only login where one is set up, with a 10-second time limit. - Each agent may make 30 governed queries a minute.
Query from an agent
Section titled “Query from an agent”An agent with an enabled authorization sees two tools through the MCP gateway:
data.describe: the tables and fields it may query, each field’s class and whether it comes back clear, masked or denied, and its purposes.data.query: the governed query.
{ "name": "data.query", "arguments": { "table": "customers", "purpose": "customer support", "fields": ["full_name", "email", "company", "phone"], "filters": [{ "field": "region", "op": "eq", "value": "EMEA" }], "limit": 10 }}The answer carries the rows and a governance receipt:
{ "rows": [{ "full_name": "A••• A•••", "email": "a•••@acmefoods.example", "company": "Acme Foods" }], "contractVersion": 1, "receipt": { "contractVersion": 1, "authorization": "Support: customer service", "purpose": "customer support", "table": "customers", "clear": ["company"], "masked": [ { "field": "full_name", "dataClass": "pii", "method": "name" }, { "field": "email", "dataClass": "pii", "method": "email" } ], "denied": [{ "field": "phone", "reason": "denied by the authorization", "code": "denied_field" }], "rowFilter": null, "limit": { "requested": 10, "applied": 10, "max": 25 }, "aggregate": null, "rowCount": 1, "outcome": "ok", "reason": "", "reasonCode": null, "recordId": "cm…" }}For totals, send aggregate instead of fields:
{ "table": "orders", "purpose": "sales analytics", "aggregate": { "groupBy": ["region"], "metrics": [{ "fn": "count" }, { "fn": "sum", "field": "amount" }] }}Filter operators are eq, neq, gt, gte, lt, lte and in (a list of up to 50 values). Dates compare as ISO 8601 strings.
Contract v1
Section titled “Contract v1”The data.query request, the receipt and the data classes form a contract, frozen as version 1. Agents built against it keep working as the gateway evolves.
Versioning
Section titled “Versioning”- A request may carry
"version": 1. A request withoutversionis read as version 1, so agents written before the contract was versioned need no change. - Any other version is refused: the receipt’s
outcomeisdenied, itsreasonCodeisunsupported_versionand itsreasonnames the version to send. The version is checked before anything else in the request. - Every answer carries
contractVersion, and so does its receipt, allowed or refused.
The full contract (the version, data classes, filter operators, mask methods, reason codes and the JSON Schemas of a request, a receipt and an answer) is served by GET /api/data-gateway/contract to anyone with View the data dictionary. The request schema is also the inputSchema of data.query in the MCP gateway’s tools/list.
Request
Section titled “Request”| Key | Required | Type | Notes |
|---|---|---|---|
version | No | integer | 1. Omit for 1. |
table | Yes | string | A table from data.describe; a plain identifier. |
purpose | Yes | string | One of the authorization’s purposes. |
fields | No | string[] | At most 50. Omit for every field the agent may see. |
filters | No | object[] | At most 10, each { field, op, value }. op is eq, neq, gt, gte, lt, lte or in; in takes a list of 1 to 50 strings or numbers. |
limit | No | integer | At least 1; capped by the authorization, at most 200. |
aggregate | No | object | { groupBy?: string[] (at most 5), metrics: { fn, field? }[] (1 to 5) }. fn is count, sum, avg, min or max. |
Any other key is refused with unknown_keys.
Answer and receipt
Section titled “Answer and receipt”An answer is { contractVersion, rows, receipt }. Every receipt has these fields:
| Field | Type | Notes |
|---|---|---|
contractVersion | integer | 1. |
authorization | string or null | The authorization the query was read under. |
purpose, table | string | As asked. |
clear | string[] | Fields returned as is. |
masked | object[] | { field, dataClass, method }. method is email, phone, card, national_id, name, bucket, partial or aggregated. |
denied | object[] | { field, reason, code }, for fields left out of an answer that went ahead. |
rowFilter | string or null | The authorization’s row filter, when one applied. |
limit | object | { requested, applied, max, authorized? }. |
aggregate | object or null | { suppressedGroups, minGroupSize } for an aggregate. |
rowCount | integer | Rows returned. |
outcome | string | ok, denied or error. |
reason | string | Why, in words. Empty when ok. The wording may change; match on reasonCode. |
reasonCode | string or null | Why, as a code. null when ok. |
recordId | string or null | The access record the call left. |
Data classes
Section titled “Data classes”public, internal, customer, pii, financial and secret, as described under Data classes.
Reason codes
Section titled “Reason codes”| Code | Outcome | When |
|---|---|---|
not_object | denied | The request is not a JSON object. |
unsupported_version | denied | version is not 1. |
unknown_keys | denied | The request has a key the contract does not define (SQL included). |
table_required | denied | No table, or not a plain identifier. |
purpose_required | denied | No purpose. |
invalid_fields | denied | fields is not a list of at most 50 identifiers. |
invalid_filter | denied | A filter is malformed: field, operator or value. |
invalid_limit | denied | limit is not a positive whole number. |
invalid_aggregate | denied | aggregate is malformed: group-by, metrics, function or field. |
no_authorization | denied | The agent holds no enabled data authorization. |
purpose_not_authorized | denied | No authorization lists the purpose. |
table_not_authorized | denied | No authorization for the purpose covers the table. |
table_not_in_dictionary | denied | The table is not in the data dictionary. |
filter_not_clear | denied | A filter names a field that is not returned clear. |
invalid_filter_value | denied | A filter value does not fit the column: a number, a date, or eq/neq only for booleans. |
row_filter_misconfigured | denied | The authorization’s row filter names a column not in the dictionary. |
group_not_clear | denied | A group-by field is not returned clear. |
aggregate_field_denied | denied | A metric names a denied field. |
aggregate_field_not_numeric | denied | sum, avg, min or max of a non-numeric field. |
masked_min_max | denied | min or max of a masked field. |
aggregates_only | denied | The authorization allows totals only. |
no_fields_allowed | denied | None of the requested fields may be returned. |
agent_authorization_denied | denied | The agent’s authorization refused the read (kill switch, run context, data scope). |
rate_limited | denied | More than 30 governed queries in a minute. |
source_error | error | The data source could not answer. |
A field in denied carries one of these codes:
| Code | When |
|---|---|
not_in_dictionary | The field is not in the dictionary. |
type_not_offered | The column’s type (Array, Map, Tuple, JSON) is never returned. |
denied_field | The authorization lists it under denied fields. |
never_released | Its class is secret. |
class_not_allowed | The authorization releases neither its class clear nor masked. |
Compatibility and deprecation
Section titled “Compatibility and deprecation”- Within version 1, changes are additive only: a new optional request key, a new receipt field, a new reason code, a new mask method or a new data class. Agents should ignore receipt fields they do not know and treat an unknown
reasonCodeas a refusal. - Breaking changes ship only as a new version: removing or renaming a key, field, data class, reason code or outcome; making a key required; changing a type or a meaning. An agent opts in by sending the new
version. - Version 1 stays supported for at least 12 months after version 2 ships. This page will say when version 2 ships and when version 1 is retired.
Try it
Section titled “Try it”Try it runs one governed query exactly as the chosen agent would: the same authorization, the same masking, at most 25 rows. It shows the request the agent would send, the rows and the receipt, with its contract version and, for a refusal, its reason code. Each run leaves an access record marked Try it, with the person who ran it.
Access log and evidence
Section titled “Access log and evidence”Access log lists every governed read with the agent, purpose, table, how many fields came back clear, masked or denied, the row count and the outcome, newest first, 25 to a page. Dictionary and authorization changes are in the organisation’s audit log.
- Filter by agent, table, outcome (allowed, denied, error) and period. The agent and table lists offer what the environment’s records name, including agents revoked since.
- Details opens one record: the agent and its id, the channel, the authorization it was read under (or that it has been removed since), the person who ran a Try it query, the fields returned clear, masked and denied, the fields filtered on (names and operators only), the row count, the duration and the reason for a refusal.
- Export CSV and Export JSON download the filtered log, newest first, at most 10,000 records; the
X-Shield360-Truncatedheader and the JSON’struncatedfield say when there were more. Exporting needs View the data dictionary and Export data protection evidence (compliance.dataprotection.export). Every export is recorded in the audit log asdata_gateway.access.export, with its filters and how many records it held.
An access record never holds a row, a filter value or a result, so neither the detail view nor an export can carry one. The routes behind the log (GET /api/data-gateway/access, /api/data-gateway/access/{id} and /api/data-gateway/access/export) read the current environment only and answer 400 to a malformed filter.
How long access records are kept
Section titled “How long access records are kept”Access records are kept for 365 days, then deleted by the daily retention job, except those a legal hold (Govern → Records → Legal holds) covers: a record stays while an active hold in the same environment names the agent, or the person who ran a Try it query (by email, the part before the @, or name), and the record falls inside the hold’s period. Each environment’s purge is recorded in the audit log as data_gateway.access.purge, with how many records were deleted and how many were kept for a hold. See Data retention.
The ISO/IEC 42001 controls A.7.2 (data for AI systems) and A.7.5 (data provenance) include the evidence check Data reaching AI agents is classified, authorized and recorded on Controls. It is not applicable until a table is in the dictionary, a gap while any column’s class is unconfirmed, and met when every column is classified, with the period’s governed reads counted.
Lineage and the classification catalog
Section titled “Lineage and the classification catalog”Every governed read also appears in data lineage: the dictionary table as the source, the agent as the consumer and data.query (or Try it) as the destination, with the classes of the fields asked for and whether they came back clear, masked or were refused. The classification catalog gives each data class an owner and a sensitivity, and shows how many dictionary columns carry it and how many authorizations release it.
Limits
Section titled “Limits”- ClickHouse is the only source. The data source is the environment’s own connected ClickHouse database. Other databases are not supported yet.
- Agents are the only principals. Authorizations are given to agents. People, API keys and roles cannot hold one yet; a person reads governed data only through Try it, as an agent.
- Row filters compare for equality. An authorization’s row filter keeps the rows where one column equals one value.