@yeaft/webchat-agent 1.0.416 → 1.0.418

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 (31) hide show
  1. package/browser-runtime/browser-install.js +39 -0
  2. package/browser-runtime/chromium.js +192 -0
  3. package/browser-runtime/cli.js +1 -1
  4. package/browser-runtime/extension/manifest.json +3 -0
  5. package/browser-runtime/extension/offscreen.js +201 -25
  6. package/browser-runtime/extension/popup.js +4 -1
  7. package/browser-runtime/extension/service-worker.js +47 -7
  8. package/browser-runtime/extension.js +1 -1
  9. package/browser-runtime/index.js +3 -0
  10. package/browser-runtime/local-bridge.js +194 -0
  11. package/browser-runtime/messages.js +123 -0
  12. package/browser-runtime/service.js +769 -30
  13. package/connection/index.js +2 -0
  14. package/connection/message-router.js +2 -0
  15. package/index.js +13 -0
  16. package/local-runtime/server/.env.example +10 -0
  17. package/local-runtime/server/browser-runtime-routes.js +284 -0
  18. package/local-runtime/server/client-protocol.js +8 -0
  19. package/local-runtime/server/config.js +28 -0
  20. package/local-runtime/server/handlers/agent-browser.js +393 -0
  21. package/local-runtime/server/handlers/agent-sync.js +14 -0
  22. package/local-runtime/server/handlers/client-browser.js +337 -0
  23. package/local-runtime/server/ws-agent.js +9 -1
  24. package/local-runtime/server/ws-client.js +41 -2
  25. package/local-runtime/version.json +1 -1
  26. package/local-runtime/web/app.bundle.js +220 -98
  27. package/local-runtime/web/app.bundle.js.gz +0 -0
  28. package/local-runtime/web/index.html +2 -2
  29. package/local-runtime/web/style.bundle.css +1 -1
  30. package/local-runtime/web/style.bundle.css.gz +0 -0
  31. package/package.json +1 -1
