Skip to content

Capability Tokens

Capability-based authorization is the foundation of Adaptive Sentience's security model. Instead of asking "who is this user?", capabilities ask "what is this request allowed to do?"


Overview

Problem: Traditional identity-based access control (RBAC, ACL) creates tight coupling between users and permissions, making it difficult to delegate limited authority, operate offline, or implement zero-trust architectures.

Solution: Capability tokens grant specific permissions to perform operations without requiring centralized authentication. The token itself is the authority.


Core Principles

1. Capabilities Are Permissions

A capability token explicitly states what it can do:

{
  "capabilities": [
    "tool:pii_redact",
    "tool:summarize"
  ]
}

This token grants permission to: - Execute the pii_redact tool - Execute the summarize tool

Nothing more, nothing less.

2. No Ambient Authority

Unlike cookies or session tokens, capability tokens carry their own authority. You don't check "who sent this?" - you check "does this token grant the required capability?"

Traditional approach:

Request → Who are you? → What role? → Does role have permission? → Execute

Capability approach:

Request → Does token have capability? → Execute

3. Principle of Least Privilege

Tokens should grant the minimum capabilities needed:

# Good - specific capabilities
token = {
    "capabilities": ["tool:pii_redact"]
}

# Bad - overly broad
token = {
    "capabilities": ["admin:*"]
}

4. Delegation and Attenuation

Capability tokens can be delegated with reduced permissions:

# Original token
original = {
    "capabilities": ["tool:pii_redact", "tool:summarize", "resource:prod_db"]
}

# Delegated token - subset of capabilities
delegated = {
    "capabilities": ["tool:pii_redact"],  # Removed summarize and prod_db
    "delegated_from": "token:abc123",
    "expires_at": "2024-12-31T23:59:59Z"  # Added expiration
}

Token Structure

Basic Token

{
  "capabilities": [
    "tool:pii_redact",
    "tool:summarize"
  ]
}

Full Token with Metadata

{
  "token_id": "token:a1b2c3d4e5f6",
  "capabilities": [
    "tool:pii_redact",
    "resource:customer_db"
  ],
  "issued_at": "2024-01-15T10:00:00Z",
  "expires_at": "2024-12-31T23:59:59Z",
  "issuer": "gateway:abc123",
  "delegated_from": null,
  "metadata": {
    "purpose": "PII redaction for customer support",
    "department": "support"
  }
}

Token Fields

Field Required Description
capabilities Yes Array of capability strings
token_id No Unique identifier for audit trails
issued_at No ISO 8601 timestamp of issuance
expires_at No ISO 8601 timestamp of expiration
issuer No Node ID that issued the token
delegated_from No Parent token ID (for delegation)
metadata No Application-specific metadata

Capability Naming

Naming Conventions

Capabilities follow a hierarchical naming scheme:

<category>:<resource>[:<action>]

Examples:

tool:pii_redact              # Execute pii_redact tool
tool:summarize:v2            # Execute summarize tool (v2 only)
resource:customer_db         # Access customer database
resource:prod_db:read        # Read-only access to production DB
action:approve_payment       # Approve payment workflow
admin:user_management        # Administer user accounts
admin:*                      # All admin capabilities (use sparingly)

Common Categories

Category Description Examples
tool:* Tool execution tool:pii_redact, tool:summarize
resource:* Resource access resource:prod_db, resource:s3_bucket
action:* Privileged actions action:approve_payment, action:deploy
admin:* Administrative admin:user_mgmt, admin:node_mgmt
read:* Read-only access read:customer_data
write:* Write access write:audit_log

Hierarchical Capabilities

Use colons to create capability hierarchies:

# Database access hierarchy
"resource:db"                    # Access any database
"resource:db:customer"           # Access customer database
"resource:db:customer:read"      # Read-only customer database

Validation can check for hierarchical matches:

def has_capability(token, required):
    # Exact match
    if required in token["capabilities"]:
        return True

    # Wildcard match (resource:db:* matches resource:db:customer:read)
    for cap in token["capabilities"]:
        if cap.endswith(":*") and required.startswith(cap[:-2]):
            return True

    return False

