Use case

Identity and permissions for AI agents

An agent that shares its owner's API key cannot be limited, traced, or switched off on its own. theAuth gives each agent its own token, its own permissions, a named human owner, and a record of every decision.

The problem

Agents act for people, and your logs only know the person.

Once an orchestrator starts sub-agents, calls tools over MCP, and runs without anyone watching, the usual session model stops answering the questions that matter. Which agent made that call? Who is responsible for it? Could it have done less? Can I stop just that one?

Reusing the owner's credentials answers none of those. You need identity at the agent level, with the human owner attached, and a policy check on every action rather than once at login.

The theAuth path

Five controls on one agent record.

Agent identityauth.agent.create
Each agent gets a cryptographic bearer token with the kv_ prefix and a type of autonomous, delegated or service. The ownerId field ties it to the responsible user.
Permissionsresource + actions
Permissions are { resource, actions } pairs with wildcard matching, for example mcp:github:* with read. auth.authorize() returns allowed and an auditId.
DelegationmaxDepth
auth.delegate() hands a subset of an agent's permissions to another with an expiry and a maxDepth (default 3). Anything wider than the parent holds is rejected. Revoking a chain cascades down.
Approval flowsrequireApproval
A permission with constraints: { requireApproval: true } denies the call with a reason you can catch. You create an approval request, notify the owner, and approve or deny later.
Budgets and auditpolicies, audit
Budget policies cap token cost and call counts per agent, user or tenant. Every authorization decision, allowed or denied, is written to the audit log and can be filtered by agent, user and result.
A human owner creates an agent, and the agent can hand a subset of its permissions to a sub-agent. Each hop adds one to the delegation depth, which is capped by maxDepth (default 3), and revoking a link also revokes every link created downstream of it.

Step by step

One orchestrator, one scoped sub-agent.

Excerpts that build on each other. auth is the instance from createTheAuth.

  1. Create the agent with a human owner

    ownerId is required. It is what an auditor follows from an action back to a person.

    The sub-agent starts with no permissions of its own. It will only get what the orchestrator hands over.

    agents.ts
    const orchestrator = await auth.agent.create({
      ownerId: "user-123",
      name: "planner",
      type: "autonomous",
      permissions: [
        { resource: "mcp:github:*", actions: ["read", "write", "comment"] },
      ],
    });
    
    const reviewer = await auth.agent.create({
      ownerId: "user-123",
      name: "code-reviewer",
      type: "delegated",
      permissions: [],
    });
  2. Check every action

    Call authorize before the tool runs, not after. The decision is logged either way, and auditId links your own logs to it.

    authorize.ts
    const result = await auth.authorize(orchestrator.id, {
      action: "read",
      resource: "mcp:github:repos",
    });
    // { allowed: true, auditId: "aud_..." }
  3. Delegate narrowly, with a depth limit

    The delegated permissions must be a subset of what the parent holds: narrower resources and fewer actions pass, wider ones fail with INSUFFICIENT_PERMISSIONS.

    With maxDepth: 1 the reviewer cannot pass its access to anyone else. Revoking the chain later removes it and everything downstream.

    delegate.ts
    const chain = await auth.delegate({
      fromAgent: orchestrator.id,
      toAgent: reviewer.id,
      permissions: [{ resource: "mcp:github:pulls", actions: ["read", "comment"] }],
      expiresAt: new Date(Date.now() + 30 * 60_000), // 30 minutes
      maxDepth: 1,
    });
    
    // Later: cascades to every chain created downstream.
    await auth.delegation.revoke(chain.id);
  4. Put a person in front of sensitive actions

    Mark the permission, catch the denial, and create an approval request. theAuth stores the request and fires your webhookUrl or onApprovalNeeded handler. Delivering the notification is your application's job.

    Requests expire after a TTL, five minutes by default. The agent does not block while it waits.

    approval.ts
    const filer = await auth.agent.create({
      ownerId: "user-123",
      name: "file-manager",
      type: "autonomous",
      permissions: [
        { resource: "file:prod-data/*", actions: ["read"] },
        {
          resource: "file:prod-data/*",
          actions: ["delete"],
          constraints: { requireApproval: true },
        },
      ],
    });
    
    const attempt = await auth.authorize(filer.id, {
      action: "delete",
      resource: "file:prod-data/dataset.csv",
    });
    
    if (!attempt.allowed && attempt.reason?.includes("requires human approval")) {
      const req = await auth.approval.request({
        agentId: filer.id,
        userId: filer.ownerId,
        action: "delete",
        resource: "file:prod-data/dataset.csv",
      });
      // later, from your UI handler:
      await auth.approval.approve(req.id, "reviewer@example.com");
    }
  5. Cap spend

    A budget policy sets limits and an action when they are hit: warn, throttle, block or revoke. Policies live on auth.policies.

    Evaluate a policy with checkBudget before you spend. We could not confirm that authorize() calls it for you, so call it yourself at the point where your agent makes an LLM request.

    budget.ts
    await auth.policies.create({
      agentId: orchestrator.id,
      limits: { maxTokensCostPerDay: 1000, maxCallsPerDay: 500 },
      action: "block",
    });
    
    const budget = await auth.policies.checkBudget(orchestrator.id);
    if (!budget.allowed) throw new Error(budget.reason);
  6. Answer the audit question

    Filter by agent, owner, outcome and time. Denied calls are the interesting ones. Exports come out as JSON or CSV.

    audit.ts
    const denied = await auth.audit.query({
      userId: "user-123",
      result: "denied",
      since: new Date("2026-10-01"),
      limit: 100,
    });
    
    const csv = await auth.audit.export({ format: "csv" });

Checked against the package source for @glinr/theauth 0.5.0 on 2026-10-06. Compliance mappings of the audit log to the EU AI Act, NIST, SOC 2 and ISO 42001 are reports, not certifications; see the compliance docs for what they cover.

Pitfalls

Mistakes that leave agents over-trusted.

  • Giving the orchestrator's token to sub-agents.That erases the boundary. Create each sub-agent and delegate a subset instead.
  • Delegating with the default depth.maxDepth defaults to 3. If a sub-agent has no reason to delegate further, set maxDepth: 1.
  • Long-lived delegations.Delegations take an expiresAt. Give them the length of the task, not of the project.
  • Revocation does not stop work in flight.It takes effect on the next authorize() call. Authorize before each action rather than once per session.
  • Expecting approval notifications to arrive by themselves.theAuth persists the request and calls your webhook or handler. Sending the Slack message or email is yours to build. Poll listPending() as a fallback if a webhook fails.
  • Assuming budgets are enforced inside authorize.Call auth.policies.checkBudget() yourself before LLM calls until you have confirmed otherwise in your version.
  • Pending approvals that never expire.Expired requests stay marked pending until auth.approval.cleanup() runs. Schedule it.

Get started

Give your first agent an identity.

Install the package, create an agent with scoped permissions, and read its first audit record. The core runs on Postgres, SQLite, MySQL or D1, and the Go module needs a single go get.

  • npm install @glinr/theauth
  • go get github.com/glincker/theauth-go
Or skip hosting with theAuth Cloud