agent-portal-2 0.1.0
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.
- package/.continue/agents/new-config.yaml +22 -0
- package/AGENT_STEERING.md +36 -0
- package/ARCHITECTURE.md +13 -0
- package/CHANGELOG.md +97 -0
- package/CLI.md +38 -0
- package/CONTRIBUTING.md +55 -0
- package/INSTALLATION.md +58 -0
- package/LICENSE +60 -0
- package/PLUGIN_SYSTEM.md +33 -0
- package/PYTHON_SDK.md +22 -0
- package/QUICKSTART.md +19 -0
- package/README.md +385 -0
- package/RELEASE_NOTES_v0.1.0.md +281 -0
- package/ROADMAP.md +3 -0
- package/RUNTIME.md +44 -0
- package/SAFETY_MODEL.md +24 -0
- package/TESTING.md +35 -0
- package/TROUBLESHOOTING.md +30 -0
- package/UPGRADE_GUIDE.md +288 -0
- package/VS_CODE_EXTENSION.md +47 -0
- package/agent-portal.config.json +20 -0
- package/apps/desktop/agent-portal-desktop.zip +0 -0
- package/apps/desktop/fixtures/local-workflow.html +151 -0
- package/apps/desktop/package.json +18 -0
- package/apps/desktop/src/main.ts +117 -0
- package/apps/desktop/tsconfig.json +8 -0
- package/apps/vscode-extension/LICENSE +60 -0
- package/apps/vscode-extension/README.md +20 -0
- package/apps/vscode-extension/media/agent-portal-logo.png +0 -0
- package/apps/vscode-extension/package.json +149 -0
- package/apps/vscode-extension/src/extension.ts +614 -0
- package/apps/vscode-extension/tsconfig.json +12 -0
- package/assets/branding/agent-portal-logo.png +0 -0
- package/connectors/chatgpt-tools/README.md +9 -0
- package/connectors/claude-mcp-server/README.md +9 -0
- package/connectors/gemini-connector/README.md +9 -0
- package/connectors/rest-websocket-api/README.md +9 -0
- package/docs/MCP_SERVER.md +68 -0
- package/docs/architecture.md +214 -0
- package/docs/roadmap.md +125 -0
- package/package.json +21 -0
- package/packages/agent-portal-mcp/README.md +12 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/__init__.py +3 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/bridge/__init__.py +1 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/bridge/runtime_client.py +180 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/cli.py +32 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/doctor.py +71 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/schemas/__init__.py +1 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/schemas/actions.py +17 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/schemas/results.py +24 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/schemas/risk.py +20 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/security/__init__.py +1 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/security/policy.py +27 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/server.py +148 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/tool_registry.py +58 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/tools/__init__.py +1 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/tools/browser.py +89 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/tools/common.py +98 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/tools/inspection.py +93 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/tools/navigation.py +93 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/tools/reports.py +34 -0
- package/packages/agent-portal-mcp/agent_portal_mcp/tools/steering.py +93 -0
- package/packages/agent-portal-mcp/pyproject.toml +20 -0
- package/packages/agent-portal-mcp/tests/test_doctor.py +20 -0
- package/packages/agent-portal-mcp/tests/test_mcp_server.py +161 -0
- package/packages/core/package.json +15 -0
- package/packages/core/src/index.ts +1842 -0
- package/packages/core/tsconfig.json +8 -0
- package/packages/mcp-server/package.json +15 -0
- package/packages/mcp-server/src/index.ts +73 -0
- package/packages/mcp-server/tsconfig.json +8 -0
- package/packages/sdk/package.json +15 -0
- package/packages/sdk/src/index.ts +544 -0
- package/packages/sdk/tsconfig.json +8 -0
- package/plugins/README.md +16 -0
- package/plugins/agent-portal-browser/plugin.json +19 -0
- package/plugins/agent-portal-python/plugin.json +16 -0
- package/plugins/agent-portal-skills/plugin.json +19 -0
- package/plugins/agent-portal-vscode/plugin.json +27 -0
- package/plugins/example-runtime-plugin/README.md +3 -0
- package/plugins/example-runtime-plugin/plugin.json +20 -0
- package/plugins/plugin.schema.json +53 -0
- package/python/README.md +18 -0
- package/python/agent_portal/__init__.py +5 -0
- package/python/agent_portal/__main__.py +5 -0
- package/python/agent_portal/browser.py +393 -0
- package/python/agent_portal/cli.py +164 -0
- package/python/agent_portal/config.py +31 -0
- package/python/agent_portal/doctor.py +165 -0
- package/python/agent_portal/exceptions.py +39 -0
- package/python/agent_portal/logging_utils.py +33 -0
- package/python/agent_portal/metrics.py +309 -0
- package/python/agent_portal/models.py +160 -0
- package/python/agent_portal/plugin_system.py +42 -0
- package/python/agent_portal/rate_limit.py +253 -0
- package/python/agent_portal/runtime.py +739 -0
- package/python/agent_portal/server.py +351 -0
- package/python/agent_portal/validation.py +299 -0
- package/python/pyproject.toml +29 -0
- package/python/tests/test_config.py +24 -0
- package/python/tests/test_doctor.py +19 -0
- package/python/tests/test_metrics.py +180 -0
- package/python/tests/test_rate_limit.py +237 -0
- package/python/tests/test_runtime.py +122 -0
- package/python/tests/test_server.py +53 -0
- package/python/tests/test_validation.py +170 -0
- package/releases/desktop/agent-portal-desktop/README.md +378 -0
- package/releases/desktop/agent-portal-desktop/RELEASE_NOTES.md +14 -0
- package/releases/desktop/agent-portal-desktop/assets/branding/agent-portal-logo.png +0 -0
- package/releases/desktop/agent-portal-desktop/fixtures/local-workflow.html +151 -0
- package/releases/desktop/agent-portal-desktop/launch-agent-portal.bat +4 -0
- package/releases/desktop/agent-portal-desktop.zip +0 -0
- package/releases/python/agent_portal-0.0.2-py3-none-any.whl +0 -0
- package/releases/python/agent_portal-0.0.2.tar.gz +0 -0
- package/scripts/package_desktop.mjs +117 -0
- package/scripts/release_python.py +46 -0
- package/tests/plugin-manifest.test.mjs +26 -0
- package/tests/runtime.test.mjs +41 -0
- package/tests/vscode-extension.test.mjs +22 -0
- package/tsconfig.base.json +16 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Rate limiting and throttling for Agent Portal server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from threading import Lock
|
|
9
|
+
from typing import Callable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class RateLimitConfig:
|
|
14
|
+
"""Configuration for rate limiting."""
|
|
15
|
+
requests_per_minute: int = 60
|
|
16
|
+
requests_per_hour: int = 1000
|
|
17
|
+
burst_limit: int = 10
|
|
18
|
+
burst_window_seconds: float = 1.0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ClientInfo:
|
|
23
|
+
"""Information about a client for rate limiting."""
|
|
24
|
+
request_times: list[float]
|
|
25
|
+
burst_request_times: list[float]
|
|
26
|
+
is_blocked: bool = False
|
|
27
|
+
blocked_until: float = 0.0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class RateLimiter:
|
|
31
|
+
"""
|
|
32
|
+
Thread-safe rate limiter using sliding window algorithm.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, config: RateLimitConfig | None = None) -> None:
|
|
36
|
+
self.config = config or RateLimitConfig()
|
|
37
|
+
self.clients: dict[str, ClientInfo] = {}
|
|
38
|
+
self._lock = Lock()
|
|
39
|
+
|
|
40
|
+
def check_rate_limit(self, client_id: str) -> tuple[bool, str | None]:
|
|
41
|
+
"""
|
|
42
|
+
Check if a client is within rate limits.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
client_id: Unique identifier for the client (IP address or token)
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Tuple of (allowed, error_message)
|
|
49
|
+
"""
|
|
50
|
+
with self._lock:
|
|
51
|
+
now = time.time()
|
|
52
|
+
|
|
53
|
+
# Get or create client info
|
|
54
|
+
if client_id not in self.clients:
|
|
55
|
+
self.clients[client_id] = ClientInfo(request_times=[], burst_request_times=[])
|
|
56
|
+
|
|
57
|
+
client = self.clients[client_id]
|
|
58
|
+
|
|
59
|
+
# Check if client is currently blocked
|
|
60
|
+
if client.is_blocked:
|
|
61
|
+
if now < client.blocked_until:
|
|
62
|
+
remaining = int(client.blocked_until - now)
|
|
63
|
+
return False, f"Rate limit exceeded. Try again in {remaining} seconds."
|
|
64
|
+
else:
|
|
65
|
+
# Unblock the client
|
|
66
|
+
client.is_blocked = False
|
|
67
|
+
client.blocked_until = 0.0
|
|
68
|
+
|
|
69
|
+
# Clean up old request times (older than 1 hour)
|
|
70
|
+
one_hour_ago = now - 3600
|
|
71
|
+
client.request_times = [t for t in client.request_times if t > one_hour_ago]
|
|
72
|
+
|
|
73
|
+
# Clean up burst times
|
|
74
|
+
burst_window = now - self.config.burst_window_seconds
|
|
75
|
+
client.burst_request_times = [t for t in client.burst_request_times if t > burst_window]
|
|
76
|
+
|
|
77
|
+
# Check burst limit
|
|
78
|
+
if len(client.burst_request_times) >= self.config.burst_limit:
|
|
79
|
+
client.is_blocked = True
|
|
80
|
+
client.blocked_until = now + 60 # Block for 1 minute
|
|
81
|
+
return False, f"Burst limit exceeded ({self.config.burst_limit} requests in {self.config.burst_window_seconds}s). Blocked for 60 seconds."
|
|
82
|
+
|
|
83
|
+
# Check per-minute limit
|
|
84
|
+
one_minute_ago = now - 60
|
|
85
|
+
requests_last_minute = sum(1 for t in client.request_times if t > one_minute_ago)
|
|
86
|
+
if requests_last_minute >= self.config.requests_per_minute:
|
|
87
|
+
return False, f"Rate limit exceeded: {self.config.requests_per_minute} requests per minute."
|
|
88
|
+
|
|
89
|
+
# Check per-hour limit
|
|
90
|
+
if len(client.request_times) >= self.config.requests_per_hour:
|
|
91
|
+
return False, f"Rate limit exceeded: {self.config.requests_per_hour} requests per hour."
|
|
92
|
+
|
|
93
|
+
# Record this request
|
|
94
|
+
client.request_times.append(now)
|
|
95
|
+
client.burst_request_times.append(now)
|
|
96
|
+
|
|
97
|
+
return True, None
|
|
98
|
+
|
|
99
|
+
def record_request(self, client_id: str) -> None:
|
|
100
|
+
"""Record a request for rate limiting (should be called after successful response)."""
|
|
101
|
+
with self._lock:
|
|
102
|
+
now = time.time()
|
|
103
|
+
if client_id not in self.clients:
|
|
104
|
+
self.clients[client_id] = ClientInfo(request_times=[], burst_request_times=[])
|
|
105
|
+
|
|
106
|
+
self.clients[client_id].request_times.append(now)
|
|
107
|
+
self.clients[client_id].burst_request_times.append(now)
|
|
108
|
+
|
|
109
|
+
def reset_client(self, client_id: str) -> None:
|
|
110
|
+
"""Reset rate limit state for a specific client."""
|
|
111
|
+
with self._lock:
|
|
112
|
+
if client_id in self.clients:
|
|
113
|
+
del self.clients[client_id]
|
|
114
|
+
|
|
115
|
+
def cleanup_old_clients(self, max_age_hours: float = 24.0) -> int:
|
|
116
|
+
"""
|
|
117
|
+
Remove clients that haven't made requests in a while.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Number of clients removed
|
|
121
|
+
"""
|
|
122
|
+
with self._lock:
|
|
123
|
+
now = time.time()
|
|
124
|
+
cutoff = now - (max_age_hours * 3600)
|
|
125
|
+
|
|
126
|
+
to_remove = [
|
|
127
|
+
client_id
|
|
128
|
+
for client_id, client in self.clients.items()
|
|
129
|
+
if not client.request_times or client.request_times[-1] < cutoff
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
for client_id in to_remove:
|
|
133
|
+
del self.clients[client_id]
|
|
134
|
+
|
|
135
|
+
return len(to_remove)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class ActionThrottler:
|
|
139
|
+
"""
|
|
140
|
+
Throttles specific action types to prevent abuse.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
def __init__(self) -> None:
|
|
144
|
+
self._action_counts: dict[str, list[float]] = defaultdict(list)
|
|
145
|
+
self._lock = Lock()
|
|
146
|
+
|
|
147
|
+
# Configure throttling limits per action type
|
|
148
|
+
self._limits = {
|
|
149
|
+
"execute": {"max_per_minute": 5, "max_per_hour": 50},
|
|
150
|
+
"open_url": {"max_per_minute": 20, "max_per_hour": 200},
|
|
151
|
+
"type": {"max_per_minute": 30, "max_per_hour": 300},
|
|
152
|
+
"click": {"max_per_minute": 60, "max_per_hour": 600},
|
|
153
|
+
"screenshot": {"max_per_minute": 10, "max_per_hour": 100},
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
def check_action_allowed(
|
|
157
|
+
self,
|
|
158
|
+
action_type: str,
|
|
159
|
+
client_id: str | None = None
|
|
160
|
+
) -> tuple[bool, str | None]:
|
|
161
|
+
"""
|
|
162
|
+
Check if an action is allowed based on throttling rules.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
action_type: The type of action being performed
|
|
166
|
+
client_id: Optional client identifier for per-client limits
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Tuple of (allowed, error_message)
|
|
170
|
+
"""
|
|
171
|
+
with self._lock:
|
|
172
|
+
now = time.time()
|
|
173
|
+
key = f"{action_type}:{client_id}" if client_id else action_type
|
|
174
|
+
|
|
175
|
+
# Get or create action times list
|
|
176
|
+
if key not in self._action_counts:
|
|
177
|
+
self._action_counts[key] = []
|
|
178
|
+
|
|
179
|
+
times = self._action_counts[key]
|
|
180
|
+
|
|
181
|
+
# Clean up old times (older than 1 hour)
|
|
182
|
+
one_hour_ago = now - 3600
|
|
183
|
+
times[:] = [t for t in times if t > one_hour_ago]
|
|
184
|
+
|
|
185
|
+
# Get limits for this action type
|
|
186
|
+
limits = self._limits.get(action_type, {"max_per_minute": 60, "max_per_hour": 600})
|
|
187
|
+
|
|
188
|
+
# Check per-minute limit
|
|
189
|
+
one_minute_ago = now - 60
|
|
190
|
+
requests_last_minute = sum(1 for t in times if t > one_minute_ago)
|
|
191
|
+
if requests_last_minute >= limits["max_per_minute"]:
|
|
192
|
+
return False, f"Action '{action_type}' throttled: {limits['max_per_minute']} per minute."
|
|
193
|
+
|
|
194
|
+
# Check per-hour limit
|
|
195
|
+
if len(times) >= limits["max_per_hour"]:
|
|
196
|
+
return False, f"Action '{action_type}' throttled: {limits['max_per_hour']} per hour."
|
|
197
|
+
|
|
198
|
+
# Record this action
|
|
199
|
+
times.append(now)
|
|
200
|
+
|
|
201
|
+
return True, None
|
|
202
|
+
|
|
203
|
+
def cleanup_old_entries(self, max_age_hours: float = 24.0) -> int:
|
|
204
|
+
"""Remove old entries to prevent memory leaks."""
|
|
205
|
+
with self._lock:
|
|
206
|
+
now = time.time()
|
|
207
|
+
cutoff = now - (max_age_hours * 3600)
|
|
208
|
+
|
|
209
|
+
keys_to_remove = []
|
|
210
|
+
for key, times in self._action_counts.items():
|
|
211
|
+
times[:] = [t for t in times if t > cutoff]
|
|
212
|
+
if not times:
|
|
213
|
+
keys_to_remove.append(key)
|
|
214
|
+
|
|
215
|
+
for key in keys_to_remove:
|
|
216
|
+
del self._action_counts[key]
|
|
217
|
+
|
|
218
|
+
return len(keys_to_remove)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# Decorator for rate limiting functions
|
|
222
|
+
def rate_limited(
|
|
223
|
+
rate_limiter: RateLimiter,
|
|
224
|
+
client_id_func: Callable[..., str]
|
|
225
|
+
):
|
|
226
|
+
"""
|
|
227
|
+
Decorator to apply rate limiting to a function.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
rate_limiter: RateLimiter instance to use
|
|
231
|
+
client_id_func: Function to extract client_id from function arguments
|
|
232
|
+
"""
|
|
233
|
+
def decorator(func):
|
|
234
|
+
def wrapper(*args, **kwargs):
|
|
235
|
+
client_id = client_id_func(*args, **kwargs)
|
|
236
|
+
allowed, error = rate_limiter.check_rate_limit(client_id)
|
|
237
|
+
|
|
238
|
+
if not allowed:
|
|
239
|
+
from .exceptions import AgentPortalError
|
|
240
|
+
raise AgentPortalError(
|
|
241
|
+
error or "Rate limit exceeded",
|
|
242
|
+
module="agent_portal.rate_limit",
|
|
243
|
+
likely_cause="Too many requests from this client",
|
|
244
|
+
suggested_fix="Wait and retry, or contact administrator",
|
|
245
|
+
can_continue=True,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
result = func(*args, **kwargs)
|
|
249
|
+
rate_limiter.record_request(client_id)
|
|
250
|
+
return result
|
|
251
|
+
|
|
252
|
+
return wrapper
|
|
253
|
+
return decorator
|