# Record
**Author:** @cameron.stream (`did:plc:gfrmhdmjvxn2sjedzboeudef`)

## `knowledge-agent-authority-and-effects`
**Collection:** `site.standard.document`
**AT URI:** `at://did:plc:gfrmhdmjvxn2sjedzboeudef/site.standard.document/knowledge-agent-authority-and-effects`

**Title:** Agent Authority and Effects
**Published:** Wed, 12 Aug 2026 19:50:08 GMT
**Updated:** Wed, 12 Aug 2026 19:35:00 GMT
**Description:** A design guide to tool availability, invocation approval, business authority, idempotency, and proof that an external action occurred.
**Publication:** `at://did:plc:gfrmhdmjvxn2sjedzboeudef/site.standard.publication/3mr4py6clps2f`
**Path:** /agent-authority-and-effects
**Tags:** knowledge, practice, ai, agents, letta, agent-sdk, permissions, authorization, reliability

**Content:**
````json
{
  "text": "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.\n\nThe [Letta Agent SDK permission system](https://docs.letta.com/agent-sdk/permissions/index.md) controls tool exposure and invocation. The host application still owns business rules, retry behavior, and proof of external effects.\n\n## Five separate boundaries\n\nTreat each layer as a different question:\n\n| Boundary | Question | Typical mechanism |\n| --- | --- | --- |\n| Tool availability | Can the agent see this capability? | `allowedTools`, registered client tools, MCP server selection |\n| Invocation approval | May this proposed call run now? | `permissionMode`, `canUseTool`, edited input, deny or interrupt |\n| Business authority | May this actor perform this action on this target? | Application identity, tenant scope, roles, limits, policy checks |\n| Effect identity | Is this a new operation or a retry? | Operation ID, idempotency key, compare-and-swap condition |\n| Receipt | What did the external system record? | Provider ID, version, commit hash, readback, timestamp |\n\nCollapsing 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.\n\n## Tool exposure is the first limit\n\nClient 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.\n\nUse `allowedTools` to expose only the tools needed for that session:\n\n```typescript\nawait using session = client.resumeSession(conversationId, {\n  tools: [lookupCustomer, publishReport],\n  allowedTools: [\"lookup_customer\", \"publish_report\"],\n});\n```\n\nThis 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.\n\n## Approval governs a proposed call\n\n`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.\n\nAn 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.\n\nPending 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.\n\n## Business rules belong inside the tool\n\nThe 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.\n\nThe following sketch shows the order:\n\n```typescript\nasync function publishReport(input: PublishInput, actor: Actor) {\n  const request = PublishInputSchema.parse(input);\n\n  await policy.require(actor, \"report.publish\", request.workspaceId);\n\n  const operationId = createOperationId({\n    actorId: actor.id,\n    workspaceId: request.workspaceId,\n    contentDigest: sha256(request.markdown),\n  });\n\n  const previous = await reports.findByOperationId(operationId);\n  if (previous) return previous.receipt;\n\n  const created = await provider.createReport({\n    ...request,\n    idempotencyKey: operationId,\n  });\n\n  const observed = await provider.getReport(created.id);\n  return {\n    operationId,\n    providerId: observed.id,\n    version: observed.version,\n    contentDigest: sha256(observed.markdown),\n  };\n}\n```\n\nThe 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.\n\n## Treat timeouts as unknown outcomes\n\nIf 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.\n\n[Recoverable Agent Execution](https://cameron.stream/knowledge/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.\n\nThe 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.\n\n## Return receipts as tool results\n\nA useful write tool returns identifiers that the application and agent can inspect later:\n\n- the application operation ID;\n- the external provider's object or message ID;\n- the resulting version or commit;\n- a digest of the observed contents;\n- the provider timestamp;\n- whether the result came from a new write or retry reconciliation.\n\nThe agent's prose can explain the result, but it should not be the only evidence. [Agent Trajectory Observability](https://cameron.stream/knowledge/agent-trajectory-observability) connects the selected context, model run, tool call, and receipt so a later reviewer can reconstruct what happened.\n\n## A practical default\n\nBegin 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.\n\nOnly 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.\n\n## Sources\n\n- [Letta Agent SDK permissions](<https://docs.letta.com/agent-sdk/permissions/index.md>)\n- [MCP and client tools](<https://docs.letta.com/agent-sdk/mcp/index.md>)\n- [Letta Agent SDK session lifecycle](<https://docs.letta.com/agent-sdk/sessions/index.md>)\n- [Sending messages with the Letta Agent SDK](<https://docs.letta.com/agent-sdk/messages/index.md>)",
  "$type": "site.standard.content.markdown",
  "version": "1.0"
}
````

---
*Fetched from https://enoki.us-east.host.bsky.network via `com.atproto.repo.getRecord`*