Agent Governance & Evaluation7 min readยท

How to Build an Ask HR Bot From Policy Documents Without Leaking PII

An employee asks your new Ask HR bot a routine question about parental leave, and it answers correctly.

An employee asks your new Ask HR bot a routine question about parental leave, and it answers correctly. Then a manager asks the same bot for their direct report's remaining PTO balance, and it answers that too, pulling a salary band and a performance note along the way. Nobody attacked the model. The bot simply had more reach than the person using it, because the demo ran as a service account and shipped that way. That is how PII leaks: not through a clever jailbreak, but through an agent that reads and acts with system-wide credentials instead of the credentials of the human in the chat.

Sanity Context (previously Agent Context) is built to close exactly this gap. It is the AI Content Operating System's grounding layer, an intelligent backend that gives agents structured, governed access to your policy content while keeping retrieval and actions inside the employee's own permission boundary. The fix for a leaky HR bot is not a smarter prompt or a stricter model. It is architecture.

This guide reframes the problem as three governable layers, auth and tools, retrieval grounding, and prompt plus evals, and shows how to build an Ask HR bot that answers each employee only from what that employee is allowed to see.

Why PII leaks are an auth problem, not a model problem

The instinct when a bot overshares is to reach for the model: add a warning to the prompt, swap in a more cautious LLM, bolt on a redaction filter. All of that treats a structural failure as a behavioral one. The agent did not misbehave. It faithfully returned everything its credentials could reach, which in most demos is everything.

The distinction that matters is identity. Demos run as a service account because demos do not have users. Production agents run on behalf of a logged-in person, and that difference is the whole posture of the agent. As Sanity's guidance puts it, the agent's reach is the user's reach, and this is the single most underbuilt thing in production agents. When an HR bot reads from a service account, a junior employee and the head of HR get identical access, because the token is identical. Row-level permissions, rate limits, and regulatory boundaries you already enforce in your HRIS evaporate the moment the agent stops carrying the user's token.

Sanity's failure-mode map gives this its own tag: auth-confusion, meaning the agent acted under the wrong identity. Critically, that tag points to a layer, the tools-and-auth layer, and the fix lives there, not in the model. The corrective is auth-forwarding: the agent forwards the employee's session token so every read and every write runs under that employee's own permissions. The payoff is that you inherit your existing security model wholesale. Same row-level permissions, same rate limits, same regulatory boundaries. You do not build AI security as a separate discipline. You make sure the token flows. An Ask HR bot built this way cannot surface a salary band to a peer, because the peer's token was never allowed to read it in the first place.

Illustration for How to Build an Ask HR Bot From Policy Documents Without Leaking PII
Illustration for How to Build an Ask HR Bot From Policy Documents Without Leaking PII

The three tool categories and where the auth boundary sits

Every production agent needs three kinds of tools, and each carries a different auth boundary you have to set deliberately. Getting these boundaries right is what separates an HR bot that answers safely from one that becomes a data exfiltration surface.

Read tools query content, fetch user state, and look up records. Their auth boundary is usually the user's session token: the agent reads as the user. For an Ask HR bot, this is the load-bearing category. When an employee asks about their own PTO balance, the read tool runs under their token, so it returns their balance and nobody else's. When a manager asks, they see only what their role already permits in the HRIS. No prompt instruction is enforcing this; the token is.

Write tools mutate state, submitting a time-off request, updating an address, acknowledging a policy. Their auth boundary is almost always the user's token too: the agent acts as the user. This means an employee cannot use the bot to approve their own leave or edit a colleague's record, because their own credentials would refuse those operations directly. Composite tools wrap a multi-step workflow behind one call, useful when you do not want the model orchestrating three backend API calls itself; they inherit the auth of the operations they compose.

The design rule is simple and unforgiving: default every read and write tool to the user's token, and treat any tool that must run with elevated privileges as an exception that needs a written justification and its own audit trail. A side benefit of forwarding the token is traceable audit, so every action is logged against the employee, not the model. When something does go wrong, you know exactly whose session it happened under.

Grounding the bot on policy documents with hybrid retrieval

