webagents 0.1.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- webagents/__init__.py +18 -0
- webagents/agents/__init__.py +13 -0
- webagents/agents/core/__init__.py +19 -0
- webagents/agents/core/base_agent.py +1834 -0
- webagents/agents/core/handoffs.py +293 -0
- webagents/agents/handoffs/__init__.py +0 -0
- webagents/agents/interfaces/__init__.py +0 -0
- webagents/agents/lifecycle/__init__.py +0 -0
- webagents/agents/skills/__init__.py +109 -0
- webagents/agents/skills/base.py +136 -0
- webagents/agents/skills/core/__init__.py +8 -0
- webagents/agents/skills/core/guardrails/__init__.py +0 -0
- webagents/agents/skills/core/llm/__init__.py +0 -0
- webagents/agents/skills/core/llm/anthropic/__init__.py +1 -0
- webagents/agents/skills/core/llm/litellm/__init__.py +10 -0
- webagents/agents/skills/core/llm/litellm/skill.py +538 -0
- webagents/agents/skills/core/llm/openai/__init__.py +1 -0
- webagents/agents/skills/core/llm/xai/__init__.py +1 -0
- webagents/agents/skills/core/mcp/README.md +375 -0
- webagents/agents/skills/core/mcp/__init__.py +15 -0
- webagents/agents/skills/core/mcp/skill.py +731 -0
- webagents/agents/skills/core/memory/__init__.py +11 -0
- webagents/agents/skills/core/memory/long_term_memory/__init__.py +10 -0
- webagents/agents/skills/core/memory/long_term_memory/memory_skill.py +639 -0
- webagents/agents/skills/core/memory/short_term_memory/__init__.py +9 -0
- webagents/agents/skills/core/memory/short_term_memory/skill.py +341 -0
- webagents/agents/skills/core/memory/vector_memory/skill.py +447 -0
- webagents/agents/skills/core/planning/__init__.py +9 -0
- webagents/agents/skills/core/planning/planner.py +343 -0
- webagents/agents/skills/ecosystem/__init__.py +0 -0
- webagents/agents/skills/ecosystem/crewai/__init__.py +1 -0
- webagents/agents/skills/ecosystem/database/__init__.py +1 -0
- webagents/agents/skills/ecosystem/filesystem/__init__.py +0 -0
- webagents/agents/skills/ecosystem/google/__init__.py +0 -0
- webagents/agents/skills/ecosystem/google/calendar/__init__.py +6 -0
- webagents/agents/skills/ecosystem/google/calendar/skill.py +306 -0
- webagents/agents/skills/ecosystem/n8n/__init__.py +0 -0
- webagents/agents/skills/ecosystem/openai_agents/__init__.py +0 -0
- webagents/agents/skills/ecosystem/web/__init__.py +0 -0
- webagents/agents/skills/ecosystem/zapier/__init__.py +0 -0
- webagents/agents/skills/robutler/__init__.py +11 -0
- webagents/agents/skills/robutler/auth/README.md +63 -0
- webagents/agents/skills/robutler/auth/__init__.py +17 -0
- webagents/agents/skills/robutler/auth/skill.py +354 -0
- webagents/agents/skills/robutler/crm/__init__.py +18 -0
- webagents/agents/skills/robutler/crm/skill.py +368 -0
- webagents/agents/skills/robutler/discovery/README.md +281 -0
- webagents/agents/skills/robutler/discovery/__init__.py +16 -0
- webagents/agents/skills/robutler/discovery/skill.py +230 -0
- webagents/agents/skills/robutler/kv/__init__.py +6 -0
- webagents/agents/skills/robutler/kv/skill.py +80 -0
- webagents/agents/skills/robutler/message_history/__init__.py +9 -0
- webagents/agents/skills/robutler/message_history/skill.py +270 -0
- webagents/agents/skills/robutler/messages/__init__.py +0 -0
- webagents/agents/skills/robutler/nli/__init__.py +13 -0
- webagents/agents/skills/robutler/nli/skill.py +687 -0
- webagents/agents/skills/robutler/notifications/__init__.py +5 -0
- webagents/agents/skills/robutler/notifications/skill.py +141 -0
- webagents/agents/skills/robutler/payments/__init__.py +41 -0
- webagents/agents/skills/robutler/payments/exceptions.py +255 -0
- webagents/agents/skills/robutler/payments/skill.py +610 -0
- webagents/agents/skills/robutler/storage/__init__.py +10 -0
- webagents/agents/skills/robutler/storage/files/__init__.py +9 -0
- webagents/agents/skills/robutler/storage/files/skill.py +445 -0
- webagents/agents/skills/robutler/storage/json/__init__.py +9 -0
- webagents/agents/skills/robutler/storage/json/skill.py +336 -0
- webagents/agents/skills/robutler/storage/kv/skill.py +88 -0
- webagents/agents/skills/robutler/storage.py +389 -0
- webagents/agents/tools/__init__.py +0 -0
- webagents/agents/tools/decorators.py +426 -0
- webagents/agents/tracing/__init__.py +0 -0
- webagents/agents/workflows/__init__.py +0 -0
- webagents/api/__init__.py +17 -0
- webagents/api/client.py +1207 -0
- webagents/api/types.py +253 -0
- webagents/scripts/__init__.py +0 -0
- webagents/server/__init__.py +28 -0
- webagents/server/context/__init__.py +0 -0
- webagents/server/context/context_vars.py +121 -0
- webagents/server/core/__init__.py +0 -0
- webagents/server/core/app.py +843 -0
- webagents/server/core/middleware.py +69 -0
- webagents/server/core/models.py +98 -0
- webagents/server/core/monitoring.py +59 -0
- webagents/server/endpoints/__init__.py +0 -0
- webagents/server/interfaces/__init__.py +0 -0
- webagents/server/middleware.py +330 -0
- webagents/server/models.py +92 -0
- webagents/server/monitoring.py +659 -0
- webagents/utils/__init__.py +0 -0
- webagents/utils/logging.py +359 -0
- webagents-0.1.12.dist-info/METADATA +99 -0
- webagents-0.1.12.dist-info/RECORD +96 -0
- webagents-0.1.12.dist-info/WHEEL +4 -0
- webagents-0.1.12.dist-info/entry_points.txt +2 -0
- webagents-0.1.12.dist-info/licenses/LICENSE +1 -0
File without changes
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# AuthSkill (Robutler V2)
|
2
|
+
|
3
|
+
Owner-aware authentication and authorization for agents. Integrates with the Robutler Portal and supports secure owner assertions for owner-only tools (e.g., ControlSkill).
|
4
|
+
|
5
|
+
## Features
|
6
|
+
- API key validation against the Portal
|
7
|
+
- Owner scope detection (admin/user/owner)
|
8
|
+
- Owner assertions via `X-Owner-Assertion`:
|
9
|
+
- RS256 with JWKS (recommended for external agents)
|
10
|
+
- HS256 fallback for in-infra setups
|
11
|
+
- Identity propagation: `origin_user_id`, `peer_user_id`, `agent_owner_user_id`
|
12
|
+
|
13
|
+
## Headers
|
14
|
+
- `Authorization: Bearer <key>` one of:
|
15
|
+
- Service token (srv_...) for backend-to-backend
|
16
|
+
- Agent API key (rok_...)
|
17
|
+
- Session/JWT for owner (when calling Portal endpoints)
|
18
|
+
- `X-Owner-Assertion: <jwt>` short‑lived JWT binding the caller to an agent (see below)
|
19
|
+
- `X-Origin-User-ID`, `X-Peer-User-ID`, `X-Agent-Owner-User-ID` optional hints
|
20
|
+
|
21
|
+
## Owner Assertion JWT
|
22
|
+
- Claims:
|
23
|
+
- `iss`: robutler-portal
|
24
|
+
- `aud`: robutler-agent:<agentId>
|
25
|
+
- `sub`: origin_user_id
|
26
|
+
- `agent_id`: <agentId>
|
27
|
+
- `owner_user_id`: <ownerId>
|
28
|
+
- `jti`: unique id
|
29
|
+
- `iat/nbf/exp`: short TTL (2–5 minutes)
|
30
|
+
- Signing:
|
31
|
+
- RS256 with `OWNER_ASSERTION_PRIVATE_KEY` and JWKS served at `/api/auth/jwks`
|
32
|
+
- HS256 fallback with `OWNER_ASSERTION_SECRET` (or `AUTH_SECRET`)
|
33
|
+
|
34
|
+
## Env (Agent Service)
|
35
|
+
- RS256: `OWNER_ASSERTION_JWKS_URL` → Portal JWKS URL
|
36
|
+
- HS256: `OWNER_ASSERTION_SECRET` (shared with Portal) or rely on `AUTH_SECRET`
|
37
|
+
|
38
|
+
## Env (Portal)
|
39
|
+
- RS256: `OWNER_ASSERTION_PRIVATE_KEY` (PEM), `OWNER_ASSERTION_KID`
|
40
|
+
- HS256: `OWNER_ASSERTION_SECRET` or `AUTH_SECRET`
|
41
|
+
- `SERVICE_TOKEN` for backend issuance
|
42
|
+
|
43
|
+
## Issuance API (Portal)
|
44
|
+
- `POST /api/auth/owner-assertion`
|
45
|
+
- Service token: mint for any owner userId
|
46
|
+
- Owner session/API key: owner-only; assertion is always for the caller
|
47
|
+
- Body: `{ agentId, originUserId?, ttlSeconds? }`
|
48
|
+
- Response: `{ assertion, expiresAt }`
|
49
|
+
- `GET /api/auth/jwks` → public keys for RS256 verification
|
50
|
+
|
51
|
+
## Verification (AuthSkill)
|
52
|
+
- Validates `Authorization` with Portal
|
53
|
+
- Verifies `X-Owner-Assertion` when present:
|
54
|
+
- RS256 via JWKS if `OWNER_ASSERTION_JWKS_URL` is set
|
55
|
+
- HS256 via `OWNER_ASSERTION_SECRET`/`AUTH_SECRET` fallback
|
56
|
+
- Enforces `aud = robutler-agent:<agentId>`
|
57
|
+
- Enforces `claims.agent_id == agent.id`
|
58
|
+
- Upgrades scope to OWNER when the verified user is the agent owner
|
59
|
+
|
60
|
+
## Best Practices
|
61
|
+
- Always send over TLS; never log assertion values
|
62
|
+
- Short TTL; consider `jti` replay cache for high-security deployments
|
63
|
+
- Don’t expose agent API keys to browsers; inject headers server-side only
|
@@ -0,0 +1,17 @@
|
|
1
|
+
"""
|
2
|
+
AuthSkill Package - Robutler V2.0
|
3
|
+
|
4
|
+
Authentication and authorization skill for Robutler platform.
|
5
|
+
Provides user authentication, API key validation, role-based access control,
|
6
|
+
and integration with Robutler platform services.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from .skill import AuthSkill, AuthScope, AuthContext, AuthenticationError, AuthorizationError
|
10
|
+
|
11
|
+
__all__ = [
|
12
|
+
"AuthSkill",
|
13
|
+
"AuthScope",
|
14
|
+
"AuthContext",
|
15
|
+
"AuthenticationError",
|
16
|
+
"AuthorizationError"
|
17
|
+
]
|
@@ -0,0 +1,354 @@
|
|
1
|
+
"""
|
2
|
+
AuthSkill - Robutler V2.0 Platform Integration
|
3
|
+
|
4
|
+
Authentication and authorization skill for Robutler platform.
|
5
|
+
Integrates with Robutler Portal APIs for user authentication, API key validation,
|
6
|
+
and platform service integration.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import os
|
10
|
+
from typing import Dict, Any, List, Optional
|
11
|
+
from dataclasses import dataclass
|
12
|
+
from enum import Enum
|
13
|
+
|
14
|
+
from webagents.agents.skills.base import Skill
|
15
|
+
from webagents.agents.tools.decorators import tool, hook
|
16
|
+
from webagents.api import RobutlerClient
|
17
|
+
from webagents.api.types import User, ApiKey, AuthResponse
|
18
|
+
from typing import Any as _Any
|
19
|
+
try:
|
20
|
+
from jose import jwt as jose_jwt # python-jose
|
21
|
+
except Exception:
|
22
|
+
jose_jwt = None # type: ignore
|
23
|
+
|
24
|
+
|
25
|
+
class AuthScope(Enum):
|
26
|
+
"""Authentication scopes for role-based access control"""
|
27
|
+
ADMIN = "admin"
|
28
|
+
OWNER = "owner"
|
29
|
+
USER = "user"
|
30
|
+
ALL = "all"
|
31
|
+
|
32
|
+
|
33
|
+
@dataclass
|
34
|
+
class AuthContext:
|
35
|
+
"""Authentication context for requests (harmonized)
|
36
|
+
|
37
|
+
- user_id: ID of the caller. Prefer JWT `sub` when present; otherwise the API key owner's user ID.
|
38
|
+
- agent_id: Agent ID asserted by JWT, when present and verified.
|
39
|
+
- authenticated: True if API key (and/or assertion) verification succeeds.
|
40
|
+
- scope: Authorization scope derived from platform user and agent ownership.
|
41
|
+
- assertion: Decoded JWT claims when an owner assertion is provided and verified.
|
42
|
+
"""
|
43
|
+
user_id: Optional[str] = None
|
44
|
+
agent_id: Optional[str] = None
|
45
|
+
authenticated: bool = False
|
46
|
+
scope: AuthScope = AuthScope.USER
|
47
|
+
assertion: Optional[Dict[str, Any]] = None
|
48
|
+
|
49
|
+
|
50
|
+
class AuthSkill(Skill):
|
51
|
+
"""
|
52
|
+
Authentication and authorization skill for Robutler platform
|
53
|
+
|
54
|
+
Features:
|
55
|
+
- Platform integration with Robutler Portal APIs
|
56
|
+
- API key authentication and validation
|
57
|
+
- User information retrieval
|
58
|
+
- Credit tracking and usage management
|
59
|
+
- Request authentication hooks
|
60
|
+
- Role-based access control
|
61
|
+
"""
|
62
|
+
|
63
|
+
def __init__(self, config: Dict[str, Any] = None):
|
64
|
+
super().__init__(config, scope="all")
|
65
|
+
|
66
|
+
# Configuration
|
67
|
+
self.config = config or {}
|
68
|
+
self.require_auth = self.config.get('require_auth', True)
|
69
|
+
# Prefer internal portal URL, then public URL, then localhost for dev
|
70
|
+
self.platform_api_url = (
|
71
|
+
self.config.get('platform_api_url')
|
72
|
+
or os.getenv('ROBUTLER_INTERNAL_API_URL')
|
73
|
+
or os.getenv('ROBUTLER_API_URL')
|
74
|
+
or 'http://localhost:3000'
|
75
|
+
)
|
76
|
+
self.api_key = self.config.get('api_key')
|
77
|
+
|
78
|
+
# Cache configuration
|
79
|
+
self._cache_ttl = self.config.get('cache_ttl', 300) # 5 minutes default
|
80
|
+
|
81
|
+
# API client for platform integration
|
82
|
+
self.client: Optional[RobutlerClient] = None
|
83
|
+
|
84
|
+
async def initialize(self, agent) -> None:
|
85
|
+
"""Initialize AuthSkill with Robutler Platform client"""
|
86
|
+
from webagents.utils.logging import get_logger, log_skill_event
|
87
|
+
|
88
|
+
self.agent = agent
|
89
|
+
self.logger = get_logger('skill.robutler.auth', agent.name)
|
90
|
+
|
91
|
+
# Initialize Robutler Platform client
|
92
|
+
try:
|
93
|
+
# Use api_key as priority, fallback to agent's API key
|
94
|
+
final_api_key = self.api_key or getattr(agent, 'api_key', None)
|
95
|
+
|
96
|
+
self.client = RobutlerClient(
|
97
|
+
api_key=final_api_key,
|
98
|
+
base_url=self.platform_api_url
|
99
|
+
)
|
100
|
+
|
101
|
+
# Test connection
|
102
|
+
health_response = await self.client.health_check()
|
103
|
+
if health_response.success:
|
104
|
+
self.logger.info(f"Connected to Robutler Platform: {self.platform_api_url}")
|
105
|
+
else:
|
106
|
+
self.logger.warning(f"Platform health check failed: {health_response.message}")
|
107
|
+
|
108
|
+
except Exception as e:
|
109
|
+
self.logger.error(f"Failed to initialize Robutler Platform client: {e}")
|
110
|
+
# Continue without platform integration for testing
|
111
|
+
self.client = None
|
112
|
+
|
113
|
+
log_skill_event(agent.name, 'auth', 'initialized', {
|
114
|
+
'require_auth': self.require_auth,
|
115
|
+
'platform_api_url': self.platform_api_url,
|
116
|
+
'has_platform_client': bool(self.client),
|
117
|
+
'cache_ttl': self._cache_ttl
|
118
|
+
})
|
119
|
+
|
120
|
+
# ===== AUTHENTICATION HOOKS =====
|
121
|
+
|
122
|
+
@hook("on_connection", priority=0, scope="all")
|
123
|
+
async def validate_request_auth(self, context) -> Any:
|
124
|
+
"""Validate authentication for incoming requests using Robutler Platform"""
|
125
|
+
if not self.require_auth:
|
126
|
+
return context
|
127
|
+
|
128
|
+
# Extract API key from request (may be absent)
|
129
|
+
api_key = self._extract_api_key_from_context(context)
|
130
|
+
|
131
|
+
# 1) Try API key authentication first (preferred when present)
|
132
|
+
auth_context = None
|
133
|
+
if api_key:
|
134
|
+
auth_context = await self._authenticate_api_key(api_key)
|
135
|
+
|
136
|
+
# 2) If API key auth failed or not provided, try owner assertion only
|
137
|
+
if not auth_context or not auth_context.authenticated:
|
138
|
+
assertion_only_context = await self._authenticate_with_owner_assertion_only(context)
|
139
|
+
if assertion_only_context and assertion_only_context.authenticated:
|
140
|
+
context.auth = assertion_only_context
|
141
|
+
return context
|
142
|
+
|
143
|
+
# 3) If API key auth succeeded, set context
|
144
|
+
if auth_context and auth_context.authenticated:
|
145
|
+
context.auth = auth_context
|
146
|
+
return context
|
147
|
+
|
148
|
+
# Neither worked
|
149
|
+
raise AuthenticationError("Authentication failed (API key or owner assertion required)")
|
150
|
+
|
151
|
+
|
152
|
+
# ===== INTERNAL METHODS =====
|
153
|
+
|
154
|
+
def _extract_api_key_from_context(self, context) -> Optional[str]:
|
155
|
+
"""Extract API key from request context"""
|
156
|
+
# Try to get from headers (Authorization: Bearer <token>)
|
157
|
+
headers = getattr(context.request, 'headers', {})
|
158
|
+
auth_header = headers.get('authorization', headers.get('Authorization'))
|
159
|
+
|
160
|
+
if auth_header and auth_header.startswith('Bearer '):
|
161
|
+
return auth_header[7:] # Remove 'Bearer ' prefix
|
162
|
+
|
163
|
+
# Try X-API-Key header
|
164
|
+
api_key_header = headers.get('x-api-key', headers.get('X-API-Key'))
|
165
|
+
if api_key_header:
|
166
|
+
return api_key_header
|
167
|
+
|
168
|
+
# Try to get from query parameters
|
169
|
+
query_params = getattr(context.request, 'query_params', {})
|
170
|
+
if 'api_key' in query_params:
|
171
|
+
return query_params['api_key']
|
172
|
+
|
173
|
+
# Try to get from context data directly
|
174
|
+
return context.get('api_key')
|
175
|
+
|
176
|
+
def _extract_owner_assertion(self, context) -> Optional[str]:
|
177
|
+
"""Extract X-Owner-Assertion from headers"""
|
178
|
+
if not hasattr(context, 'request') or not context.request:
|
179
|
+
return None
|
180
|
+
headers = getattr(context.request, 'headers', {}) or {}
|
181
|
+
return headers.get('X-Owner-Assertion') or headers.get('x-owner-assertion')
|
182
|
+
|
183
|
+
def _extract_header(self, context, header_name: str) -> Optional[str]:
|
184
|
+
"""Extract header value from context.request"""
|
185
|
+
if not hasattr(context, 'request') or not context.request:
|
186
|
+
return None
|
187
|
+
|
188
|
+
headers = getattr(context.request, 'headers', {})
|
189
|
+
if not headers:
|
190
|
+
return None
|
191
|
+
|
192
|
+
# Try exact match first
|
193
|
+
if header_name in headers:
|
194
|
+
return headers[header_name]
|
195
|
+
|
196
|
+
# Try case-insensitive match
|
197
|
+
header_name_lower = header_name.lower()
|
198
|
+
for key, value in headers.items():
|
199
|
+
if key.lower() == header_name_lower:
|
200
|
+
return value
|
201
|
+
|
202
|
+
return None
|
203
|
+
|
204
|
+
def _is_agent_owner(self, user_id: str) -> bool:
|
205
|
+
"""Check if the user is the owner of the current agent"""
|
206
|
+
# Check agent metadata only (context does not carry owner id)
|
207
|
+
if hasattr(self.agent, 'owner_user_id'):
|
208
|
+
return user_id == self.agent.owner_user_id
|
209
|
+
|
210
|
+
return False
|
211
|
+
|
212
|
+
async def _authenticate_with_owner_assertion_only(self, context) -> Optional[AuthContext]:
|
213
|
+
"""Authenticate using only X-Owner-Assertion (RS256/JWKS), without API key.
|
214
|
+
Grants authenticated USER scope; elevates to OWNER if assertion.owner_user_id == agent.owner_user_id.
|
215
|
+
"""
|
216
|
+
try:
|
217
|
+
assertion_token = self._extract_owner_assertion(context)
|
218
|
+
if not assertion_token or jose_jwt is None:
|
219
|
+
return None
|
220
|
+
jwks_url = os.getenv('OWNER_ASSERTION_JWKS_URL') or f"{(self.platform_api_url or '').rstrip('/')}/api/auth/jwks"
|
221
|
+
if not jwks_url:
|
222
|
+
return None
|
223
|
+
import requests
|
224
|
+
# Fetch JWKS and select key by kid
|
225
|
+
hdr = jose_jwt.get_unverified_header(assertion_token)
|
226
|
+
kid = hdr.get('kid')
|
227
|
+
r = requests.get(jwks_url, timeout=5)
|
228
|
+
r.raise_for_status()
|
229
|
+
keys = (r.json() or {}).get('keys', [])
|
230
|
+
selected_key = None
|
231
|
+
for k in keys:
|
232
|
+
if not kid or k.get('kid') == kid:
|
233
|
+
selected_key = k
|
234
|
+
break
|
235
|
+
if not selected_key and keys:
|
236
|
+
selected_key = keys[0]
|
237
|
+
if not selected_key:
|
238
|
+
raise Exception('No JWKS key available for owner assertion verification')
|
239
|
+
# Decode with selected JWK
|
240
|
+
claims = jose_jwt.decode(
|
241
|
+
assertion_token,
|
242
|
+
selected_key,
|
243
|
+
algorithms=['RS256'],
|
244
|
+
audience=f"robutler-agent:{getattr(self.agent, 'id', '')}",
|
245
|
+
)
|
246
|
+
if claims.get('agent_id') and getattr(self.agent, 'id', None) and claims['agent_id'] != getattr(self.agent, 'id'):
|
247
|
+
raise Exception('Owner assertion agent_id mismatch')
|
248
|
+
|
249
|
+
acting_user_id = claims.get('sub')
|
250
|
+
owner_user_id = claims.get('owner_user_id')
|
251
|
+
scope = AuthScope.OWNER if (owner_user_id and hasattr(self.agent, 'owner_user_id') and owner_user_id == getattr(self.agent, 'owner_user_id')) else AuthScope.USER
|
252
|
+
|
253
|
+
return AuthContext(
|
254
|
+
user_id=acting_user_id,
|
255
|
+
agent_id=claims.get('agent_id'),
|
256
|
+
authenticated=True,
|
257
|
+
scope=scope,
|
258
|
+
assertion=claims,
|
259
|
+
)
|
260
|
+
except Exception as e:
|
261
|
+
try:
|
262
|
+
self.logger.debug(f"Owner assertion-only authentication failed: {e}")
|
263
|
+
except Exception:
|
264
|
+
pass
|
265
|
+
return None
|
266
|
+
|
267
|
+
async def _authenticate_api_key(self, api_key: str) -> Optional[AuthContext]:
|
268
|
+
"""Authenticate API key with Robutler Platform and merge optional owner assertion (JWT)."""
|
269
|
+
|
270
|
+
if not self.client:
|
271
|
+
self.logger.warning("Platform client not available for authentication")
|
272
|
+
return None
|
273
|
+
|
274
|
+
try:
|
275
|
+
auth_response = await self.client.validate_api_key(api_key)
|
276
|
+
|
277
|
+
if auth_response.success and auth_response.user:
|
278
|
+
# Determine scope based on user role and ownership
|
279
|
+
if auth_response.user.is_admin:
|
280
|
+
scope = AuthScope.ADMIN
|
281
|
+
elif self._is_agent_owner(auth_response.user.id):
|
282
|
+
scope = AuthScope.OWNER
|
283
|
+
self.logger.info(f"User {auth_response.user.id} is the agent owner - granting OWNER scope")
|
284
|
+
else:
|
285
|
+
scope = AuthScope.USER
|
286
|
+
|
287
|
+
auth_context = AuthContext(
|
288
|
+
user_id=getattr(auth_response.user, 'id', None),
|
289
|
+
authenticated=True,
|
290
|
+
scope=scope,
|
291
|
+
)
|
292
|
+
|
293
|
+
# Optional: verify owner assertion JWT to attach acting identity and agent binding
|
294
|
+
assertion_token = None
|
295
|
+
try:
|
296
|
+
from webagents.server.context.context_vars import get_context as _gc
|
297
|
+
ctx_for_assert = _gc()
|
298
|
+
assertion_token = self._extract_owner_assertion(ctx_for_assert) if ctx_for_assert else None
|
299
|
+
except Exception:
|
300
|
+
assertion_token = None
|
301
|
+
|
302
|
+
jwks_url = os.getenv('OWNER_ASSERTION_JWKS_URL') or f"{(self.platform_api_url or '').rstrip('/')}/api/auth/jwks"
|
303
|
+
if assertion_token and jwks_url and jose_jwt is not None:
|
304
|
+
try:
|
305
|
+
import requests
|
306
|
+
hdr = jose_jwt.get_unverified_header(assertion_token)
|
307
|
+
kid = hdr.get('kid')
|
308
|
+
r = requests.get(jwks_url, timeout=5)
|
309
|
+
r.raise_for_status()
|
310
|
+
keys = (r.json() or {}).get('keys', [])
|
311
|
+
selected_key = None
|
312
|
+
for k in keys:
|
313
|
+
if not kid or k.get('kid') == kid:
|
314
|
+
selected_key = k
|
315
|
+
break
|
316
|
+
if not selected_key and keys:
|
317
|
+
selected_key = keys[0]
|
318
|
+
if not selected_key:
|
319
|
+
raise Exception('No JWKS key available for owner assertion verification')
|
320
|
+
claims = jose_jwt.decode(
|
321
|
+
assertion_token,
|
322
|
+
selected_key,
|
323
|
+
algorithms=['RS256'],
|
324
|
+
audience=f"robutler-agent:{getattr(self.agent, 'id', '')}",
|
325
|
+
)
|
326
|
+
# Enforce agent binding if claim present
|
327
|
+
if claims.get('agent_id') and getattr(self.agent, 'id', None) and claims['agent_id'] != getattr(self.agent, 'id'):
|
328
|
+
raise Exception('Owner assertion agent_id mismatch')
|
329
|
+
# Harmonized fields
|
330
|
+
auth_context.user_id = claims.get('sub') or auth_context.user_id
|
331
|
+
auth_context.agent_id = claims.get('agent_id') or auth_context.agent_id
|
332
|
+
auth_context.assertion = claims
|
333
|
+
# Owner scope remains derived from API key user vs agent ownership
|
334
|
+
except Exception as e:
|
335
|
+
self.logger.debug(f"Owner assertion verification failed or absent: {e}")
|
336
|
+
return auth_context
|
337
|
+
else:
|
338
|
+
self.logger.warning(f"API key validation failed: {auth_response.message}")
|
339
|
+
return None
|
340
|
+
|
341
|
+
except Exception as e:
|
342
|
+
self.logger.error(f"API key authentication error: {e}")
|
343
|
+
return None
|
344
|
+
|
345
|
+
|
346
|
+
# Custom exceptions for authentication/authorization
|
347
|
+
class AuthenticationError(Exception):
|
348
|
+
"""Raised when authentication fails"""
|
349
|
+
pass
|
350
|
+
|
351
|
+
|
352
|
+
class AuthorizationError(Exception):
|
353
|
+
"""Raised when authorization fails"""
|
354
|
+
pass
|
@@ -0,0 +1,18 @@
|
|
1
|
+
"""
|
2
|
+
CRM & Analytics Skill Package - Robutler Platform Integration
|
3
|
+
|
4
|
+
CRM and analytics skill for Robutler platform.
|
5
|
+
Provides contact management and event tracking capabilities.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from .skill import (
|
9
|
+
CRMAnalyticsSkill,
|
10
|
+
Contact,
|
11
|
+
AnalyticsEvent
|
12
|
+
)
|
13
|
+
|
14
|
+
__all__ = [
|
15
|
+
"CRMAnalyticsSkill",
|
16
|
+
"Contact",
|
17
|
+
"AnalyticsEvent"
|
18
|
+
]
|