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:
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:
Capability approach:
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¶
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:
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¶
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¶
-
Short-lived tokens for sensitive operations:
-
Long-lived tokens for automated systems:
-
No expiration for development:
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:
Production:
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:
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:
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:
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:
-
Check token capabilities:
-
Verify required capability:
-
Add missing capability:
Token Expired¶
Error: Token expired
Solution:
-
Check expiration:
-
Issue new token:
-
Remove expiration for dev:
Invalid Token Format¶
Error: Invalid token format
Solution:
-
Verify token is a dict:
-
Verify capabilities is an array:
-
Check for typos:
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¶
- Tool Contracts - Define tools with capability requirements
- Trust Pairing - Secure node-to-node communication
- Execution Evidence - Audit trails with token usage
- Offline Operation - Token validation in offline scenarios