Skip to main content

Playbook: Data-Analyst Agent

The scenario

An agent answers business questions: it queries datasets, builds summaries, and drafts reports. It should be able to read analytical data — and nothing else. No exports of raw PII, no writes, no touching production systems.

The risk

Read-only sounds safe, but "read" for an analytics agent often quietly includes the ability to export sensitive rows, run expensive jobs, or reach datasets outside its remit. Without an explicit boundary, "just querying data" can become a data-exfiltration path.

The mandate design

The key move here is least privilege: enumerate the few read actions the agent needs, bound them to one dataset domain, and rely on Mandaitor to deny everything you did not list.

FieldValueWhy
Principaluser:analytics-owner@acme.comA human owns the delegation
Delegateagent:insights-botThe specific analytics agent
Actionsanalytics.dataset.query, analytics.report.readRead-only — no export, no write
Resourceacme:dataset:sales/*One dataset domain, not the whole warehouse
Constraints7-day expiry; rate limitShort-lived, bounded volume

Create the mandate

const mandate = await mandaitor.createMandate({
principal: {
type: "NATURAL_PERSON",
subject_id: "user:analytics-owner@acme.com",
display_name: "Analytics Owner",
},
delegate: {
type: "AGENT",
subject_id: "agent:insights-bot",
display_name: "Insights Bot",
},
scope: {
actions: ["analytics.dataset.query", "analytics.report.read"],
resources: ["acme:dataset:sales/*"],
effect: "ALLOW",
},
constraints: {
time: { duration: "P7D" },
rate_limits: { max_operations: 500, window_seconds: 3600 },
},
});

Enforce it at runtime

Verify before every query executes. Because writes and exports are simply not in scope, they fail without you writing a single if for each one.

async function queryDatasetTool(args: { dataset: string; sql: string }) {
const result = await mandaitor.verify({
delegate_subject_id: "agent:insights-bot",
action: "analytics.dataset.query",
resource: `acme:dataset:${args.dataset}`,
context: { purpose: "analysis" },
});

if (result.decision !== "ALLOW") {
return { authorized: false, reason_codes: result.reason_codes };
}

const rows = await warehouse.query(args.dataset, args.sql);
return { authorized: true, rows, proof: result.proof };
}
Least privilege is a denial list you never have to write

You did not add a rule to block analytics.dataset.export or analytics.dataset.delete. You did not have to — anything not in scope.actions is denied by default. That is the whole point of scoping to the minimum.

What should be denied

Confirm in the Sandbox that the agent cannot:

TestExpectedReason
analytics.dataset.query on acme:dataset:sales/q3ALLOWIn scope, resource matches
analytics.dataset.export on acme:dataset:sales/q3DENYAction not in scope
analytics.dataset.query on acme:dataset:hr/salariesDENYResource outside acme:dataset:sales/*
analytics.report.write on acme:dataset:sales/q3DENYAction not in scope

A read-only agent should pass row one and fail the rest.

Evidence & review

Read access is still access. The proof on each allowed query gives you an auditable record of which datasets the agent touched and why (from the context.purpose), which is exactly what a data-governance reviewer wants when asked "what did the AI see?"

Adapt it

  • Purpose-bound reads: require a purpose in context and reject queries that do not declare one, so every read is attributable.
  • Per-analyst scope: issue one mandate per human analyst so the agent's reads inherit that person's data entitlements.
  • Allow one narrow write: if the agent must save a report, add a single analytics.report.create action scoped to a reports resource — and nothing else.