web-agent-bridge 2.4.0 → 2.6.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/README.ar.md +18 -0
- package/README.md +32 -0
- package/package.json +1 -1
- package/public/.well-known/agent-tools.json +180 -0
- package/sdk/index.d.ts +170 -0
- package/sdk/index.js +246 -1
- package/sdk/package.json +1 -1
- package/server/adapters/index.js +520 -0
- package/server/control-plane/index.js +301 -0
- package/server/data-plane/index.js +354 -0
- package/server/index.js +6 -0
- package/server/llm/index.js +404 -0
- package/server/migrations/004_agent_os.sql +158 -0
- package/server/observability/failure-analysis.js +337 -0
- package/server/observability/index.js +394 -0
- package/server/protocol/capabilities.js +223 -0
- package/server/protocol/index.js +243 -0
- package/server/protocol/schema.js +584 -0
- package/server/registry/certification.js +271 -0
- package/server/registry/index.js +326 -0
- package/server/routes/runtime.js +1136 -0
- package/server/runtime/event-bus.js +210 -0
- package/server/runtime/index.js +233 -0
- package/server/runtime/replay.js +264 -0
- package/server/runtime/sandbox.js +266 -0
- package/server/runtime/scheduler.js +395 -0
- package/server/runtime/session-engine.js +293 -0
- package/server/runtime/state-manager.js +188 -0
- package/server/security/index.js +368 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Session Engine
|
|
5
|
+
*
|
|
6
|
+
* Manages browser execution sessions with cookies, tokens,
|
|
7
|
+
* and state persistence across navigation.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
const { bus } = require('../runtime/event-bus');
|
|
12
|
+
|
|
13
|
+
class SessionEngine {
|
|
14
|
+
constructor() {
|
|
15
|
+
this._sessions = new Map(); // sessionId → BrowserSession
|
|
16
|
+
this._cookieJars = new Map(); // sessionId → CookieJar
|
|
17
|
+
this._maxSessions = 500;
|
|
18
|
+
this._defaultTTL = 3600_000; // 1 hour
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create a new browser execution session
|
|
23
|
+
*/
|
|
24
|
+
create(config = {}) {
|
|
25
|
+
const sessionId = `sess_${crypto.randomBytes(16).toString('hex')}`;
|
|
26
|
+
|
|
27
|
+
const session = {
|
|
28
|
+
id: sessionId,
|
|
29
|
+
agentId: config.agentId || null,
|
|
30
|
+
siteId: config.siteId || null,
|
|
31
|
+
state: 'active',
|
|
32
|
+
viewport: config.viewport || { width: 1920, height: 1080 },
|
|
33
|
+
userAgent: config.userAgent || null,
|
|
34
|
+
proxy: config.proxy || null,
|
|
35
|
+
localStorage: {},
|
|
36
|
+
sessionStorage: {},
|
|
37
|
+
variables: {}, // Arbitrary key-value store for the agent
|
|
38
|
+
history: [],
|
|
39
|
+
createdAt: Date.now(),
|
|
40
|
+
expiresAt: Date.now() + (config.ttl || this._defaultTTL),
|
|
41
|
+
lastActivity: Date.now(),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
this._sessions.set(sessionId, session);
|
|
45
|
+
this._cookieJars.set(sessionId, new CookieJar());
|
|
46
|
+
this._evict();
|
|
47
|
+
|
|
48
|
+
bus.emit('session.created', { sessionId, agentId: session.agentId, siteId: session.siteId });
|
|
49
|
+
return session;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get session
|
|
54
|
+
*/
|
|
55
|
+
get(sessionId) {
|
|
56
|
+
const session = this._sessions.get(sessionId);
|
|
57
|
+
if (!session) return null;
|
|
58
|
+
if (session.expiresAt < Date.now()) {
|
|
59
|
+
this.destroy(sessionId);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return session;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Update session last activity
|
|
67
|
+
*/
|
|
68
|
+
touch(sessionId) {
|
|
69
|
+
const session = this._sessions.get(sessionId);
|
|
70
|
+
if (session) session.lastActivity = Date.now();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Set cookies for a session
|
|
75
|
+
*/
|
|
76
|
+
setCookies(sessionId, cookies) {
|
|
77
|
+
const jar = this._cookieJars.get(sessionId);
|
|
78
|
+
if (!jar) return;
|
|
79
|
+
for (const cookie of cookies) {
|
|
80
|
+
jar.set(cookie);
|
|
81
|
+
}
|
|
82
|
+
this.touch(sessionId);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get cookies for a session/domain
|
|
87
|
+
*/
|
|
88
|
+
getCookies(sessionId, domain = null) {
|
|
89
|
+
const jar = this._cookieJars.get(sessionId);
|
|
90
|
+
if (!jar) return [];
|
|
91
|
+
return jar.getAll(domain);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Set localStorage value
|
|
96
|
+
*/
|
|
97
|
+
setStorage(sessionId, key, value, type = 'local') {
|
|
98
|
+
const session = this._sessions.get(sessionId);
|
|
99
|
+
if (!session) return;
|
|
100
|
+
const store = type === 'session' ? session.sessionStorage : session.localStorage;
|
|
101
|
+
store[key] = value;
|
|
102
|
+
this.touch(sessionId);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get localStorage value
|
|
107
|
+
*/
|
|
108
|
+
getStorage(sessionId, key, type = 'local') {
|
|
109
|
+
const session = this._sessions.get(sessionId);
|
|
110
|
+
if (!session) return null;
|
|
111
|
+
const store = type === 'session' ? session.sessionStorage : session.localStorage;
|
|
112
|
+
return store[key] || null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Set variable in session
|
|
117
|
+
*/
|
|
118
|
+
setVariable(sessionId, key, value) {
|
|
119
|
+
const session = this._sessions.get(sessionId);
|
|
120
|
+
if (session) {
|
|
121
|
+
session.variables[key] = value;
|
|
122
|
+
this.touch(sessionId);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get variable from session
|
|
128
|
+
*/
|
|
129
|
+
getVariable(sessionId, key) {
|
|
130
|
+
const session = this._sessions.get(sessionId);
|
|
131
|
+
return session?.variables[key] || null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Record navigation in session history
|
|
136
|
+
*/
|
|
137
|
+
recordNavigation(sessionId, url, title = '') {
|
|
138
|
+
const session = this._sessions.get(sessionId);
|
|
139
|
+
if (!session) return;
|
|
140
|
+
session.history.push({
|
|
141
|
+
url,
|
|
142
|
+
title,
|
|
143
|
+
timestamp: Date.now(),
|
|
144
|
+
index: session.history.length,
|
|
145
|
+
});
|
|
146
|
+
this.touch(sessionId);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Export session state (for transfer/persistence)
|
|
151
|
+
*/
|
|
152
|
+
export(sessionId) {
|
|
153
|
+
const session = this._sessions.get(sessionId);
|
|
154
|
+
if (!session) return null;
|
|
155
|
+
return {
|
|
156
|
+
id: session.id,
|
|
157
|
+
agentId: session.agentId,
|
|
158
|
+
siteId: session.siteId,
|
|
159
|
+
viewport: session.viewport,
|
|
160
|
+
userAgent: session.userAgent,
|
|
161
|
+
localStorage: { ...session.localStorage },
|
|
162
|
+
sessionStorage: { ...session.sessionStorage },
|
|
163
|
+
variables: { ...session.variables },
|
|
164
|
+
cookies: this.getCookies(sessionId),
|
|
165
|
+
history: [...session.history],
|
|
166
|
+
createdAt: session.createdAt,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Import session state (restore from exported data)
|
|
172
|
+
*/
|
|
173
|
+
import(data) {
|
|
174
|
+
const session = this.create({
|
|
175
|
+
agentId: data.agentId,
|
|
176
|
+
siteId: data.siteId,
|
|
177
|
+
viewport: data.viewport,
|
|
178
|
+
userAgent: data.userAgent,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
session.localStorage = data.localStorage || {};
|
|
182
|
+
session.sessionStorage = data.sessionStorage || {};
|
|
183
|
+
session.variables = data.variables || {};
|
|
184
|
+
session.history = data.history || [];
|
|
185
|
+
|
|
186
|
+
if (data.cookies) {
|
|
187
|
+
this.setCookies(session.id, data.cookies);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return session;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Destroy session
|
|
195
|
+
*/
|
|
196
|
+
destroy(sessionId) {
|
|
197
|
+
this._sessions.delete(sessionId);
|
|
198
|
+
this._cookieJars.delete(sessionId);
|
|
199
|
+
bus.emit('session.destroyed', { sessionId });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* List sessions
|
|
204
|
+
*/
|
|
205
|
+
list(filters = {}, limit = 50) {
|
|
206
|
+
const now = Date.now();
|
|
207
|
+
let sessions = Array.from(this._sessions.values()).filter(s => s.expiresAt >= now);
|
|
208
|
+
|
|
209
|
+
if (filters.agentId) sessions = sessions.filter(s => s.agentId === filters.agentId);
|
|
210
|
+
if (filters.siteId) sessions = sessions.filter(s => s.siteId === filters.siteId);
|
|
211
|
+
if (filters.state) sessions = sessions.filter(s => s.state === filters.state);
|
|
212
|
+
|
|
213
|
+
return sessions.slice(0, limit).map(s => ({
|
|
214
|
+
id: s.id,
|
|
215
|
+
agentId: s.agentId,
|
|
216
|
+
siteId: s.siteId,
|
|
217
|
+
state: s.state,
|
|
218
|
+
historyLength: s.history.length,
|
|
219
|
+
createdAt: s.createdAt,
|
|
220
|
+
lastActivity: s.lastActivity,
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
getStats() {
|
|
225
|
+
return {
|
|
226
|
+
activeSessions: this._sessions.size,
|
|
227
|
+
maxSessions: this._maxSessions,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
_evict() {
|
|
232
|
+
const now = Date.now();
|
|
233
|
+
for (const [id, session] of this._sessions) {
|
|
234
|
+
if (session.expiresAt < now) this.destroy(id);
|
|
235
|
+
}
|
|
236
|
+
if (this._sessions.size > this._maxSessions) {
|
|
237
|
+
const sorted = Array.from(this._sessions.entries())
|
|
238
|
+
.sort((a, b) => a[1].lastActivity - b[1].lastActivity);
|
|
239
|
+
const toRemove = sorted.slice(0, sorted.length - this._maxSessions);
|
|
240
|
+
for (const [id] of toRemove) this.destroy(id);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ─── Cookie Jar ─────────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
class CookieJar {
|
|
248
|
+
constructor() {
|
|
249
|
+
this._cookies = new Map(); // domain:name → cookie
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
set(cookie) {
|
|
253
|
+
const key = `${cookie.domain || '*'}:${cookie.name}`;
|
|
254
|
+
this._cookies.set(key, {
|
|
255
|
+
name: cookie.name,
|
|
256
|
+
value: cookie.value,
|
|
257
|
+
domain: cookie.domain || '*',
|
|
258
|
+
path: cookie.path || '/',
|
|
259
|
+
expires: cookie.expires || null,
|
|
260
|
+
httpOnly: cookie.httpOnly || false,
|
|
261
|
+
secure: cookie.secure || false,
|
|
262
|
+
sameSite: cookie.sameSite || 'Lax',
|
|
263
|
+
setAt: Date.now(),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
get(name, domain = '*') {
|
|
268
|
+
return this._cookies.get(`${domain}:${name}`) || null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
getAll(domain = null) {
|
|
272
|
+
const now = Date.now();
|
|
273
|
+
const result = [];
|
|
274
|
+
for (const cookie of this._cookies.values()) {
|
|
275
|
+
if (cookie.expires && cookie.expires < now) continue;
|
|
276
|
+
if (domain && cookie.domain !== '*' && cookie.domain !== domain) continue;
|
|
277
|
+
result.push({ ...cookie });
|
|
278
|
+
}
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
delete(name, domain = '*') {
|
|
283
|
+
this._cookies.delete(`${domain}:${name}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
clear() {
|
|
287
|
+
this._cookies.clear();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const sessionEngine = new SessionEngine();
|
|
292
|
+
|
|
293
|
+
module.exports = { SessionEngine, CookieJar, sessionEngine };
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WAB Runtime - State Manager
|
|
5
|
+
*
|
|
6
|
+
* Manages agent state, task checkpoints, and long-running task persistence.
|
|
7
|
+
* Provides rollback capabilities and state snapshots.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
|
|
12
|
+
class StateManager {
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this._states = new Map(); // entityId → current state
|
|
15
|
+
this._checkpoints = new Map(); // entityId → [checkpoint, ...]
|
|
16
|
+
this._maxCheckpoints = options.maxCheckpoints || 50;
|
|
17
|
+
this._ttl = options.ttl || 24 * 3600_000; // 24h default
|
|
18
|
+
this._stats = { saves: 0, restores: 0, checkpoints: 0, rollbacks: 0 };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Save state for an entity (agent or task)
|
|
23
|
+
*/
|
|
24
|
+
save(entityId, state) {
|
|
25
|
+
const entry = {
|
|
26
|
+
state: _deepClone(state),
|
|
27
|
+
updatedAt: Date.now(),
|
|
28
|
+
version: (this._states.get(entityId)?.version || 0) + 1,
|
|
29
|
+
};
|
|
30
|
+
this._states.set(entityId, entry);
|
|
31
|
+
this._stats.saves++;
|
|
32
|
+
return entry.version;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get current state
|
|
37
|
+
*/
|
|
38
|
+
get(entityId) {
|
|
39
|
+
const entry = this._states.get(entityId);
|
|
40
|
+
if (!entry) return null;
|
|
41
|
+
if (Date.now() - entry.updatedAt > this._ttl) {
|
|
42
|
+
this._states.delete(entityId);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return _deepClone(entry.state);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create a checkpoint (point-in-time snapshot for rollback)
|
|
50
|
+
*/
|
|
51
|
+
checkpoint(entityId, label = '') {
|
|
52
|
+
const entry = this._states.get(entityId);
|
|
53
|
+
if (!entry) throw new Error(`No state found for entity: ${entityId}`);
|
|
54
|
+
|
|
55
|
+
const cp = {
|
|
56
|
+
id: `cp_${crypto.randomBytes(8).toString('hex')}`,
|
|
57
|
+
label,
|
|
58
|
+
state: _deepClone(entry.state),
|
|
59
|
+
version: entry.version,
|
|
60
|
+
createdAt: Date.now(),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
if (!this._checkpoints.has(entityId)) this._checkpoints.set(entityId, []);
|
|
64
|
+
const cps = this._checkpoints.get(entityId);
|
|
65
|
+
cps.push(cp);
|
|
66
|
+
|
|
67
|
+
// Limit checkpoints
|
|
68
|
+
if (cps.length > this._maxCheckpoints) {
|
|
69
|
+
cps.splice(0, cps.length - this._maxCheckpoints);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this._stats.checkpoints++;
|
|
73
|
+
return cp.id;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Rollback to a checkpoint
|
|
78
|
+
*/
|
|
79
|
+
rollback(entityId, checkpointId) {
|
|
80
|
+
const cps = this._checkpoints.get(entityId);
|
|
81
|
+
if (!cps) throw new Error(`No checkpoints for entity: ${entityId}`);
|
|
82
|
+
|
|
83
|
+
const idx = cps.findIndex(cp => cp.id === checkpointId);
|
|
84
|
+
if (idx === -1) throw new Error(`Checkpoint not found: ${checkpointId}`);
|
|
85
|
+
|
|
86
|
+
const cp = cps[idx];
|
|
87
|
+
this._states.set(entityId, {
|
|
88
|
+
state: _deepClone(cp.state),
|
|
89
|
+
updatedAt: Date.now(),
|
|
90
|
+
version: (this._states.get(entityId)?.version || 0) + 1,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Remove checkpoints after the restored one
|
|
94
|
+
cps.splice(idx + 1);
|
|
95
|
+
this._stats.rollbacks++;
|
|
96
|
+
return cp;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* List checkpoints for an entity
|
|
101
|
+
*/
|
|
102
|
+
listCheckpoints(entityId) {
|
|
103
|
+
const cps = this._checkpoints.get(entityId) || [];
|
|
104
|
+
return cps.map(cp => ({
|
|
105
|
+
id: cp.id,
|
|
106
|
+
label: cp.label,
|
|
107
|
+
version: cp.version,
|
|
108
|
+
createdAt: cp.createdAt,
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Delete state and checkpoints for an entity
|
|
114
|
+
*/
|
|
115
|
+
delete(entityId) {
|
|
116
|
+
this._states.delete(entityId);
|
|
117
|
+
this._checkpoints.delete(entityId);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get all active entity IDs
|
|
122
|
+
*/
|
|
123
|
+
listEntities() {
|
|
124
|
+
const entities = [];
|
|
125
|
+
const now = Date.now();
|
|
126
|
+
for (const [id, entry] of this._states) {
|
|
127
|
+
if (now - entry.updatedAt > this._ttl) {
|
|
128
|
+
this._states.delete(id);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
entities.push({ id, version: entry.version, updatedAt: entry.updatedAt });
|
|
132
|
+
}
|
|
133
|
+
return entities;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Merge partial state update
|
|
138
|
+
*/
|
|
139
|
+
merge(entityId, partial) {
|
|
140
|
+
const current = this.get(entityId) || {};
|
|
141
|
+
const merged = { ...current, ...partial };
|
|
142
|
+
return this.save(entityId, merged);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Transition state with validation
|
|
147
|
+
*/
|
|
148
|
+
transition(entityId, field, from, to) {
|
|
149
|
+
const state = this.get(entityId);
|
|
150
|
+
if (!state) throw new Error(`No state for: ${entityId}`);
|
|
151
|
+
if (state[field] !== from) {
|
|
152
|
+
throw new Error(`Invalid transition: ${field} is ${state[field]}, expected ${from}`);
|
|
153
|
+
}
|
|
154
|
+
state[field] = to;
|
|
155
|
+
return this.save(entityId, state);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
getStats() {
|
|
159
|
+
return {
|
|
160
|
+
...this._stats,
|
|
161
|
+
activeEntities: this._states.size,
|
|
162
|
+
totalCheckpoints: Array.from(this._checkpoints.values()).reduce((s, c) => s + c.length, 0),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Cleanup expired states
|
|
168
|
+
*/
|
|
169
|
+
cleanup() {
|
|
170
|
+
const now = Date.now();
|
|
171
|
+
let cleaned = 0;
|
|
172
|
+
for (const [id, entry] of this._states) {
|
|
173
|
+
if (now - entry.updatedAt > this._ttl) {
|
|
174
|
+
this._states.delete(id);
|
|
175
|
+
this._checkpoints.delete(id);
|
|
176
|
+
cleaned++;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return cleaned;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function _deepClone(obj) {
|
|
184
|
+
if (obj === null || typeof obj !== 'object') return obj;
|
|
185
|
+
return JSON.parse(JSON.stringify(obj));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = { StateManager };
|