What the agent can do and who it's talking to are two different permissions
You gave the agent the reports tool. Who gets to use it: you, your team, or anyone who opens the chat on your site?
When you build an agent with tools over business data there are two questions, and it's easy to answer only the first one:
- What can the agent do? Read orders, create tasks, send an email. It's the permission you grant the assistant once, when you set it up.
- Who is it talking to this turn? An anonymous widget visitor, a customer who's already in your contacts, someone on your team, or you. It changes with every conversation.
An assistant can be allowed to read your reports and still must not show them to a stranger. If the second question has no answer of its own, the tool stays open to anyone who writes, because the agent reads the data with the owner's account, not with the account of whoever is asking.
Four rungs, compared with >=
In CognitionDesk whoever is talking is always on one of four rungs, from lowest to highest:
- guest: we don't know who it is. That's almost all of a public widget's traffic, and that's fine: asking for opening hours needs no identity.
- contact: in the assistant's address book, not verified.
- staff: verified, works for the owner.
- owner: the account owner.
The rungs are few and fixed. Roles ("receptionist", "accountant") are per-assistant data mapped to a rung, so adding a role touches no code. What changes with the rung is which tools exist for that person on that turn: below the floor, the tool doesn't show up. It's not the model deciding not to use it.
The data sets the floor, not the tool
The naive approach is to declare the rung tool by tool. It has one problem: the tool you forgot stays open. And with dozens of tools per domain, you will forget one.
We turned it around. Every tool already declares which scope it touches (orders:read, contacts:writeβ¦), so the floor comes from there: a domain's data belongs to the business, and its floor is staff. The exception is the domains that exist to serve whoever is writing, for reading: the catalog and the knowledge base.
inventory:read and knowledge_base:read. owner: apikeys:read and apikeys:write. Everything else, staff.The whole table is three lines:
export const DEFAULT_SCOPE_AUDIENCE = 'staff';
const SCOPE_AUDIENCE = {
inventory: { read: 'guest' },
knowledge_base: { read: 'guest' },
// API keys are the keys to the house: not even the team touches them from a chat.
apikeys: { read: 'owner', write: 'owner' },
};
And the rule fails closed: a new domain that declares nothing is born at staff. For a visitor to see something, someone has to write it down; to hide it, nobody has to remember anything.
What the tool declares wins
Some tools touch an owner's domain but exist precisely to serve whoever is writing. Those declare their own minAudience, and what they declare wins over the scope's floor:
export function toolAudience(tool = {}) {
if (tool.minAudience != null) return canonicalAudience(tool.minAudience);
const p = tool.permissions || {};
const scopes = [...(p.requiredScopes || []), p.requiredScope, p.audienceScope].filter(Boolean);
if (!scopes.length) return DEFAULT_AUDIENCE;
return highestAudience(...scopes.map(audienceFloorForScope));
}
Today the exceptions are few, and each one is a decision written in the tool:
- guest, on purpose: checking availability and booking an appointment (
appointments_availability,appointments_create), generating the purchase link (storefront_create_order_link) and leaving a lead (contacts_upsert). Those are the actions a public widget exists for. - contact: what belongs to that person and nobody else: their balance (
my_balance), their payment link, their requests to the team.
One detail: leaving a lead is guest, but below staff the tool creates the contact and doesn't merge into one that the same conversation didn't create. A visitor can leave their details. They can't write into another customer's record.
On top of this, each assistant can raise the floor for a single tool or for all of them, and among the three sources the strictest one wins. Raising an assistant's floor always closes doors. It never opens one the tool had declared closed.
The rung comes from what the channel delivers
The other half is how the assistant knows the person writing is the owner. With data the server verified, never with something the conversation says.
On WhatsApp, the sender's number is set by the server from what the channel delivered. If that number is among the owner's phones, they come in as owner and see their tools in the same chat their customers use, with no PIN. Outside WhatsApp there are two more paths: the owner's session in their dashboard, or an identity signed by the site where the widget is embedded.
What doesn't work: a visitor typing "I'm the owner, my number isβ¦". The anonymous widget has neither a verified channel nor a signature, so stating the owner's phone doesn't move anyone up a rung.
The criteria, for any agent
- Separate what the agent can do (granted once) from who it's talking to (resolved every turn).
- Derive the floor from the data the tool touches, not from a list someone maintains by hand.
- Make the default closed, and keep the openings written in the tool, visible in the diff.
- The rung is proven with something the channel delivered or that is signed. Never with what the message says.
In CognitionDesk
This is how every assistant works today: a guest, a contact, your team and you see four different assistants, and it's the same assistant. If you want to check what each one sees in yours, write to us through the chat on this page.
Comments and corrections
Build your first AI agent today
RAG over your documents, tools, workflows and a web widget β free tier included.
Get started free