Agent identity
Identity for AI agents
An agent is not a user and not a shared API key. In theAuth it is an identity with an owner, its own kv_ bearer token, scoped permissions, an optional expiry and an audit row for every decision.
- id
- agt_…
- token
- kv_…shown once, SHA-256 hash stored
- owner
- user-123
- type
- autonomous
- permissions
- mcp:github:* [read]file:prod-data/* [delete] requireApproval
Anatomy
What the token is, and is not
Tokens are kv_ plus 32 random bytes. The plaintext is returned once at creation. Only a SHA-256 hash is stored, so a database dump does not reveal a live token. Rotation issues a new token and invalidates the old one.
Permissions
Wildcard matching, then constraints
A permission is a colon-separated resource pattern plus actions. A * segment matches everything from that position on. Constraints must all pass: maxCallsPerHour, allowedArgPatterns, timeWindow (UTC), ipAllowlist (CIDR) and requireApproval.
| Pattern | Resource | Match |
|---|---|---|
| mcp:github:* | mcp:github:repos | Yes |
| mcp:github:* | mcp:github:repos:comments | Yes, * covers the remaining segments |
| mcp:github:* | mcp:slack:channels | No |
| mcp:github:repos | mcp:github:repos:comments | No, exact patterns must match in length |
| * | any | Yes |
How a delegation chain is checked
Narrow at every hop, capped by depth
An orchestrator hands a subset of its permissions to a sub-agent. Two checks run when the link is created, and more run on every call.
- plannerautonomousmcp:github:* [read, write, comment]delegate(planner, code-reviewer, mcp:github:pulls [read, comment], maxDepth 2)
Subset. The parent must hold every permission it hands over. Wider resources or new actions are rejected.
- code-reviewerdelegated, depth 1mcp:github:pulls [read, comment]delegate(code-reviewer, diff-fetcher, ..., maxDepth 2)
Depth. New depth is the deepest incoming link plus one. It must not exceed this call's maxDepth (default 3). Depth 2 passes.
- diff-fetcherdelegated, depth 2mcp:github:pulls [read]delegate(diff-fetcher, another-agent, ..., maxDepth 2)
Rejected. Depth 3 exceeds the maximum of 2, which prevents unbounded chains.
- another-agentno link created
On every authorize() call
- AStatus. The agent must exist and be active, not revoked or expired.
- BOwn permissions. Pattern, action and constraints are evaluated first.
- CDelegated permissions. If B denies, grants from active, unexpired chains are tried.
- DAudit. The decision is written and its auditId returned.
Sensitive actions
Approval, budgets and audit
Denied until approved
A permission with
requireApproval: trueis denied. Your app catches the denial.Request created
approval.request()persists it with a default TTL of 5 minutes and fires yourwebhookUrloronApprovalNeeded.Human decides
Your UI calls
approval.approve()ordeny(). Delivering the notification is your app's job.Agent retries
The retried action passes the permission check once the request is approved.
- Budget policies cap token cost and call counts per agent, user or tenant. On breach: warn, throttle, block or revoke. Budget docs.
- Audit trail. Each entry records agent, owner, action, resource, parameters, result (allowed, denied, rate_limited), duration and timestamp. Export as JSON or CSV. Audit docs.
- Trust score. Each agent gets a 0 to 100 score from its age, call volume and audit history. See trust scoring. Automatic anomaly detection is not shipped: the
anomaly.detectedevent name is reserved, but nothing emits it yet.
Approval requests are stored by theAuth, but it ships no approval UI and does not deliver the notification. You build that step.
Create and authorize
Create an agent, authorize an action
The same calls run on Postgres, SQLite, MySQL or D1. Go has its own agent model on top of OAuth client credentials, documented in the Go package.
import { createTheAuth } from "@glinr/theauth";
const auth = await createTheAuth({
database: { provider: "postgres", url: process.env.DATABASE_URL },
});
// Create an AI agent with scoped MCP permissions
const agent = await auth.agent.create({
ownerId: "user-123",
name: "github-reader",
type: "autonomous",
permissions: [{ resource: "mcp:github:*", actions: ["read"] }],
});
const result = await auth.authorize(agent.id, {
action: "read",
resource: "mcp:github:repos",
});
// { allowed: true, auditId: "aud_..." }
Compared
Agent identity, shared API keys, human sessions
General patterns, not claims about any vendor. A shared key is often the right call for one service talking to one API you control.
| Question | Shared API key | Human session | theAuth agent |
|---|---|---|---|
| Who is it? | Whoever holds the string | A person at a browser | One named agent with an owner |
| Scope | Whatever the key allows, usually coarse | The user's full rights | Resource and action patterns plus constraints |
| Handing work to a sub-agent | Copy the key | Not designed for it | Narrower delegation with a depth cap |
| Attribution in logs | The key, not the caller | The user, not the agent | Per agent and owner on every decision |
| Stopping one caller | Rotate for everyone | End the session | Revoke or rotate one agent |
| Human sign-off | Build it yourself | The user is already present | requireApproval constraint |
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/theauthgo get github.com/glincker/theauth-go