Blog

An agent needs two credential layers, and they must be mutually exclusive

Kavela
The team building Kavela
2 min read

When you publish an agent for other people to use, every tool it has raises one question: whose account does it act on? There are exactly two correct answers. Either each user connects their own account, or the builder connects once and everyone shares it. A tool that is somehow in both states at once is how one user's data reaches another.

The two layers

User-level. Each person using the agent connects their own credential. Their mail, their messaging account, their calendar. The agent acts as them, against their data, and the blast radius of a mistake stops at their own account. This is the majority case and it should be the default.

Builder-level. The person who built the agent connects once, and every user of that agent shares that connection. This is right when the credential is genuinely the builder's: a company knowledge base, a service account for a shared system, an API key the builder pays for.

Both are legitimate. The bug is not choosing one over the other. The bug is ambiguity.

Why "both" is not a state you can allow

Imagine a tool that has a builder key configured and also accepts a per-user connection. At run time, something has to decide which one to use. That decision lives in code, and it will eventually be wrong, because it is being made implicitly at the moment a request arrives rather than explicitly when the tool was set up.

The failure is quiet. Nothing errors. A user asks the agent to check their inbox, the resolver falls back to the builder key, and the agent cheerfully reads somebody else's mail. Every layer behaves exactly as written. The system is still wrong.

Fallbacks make this worse, not better. "Use the user's connection if present, otherwise the builder's" sounds defensive and is actually the exact bug: the case where a user has not connected yet is precisely the case where you must stop, not substitute someone else's identity.

Make it impossible rather than careful

We enforce exclusivity at the database, not in the code that reads it. A tool row can carry a user-level connection slot or a builder key, and it is not permitted to carry both. A tool in an invalid state cannot be written in the first place, so no resolver ever has to arbitrate.

This is worth the constraint because of where the alternative fails. Enforcing it in application code means every future call site has to remember the rule. That works until someone adds a code path in a hurry, and the resulting bug is a credential leak that no test asserts on, because the code did what it said.

The general principle: when two states must never coexist, encode that where the data lives. Comments and conventions are advice. A constraint is a guarantee.

What this means if you are building an agent for other people

Decide the layer per tool, deliberately, at the moment you add the tool. The question to ask is simple: if this agent had a thousand users, whose account should this action happen on?

If the answer is "theirs", it is user-level, and your setup flow has to ask each user to connect. If the answer is "mine, and that is fine for everyone", it is builder-level. If the answer is "it depends", that is not a third option. It means there are two tools wearing one name, and you should split them.