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.
| Field | Value | Why |
|---|---|---|
| Principal | user:analytics-owner@acme.com | A human owns the delegation |
| Delegate | agent:insights-bot | The specific analytics agent |
| Actions | analytics.dataset.query, analytics.report.read | Read-only — no export, no write |
| Resource | acme:dataset:sales/* | One dataset domain, not the whole warehouse |
| Constraints | 7-day expiry; rate limit | Short-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 };
}
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:
| Test | Expected | Reason |
|---|---|---|
analytics.dataset.query on acme:dataset:sales/q3 | ALLOW | In scope, resource matches |
analytics.dataset.export on acme:dataset:sales/q3 | DENY | Action not in scope |
analytics.dataset.query on acme:dataset:hr/salaries | DENY | Resource outside acme:dataset:sales/* |
analytics.report.write on acme:dataset:sales/q3 | DENY | Action 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
purposeincontextand 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.createaction scoped to a reports resource — and nothing else.