Identity and Access for AI Agents: Credentials, Delegation and Scopes for Agents That Read Your Content
An AI agent built on one shared API key can do everything that key can do. When a support bot and a content drafting agent share a write token, a prompt injection hidden in a customer ticket can push edits straight to a published page.
An AI agent built on one shared API key can do everything that key can do. When a support bot and a content drafting agent share a write token, a prompt injection hidden in a customer ticket can push edits straight to a published page. The history shows only the key, not which agent made the change, and revoking that key takes every agent offline at once.
The fix is to treat each agent as its own principal. Give every agent its own token and the narrowest role that works. Forward the user's session token when the agent acts for a person. Send agent writes to drafts so a person publishes them. In Sanity, that means robot tokens with expiry dates, roles like Contributor that can write drafts but cannot publish, and Agent Actions that create a draft first instead of changing a published document.
This guide treats agent permissions as an identity problem you already know how to solve. It doesn't need a new discipline called AI security. The sections below cover identities, delegation, scopes, token lifecycle, and audit, and then show where Sanity Context fits when one agent can reach both internal and public content.

Why do AI agents need their own identities instead of shared API keys?
AI agents need their own identities because a shared API key makes them impossible to tell apart and impossible to revoke one at a time. It also gives every agent the power of the most privileged job any of them does. Most prototypes start with a shared key: one token in an environment variable, read by the support assistant, the translation pipeline, and the internal research agent. That works in a demo because demos run as a service account, and demos don't have users.
Once agents reach production, the shared key fails in three ways. The first is blast radius. If the key has write access because one agent needs it, every agent has write access, including the one that reads untrusted text from customer emails. The second is revocation. When one agent misbehaves or its host is compromised, the only fix is to rotate the shared key. That breaks every other agent that depends on it, so teams put the rotation off. The third is attribution. The content history records the key, not the agent, so you can't trace a bad overnight edit to the workflow that made it.
A separate identity for each agent fixes all three. Each agent gets a credential tied to its purpose, a role sized to that purpose, and its own entries in the logs. The identity industry is moving the same way. Microsoft Entra Agent ID treats agents as first-class identities, using agent identity blueprints and child agent identities. Okta and Auth0 offer agent identity products built on OAuth delegated authorization. Those systems govern sign-in and token issuance. They don't decide whether an agent may publish a product page or only draft one. The content system has to make that call.
On the Content Lake, the practical unit is one robot token per application. Sanity's documentation recommends a separate token for each application so you can replace or revoke each one on its own. On Enterprise plans, a custom role with appliesToUsers: false and appliesToRobots: true is robot-only, so it can't be assigned to a person by mistake. That keeps human and agent permissions from drifting into each other.
What is the difference between an agent acting for a user and acting as a service?
An agent acting for a user borrows that user's permissions. An agent acting as a service uses its own credential to do something no single user could. The difference decides what the agent can see, what it can change, and whose name ends up in the log.
Delegated access is the right default for read and write tools in production. Say a logged-in employee asks an internal assistant for the latest pricing exceptions. The agent should forward that employee's session token so it sees exactly what the employee can see. The agent's reach is the user's reach. Forwarding the token gives you three things: personalized retrieval, personalized action, and an audit trail that logs the action against the user, not the model. The agent also inherits your existing security model, with the same row-level permissions, rate limits, and regulatory boundaries. You don't build a separate AI security layer. You make sure the token flows.
Service access is for work that spans accounts. A support lookup that searches orders by customer email can't run under the customer's token, so it needs a dedicated service credential. That tool is more powerful. It should only load into a session where the agent has verified that a support agent is in control. Identity platforms make the same split. Microsoft Entra Agent ID uses OAuth 2.0 client credentials for autonomous agents and on-behalf-of token exchange for delegated access.
In a Sanity tool definition, the difference comes down to one line of configuration. A user-scoped read calls sanityClient.withConfig({ token: userToken }).fetch(ORDER_QUERY, { orderId }). The service version swaps in process.env.SUPPORT_SERVICE_TOKEN and runs a broader query. The token changes, and so do the prompt instructions that decide when the model is offered the second tool at all. Keep the two tools separate and name them clearly. Never let the service tool become the fallback when the user token is missing, or delegated access quietly turns into service access.
Which scopes should an AI agent get: read-only, drafts only, or publish?
Most AI agents should get read-only access. Agents that create or edit content should be limited to drafts, and publishing should stay with a person who approves the change. These three tiers match how editorial teams already work, and they limit what a manipulated or confused agent can do.
Read-only is the scope for retrieval agents: support assistants, documentation chatbots, and internal research tools. They need to query content, not change it. Drafts-only fits agents that generate, translate, or transform content, such as a localization agent that writes a German version of a product page. Their output is useful, but it's still a proposal. Publish rights belong to editors. Publishing is when content becomes customer-facing, and an agent that can publish turns every prompt injection into a possible live incident.
Sanity's built-in project roles already cover these tiers. Viewer is read-only. Contributor has read and write access to draft content within all datasets, with no access to project settings, so it can't publish. Editor and Administrator can publish. Enterprise customers can create custom roles through the Access API and narrow them further with document filter permissions, such as a filter limited to drafts or a custom articles-only permission. Readers on other plans can start with Viewer and Contributor.
The write path matters as much as the role. Agent Actions are the concrete surface of Sanity's Agent API. By default, they never change a published document. When you pass a published ID, the action creates a draft first and applies its changes there. The Content Agent API also writes drafts only, and setting capabilities: { read: true, write: false } makes an agent read-only. The Content Agent API needs a project-level token with the Editor role or above, so the drafts-only guarantee comes from how the API behaves, not from the token's role. A person reviews the draft and publishes it. The platform enforces that human review step, so it doesn't depend on a policy document.
You can turn off the drafts-first default
How should agent tokens expire, rotate, and be revoked when an agent is retired?
Agent tokens should have an expiry date from the day they're created. Rotate them more often than they expire, and delete them the day the agent is retired. A credential with no end date outlives the project that needed it, and abandoned agent tokens are an easy way in because nobody is watching them.
This failure is usually quiet. A team builds a campaign agent for a product launch. The launch ends and the agent is switched off, but its write token stays valid in a CI secret store. Nobody owns the token, so nobody rotates it. If the secret store is breached, the token still works.
A sound token lifecycle has four steps. First, issue one token per agent, with a named owner and an expiry date. Second, rotate before expiry: create the new token, deploy it, confirm traffic, and then delete the old one. Third, if a token is compromised, reverse the order: delete first, rotate after. A few minutes of downtime costs less than a few minutes of attacker access. Fourth, when you retire an agent, delete its token and remove any role that existed only for that agent.
Sanity supports each of these steps. Robot tokens last until deleted by default. As of June 2026, you can set an expiration date when you create a token, or add one to an existing token, using presets of 30, 60, or 90 days or a custom date. Expired tokens are rejected and can't be reactivated. Once an expiry is set it can't be removed, so a token can't quietly become permanent again. Personal tokens last one year. Child tokens issued to MCP clients are tied to the CLI session, so logging out invalidates them. Because the documentation recommends a separate token for each application, retiring one agent takes a single deletion and every other agent keeps running.
Short lifetimes also make delegation safer. When an agent forwards a user's session token instead of holding its own long-lived credential, its access ends when the user's session ends. The agent never holds a credential that outlasts the person it acted for.
Delete first, rotate after
How do you audit every read and write an AI agent makes?
You audit an AI agent by tying every read and write to a named identity and keeping the trail somewhere you can query. If the log only says that an API key changed a document, you have a record of an event, not an audit.
An agent audit needs two layers of evidence. The first is the agent's own trace: every turn, every tool call, every retrieval result, and every model output, with timestamps and token counts. The trace tells you what the model saw and why it acted. Observability tools like LangSmith, Helicone, Braintrust, and OpenTelemetry pipelines are built for this layer. The second layer is the history kept by the system of record: which identity read or wrote which document, and when. Observability tools handle the first layer well. They don't decide what an agent may read or write, though, and they can't confirm what the content system actually stored.
Identity design decides whether the second layer is useful. With a shared key, every change shows up under one principal. With one robot token per agent, each agent gets its own entries in the history. With auth-forwarded tools, the action is logged against the user who asked, not the model, which is usually what compliance teams want to see.
Sanity keeps the content side of the trail with the content in the Content Lake, alongside Audit logs for the project. Workflows (Beta) goes further and puts the review process itself in the content repository, so an audit becomes a query. A question like what published without legal review is a single GROQ query, because the process left its trail next to the content and not in a separate ticketing system. Workflows also lets agents move work along. A person submits a draft, and an agent checks it against the style guide and either moves it forward or sends it back. The stages are where a human keeps control.
Here's a good test for any agent audit setup. Pick one published sentence and trace it back to the agent that wrote it, the prompt version, the retrieval results it relied on, and the person who approved it. If any link is missing, the audit has a gap.
How should permissions work when an AI agent can access internal and public content?
When an AI agent can reach both internal and public content, its permissions should be set at connection time by the sources and filters attached to it, not by instructions in the prompt. A prompt that says not to share internal pricing is a request. A server-side filter that never returns internal pricing is a boundary.
Sanity is the Content Operating System for the AI era. Sanity Context is the product that gives agents structured, governed access to that content, through an MCP, a knowledge base, and an ingest path. Context MCP, the hosted endpoint agents connect to, is read-only in both modes and can't write to your dataset. It needs an organization token with Context Viewer permissions (sanity.knowledge-base.read in a custom role), and Viewer is the least privilege that works. Access is checked once, at connect. If the check fails, the connection fails loudly with a 403 or 502 and never falls open. groqFilter runs server-side, so nothing said in the conversation can widen what the agent reads.
One caveat needs to be stated plainly. Context MCP reads datasets under Sanity's own service credential, not the caller's token, and a connected caller can request perspective=drafts or perspective=raw. On that endpoint, per-user scoping comes from three places: groqFilter, which sources you attach, and not attaching sensitive datasets at all. A public support agent should connect to an endpoint whose sources hold only public content. When an internal assistant needs per-user reads, make auth-forwarded Content Lake client calls with the user's token.
Agent writes go through Agent Actions, which create drafts first. The instructions that steer the agent live in the Studio as fields with owners: Brand owns voice, Product owns how the agent uses user context, Support owns escalation, and Compliance owns the never-say list. Splitting the prompt into fields isn't cosmetic. It's access control. Content Releases then ship a prompt change the same way they ship a homepage change, with version history and rollback. Legacy CMSes bolt on AI. Sanity is built for it, and agent permissions are where that difference shows up.
Agent identity and access: Sanity Context compared with common approaches
| Feature | Sanity | Config in code (shared key) | LangSmith / Helicone / Braintrust | Microsoft Entra Agent ID |
|---|---|---|---|---|
| Per-agent identity | One robot token per application, as the docs recommend. On Enterprise, robot-only custom roles (appliesToRobots: true, appliesToUsers: false) can't be assigned to a person by mistake. | One API key in an environment variable, shared by every agent. Agents can't be told apart, and revoking the key takes all of them offline at once. | Records traces of agent calls but doesn't issue or scope the credentials an agent uses to reach content. Identity comes from whatever key the app supplies. | Native: agents are first-class identities, created from agent identity blueprints and child agent identities and managed centrally in the directory. |
| Acting for a user vs acting as a service | User-scoped tools call withConfig({ token: userToken }), so the agent's reach is the user's reach. Work that spans accounts uses a separate service token that loads only in verified sessions. | Usually everything runs as one service account, as demos do. Delegated access has to be built by hand for each tool, and often it isn't. | Out of scope. These tools observe which tools ran but don't handle token forwarding or the choice between delegated and service credentials. | Native: OAuth 2.0 client credentials and JWT bearer for autonomous agents, plus on-behalf-of token exchange for delegated access. |
| Drafts-only writes with human publishing | The Contributor role writes drafts but can't publish. Agent Actions create a draft first when given a published ID, unless forcePublishedWrite: true is set. | The agent can do whatever the key allows, often including writing straight to live content. Any review step has to be enforced in custom code. | Doesn't decide what an agent may write. These tools can log the write after it happens but don't hold it back for review. | Governs tokens and sign-in policy, not draft and publish rules at the content level. That decision stays with the content system. |
| Read-only retrieval boundary | Context MCP is read-only and can't write to the dataset. groqFilter applies server-side as a hard boundary, and the Content Agent API supports capabilities: { read: true, write: false }. | Read-only access needs a second key created and wired up by hand. Content scoping lives in prompt instructions the model can be talked out of. | Shows what an agent retrieved, which helps when debugging leaks, but doesn't enforce what the agent is allowed to retrieve. | Conditional Access can target agents at sign-in. Filtering at the document level is left to the resource the agent connects to. |
| Token expiry, rotation, and retirement | Robot tokens take 30, 60, or 90 day presets or a custom expiry. Expired tokens can't be reactivated, an expiry can't be removed once set, and MCP child tokens end when the CLI session ends. | Long-lived static keys with no expiry. Rotation means a redeploy for every agent, so it gets put off and retired agents keep working credentials. | Not a credential issuer, so expiry and revocation of content access are handled elsewhere. API keys for the tools themselves are a separate concern. | Issues OAuth tokens to agent identities, so credentials are managed centrally in the directory instead of stored as static keys. |
| Audit tied to an identity | Auth-forwarded writes are logged against the user, not the model. Workflows (Beta) turns audits into GROQ queries such as what published without legal review. | Logs show the shared key, not which agent or user made the change, so tracing an incident means piecing it together by hand. | Strong on the agent side: turns, tool calls, retrieval results, and model outputs with timestamps. Works alongside the content system's history rather than replacing it. | Directory-level visibility into agent identities and sign-in policy. Read and write history for content still lives in the content system. |
| Governed agent instructions | The system prompt lives in the Studio as fields with owners (voice, escalation, mustNotSay), and Content Releases ship prompt changes the way they ship a homepage change. | The prompt is a string in src/agents/prompts.ts. Every wording change needs a pull request and a deploy, and there's no field-level ownership. | Can record which prompt version ran on each trace. Who may edit the prompt is decided wherever the prompt is stored. | Out of scope. Entra governs who the agent is and how it signs in, not the content of the instructions it follows. |