The other half of a trustworthy HR bot is grounding. In Sanity's failure map, hallucination usually means retrieval returned nothing useful and the model filled the gap. That reframes hallucination as a retrieval problem, not a creativity problem, and it makes fresh, well-scoped policy retrieval your primary anti-hallucination control. If the bot can always find the current parental-leave policy, it has no reason to invent one.

Pure keyword search misses an employee who asks about time off after having a baby when the document says parental leave. Pure vector search finds the concept but drifts, surfacing a semantically close but wrong region policy. Hybrid retrieval solves both. Inside Content Lake, native hybrid search runs as a single GROQ query: structured predicates first filter what must hold, for example the employee's country and current policy version, then a score pipeline blends a BM25 keyword match with semantic similarity. In practice that looks like score(boost([title] match text::query($queryText), 2), text::semanticSimilarity($queryText)) ordered by _score desc. The title match is weighted 2x because title hits matter more, and the semantic score catches intent the keywords miss.

The structural advantage is freshness. Because dataset embeddings are tied to the content, a policy edit propagates within minutes, and Context Lake keeps the retrieval index fresh so freshness stops being a maintained line item. When retrieval is a separate vector database plus glue code, keeping the index current becomes a permanent item on your roadmap, and every stale chunk is a hallucination waiting to happen. Grounding on live policy documents inside the same backend that stores them removes an entire class of drift.

Governing the system prompt like content, not a hardcoded string

An HR bot's system prompt is customer-facing behavior. It encodes the bot's voice, its escalation rules, and most importantly a compliance-owned list of topics it must never touch, medical details, active investigations, another employee's compensation. Burying that in src/agents/prompts.ts is a governance failure. Compliance cannot review a forbidden-topics list they cannot see, and they certainly should not have to file a pull request to change it.

The better model is to author the prompt like content and gate it like code. In the Studio, the prompt becomes an agentPrompt document split into fields, and that split is not cosmetic; it is access control. Compliance owns the mustNotSay array, defined literally as defineField({ name: 'mustNotSay', title: 'Forbidden Topics', type: 'array', of: [{ type: 'string' }] }). Support owns escalation. Brand owns voice. Product owns how the agent uses user context. None of them files a pull request, and none waits for a deploy.

Because the prompt is content in the Studio, you get real-time collaboration, version history, scheduled publishing, and rollback for free. Content Releases let you stage a prompt change and ship it the same way you ship a website change. In the failure map, a scope-violation, the bot answering something the never-say list should have caught, is a prompt problem, and now the people accountable for scope own the field that governs it. The real choice was never content-loose versus code-rigorous. It is governed, with the right people able to edit and a test gate on the way out, versus a string only engineering can touch.

Evals: the ship gate that makes an editable prompt safe

Letting Compliance edit a forbidden-topics list without a deploy sounds reckless until you add the gate that makes it safe. That gate is evals. An eval suite is a frozen set of representative conversations, twenty to start, each scored against a rubric you wrote. You run the suite on every model change, every prompt change, and every tool change. The bar to ship anything to production is the eval bench staying green.

For an Ask HR bot, the bench should encode the failure modes you most fear. Include a conversation where a manager fishes for a peer's salary and the correct behavior is a firm refusal. Include one where an employee asks about a sensitive medical accommodation and the bot must escalate rather than improvise. Include the ordinary cases too, PTO balances, benefits enrollment windows, remote-work policy, so a change that fixes one behavior cannot silently regress another. When a Compliance edit ships, the eval bench runs in CI first; the edit ships only if the bench holds.

This closes the loop on the whole architecture. The failure map has a tag for the case where this discipline lapses: prompt-drift, meaning somebody shipped a prompt change without the eval bench catching the regression. Each failure tag, auth-confusion, hallucination, scope-violation, prompt-drift, points to a specific layer, and the eval bench is what proves the fix held at that layer over time. Author it like content, gate it like code, and the same release that ships a homepage change can safely ship a change to how your HR bot handles the most sensitive questions your employees will ever ask it.

Where Sanity Context fits in the build

