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.
Files changed (120) hide show
  1. package/.continue/agents/new-config.yaml +22 -0
  2. package/AGENT_STEERING.md +36 -0
  3. package/ARCHITECTURE.md +13 -0
  4. package/CHANGELOG.md +97 -0
  5. package/CLI.md +38 -0
  6. package/CONTRIBUTING.md +55 -0
  7. package/INSTALLATION.md +58 -0
  8. package/LICENSE +60 -0
  9. package/PLUGIN_SYSTEM.md +33 -0
  10. package/PYTHON_SDK.md +22 -0
  11. package/QUICKSTART.md +19 -0
  12. package/README.md +385 -0
  13. package/RELEASE_NOTES_v0.1.0.md +281 -0
  14. package/ROADMAP.md +3 -0
  15. package/RUNTIME.md +44 -0
  16. package/SAFETY_MODEL.md +24 -0
  17. package/TESTING.md +35 -0
  18. package/TROUBLESHOOTING.md +30 -0
  19. package/UPGRADE_GUIDE.md +288 -0
  20. package/VS_CODE_EXTENSION.md +47 -0
  21. package/agent-portal.config.json +20 -0
  22. package/apps/desktop/agent-portal-desktop.zip +0 -0
  23. package/apps/desktop/fixtures/local-workflow.html +151 -0
  24. package/apps/desktop/package.json +18 -0
  25. package/apps/desktop/src/main.ts +117 -0
  26. package/apps/desktop/tsconfig.json +8 -0
  27. package/apps/vscode-extension/LICENSE +60 -0
  28. package/apps/vscode-extension/README.md +20 -0
  29. package/apps/vscode-extension/media/agent-portal-logo.png +0 -0
  30. package/apps/vscode-extension/package.json +149 -0
  31. package/apps/vscode-extension/src/extension.ts +614 -0
  32. package/apps/vscode-extension/tsconfig.json +12 -0
  33. package/assets/branding/agent-portal-logo.png +0 -0
  34. package/connectors/chatgpt-tools/README.md +9 -0
  35. package/connectors/claude-mcp-server/README.md +9 -0
  36. package/connectors/gemini-connector/README.md +9 -0
  37. package/connectors/rest-websocket-api/README.md +9 -0
  38. package/docs/MCP_SERVER.md +68 -0
  39. package/docs/architecture.md +214 -0
  40. package/docs/roadmap.md +125 -0
  41. package/package.json +21 -0
  42. package/packages/agent-portal-mcp/README.md +12 -0
  43. package/packages/agent-portal-mcp/agent_portal_mcp/__init__.py +3 -0
  44. package/packages/agent-portal-mcp/agent_portal_mcp/bridge/__init__.py +1 -0
  45. package/packages/agent-portal-mcp/agent_portal_mcp/bridge/runtime_client.py +180 -0
  46. package/packages/agent-portal-mcp/agent_portal_mcp/cli.py +32 -0
  47. package/packages/agent-portal-mcp/agent_portal_mcp/doctor.py +71 -0
  48. package/packages/agent-portal-mcp/agent_portal_mcp/schemas/__init__.py +1 -0
  49. package/packages/agent-portal-mcp/agent_portal_mcp/schemas/actions.py +17 -0
  50. package/packages/agent-portal-mcp/agent_portal_mcp/schemas/results.py +24 -0
  51. package/packages/agent-portal-mcp/agent_portal_mcp/schemas/risk.py +20 -0
  52. package/packages/agent-portal-mcp/agent_portal_mcp/security/__init__.py +1 -0
  53. package/packages/agent-portal-mcp/agent_portal_mcp/security/policy.py +27 -0
  54. package/packages/agent-portal-mcp/agent_portal_mcp/server.py +148 -0
  55. package/packages/agent-portal-mcp/agent_portal_mcp/tool_registry.py +58 -0
  56. package/packages/agent-portal-mcp/agent_portal_mcp/tools/__init__.py +1 -0
  57. package/packages/agent-portal-mcp/agent_portal_mcp/tools/browser.py +89 -0
  58. package/packages/agent-portal-mcp/agent_portal_mcp/tools/common.py +98 -0
  59. package/packages/agent-portal-mcp/agent_portal_mcp/tools/inspection.py +93 -0
  60. package/packages/agent-portal-mcp/agent_portal_mcp/tools/navigation.py +93 -0
  61. package/packages/agent-portal-mcp/agent_portal_mcp/tools/reports.py +34 -0
  62. package/packages/agent-portal-mcp/agent_portal_mcp/tools/steering.py +93 -0
  63. package/packages/agent-portal-mcp/pyproject.toml +20 -0
  64. package/packages/agent-portal-mcp/tests/test_doctor.py +20 -0
  65. package/packages/agent-portal-mcp/tests/test_mcp_server.py +161 -0
  66. package/packages/core/package.json +15 -0
  67. package/packages/core/src/index.ts +1842 -0
  68. package/packages/core/tsconfig.json +8 -0
  69. package/packages/mcp-server/package.json +15 -0
  70. package/packages/mcp-server/src/index.ts +73 -0
  71. package/packages/mcp-server/tsconfig.json +8 -0
  72. package/packages/sdk/package.json +15 -0
  73. package/packages/sdk/src/index.ts +544 -0
  74. package/packages/sdk/tsconfig.json +8 -0
  75. package/plugins/README.md +16 -0
  76. package/plugins/agent-portal-browser/plugin.json +19 -0
  77. package/plugins/agent-portal-python/plugin.json +16 -0
  78. package/plugins/agent-portal-skills/plugin.json +19 -0
  79. package/plugins/agent-portal-vscode/plugin.json +27 -0
  80. package/plugins/example-runtime-plugin/README.md +3 -0
  81. package/plugins/example-runtime-plugin/plugin.json +20 -0
  82. package/plugins/plugin.schema.json +53 -0
  83. package/python/README.md +18 -0
  84. package/python/agent_portal/__init__.py +5 -0
  85. package/python/agent_portal/__main__.py +5 -0
  86. package/python/agent_portal/browser.py +393 -0
  87. package/python/agent_portal/cli.py +164 -0
  88. package/python/agent_portal/config.py +31 -0
  89. package/python/agent_portal/doctor.py +165 -0
  90. package/python/agent_portal/exceptions.py +39 -0
  91. package/python/agent_portal/logging_utils.py +33 -0
  92. package/python/agent_portal/metrics.py +309 -0
  93. package/python/agent_portal/models.py +160 -0
  94. package/python/agent_portal/plugin_system.py +42 -0
  95. package/python/agent_portal/rate_limit.py +253 -0
  96. package/python/agent_portal/runtime.py +739 -0
  97. package/python/agent_portal/server.py +351 -0
  98. package/python/agent_portal/validation.py +299 -0
  99. package/python/pyproject.toml +29 -0
  100. package/python/tests/test_config.py +24 -0
  101. package/python/tests/test_doctor.py +19 -0
  102. package/python/tests/test_metrics.py +180 -0
  103. package/python/tests/test_rate_limit.py +237 -0
  104. package/python/tests/test_runtime.py +122 -0
  105. package/python/tests/test_server.py +53 -0
  106. package/python/tests/test_validation.py +170 -0
  107. package/releases/desktop/agent-portal-desktop/README.md +378 -0
  108. package/releases/desktop/agent-portal-desktop/RELEASE_NOTES.md +14 -0
  109. package/releases/desktop/agent-portal-desktop/assets/branding/agent-portal-logo.png +0 -0
  110. package/releases/desktop/agent-portal-desktop/fixtures/local-workflow.html +151 -0
  111. package/releases/desktop/agent-portal-desktop/launch-agent-portal.bat +4 -0
  112. package/releases/desktop/agent-portal-desktop.zip +0 -0
  113. package/releases/python/agent_portal-0.0.2-py3-none-any.whl +0 -0
  114. package/releases/python/agent_portal-0.0.2.tar.gz +0 -0
  115. package/scripts/package_desktop.mjs +117 -0
  116. package/scripts/release_python.py +46 -0
  117. package/tests/plugin-manifest.test.mjs +26 -0
  118. package/tests/runtime.test.mjs +41 -0
  119. package/tests/vscode-extension.test.mjs +22 -0
  120. 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