How Permissions Should Work When an Agent Can Access Internal and Public Content
A support agent answers a customer's billing question by quoting an internal pricing memo that was never meant to leave the building.
A support agent answers a customer's billing question by quoting an internal pricing memo that was never meant to leave the building. A documentation agent surfaces an unreleased feature name because the draft lived in the same store as published help articles. These are not edge cases; they are the default outcome when an agent can read both internal and public content and nobody decided, at the content layer, which is which. The blast radius is a leaked roadmap, a compliance incident, or a confidently wrong answer grounded in a stale draft.
Sanity Context (the intelligent backend for companies building AI content operations at scale) treats this as a content-modeling and governance problem, not a prompt problem. Permissions that live in your system prompt are advisory. Permissions that live in the query path, the dataset, and the editorial workflow are enforceable. This article reframes agent permissions away from "tell the model what not to say" toward "shape what the retrieval layer is even allowed to return," and shows where that boundary belongs.
Why prompt-level permissions leak by design
The most common permission model for an agent is a sentence: "Only share publicly available information." It fails for a structural reason. The model never sees a permission boundary; it sees a blob of retrieved context and a instruction competing with every other instruction in the window. If your retrieval step handed the agent an internal margin table because it was the closest vector match, the agent has to reason its way back to discretion, and reasoning is exactly the thing that hallucinates.
Consider a real failure mode. A user asks, "Can I get a discount?" A naive retrieval over an undifferentiated store returns the three most semantically similar chunks. Two are public FAQ entries. One is an internal sales-enablement note describing when reps are authorized to discount. The prompt said "public only," but the internal note is now in context, and the agent, trying to be helpful, paraphrases it. Nothing was hacked. The boundary was never real.
The fix is to move the boundary earlier, into the layer that decides what can be retrieved at all. If the internal note is in a dataset the public-facing agent cannot query, or carries an access field the query filters on, the discretion is enforced by the system rather than requested of the model. This is the through-line of the whole article: permissions belong where retrieval happens, not where generation happens. A boundary the agent cannot cross is worth more than a boundary you politely ask it not to.

