webagents 0.1.0__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.
Files changed (94) hide show
  1. webagents/__init__.py +18 -0
  2. webagents/__main__.py +55 -0
  3. webagents/agents/__init__.py +13 -0
  4. webagents/agents/core/__init__.py +19 -0
  5. webagents/agents/core/base_agent.py +1834 -0
  6. webagents/agents/core/handoffs.py +293 -0
  7. webagents/agents/handoffs/__init__.py +0 -0
  8. webagents/agents/interfaces/__init__.py +0 -0
  9. webagents/agents/lifecycle/__init__.py +0 -0
  10. webagents/agents/skills/__init__.py +109 -0
  11. webagents/agents/skills/base.py +136 -0
  12. webagents/agents/skills/core/__init__.py +8 -0
  13. webagents/agents/skills/core/guardrails/__init__.py +0 -0
  14. webagents/agents/skills/core/llm/__init__.py +0 -0
  15. webagents/agents/skills/core/llm/anthropic/__init__.py +1 -0
  16. webagents/agents/skills/core/llm/litellm/__init__.py +10 -0
  17. webagents/agents/skills/core/llm/litellm/skill.py +538 -0
  18. webagents/agents/skills/core/llm/openai/__init__.py +1 -0
  19. webagents/agents/skills/core/llm/xai/__init__.py +1 -0
  20. webagents/agents/skills/core/mcp/README.md +375 -0
  21. webagents/agents/skills/core/mcp/__init__.py +15 -0
  22. webagents/agents/skills/core/mcp/skill.py +731 -0
  23. webagents/agents/skills/core/memory/__init__.py +11 -0
  24. webagents/agents/skills/core/memory/long_term_memory/__init__.py +10 -0
  25. webagents/agents/skills/core/memory/long_term_memory/memory_skill.py +639 -0
  26. webagents/agents/skills/core/memory/short_term_memory/__init__.py +9 -0
  27. webagents/agents/skills/core/memory/short_term_memory/skill.py +341 -0
  28. webagents/agents/skills/core/memory/vector_memory/skill.py +447 -0
  29. webagents/agents/skills/core/planning/__init__.py +9 -0
  30. webagents/agents/skills/core/planning/planner.py +343 -0
  31. webagents/agents/skills/ecosystem/__init__.py +0 -0
  32. webagents/agents/skills/ecosystem/crewai/__init__.py +1 -0
  33. webagents/agents/skills/ecosystem/database/__init__.py +1 -0
  34. webagents/agents/skills/ecosystem/filesystem/__init__.py +0 -0
  35. webagents/agents/skills/ecosystem/google/__init__.py +0 -0
  36. webagents/agents/skills/ecosystem/google/calendar/__init__.py +6 -0
  37. webagents/agents/skills/ecosystem/google/calendar/skill.py +306 -0
  38. webagents/agents/skills/ecosystem/n8n/__init__.py +0 -0
  39. webagents/agents/skills/ecosystem/openai_agents/__init__.py +0 -0
  40. webagents/agents/skills/ecosystem/web/__init__.py +0 -0
  41. webagents/agents/skills/ecosystem/zapier/__init__.py +0 -0
  42. webagents/agents/skills/robutler/__init__.py +11 -0
  43. webagents/agents/skills/robutler/auth/README.md +63 -0
  44. webagents/agents/skills/robutler/auth/__init__.py +17 -0
  45. webagents/agents/skills/robutler/auth/skill.py +354 -0
  46. webagents/agents/skills/robutler/crm/__init__.py +18 -0
  47. webagents/agents/skills/robutler/crm/skill.py +368 -0
  48. webagents/agents/skills/robutler/discovery/README.md +281 -0
  49. webagents/agents/skills/robutler/discovery/__init__.py +16 -0
  50. webagents/agents/skills/robutler/discovery/skill.py +230 -0
  51. webagents/agents/skills/robutler/kv/__init__.py +6 -0
  52. webagents/agents/skills/robutler/kv/skill.py +80 -0
  53. webagents/agents/skills/robutler/message_history/__init__.py +9 -0
  54. webagents/agents/skills/robutler/message_history/skill.py +270 -0
  55. webagents/agents/skills/robutler/messages/__init__.py +0 -0
  56. webagents/agents/skills/robutler/nli/__init__.py +13 -0
  57. webagents/agents/skills/robutler/nli/skill.py +687 -0
  58. webagents/agents/skills/robutler/notifications/__init__.py +5 -0
  59. webagents/agents/skills/robutler/notifications/skill.py +141 -0
  60. webagents/agents/skills/robutler/payments/__init__.py +41 -0
  61. webagents/agents/skills/robutler/payments/exceptions.py +255 -0
  62. webagents/agents/skills/robutler/payments/skill.py +610 -0
  63. webagents/agents/skills/robutler/storage/__init__.py +10 -0
  64. webagents/agents/skills/robutler/storage/files/__init__.py +9 -0
  65. webagents/agents/skills/robutler/storage/files/skill.py +445 -0
  66. webagents/agents/skills/robutler/storage/json/__init__.py +9 -0
  67. webagents/agents/skills/robutler/storage/json/skill.py +336 -0
  68. webagents/agents/skills/robutler/storage/kv/skill.py +88 -0
  69. webagents/agents/skills/robutler/storage.py +389 -0
  70. webagents/agents/tools/__init__.py +0 -0
  71. webagents/agents/tools/decorators.py +426 -0
  72. webagents/agents/tracing/__init__.py +0 -0
  73. webagents/agents/workflows/__init__.py +0 -0
  74. webagents/scripts/__init__.py +0 -0
  75. webagents/server/__init__.py +28 -0
  76. webagents/server/context/__init__.py +0 -0
  77. webagents/server/context/context_vars.py +121 -0
  78. webagents/server/core/__init__.py +0 -0
  79. webagents/server/core/app.py +843 -0
  80. webagents/server/core/middleware.py +69 -0
  81. webagents/server/core/models.py +98 -0
  82. webagents/server/core/monitoring.py +59 -0
  83. webagents/server/endpoints/__init__.py +0 -0
  84. webagents/server/interfaces/__init__.py +0 -0
  85. webagents/server/middleware.py +330 -0
  86. webagents/server/models.py +92 -0
  87. webagents/server/monitoring.py +659 -0
  88. webagents/utils/__init__.py +0 -0
  89. webagents/utils/logging.py +359 -0
  90. webagents-0.1.0.dist-info/METADATA +230 -0
  91. webagents-0.1.0.dist-info/RECORD +94 -0
  92. webagents-0.1.0.dist-info/WHEEL +4 -0
  93. webagents-0.1.0.dist-info/entry_points.txt +2 -0
  94. webagents-0.1.0.dist-info/licenses/LICENSE +20 -0