Token Validation

Edge Node Validation

Edge nodes validate tokens before executing tools:

def validate_token(token, required_capability):
    """Validate capability token.

    Returns:
        (authorized, error_message)
    """
    # Check token structure
    if not isinstance(token, dict):
        return False, "Invalid token format"

    capabilities = token.get("capabilities", [])
    if not isinstance(capabilities, list):
        return False, "Capabilities must be an array"

    # Check required capability
    if required_capability not in capabilities:
        return False, f"Missing required capability: {required_capability}"

    # Check expiration
    expires_at = token.get("expires_at")
    if expires_at:
        try:
            expires = datetime.fromisoformat(expires_at.replace("Z", "+00:00"))
            if datetime.utcnow().replace(tzinfo=expires.tzinfo) > expires:
                return False, "Token expired"
        except (ValueError, AttributeError) as e:
            return False, f"Invalid expires_at format: {e}"

    return True, None

Validation Flow

1. Check token structure
2. Extract capabilities array
3. Verify required capability present
4. Check expiration (if present)
5. Return authorized=true or error

Validation Errors

Common validation errors:

// Missing capability
{
  "ok": false,
  "error": "Missing required capability: tool:pii_redact"
}

// Token expired
{
  "ok": false,
  "error": "Token expired"
}

// Invalid format
{
  "ok": false,
  "error": "Invalid token format"
}

Token Expiration

Setting Expiration

{
  "capabilities": ["tool:pii_redact"],
  "expires_at": "2024-12-31T23:59:59Z"
}

Expiration Checking

from datetime import datetime

def is_expired(token):
    expires_at = token.get("expires_at")
    if not expires_at:
        return False  # No expiration = never expires

    expires = datetime.fromisoformat(expires_at.replace("Z", "+00:00"))
    now = datetime.utcnow().replace(tzinfo=expires.tzinfo)
    return now > expires

Best Practices for Expiration

  1. Short-lived tokens for sensitive operations:

    {
      "capabilities": ["action:approve_payment"],
      "expires_at": "2024-01-15T12:00:00Z"  // 1 hour validity
    }
    

  2. Long-lived tokens for automated systems:

    {
      "capabilities": ["tool:pii_redact"],
      "expires_at": "2025-01-15T00:00:00Z"  // 1 year validity
    }
    

  3. No expiration for development:

    {
      "capabilities": ["tool:pii_redact"]
      // No expires_at = never expires
    }
    


Token Generation

Development Tokens

For development and testing, use simple self-asserted tokens:

# Simple dev token
dev_token = {
    "capabilities": ["tool:pii_redact", "tool:summarize"]
}

# Token with expiration
from datetime import datetime, timedelta

token = {
    "capabilities": ["tool:pii_redact"],
    "issued_at": datetime.utcnow().isoformat() + "Z",
    "expires_at": (datetime.utcnow() + timedelta(hours=1)).isoformat() + "Z"
}

Production Token Issuance

In production, tokens should be issued by a trusted authority:

import uuid
from datetime import datetime, timedelta

class TokenIssuer:
    """Issue capability tokens with tracking."""

    def __init__(self, issuer_id):
        self.issuer_id = issuer_id

    def issue_token(self, capabilities, ttl_hours=24):
        """Issue a new capability token.

        Args:
            capabilities: List of capability strings
            ttl_hours: Time-to-live in hours

        Returns:
            Token dict
        """
        now = datetime.utcnow()
        token_id = f"token:{uuid.uuid4().hex[:12]}"

        token = {
            "token_id": token_id,
            "capabilities": capabilities,
            "issued_at": now.isoformat() + "Z",
            "expires_at": (now + timedelta(hours=ttl_hours)).isoformat() + "Z",
            "issuer": self.issuer_id
        }

        # Log token issuance for audit trail
        self._log_issuance(token)

        return token

    def _log_issuance(self, token):
        """Log token issuance to audit log."""
        print(f"[TOKEN] Issued {token['token_id']} with capabilities: {token['capabilities']}")

Usage:

issuer = TokenIssuer(issuer_id="gateway:abc123")