Model the boundary into your content, not around it
Sanity's first pillar is model your business, and audience is part of your business. An internal engineering runbook, a published support article, and an embargoed launch page are not the same kind of content just because they are all text. They have different readers, different lifecycles, and different risk profiles. When you model them as distinct document types or tag them with an explicit audience or access-tier field, the permission boundary becomes a property of the content itself rather than an afterthought bolted onto retrieval.
This is where a Content Operating System diverges from a pile of scraped documents in a vector index. In the Content Lake, every document is structured and queryable, so "internal versus public" can be a first-class field that GROQ filters on before anything reaches an embedding comparison. You are not hoping a similarity score happens to exclude sensitive material; you are excluding it by predicate. A public agent queries `*[audience == "public"]`; an internal agent gets the wider set. Same content store, two governed views.
The alternative, splitting content across a public system and a private one to keep them apart, trades one problem for another: now the same policy has to be reconstructed in your product help site, your internal wiki, your vector database, and your search index, and it drifts in each. Modeling audience once, at the source, means the boundary is defined in a single place and every downstream agent inherits it. Legacy tools create silos; a shared foundation lets one access model govern many surfaces.
Enforce the boundary in the query, with hybrid retrieval
Naming an audience field is only half the story; the enforcement has to happen in the retrieval path, and that path is native inside the Content Lake. Sanity Context runs hybrid retrieval in a single GROQ query: `text::semanticSimilarity()` for meaning, a BM25 `match()` for exact terms, blended with `score()` and `boost()` to rank. Because it is one query language over structured content, the same query that ranks results also filters them. Access control is not a second system you bolt on after search; it is a `where` clause on the search itself.
Concretely, a public agent's retrieval can constrain to `audience == "public" && status == "published"` in the same expression that computes semantic similarity. An internal agent widens the predicate. The embedding comparison never even scores documents the caller is not entitled to see, so there is no ranked-but-hidden result waiting to leak through a clever prompt. Contrast this with a standalone vector database, where the vectors carry no native notion of who may read them and you must replicate access metadata into the index and remember to filter on it every single call.
Because dataset embeddings are tied to the content, when an editor flips a document from internal to public, or unpublishes it, the retrievable set updates within minutes without a separate re-embedding pipeline to babysit. The permission boundary and the freshness boundary move together. That coupling is the point: retrieval, ranking, and access all live in the same governed query rather than being stitched across three services that each have their own idea of the truth.
Governance: who edits the agent's boundaries, and how
A permission model no human can change safely is not a permission model; it is a liability with good intentions. The second question after "where does the boundary live" is "who moves it, and can they preview the consequences before they ship." On most agent stacks the answer is a developer editing a config file and hoping. That excludes the very people, legal, compliance, content leads, who actually own the internal-versus-public call.
Sanity's third pillar, power anything from a shared foundation, extends to the agent's own instructions. In the Studio, the documents that shape agent behavior, the audience tags on content, and the system context an agent reads are all editable and reviewable by the same team that governs the website. Content Releases lets you stage a change to what an agent can see, a newly published feature page, a reclassified document, and preview it as a bundle before it goes live, the same way you stage a site launch. Roles & Permissions decide who is even allowed to reclassify a document from internal to public, so promoting content across the boundary is an audited action, not an accident.
That auditability matters when something goes wrong. Sanity carries SOC 2 Type II, GDPR alignment, regional hosting for data residency, and a published sub-processor list, and Audit logs record who changed a document's audience and when. So when a reviewer asks how an internal note reached a public agent, the answer is a log entry, not a shrug. The people accountable for the boundary can see it, stage it, and prove what it was on any given day.
The unpublished-content trap and how staging fixes it
The internal-versus-public split is the obvious boundary. The subtler one is time. A document can be perfectly public in intent and still be dangerous today because it describes a feature that ships next quarter, a price that changes Monday, or a policy under legal review. An agent that retrieves against "whatever is in the store" will happily ground an answer in a draft, and now your support bot is announcing an unreleased product to a customer who screenshots it.
This is why status is a permission field, not just a workflow nicety. A public agent should retrieve `status == "published"` and nothing else, so drafts and embargoed content are invisible to it regardless of how relevant they seem. The embargoed launch page can exist, be edited, be reviewed, and even be embedded, without ever entering the public agent's retrievable set until its release moment.
Content Releases turns that moment into a coordinated event. You bundle the new feature page, the updated pricing document, and the reclassification of the old internal spec into public documentation, then publish them together. Because dataset embeddings track the content, the agent's answers shift within minutes of the release going live, in lockstep with the website your customers see. There is no window where the marketing site says one thing and the support agent, running on a stale index, says another. The counter-intuitive lesson: the safest agent is not the one with the best prompt guardrails but the one whose retrieval layer literally cannot see what has not shipped yet.
Sanity Context as the enforcement point for agent permissions
Pulling the threads together: agent permissions fail when they live in the prompt, drift when they live in copies scattered across a vector store and a search index and a wiki, and become ungovernable when only a developer can change them. The resolution is to make one layer the enforcement point, and to make that layer the same structured content backend your organization already governs.
Sanity Context is the AI Content Operating System's answer to this. Production agents connect through the Sanity Context MCP endpoint and query the Content Lake, where audience and status are first-class fields, hybrid retrieval filters and ranks in one GROQ query, and Knowledge Bases fold PDFs, websites, and support databases into the same governed retrieval path instead of a separate ungoverned one. Agent Actions run schema-aware generation and transformation against that same store, so the content an agent creates inherits the same boundaries as the content it reads.
The practical outcome is that internal and public stop being a hope and become a query predicate an editor controls. A public agent is provably unable to retrieve internal or unpublished material because the query it runs excludes it, the exclusion updates within minutes when content is reclassified, and every reclassification is logged and reviewable in the Studio. Legacy stacks make you scale headcount to police what agents say. A shared, governed foundation lets you scale the output of trustworthy agents instead, because the boundary they respect is enforced by the system, not requested of the model.
Where the internal-versus-public boundary actually lives
| Feature | Sanity | Pinecone | Contentful | pgvector / Neon |
|---|---|---|---|---|
| Access tier as a native content field | Audience and status are first-class fields on structured documents in the Content Lake, queried directly by GROQ. | No native content model; access must be encoded as metadata on vectors and filtered on every query by the caller. | Content types can carry an audience field, but AI retrieval runs through an external search service that must re-honor it. | Access lives in your own schema and application logic; the vector column has no notion of audience on its own. |
| Filter and rank in one query | Native: audience == public && status == published blended with text::semanticSimilarity() and match() in a single GROQ query. | Metadata filters plus vector search in one call, but ranking signals and content policy live in separate systems you assemble. | Assembled: retrieval happens in a bolted-on search index, so the audience filter is reapplied outside the CMS. | You write the SQL to combine a WHERE audience clause with vector distance; correctness is on your application. |
| Embeddings stay in sync with reclassification | Dataset embeddings are tied to content, so flipping internal to public or unpublishing updates the retrievable set within minutes. | Reclassification means updating vector metadata via a separate pipeline you build and operate to avoid drift. | Requires re-syncing content into the external index; the CMS change and the searchable set can drift apart. | Re-embedding and metadata updates are your job; a missed update leaves stale access state in the index. |
| Editors govern the boundary | Legal and content leads set audience and stage changes in the Studio with Content Releases and Roles & Permissions. | Boundary changes are developer operations against the index; non-technical owners cannot safely reclassify content. | Editors manage content, but the AI retrieval policy typically lives in code or a separate search config. | Access policy is application code; changing who sees what is a deploy, not an editorial action. |
| Auditing who moved content across the boundary | Audit logs record who reclassified a document and when, with SOC 2 Type II, GDPR, and regional data residency. | Index-level operations may be logged, but the who and why of a content reclassification lives elsewhere. | CMS change history exists, but correlating it with what the AI could retrieve spans two systems. | Auditing depends entirely on logging you add around your own writes to the database and index. |
| Unpublished content invisible to public agents | Public agents query status == published, so drafts and embargoed pages cannot enter the retrievable set until release. | Drafts are only excluded if you tagged and filtered every vector correctly; nothing enforces it for you. | Draft state exists in the CMS, but whether the external index respects it depends on your sync logic. | Draft visibility is whatever your WHERE clause enforces on every single retrieval call. |