End-user identity
Cheela does not authenticate your users and does not try to. It carries an opaque credential from the caller to your handler, and your own code verifies it exactly as your REST API already does.
The problem
order-status is harmless. my-orders is not — it acts as somebody, and it has to know who.
That question gets sharper once you publish a manifest, because then every capability is callable by strangers, including agents that have no account with you at all. A capability reading a customer’s own records without an identity check is a data leak, not a feature.
How it works
One field, carried end to end and interpreted at exactly one point — yours.
browser Cheela your endpoint
│ │ │
│ endUserToken ───▶│ │
│ │ ── forwarded untouched ───▶│
│ │ (never parsed, │ your verify code runs
│ │ never logged, │ here, and only here
│ │ never traced) │The credential is whatever your application already issues — a session JWT, an opaque session id, anything. Cheela treats it as bytes.
metadata is developer-supplied context and is recorded in execution traces. A user credential must not be, so it travels in its own field that the trace path never touches.
Marking a capability
Set requiresEndUser on anything that acts as somebody: placing an order, reading order history, changing an account.
runtime.register(
{
name: "my-orders",
description: "Lists the signed-in customer's recent orders",
version: "1.0.0",
requiresEndUser: true,
// No userId field. See below.
input: z.object({
limit: z.number().int().positive().max(50).default(10),
}),
},
{
name: "list",
async handler(context, input) {
const userId = await verifySession(context.endUserToken!);
return { orders: await db.orders.forUser(userId, input.limit) };
},
},
);The flag is enforced in two places, before your handler runs:
| WHERE | WHAT HAPPENS WITHOUT A CREDENTIAL |
|---|---|
| Your runtime | Runtime.execute() throws before dispatching, so forgetting a check in one implementation cannot make the capability reachable anonymously. |
| The public broker | Refused with 401 before anything is metered — otherwise calls that could never succeed would still drain the owner’s quota. |
Leaving the flag off means the capability is callable anonymously, including by a third party’s agent that found you through your published manifest. For read-only public data that is exactly right. For anything else it is a bug.
Sending the credential
From React
Give CheelaProvider a function, not a string. A shopper can sign in long after the widget mounted, and a value read once would pin whatever was true then.
<CheelaProvider
apiKey={process.env.NEXT_PUBLIC_CHEELA_PUBLIC_KEY!}
endUserToken={() => session?.token}
>
<Chat />
</CheelaProvider>Return undefined for a visitor who is not signed in. Capabilities marked requiresEndUser then refuse to run, which is the intended outcome rather than a failure to handle.
Over HTTP
{
"messages": [ ... ],
"endUserToken": "session_abc123"
}The same field works on the broker path, where it is optional and usually absent — most callers there are strangers’ agents with no account. It is accepted rather than forbidden because a shop may well issue its own users a credential to hand to an agent they trust.
Verifying it
Use the code your API already uses. This is the whole point: there is no second identity system to keep in sync.
async function verifySession(token: string): Promise<string> {
// Whatever your REST API does today — a JWT check, a session-store lookup.
const claims = await jwt.verify(token, process.env.SESSION_SECRET!);
return claims.sub;
}Throwing fails the capability call, which is the correct outcome for a credential you do not recognise. The model sees a tool_result carrying the error and can tell the user to sign in.
Never take identity from input
The tempting version of my-orders takes a userId parameter. Do not write it.
// WRONG — whoever calls picks whose orders to read.
input: z.object({ userId: z.string(), limit: z.number() }),A model can be talked into passing any value, and on the broker path the caller writes the input directly. Identity comes from the verified credential; the input carries only what the user is asking for.
What Cheela does not do
- Does not parse it. No assumption that it is a JWT, or has a shape at all.
- Does not verify it. Only you can — it is your secret and your session store.
- Does not store it. Not in traces, not in analytics, not in logs.
- Does not refresh it. An expired credential fails in your handler, like any other.
What Cheela does do is enforce presence: requiresEndUser guarantees your handler never runs without one, so the check you might forget is the one you no longer have to write.
A complete working example lives in the demo storefront's capability set: every capability that touches an order sets requiresEndUser and resolves the token through the same session table the REST API uses, which is what stops a capability and GET /api/orders disagreeing about who someone is. You can use it at demo-shop.