ESRA — Security and Governance
ESRA puts an AI client in front of enterprise databases. Every control below is required, not advisory.
Roles — the ESRA & Data area
ESRA introduces its own administrative area rather than folding into an existing one, because a data-platform owner should be able to hold authority over enterprise data sources and nothing else — and because folding it in would have silently widened every existing toolset administrator's authority.
| Role | Authority |
|---|---|
ESRA_DATA_READ |
read the whole area, exposures included |
ESRA_DATA_MANAGE |
what exists and what is searchable — sources, connections, mappings, fields, capabilities |
ESRA_EXPOSURE_MANAGE |
who gets to see it — exposures, audiences, source subsets, publication |
Each is granted either against a tenant or against none — a grant with no tenant is what makes an operator cross-tenant, and there is no separate role name for it. Neither manage role implies the other; both imply read, and each carries the lookups an ESRA screen needs to name a model profile, a client application or a trusted issuer.
Triple control by construction
Getting enterprise data in front of an AI client requires four role holders, in sequence:
SECURITY_MANAGE creates and rotates the connection secret
↓
ESRA_DATA_MANAGE models the source, mappings and searchable/returnable fields
↓
GOVERNANCE_MANAGE authors the access policy for the exposure's tool
↓
ESRA_EXPOSURE_MANAGE binds audience and source subset, and publishes
No single role can complete that chain. This is a stated requirement, and integration tests assert each link independently: a holder of exactly one role is rejected at the next step.
What ESRA & Data deliberately does not own
| Surface | Owning policy | Why |
|---|---|---|
| Connection secrets | SECURITY_MANAGE |
secrets stay in one area, always |
| Service-LLM model profiles | AI_SETTINGS_READ to list, ESRA_DATA_MANAGE to bind |
profiles are owned by AI settings; ESRA only points at one |
| The exposure's access policy | GOVERNANCE_MANAGE |
who may invoke a tool is a governance decision, uniformly |
| Retention classification, audit exploration | GOVERNANCE_* |
a compliance surface, not a data-platform surface |
The Seven Authorization Layers
- Capability invocation — the exposure's access policy, evaluated identically to any other tool. A caller who fails it sees no trace that the tool exists.
- Source availability — the effective scope, computed before any model reasoning.
- Search mapping — a plan may only name a mapping belonging to a selected source.
- Field projection — returnable allowlist and restricted-field exclusion, applied at validation and again at response assembly.
- Record level — claim-derived parameterized predicates applied in the database query.
- Answer generation — gated per source and per effective response mode.
- Administrative configuration — the role area above.
Authorization Precedes All LLM Reasoning
An unauthorized source never appears in a prompt, a log, a diagnostic, an audit dimension or a response — including its existence. Source resolution filters by tenant, client application, capability binding, source policy, lifecycle state and health before any metadata reaches a model.
Selection output is validated against the candidate set that was supplied. A source id the model was not given is dropped and audited.
SQL Safety by Construction
model → structured intent → deterministic validation → provider query builder → parameterized SQL
The model never emits SQL. No value derived from a prompt, a model response or a caller claim ever reaches command text — such values travel exclusively as parameters. Relation and field identifiers come from configuration rows the validator already matched, and are emitted through the provider's identifier quoter.
Quoting is defence in depth, not the primary control. Both exist because a future code path that forgets the validator must still not be injectable.
Each supported database engine has its own full-text query syntax, and provider query builders are deliberately conservative about which functions and query forms they use, so that content coming from a prompt or a caller's claims can never be interpreted as an operator or as structure within the query — only ever as a literal search term. This behavior is verified by a dedicated test suite per provider.
Record-Level Security
Claim-derived predicates are applied in the executed query itself, never as post-filtering in memory, using the same claim-placeholder template syntax described for individual sources.
The template is authored by an ESRA_DATA_MANAGE holder, versioned with the source, validated at
publication (no separators, no comment sequences, balanced parentheses, every placeholder resolvable),
and audited on change.
A missing claim is fail-closed. The query is not executed and the source is dropped with a warning. ESRA never substitutes a default and never omits the predicate.
Prompt-Injection Resistance
Retrieved database content is untrusted. It may contain text attempting to instruct the model.
Prompts use fixed structural separation — system instructions, then the request, then reference data, then the output contract — with reference data always last before the output contract, so the final instruction the model reads is ESRA's.
Untrusted content is wrapped in structural markers that identify where it begins and ends, each tagged to its specific result. Any occurrence of those markers already present in the retrieved content is stripped before insertion, so a malicious record can never forge a boundary of its own — verified by tests that seed exactly such content.
This is mitigation, not a guarantee, which is why every task's output is validated against a whitelist afterwards. The model boundary is the weakest link in the chain; the validators are the strong ones. Candidates are referenced by opaque per-invocation ids, so a model can never emit a record identifier it was not given.
A security suite seeds records containing injection payloads and asserts no behavioural change: selection unchanged, plans unchanged, no unknown ids in output.
What Is Never Sent to a Model
- connection strings, hosts, ports, credentials, secret references;
- relation names, schema names, physical column names not exposed as business fields;
- restricted fields, or any field outside the returnable allowlist;
- record-filter templates, claim values;
- unauthorized sources' metadata — including their existence;
- other tenants' anything;
- provider scores during reranking; raw provider errors or diagnostics anywhere;
- prior invocations' content — each invocation is independent, there is no conversation state.
Multi-Tenancy
Enforced explicitly at every layer rather than assumed from the layer above. There is no global EF query filter — every repository filters by tenant explicitly, per platform convention.
- Configuration — sources, capabilities and exposures are tenant-scoped; names are unique within a tenant; cross-tenant references are rejected in the domain and re-asserted at publication.
- Runtime — the snapshot is keyed to the tenant and client application; the execution context carries the tenant from the authenticated request, so the core has no way to construct one for a different tenant.
- Caching — results are cache-isolated per tenant, client application, capability and the caller's effective authorization, so a change in configuration or authorization invalidates the relevant cache entries automatically. A cache-isolation test asserts a warm cache for tenant A never serves tenant B.
- Connections and secrets — secret references resolve within the owning tenant's scope; the outbound allowlist is consulted with the tenant id, so one tenant's allowlisted host does not become another's.
- Audit and telemetry — the tenant dimension is present on every record, including failures and denials.
Output Safety
Field allowlist → masking → size cap → citation validation → error sanitization.
Never returned: SQL, connection details, credentials, stack traces, provider diagnostics, unauthorized fields. Provider exceptions become a closed set of classifications; the raw exception goes to logs only.
Anything unclassified becomes ESRA_INTERNAL with the detail logged and never returned.