token = issuer.issue_token(
    capabilities=["tool:pii_redact"],
    ttl_hours=1
)

Token Delegation

Creating Delegated Tokens

Delegation allows creating tokens with reduced permissions:

def delegate_token(original_token, new_capabilities, ttl_hours=1):
    """Create delegated token with subset of capabilities.

    Args:
        original_token: Parent token
        new_capabilities: Subset of parent capabilities
        ttl_hours: Time-to-live for delegated token

    Returns:
        Delegated token

    Raises:
        ValueError: If new_capabilities not subset of original
    """
    original_caps = set(original_token["capabilities"])
    new_caps = set(new_capabilities)

    # Verify attenuation (new capabilities must be subset)
    if not new_caps.issubset(original_caps):
        raise ValueError(
            f"Delegated capabilities must be subset of original. "
            f"Extra: {new_caps - original_caps}"
        )

    now = datetime.utcnow()
    delegated_token = {
        "token_id": f"token:{uuid.uuid4().hex[:12]}",
        "capabilities": list(new_capabilities),
        "issued_at": now.isoformat() + "Z",
        "expires_at": (now + timedelta(hours=ttl_hours)).isoformat() + "Z",
        "delegated_from": original_token.get("token_id")
    }

    return delegated_token

Usage:

# Original token
original = {
    "token_id": "token:abc123",
    "capabilities": ["tool:pii_redact", "tool:summarize", "resource:prod_db"]
}

# Delegate with reduced permissions
delegated = delegate_token(
    original,
    new_capabilities=["tool:pii_redact"],  # Only PII redaction
    ttl_hours=1  # Shorter TTL
)

print(delegated)
# {
#   "token_id": "token:def456",
#   "capabilities": ["tool:pii_redact"],
#   "expires_at": "2024-01-15T11:00:00Z",
#   "delegated_from": "token:abc123"
# }

Delegation Chains

Tokens can be delegated multiple times:

Original Token
  ↓ delegate (reduce capabilities + add expiration)
Delegated Token 1
  ↓ delegate (further reduce capabilities)
Delegated Token 2

Track delegation chains via delegated_from:

{
  "token_id": "token:ghi789",
  "capabilities": ["tool:pii_redact"],
  "delegated_from": "token:def456",
  "metadata": {
    "delegation_chain": ["token:abc123", "token:def456", "token:ghi789"]
  }
}

Token Revocation

Revocation Challenges

Capability tokens are bearer tokens - possession equals authority. This makes revocation challenging in offline scenarios.

Revocation Strategies

1. Expiration-Based:

Use short TTLs and require token refresh:

token = {
    "capabilities": ["tool:pii_redact"],
    "expires_at": (datetime.utcnow() + timedelta(minutes=15)).isoformat() + "Z"
}

2. Revocation Lists:

Maintain a list of revoked token IDs:

class TokenValidator:
    def __init__(self):
        self.revoked_tokens = set()

    def revoke_token(self, token_id):
        """Add token to revocation list."""
        self.revoked_tokens.add(token_id)

    def is_valid(self, token):
        """Check if token is valid (not revoked)."""
        token_id = token.get("token_id")
        if token_id in self.revoked_tokens:
            return False, "Token revoked"

        # Check expiration
        if self.is_expired(token):
            return False, "Token expired"

        return True, None

3. Version-Based Revocation:

Increment token version and reject old versions:

{
  "token_id": "token:abc123",
  "capabilities": ["tool:pii_redact"],
  "version": 2  // Old version=1 tokens are invalid
}

Advanced Topics

Multi-Capability Requirements

Some tools require multiple capabilities:

# Tool contract
{
  "name": "customer_report",
  "required_capabilities": [
    "resource:customer_db",
    "resource:analytics_db"
  ]
}

# Token must have ALL required capabilities
token = {
  "capabilities": [
    "resource:customer_db",
    "resource:analytics_db"
  ]
}

Conditional Capabilities

Add conditions to capabilities:

{
  "capabilities": [
    {
      "name": "resource:customer_db",
      "conditions": {
        "time_window": ["09:00", "17:00"],
        "ip_whitelist": ["192.168.1.0/24"]
      }
    }
  ]
}

