AOAuth Skill
Agent OAuth (AOAuth) is an OAuth 2.0 extension for agent-to-agent authentication. It supports both centralized Portal mode and decentralized self-issued mode.
TypeScript: the TS SDK ships
AuthSkill, which performs JWT verification via JWKS — sufficient to validate inbound AOAuth tokens. Token generation, OIDC discovery endpoints, allow/deny list management, and self-issued key publishing are Python-only today. Track the gap in the parity matrix.
Overview
The AOAuth skill provides:
- Token Generation - Create signed JWT tokens for agent-to-agent calls
- Token Validation - Verify incoming tokens from trusted issuers
- Automatic Injection - Hooks inject Bearer tokens into outgoing requests
- OIDC Discovery - Standard endpoints for key and configuration discovery
Operating Modes
| Mode | Description | Use Case |
|---|---|---|
| Portal | Tokens signed by Robutler Portal with namespace scopes | Production deployments |
| Self-Issued | Agent generates and signs own tokens | Development, federated systems |
Mode is determined by configuration: if authority is set, Portal mode is used; otherwise, Self-Issued mode.
Configuration
Portal Mode (Production)
skills:
auth:
authority: "https://robutler.ai"
agent_id: "my-agent"
allowed_scopes:
- read
- write
- namespace:*In Portal mode:
- Portal signs all tokens and assigns namespace scopes
- Token validation uses Portal's JWKS
- Centralized trust management
Self-Issued Mode (Development)
skills:
auth:
base_url: "@my-local-agent"
allowed_scopes:
- read
- write
allow:
- "@myteam/*"
- "@trusted-agent"
deny:
- "@banned-*"In Self-Issued mode:
- Agent generates RSA keys and signs own tokens
- Publishes JWKS at
/.well-known/jwks.json - Trust managed via allow/deny lists with glob patterns
Full Configuration Reference
skills:
auth:
# Operating Mode
authority: "https://robutler.ai" # Set for Portal mode, omit for self-issued
# Agent Identity
agent_id: "my-agent" # Unique agent identifier
base_url: "@my-agent" # Agent URL (or @name for normalization)
# Token Settings
token_ttl: 300 # Token lifetime in seconds (default: 5 min)
# Scope Control
allowed_scopes: # Scopes this agent accepts
- read
- write
- namespace:* # Wildcard for all namespace scopes
- tools:* # Wildcard for all tool scopes
# Trust Configuration
trusted_issuers: # Explicit trusted issuers
- issuer: "https://partner.ai"
jwks_uri: "https://partner.ai/.well-known/jwks.json"
type: "agent"
allow: # Allow list (glob patterns)
- "@myteam/*"
- "@trusted-agent"
deny: # Deny list (takes precedence)
- "@banned-*"
# OAuth Providers
google:
client_id: "${GOOGLE_CLIENT_ID}"
client_secret: "${GOOGLE_CLIENT_SECRET}"
hosted_domain: "company.com" # Optional G Suite restriction
robutler:
client_id: "my-agent"
client_secret: "${ROBUTLER_SECRET}"
# Key Management
keys_dir: "~/.webagents/keys" # RSA key storage
jwks_cache_ttl: 3600 # JWKS cache lifetime (1 hour)Usage
SDK API
import { BaseAgent } from 'webagents';
import { AuthSkill } from 'webagents/skills/auth';
// JWT verification via JWKS — validates incoming AOAuth tokens.
const authSkill = new AuthSkill({
jwksUri: 'https://robutler.ai/.well-known/jwks.json',
jwksCacheTtl: 3600,
audience: 'https://robutler.ai/agents/my-agent',
});
const agent = new BaseAgent({
name: 'my-agent',
skills: [authSkill],
});
// Validate incoming token (the on_connection hook does this automatically;
// call manually only when you need to verify a token outside a request).
const payload = await authSkill.verifyJwt(token);
if (payload) {
console.log(`Authenticated: ${payload.sub}`);
console.log(`Scopes: ${payload.scope}`);
}
// Token generation, allow/deny lists, and self-issued key publishing are
// Python-only today — see the parity matrix.Automatic Token Handling
The skill registers hooks for automatic token handling:
on_request_outgoing- Injects Bearer token into outgoing agent requestson_connection- Validates incoming Bearer tokens and attachesAuthContext
No manual token handling required for standard agent-to-agent calls.
CLI Commands
| Command | Description |
|---|---|
webagents login | Authenticate with robutler.ai |
webagents logout | Clear credentials |
webagents whoami | Show current authenticated user |
webagents token | Display current token |
webagents token --refresh | Refresh token |
Slash Commands (REPL)
| Command | Description |
|---|---|
/auth | Show AOAuth status and configuration |
/auth/token <target> | Generate token for target agent |
/auth/validate <token> | Validate a JWT token |
/auth/jwks | Show JWKS cache statistics |
HTTP Endpoints
The skill exposes standard OAuth/OIDC endpoints:
| Endpoint | Description |
|---|---|
/.well-known/openid-configuration | OpenID Connect Discovery |
/.well-known/jwks.json | JSON Web Key Set (public keys) |
/auth/token | OAuth token endpoint |
Token Endpoint
# Client credentials grant (agent-to-agent)
curl -X POST https://agent.example.com/auth/token \
-d "grant_type=client_credentials" \
-d "client_id=caller-agent" \
-d "client_secret=secret" \
-d "scope=read write" \
-d "target=@target-agent"JWT Token Structure
AOAuth tokens include standard OAuth claims plus AOAuth-specific extensions:
{
"iss": "https://robutler.ai",
"sub": "agent-a",
"aud": "https://robutler.ai/agents/agent-b",
"exp": 1234567890,
"iat": 1234567890,
"jti": "unique-token-id",
"scope": "read write namespace:production",
"client_id": "agent-a",
"token_type": "Bearer",
"aoauth": {
"mode": "portal",
"agent_url": "https://robutler.ai/agents/agent-a"
}
}Scope Format
Scopes are space-separated strings:
read,write,admin- Basic permissionsnamespace:production- Portal-assigned namespace membershiptools:search- Tool-specific access
Wildcard patterns like namespace:* in allowed_scopes accept all scopes with that prefix.
Trust Model
Portal Mode
Self-Issued Mode
AuthContext
The AuthContext object is attached to the request context after validation:
@dataclass
class AuthContext:
user_id: Optional[str] # User identity
agent_id: Optional[str] # Agent identity
source_agent: Optional[str] # Calling agent
authenticated: bool # Validation succeeded
scopes: List[str] # Granted scopes
namespaces: List[str] # Extracted namespace:* scopes
issuer: Optional[str] # Token issuer
issuer_type: str # "portal", "agent", "user"
raw_claims: Dict[str, Any] # Full JWT claimsChecking Permissions
import type { Context } from 'webagents';
function checkPermissions(ctx: Context) {
if (ctx.hasScope('write')) {
// Allowed to write
}
const namespaces = (ctx.auth?.scopes ?? [])
.filter((s) => s.startsWith('namespace:'))
.map((s) => s.slice('namespace:'.length));
if (namespaces.includes('production')) {
// Has production namespace access
}
}Security Considerations
- Key Storage - RSA keys stored in
~/.webagents/keys/with proper permissions - Token TTL - Default 5 minutes; adjust based on security requirements
- Allow/Deny Lists - Use specific patterns; empty allow list means "allow all non-denied"
- JWKS Caching - Smart caching with auto-refresh on key rotation
- Portal Mode - Recommended for production; centralizes trust management
Dependencies
PyJWT>=2.8
cryptography>=41.0
httpx>=0.25