Agent authority is the set of rules that determines which external changes an agent may cause. A tool being available does not mean every call is authorized. A person approving a call does not prove that the action succeeded. Safe agent applications keep those decisions separate.
The Letta Agent SDK permission system controls tool exposure and invocation. The host application still owns business rules, retry behavior, and proof of external effects.
Five separate boundaries
Treat each layer as a different question:
| Boundary | Question | Typical mechanism |
|---|---|---|
| Tool availability | Can the agent see this capability? | allowedTools, registered client tools, MCP server selection |
| Invocation approval | May this proposed call run now? | permissionMode, canUseTool, edited input, deny or interrupt |
| Business authority | May this actor perform this action on this target? | Application identity, tenant scope, roles, limits, policy checks |
| Effect identity | Is this a new operation or a retry? | Operation ID, idempotency key, compare-and-swap condition |
| Receipt | What did the external system record? | Provider ID, version, commit hash, readback, timestamp |
Collapsing the layers creates predictable failures. An allowlisted publish_report tool can still target the wrong workspace. A user can approve a request whose input changed after the preview. A successful tool return can describe a planned write rather than the record stored by the provider.
Tool exposure is the first limit
Client tools and Model Context Protocol (MCP) tools are supplied by the application when it creates or resumes a session. Their implementations and credentials remain in the SDK host process. The agent receives their schemas and results.
Use allowedTools to expose only the tools needed for that session:
await using session = client.resumeSession(conversationId, {
tools: [lookupCustomer, publishReport],
allowedTools: ["lookup_customer", "publish_report"],
});This is a capability filter, not a business policy. It prevents the agent from calling tools outside the list. It does not decide which customer the current user may read or which workspace may receive a report.
Approval governs a proposed call
permissionMode determines which calls require review. canUseTool can allow, deny, or edit the input of a specific tool call. The callback can wait for a person or another application service before returning.
An approval interface should show the exact target and consequential parameters. “Allow publish_report?” is too weak. Show the workspace, title, visibility, overwrite behavior, and content digest. If the approved input changes, request approval again.
Pending approvals can survive a disconnected SDK client. A new session can inspect pendingControlRequests and call recoverPendingApprovals(). Recovery resubmits the request to the new session's callback; it does not silently approve the action.
Business rules belong inside the tool
The tool handler receives untrusted model output. Validate it even after approval. A write-capable handler should derive the current actor from authenticated application state rather than accept an actor or tenant identifier chosen by the model.
The following sketch shows the order:
async function publishReport(input: PublishInput, actor: Actor) {
const request = PublishInputSchema.parse(input);
await policy.require(actor, "report.publish", request.workspaceId);
const operationId = createOperationId({
actorId: actor.id,
workspaceId: request.workspaceId,
contentDigest: sha256(request.markdown),
});
const previous = await reports.findByOperationId(operationId);
if (previous) return previous.receipt;
const created = await provider.createReport({
...request,
idempotencyKey: operationId,
});
const observed = await provider.getReport(created.id);
return {
operationId,
providerId: observed.id,
version: observed.version,
contentDigest: sha256(observed.markdown),
};
}The policy check binds the action to the authenticated actor. The operation ID distinguishes a retry from a new request. Provider readback verifies the stored record rather than trusting the create response alone.
Treat timeouts as unknown outcomes
If a connection fails after send() or during a tool call, the action may already have reached the runtime or provider. Repeating it immediately can create two issues, two charges, or two messages.
Recoverable Agent Execution records intent before a consequential action and stores a receipt after it. The difficult interval lies between those records. Recovery code must query by operation ID, compare the target's current version, or use a provider idempotency key. A timeout means the outcome is unknown until reconciled.
The same principle applies to the SDK stream. Missed events are not replayed after reconnect. Read conversation history or a consolidated state snapshot before deciding whether to resend.
Return receipts as tool results
A useful write tool returns identifiers that the application and agent can inspect later:
- the application operation ID;
- the external provider's object or message ID;
- the resulting version or commit;
- a digest of the observed contents;
- the provider timestamp;
- whether the result came from a new write or retry reconciliation.
The agent's prose can explain the result, but it should not be the only evidence. Agent Trajectory Observability connects the selected context, model run, tool call, and receipt so a later reviewer can reconstruct what happened.
A practical default
Begin with read-only tools. Add one write tool whose target and parameters are explicit. Run it in strict or standard mode, enforce domain policy inside the handler, assign every operation an identity, and read the result back from the provider.
Only relax approval when observed calls show a narrow class of effects with stable inputs, testable policy, and reliable reconciliation. Faster approval is useful. Ambiguous authority with better latency is still ambiguous authority.