This is not yet implemented in Adaptive Sentience but shows future direction.

Capability Inference

Some systems infer capabilities from hierarchies:

# Token has broad capability
token = {"capabilities": ["resource:db:*"]}

# Matches specific requirement
required = "resource:db:customer:read"

# Inference: resource:db:* includes resource:db:customer:read

Use Cases

1. Field Operations

Scenario: Field technician needs to redact PII from customer reports.

# Issue field token with limited capabilities
field_token = {
    "token_id": "token:field001",
    "capabilities": [
        "tool:pii_redact",
        "resource:field_reports:read"
    ],
    "expires_at": "2024-01-15T23:59:59Z",  # End of day
    "metadata": {
        "technician_id": "tech:alice",
        "location": "Site-42"
    }
}

2. Third-Party Integration

Scenario: Partner system needs to call specific tools.

# Issue API token for partner
partner_token = {
    "token_id": "token:partner_acme",
    "capabilities": [
        "tool:validate_schema",
        "tool:unit_convert"
    ],
    "expires_at": "2025-01-15T00:00:00Z",  # 1 year
    "metadata": {
        "partner": "ACME Corp",
        "contract_id": "CONTRACT-2024-001"
    }
}

3. Temporary Admin Access

Scenario: Grant temporary elevated access.

# Original admin token
admin_token = {
    "capabilities": ["admin:*"]
}

# Delegate temporary reduced admin access
temp_token = delegate_token(
    admin_token,
    new_capabilities=["admin:user_management"],  # Only user mgmt
    ttl_hours=2  # 2 hour window
)

4. Automated Workflows

Scenario: Background job needs specific capabilities.

# Issue long-lived service token
service_token = {
    "token_id": "token:service_backup",
    "capabilities": [
        "resource:prod_db:read",
        "resource:backup_storage:write"
    ],
    "expires_at": "2025-12-31T23:59:59Z",  # 2 year validity
    "metadata": {
        "service": "backup-scheduler",
        "purpose": "Automated DB backups"
    }
}

Security Considerations

1. Token Storage

Development:

# OK for dev - token in code
dev_token = {"capabilities": ["tool:pii_redact"]}

Production:

# Store in environment variable
export MESH_TOKEN='{"capabilities":["tool:pii_redact"]}'

import os
import json

token = json.loads(os.getenv("MESH_TOKEN"))

Secure storage: - Use secrets management (AWS Secrets Manager, HashiCorp Vault) - Encrypt tokens at rest - Rotate tokens regularly

2. Token Transmission

HTTP requests:

# Include token in request body (HTTPS only)
curl -X POST https://gateway.example.com/v1/tool_call \
  -H "Content-Type: application/json" \
  -d '{
    "token": {"capabilities": ["tool:pii_redact"]},
    "tool_name": "pii_redact",
    "tool_args": {...}
  }'

Never: - Send tokens over unencrypted HTTP - Log tokens to files - Include tokens in URLs (query parameters)

3. Least Privilege

Always grant minimum required capabilities:

# Good - specific capabilities
{"capabilities": ["tool:pii_redact"]}

# Bad - overly broad
{"capabilities": ["admin:*", "resource:*", "tool:*"]}

4. Token Rotation

Rotate tokens regularly:

# Issue new token every 24 hours
token = issue_token(
    capabilities=["tool:pii_redact"],
    ttl_hours=24
)

5. Audit Token Usage

Log all token usage for audit trails:

def log_token_usage(token, tool_name, result):
    """Log token usage to audit log."""
    print(f"[AUDIT] Token {token.get('token_id')} used for {tool_name}: {result}")

Best Practices

1. Use Token IDs

Always include token_id for audit trails:

token = {
    "token_id": f"token:{uuid.uuid4().hex[:12]}",
    "capabilities": [...]
}

2. Set Expiration

Set reasonable expiration times:

# Short-lived (hours)
{"expires_at": "2024-01-15T12:00:00Z"}

# Medium-lived (days)
{"expires_at": "2024-01-20T00:00:00Z"}

