Framework-agnostic agent service SDK for A2A delegation with Keycard authentication. Supports CrewAI, LangChain, and custom agents.
This project has been archived.
The maintainers of this project have marked this project as archived. No new releases are expected.
Project description
KeycardAI Agents
Framework-agnostic agent service SDK for A2A (Agent-to-Agent) delegation with Keycard OAuth authentication.
Features
- ๐ Built-in OAuth: Automatic JWKS validation, token exchange, delegation chains
- ๐ Dual Protocol Support: A2A JSONRPC + custom REST endpoints (same executor powers both)
- ๐ง Framework Agnostic: Supports CrewAI, LangChain, custom via
AgentExecutorprotocol - ๐ Service Delegation: RFC 8693 token exchange preserves user context
- ๐ค User Auth: PKCE OAuth flow with browser-based login
A2A Protocol Integration
We use a2a-python SDK for protocol compliance while adding production-ready authentication:
- โ
Full A2A JSONRPC support - Standards-compliant
/a2a/jsonrpcendpoint - โ
Plus simpler REST endpoint - Custom
/invokefor easier integration - โ Production OAuth layer - BearerAuthMiddleware, JWKS, token exchange (A2A SDK has none)
- โ Delegation chain tracking - JWT-based audit trail for service-to-service calls
- โ Dual protocol support - Same executor powers both JSONRPC and REST endpoints
Result: A2A standards compliance + Keycard security + flexible APIs = Best of both worlds
Installation
pip install keycardai-agents
# With CrewAI support
pip install 'keycardai-agents[crewai]'
Quick Start
CrewAI Service
import os
from crewai import Agent, Crew, Task
from keycardai.agents import AgentServiceConfig
from keycardai.agents.integrations.crewai import CrewAIExecutor
from keycardai.agents.server import serve_agent
def create_my_crew():
agent = Agent(role="Assistant", goal="Help users", backstory="AI helper")
task = Task(description="{task}", agent=agent, expected_output="Response")
return Crew(agents=[agent], tasks=[task])
config = AgentServiceConfig(
service_name="My Service",
client_id=os.getenv("CLIENT_ID"),
client_secret=os.getenv("CLIENT_SECRET"),
identity_url="http://localhost:8000",
zone_id=os.getenv("ZONE_ID"),
agent_executor=CrewAIExecutor(create_my_crew), # Framework adapter
capabilities=["assistance"],
)
serve_agent(config) # Starts server with OAuth middleware
Custom Executor
from keycardai.agents.server import LambdaExecutor
def my_logic(task, inputs):
return f"Processed: {task}"
config = AgentServiceConfig(
# ... same config as above
agent_executor=LambdaExecutor(my_logic), # Simple function wrapper
)
Advanced: Custom Executor Class
from keycardai.agents.server import AgentExecutor
class MyFrameworkExecutor:
"""Implement AgentExecutor protocol for any framework."""
def execute(self, task, inputs):
# Your framework logic here
result = my_framework.run(task, inputs)
return result
def set_token_for_delegation(self, access_token):
# Optional: handle delegation token
self.context.set_auth(access_token)
config = AgentServiceConfig(
# ...
agent_executor=MyFrameworkExecutor(),
)
Client Usage
User Authentication (PKCE)
from keycardai.agents.client import AgentClient
async with AgentClient(config) as client:
# Automatically: OAuth discovery โ Browser login โ Token exchange
result = await client.invoke("https://service.com", task="Hello")
Service-to-Service (Token Exchange)
from keycardai.agents.server import DelegationClient
client = DelegationClient(service_config)
# Get delegation token (RFC 8693) - preserves user context
token = await client.get_delegation_token(
"https://target.com",
subject_token="user_token"
)
# Invoke with token
result = await client.invoke_service(
"https://target.com",
task="Process data",
token=token
)
# Result includes delegation_chain: ["service_a", "service_b"]
Architecture
Server
Your Agent
โ
AgentExecutor.execute(task, inputs)
โ
AgentServer (keycardai-agents)
โโ OAuth Middleware (BearerAuthMiddleware)
โ โโ JWKS validation
โ โโ Token audience check
โ โโ Delegation chain extraction
โโ /invoke (protected, REST-like)
โโ /a2a/jsonrpc (protected, A2A JSONRPC)
โ โโ message/send
โ โโ message/stream
โ โโ tasks/* (get, cancel, list)
โโ /.well-known/agent-card.json (A2A format)
โโ /.well-known/oauth-protected-resource
โโ /status
Dual Protocol Support
The SDK provides two ways to invoke agents:
-
A2A JSONRPC (
/a2a/jsonrpc) - Standards-compliant- Use when: Integrating with A2A ecosystem, need standard protocol
- Methods:
message/send,message/stream,tasks/get, etc. - Bridge:
KeycardToA2AExecutorBridgeadapts your executor to A2A protocol
-
Custom REST (
/invoke) - Simpler API- Use when: Direct service calls, simpler integration
- Format:
{"task": "...", "inputs": {...}} - Direct executor invocation
Both endpoints share the same underlying executor - write once, support both protocols.
OAuth Flow
User โ OAuth Login (PKCE)
โ
User Token โ Service A
โ
Service A โ Token Exchange (RFC 8693) โ Service B Token
โ
Service A โ Calls Service B with Service B Token
โ
Service B validates token (JWKS)
Service B updates delegation_chain
A2A Protocol Compliance
Agent Card
Services expose A2A-compliant agent cards at /.well-known/agent-card.json:
{
"name": "My Service",
"url": "https://my-service.com",
"version": "1.0.0",
"protocolVersion": "0.3.0",
"skills": [
{
"id": "assistance",
"name": "Assistance",
"description": "assistance capability",
"tags": ["assistance"]
}
],
"capabilities": {
"streaming": false,
"multiTurn": true
},
"additionalInterfaces": [
{
"url": "https://my-service.com/invoke",
"transport": "http+json"
}
],
"securitySchemes": {
"oauth2": {
"type": "oauth2",
"flows": {
"authorizationCode": {
"authorizationUrl": "https://zone.keycard.cloud/oauth/authorize",
"tokenUrl": "https://zone.keycard.cloud/oauth/token"
}
}
}
}
}
Endpoints
A2A JSONRPC Endpoint (Standards-Compliant)
POST /a2a/jsonrpc
Authorization: Bearer <token>
Content-Type: application/json
{
"jsonrpc": "2.0",
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [{"text": "Do something"}]
}
},
"id": 1
}
Response:
{
"jsonrpc": "2.0",
"result": {
"task": {
"taskId": "task-123",
"state": "completed",
"result": {...}
}
},
"id": 1
}
Supported methods:
message/send- Send message to agentmessage/stream- Stream agent responsestasks/get- Get task statustasks/cancel- Cancel running tasktasks/list- List all tasks
Custom REST Endpoint (Simpler API)
POST /invoke
Authorization: Bearer <token>
{
"task": "Do something",
"inputs": {"key": "value"}
}
Response:
{
"result": "Done",
"delegation_chain": ["service_a", "service_b"]
}
Use /invoke for: Direct service calls, easier integration, delegation chain tracking.
Use /a2a/jsonrpc for: A2A ecosystem integration, standard protocol compliance, task management.
Framework Support
CrewAI
from keycardai.agents.integrations.crewai import CrewAIExecutor
executor = CrewAIExecutor(lambda: create_my_crew())
Features:
- Automatic delegation token context
- Supports CrewAI tools
- Handles
crew.kickoff()execution
LangChain, AutoGen, Custom
Implement the AgentExecutor protocol:
class MyExecutor:
def execute(self, task, inputs):
# Your logic
return result
API Reference
AgentServiceConfig
@dataclass
class AgentServiceConfig:
service_name: str # Human-readable name
client_id: str # Keycard Application client ID
client_secret: str # Keycard Application secret
identity_url: str # Public URL
zone_id: str # Keycard zone ID
agent_executor: AgentExecutor # REQUIRED: Executor instance
# Optional
authorization_server_url: str | None = None
port: int = 8000
host: str = "0.0.0.0"
description: str = ""
capabilities: list[str] = []
AgentExecutor Protocol
class AgentExecutor(Protocol):
def execute(
self,
task: dict[str, Any] | str,
inputs: dict[str, Any] | None = None,
) -> Any:
"""Execute agent task."""
...
def set_token_for_delegation(self, access_token: str) -> None:
"""Optional: Set token for delegation."""
...
KeycardToA2AExecutorBridge
Bridge adapter that makes your executor work with A2A JSONRPC protocol:
from keycardai.agents.server import KeycardToA2AExecutorBridge, SimpleExecutor
# Your executor
executor = SimpleExecutor()
# Wrap for A2A JSONRPC support
a2a_executor = KeycardToA2AExecutorBridge(executor)
# Now works with A2A DefaultRequestHandler
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryTaskStore
handler = DefaultRequestHandler(
agent_executor=a2a_executor,
task_store=InMemoryTaskStore()
)
What it does:
- Converts A2A
RequestContextโ Keycardtask/inputsformat - Calls your synchronous executor
- Publishes result as A2A Task events
- Handles delegation tokens
Note: This bridge is automatically configured when using serve_agent() - you don't need to use it directly unless building custom A2A integrations.
serve_agent()
Start an agent service (blocking):
serve_agent(config: AgentServiceConfig) -> None
AgentClient
User authentication with PKCE OAuth:
from keycardai.agents.client import AgentClient
async with AgentClient(service_config) as client:
result = await client.invoke(service_url, task, inputs)
agent_card = await client.discover_service(service_url)
DelegationClient
Service-to-service with token exchange:
from keycardai.agents.server import DelegationClient
client = DelegationClient(service_config)
token = await client.get_delegation_token(target_url, subject_token)
result = await client.invoke_service(url, task, token)
Service Delegation
Pattern
# In Service A (orchestrator)
from keycardai.agents.server import DelegationClient
client = DelegationClient(service_a_config)
# Discover Service B
card = await client.discover_service("https://service-b.com")
# Get token with user context
token = await client.get_delegation_token(
"https://service-b.com",
subject_token=user_access_token
)
# Call Service B
result = await client.invoke_service(
"https://service-b.com",
task="Process data",
token=token
)
# Result includes delegation chain for audit
print(result["delegation_chain"])
# ["user_service", "service_a", "service_b"]
Delegation Chain Tracking
- User authenticates โ Token with empty
delegation_chain - User calls Service A โ Service A adds itself to chain
- Service A calls Service B โ Token exchange preserves chain
- Service B adds itself โ Full chain in response for audit
Production Deployment
Environment Variables
# Required
export KEYCARD_ZONE_ID="your_zone_id"
export KEYCARD_CLIENT_ID="service_client_id"
export KEYCARD_CLIENT_SECRET="client_secret"
export SERVICE_URL="https://your-service.com"
# Optional
export PORT="8000"
export HOST="0.0.0.0"
Health Checks
# Liveness
curl https://your-service.com/status
# Agent card
curl https://your-service.com/.well-known/agent-card.json
Security
- Token Validation: JWKS-based JWT signature verification
- Audience Check: Token
audmust match service URL - Issuer Validation: Token
issfrom Keycard zone - Delegation Chain: Preserved for audit trail
Examples
See examples/ directory:
oauth_client_usage.py- PKCE user authentication
FAQ
Q: Why not use the A2A SDK server?
A: The A2A SDK has no authentication layer. We'd have to rebuild all OAuth infrastructure.
Q: Can I use LangChain/AutoGen?
A: Yes! Implement the AgentExecutor protocol or use LambdaExecutor for simple functions.
Q: What's the difference between AgentClient and DelegationClient?
A:
AgentClient: User authentication with PKCE (browser-based login)DelegationClient: Service-to-service with token exchange (RFC 8693)
Q: Do I need CrewAI?
A: No! Use any framework or write custom logic. Just implement AgentExecutor.
Support
- GitHub: https://github.com/keycardai/python-sdk
- Issues: https://github.com/keycardai/python-sdk/issues
- Docs: https://docs.keycard.ai
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file keycardai_agents-0.1.1.tar.gz.
File metadata
- Download URL: keycardai_agents-0.1.1.tar.gz
- Upload date:
- Size: 31.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.9.22 {"installer":{"name":"uv","version":"0.9.22","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fadb89cdff4e872db20a51fa93dc42e005cfa88839fb7b4b779df9b826e83bac
|
|
| MD5 |
0711b96e58b16c512bc26e9adcc2bb20
|
|
| BLAKE2b-256 |
c17515861e463adcef96da8f696c1d5d6989d9e752d39190a55e3698d04aa3bf
|
File details
Details for the file keycardai_agents-0.1.1-py3-none-any.whl.
File metadata
- Download URL: keycardai_agents-0.1.1-py3-none-any.whl
- Upload date:
- Size: 34.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.9.22 {"installer":{"name":"uv","version":"0.9.22","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
26149c4c085a35e22e06fd6c5cb79d4b9f5e4ec4aabf46e8edaded67e2a85da3
|
|
| MD5 |
7bde64d4cc938a19767e09111c7ccce5
|
|
| BLAKE2b-256 |
10470a7c6af6936963e0a9eeded5f1bef8c085ee5ee2a65088baee70905628d0
|