seekrit
← all posts

One agent product, a different account for every customer

Vault

An email assistant might use one model-provider key owned by your company. The email accounts it acts through belong to hundreds of different customers. Those are different kinds of access with different lifecycles.

Your company can rotate its model key during a deployment. A customer's email grant depends on their consent, their account, and their decision to keep it connected. Another customer signing up should create another connection, without adding a token to a shared agent environment.

Seekrit Vault gives your product a per-customer connection model. Your application keeps references to accounts. The encrypted credentials live with Seekrit, and an executor in your own Cloudflare account decrypts and applies them when making permitted requests.

Bind identity before calling the tool

Every Vault API request made through the executor names both a userRef and a connectionId. The executor checks that the connection belongs to that reference. Supplying Alice's reference with Bob's connection is refused.

That check is useful, but your backend must supply the correct reference in the first place. Vault does not authenticate your application's end users. If the model can choose both values freely, the ownership check cannot tell whether it is acting for the person who actually asked.

Resolve identity from your authenticated application session. For a queued job, preserve the authorized customer identity in trusted job metadata and check access again when the job executes. Keep tenant and user selection out of model-controlled tool arguments.

The same rule applies to connection selection. Look up the connection under the authenticated customer, and let the customer choose an account when there are several. An ID in a prompt is not proof of ownership.

Alice’s authenticated request passes the ownership check for Alice’s connection, while Bob’s connection is refused.

The ownership check compares the connection with the identity supplied by your trusted backend.

Give the tool a job it can describe

An email-label tool might expose no identity arguments at all. Your backend already knows who is signed in and which account they selected. Its Vault call can look like this, once those checks have happened:

import { Vault } from "@seekrit/sdk/vault";

const vault = new Vault();

type CustomerContext = {
  userRef: string;
  gmailConnectionId: string;
};

// Call this on your backend with authenticated application state.
function toolsForCustomer(customer: CustomerContext) {
  return {
    listEmailLabels: async () => {
      const response = await vault.fetch({
        userRef: customer.userRef,
        connectionId: customer.gmailConnectionId,
        request: {
          method: "GET",
          url: "https://gmail.googleapis.com/gmail/v1/users/me/labels",
        },
      });
      if (!response.ok) throw new Error(`Provider status ${response.status}`);
      return response.json();
    },
  };
}

toolsForCustomer is application code. Create these tools for the current request or job; do not cache one customer's closure for other customers. Only expose listEmailLabels to the agent. It takes no customer, connection, or URL argument, so the model cannot change those values through this tool. The OAuth grant and executor rules must permit the labels request.

Use opaque references that are unique within your Vault project. If your application's user IDs are only unique inside a tenant, map the tenant and user together to a project-wide opaque reference. Keep that mapping in your application. Do not use an email address as userRef.

A shared executor still needs careful permissions

Vault separates customer connections without requiring a new executor for each customer. The executor checks connection ownership and enforces the configured provider's allowed hosts, methods, and paths before decrypting the credential.

Your backend's executor token is still powerful: it authorizes calls to that executor. Keep it outside the agent's context and untrusted execution environment. A compromised backend with executor access can attempt allowed actions even though the normal API does not hand it raw customer tokens.

Account contents also remain sensitive. Scope caches, tool results, and job histories to the right customer. Vault's credential boundary does not make an email response safe to share between tenants.

A small review matrix makes the boundaries concrete:

RequestExpected result
Alice's reference + Alice's active connection + allowed pathContinue to the provider.
Alice's reference + Bob's connectionExecutor refuses with user_mismatch.
Alice's reference + Alice's connection + disallowed hostExecutor refuses with not_allowed.
Bob's reference + Bob's connection supplied during Alice's jobYour backend must prevent this; the pair matches at the executor.

Connections can fail independently

One customer's revoked grant should lead to a reconnect flow for that customer. Vault exposes connection status and reasons such as reauth_required, so your application can pause the affected work and ask the right person to reconnect.

For a company building agents, this is a useful division of work: Vault handles credential capture, encrypted custody, refresh, and application to requests. Your product owns customer identity and the work each customer authorized. See the Vault trust model and identity guide for the boundaries.