# Long-lived (months)
{"expires_at": "2024-06-15T00:00:00Z"}

3. Document Capabilities

Document what each capability grants:

# capabilities.md
CAPABILITIES = {
    "tool:pii_redact": "Execute PII redaction tool",
    "resource:customer_db": "Access customer database",
    "admin:user_management": "Create, update, delete users"
}

4. Validate Early

Validate tokens at the edge:

# Edge node validates before execution
def call_tool(tool_name, args, token):
    valid, error = validate_token(token, required_capability)
    if not valid:
        return {"ok": False, "error": error}

    # Execute tool
    ...

5. Monitor Token Usage

Track token usage metrics:

# Metrics to track
- Tokens issued per day
- Token validation failures
- Most-used capabilities
- Expired token attempts

Development Workflow

1. Dev Token Mode

For development, use the gateway's dev token mode:

python -m gateway.http_gateway --dev-token

This accepts any capability token without validation.

Dev Token Mode

NEVER use --dev-token in production. It disables all token validation.

2. Generate Test Tokens

# test_tokens.py
def get_test_token(capabilities):
    """Generate test token for development."""
    return {
        "token_id": f"test:{uuid.uuid4().hex[:8]}",
        "capabilities": capabilities,
        "issued_at": datetime.utcnow().isoformat() + "Z",
        "metadata": {"environment": "test"}
    }

Usage:

from test_tokens import get_test_token

token = get_test_token(["tool:pii_redact"])

3. Test Token Validation

def test_token_validation():
    """Test token validation logic."""
    # Valid token
    result = call_tool(
        "pii_redact",
        {"text": "test"},
        {"capabilities": ["tool:pii_redact"]}
    )
    assert result["ok"]

    # Invalid token (missing capability)
    result = call_tool(
        "pii_redact",
        {"text": "test"},
        {"capabilities": ["tool:other"]}
    )
    assert not result["ok"]
    assert "Missing required capability" in result["error"]

Troubleshooting

Missing Capability Error

Error: Missing required capability: tool:pii_redact

Solution:

  1. Check token capabilities:

    print(token["capabilities"])
    

  2. Verify required capability:

    curl http://localhost:8787/v1/catalog | jq '.mesh[].tools[] | select(.name=="pii_redact") | .required_capability'
    

  3. Add missing capability:

    token["capabilities"].append("tool:pii_redact")
    

Token Expired

Error: Token expired

Solution:

  1. Check expiration:

    print(token.get("expires_at"))
    

  2. Issue new token:

    token = issue_token(capabilities=[...], ttl_hours=24)
    

  3. Remove expiration for dev:

    del token["expires_at"]  # Never expires
    

Invalid Token Format

Error: Invalid token format

Solution:

  1. Verify token is a dict:

    assert isinstance(token, dict)
    

  2. Verify capabilities is an array:

    assert isinstance(token["capabilities"], list)
    

  3. Check for typos:

    // Wrong
    {"capability": ["tool:pii_redact"]}  // Should be "capabilities" (plural)
    
    // Correct
    {"capabilities": ["tool:pii_redact"]}
    


Reference

CapabilityToken Python Class

from dataclasses import dataclass
from datetime import datetime

@dataclass(frozen=True)
class CapabilityToken:
    """Capability token for authorization."""

    token_id: str
    capabilities: set[str]
    issued_at: datetime
    expires_at: datetime | None = None

    def is_expired(self) -> bool:
        """Check if token is expired."""
        if self.expires_at is None:
            return False
        return datetime.utcnow() > self.expires_at

    def has_capability(self, capability: str) -> bool:
        """Check if token contains a specific capability."""
        return capability in self.capabilities

    def has_all_capabilities(self, required: set[str]) -> bool:
        """Check if token contains all required capabilities."""
        return required.issubset(self.capabilities)

Token Validation Algorithm

1. Check token is dict
2. Extract capabilities array
3. Check required capability in capabilities
4. If expires_at present:
   a. Parse ISO 8601 timestamp
   b. Compare to current time
   c. Reject if expired
5. Return (authorized, error_message)

Next Steps


Further Reading