genxai-framework 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.
- cli/__init__.py +3 -0
- cli/commands/__init__.py +6 -0
- cli/commands/approval.py +85 -0
- cli/commands/audit.py +127 -0
- cli/commands/metrics.py +25 -0
- cli/commands/tool.py +389 -0
- cli/main.py +32 -0
- genxai/__init__.py +81 -0
- genxai/api/__init__.py +5 -0
- genxai/api/app.py +21 -0
- genxai/config/__init__.py +5 -0
- genxai/config/settings.py +37 -0
- genxai/connectors/__init__.py +19 -0
- genxai/connectors/base.py +122 -0
- genxai/connectors/kafka.py +92 -0
- genxai/connectors/postgres_cdc.py +95 -0
- genxai/connectors/registry.py +44 -0
- genxai/connectors/sqs.py +94 -0
- genxai/connectors/webhook.py +73 -0
- genxai/core/__init__.py +37 -0
- genxai/core/agent/__init__.py +32 -0
- genxai/core/agent/base.py +206 -0
- genxai/core/agent/config_io.py +59 -0
- genxai/core/agent/registry.py +98 -0
- genxai/core/agent/runtime.py +970 -0
- genxai/core/communication/__init__.py +6 -0
- genxai/core/communication/collaboration.py +44 -0
- genxai/core/communication/message_bus.py +192 -0
- genxai/core/communication/protocols.py +35 -0
- genxai/core/execution/__init__.py +22 -0
- genxai/core/execution/metadata.py +181 -0
- genxai/core/execution/queue.py +201 -0
- genxai/core/graph/__init__.py +30 -0
- genxai/core/graph/checkpoints.py +77 -0
- genxai/core/graph/edges.py +131 -0
- genxai/core/graph/engine.py +813 -0
- genxai/core/graph/executor.py +516 -0
- genxai/core/graph/nodes.py +161 -0
- genxai/core/graph/trigger_runner.py +40 -0
- genxai/core/memory/__init__.py +19 -0
- genxai/core/memory/base.py +72 -0
- genxai/core/memory/embedding.py +327 -0
- genxai/core/memory/episodic.py +448 -0
- genxai/core/memory/long_term.py +467 -0
- genxai/core/memory/manager.py +543 -0
- genxai/core/memory/persistence.py +297 -0
- genxai/core/memory/procedural.py +461 -0
- genxai/core/memory/semantic.py +526 -0
- genxai/core/memory/shared.py +62 -0
- genxai/core/memory/short_term.py +303 -0
- genxai/core/memory/vector_store.py +508 -0
- genxai/core/memory/working.py +211 -0
- genxai/core/state/__init__.py +6 -0
- genxai/core/state/manager.py +293 -0
- genxai/core/state/schema.py +115 -0
- genxai/llm/__init__.py +14 -0
- genxai/llm/base.py +150 -0
- genxai/llm/factory.py +329 -0
- genxai/llm/providers/__init__.py +1 -0
- genxai/llm/providers/anthropic.py +249 -0
- genxai/llm/providers/cohere.py +274 -0
- genxai/llm/providers/google.py +334 -0
- genxai/llm/providers/ollama.py +147 -0
- genxai/llm/providers/openai.py +257 -0
- genxai/llm/routing.py +83 -0
- genxai/observability/__init__.py +6 -0
- genxai/observability/logging.py +327 -0
- genxai/observability/metrics.py +494 -0
- genxai/observability/tracing.py +372 -0
- genxai/performance/__init__.py +39 -0
- genxai/performance/cache.py +256 -0
- genxai/performance/pooling.py +289 -0
- genxai/security/audit.py +304 -0
- genxai/security/auth.py +315 -0
- genxai/security/cost_control.py +528 -0
- genxai/security/default_policies.py +44 -0
- genxai/security/jwt.py +142 -0
- genxai/security/oauth.py +226 -0
- genxai/security/pii.py +366 -0
- genxai/security/policy_engine.py +82 -0
- genxai/security/rate_limit.py +341 -0
- genxai/security/rbac.py +247 -0
- genxai/security/validation.py +218 -0
- genxai/tools/__init__.py +21 -0
- genxai/tools/base.py +383 -0
- genxai/tools/builtin/__init__.py +131 -0
- genxai/tools/builtin/communication/__init__.py +15 -0
- genxai/tools/builtin/communication/email_sender.py +159 -0
- genxai/tools/builtin/communication/notification_manager.py +167 -0
- genxai/tools/builtin/communication/slack_notifier.py +118 -0
- genxai/tools/builtin/communication/sms_sender.py +118 -0
- genxai/tools/builtin/communication/webhook_caller.py +136 -0
- genxai/tools/builtin/computation/__init__.py +15 -0
- genxai/tools/builtin/computation/calculator.py +101 -0
- genxai/tools/builtin/computation/code_executor.py +183 -0
- genxai/tools/builtin/computation/data_validator.py +259 -0
- genxai/tools/builtin/computation/hash_generator.py +129 -0
- genxai/tools/builtin/computation/regex_matcher.py +201 -0
- genxai/tools/builtin/data/__init__.py +15 -0
- genxai/tools/builtin/data/csv_processor.py +213 -0
- genxai/tools/builtin/data/data_transformer.py +299 -0
- genxai/tools/builtin/data/json_processor.py +233 -0
- genxai/tools/builtin/data/text_analyzer.py +288 -0
- genxai/tools/builtin/data/xml_processor.py +175 -0
- genxai/tools/builtin/database/__init__.py +15 -0
- genxai/tools/builtin/database/database_inspector.py +157 -0
- genxai/tools/builtin/database/mongodb_query.py +196 -0
- genxai/tools/builtin/database/redis_cache.py +167 -0
- genxai/tools/builtin/database/sql_query.py +145 -0
- genxai/tools/builtin/database/vector_search.py +163 -0
- genxai/tools/builtin/file/__init__.py +17 -0
- genxai/tools/builtin/file/directory_scanner.py +214 -0
- genxai/tools/builtin/file/file_compressor.py +237 -0
- genxai/tools/builtin/file/file_reader.py +102 -0
- genxai/tools/builtin/file/file_writer.py +122 -0
- genxai/tools/builtin/file/image_processor.py +186 -0
- genxai/tools/builtin/file/pdf_parser.py +144 -0
- genxai/tools/builtin/test/__init__.py +15 -0
- genxai/tools/builtin/test/async_simulator.py +62 -0
- genxai/tools/builtin/test/data_transformer.py +99 -0
- genxai/tools/builtin/test/error_generator.py +82 -0
- genxai/tools/builtin/test/simple_math.py +94 -0
- genxai/tools/builtin/test/string_processor.py +72 -0
- genxai/tools/builtin/web/__init__.py +15 -0
- genxai/tools/builtin/web/api_caller.py +161 -0
- genxai/tools/builtin/web/html_parser.py +330 -0
- genxai/tools/builtin/web/http_client.py +187 -0
- genxai/tools/builtin/web/url_validator.py +162 -0
- genxai/tools/builtin/web/web_scraper.py +170 -0
- genxai/tools/custom/my_test_tool_2.py +9 -0
- genxai/tools/dynamic.py +105 -0
- genxai/tools/mcp_server.py +167 -0
- genxai/tools/persistence/__init__.py +6 -0
- genxai/tools/persistence/models.py +55 -0
- genxai/tools/persistence/service.py +322 -0
- genxai/tools/registry.py +227 -0
- genxai/tools/security/__init__.py +11 -0
- genxai/tools/security/limits.py +214 -0
- genxai/tools/security/policy.py +20 -0
- genxai/tools/security/sandbox.py +248 -0
- genxai/tools/templates.py +435 -0
- genxai/triggers/__init__.py +19 -0
- genxai/triggers/base.py +104 -0
- genxai/triggers/file_watcher.py +75 -0
- genxai/triggers/queue.py +68 -0
- genxai/triggers/registry.py +82 -0
- genxai/triggers/schedule.py +66 -0
- genxai/triggers/webhook.py +68 -0
- genxai/utils/__init__.py +1 -0
- genxai/utils/tokens.py +295 -0
- genxai_framework-0.1.0.dist-info/METADATA +495 -0
- genxai_framework-0.1.0.dist-info/RECORD +156 -0
- genxai_framework-0.1.0.dist-info/WHEEL +5 -0
- genxai_framework-0.1.0.dist-info/entry_points.txt +2 -0
- genxai_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
- genxai_framework-0.1.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""Rate limiting for GenXAI using token bucket algorithm."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import asyncio
|
|
5
|
+
from typing import Optional, Dict
|
|
6
|
+
from functools import wraps
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class RateLimitConfig:
|
|
13
|
+
"""Rate limit configuration."""
|
|
14
|
+
requests_per_minute: int
|
|
15
|
+
requests_per_hour: int
|
|
16
|
+
requests_per_day: int
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Rate limit tiers
|
|
20
|
+
RATE_LIMITS = {
|
|
21
|
+
"free": RateLimitConfig(
|
|
22
|
+
requests_per_minute=10,
|
|
23
|
+
requests_per_hour=100,
|
|
24
|
+
requests_per_day=1000,
|
|
25
|
+
),
|
|
26
|
+
"pro": RateLimitConfig(
|
|
27
|
+
requests_per_minute=60,
|
|
28
|
+
requests_per_hour=1000,
|
|
29
|
+
requests_per_day=10000,
|
|
30
|
+
),
|
|
31
|
+
"enterprise": RateLimitConfig(
|
|
32
|
+
requests_per_minute=300,
|
|
33
|
+
requests_per_hour=10000,
|
|
34
|
+
requests_per_day=100000,
|
|
35
|
+
),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TokenBucket:
|
|
40
|
+
"""Token bucket for rate limiting."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, rate: float, capacity: int):
|
|
43
|
+
"""Initialize token bucket.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
rate: Tokens per second
|
|
47
|
+
capacity: Bucket capacity
|
|
48
|
+
"""
|
|
49
|
+
self.rate = rate
|
|
50
|
+
self.capacity = capacity
|
|
51
|
+
self.tokens = capacity
|
|
52
|
+
self.last_update = time.time()
|
|
53
|
+
self.lock = asyncio.Lock()
|
|
54
|
+
|
|
55
|
+
async def consume(self, tokens: int = 1) -> bool:
|
|
56
|
+
"""Consume tokens from bucket.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
tokens: Number of tokens to consume
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
True if tokens consumed, False if rate limited
|
|
63
|
+
"""
|
|
64
|
+
async with self.lock:
|
|
65
|
+
now = time.time()
|
|
66
|
+
elapsed = now - self.last_update
|
|
67
|
+
|
|
68
|
+
# Add tokens based on elapsed time
|
|
69
|
+
self.tokens = min(
|
|
70
|
+
self.capacity,
|
|
71
|
+
self.tokens + elapsed * self.rate
|
|
72
|
+
)
|
|
73
|
+
self.last_update = now
|
|
74
|
+
|
|
75
|
+
# Check if enough tokens
|
|
76
|
+
if self.tokens >= tokens:
|
|
77
|
+
self.tokens -= tokens
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
async def get_remaining(self) -> int:
|
|
83
|
+
"""Get remaining tokens.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Number of remaining tokens
|
|
87
|
+
"""
|
|
88
|
+
async with self.lock:
|
|
89
|
+
now = time.time()
|
|
90
|
+
elapsed = now - self.last_update
|
|
91
|
+
|
|
92
|
+
tokens = min(
|
|
93
|
+
self.capacity,
|
|
94
|
+
self.tokens + elapsed * self.rate
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return int(tokens)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class RateLimiter:
|
|
101
|
+
"""Rate limiter using token bucket algorithm."""
|
|
102
|
+
|
|
103
|
+
def __init__(self, storage: str = "memory"):
|
|
104
|
+
"""Initialize rate limiter.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
storage: Storage backend (memory or redis)
|
|
108
|
+
"""
|
|
109
|
+
self.storage = storage
|
|
110
|
+
self.buckets: Dict[str, Dict[str, TokenBucket]] = {}
|
|
111
|
+
|
|
112
|
+
# Try to import Redis if using redis storage
|
|
113
|
+
if storage == "redis":
|
|
114
|
+
try:
|
|
115
|
+
import redis
|
|
116
|
+
redis_url = os.getenv("REDIS_URL", "redis://localhost:6379")
|
|
117
|
+
self.redis_client = redis.from_url(redis_url)
|
|
118
|
+
except ImportError:
|
|
119
|
+
print("Redis not available, falling back to memory storage")
|
|
120
|
+
self.storage = "memory"
|
|
121
|
+
|
|
122
|
+
async def check_rate_limit(
|
|
123
|
+
self,
|
|
124
|
+
key: str,
|
|
125
|
+
tier: str = "free",
|
|
126
|
+
cost: int = 1
|
|
127
|
+
) -> bool:
|
|
128
|
+
"""Check if request is within rate limit.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
key: User ID or API key
|
|
132
|
+
tier: Rate limit tier (free, pro, enterprise)
|
|
133
|
+
cost: Cost in tokens (default: 1)
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
True if within limit, False if rate limited
|
|
137
|
+
"""
|
|
138
|
+
config = RATE_LIMITS.get(tier, RATE_LIMITS["free"])
|
|
139
|
+
|
|
140
|
+
# Check minute limit
|
|
141
|
+
minute_key = f"{key}:minute"
|
|
142
|
+
if not await self._check_bucket(
|
|
143
|
+
minute_key,
|
|
144
|
+
rate=config.requests_per_minute / 60,
|
|
145
|
+
capacity=config.requests_per_minute,
|
|
146
|
+
cost=cost
|
|
147
|
+
):
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
# Check hour limit
|
|
151
|
+
hour_key = f"{key}:hour"
|
|
152
|
+
if not await self._check_bucket(
|
|
153
|
+
hour_key,
|
|
154
|
+
rate=config.requests_per_hour / 3600,
|
|
155
|
+
capacity=config.requests_per_hour,
|
|
156
|
+
cost=cost
|
|
157
|
+
):
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
# Check day limit
|
|
161
|
+
day_key = f"{key}:day"
|
|
162
|
+
if not await self._check_bucket(
|
|
163
|
+
day_key,
|
|
164
|
+
rate=config.requests_per_day / 86400,
|
|
165
|
+
capacity=config.requests_per_day,
|
|
166
|
+
cost=cost
|
|
167
|
+
):
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
return True
|
|
171
|
+
|
|
172
|
+
async def _check_bucket(
|
|
173
|
+
self,
|
|
174
|
+
key: str,
|
|
175
|
+
rate: float,
|
|
176
|
+
capacity: int,
|
|
177
|
+
cost: int
|
|
178
|
+
) -> bool:
|
|
179
|
+
"""Check token bucket.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
key: Bucket key
|
|
183
|
+
rate: Tokens per second
|
|
184
|
+
capacity: Bucket capacity
|
|
185
|
+
cost: Tokens to consume
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
True if tokens consumed, False if rate limited
|
|
189
|
+
"""
|
|
190
|
+
if self.storage == "memory":
|
|
191
|
+
# Get or create bucket
|
|
192
|
+
if key not in self.buckets:
|
|
193
|
+
self.buckets[key] = {}
|
|
194
|
+
|
|
195
|
+
if "bucket" not in self.buckets[key]:
|
|
196
|
+
self.buckets[key]["bucket"] = TokenBucket(rate, capacity)
|
|
197
|
+
|
|
198
|
+
bucket = self.buckets[key]["bucket"]
|
|
199
|
+
return await bucket.consume(cost)
|
|
200
|
+
|
|
201
|
+
elif self.storage == "redis":
|
|
202
|
+
# Redis-based rate limiting using Lua script
|
|
203
|
+
# This is a simplified version
|
|
204
|
+
return await self._check_redis_bucket(key, rate, capacity, cost)
|
|
205
|
+
|
|
206
|
+
return True
|
|
207
|
+
|
|
208
|
+
async def _check_redis_bucket(
|
|
209
|
+
self,
|
|
210
|
+
key: str,
|
|
211
|
+
rate: float,
|
|
212
|
+
capacity: int,
|
|
213
|
+
cost: int
|
|
214
|
+
) -> bool:
|
|
215
|
+
"""Check rate limit using Redis.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
key: Bucket key
|
|
219
|
+
rate: Tokens per second
|
|
220
|
+
capacity: Bucket capacity
|
|
221
|
+
cost: Tokens to consume
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
True if within limit, False if rate limited
|
|
225
|
+
"""
|
|
226
|
+
# Simplified Redis implementation
|
|
227
|
+
# In production, use a Lua script for atomicity
|
|
228
|
+
try:
|
|
229
|
+
current = self.redis_client.get(key)
|
|
230
|
+
if current is None:
|
|
231
|
+
self.redis_client.setex(key, 60, capacity - cost)
|
|
232
|
+
return True
|
|
233
|
+
|
|
234
|
+
current = int(current)
|
|
235
|
+
if current >= cost:
|
|
236
|
+
self.redis_client.decrby(key, cost)
|
|
237
|
+
return True
|
|
238
|
+
|
|
239
|
+
return False
|
|
240
|
+
except Exception:
|
|
241
|
+
# Fallback to allowing request if Redis fails
|
|
242
|
+
return True
|
|
243
|
+
|
|
244
|
+
async def get_remaining(self, key: str, tier: str = "free") -> Dict[str, int]:
|
|
245
|
+
"""Get remaining requests.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
key: User ID or API key
|
|
249
|
+
tier: Rate limit tier
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Dictionary with remaining requests per period
|
|
253
|
+
"""
|
|
254
|
+
config = RATE_LIMITS.get(tier, RATE_LIMITS["free"])
|
|
255
|
+
|
|
256
|
+
minute_key = f"{key}:minute"
|
|
257
|
+
hour_key = f"{key}:hour"
|
|
258
|
+
day_key = f"{key}:day"
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
"minute": await self._get_bucket_remaining(minute_key),
|
|
262
|
+
"hour": await self._get_bucket_remaining(hour_key),
|
|
263
|
+
"day": await self._get_bucket_remaining(day_key),
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async def _get_bucket_remaining(self, key: str) -> int:
|
|
267
|
+
"""Get remaining tokens in bucket.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
key: Bucket key
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Remaining tokens
|
|
274
|
+
"""
|
|
275
|
+
if self.storage == "memory":
|
|
276
|
+
if key in self.buckets and "bucket" in self.buckets[key]:
|
|
277
|
+
return await self.buckets[key]["bucket"].get_remaining()
|
|
278
|
+
return 0
|
|
279
|
+
|
|
280
|
+
elif self.storage == "redis":
|
|
281
|
+
try:
|
|
282
|
+
current = self.redis_client.get(key)
|
|
283
|
+
return int(current) if current else 0
|
|
284
|
+
except Exception:
|
|
285
|
+
return 0
|
|
286
|
+
|
|
287
|
+
return 0
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class RateLimitExceeded(Exception):
|
|
291
|
+
"""Rate limit exceeded exception."""
|
|
292
|
+
pass
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# Global rate limiter
|
|
296
|
+
_rate_limiter = None
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def get_rate_limiter() -> RateLimiter:
|
|
300
|
+
"""Get global rate limiter.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
RateLimiter instance
|
|
304
|
+
"""
|
|
305
|
+
global _rate_limiter
|
|
306
|
+
|
|
307
|
+
if _rate_limiter is None:
|
|
308
|
+
storage = os.getenv("RATE_LIMIT_STORAGE", "memory")
|
|
309
|
+
_rate_limiter = RateLimiter(storage)
|
|
310
|
+
|
|
311
|
+
return _rate_limiter
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def rate_limit(tier: str = "free", cost: int = 1):
|
|
315
|
+
"""Decorator for rate limiting.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
tier: Rate limit tier
|
|
319
|
+
cost: Cost in tokens
|
|
320
|
+
|
|
321
|
+
Usage:
|
|
322
|
+
@rate_limit(tier="pro", cost=1)
|
|
323
|
+
async def my_endpoint():
|
|
324
|
+
pass
|
|
325
|
+
"""
|
|
326
|
+
def decorator(func):
|
|
327
|
+
@wraps(func)
|
|
328
|
+
async def wrapper(*args, **kwargs):
|
|
329
|
+
# Get user key from kwargs or context
|
|
330
|
+
user_id = kwargs.get("user_id", "anonymous")
|
|
331
|
+
|
|
332
|
+
# Check rate limit
|
|
333
|
+
limiter = get_rate_limiter()
|
|
334
|
+
if not await limiter.check_rate_limit(user_id, tier, cost):
|
|
335
|
+
raise RateLimitExceeded(f"Rate limit exceeded for tier: {tier}")
|
|
336
|
+
|
|
337
|
+
return await func(*args, **kwargs)
|
|
338
|
+
|
|
339
|
+
return wrapper
|
|
340
|
+
|
|
341
|
+
return decorator
|
genxai/security/rbac.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""Role-Based Access Control (RBAC) for GenXAI."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import List, Set, Optional
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Role(Enum):
|
|
10
|
+
"""User roles."""
|
|
11
|
+
ADMIN = "admin"
|
|
12
|
+
DEVELOPER = "developer"
|
|
13
|
+
OPERATOR = "operator"
|
|
14
|
+
VIEWER = "viewer"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Permission(Enum):
|
|
18
|
+
"""System permissions."""
|
|
19
|
+
# Agent permissions
|
|
20
|
+
AGENT_CREATE = "agent:create"
|
|
21
|
+
AGENT_READ = "agent:read"
|
|
22
|
+
AGENT_UPDATE = "agent:update"
|
|
23
|
+
AGENT_DELETE = "agent:delete"
|
|
24
|
+
AGENT_EXECUTE = "agent:execute"
|
|
25
|
+
|
|
26
|
+
# Workflow permissions
|
|
27
|
+
WORKFLOW_CREATE = "workflow:create"
|
|
28
|
+
WORKFLOW_READ = "workflow:read"
|
|
29
|
+
WORKFLOW_UPDATE = "workflow:update"
|
|
30
|
+
WORKFLOW_DELETE = "workflow:delete"
|
|
31
|
+
WORKFLOW_EXECUTE = "workflow:execute"
|
|
32
|
+
|
|
33
|
+
# Tool permissions
|
|
34
|
+
TOOL_CREATE = "tool:create"
|
|
35
|
+
TOOL_READ = "tool:read"
|
|
36
|
+
TOOL_UPDATE = "tool:update"
|
|
37
|
+
TOOL_DELETE = "tool:delete"
|
|
38
|
+
TOOL_EXECUTE = "tool:execute"
|
|
39
|
+
|
|
40
|
+
# Memory permissions
|
|
41
|
+
MEMORY_READ = "memory:read"
|
|
42
|
+
MEMORY_WRITE = "memory:write"
|
|
43
|
+
MEMORY_DELETE = "memory:delete"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Role-Permission mapping
|
|
47
|
+
ROLE_PERMISSIONS: dict[Role, Set[Permission]] = {
|
|
48
|
+
Role.ADMIN: set(Permission), # All permissions
|
|
49
|
+
|
|
50
|
+
Role.DEVELOPER: {
|
|
51
|
+
Permission.AGENT_CREATE,
|
|
52
|
+
Permission.AGENT_READ,
|
|
53
|
+
Permission.AGENT_UPDATE,
|
|
54
|
+
Permission.AGENT_EXECUTE,
|
|
55
|
+
Permission.WORKFLOW_CREATE,
|
|
56
|
+
Permission.WORKFLOW_READ,
|
|
57
|
+
Permission.WORKFLOW_UPDATE,
|
|
58
|
+
Permission.WORKFLOW_EXECUTE,
|
|
59
|
+
Permission.TOOL_READ,
|
|
60
|
+
Permission.TOOL_EXECUTE,
|
|
61
|
+
Permission.MEMORY_READ,
|
|
62
|
+
Permission.MEMORY_WRITE,
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
Role.OPERATOR: {
|
|
66
|
+
Permission.AGENT_READ,
|
|
67
|
+
Permission.AGENT_EXECUTE,
|
|
68
|
+
Permission.WORKFLOW_READ,
|
|
69
|
+
Permission.WORKFLOW_EXECUTE,
|
|
70
|
+
Permission.TOOL_READ,
|
|
71
|
+
Permission.TOOL_EXECUTE,
|
|
72
|
+
Permission.MEMORY_READ,
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
Role.VIEWER: {
|
|
76
|
+
Permission.AGENT_READ,
|
|
77
|
+
Permission.WORKFLOW_READ,
|
|
78
|
+
Permission.TOOL_READ,
|
|
79
|
+
Permission.MEMORY_READ,
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class User:
|
|
86
|
+
"""User model."""
|
|
87
|
+
user_id: str
|
|
88
|
+
role: Role
|
|
89
|
+
|
|
90
|
+
def has_permission(self, permission: Permission) -> bool:
|
|
91
|
+
"""Check if user has permission.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
permission: Permission to check
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
True if user has permission
|
|
98
|
+
"""
|
|
99
|
+
return permission in ROLE_PERMISSIONS[self.role]
|
|
100
|
+
|
|
101
|
+
def has_any_permission(self, permissions: List[Permission]) -> bool:
|
|
102
|
+
"""Check if user has any of the permissions.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
permissions: List of permissions
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
True if user has any permission
|
|
109
|
+
"""
|
|
110
|
+
user_permissions = ROLE_PERMISSIONS[self.role]
|
|
111
|
+
return any(p in user_permissions for p in permissions)
|
|
112
|
+
|
|
113
|
+
def has_all_permissions(self, permissions: List[Permission]) -> bool:
|
|
114
|
+
"""Check if user has all permissions.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
permissions: List of permissions
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
True if user has all permissions
|
|
121
|
+
"""
|
|
122
|
+
user_permissions = ROLE_PERMISSIONS[self.role]
|
|
123
|
+
return all(p in user_permissions for p in permissions)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class PermissionDenied(Exception):
|
|
127
|
+
"""Permission denied exception."""
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# Current user context (thread-local in production)
|
|
132
|
+
_current_user: Optional[User] = None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def set_current_user(user: User):
|
|
136
|
+
"""Set current user.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
user: User object
|
|
140
|
+
"""
|
|
141
|
+
global _current_user
|
|
142
|
+
_current_user = user
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def get_current_user() -> Optional[User]:
|
|
146
|
+
"""Get current user.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Current user or None
|
|
150
|
+
"""
|
|
151
|
+
return _current_user
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def require_permission(permission: Permission):
|
|
155
|
+
"""Decorator to require permission.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
permission: Required permission
|
|
159
|
+
|
|
160
|
+
Usage:
|
|
161
|
+
@require_permission(Permission.AGENT_EXECUTE)
|
|
162
|
+
async def execute_agent():
|
|
163
|
+
pass
|
|
164
|
+
"""
|
|
165
|
+
def decorator(func):
|
|
166
|
+
@wraps(func)
|
|
167
|
+
async def async_wrapper(*args, **kwargs):
|
|
168
|
+
user = get_current_user()
|
|
169
|
+
if not user:
|
|
170
|
+
raise PermissionDenied("No user context")
|
|
171
|
+
|
|
172
|
+
if not user.has_permission(permission):
|
|
173
|
+
raise PermissionDenied(
|
|
174
|
+
f"User {user.user_id} missing permission: {permission.value}"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return await func(*args, **kwargs)
|
|
178
|
+
|
|
179
|
+
@wraps(func)
|
|
180
|
+
def sync_wrapper(*args, **kwargs):
|
|
181
|
+
user = get_current_user()
|
|
182
|
+
if not user:
|
|
183
|
+
raise PermissionDenied("No user context")
|
|
184
|
+
|
|
185
|
+
if not user.has_permission(permission):
|
|
186
|
+
raise PermissionDenied(
|
|
187
|
+
f"User {user.user_id} missing permission: {permission.value}"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return func(*args, **kwargs)
|
|
191
|
+
|
|
192
|
+
# Return appropriate wrapper
|
|
193
|
+
import asyncio
|
|
194
|
+
if asyncio.iscoroutinefunction(func):
|
|
195
|
+
return async_wrapper
|
|
196
|
+
else:
|
|
197
|
+
return sync_wrapper
|
|
198
|
+
|
|
199
|
+
return decorator
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def require_role(role: Role):
|
|
203
|
+
"""Decorator to require specific role.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
role: Required role
|
|
207
|
+
|
|
208
|
+
Usage:
|
|
209
|
+
@require_role(Role.ADMIN)
|
|
210
|
+
async def admin_function():
|
|
211
|
+
pass
|
|
212
|
+
"""
|
|
213
|
+
def decorator(func):
|
|
214
|
+
@wraps(func)
|
|
215
|
+
async def async_wrapper(*args, **kwargs):
|
|
216
|
+
user = get_current_user()
|
|
217
|
+
if not user:
|
|
218
|
+
raise PermissionDenied("No user context")
|
|
219
|
+
|
|
220
|
+
if user.role != role:
|
|
221
|
+
raise PermissionDenied(
|
|
222
|
+
f"User {user.user_id} requires role: {role.value}"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
return await func(*args, **kwargs)
|
|
226
|
+
|
|
227
|
+
@wraps(func)
|
|
228
|
+
def sync_wrapper(*args, **kwargs):
|
|
229
|
+
user = get_current_user()
|
|
230
|
+
if not user:
|
|
231
|
+
raise PermissionDenied("No user context")
|
|
232
|
+
|
|
233
|
+
if user.role != role:
|
|
234
|
+
raise PermissionDenied(
|
|
235
|
+
f"User {user.user_id} requires role: {role.value}"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
return func(*args, **kwargs)
|
|
239
|
+
|
|
240
|
+
# Return appropriate wrapper
|
|
241
|
+
import asyncio
|
|
242
|
+
if asyncio.iscoroutinefunction(func):
|
|
243
|
+
return async_wrapper
|
|
244
|
+
else:
|
|
245
|
+
return sync_wrapper
|
|
246
|
+
|
|
247
|
+
return decorator
|