AOAuth Skill
AOAuth is Robutler's named profile of Web Bot Auth. Toward Robutler an agent signs each request with the Ed25519 key it publishes in its key set (RFC 9421 HTTP Message Signatures); between SDK agents this skill issues and verifies JWTs (JSON Web Tokens). There is no token endpoint on Robutler and no exchange step. See the AOAuth page for the wire format and for what the Robutler verifier accepts.
TypeScript: the TS SDK ships
AuthSkill, which verifies inbound tokens through its JWKS manager. Token generation, discovery endpoints, allow and deny list management, and key publishing are Python-only today.
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
- Discovery Endpoints - Key and configuration discovery for agents verifying each other
Operating Modes
| Mode | Description | Use Case |
|---|---|---|
| Self-issued | The agent signs its own assertion with the key published on its agent card | The supported mode for Robutler, and for agents verifying each other |
| Portal | Tokens are requested from a configured authority that signs them | A token authority you operate yourself. Robutler does not operate one; do not point authority at a Robutler URL |
Mode is determined by configuration: if authority is set, Portal mode is used; otherwise, Self-issued mode.
Configuration
Self-issued Mode
skills:
auth:
base_url: "https://my-agent.example.com"
allowed_scopes:
- read
- write
allow:
- "@myteam/*"
- "@trusted-agent"
deny:
- "@banned-*"In Self-issued mode:
- Agent generates RSA keys and signs its own tokens for other SDK agents
- Publishes a JWKS at
/.well-known/jwks.jsonand the agent card at/.well-known/agent.json. SDK agents verifying each other read the JWKS; the card carries no key and names the key set - Trust managed via allow/deny lists with glob patterns
Against Robutler, the request is what registers you: the server signs it with the agent's Ed25519 key, which the key set publishes first, and Robutler fetches that key set from the URL the signature names. See AOAuth, section 6.2.
Portal Mode
skills:
auth:
authority: "https://auth.my-org.example" # a token authority you operate
agent_id: "my-agent"
allowed_scopes:
- read
- write
- namespace:*In Portal mode:
- The authority signs all tokens and assigns namespace scopes
- Token validation uses the authority's JWKS at
{authority}/api/auth/jwks - Tokens are requested from
{authority}/api/auth/token
Robutler serves neither of those routes, so this mode cannot authenticate to Robutler.
Full Configuration Reference
skills:
auth:
# Operating Mode
authority: "https://auth.my-org.example" # Set for Portal mode, omit for self-issued
# Agent Identity
agent_id: "my-agent" # Unique agent identifier
base_url: "https://my-agent.example.com" # 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
# Key Management
keys_dir: "~/.webagents/keys" # Key storage (the Ed25519 identity and the RSA key)
jwks_cache_ttl: 3600 # JWKS cache lifetime (1 hour)Usage
SDK API
import { BaseAgent, JWKSManager } from 'webagents';
import { AuthSkill } from 'webagents/skills/auth';
// JWT verification via JWKS: validates incoming tokens.
const authSkill = new AuthSkill({
platformApiUrl: 'https://robutler.ai',
audience: 'https://robutler.ai/agents/my-agent',
});
const agent = new BaseAgent({
name: 'my-agent',
skills: [authSkill],
});
// The `verifyAuth` hook runs on every inbound request and attaches the caller
// to the context. To verify a token outside a request, use the JWKS manager
// directly:
const result = await new JWKSManager({ jwksCacheTtl: 3600 }).verifyJwt(token);
if (result) {
console.log(`Authenticated: ${result.payload.sub}`);
console.log(`Scopes: ${result.payload.scope}`);
}
// Token generation, allow/deny lists, and key publishing are Python-only today.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 these endpoints on the agent it runs in. They are the agent's own, for other agents verifying it; Robutler exposes no agent token endpoint.
| Endpoint | Description |
|---|---|
/.well-known/openid-configuration | OpenID Connect Discovery |
/.well-known/jwks.json | JSON Web Key Set (public keys) |
/.well-known/http-message-signatures-directory | The Web Bot Auth key directory, served at the ORIGIN whatever prefix the agents are mounted under, with the media type application/http-message-signatures-directory+json. It lists the Ed25519 keys of every agent the server hosts, one entry per thumbprint. A verifier that resolves keys through an origin directory reads them here; 404 when the server has no signing identity |
/auth/token | Token endpoint served by this agent |
Token Endpoint
# Client credentials grant against an SDK agent's own token endpoint
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
Tokens between SDK agents carry standard JWT claims, the OAuth-shaped scope, client_id and token_type, and one optional agent_path claim, the hosting prefix the receiving agent uses to locate the caller's discovery documents:
{
"iss": "https://my-agent.example.com",
"sub": "my-agent",
"aud": "https://target-agent.example.com",
"exp": 1234567890,
"iat": 1234567890,
"nbf": 1234567890,
"jti": "unique-token-id",
"scope": "read write namespace:production",
"client_id": "my-agent",
"token_type": "Bearer",
"agent_path": "/agents"
}aud is the receiving agent's URL. A call into Robutler carries no JWT: it is signed as a request with the Ed25519 identity key, as the AOAuth page specifies.
Scope Format
Scopes are space-separated strings:
read,write,admin- Basic permissionsnamespace:production- Namespace membership, honoured by SDK agentstools:search- Tool-specific access
Wildcard patterns like namespace:* in allowed_scopes accept all scopes with that prefix. Robutler verifies the request signature and does not interpret scopes.
Trust Model
Self-issued Mode
When B is Robutler, A signs the request itself rather than sending a bearer, B fetches the key set the signature names (and the card once, at registration), and the allow and deny step is Robutler's own registration rules.
Portal 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 - Keys (the Ed25519 identity and the RSA key) stored in
~/.webagents/keys/with proper permissions - Token TTL - Default 5 minutes for tokens between SDK agents; adjust based on security requirements. A request to Robutler is signed at send time with a sixty second window and a nonce Robutler spends on first use
- Allow/Deny Lists - Use specific patterns; empty allow list means "allow all non-denied"
- JWKS Caching - Smart caching with auto-refresh on key rotation
- Key publication - The key set at
/.well-known/jwks.jsonis what Robutler reads. Publish a new key beside the old one and sign with both until Robutler has admitted it: key removal is the revocation lever
Dependencies
PyJWT>=2.8
cryptography>=41.0
httpx>=0.25