What it can do, and as whom
These are the questions your security team is going to ask. I’d rather you hear the answers from me first.
Seven questions, straight answers
Every control below fails closed. If it isn’t configured, it isn’t allowed. Tap a question for the long answer.
- Can someone act as another user?No
- Can one person’s chat or memory leak to another?No
- Can an API caller run as a system?Only with a grant
- Can the model run shell commands or touch the disk?No
- Can a session send the agent to any URL?Only allowlisted
- Can an MCP server trust who’s calling?Yes
- What can a run reach?Depends where it runs
Can someone act as another user?
NoIdentity is always the verified caller or a trusted first-party assertion. It’s never something the client made up.
| Surface | Identity | Verified by |
|---|---|---|
| Chat | The Cognito sub in the bearer token on the socket | AgentCore’s inbound JWT authorizer, against the shared user pool |
POST /agent/sessions, /agent/connect, /agent/tasks | The Cognito sub of the API caller | The CoreApi Cognito authorizer |
Run Agent Task event | The principal in the event detail | The account-internal bus: only first-party Lambdas can put events on it |
| Persistent agent task | { type: system, id: <type>/<agentId> } | handleTask in your stack: the trigger’s agent must belong to the type and match the principal |
| Session ownership | The userId stored at creation | The host compares it to the verified caller at connect and at task time and refuses a mismatch |
- 1Cognito JWT on every CoreApi call: the caller’s
subbecomes the identity. - 2AgentCore’s inbound JWT authorizer on the socket, then the runtime’s session-owner check.
- 3
McpAllowedHostswhen a session is created: an MCP URL outside the allowlist is rejected. - 4
SystemTaskPrincipalsbefore an API caller may run as a system id. - 5Only first-party Lambdas can put events on the account-internal bus.
- 6Outbound MCP calls carry the authority-minted
authHeader, applied after user headers. - 7
http_requestlets the model pick the URL. Turn it on deliberately and put a fence around egress.
Can one person’s chat or memory leak to another?
NoSessions are owned, and memory is scoped by the verified id.
Every session stores its owner when it’s created, and the host compares that owner to the verified caller on every connect and every task. So leak or guess a sessionId and you still can’t resume someone else’s conversation.
Long-term memory is namespaced by the verified sub (/facts/{sub}, /users/{sub}/preferences), not by anything the client sends. Recall never crosses users.
Can an API caller run as a system?
Only with a grantOnly with an explicit grant in a deploy parameter, the same way the MCP allowlist works.
Autonomous work runs as one of two identities, and they don’t get the same trust. { type: "user", id: sub } is a verified person, and it’s the default for POST /agent/tasks. { type: "system", id: "booked" } is a service with nobody behind it. A first-party emitter on the internal bus can claim a system principal for free, because only first-party code can reach that bus. Over the public API, claiming one is a privilege.
| Where | Gate |
|---|---|
Internal bus (requestAgentTask) | None: a first-party emitter is already trusted to assert a principal. |
POST /agent/tasks { system } | The SystemTaskPrincipals deploy parameter: comma-separated sub:systemId grants; sub:* allows any id; empty rejects all. Fails closed. |
| Launcher record | When a person launches a system task over the API, their sub is stored as createdBy so they can still read the task back even though the run acts as the system. |
# sub abc-123 may run as "booked" only; svc-sub may run as any system id
SystemTaskPrincipals="abc-123:booked,svc-sub:*"Can the model run shell commands or touch the disk?
NoThe SDK’s bash and file_editor are deliberately left out.
In a shared hosted runtime, shell exec is remote code execution, and filesystem access reaches the host’s disk. No thanks. A host that really wants them can import them from the SDK and register them itself, as an explicit choice that shows up in code review.
What a session can use is a registry of named first-party tools. Turning on a tool that already ships is a data change. Writing a new one is the only time you touch code.
import { tool, builtinTools, resolveTools, createAssistant } from '@readysetcloud/agent';
import { z } from 'zod';
// Host registry: built-ins plus the host’s own.
const TOOL_REGISTRY = {
...builtinTools, // http_request, notebook
get_current_time: () => tool({
name: 'get_current_time',
description: 'Returns the current server time as an ISO-8601 string.',
inputSchema: z.object({}),
callback: async () => new Date().toISOString(),
}),
};
// A session opts in by name: { tools: ['http_request', 'get_current_time'] }
const tools = resolveTools(config.tools, TOOL_REGISTRY, { sessionId, userId });
const agent = createAssistant({ sessionId, tools: [...tools, ...mcpClients] });| Built-in | What it does | Watch out |
|---|---|---|
http_request | Generic HTTP client. The web-search seam: point it at a search API and let the model read the JSON back. Also any public REST endpoint or webhook. | An outbound-request surface: the model chooses the URL. Front it with an allowlist or prefer a scoped MCP gateway when egress matters. |
notebook | An in-invocation scratchpad the model writes to and re-reads across steps of a run. | State lives in the invocation, not on the host. |
Can a session send the agent to any URL?
Only allowlistedOnly hosts in McpAllowedHosts. An empty list rejects everything, so it’s opt-in.
Attaching an MCP server is the one place a session gets to point the runtime at an outbound URL, so it gets the most scrutiny. Servers are keyed by label, and the host connects to each one at build time and adds its tools alongside the first-party ones. Secrets never live in the session row, since users create those. They come from the host’s environment through ${VAR} interpolation.
await createSession({
userId,
mcpServers: {
blog: {
url: 'https://mcp.example.com/mcp',
headers: { 'x-api-version': '2025-01' }, // user-supplied; ${VAR} interpolated by the host
authHeader: { // authority-minted, forwarded verbatim
name: 'x-booked-auth',
value: '<base64url(payload)>.<sig>',
},
},
},
});| Control | Owner | Rule |
|---|---|---|
| SSRF allowlist | Host (create-session) | Rejects any mcpServers whose URL host is not in McpAllowedHosts. Empty rejects all, so it is opt-in. |
| Secrets | Host environment | ${VAR} in string fields resolves against the host’s env. A secret never belongs in the session row, which the user creates. |
authHeader | An authority (a trusted server-side session creator) | Mints a token binding tenant, user, session, and a version; the runtime forwards it verbatim, applied after user headers so they cannot shadow it, with no interpolation. The MCP server verifies it with the shared secret. |
The other outbound door is the http_request built-in, which lets the model pick the URL. Turn it on deliberately, and put a fence around egress when that matters.
Can an MCP server trust who’s calling?
YesYes, when a trusted authority mints an authHeader for the session.
The runtime is just a courier. A trusted server-side session creator mints a token that binds tenant, user, session, and a version. The runtime forwards it verbatim, after the user’s headers so they can’t shadow it, and never looks inside. The MCP server checks the signature with a shared secret.
x-booked-auth) and never looks inside. The token binds tenant, user, session, and a version, so a leaked one can’t hop to another session, and the authority revokes it by bumping the version. Hit replay to follow the token.The threat model, plainly: the HMAC proves the authority minted this token for user U. It does not prove the presenter is this runtime. That’s fine here because users can’t reach the config row, the host is allowlisted, and transport is TLS. The token gets replayed on every reconnect for the session’s ~30-day life, so bind it to the sessionId and include a version the authority can bump to revoke it.
What can a run reach?
Depends where it runsThe shared runtime and task Lambda enforce their boundaries for you. In library mode, your stack owns them.
| Boundary | Chat runtime | Task Lambda | Library mode · persistent agents |
|---|---|---|---|
| Bedrock | The deploy’s model grant | Same grant | Your stack’s |
| Table | AgentChatTable, by session | Same table, by task and session | Any table via tableName |
| Cross-session memory | AgentCore Memory, per verified user | None by default | Whatever memoryManager you pass |
| Outbound network | MCP hosts in the allowlist; http_request if enabled | Same | Yours |
| Identity | Verified JWT | Event principal or verified JWT | Yours |
Library mode means passing tableName and running the core in your own stack, which is exactly how every persistent agent runs. It doesn’t get the shared runtime’s guarantees for free. Your stack owns the Bedrock grant, the allowlist, and memory isolation.