Skip to content

Agent data gateway: governed data for AI agents

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:

IdeaWhat it does in Shield360
Data dictionaryThe tables of your connected database that agents may use, each column tagged with a data class, each table with an owner.
Authorization objectsWhich 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.
MaskingFields of a masked class are masked before the result leaves Shield360: j•••@acme.com, ****1234, a salary as a band.
Change documentsEvery 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.

  • 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.
Import tables

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.

Review the suggested classes

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.

Name the owner

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.

ClassDefaultNotes
PublicReleased
InternalReleasedThe default for columns whose names suggest nothing.
CustomerDeniedBusiness facts about customers: company, region, tier.
Personal (PII)DeniedNames, emails, phone numbers, addresses, national ids.
FinancialDeniedCard numbers, salaries, amounts.
SecretNever releasedNo authorization can release it, clear or masked.

Columns of compound types (Array, Map, Tuple, JSON) are listed but never returned.

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.column entries 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.
  • Filtering or grouping is allowed only on fields returned clear, so a filter cannot be used to guess a masked or denied value.
  • min and max of a masked field are refused: each is one record’s value. Use sum or avg.
  • 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.

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.

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.

  • A request may carry "version": 1. A request without version is read as version 1, so agents written before the contract was versioned need no change.
  • Any other version is refused: the receipt’s outcome is denied, its reasonCode is unsupported_version and its reason names 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.

KeyRequiredTypeNotes
versionNointeger1. Omit for 1.
tableYesstringA table from data.describe; a plain identifier.
purposeYesstringOne of the authorization’s purposes.
fieldsNostring[]At most 50. Omit for every field the agent may see.
filtersNoobject[]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.
limitNointegerAt least 1; capped by the authorization, at most 200.
aggregateNoobject{ 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.

An answer is { contractVersion, rows, receipt }. Every receipt has these fields:

FieldTypeNotes
contractVersioninteger1.
authorizationstring or nullThe authorization the query was read under.
purpose, tablestringAs asked.
clearstring[]Fields returned as is.
maskedobject[]{ field, dataClass, method }. method is email, phone, card, national_id, name, bucket, partial or aggregated.
deniedobject[]{ field, reason, code }, for fields left out of an answer that went ahead.
rowFilterstring or nullThe authorization’s row filter, when one applied.
limitobject{ requested, applied, max, authorized? }.
aggregateobject or null{ suppressedGroups, minGroupSize } for an aggregate.
rowCountintegerRows returned.
outcomestringok, denied or error.
reasonstringWhy, in words. Empty when ok. The wording may change; match on reasonCode.
reasonCodestring or nullWhy, as a code. null when ok.
recordIdstring or nullThe access record the call left.

public, internal, customer, pii, financial and secret, as described under Data classes.

CodeOutcomeWhen
not_objectdeniedThe request is not a JSON object.
unsupported_versiondeniedversion is not 1.
unknown_keysdeniedThe request has a key the contract does not define (SQL included).
table_requireddeniedNo table, or not a plain identifier.
purpose_requireddeniedNo purpose.
invalid_fieldsdeniedfields is not a list of at most 50 identifiers.
invalid_filterdeniedA filter is malformed: field, operator or value.
invalid_limitdeniedlimit is not a positive whole number.
invalid_aggregatedeniedaggregate is malformed: group-by, metrics, function or field.
no_authorizationdeniedThe agent holds no enabled data authorization.
purpose_not_authorizeddeniedNo authorization lists the purpose.
table_not_authorizeddeniedNo authorization for the purpose covers the table.
table_not_in_dictionarydeniedThe table is not in the data dictionary.
filter_not_cleardeniedA filter names a field that is not returned clear.
invalid_filter_valuedeniedA filter value does not fit the column: a number, a date, or eq/neq only for booleans.
row_filter_misconfigureddeniedThe authorization’s row filter names a column not in the dictionary.
group_not_cleardeniedA group-by field is not returned clear.
aggregate_field_denieddeniedA metric names a denied field.
aggregate_field_not_numericdeniedsum, avg, min or max of a non-numeric field.
masked_min_maxdeniedmin or max of a masked field.
aggregates_onlydeniedThe authorization allows totals only.
no_fields_alloweddeniedNone of the requested fields may be returned.
agent_authorization_denieddeniedThe agent’s authorization refused the read (kill switch, run context, data scope).
rate_limiteddeniedMore than 30 governed queries in a minute.
source_errorerrorThe data source could not answer.

A field in denied carries one of these codes:

CodeWhen
not_in_dictionaryThe field is not in the dictionary.
type_not_offeredThe column’s type (Array, Map, Tuple, JSON) is never returned.
denied_fieldThe authorization lists it under denied fields.
never_releasedIts class is secret.
class_not_allowedThe authorization releases neither its class clear nor masked.
  • 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 reasonCode as 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 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 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-Truncated header and the JSON’s truncated field 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 as data_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.

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.

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.

  • 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.