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,309 @@
|
|
|
1
|
+
"""Metrics and telemetry system for Agent Portal."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from threading import Lock
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MetricType(Enum):
|
|
16
|
+
"""Types of metrics."""
|
|
17
|
+
COUNTER = "counter"
|
|
18
|
+
GAUGE = "gauge"
|
|
19
|
+
HISTOGRAM = "histogram"
|
|
20
|
+
TIMER = "timer"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class MetricValue:
|
|
25
|
+
"""A single metric value."""
|
|
26
|
+
name: str
|
|
27
|
+
value: float
|
|
28
|
+
timestamp: float
|
|
29
|
+
tags: dict[str, str] = field(default_factory=dict)
|
|
30
|
+
metric_type: MetricType = MetricType.GAUGE
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class HistogramBucket:
|
|
35
|
+
"""A histogram bucket for tracking distributions."""
|
|
36
|
+
count: int = 0
|
|
37
|
+
sum: float = 0.0
|
|
38
|
+
min: float = float("inf")
|
|
39
|
+
max: float = float("-inf")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class MetricsCollector:
|
|
43
|
+
"""
|
|
44
|
+
Thread-safe metrics collector for runtime observability.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, max_samples: int = 10000) -> None:
|
|
48
|
+
self.max_samples = max_samples
|
|
49
|
+
self._counters: dict[str, float] = defaultdict(float)
|
|
50
|
+
self._gauges: dict[str, float] = defaultdict(float)
|
|
51
|
+
self._histograms: dict[str, HistogramBucket] = defaultdict(HistogramBucket)
|
|
52
|
+
self._timers: dict[str, list[float]] = defaultdict(list)
|
|
53
|
+
self._samples: list[MetricValue] = []
|
|
54
|
+
self._lock = Lock()
|
|
55
|
+
|
|
56
|
+
# Initialize built-in metrics
|
|
57
|
+
self.init_builtin_metrics()
|
|
58
|
+
|
|
59
|
+
def init_builtin_metrics(self) -> None:
|
|
60
|
+
"""Initialize built-in runtime metrics."""
|
|
61
|
+
with self._lock:
|
|
62
|
+
# Runtime metrics
|
|
63
|
+
self._gauges["runtime.uptime_seconds"] = 0.0
|
|
64
|
+
self._gauges["runtime.active_sessions"] = 0.0
|
|
65
|
+
self._gauges["runtime.browser_connected"] = 0.0
|
|
66
|
+
|
|
67
|
+
# Action metrics
|
|
68
|
+
self._counters["actions.total"] = 0.0
|
|
69
|
+
self._counters["actions.completed"] = 0.0
|
|
70
|
+
self._counters["actions.failed"] = 0.0
|
|
71
|
+
self._counters["actions.blocked"] = 0.0
|
|
72
|
+
self._counters["actions.approved"] = 0.0
|
|
73
|
+
self._counters["actions.rejected"] = 0.0
|
|
74
|
+
|
|
75
|
+
# Browser metrics
|
|
76
|
+
self._counters["browser.navigations"] = 0.0
|
|
77
|
+
self._counters["browser.screenshots"] = 0.0
|
|
78
|
+
self._counters["browser.errors"] = 0.0
|
|
79
|
+
|
|
80
|
+
# Network metrics
|
|
81
|
+
self._counters["network.requests"] = 0.0
|
|
82
|
+
self._counters["network.failures"] = 0.0
|
|
83
|
+
self._counters["network.console_errors"] = 0.0
|
|
84
|
+
|
|
85
|
+
def increment(self, name: str, value: float = 1.0, tags: dict[str, str] | None = None) -> None:
|
|
86
|
+
"""
|
|
87
|
+
Increment a counter metric.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
name: Metric name
|
|
91
|
+
value: Amount to increment by
|
|
92
|
+
tags: Optional tags for the metric
|
|
93
|
+
"""
|
|
94
|
+
with self._lock:
|
|
95
|
+
self._counters[name] += value
|
|
96
|
+
self._add_sample(name, value, MetricType.COUNTER, tags or {})
|
|
97
|
+
|
|
98
|
+
def set_gauge(self, name: str, value: float, tags: dict[str, str] | None = None) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Set a gauge metric value.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
name: Metric name
|
|
104
|
+
value: Value to set
|
|
105
|
+
tags: Optional tags for the metric
|
|
106
|
+
"""
|
|
107
|
+
with self._lock:
|
|
108
|
+
self._gauges[name] = value
|
|
109
|
+
self._add_sample(name, value, MetricType.GAUGE, tags or {})
|
|
110
|
+
|
|
111
|
+
def record_histogram(self, name: str, value: float, tags: dict[str, str] | None = None) -> None:
|
|
112
|
+
"""
|
|
113
|
+
Record a value in a histogram.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
name: Metric name
|
|
117
|
+
value: Value to record
|
|
118
|
+
tags: Optional tags for the metric
|
|
119
|
+
"""
|
|
120
|
+
with self._lock:
|
|
121
|
+
bucket = self._histograms[name]
|
|
122
|
+
bucket.count += 1
|
|
123
|
+
bucket.sum += value
|
|
124
|
+
bucket.min = min(bucket.min, value)
|
|
125
|
+
bucket.max = max(bucket.max, value)
|
|
126
|
+
self._add_sample(name, value, MetricType.HISTOGRAM, tags or {})
|
|
127
|
+
|
|
128
|
+
def start_timer(self, name: str) -> TimerContext:
|
|
129
|
+
"""
|
|
130
|
+
Start a timer for a metric.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
name: Metric name
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
TimerContext that will record the elapsed time when exited
|
|
137
|
+
"""
|
|
138
|
+
return TimerContext(self, name)
|
|
139
|
+
|
|
140
|
+
def record_timer(self, name: str, duration_seconds: float, tags: dict[str, str] | None = None) -> None:
|
|
141
|
+
"""
|
|
142
|
+
Record a timer value.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
name: Metric name
|
|
146
|
+
duration_seconds: Duration in seconds
|
|
147
|
+
tags: Optional tags for the metric
|
|
148
|
+
"""
|
|
149
|
+
with self._lock:
|
|
150
|
+
self._timers[name].append(duration_seconds)
|
|
151
|
+
# Keep only last 1000 timer samples per name
|
|
152
|
+
if len(self._timers[name]) > 1000:
|
|
153
|
+
self._timers[name] = self._timers[name][-1000:]
|
|
154
|
+
self._add_sample(name, duration_seconds, MetricType.TIMER, tags or {})
|
|
155
|
+
|
|
156
|
+
def get_counter(self, name: str) -> float:
|
|
157
|
+
"""Get the current value of a counter."""
|
|
158
|
+
with self._lock:
|
|
159
|
+
return self._counters.get(name, 0.0)
|
|
160
|
+
|
|
161
|
+
def get_gauge(self, name: str) -> float:
|
|
162
|
+
"""Get the current value of a gauge."""
|
|
163
|
+
with self._lock:
|
|
164
|
+
return self._gauges.get(name, 0.0)
|
|
165
|
+
|
|
166
|
+
def get_histogram(self, name: str) -> dict[str, Any]:
|
|
167
|
+
"""Get histogram statistics."""
|
|
168
|
+
with self._lock:
|
|
169
|
+
bucket = self._histograms.get(name)
|
|
170
|
+
if not bucket or bucket.count == 0:
|
|
171
|
+
return {"count": 0, "sum": 0.0, "min": 0.0, "max": 0.0, "avg": 0.0}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
"count": bucket.count,
|
|
175
|
+
"sum": bucket.sum,
|
|
176
|
+
"min": bucket.min,
|
|
177
|
+
"max": bucket.max,
|
|
178
|
+
"avg": bucket.sum / bucket.count,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
def get_timer_stats(self, name: str) -> dict[str, Any]:
|
|
182
|
+
"""Get timer statistics."""
|
|
183
|
+
with self._lock:
|
|
184
|
+
times = self._timers.get(name, [])
|
|
185
|
+
if not times:
|
|
186
|
+
return {"count": 0, "min": 0.0, "max": 0.0, "avg": 0.0, "p50": 0.0, "p95": 0.0, "p99": 0.0}
|
|
187
|
+
|
|
188
|
+
sorted_times = sorted(times)
|
|
189
|
+
count = len(sorted_times)
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
"count": count,
|
|
193
|
+
"min": min(times),
|
|
194
|
+
"max": max(times),
|
|
195
|
+
"avg": sum(times) / count,
|
|
196
|
+
"p50": sorted_times[int(count * 0.5)] if count > 0 else 0.0,
|
|
197
|
+
"p95": sorted_times[int(count * 0.95)] if count > 0 else 0.0,
|
|
198
|
+
"p99": sorted_times[int(count * 0.99)] if count > 0 else 0.0,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
def get_all_metrics(self) -> dict[str, Any]:
|
|
202
|
+
"""
|
|
203
|
+
Get all current metrics.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Dictionary containing all metrics
|
|
207
|
+
"""
|
|
208
|
+
with self._lock:
|
|
209
|
+
return {
|
|
210
|
+
"counters": dict(self._counters),
|
|
211
|
+
"gauges": dict(self._gauges),
|
|
212
|
+
"histograms": {
|
|
213
|
+
name: self.get_histogram(name)
|
|
214
|
+
for name in self._histograms
|
|
215
|
+
},
|
|
216
|
+
"timers": {
|
|
217
|
+
name: self.get_timer_stats(name)
|
|
218
|
+
for name in self._timers
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
def _add_sample(self, name: str, value: float, metric_type: MetricType, tags: dict[str, str]) -> None:
|
|
223
|
+
"""Add a sample to the samples list."""
|
|
224
|
+
self._samples.append(MetricValue(
|
|
225
|
+
name=name,
|
|
226
|
+
value=value,
|
|
227
|
+
timestamp=time.time(),
|
|
228
|
+
tags=tags,
|
|
229
|
+
metric_type=metric_type
|
|
230
|
+
))
|
|
231
|
+
|
|
232
|
+
# Keep only recent samples to prevent memory issues
|
|
233
|
+
if len(self._samples) > self.max_samples:
|
|
234
|
+
self._samples = self._samples[-(self.max_samples // 2):]
|
|
235
|
+
|
|
236
|
+
def reset(self) -> None:
|
|
237
|
+
"""Reset all metrics."""
|
|
238
|
+
with self._lock:
|
|
239
|
+
self._counters.clear()
|
|
240
|
+
self._gauges.clear()
|
|
241
|
+
self._histograms.clear()
|
|
242
|
+
self._timers.clear()
|
|
243
|
+
self._samples.clear()
|
|
244
|
+
self.init_builtin_metrics()
|
|
245
|
+
|
|
246
|
+
def export_to_file(self, path: Path) -> None:
|
|
247
|
+
"""Export metrics to a JSON file."""
|
|
248
|
+
metrics = self.get_all_metrics()
|
|
249
|
+
metrics["exported_at"] = time.time()
|
|
250
|
+
path.write_text(json.dumps(metrics, indent=2), encoding="utf8")
|
|
251
|
+
|
|
252
|
+
def get_recent_samples(self, limit: int = 100) -> list[dict[str, Any]]:
|
|
253
|
+
"""Get recent metric samples."""
|
|
254
|
+
with self._lock:
|
|
255
|
+
samples = self._samples[-limit:] if self._samples else []
|
|
256
|
+
return [
|
|
257
|
+
{
|
|
258
|
+
"name": s.name,
|
|
259
|
+
"value": s.value,
|
|
260
|
+
"timestamp": s.timestamp,
|
|
261
|
+
"tags": s.tags,
|
|
262
|
+
"type": s.metric_type.value,
|
|
263
|
+
}
|
|
264
|
+
for s in samples
|
|
265
|
+
]
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class TimerContext:
|
|
269
|
+
"""Context manager for timing operations."""
|
|
270
|
+
|
|
271
|
+
def __init__(self, collector: MetricsCollector, name: str, tags: dict[str, str] | None = None) -> None:
|
|
272
|
+
self.collector = collector
|
|
273
|
+
self.name = name
|
|
274
|
+
self.tags = tags or {}
|
|
275
|
+
self.start_time: float | None = None
|
|
276
|
+
|
|
277
|
+
def __enter__(self) -> TimerContext:
|
|
278
|
+
self.start_time = time.time()
|
|
279
|
+
return self
|
|
280
|
+
|
|
281
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
282
|
+
if self.start_time is not None:
|
|
283
|
+
duration = time.time() - self.start_time
|
|
284
|
+
self.collector.record_timer(self.name, duration, self.tags)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# Global metrics instance
|
|
288
|
+
_global_metrics: MetricsCollector | None = None
|
|
289
|
+
_metrics_lock = Lock()
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def get_metrics() -> MetricsCollector:
|
|
293
|
+
"""Get or create the global metrics collector."""
|
|
294
|
+
global _global_metrics
|
|
295
|
+
|
|
296
|
+
if _global_metrics is None:
|
|
297
|
+
with _metrics_lock:
|
|
298
|
+
if _global_metrics is None:
|
|
299
|
+
_global_metrics = MetricsCollector()
|
|
300
|
+
|
|
301
|
+
return _global_metrics
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def reset_metrics() -> None:
|
|
305
|
+
"""Reset the global metrics collector."""
|
|
306
|
+
global _global_metrics
|
|
307
|
+
with _metrics_lock:
|
|
308
|
+
if _global_metrics is not None:
|
|
309
|
+
_global_metrics.reset()
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, dataclass, field
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def utc_now() -> str:
|
|
9
|
+
return datetime.now(timezone.utc).isoformat()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
RiskLevel = Literal["safe", "low", "medium", "high", "blocked"]
|
|
13
|
+
ActionStatus = Literal["pending", "approved", "rejected", "completed", "failed", "blocked"]
|
|
14
|
+
RuntimeStatus = Literal[
|
|
15
|
+
"idle",
|
|
16
|
+
"thinking",
|
|
17
|
+
"acting",
|
|
18
|
+
"waiting-approval",
|
|
19
|
+
"paused",
|
|
20
|
+
"blocked",
|
|
21
|
+
"finished",
|
|
22
|
+
"failed",
|
|
23
|
+
"stopped",
|
|
24
|
+
]
|
|
25
|
+
ActionMode = Literal["read-only", "assisted", "autonomous", "manual-override"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(slots=True)
|
|
29
|
+
class RuntimeConfigModel:
|
|
30
|
+
runtime_host: str = "127.0.0.1"
|
|
31
|
+
runtime_port: int = 8765
|
|
32
|
+
screenshot_directory: str = "workspaces/runtime/screenshots"
|
|
33
|
+
report_directory: str = "workspaces/runtime/reports"
|
|
34
|
+
default_browser: str = "chromium"
|
|
35
|
+
action_mode: ActionMode = "assisted"
|
|
36
|
+
approval_policy: RiskLevel = "medium"
|
|
37
|
+
allowed_domains: list[str] = field(default_factory=list)
|
|
38
|
+
blocked_domains: list[str] = field(default_factory=list)
|
|
39
|
+
enabled_plugins: list[str] = field(default_factory=list)
|
|
40
|
+
log_level: str = "INFO"
|
|
41
|
+
retention_days: int = 14
|
|
42
|
+
sensitive_screenshots_enabled: bool = False
|
|
43
|
+
safe_mode: bool = True
|
|
44
|
+
api_token: str | None = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(slots=True)
|
|
48
|
+
class BrowserState:
|
|
49
|
+
browser_name: str = "chromium"
|
|
50
|
+
connected: bool = False
|
|
51
|
+
current_url: str | None = None
|
|
52
|
+
page_title: str | None = None
|
|
53
|
+
last_error: str | None = None
|
|
54
|
+
session_id: str | None = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(slots=True)
|
|
58
|
+
class RiskPolicy:
|
|
59
|
+
mode: ActionMode = "assisted"
|
|
60
|
+
approval_threshold: RiskLevel = "medium"
|
|
61
|
+
domain_lock: str | None = None
|
|
62
|
+
tab_lock: str | None = None
|
|
63
|
+
read_only: bool = False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(slots=True)
|
|
67
|
+
class PluginManifestModel:
|
|
68
|
+
name: str
|
|
69
|
+
version: str
|
|
70
|
+
type: str
|
|
71
|
+
permissions: list[str]
|
|
72
|
+
entryPoint: str | None = None
|
|
73
|
+
commands: list[str] = field(default_factory=list)
|
|
74
|
+
settings: dict[str, Any] = field(default_factory=dict)
|
|
75
|
+
panels: list[str] = field(default_factory=list)
|
|
76
|
+
lifecycleHooks: list[str] = field(default_factory=list)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(slots=True)
|
|
80
|
+
class ActionRequest:
|
|
81
|
+
action_type: str
|
|
82
|
+
reason: str
|
|
83
|
+
target: str | None = None
|
|
84
|
+
payload: str | None = None
|
|
85
|
+
risk_level: RiskLevel = "low"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(slots=True)
|
|
89
|
+
class ActionResult:
|
|
90
|
+
action_id: str
|
|
91
|
+
action_type: str
|
|
92
|
+
status: ActionStatus
|
|
93
|
+
reason: str
|
|
94
|
+
target: str | None = None
|
|
95
|
+
payload: str | None = None
|
|
96
|
+
result: str | None = None
|
|
97
|
+
error: dict[str, object] | None = None
|
|
98
|
+
risk_level: RiskLevel = "low"
|
|
99
|
+
before_screenshot: str | None = None
|
|
100
|
+
after_screenshot: str | None = None
|
|
101
|
+
created_at: str = field(default_factory=utc_now)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(slots=True)
|
|
105
|
+
class SessionState:
|
|
106
|
+
session_id: str
|
|
107
|
+
started_at: str = field(default_factory=utc_now)
|
|
108
|
+
ended_at: str | None = None
|
|
109
|
+
runtime_status: RuntimeStatus = "idle"
|
|
110
|
+
current_goal: str | None = None
|
|
111
|
+
pending_actions: list[ActionResult] = field(default_factory=list)
|
|
112
|
+
approved_actions: list[ActionResult] = field(default_factory=list)
|
|
113
|
+
completed_actions: list[ActionResult] = field(default_factory=list)
|
|
114
|
+
rejected_actions: list[ActionResult] = field(default_factory=list)
|
|
115
|
+
blocked_actions: list[ActionResult] = field(default_factory=list)
|
|
116
|
+
failed_actions: list[ActionResult] = field(default_factory=list)
|
|
117
|
+
logs: list[str] = field(default_factory=list)
|
|
118
|
+
console_errors: list[str] = field(default_factory=list)
|
|
119
|
+
network_errors: list[str] = field(default_factory=list)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(slots=True)
|
|
123
|
+
class HealthCheckResult:
|
|
124
|
+
name: str
|
|
125
|
+
status: Literal["passed", "warning", "failed"]
|
|
126
|
+
details: str
|
|
127
|
+
suggested_fix: str | None = None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(slots=True)
|
|
131
|
+
class DoctorReport:
|
|
132
|
+
checks: list[HealthCheckResult] = field(default_factory=list)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass(slots=True)
|
|
136
|
+
class SessionReport:
|
|
137
|
+
project_name: str
|
|
138
|
+
runtime_version: str
|
|
139
|
+
session_id: str
|
|
140
|
+
start_time: str
|
|
141
|
+
end_time: str
|
|
142
|
+
browser_used: str
|
|
143
|
+
current_url: str | None
|
|
144
|
+
goals: list[str]
|
|
145
|
+
actions_proposed: list[ActionResult]
|
|
146
|
+
actions_approved: list[ActionResult]
|
|
147
|
+
actions_rejected: list[ActionResult]
|
|
148
|
+
actions_blocked: list[ActionResult]
|
|
149
|
+
actions_completed: list[ActionResult]
|
|
150
|
+
console_errors: list[str]
|
|
151
|
+
network_errors: list[str]
|
|
152
|
+
screenshots: list[str]
|
|
153
|
+
risk_events: list[str]
|
|
154
|
+
failed_steps: list[str]
|
|
155
|
+
suggested_fixes: list[str]
|
|
156
|
+
reproduction_steps: list[str]
|
|
157
|
+
environment_details: dict[str, str]
|
|
158
|
+
|
|
159
|
+
def to_dict(self) -> dict[str, Any]:
|
|
160
|
+
return asdict(self)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .models import PluginManifestModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
PLUGIN_DIR = "plugins"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def discover_plugins(base_path: Path | None = None) -> list[Path]:
|
|
13
|
+
root = base_path or Path.cwd()
|
|
14
|
+
plugin_root = root / PLUGIN_DIR
|
|
15
|
+
if not plugin_root.exists():
|
|
16
|
+
return []
|
|
17
|
+
return sorted(plugin_root.glob("*/plugin.json"))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_plugin_manifest(plugin_path: Path) -> PluginManifestModel:
|
|
21
|
+
raw = json.loads(plugin_path.read_text(encoding="utf8"))
|
|
22
|
+
return PluginManifestModel(**raw)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def validate_plugin_manifest(plugin_path: Path) -> list[str]:
|
|
26
|
+
try:
|
|
27
|
+
manifest = load_plugin_manifest(plugin_path)
|
|
28
|
+
except Exception as exc:
|
|
29
|
+
return [f"Failed to load manifest {plugin_path}: {exc}"]
|
|
30
|
+
|
|
31
|
+
errors: list[str] = []
|
|
32
|
+
if not manifest.name:
|
|
33
|
+
errors.append("Plugin manifest is missing a name.")
|
|
34
|
+
if not manifest.version:
|
|
35
|
+
errors.append("Plugin manifest is missing a version.")
|
|
36
|
+
if not manifest.type:
|
|
37
|
+
errors.append("Plugin manifest is missing a type.")
|
|
38
|
+
if not isinstance(manifest.permissions, list):
|
|
39
|
+
errors.append("Plugin permissions must be a list.")
|
|
40
|
+
if not isinstance(manifest.commands, list):
|
|
41
|
+
errors.append("Plugin commands must be a list.")
|
|
42
|
+
return errors
|