@@ -0,0 +1,393 @@
1
+ import { sendToAgent, sendToWebClient } from '../ws-utils.js';
2
+ import {
3
+ browserClientForPeer,
4
+ browserPeers,
5
+ browserRoutes,
6
+ completeBrowserRequest,
7
+ deleteBrowserPeer,
8
+ findBrowserRequest,
9
+ deleteBrowserRoute,
10
+ getBrowserPeer,
11
+ getBrowserRoute,
12
+ installBrowserRoute,
13
+ } from '../browser-runtime-routes.js';
14
+
15
+ const AGENT_BROWSER_TYPES = new Set([
16
+ 'browser_runtime_status_result',
17
+ 'browser_runtime_install_progress',
18
+ 'browser_runtime_error',
19
+ 'browser_session_created',
20
+ 'browser_session_error',
21
+ 'browser_session_snapshot',
22
+ 'browser_session_list_result',
23
+ 'browser_peer_prepared',
24
+ 'browser_peer_offer',
25
+ 'browser_peer_ice_candidate',
26
+ 'browser_peer_state',
27
+ 'browser_peer_error',
28
+ ]);
29
+
30
+ function clean(value, max = 512) {
31
+ return typeof value === 'string' ? value.trim().slice(0, max) : '';
32
+ }
33
+
34
+ function sessionSnapshot(msg) {
35
+ return {
36
+ browserSessionId: clean(msg?.browserSessionId),
37
+ revision: Number(msg?.revision) || 1,
38
+ state: clean(msg?.state, 32) || 'unknown',
39
+ activeUrl: clean(msg?.activeUrl, 4096) || 'about:blank',
40
+ title: clean(msg?.title, 512),
41
+ pageRevision: Number(msg?.pageRevision) || 1,
42
+ captureMode: clean(msg?.captureMode, 32) || null,
43
+ viewport: msg?.viewport && typeof msg.viewport === 'object' ? {
44
+ width: Number(msg.viewport.width) || 0,
45
+ height: Number(msg.viewport.height) || 0,
46
+ deviceScaleFactor: Number(msg.viewport.deviceScaleFactor) || 1,
47
+ } : null,
48
+ viewerCount: Math.max(0, Number(msg?.viewerCount) || 0),
49
+ interactivePeerCount: Math.max(0, Number(msg?.interactivePeerCount) || 0),
50
+ authorizedProducerCount: Math.max(0, Number(msg?.authorizedProducerCount) || 0),
51
+ expiresAt: Number(msg?.expiresAt) || null,
52
+ terminalReason: clean(msg?.terminalReason, 128) || null,
53
+ safeError: clean(msg?.safeError, 500) || null,
54
+ sourceRef: msg?.sourceRef && typeof msg.sourceRef === 'object' ? {
55
+ kind: clean(msg.sourceRef.kind, 32),
56
+ sessionId: clean(msg.sourceRef.sessionId) || undefined,
57
+ conversationId: clean(msg.sourceRef.conversationId) || undefined,
58
+ workItemId: clean(msg.sourceRef.workItemId) || undefined,
59
+ } : null,
60
+ };
61
+ }
62
+
63
+ function safeByteCount(value) {
64
+ const number = Number(value);
65
+ if (!Number.isSafeInteger(number) || number < 0) return 0;
66
+ return Math.min(number, 4 * 1024 * 1024 * 1024);
67
+ }
68
+
69
+ function publicRuntimeMessage(agentId, requestId, msg) {
70
+ if (msg?.type === 'browser_runtime_error') {
71
+ return {
72
+ type: msg.type,
73
+ agentId,
74
+ requestId,
75
+ code: clean(msg.code, 128) || 'browser_runtime_error',
76
+ safeError: clean(msg.safeError, 500) || null,
77
+ };
78
+ }
79
+ if (msg?.type === 'browser_runtime_install_progress') {
80
+ return {
81
+ type: msg.type,
82
+ agentId,
83
+ requestId,
84
+ downloadedBytes: safeByteCount(msg.downloadedBytes),
85
+ totalBytes: safeByteCount(msg.totalBytes),
86
+ };
87
+ }
88
+ return {
89
+ type: 'browser_runtime_status_result',
90
+ agentId,
91
+ requestId,
92
+ supported: msg?.supported === true,
93
+ state: clean(msg?.state, 32) || 'unknown',
94
+ installed: msg?.installed === true,
95
+ enabled: msg?.enabled === true,
96
+ ready: msg?.ready === true,
97
+ buildId: clean(msg?.buildId, 128) || null,
98
+ platform: clean(msg?.platform, 32) || null,
99
+ downloadBytes: safeByteCount(msg?.downloadBytes),
100
+ downloadedBytes: safeByteCount(msg?.downloadedBytes),
101
+ totalBytes: safeByteCount(msg?.totalBytes),
102
+ probeCode: clean(msg?.probeCode, 128) || null,
103
+ safeError: clean(msg?.safeError, 500) || null,
104
+ };
105
+ }
106
+
107
+ function publicSessionMessage(agentId, msg) {
108
+ if (msg?.type === 'browser_session_error') {
109
+ return {
110
+ type: 'browser_session_error',
111
+ agentId,
112
+ requestId: clean(msg.requestId) || null,
113
+ browserSessionId: clean(msg.browserSessionId) || null,
114
+ code: clean(msg.code, 128) || 'browser_runtime_error',
115
+ safeError: clean(msg.safeError, 500) || null,
116
+ };
117
+ }
118
+ if (msg?.type === 'browser_session_list_result') {
119
+ return {
120
+ type: msg.type,
121
+ agentId,
122
+ requestId: clean(msg.requestId) || null,
123
+ sessions: (Array.isArray(msg.sessions) ? msg.sessions : []).slice(0, 64).map(sessionSnapshot),
124
+ };
125
+ }
126
+ return {
127
+ type: clean(msg?.type, 64),
128
+ agentId,
129
+ requestId: clean(msg?.requestId) || null,
130
+ ...sessionSnapshot(msg),
131
+ };
132
+ }
133
+
134
+ async function sendCreateResponse(agentId, msg) {
135
+ const request = completeBrowserRequest(agentId, msg, { kinds: 'browser_session_create' });
136
+ if (!request) return true;
137
+ const client = browserClientForPeer({
138
+ clientId: request.clientId,
139
+ webConnectionId: request.webConnectionId,
140
+ webConnectionGeneration: request.webConnectionGeneration,
141
+ });
142
+ if (!client || client.userId !== request.ownerUserId) return true;
143
+ if (msg.type === 'browser_session_created') {
144
+ const route = installBrowserRoute({ agentId, ownerUserId: request.ownerUserId, msg });
145
+ if (!route) {
146
+ await sendToWebClient(client, {
147
+ type: 'browser_session_error',
148
+ agentId,
149
+ requestId: request.requestId,
150
+ browserSessionId: clean(msg.browserSessionId) || null,
151
+ code: 'browser_route_capacity',
152
+ safeError: 'browser_route_capacity',
153
+ });
154
+ await sendToAgent(agent, {
155
+ type: 'browser_session_close',
156
+ agentId,
157
+ requestId: `server-cleanup-${request.serverRequestId}`,
158
+ browserSessionId: clean(msg.browserSessionId),
159
+ expectedRevision: Number(msg.revision) || 1,
160
+ serverIdentity: {
161
+ ownerUserId: request.ownerUserId,
162
+ clientId: request.clientId,
163
+ webConnectionId: request.webConnectionId,
164
+ webConnectionGeneration: request.webConnectionGeneration,
165
+ },
166
+ }).catch(() => {});
167
+ return true;
168
+ }
169
+ }
170
+ const publicResponse = publicSessionMessage(agentId, request.response);
171
+ // Create request replay is served from the ledger. Persist only the explicit
172
+ // public projection, never the Agent-originated object.
173
+ request.response = publicResponse;
174
+ await sendToWebClient(client, publicResponse);
175
+ return true;
176
+ }
177
+
178
+ /** Agent-authenticated Browser events routed only through Server-owned ledgers. */
179
+ export async function handleAgentBrowser(agentId, agent, msg) {
180
+ if (!AGENT_BROWSER_TYPES.has(msg?.type)) return false;
181
+ if (msg.type === 'browser_runtime_install_progress') {
182
+ const request = findBrowserRequest(agentId, msg.requestId);
183
+ if (!request || request.state !== 'pending' || request.kind !== 'browser_runtime_install') return true;
184
+ const client = browserClientForPeer({
185
+ clientId: request.clientId,
186
+ webConnectionId: request.webConnectionId,
187
+ webConnectionGeneration: request.webConnectionGeneration,
188
+ });
189
+ if (client?.userId === request.ownerUserId) {
190
+ await sendToWebClient(client, publicRuntimeMessage(agentId, request.requestId, msg));
191
+ }
192
+ return true;
193
+ }
194
+ if (msg.type === 'browser_runtime_status_result' || msg.type === 'browser_runtime_error') {
195
+ const pendingRequest = findBrowserRequest(agentId, msg.requestId);
196
+ const request = completeBrowserRequest(agentId, msg, {
197
+ consume: pendingRequest?.kind === 'browser_runtime_status',
198
+ kinds: ['browser_runtime_status', 'browser_runtime_install', 'browser_runtime_enable'],
199
+ });
200
+ if (!request) return true;
201
+ const client = browserClientForPeer({
202
+ clientId: request.clientId,
203
+ webConnectionId: request.webConnectionId,
204
+ webConnectionGeneration: request.webConnectionGeneration,
205
+ });
206
+ if (client?.userId !== request.ownerUserId) return true;
207
+ const response = publicRuntimeMessage(agentId, request.requestId, msg);
208
+ request.response = response;
209
+ await sendToWebClient(client, response);
210
+ return true;
211
+ }
212
+ if (msg.type === 'browser_session_created'
213
+ || (msg.type === 'browser_session_error' && msg.requestId)) {
214
+ return sendCreateResponse(agentId, msg);
215
+ }
216
+
217
+ if (msg.type === 'browser_session_list_result') {
218
+ const request = completeBrowserRequest(agentId, msg, {
219
+ consume: true,
220
+ kinds: 'browser_session_list',
221
+ });
222
+ if (!request) return true;
223
+ for (const snapshot of Array.isArray(msg.sessions) ? msg.sessions : []) {
224
+ if (!snapshot?.browserSessionId) continue;
225
+ installBrowserRoute({ agentId, ownerUserId: request.ownerUserId, msg: snapshot });
226
+ }
227
+ const client = browserClientForPeer({
228
+ clientId: request.clientId,
229
+ webConnectionId: request.webConnectionId,
230
+ webConnectionGeneration: request.webConnectionGeneration,
231
+ });
232
+ if (client?.userId === request.ownerUserId) {
233
+ await sendToWebClient(client, publicSessionMessage(agentId, request.response));
234
+ }
235
+ return true;
236
+ }
237
+
238
+ if (msg.type === 'browser_session_snapshot' && msg.requestId) {
239
+ const request = completeBrowserRequest(agentId, msg, {
240
+ consume: true,
241
+ kinds: ['browser_session_get', 'browser_session_close'],
242
+ });
243
+ if (!request) return true;
244
+ const client = browserClientForPeer({
245
+ clientId: request.clientId,
246
+ webConnectionId: request.webConnectionId,
247
+ webConnectionGeneration: request.webConnectionGeneration,
248
+ });
249
+ if (client?.userId === request.ownerUserId) {
250
+ const route = getBrowserRoute(agentId, msg.browserSessionId);
251
+ if (route) {
252
+ route.revision = Number(msg.revision) || route.revision;
253
+ route.state = clean(msg.state, 32) || route.state;
254
+ if (route.state === 'closed' || route.state === 'failed') {
255
+ deleteBrowserRoute(agentId, route.browserSessionId);
256
+ }
257
+ }
258
+ await sendToWebClient(client, publicSessionMessage(agentId, request.response));
259
+ }
260
+ return true;
261
+ }
262
+
263
+ if (msg.type === 'browser_session_snapshot') {
264
+ const route = getBrowserRoute(agentId, msg.browserSessionId);
265
+ if (!route) return true;
266
+ route.revision = Number(msg.revision) || route.revision;
267
+ route.state = clean(msg.state, 32) || route.state;
268
+ route.updatedAt = Date.now();
269
+ const recipients = [];
270
+ for (const peer of browserPeers.values()) {
271
+ if (peer.agentId !== agentId || peer.browserSessionId !== route.browserSessionId) continue;
272
+ const client = browserClientForPeer(peer);
273
+ if (client && !recipients.includes(client)) recipients.push(client);
274
+ }
275
+ if (route.state === 'closed' || route.state === 'failed') {
276
+ deleteBrowserRoute(agentId, route.browserSessionId);
277
+ }
278
+ const snapshot = publicSessionMessage(agentId, msg);
279
+ for (const client of recipients) await sendToWebClient(client, snapshot);
280
+ return true;
281
+ }
282
+
283
+ const peer = getBrowserPeer(msg.peerId);
284
+ if (!peer || peer.agentId !== agentId
285
+ || peer.browserSessionId !== msg.browserSessionId
286
+ || peer.connectionGeneration !== Number(msg.connectionGeneration)) return true;
287
+ const route = getBrowserRoute(agentId, peer.browserSessionId);
288
+ if (!route || route.ownerUserId !== peer.ownerUserId) {
289
+ deleteBrowserPeer(peer.peerId);
290
+ return true;
291
+ }
292
+ const client = browserClientForPeer(peer);
293
+ if (!client || client.userId !== route.ownerUserId) {
294
+ deleteBrowserPeer(peer.peerId);
295
+ return true;
296
+ }
297
+
298
+ if (msg.type === 'browser_peer_prepared') {
299
+ peer.state = 'prepared';
300
+ await sendToWebClient(client, {
301
+ type: msg.type,
302
+ agentId,
303
+ browserSessionId: peer.browserSessionId,
304
+ peerId: peer.peerId,
305
+ requestId: peer.requestId,
306
+ connectionGeneration: peer.connectionGeneration,
307
+ iceTransportPolicy: peer.iceTransportPolicy,
308
+ iceServers: peer.webIceServers || [],
309
+ role: peer.role,
310
+ });
311
+ if (peer.pendingOffer) {
312
+ await sendToWebClient(client, peer.pendingOffer);
313
+ peer.pendingOffer = null;
314
+ peer.state = 'offered';
315
+ }
316
+ for (const candidate of peer.pendingCandidates.splice(0)) {
317
+ await sendToWebClient(client, candidate);
318
+ }
319
+ return true;
320
+ }
321
+ if (msg.type === 'browser_peer_offer') {
322
+ const description = msg.description;
323
+ if (description?.type !== 'offer' || typeof description.sdp !== 'string' || description.sdp.length > 96 * 1024) {
324
+ deleteBrowserPeer(peer.peerId);
325
+ return true;
326
+ }
327
+ const offer = {
328
+ type: msg.type,
329
+ agentId,
330
+ browserSessionId: peer.browserSessionId,
331
+ peerId: peer.peerId,
332
+ requestId: peer.requestId,
333
+ connectionGeneration: peer.connectionGeneration,
334
+ description: { type: 'offer', sdp: description.sdp },
335
+ iceServers: peer.webIceServers || [],
336
+ role: peer.role,
337
+ };
338
+ if (peer.state !== 'prepared' && peer.state !== 'offered') {
339
+ peer.pendingOffer = offer;
340
+ return true;
341
+ }
342
+ peer.state = 'offered';
343
+ await sendToWebClient(client, offer);
344
+ return true;
345
+ }
346
+ if (msg.type === 'browser_peer_ice_candidate') {
347
+ if (peer.agentCandidateCount >= 128) return true;
348
+ const raw = msg.candidate;
349
+ if (raw != null && (typeof raw !== 'object'
350
+ || typeof raw.candidate !== 'string'
351
+ || raw.candidate.length > 4096)) return true;
352
+ peer.agentCandidateCount += 1;
353
+ const candidate = {
354
+ type: msg.type,
355
+ agentId,
356
+ browserSessionId: peer.browserSessionId,
357
+ peerId: peer.peerId,
358
+ connectionGeneration: peer.connectionGeneration,
359
+ candidate: raw == null ? null : {
360
+ candidate: raw.candidate,
361
+ sdpMid: clean(raw.sdpMid, 256) || null,
362
+ sdpMLineIndex: Number.isInteger(raw.sdpMLineIndex) ? raw.sdpMLineIndex : null,
363
+ usernameFragment: clean(raw.usernameFragment, 256) || null,
364
+ },
365
+ };
366
+ if (peer.state !== 'prepared' && peer.state !== 'offered') {
367
+ if (peer.pendingCandidates.length < 128) peer.pendingCandidates.push(candidate);
368
+ return true;
369
+ }
370
+ await sendToWebClient(client, candidate);
371
+ return true;
372
+ }
373
+ if (msg.type === 'browser_peer_state') {
374
+ peer.state = clean(msg.state, 32) || peer.state;
375
+ if (peer.state === 'connected') peer.expiresAt = null;
376
+ if (['failed', 'disconnected', 'closed'].includes(peer.state)) deleteBrowserPeer(peer.peerId);
377
+ }
378
+ if (msg.type === 'browser_peer_error') deleteBrowserPeer(peer.peerId);
379
+ await sendToWebClient(client, {
380
+ type: msg.type,
381
+ agentId,
382
+ browserSessionId: peer.browserSessionId,
383
+ peerId: peer.peerId,
384
+ connectionGeneration: peer.connectionGeneration,
385
+ requestId: peer.requestId,
386
+ state: clean(msg.state, 32) || null,
387
+ code: clean(msg.code, 128) || null,
388
+ safeError: clean(msg.safeError, 500) || null,
389
+ });
390
+ return true;
391
+ }
392
+
393
+ export { AGENT_BROWSER_TYPES };
@@ -74,6 +74,20 @@ export async function handleAgentSync(agentId, agent, msg) {
74
74
  break;
75
75
  }
76
76
 
77
+ case 'agent_capabilities_updated': {
78
+ const capabilities = Array.isArray(msg.capabilities)
79
+ ? [...new Set(msg.capabilities.filter(value => (
80
+ typeof value === 'string' && value.length > 0 && value.length <= 128
81
+ )).slice(0, 128))]
82
+ : null;
83
+ if (!capabilities || capabilities.length === 0) break;
84
+ agent.capabilities = capabilities;
85
+ // Transport encryption negotiation is connection-scoped and immutable.
86
+ // A runtime capability refresh must never flip framing mid-connection.
87
+ await broadcastAgentList();
88
+ break;
89
+ }
90
+
77
91
  case 'agent_metrics': {
78
92
  agent.metrics = normalizeAgentMetrics(msg.metrics || {});
79
93
  agent.metricsUpdatedAt = Date.now();