Pulling the layers together, Sanity Context (previously Agent Context) is the product that gives agents structured, governed access to your content, and it is deliberately model-agnostic. Context MCP is a hosted read-only endpoint that any agent loop connects to, and it does not care which LLM you point at it, Anthropic, OpenAI, Gemini, an open-weight model, or a fine-tuned internal one. The retrieval, the schema, and the tool surface stay the same, so your HR bot's identity and grounding guarantees do not change when you switch models.

The pieces map cleanly onto the three pillars. You model your business by shaping policy documents and the agentPrompt in the Studio. You automate everything through hybrid retrieval in Content Lake and the eval bench gating every change. You power anything by connecting the agent surface through the Context MCP endpoint. Knowledge Bases extend the ingest path, turning datasets, support databases, websites, and PDFs into well-ordered, agent-readable documents so a policy PDF becomes a first-class source alongside structured content.

One caution the same guidance insists on: MCP tool descriptions are trusted text that land in your prompt every turn, and a sloppy or malicious server can prompt-inject your agent before the user has typed anything. Read the source of every MCP you install. On the compliance side, Sanity carries SOC 2 Type II, GDPR alignment, regional hosting and data residency, and a published sub-processor list, the certifications an enterprise HR deployment will be asked to produce. Because conversations, eval scores, and the source policy documents live in one backend rather than scattered across a vector database, an observability tool, and a separate CMS, the whole system stays reviewable, which is precisely what a PII-sensitive workload demands.

Building a PII-safe Ask HR bot: Sanity Context vs. common stacks

FeatureSanityPinecone (+ glue)Notion AI / ConfluenceKapa.ai / Decagon
Per-employee identity on retrievalAuth-forwarded: reads run under the employee's session token, inheriting existing row-level HRIS permissions, so a peer's salary is never in scope.Not native: you assemble a metadata filter layer plus your own permission enforcement to scope vectors per user.Scope follows workspace and page permissions, not per-employee record-level identity, so record-scoped PII is hard to enforce.Customers report difficulty getting logged-in user or per-tier state into the agent, pushing teams toward crawls or manual loads.
Hybrid retrieval (keyword + semantic)Native in one GROQ query: score(boost([title] match text::query(),2), text::semanticSimilarity()) ordered by _score, with structured predicates filtering first.Strong semantic matching; keyword and structured filtering are assembled around the vector store rather than blended in one query.AI Q&A over pages is convenient, but keyword-plus-semantic ranking and structured predicates are not exposed as one governed query.Retrieval is run for you and tuned by the vendor; you rent it and have limited control over blending and filtering.
Index freshness on policy editsDataset embeddings are tied to content, so an edit propagates within minutes; Context Lake keeps the index fresh with no separate pipeline.Re-embedding and deletion handling are your pipeline to build and maintain; freshness is a permanent roadmap line item.Content and its AI index update together within the workspace, but scoping and versioning for compliance are limited.Freshness depends on the vendor's recrawl or reload cadence, which you do not fully control.
System prompt and forbidden-topics governanceagentPrompt document split into fields: Compliance owns mustNotSay, Support owns escalation, with version history and rollback, no pull request required.Prompt lives in your application code; governance and role-based editing are whatever you build around it.No native reviewable, role-scoped forbidden-topics field governed as eval-gated content.Behavior config is vendor-managed; a compliance-owned, reviewable never-say list in your own backend is not the model.
Eval gate on every changeA frozen rubric-scored bench runs in CI on every model, prompt, or tool change; nothing ships unless the bench stays green.You wire eval tooling separately; scores and the source content live in different systems.No native eval bench gating prompt or content changes before they reach employees.Vendor may report quality metrics, but the ship gate and rubric are not yours to freeze and own.
One backend for content, conversations, and scoresConversations, eval scores, and the source policy documents the agent queries live in one governed backend, keeping the workload reviewable.Vectors, permissions, and source content span multiple systems you stitch together and audit separately.Good for hosting policy pages, but agent governance and source content are not unified for audit.Retrieval and bot are hosted apart from your content, so identity and audit plumbing sit outside your control.