Status: Draft Published: March 2026 Authors: AgentsID Research Repository: github.com/agentsid/permission-spec Canonical URL: agentsid.dev/spec
This specification defines a standard format for expressing, evaluating, and auditing permission rules for AI agent tool calls. It is designed for use with the Model Context Protocol (MCP) and any agent framework where a language model invokes external tools. The specification covers permission rule format, constraint types, evaluation algorithm, delegation chains, audit log format, agent identity tokens, and extension mechanisms.
The goal is to provide a common language for agent permissions that can be adopted independently of any specific runtime, platform, or vendor.
- Introduction
- Terminology
- Permission Rule Format
- 3.1 Tool Patterns
- 3.2 Actions
- 3.3 Conditions
- 3.4 Priority
- Constraint Types
- 4.1 Schedule
- 4.2 Rate Limit
- 4.3 Data Classification
- 4.4 Budget
- 4.5 Sequence
- 4.6 Session Limit
- 4.7 Risk Score
- 4.8 IP Allowlist
- 4.9 Chain Depth
- 4.10 Cooldown
- 4.11 Anomaly Detection
- 4.12 Approval Gate
- Evaluation Algorithm
- Delegation Protocol
- Audit Log Format
- Agent Identity Format
- Extension Mechanism
- Security Considerations
- Reference Implementation
AI agents operating through the Model Context Protocol have unrestricted access to every tool a server exposes by default. There is no standard mechanism for expressing which tools an agent may call, under what conditions, with what parameter constraints, or subject to what approval requirements.
This creates three classes of risk:
- Over-privilege — agents can call destructive or sensitive tools they should not have access to
- Scope ambiguity — agents infer the broadest valid scope for tool parameters in the absence of constraints
- Auditability gaps — there is no standard for logging what an agent did, when, and under whose authority
This specification defines a portable, vendor-neutral format for addressing all three. A permission policy is a JSON document. An evaluation engine is a pure function. An audit log is a hash-chained append-only ledger. Any runtime can implement this spec without depending on AgentsID infrastructure.
- Deny-first — absent a matching allow rule, all tool calls are denied
- Least privilege — rules grant the minimum necessary access
- Composable — rules combine without conflict via explicit priority
- Portable — the spec is a JSON schema, not a platform dependency
- Auditable — every evaluation decision is logged with cryptographic integrity
This specification is a complement to MCP, not a replacement. MCP defines how tools are discovered and called. This specification defines who may call them, under what conditions, and with what audit trail. It operates as a policy enforcement layer between the agent and the MCP server.
| Term | Definition |
|---|---|
| Agent | An LLM-driven process that invokes tools to complete tasks |
| Principal | A human user or system that authorizes an agent to act on their behalf |
| Tool | A callable function exposed by an MCP server |
| Permission Rule | A JSON object expressing whether a tool call is allowed or denied |
| Policy | An ordered collection of permission rules |
| Constraint | A condition attached to a rule that must be satisfied for the rule to match |
| Delegation | The act of a principal granting a subset of their permissions to an agent or sub-agent |
| Chain | An ordered sequence of delegations from a root principal to a leaf agent |
| Audit Entry | An immutable record of a tool call evaluation decision |
A permission policy is a JSON object with the following top-level structure:
{
"version": "1.0",
"agentId": "agent_abc123",
"issuedAt": "2026-03-29T00:00:00Z",
"expiresAt": "2026-04-29T00:00:00Z",
"rules": [ ...PermissionRule[] ]
}Each rule targets one or more tools using glob patterns:
{
"tools": ["github.*", "filesystem.read_*"],
"action": "allow"
}Pattern matching rules:
| Pattern | Matches |
|---|---|
* |
Any single tool name segment |
** |
Any tool name, including namespaced tools |
github.* |
All tools in the github namespace |
filesystem.read_file |
Exactly one tool |
!filesystem.write_* |
Negation — exclude write tools |
Patterns are evaluated in the order listed. The first matching pattern in the list determines the match. Negation patterns (!) cause the rule to skip this tool if matched.
type Action = "allow" | "deny"allow— the tool call is permitted (subject to constraints)deny— the tool call is rejected immediately, no further rules evaluated
A rule may include a conditions object restricting which parameter values are allowed:
{
"tools": ["filesystem.write_file"],
"action": "allow",
"conditions": {
"path": {
"pattern": "^/home/user/projects/",
"maxLength": 512
},
"content": {
"maxLength": 1048576
}
}
}Condition types:
| Type | Description |
|---|---|
pattern |
Regex the parameter value must match |
enum |
Allowed values (exact match) |
maxLength |
Maximum string length |
minLength |
Minimum string length |
max |
Maximum numeric value |
min |
Minimum numeric value |
notContains |
Strings the value must not contain |
allowedKeys |
For object parameters: permitted keys |
If a condition fails, the rule does not match and evaluation continues to the next rule.
Rules are evaluated in order. The first matching rule determines the outcome. To override a broader rule with a more specific one, place the specific rule first:
{
"rules": [
{
"tools": ["filesystem.write_file"],
"action": "deny",
"conditions": { "path": { "pattern": "^\\.ssh/" } }
},
{
"tools": ["filesystem.*"],
"action": "allow"
}
]
}In this example, write_file to .ssh/ paths is denied while all other filesystem tools are allowed.
Constraints attach runtime conditions to rules beyond parameter validation. They are specified in a constraints array on the rule:
{
"tools": ["github.push_files"],
"action": "allow",
"constraints": [
{ "type": "rateLimit", "max": 10, "windowSeconds": 3600 },
{ "type": "schedule", "daysOfWeek": [1,2,3,4,5], "hoursUTC": [8, 20] }
]
}Restricts tool calls to specific time windows.
{
"type": "schedule",
"daysOfWeek": [1, 2, 3, 4, 5],
"hoursUTC": [8, 17]
}| Field | Type | Description |
|---|---|---|
daysOfWeek |
number[] |
ISO weekday numbers (1=Monday, 7=Sunday) |
hoursUTC |
[number, number] |
UTC hour range [start, end] (exclusive end) |
timezone |
string |
IANA timezone for evaluation (default: UTC) |
Limits how many times a tool can be called within a time window.
{
"type": "rateLimit",
"max": 100,
"windowSeconds": 3600,
"scope": "agent"
}| Field | Type | Description |
|---|---|---|
max |
number |
Maximum calls allowed |
windowSeconds |
number |
Time window duration |
scope |
"agent" | "principal" | "global" |
Counter scope |
Prevents tools from accessing or returning data above a specified sensitivity level.
{
"type": "dataClassification",
"maxLevel": "confidential"
}Levels (ascending sensitivity): public → internal → confidential → restricted → secret
Caps the total cost (in USD, tokens, or custom units) that a tool may consume per window.
{
"type": "budget",
"currency": "usd",
"max": 10.00,
"windowSeconds": 86400
}Requires that specific tools be called (or not called) before this tool is permitted.
{
"type": "sequence",
"requires": ["filesystem.read_file"],
"forbids": ["github.push_files"]
}| Field | Description |
|---|---|
requires |
Tools that must have been called earlier in this session |
forbids |
Tools that must not have been called earlier in this session |
Caps the total number of times a tool may be called per session.
{
"type": "sessionLimit",
"max": 5
}Blocks the call if the computed risk score for the tool invocation exceeds a threshold.
{
"type": "riskScore",
"maxScore": 0.7
}Risk scores are computed by the evaluation engine based on parameter values, context, and historical patterns. The scoring algorithm is defined in the reference implementation.
Restricts tool calls to requests originating from specified IP ranges.
{
"type": "ipAllowlist",
"cidrs": ["10.0.0.0/8", "192.168.1.0/24"]
}Limits how many levels deep in a delegation chain this rule applies.
{
"type": "chainDepth",
"max": 2
}A chain depth of 1 means only the direct delegate of the principal. A chain depth of 2 allows one level of sub-delegation.
Requires a minimum elapsed time between calls to a tool.
{
"type": "cooldown",
"seconds": 300
}Blocks the call if behavioral anomaly detection flags it.
{
"type": "anomalyDetection",
"sensitivity": "medium",
"action": "deny"
}| Sensitivity | Description |
|---|---|
low |
Only flag statistically extreme outliers |
medium |
Flag deviations from established baseline patterns |
high |
Block on any deviation from expected behavior |
Pauses execution and requires a human (or automated system) to approve the call before it proceeds.
{
"type": "approvalGate",
"approvers": ["principal", "admin@example.com"],
"timeoutSeconds": 300,
"timeoutAction": "deny"
}| Field | Description |
|---|---|
approvers |
Identities that may approve. "principal" refers to the issuing principal. |
timeoutSeconds |
How long to wait for approval before applying timeoutAction |
timeoutAction |
"deny" or "allow" — what to do on timeout |
If no rule matches a tool call, the call is denied. An explicit allow rule is required for every tool an agent may call.
Input: tool_name, parameters, context
1. For each rule in rules (in order):
a. Test tool_name against rule.tools patterns
- If no pattern matches: skip rule
- If a negation pattern matches: skip rule
b. Test parameters against rule.conditions
- If any condition fails: skip rule
c. Evaluate rule.constraints
- If any constraint fails: skip rule
d. Return rule.action ("allow" or "deny")
2. If no rule matched: return "deny"
When multiple patterns could match a tool:
- Exact matches take precedence over glob matches
- More specific globs (fewer wildcards) take precedence over less specific globs
- Negation patterns (
!) are evaluated before positive patterns at each specificity level
A deny rule with no conditions short-circuits all subsequent evaluation for the matched tool. This ensures explicit denials cannot be overridden by subsequent allow rules.
[
{ "tools": ["shell.*"], "action": "deny" },
{ "tools": ["**"], "action": "allow" }
]In this example, shell.* tools are always denied even though ** would otherwise allow them.
A delegation is a signed permission policy issued by a principal to an agent, with scope equal to or narrower than the principal's own permissions.
{
"version": "1.0",
"delegationId": "del_xyz789",
"issuedBy": "principal_abc",
"issuedTo": "agent_def456",
"issuedAt": "2026-03-29T00:00:00Z",
"expiresAt": "2026-03-30T00:00:00Z",
"parentDelegationId": null,
"rules": [ ...PermissionRule[] ],
"signature": "hmac-sha256:..."
}A delegation may only grant permissions that the issuing principal holds. An attempt to delegate a tool or permission that the principal does not have is invalid.
Formally: delegate.rules ⊆ principal.rules
Sub-delegations must satisfy: sub_delegate.rules ⊆ delegate.rules
Revoking a delegation automatically revokes all sub-delegations issued from it. The revocation propagates to the full chain.
Revocation is recorded as an audit entry (see Section 7) and enforcement is immediate.
Every evaluation decision produces an immutable audit entry:
{
"entryId": "entry_abc123",
"timestamp": "2026-03-29T12:34:56.789Z",
"agentId": "agent_def456",
"delegationId": "del_xyz789",
"tool": "github.push_files",
"parameters": {
"owner": "myorg",
"repo": "myrepo",
"branch": "main"
},
"decision": "allow",
"matchedRule": 2,
"constraintsEvaluated": ["rateLimit", "schedule"],
"durationMs": 3,
"prevEntryHash": "sha256:e3b0c44298fc1c149afb...",
"entryHash": "sha256:a665a45920422f9d417e..."
}| Field | Description |
|---|---|
entryId |
Unique identifier for this entry |
timestamp |
ISO 8601 with milliseconds |
agentId |
The agent that made the call |
delegationId |
The delegation under which the call was made |
tool |
Fully qualified tool name |
parameters |
Sanitized parameters (secrets redacted) |
decision |
"allow" or "deny" |
matchedRule |
Index of the matching rule, or null if default deny |
constraintsEvaluated |
List of constraint types that were checked |
durationMs |
Evaluation time |
prevEntryHash |
SHA-256 hash of the previous entry |
entryHash |
SHA-256 hash of this entry's canonical JSON |
Each entry's entryHash is computed over the canonical JSON of the entry with entryHash set to null:
entryHash = SHA-256(canonicalize(entry with entryHash=null))
The first entry uses prevEntryHash: "genesis".
Any tampering with a historical entry invalidates all subsequent hashes, making the log append-only by construction.
function verifyChain(entries: AuditEntry[]): boolean {
for (let i = 1; i < entries.length; i++) {
const prev = entries[i - 1]
const curr = entries[i]
if (curr.prevEntryHash !== prev.entryHash) return false
const computed = sha256(canonicalize({ ...curr, entryHash: null }))
if (computed !== curr.entryHash) return false
}
return true
}Agent IDs are prefixed strings with a random component:
agent_[a-zA-Z0-9]{16}
Example: agent_dK9mPqR2xL4wNv8j
Agent identity tokens are HMAC-SHA256 signed payloads:
{
"agentId": "agent_dK9mPqR2xL4wNv8j",
"principalId": "principal_abc123",
"issuedAt": 1743206400,
"expiresAt": 1743292800,
"scope": ["github.*", "filesystem.read_*"],
"delegationId": "del_xyz789"
}Encoded as: base64url(header).base64url(payload).hmac-sha256-signature
The signing key is the project's secret key (never transmitted). Verification requires only the key and the token.
| State | Description |
|---|---|
active |
Valid, within expiry, not revoked |
expired |
Past expiresAt |
revoked |
Explicitly invalidated before expiry |
suspended |
Temporarily blocked (anomaly detection) |
Tokens move from active to expired automatically. Revocation is explicit and irreversible. Suspension is reversible.
Implementors may define custom constraint types using the x- prefix:
{
"type": "x-geofence",
"allowedCountries": ["US", "CA", "GB"],
"denyAction": "deny"
}Custom constraints must:
- Use the
x-prefix - Be documented in the policy's
extensionsfield - Fail closed (deny) if the evaluator does not recognize the type
{
"version": "1.0",
"extensions": {
"x-geofence": {
"spec": "https://example.com/specs/geofence-constraint-v1",
"failBehavior": "deny"
}
},
"rules": [ ... ]
}Permission policies contain authorization decisions and must be stored with appropriate access controls. Policies should not be stored in plaintext in version control unless the repository is private and access-controlled.
Parameters that match common secret patterns (tokens, passwords, keys) must be redacted before writing to the audit log. Redaction uses [REDACTED] as the replacement value. Pattern matching for redaction follows OWASP secret detection guidelines.
Schedule and expiry constraints depend on accurate system time. Implementations should reject tokens or evaluate schedule constraints with a maximum clock skew tolerance of 60 seconds.
Delegation signatures must be verified before processing. An unverified delegation must be treated as if it does not exist (default deny). The signing algorithm (HMAC-SHA256) requires both parties to share the secret key — delegations cannot be forged by agents.
This specification does not prevent prompt injection at the LLM layer. However, by enforcing deny-first evaluation at the tool call layer, it limits the impact of a successful injection: even if an agent is manipulated into calling a tool, the policy engine will deny the call if no matching allow rule exists.
AgentsID provides a production reference implementation of this specification:
| Component | Package | Description |
|---|---|---|
| Policy evaluator | @agentsid/sdk |
Core evaluation engine (TypeScript) |
| MCP middleware | @agentsid/guard |
Drop-in MCP server with 50 tools |
| Audit log | @agentsid/sdk |
Hash-chained ledger with verification |
| Agent identity | @agentsid/sdk |
Token issuance and verification |
| Scanner | @agentsid/scanner |
Static analysis of MCP tool definitions |
Quick start:
npm install @agentsid/sdkimport { AgentsID } from "@agentsid/sdk"
const client = new AgentsID({ apiKey: process.env.AGENTSID_API_KEY })
const result = await client.validate({
agentId: "agent_abc123",
tool: "github.push_files",
parameters: { owner: "myorg", repo: "myrepo", branch: "main" }
})
if (!result.allowed) {
throw new Error(`Tool call denied: ${result.reason}`)
}Full documentation: agentsid.dev/docs
| Version | Date | Changes |
|---|---|---|
| 1.0 | 2026-03-29 | Initial draft |
This specification is published under the Creative Commons Attribution 4.0 International License. Implementations are encouraged without restriction.