File without changes
@@ -0,0 +1,11 @@
1
+ """
2
+ WebAgents Platform Skills
3
+
4
+ Skills that integrate with WebAgents platform services.
5
+ """
6
+
7
+ from .crm import CRMAnalyticsSkill
8
+
9
+ __all__ = [
10
+ 'CRMAnalyticsSkill',
11
+ ]
@@ -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 - WebAgents V2.0
3
+
4
+ Authentication and authorization skill for WebAgents platform.
5
+ Provides user authentication, API key validation, role-based access control,
6
+ and integration with WebAgents 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 - WebAgents V2.0 Platform Integration
3
+
4
+ Authentication and authorization skill for WebAgents platform.
5
+ Integrates with WebAgents 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 robutler.api import RobutlerClient
17
+ from robutler.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 WebAgents platform
53
+
54
+ Features:
55
+ - Platform integration with WebAgents 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 WebAgents Platform client"""
86
+ from webagents.utils.logging import get_logger, log_skill_event
87
+
88
+ self.agent = agent
89
+ self.logger = get_logger('skill.webagents.auth', agent.name)
90
+
91
+ # Initialize WebAgents 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 WebAgents 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 WebAgents 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 WebAgents 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"webagents-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 WebAgents 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"webagents-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 - WebAgents Platform Integration
3
+
4
+ CRM and analytics skill for WebAgents 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
+ ]