web-agent-bridge 2.5.0 → 2.7.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.
@@ -0,0 +1,126 @@
1
+ -- Migration 005: Marketplace & Usage Metering tables
2
+
3
+ -- Marketplace listings
4
+ CREATE TABLE IF NOT EXISTS marketplace_listings (
5
+ id TEXT PRIMARY KEY,
6
+ name TEXT NOT NULL,
7
+ description TEXT DEFAULT '',
8
+ type TEXT NOT NULL,
9
+ category TEXT DEFAULT 'automation',
10
+ seller_id TEXT NOT NULL,
11
+ seller_name TEXT DEFAULT 'Anonymous',
12
+ price REAL DEFAULT 0,
13
+ currency TEXT DEFAULT 'usd',
14
+ version TEXT DEFAULT '1.0.0',
15
+ tags TEXT DEFAULT '[]',
16
+ icon TEXT,
17
+ readme TEXT DEFAULT '',
18
+ install_command TEXT,
19
+ config_schema TEXT DEFAULT '{}',
20
+ entry_point TEXT,
21
+ installs INTEGER DEFAULT 0,
22
+ revenue REAL DEFAULT 0,
23
+ rating REAL DEFAULT 0,
24
+ review_count INTEGER DEFAULT 0,
25
+ status TEXT DEFAULT 'pending_review',
26
+ rejection_reason TEXT,
27
+ published_at INTEGER,
28
+ created_at INTEGER NOT NULL,
29
+ updated_at INTEGER NOT NULL
30
+ );
31
+
32
+ CREATE INDEX IF NOT EXISTS idx_mkt_listings_status ON marketplace_listings(status);
33
+ CREATE INDEX IF NOT EXISTS idx_mkt_listings_category ON marketplace_listings(category);
34
+ CREATE INDEX IF NOT EXISTS idx_mkt_listings_seller ON marketplace_listings(seller_id);
35
+
36
+ -- Marketplace purchases
37
+ CREATE TABLE IF NOT EXISTS marketplace_purchases (
38
+ id TEXT PRIMARY KEY,
39
+ listing_id TEXT NOT NULL,
40
+ listing_name TEXT,
41
+ buyer_id TEXT NOT NULL,
42
+ seller_id TEXT NOT NULL,
43
+ price REAL DEFAULT 0,
44
+ commission REAL DEFAULT 0,
45
+ seller_earning REAL DEFAULT 0,
46
+ currency TEXT DEFAULT 'usd',
47
+ status TEXT DEFAULT 'pending_payment',
48
+ created_at INTEGER NOT NULL,
49
+ completed_at INTEGER,
50
+ FOREIGN KEY (listing_id) REFERENCES marketplace_listings(id)
51
+ );
52
+
53
+ CREATE INDEX IF NOT EXISTS idx_mkt_purchases_buyer ON marketplace_purchases(buyer_id);
54
+ CREATE INDEX IF NOT EXISTS idx_mkt_purchases_seller ON marketplace_purchases(seller_id);
55
+
56
+ -- Marketplace reviews
57
+ CREATE TABLE IF NOT EXISTS marketplace_reviews (
58
+ id TEXT PRIMARY KEY,
59
+ listing_id TEXT NOT NULL,
60
+ user_id TEXT NOT NULL,
61
+ rating INTEGER NOT NULL,
62
+ comment TEXT DEFAULT '',
63
+ created_at INTEGER NOT NULL,
64
+ FOREIGN KEY (listing_id) REFERENCES marketplace_listings(id)
65
+ );
66
+
67
+ -- Seller earnings
68
+ CREATE TABLE IF NOT EXISTS marketplace_earnings (
69
+ seller_id TEXT PRIMARY KEY,
70
+ total REAL DEFAULT 0,
71
+ pending REAL DEFAULT 0,
72
+ paid REAL DEFAULT 0,
73
+ last_payout INTEGER
74
+ );
75
+
76
+ -- Usage metering daily records
77
+ CREATE TABLE IF NOT EXISTS usage_metering (
78
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
79
+ entity_id TEXT NOT NULL,
80
+ metric TEXT NOT NULL,
81
+ date TEXT NOT NULL,
82
+ count INTEGER DEFAULT 0,
83
+ overage INTEGER DEFAULT 0,
84
+ overage_cost REAL DEFAULT 0,
85
+ UNIQUE(entity_id, metric, date)
86
+ );
87
+
88
+ CREATE INDEX IF NOT EXISTS idx_usage_entity ON usage_metering(entity_id);
89
+ CREATE INDEX IF NOT EXISTS idx_usage_date ON usage_metering(date);
90
+
91
+ -- Hosted runtime instances
92
+ CREATE TABLE IF NOT EXISTS hosted_instances (
93
+ id TEXT PRIMARY KEY,
94
+ agent_id TEXT NOT NULL,
95
+ tier TEXT DEFAULT 'starter',
96
+ region TEXT DEFAULT 'auto',
97
+ cpu TEXT DEFAULT '0.5',
98
+ memory TEXT DEFAULT '512',
99
+ status TEXT DEFAULT 'starting',
100
+ execution_count INTEGER DEFAULT 0,
101
+ compute_minutes REAL DEFAULT 0,
102
+ errors INTEGER DEFAULT 0,
103
+ started_at INTEGER NOT NULL,
104
+ stopped_at INTEGER,
105
+ last_activity INTEGER
106
+ );
107
+
108
+ CREATE INDEX IF NOT EXISTS idx_hosted_agent ON hosted_instances(agent_id);
109
+ CREATE INDEX IF NOT EXISTS idx_hosted_status ON hosted_instances(status);
110
+
111
+ -- Hosted executions
112
+ CREATE TABLE IF NOT EXISTS hosted_executions (
113
+ id TEXT PRIMARY KEY,
114
+ instance_id TEXT NOT NULL,
115
+ agent_id TEXT NOT NULL,
116
+ task_type TEXT,
117
+ task_action TEXT,
118
+ status TEXT DEFAULT 'running',
119
+ started_at INTEGER NOT NULL,
120
+ completed_at INTEGER,
121
+ compute_ms INTEGER DEFAULT 0,
122
+ error TEXT,
123
+ FOREIGN KEY (instance_id) REFERENCES hosted_instances(id)
124
+ );
125
+
126
+ CREATE INDEX IF NOT EXISTS idx_hexe_instance ON hosted_executions(instance_id);
@@ -0,0 +1,337 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Failure Analysis & Classification
5
+ *
6
+ * Classifies failures, detects patterns, and provides root cause analysis.
7
+ */
8
+
9
+ const { bus } = require('../runtime/event-bus');
10
+
11
+ const FailureClass = {
12
+ NETWORK: 'network',
13
+ TIMEOUT: 'timeout',
14
+ PERMISSION: 'permission',
15
+ VALIDATION: 'validation',
16
+ EXECUTION: 'execution',
17
+ RATE_LIMIT: 'rate_limit',
18
+ RESOURCE: 'resource',
19
+ DEPENDENCY: 'dependency',
20
+ UNKNOWN: 'unknown',
21
+ };
22
+
23
+ const Severity = {
24
+ LOW: 'low',
25
+ MEDIUM: 'medium',
26
+ HIGH: 'high',
27
+ CRITICAL: 'critical',
28
+ };
29
+
30
+ class FailureAnalyzer {
31
+ constructor() {
32
+ this._history = []; // All failures
33
+ this._patterns = new Map(); // pattern key → PatternInfo
34
+ this._maxHistory = 10000;
35
+ this._classifiers = this._defaultClassifiers();
36
+ }
37
+
38
+ /**
39
+ * Classify a failure
40
+ */
41
+ classify(error, context = {}) {
42
+ const failure = {
43
+ id: `fail_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
44
+ timestamp: Date.now(),
45
+ error: {
46
+ message: error.message || String(error),
47
+ code: error.code || error.statusCode || null,
48
+ stack: error.stack?.split('\n').slice(0, 5).join('\n') || null,
49
+ },
50
+ context: {
51
+ taskId: context.taskId || null,
52
+ agentId: context.agentId || null,
53
+ action: context.action || null,
54
+ domain: context.domain || null,
55
+ siteId: context.siteId || null,
56
+ },
57
+ classification: null,
58
+ severity: null,
59
+ rootCause: null,
60
+ retryable: false,
61
+ suggestedFix: null,
62
+ };
63
+
64
+ // Run classifiers
65
+ for (const classifier of this._classifiers) {
66
+ const match = classifier.test(error, context);
67
+ if (match) {
68
+ failure.classification = match.class;
69
+ failure.severity = match.severity;
70
+ failure.rootCause = match.rootCause;
71
+ failure.retryable = match.retryable;
72
+ failure.suggestedFix = match.suggestedFix;
73
+ break;
74
+ }
75
+ }
76
+
77
+ // Fallback
78
+ if (!failure.classification) {
79
+ failure.classification = FailureClass.UNKNOWN;
80
+ failure.severity = Severity.MEDIUM;
81
+ failure.retryable = false;
82
+ }
83
+
84
+ // Record & detect pattern
85
+ this._history.push(failure);
86
+ this._evict();
87
+ this._detectPattern(failure);
88
+
89
+ bus.emit('failure.classified', {
90
+ id: failure.id,
91
+ classification: failure.classification,
92
+ severity: failure.severity,
93
+ retryable: failure.retryable,
94
+ });
95
+
96
+ return failure;
97
+ }
98
+
99
+ /**
100
+ * Get failure by ID
101
+ */
102
+ getFailure(id) {
103
+ return this._history.find(f => f.id === id) || null;
104
+ }
105
+
106
+ /**
107
+ * Query failure history
108
+ */
109
+ query(filters = {}, limit = 50) {
110
+ let results = this._history;
111
+
112
+ if (filters.classification) {
113
+ results = results.filter(f => f.classification === filters.classification);
114
+ }
115
+ if (filters.severity) {
116
+ results = results.filter(f => f.severity === filters.severity);
117
+ }
118
+ if (filters.agentId) {
119
+ results = results.filter(f => f.context.agentId === filters.agentId);
120
+ }
121
+ if (filters.taskId) {
122
+ results = results.filter(f => f.context.taskId === filters.taskId);
123
+ }
124
+ if (filters.retryable !== undefined) {
125
+ results = results.filter(f => f.retryable === filters.retryable);
126
+ }
127
+ if (filters.since) {
128
+ results = results.filter(f => f.timestamp >= filters.since);
129
+ }
130
+
131
+ return results.slice(-limit).reverse();
132
+ }
133
+
134
+ /**
135
+ * Get failure patterns
136
+ */
137
+ getPatterns() {
138
+ return Array.from(this._patterns.values())
139
+ .filter(p => p.count >= 3)
140
+ .sort((a, b) => b.count - a.count)
141
+ .map(p => ({
142
+ key: p.key,
143
+ classification: p.classification,
144
+ count: p.count,
145
+ firstSeen: p.firstSeen,
146
+ lastSeen: p.lastSeen,
147
+ frequency: p.count / ((p.lastSeen - p.firstSeen) / 60000 || 1),
148
+ sample: p.sample,
149
+ }));
150
+ }
151
+
152
+ /**
153
+ * Get summary statistics
154
+ */
155
+ getSummary(since = 0) {
156
+ const relevant = since ? this._history.filter(f => f.timestamp >= since) : this._history;
157
+
158
+ const byClass = {};
159
+ const bySeverity = {};
160
+ let retryable = 0;
161
+
162
+ for (const f of relevant) {
163
+ byClass[f.classification] = (byClass[f.classification] || 0) + 1;
164
+ bySeverity[f.severity] = (bySeverity[f.severity] || 0) + 1;
165
+ if (f.retryable) retryable++;
166
+ }
167
+
168
+ return {
169
+ total: relevant.length,
170
+ byClassification: byClass,
171
+ bySeverity: bySeverity,
172
+ retryable,
173
+ patternsDetected: this._patterns.size,
174
+ activePatterns: Array.from(this._patterns.values()).filter(p => p.count >= 3).length,
175
+ };
176
+ }
177
+
178
+ getStats() {
179
+ return {
180
+ totalFailures: this._history.length,
181
+ patterns: this._patterns.size,
182
+ classifiers: this._classifiers.length,
183
+ };
184
+ }
185
+
186
+ // ── Internal ──
187
+
188
+ _defaultClassifiers() {
189
+ return [
190
+ // Network errors
191
+ {
192
+ test: (err) => {
193
+ const msg = (err.message || '').toLowerCase();
194
+ if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.code === 'ECONNRESET' ||
195
+ msg.includes('network') || msg.includes('dns') || msg.includes('socket') ||
196
+ msg.includes('fetch failed') || msg.includes('econnaborted')) {
197
+ return {
198
+ class: FailureClass.NETWORK,
199
+ severity: Severity.MEDIUM,
200
+ rootCause: `Network error: ${err.code || err.message}`,
201
+ retryable: true,
202
+ suggestedFix: 'Retry with exponential backoff. Check network connectivity.',
203
+ };
204
+ }
205
+ },
206
+ },
207
+ // Timeout
208
+ {
209
+ test: (err) => {
210
+ const msg = (err.message || '').toLowerCase();
211
+ if (err.code === 'ETIMEDOUT' || msg.includes('timeout') || msg.includes('timed out') ||
212
+ msg.includes('aborted')) {
213
+ return {
214
+ class: FailureClass.TIMEOUT,
215
+ severity: Severity.MEDIUM,
216
+ rootCause: `Operation timed out: ${err.message}`,
217
+ retryable: true,
218
+ suggestedFix: 'Increase timeout or reduce payload size.',
219
+ };
220
+ }
221
+ },
222
+ },
223
+ // Rate limit
224
+ {
225
+ test: (err) => {
226
+ if (err.statusCode === 429 || err.code === 429 ||
227
+ (err.message || '').toLowerCase().includes('rate limit')) {
228
+ return {
229
+ class: FailureClass.RATE_LIMIT,
230
+ severity: Severity.LOW,
231
+ rootCause: 'Rate limit exceeded',
232
+ retryable: true,
233
+ suggestedFix: 'Wait for rate limit window to reset. Reduce request frequency.',
234
+ };
235
+ }
236
+ },
237
+ },
238
+ // Permission
239
+ {
240
+ test: (err) => {
241
+ const code = err.statusCode || err.code;
242
+ const msg = (err.message || '').toLowerCase();
243
+ if (code === 401 || code === 403 || msg.includes('unauthorized') ||
244
+ msg.includes('forbidden') || msg.includes('permission') || msg.includes('not allowed')) {
245
+ return {
246
+ class: FailureClass.PERMISSION,
247
+ severity: Severity.HIGH,
248
+ rootCause: `Permission denied: ${err.message}`,
249
+ retryable: false,
250
+ suggestedFix: 'Request required capabilities via capability negotiation.',
251
+ };
252
+ }
253
+ },
254
+ },
255
+ // Validation
256
+ {
257
+ test: (err) => {
258
+ const code = err.statusCode || err.code;
259
+ const msg = (err.message || '').toLowerCase();
260
+ if (code === 400 || code === 422 || msg.includes('valid') || msg.includes('required') ||
261
+ msg.includes('schema') || msg.includes('missing')) {
262
+ return {
263
+ class: FailureClass.VALIDATION,
264
+ severity: Severity.LOW,
265
+ rootCause: `Validation error: ${err.message}`,
266
+ retryable: false,
267
+ suggestedFix: 'Check input schema. Ensure all required fields are present.',
268
+ };
269
+ }
270
+ },
271
+ },
272
+ // Resource
273
+ {
274
+ test: (err) => {
275
+ const code = err.statusCode || err.code;
276
+ const msg = (err.message || '').toLowerCase();
277
+ if (code === 404 || msg.includes('not found') || msg.includes('no such') ||
278
+ msg.includes('does not exist')) {
279
+ return {
280
+ class: FailureClass.RESOURCE,
281
+ severity: Severity.LOW,
282
+ rootCause: `Resource not found: ${err.message}`,
283
+ retryable: false,
284
+ suggestedFix: 'Verify resource ID or path. Use discovery endpoint first.',
285
+ };
286
+ }
287
+ },
288
+ },
289
+ // Dependency
290
+ {
291
+ test: (err) => {
292
+ const msg = (err.message || '').toLowerCase();
293
+ if (msg.includes('dependency') || msg.includes('upstream') || msg.includes('service unavailable') ||
294
+ (err.statusCode || err.code) === 503) {
295
+ return {
296
+ class: FailureClass.DEPENDENCY,
297
+ severity: Severity.HIGH,
298
+ rootCause: `Dependency failure: ${err.message}`,
299
+ retryable: true,
300
+ suggestedFix: 'Check upstream service health. Use fallback provider if available.',
301
+ };
302
+ }
303
+ },
304
+ },
305
+ ];
306
+ }
307
+
308
+ _detectPattern(failure) {
309
+ const key = `${failure.classification}:${failure.context.action || 'unknown'}:${failure.context.domain || '*'}`;
310
+ const existing = this._patterns.get(key);
311
+
312
+ if (existing) {
313
+ existing.count++;
314
+ existing.lastSeen = failure.timestamp;
315
+ existing.sample = failure.error.message;
316
+ } else {
317
+ this._patterns.set(key, {
318
+ key,
319
+ classification: failure.classification,
320
+ count: 1,
321
+ firstSeen: failure.timestamp,
322
+ lastSeen: failure.timestamp,
323
+ sample: failure.error.message,
324
+ });
325
+ }
326
+ }
327
+
328
+ _evict() {
329
+ if (this._history.length > this._maxHistory) {
330
+ this._history = this._history.slice(-this._maxHistory);
331
+ }
332
+ }
333
+ }
334
+
335
+ const failureAnalyzer = new FailureAnalyzer();
336
+
337
+ module.exports = { FailureAnalyzer, FailureClass, Severity, failureAnalyzer };