neoagent 2.1.18-beta.21 → 2.1.18-beta.22
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/docs/capabilities.md +3 -1
- package/docs/configuration.md +2 -0
- package/extensions/chrome-browser/background.mjs +209 -0
- package/extensions/chrome-browser/manifest.json +17 -0
- package/extensions/chrome-browser/popup.css +69 -0
- package/extensions/chrome-browser/popup.html +28 -0
- package/extensions/chrome-browser/popup.js +81 -0
- package/extensions/chrome-browser/protocol.mjs +309 -0
- package/package.json +4 -2
- package/server/config/origins.js +10 -0
- package/server/db/database.js +29 -0
- package/server/http/routes.js +1 -0
- package/server/index.js +2 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +31247 -31180
- package/server/routes/browser_extension.js +133 -0
- package/server/services/ai/capabilityHealth.js +27 -0
- package/server/services/browser/extension/gateway.js +65 -0
- package/server/services/browser/extension/protocol.js +49 -0
- package/server/services/browser/extension/provider.js +178 -0
- package/server/services/browser/extension/registry.js +353 -0
- package/server/services/browser/extension/zip.js +133 -0
- package/server/services/manager.js +18 -0
- package/server/services/runtime/manager.js +7 -31
- package/server/services/runtime/settings.js +0 -4
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const db = require('../../../db/database');
|
|
3
|
+
const {
|
|
4
|
+
ExtensionBrowserUnavailableError,
|
|
5
|
+
createCommandMessage,
|
|
6
|
+
parseExtensionMessage,
|
|
7
|
+
} = require('./protocol');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_PAIRING_TTL_MS = 10 * 60 * 1000;
|
|
10
|
+
const DEFAULT_COMMAND_TIMEOUT_MS = 30 * 1000;
|
|
11
|
+
|
|
12
|
+
function sha256(value) {
|
|
13
|
+
return crypto.createHash('sha256').update(String(value || '')).digest('hex');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function randomSecret(bytes = 32) {
|
|
17
|
+
return crypto.randomBytes(bytes).toString('base64url');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isoDate(offsetMs = 0) {
|
|
21
|
+
return new Date(Date.now() + offsetMs).toISOString();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function safeJson(value) {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.stringify(value || {});
|
|
27
|
+
} catch {
|
|
28
|
+
return '{}';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseJson(value) {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(value || '{}') || {};
|
|
35
|
+
} catch {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class BrowserExtensionRegistry {
|
|
41
|
+
constructor(options = {}) {
|
|
42
|
+
this.db = options.db || db;
|
|
43
|
+
this.commandTimeoutMs = Number(options.commandTimeoutMs || process.env.NEOAGENT_BROWSER_EXTENSION_COMMAND_TIMEOUT_MS || DEFAULT_COMMAND_TIMEOUT_MS);
|
|
44
|
+
this.pairingTtlMs = Number(options.pairingTtlMs || process.env.NEOAGENT_BROWSER_EXTENSION_PAIRING_TTL_MS || DEFAULT_PAIRING_TTL_MS);
|
|
45
|
+
this.connectionsByUser = new Map();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
createPairingRequest(options = {}) {
|
|
49
|
+
const pairingId = crypto.randomUUID();
|
|
50
|
+
const pairingSecret = randomSecret(32);
|
|
51
|
+
const expiresAt = isoDate(this.pairingTtlMs);
|
|
52
|
+
this.db.prepare(
|
|
53
|
+
`INSERT INTO browser_extension_pairing_requests (
|
|
54
|
+
id, pairing_secret_hash, status, expires_at, metadata_json
|
|
55
|
+
) VALUES (?, ?, 'pending', ?, ?)`
|
|
56
|
+
).run(pairingId, sha256(pairingSecret), expiresAt, safeJson({
|
|
57
|
+
extensionName: options.extensionName || null,
|
|
58
|
+
userAgent: options.userAgent || null,
|
|
59
|
+
}));
|
|
60
|
+
return { pairingId, pairingSecret, expiresAt };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getPairingRequest(pairingId) {
|
|
64
|
+
return this.db.prepare(
|
|
65
|
+
`SELECT * FROM browser_extension_pairing_requests WHERE id = ?`
|
|
66
|
+
).get(String(pairingId || '')) || null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
approvePairing(pairingId, userId) {
|
|
70
|
+
const row = this.getPairingRequest(pairingId);
|
|
71
|
+
if (!row) {
|
|
72
|
+
const error = new Error('Pairing request not found.');
|
|
73
|
+
error.status = 404;
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
if (row.status !== 'pending') {
|
|
77
|
+
const error = new Error('Pairing request is no longer pending.');
|
|
78
|
+
error.status = 409;
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
if (Date.parse(row.expires_at) <= Date.now()) {
|
|
82
|
+
this.db.prepare(
|
|
83
|
+
`UPDATE browser_extension_pairing_requests SET status = 'expired' WHERE id = ?`
|
|
84
|
+
).run(row.id);
|
|
85
|
+
const error = new Error('Pairing request expired.');
|
|
86
|
+
error.status = 410;
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
this.db.prepare(
|
|
90
|
+
`UPDATE browser_extension_pairing_requests
|
|
91
|
+
SET user_id = ?, status = 'approved', approved_at = datetime('now')
|
|
92
|
+
WHERE id = ?`
|
|
93
|
+
).run(userId, row.id);
|
|
94
|
+
return { success: true, pairingId: row.id };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
claimPairing(pairingId, pairingSecret, options = {}) {
|
|
98
|
+
const row = this.getPairingRequest(pairingId);
|
|
99
|
+
if (!row) {
|
|
100
|
+
const error = new Error('Pairing request not found.');
|
|
101
|
+
error.status = 404;
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
if (row.status !== 'approved' || !row.user_id) {
|
|
105
|
+
const error = new Error('Pairing request is not approved.');
|
|
106
|
+
error.status = 409;
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
if (Date.parse(row.expires_at) <= Date.now()) {
|
|
110
|
+
this.db.prepare(
|
|
111
|
+
`UPDATE browser_extension_pairing_requests SET status = 'expired' WHERE id = ?`
|
|
112
|
+
).run(row.id);
|
|
113
|
+
const error = new Error('Pairing request expired.');
|
|
114
|
+
error.status = 410;
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
if (sha256(pairingSecret) !== row.pairing_secret_hash) {
|
|
118
|
+
const error = new Error('Invalid pairing secret.');
|
|
119
|
+
error.status = 401;
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const tokenId = crypto.randomUUID();
|
|
124
|
+
const token = `nbe_${randomSecret(36)}`;
|
|
125
|
+
const extensionName = String(options.extensionName || 'Chrome Extension').slice(0, 120);
|
|
126
|
+
const metadata = {
|
|
127
|
+
extensionName,
|
|
128
|
+
userAgent: options.userAgent || null,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const tx = this.db.transaction(() => {
|
|
132
|
+
this.db.prepare(
|
|
133
|
+
`UPDATE browser_extension_pairing_requests
|
|
134
|
+
SET status = 'claimed', claimed_at = datetime('now')
|
|
135
|
+
WHERE id = ?`
|
|
136
|
+
).run(row.id);
|
|
137
|
+
this.db.prepare(
|
|
138
|
+
`INSERT INTO browser_extension_tokens (
|
|
139
|
+
id, user_id, token_hash, name, status, metadata_json
|
|
140
|
+
) VALUES (?, ?, ?, ?, 'active', ?)`
|
|
141
|
+
).run(tokenId, row.user_id, sha256(token), extensionName, safeJson(metadata));
|
|
142
|
+
});
|
|
143
|
+
tx();
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
token,
|
|
147
|
+
tokenId,
|
|
148
|
+
userId: row.user_id,
|
|
149
|
+
extensionName,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
validateToken(token) {
|
|
154
|
+
const row = this.db.prepare(
|
|
155
|
+
`SELECT * FROM browser_extension_tokens
|
|
156
|
+
WHERE token_hash = ? AND status = 'active'`
|
|
157
|
+
).get(sha256(token));
|
|
158
|
+
if (!row) return null;
|
|
159
|
+
return {
|
|
160
|
+
...row,
|
|
161
|
+
metadata: parseJson(row.metadata_json),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
registerConnection(tokenRow, ws, meta = {}) {
|
|
166
|
+
const userId = String(tokenRow.user_id);
|
|
167
|
+
const existing = this.connectionsByUser.get(userId);
|
|
168
|
+
if (existing && existing.ws !== ws) {
|
|
169
|
+
existing.close('replaced by a newer extension connection');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const connection = new ExtensionBrowserConnection({
|
|
173
|
+
registry: this,
|
|
174
|
+
ws,
|
|
175
|
+
userId,
|
|
176
|
+
tokenId: tokenRow.id,
|
|
177
|
+
meta: { ...tokenRow.metadata, ...meta },
|
|
178
|
+
timeoutMs: this.commandTimeoutMs,
|
|
179
|
+
});
|
|
180
|
+
this.connectionsByUser.set(userId, connection);
|
|
181
|
+
this.db.prepare(
|
|
182
|
+
`UPDATE browser_extension_tokens
|
|
183
|
+
SET last_connected_at = datetime('now'), last_seen_at = datetime('now')
|
|
184
|
+
WHERE id = ?`
|
|
185
|
+
).run(tokenRow.id);
|
|
186
|
+
return connection;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
unregisterConnection(connection) {
|
|
190
|
+
const userId = String(connection.userId);
|
|
191
|
+
if (this.connectionsByUser.get(userId) === connection) {
|
|
192
|
+
this.connectionsByUser.delete(userId);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
getConnection(userId) {
|
|
197
|
+
return this.connectionsByUser.get(String(userId));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
isConnected(userId) {
|
|
201
|
+
return Boolean(this.getConnection(userId)?.isOpen());
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async dispatch(userId, command, payload = {}, options = {}) {
|
|
205
|
+
const connection = this.getConnection(userId);
|
|
206
|
+
if (!connection || !connection.isOpen()) {
|
|
207
|
+
throw new ExtensionBrowserUnavailableError();
|
|
208
|
+
}
|
|
209
|
+
const result = await connection.sendCommand(command, payload, options);
|
|
210
|
+
this.db.prepare(
|
|
211
|
+
`UPDATE browser_extension_tokens SET last_seen_at = datetime('now') WHERE id = ?`
|
|
212
|
+
).run(connection.tokenId);
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
getStatus(userId) {
|
|
217
|
+
const connected = this.getConnection(userId);
|
|
218
|
+
const tokens = this.db.prepare(
|
|
219
|
+
`SELECT id, name, status, last_connected_at, last_seen_at, revoked_at, created_at, metadata_json
|
|
220
|
+
FROM browser_extension_tokens
|
|
221
|
+
WHERE user_id = ?
|
|
222
|
+
ORDER BY created_at DESC`
|
|
223
|
+
).all(userId).map((row) => ({
|
|
224
|
+
...row,
|
|
225
|
+
metadata: parseJson(row.metadata_json),
|
|
226
|
+
metadata_json: undefined,
|
|
227
|
+
}));
|
|
228
|
+
return {
|
|
229
|
+
connected: Boolean(connected?.isOpen()),
|
|
230
|
+
activeTokenId: connected?.tokenId || null,
|
|
231
|
+
tokens,
|
|
232
|
+
connectedMeta: connected?.meta || null,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
revoke(userId, tokenId = null) {
|
|
237
|
+
const targetTokenId = tokenId ? String(tokenId) : null;
|
|
238
|
+
if (targetTokenId) {
|
|
239
|
+
this.db.prepare(
|
|
240
|
+
`UPDATE browser_extension_tokens
|
|
241
|
+
SET status = 'revoked', revoked_at = datetime('now')
|
|
242
|
+
WHERE user_id = ? AND id = ?`
|
|
243
|
+
).run(userId, targetTokenId);
|
|
244
|
+
} else {
|
|
245
|
+
this.db.prepare(
|
|
246
|
+
`UPDATE browser_extension_tokens
|
|
247
|
+
SET status = 'revoked', revoked_at = datetime('now')
|
|
248
|
+
WHERE user_id = ? AND status = 'active'`
|
|
249
|
+
).run(userId);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const connection = this.getConnection(userId);
|
|
253
|
+
if (connection && (!targetTokenId || connection.tokenId === targetTokenId)) {
|
|
254
|
+
connection.close('extension token revoked');
|
|
255
|
+
}
|
|
256
|
+
return { success: true };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
closeAll() {
|
|
260
|
+
for (const connection of this.connectionsByUser.values()) {
|
|
261
|
+
connection.close('server shutdown');
|
|
262
|
+
}
|
|
263
|
+
this.connectionsByUser.clear();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
class ExtensionBrowserConnection {
|
|
268
|
+
constructor({ registry, ws, userId, tokenId, meta, timeoutMs }) {
|
|
269
|
+
this.registry = registry;
|
|
270
|
+
this.ws = ws;
|
|
271
|
+
this.userId = userId;
|
|
272
|
+
this.tokenId = tokenId;
|
|
273
|
+
this.meta = meta || {};
|
|
274
|
+
this.timeoutMs = timeoutMs;
|
|
275
|
+
this.pending = new Map();
|
|
276
|
+
|
|
277
|
+
ws.on('message', (data) => this.#handleMessage(data));
|
|
278
|
+
ws.on('close', () => this.#closePending(new ExtensionBrowserUnavailableError('Extension browser disconnected.')));
|
|
279
|
+
ws.on('error', (error) => this.#closePending(error));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
isOpen() {
|
|
283
|
+
return this.ws && this.ws.readyState === 1;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
close(reason) {
|
|
287
|
+
try {
|
|
288
|
+
if (this.isOpen()) {
|
|
289
|
+
this.ws.close(1000, String(reason || 'closing').slice(0, 120));
|
|
290
|
+
}
|
|
291
|
+
} catch {}
|
|
292
|
+
this.registry.unregisterConnection(this);
|
|
293
|
+
this.#closePending(new ExtensionBrowserUnavailableError('Extension browser disconnected.'));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
sendCommand(command, payload = {}, options = {}) {
|
|
297
|
+
if (!this.isOpen()) {
|
|
298
|
+
return Promise.reject(new ExtensionBrowserUnavailableError());
|
|
299
|
+
}
|
|
300
|
+
const id = crypto.randomUUID();
|
|
301
|
+
const timeoutMs = Number(options.timeoutMs || this.timeoutMs);
|
|
302
|
+
const message = createCommandMessage(id, command, payload);
|
|
303
|
+
return new Promise((resolve, reject) => {
|
|
304
|
+
const timer = setTimeout(() => {
|
|
305
|
+
this.pending.delete(id);
|
|
306
|
+
reject(new Error(`Browser extension command timed out: ${command}`));
|
|
307
|
+
}, timeoutMs);
|
|
308
|
+
this.pending.set(id, { resolve, reject, timer, command });
|
|
309
|
+
try {
|
|
310
|
+
this.ws.send(JSON.stringify(message));
|
|
311
|
+
} catch (error) {
|
|
312
|
+
clearTimeout(timer);
|
|
313
|
+
this.pending.delete(id);
|
|
314
|
+
reject(error);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
#handleMessage(data) {
|
|
320
|
+
let message;
|
|
321
|
+
try {
|
|
322
|
+
message = parseExtensionMessage(data);
|
|
323
|
+
} catch {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (!message || message.type !== 'result' || !message.id) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const pending = this.pending.get(message.id);
|
|
330
|
+
if (!pending) return;
|
|
331
|
+
clearTimeout(pending.timer);
|
|
332
|
+
this.pending.delete(message.id);
|
|
333
|
+
if (message.ok === false) {
|
|
334
|
+
pending.reject(new Error(message.error || `Browser extension command failed: ${pending.command}`));
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
pending.resolve(message.result || {});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
#closePending(error) {
|
|
341
|
+
this.registry.unregisterConnection(this);
|
|
342
|
+
for (const pending of this.pending.values()) {
|
|
343
|
+
clearTimeout(pending.timer);
|
|
344
|
+
pending.reject(error);
|
|
345
|
+
}
|
|
346
|
+
this.pending.clear();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
module.exports = {
|
|
351
|
+
BrowserExtensionRegistry,
|
|
352
|
+
sha256,
|
|
353
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const CRC_TABLE = (() => {
|
|
5
|
+
const table = new Uint32Array(256);
|
|
6
|
+
for (let i = 0; i < 256; i += 1) {
|
|
7
|
+
let c = i;
|
|
8
|
+
for (let j = 0; j < 8; j += 1) {
|
|
9
|
+
c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
|
|
10
|
+
}
|
|
11
|
+
table[i] = c >>> 0;
|
|
12
|
+
}
|
|
13
|
+
return table;
|
|
14
|
+
})();
|
|
15
|
+
|
|
16
|
+
function crc32(buffer) {
|
|
17
|
+
let crc = 0xffffffff;
|
|
18
|
+
for (const byte of buffer) {
|
|
19
|
+
crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
|
20
|
+
}
|
|
21
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function dosDateTime(date = new Date()) {
|
|
25
|
+
const year = Math.max(1980, date.getFullYear());
|
|
26
|
+
const dosTime =
|
|
27
|
+
(date.getHours() << 11) |
|
|
28
|
+
(date.getMinutes() << 5) |
|
|
29
|
+
Math.floor(date.getSeconds() / 2);
|
|
30
|
+
const dosDate =
|
|
31
|
+
((year - 1980) << 9) |
|
|
32
|
+
((date.getMonth() + 1) << 5) |
|
|
33
|
+
date.getDate();
|
|
34
|
+
return { dosTime, dosDate };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function listFiles(rootDir, prefix = '') {
|
|
38
|
+
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
|
|
39
|
+
const files = [];
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
if (entry.name.startsWith('.')) continue;
|
|
42
|
+
const fullPath = path.join(rootDir, entry.name);
|
|
43
|
+
const archivePath = path.posix.join(prefix, entry.name);
|
|
44
|
+
if (entry.isDirectory()) {
|
|
45
|
+
files.push(...listFiles(fullPath, archivePath));
|
|
46
|
+
} else if (entry.isFile()) {
|
|
47
|
+
files.push({ fullPath, archivePath });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return files;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function u16(value) {
|
|
54
|
+
const buffer = Buffer.alloc(2);
|
|
55
|
+
buffer.writeUInt16LE(value);
|
|
56
|
+
return buffer;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function u32(value) {
|
|
60
|
+
const buffer = Buffer.alloc(4);
|
|
61
|
+
buffer.writeUInt32LE(value >>> 0);
|
|
62
|
+
return buffer;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function createZipFromDirectory(rootDir) {
|
|
66
|
+
const localParts = [];
|
|
67
|
+
const centralParts = [];
|
|
68
|
+
let offset = 0;
|
|
69
|
+
|
|
70
|
+
for (const file of listFiles(rootDir)) {
|
|
71
|
+
const content = fs.readFileSync(file.fullPath);
|
|
72
|
+
const name = Buffer.from(file.archivePath.replace(/\\/g, '/'), 'utf8');
|
|
73
|
+
const stat = fs.statSync(file.fullPath);
|
|
74
|
+
const { dosTime, dosDate } = dosDateTime(stat.mtime);
|
|
75
|
+
const crc = crc32(content);
|
|
76
|
+
|
|
77
|
+
const localHeader = Buffer.concat([
|
|
78
|
+
u32(0x04034b50),
|
|
79
|
+
u16(20),
|
|
80
|
+
u16(0),
|
|
81
|
+
u16(0),
|
|
82
|
+
u16(dosTime),
|
|
83
|
+
u16(dosDate),
|
|
84
|
+
u32(crc),
|
|
85
|
+
u32(content.length),
|
|
86
|
+
u32(content.length),
|
|
87
|
+
u16(name.length),
|
|
88
|
+
u16(0),
|
|
89
|
+
name,
|
|
90
|
+
]);
|
|
91
|
+
localParts.push(localHeader, content);
|
|
92
|
+
|
|
93
|
+
const centralHeader = Buffer.concat([
|
|
94
|
+
u32(0x02014b50),
|
|
95
|
+
u16(20),
|
|
96
|
+
u16(20),
|
|
97
|
+
u16(0),
|
|
98
|
+
u16(0),
|
|
99
|
+
u16(dosTime),
|
|
100
|
+
u16(dosDate),
|
|
101
|
+
u32(crc),
|
|
102
|
+
u32(content.length),
|
|
103
|
+
u32(content.length),
|
|
104
|
+
u16(name.length),
|
|
105
|
+
u16(0),
|
|
106
|
+
u16(0),
|
|
107
|
+
u16(0),
|
|
108
|
+
u16(0),
|
|
109
|
+
u32(0),
|
|
110
|
+
u32(offset),
|
|
111
|
+
name,
|
|
112
|
+
]);
|
|
113
|
+
centralParts.push(centralHeader);
|
|
114
|
+
offset += localHeader.length + content.length;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const centralDirectory = Buffer.concat(centralParts);
|
|
118
|
+
const end = Buffer.concat([
|
|
119
|
+
u32(0x06054b50),
|
|
120
|
+
u16(0),
|
|
121
|
+
u16(0),
|
|
122
|
+
u16(centralParts.length),
|
|
123
|
+
u16(centralParts.length),
|
|
124
|
+
u32(centralDirectory.length),
|
|
125
|
+
u32(offset),
|
|
126
|
+
u16(0),
|
|
127
|
+
]);
|
|
128
|
+
return Buffer.concat([...localParts, centralDirectory, end]);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = {
|
|
132
|
+
createZipFromDirectory,
|
|
133
|
+
};
|
|
@@ -19,6 +19,7 @@ const { CLIExecutor } = require('./cli/executor');
|
|
|
19
19
|
const { IntegrationManager } = require('./integrations/manager');
|
|
20
20
|
const { ArtifactStore } = require('./artifacts/store');
|
|
21
21
|
const { RuntimeManager } = require('./runtime/manager');
|
|
22
|
+
const { BrowserExtensionRegistry } = require('./browser/extension/registry');
|
|
22
23
|
const { assertRuntimeValidation, getRuntimeValidation } = require('./runtime/validation');
|
|
23
24
|
const {
|
|
24
25
|
getErrorMessage,
|
|
@@ -47,6 +48,12 @@ function createArtifactStore(app) {
|
|
|
47
48
|
return artifactStore;
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
function createBrowserExtensionRegistry(app) {
|
|
52
|
+
const registry = registerLocal(app, 'browserExtensionRegistry', new BrowserExtensionRegistry());
|
|
53
|
+
logServiceReady('Browser extension registry ready');
|
|
54
|
+
return registry;
|
|
55
|
+
}
|
|
56
|
+
|
|
50
57
|
function createMemoryManager(app) {
|
|
51
58
|
const memoryManager = registerLocal(app, 'memoryManager', new MemoryManager());
|
|
52
59
|
logServiceReady('Memory manager ready');
|
|
@@ -260,6 +267,7 @@ function createRuntimeManager(app, cliExecutor) {
|
|
|
260
267
|
new RuntimeManager({
|
|
261
268
|
cliExecutor,
|
|
262
269
|
artifactStore: app.locals.artifactStore,
|
|
270
|
+
browserExtensionRegistry: app.locals.browserExtensionRegistry,
|
|
263
271
|
getHostBrowserProvider: (userId) => {
|
|
264
272
|
const resolver = app.locals.getBrowserControllerForUser;
|
|
265
273
|
if (typeof resolver === 'function') {
|
|
@@ -428,6 +436,7 @@ async function startServices(app, io) {
|
|
|
428
436
|
try {
|
|
429
437
|
const cliExecutor = createCliExecutor(app);
|
|
430
438
|
const artifactStore = createArtifactStore(app);
|
|
439
|
+
createBrowserExtensionRegistry(app);
|
|
431
440
|
const memoryManager = createMemoryManager(app);
|
|
432
441
|
const mcpClient = createMcpClient(app);
|
|
433
442
|
const integrationManager = createIntegrationManager(app);
|
|
@@ -560,6 +569,15 @@ async function stopServices(app) {
|
|
|
560
569
|
);
|
|
561
570
|
}
|
|
562
571
|
|
|
572
|
+
if (app.locals.browserExtensionRegistry) {
|
|
573
|
+
try {
|
|
574
|
+
app.locals.browserExtensionRegistry.closeAll();
|
|
575
|
+
logServiceReady('Browser extension connections closed');
|
|
576
|
+
} catch (err) {
|
|
577
|
+
console.error('[BrowserExtension] Shutdown error:', getErrorMessage(err));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
563
581
|
if (app.locals.cliExecutor) {
|
|
564
582
|
try {
|
|
565
583
|
app.locals.cliExecutor.killAll('shutdown');
|
|
@@ -3,36 +3,7 @@ const { LocalVmExecutionBackend } = require('./backends/local-vm');
|
|
|
3
3
|
const { RemoteWorkerExecutionBackend } = require('./backends/remote');
|
|
4
4
|
const { QemuVmManager } = require('./qemu');
|
|
5
5
|
const { getRuntimeSettings } = require('./settings');
|
|
6
|
-
|
|
7
|
-
class UnsupportedExtensionBrowserProvider {
|
|
8
|
-
constructor() {
|
|
9
|
-
this.headless = false;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
#unsupported() {
|
|
13
|
-
return { error: 'Browser extension backend is planned but not implemented yet.' };
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
navigate() { return this.#unsupported(); }
|
|
17
|
-
click() { return this.#unsupported(); }
|
|
18
|
-
clickPoint() { return this.#unsupported(); }
|
|
19
|
-
type() { return this.#unsupported(); }
|
|
20
|
-
typeText() { return this.#unsupported(); }
|
|
21
|
-
pressKey() { return this.#unsupported(); }
|
|
22
|
-
scroll() { return this.#unsupported(); }
|
|
23
|
-
extract() { return this.#unsupported(); }
|
|
24
|
-
evaluate() { return this.#unsupported(); }
|
|
25
|
-
screenshot() { return this.#unsupported(); }
|
|
26
|
-
launch() { return this.#unsupported(); }
|
|
27
|
-
closeBrowser() { return Promise.resolve({ success: true }); }
|
|
28
|
-
fill() { return this.#unsupported(); }
|
|
29
|
-
extractContent() { return this.#unsupported(); }
|
|
30
|
-
executeJS() { return this.#unsupported(); }
|
|
31
|
-
getPageInfo() { return Promise.resolve({ url: null, title: null, unsupported: true }); }
|
|
32
|
-
isLaunched() { return false; }
|
|
33
|
-
getPageCount() { return 0; }
|
|
34
|
-
setHeadless() { return Promise.resolve({ success: false, unsupported: true }); }
|
|
35
|
-
}
|
|
6
|
+
const { ExtensionBrowserProvider } = require('../browser/extension/provider');
|
|
36
7
|
|
|
37
8
|
class RuntimeManager {
|
|
38
9
|
constructor(options = {}) {
|
|
@@ -50,6 +21,11 @@ class RuntimeManager {
|
|
|
50
21
|
getToken: (userId) => getRuntimeSettings(userId).remote_worker_token,
|
|
51
22
|
artifactStore: options.artifactStore,
|
|
52
23
|
});
|
|
24
|
+
this.getExtensionBrowserProvider = options.getExtensionBrowserProvider || ((userId) => new ExtensionBrowserProvider({
|
|
25
|
+
registry: options.browserExtensionRegistry,
|
|
26
|
+
artifactStore: options.artifactStore,
|
|
27
|
+
userId,
|
|
28
|
+
}));
|
|
53
29
|
}
|
|
54
30
|
|
|
55
31
|
getSettings(userId) {
|
|
@@ -72,7 +48,7 @@ class RuntimeManager {
|
|
|
72
48
|
async getBrowserProviderForUser(userId) {
|
|
73
49
|
const settings = this.getSettings(userId);
|
|
74
50
|
if (settings.browser_backend === 'extension') {
|
|
75
|
-
return
|
|
51
|
+
return this.getExtensionBrowserProvider(userId);
|
|
76
52
|
}
|
|
77
53
|
const backend = this.resolveBackend(userId, settings.browser_backend === 'host' ? 'host' : settings.browser_backend);
|
|
78
54
|
return backend.getBrowserProviderForUser(userId);
|
|
@@ -146,10 +146,6 @@ function validateRuntimeSettings(raw = {}) {
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
if (settings.browser_backend === 'extension') {
|
|
150
|
-
issues.push('The browser extension backend is planned but not available in this build.');
|
|
151
|
-
}
|
|
152
|
-
|
|
153
149
|
if (needsRemoteWorker && !settings.remote_worker_base_url) {
|
|
154
150
|
issues.push('A remote worker URL is required when any runtime backend uses remote execution.');
|
|
155
151
|
} else if (settings.remote_worker_base_url && !isValidRemoteWorkerBaseUrl(settings.remote_worker_base_url)) {
|