@orkestrel/browser 0.0.23 → 0.0.24
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/dist/bin/main.js +9 -3
- package/dist/bin/main.js.map +1 -1
- package/dist/src/core/index.cjs +58 -13
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +18 -10
- package/dist/src/core/index.d.ts +18 -10
- package/dist/src/core/index.js +58 -13
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +392 -84
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +58 -4
- package/dist/src/server/index.d.ts +58 -4
- package/dist/src/server/index.js +390 -86
- package/dist/src/server/index.js.map +1 -1
- package/package.json +4 -4
|
@@ -237,6 +237,84 @@ var BROWSER_SERVER_OPTIONS = "BROWSER_SERVER_OPTIONS";
|
|
|
237
237
|
var BROWSER_SERVER_CRASH = "BROWSER_SERVER_CRASH";
|
|
238
238
|
/** Names an interrupted call whose outcome is unknown. */
|
|
239
239
|
var BROWSER_SERVER_UNRESOLVED = "BROWSER_SERVER_UNRESOLVED";
|
|
240
|
+
/** Names the refusal when every browser is admitted to a holder. */
|
|
241
|
+
var BROWSER_SERVER_BUSY = "BROWSER_SERVER_BUSY";
|
|
242
|
+
/** Names the refusal for an unknown or ended holder. */
|
|
243
|
+
var BROWSER_SERVER_HOLDER = "BROWSER_SERVER_HOLDER";
|
|
244
|
+
/** Defines the holder tools independently of the shared browser's vocabulary. */
|
|
245
|
+
var BROWSER_SERVER_COPY = Object.freeze({
|
|
246
|
+
acquire: Object.freeze({
|
|
247
|
+
name: "acquire",
|
|
248
|
+
description: "Acquires an independent browser and returns its holder id and tool catalog. Destroy the holder when its work ends.",
|
|
249
|
+
parameters: Object.freeze({
|
|
250
|
+
type: "object",
|
|
251
|
+
properties: Object.freeze({ purpose: Object.freeze({
|
|
252
|
+
type: "string",
|
|
253
|
+
description: "The work this browser will serve."
|
|
254
|
+
}) }),
|
|
255
|
+
required: Object.freeze(["purpose"])
|
|
256
|
+
})
|
|
257
|
+
}),
|
|
258
|
+
execute: Object.freeze({
|
|
259
|
+
name: "execute",
|
|
260
|
+
description: "Runs a tool on the selected holder, with that tool’s arguments inside arguments. Get its available tools with tools.",
|
|
261
|
+
parameters: Object.freeze({
|
|
262
|
+
type: "object",
|
|
263
|
+
properties: Object.freeze({
|
|
264
|
+
holder: Object.freeze({
|
|
265
|
+
type: "string",
|
|
266
|
+
description: "The holder id returned by acquire."
|
|
267
|
+
}),
|
|
268
|
+
name: Object.freeze({
|
|
269
|
+
type: "string",
|
|
270
|
+
description: "The tool name from this holder’s catalog."
|
|
271
|
+
}),
|
|
272
|
+
arguments: Object.freeze({
|
|
273
|
+
type: "object",
|
|
274
|
+
description: "The arguments for the selected tool."
|
|
275
|
+
})
|
|
276
|
+
}),
|
|
277
|
+
required: Object.freeze([
|
|
278
|
+
"holder",
|
|
279
|
+
"name",
|
|
280
|
+
"arguments"
|
|
281
|
+
])
|
|
282
|
+
}),
|
|
283
|
+
annotations: Object.freeze({
|
|
284
|
+
pure: false,
|
|
285
|
+
untrusted: true
|
|
286
|
+
})
|
|
287
|
+
}),
|
|
288
|
+
tools: Object.freeze({
|
|
289
|
+
name: "tools",
|
|
290
|
+
description: "Lists the selected holder’s tools, including tools supplied by its page.",
|
|
291
|
+
parameters: Object.freeze({
|
|
292
|
+
type: "object",
|
|
293
|
+
properties: Object.freeze({ holder: Object.freeze({
|
|
294
|
+
type: "string",
|
|
295
|
+
description: "The holder id returned by acquire."
|
|
296
|
+
}) }),
|
|
297
|
+
required: Object.freeze(["holder"])
|
|
298
|
+
}),
|
|
299
|
+
annotations: Object.freeze({
|
|
300
|
+
pure: true,
|
|
301
|
+
untrusted: true
|
|
302
|
+
})
|
|
303
|
+
}),
|
|
304
|
+
destroy: Object.freeze({
|
|
305
|
+
name: "destroy",
|
|
306
|
+
description: "Ends the selected holder and destroys its browser. Calls still running on that holder report an unresolved outcome.",
|
|
307
|
+
parameters: Object.freeze({
|
|
308
|
+
type: "object",
|
|
309
|
+
properties: Object.freeze({ holder: Object.freeze({
|
|
310
|
+
type: "string",
|
|
311
|
+
description: "The holder id returned by acquire."
|
|
312
|
+
}) }),
|
|
313
|
+
required: Object.freeze(["holder"])
|
|
314
|
+
}),
|
|
315
|
+
annotations: Object.freeze({ pure: false })
|
|
316
|
+
})
|
|
317
|
+
});
|
|
240
318
|
//#endregion
|
|
241
319
|
//#region src/server/errors.ts
|
|
242
320
|
/**
|
|
@@ -280,6 +358,25 @@ function isBrowserDestroyedError(value) {
|
|
|
280
358
|
//#endregion
|
|
281
359
|
//#region src/server/helpers.ts
|
|
282
360
|
/**
|
|
361
|
+
* Parses positive integer viewport dimensions in WIDTHxHEIGHT form.
|
|
362
|
+
* @param value - The viewport environment variable, or undefined when unset
|
|
363
|
+
* @returns The viewport, or undefined when absent or malformed
|
|
364
|
+
* @example
|
|
365
|
+
* parseBrowserViewport('1280x720') // { width: 1280, height: 720 }
|
|
366
|
+
*/
|
|
367
|
+
function parseBrowserViewport(value) {
|
|
368
|
+
if (value === void 0) return void 0;
|
|
369
|
+
const match = /^([0-9]+)x([0-9]+)$/u.exec(value);
|
|
370
|
+
if (match === null || match[0] !== value) return void 0;
|
|
371
|
+
const width = (0, _orkestrel_contract.parseInteger)(match[1]);
|
|
372
|
+
const height = (0, _orkestrel_contract.parseInteger)(match[2]);
|
|
373
|
+
if (width === void 0 || height === void 0 || !Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width <= 0 || height <= 0) return void 0;
|
|
374
|
+
return {
|
|
375
|
+
width,
|
|
376
|
+
height
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
283
380
|
* Probes whether a process has not been confirmed absent.
|
|
284
381
|
* @param pid - Process identifier to probe with signal zero
|
|
285
382
|
* @returns True if the host has not confirmed absence with ESRCH; false otherwise
|
|
@@ -945,7 +1042,7 @@ var FileBrowserWriter = class {
|
|
|
945
1042
|
};
|
|
946
1043
|
//#endregion
|
|
947
1044
|
//#region package.json
|
|
948
|
-
var version = "0.0.
|
|
1045
|
+
var version = "0.0.24";
|
|
949
1046
|
//#endregion
|
|
950
1047
|
//#region src/server/BrowserMCPServer.ts
|
|
951
1048
|
/**
|
|
@@ -960,8 +1057,11 @@ var version = "0.0.23";
|
|
|
960
1057
|
* absent. After the launch, a tool the toolset's manager adds under another name, such as a page
|
|
961
1058
|
* tool it adopts, is mirrored as a dispatcher with that tool's definition, and the mirror is
|
|
962
1059
|
* removed when the toolset withdraws the tool, so the server's tool list changes and a subscribed
|
|
963
|
-
* client is notified.
|
|
964
|
-
*
|
|
1060
|
+
* client is notified. The server also advertises `acquire`, `execute`, `tools`, and `destroy` with
|
|
1061
|
+
* `BROWSER_SERVER_COPY`. Startup warms the pool and leases the shared browser. The legacy
|
|
1062
|
+
* handshake and browser calls await that setup. Named holders each own a lease, a pending grant,
|
|
1063
|
+
* and loss notices; concurrent calls on a holder share its replacement grant. Ending a holder
|
|
1064
|
+
* destroys its token, and admission counts that holder until disposal settles.
|
|
965
1065
|
* A launch creates `ROOT/.profiles/PID-UUID/` exclusively, connects a browser that never attaches to an
|
|
966
1066
|
* existing endpoint, opens one page in an isolated context, and constructs the page toolset with
|
|
967
1067
|
* that context and the file journey and run stores under the root, on a manager the toolset owns.
|
|
@@ -971,6 +1071,8 @@ var version = "0.0.23";
|
|
|
971
1071
|
* A disconnect or current-page crash retires its slot. A failed call receives a liveness ping;
|
|
972
1072
|
* loss makes its outcome unresolved, while a known success remains successful. Pending losses
|
|
973
1073
|
* prefix the next outcome on a successor, or a refusal, and calls are never repeated.
|
|
1074
|
+
* Concurrent checks share one ping per slot. Cancelling a call ends only its wait; the ping keeps
|
|
1075
|
+
* its deadline and records loss for the next call.
|
|
974
1076
|
*
|
|
975
1077
|
* `start()` serves stdio and destroys the server at the end of its input, on `SIGINT`, and on
|
|
976
1078
|
* `SIGTERM`. `destroy()` stops reading requests and removes every dispatcher, then destroys every
|
|
@@ -994,6 +1096,7 @@ var BrowserMCPServer = class {
|
|
|
994
1096
|
#root;
|
|
995
1097
|
#headless;
|
|
996
1098
|
#executable;
|
|
1099
|
+
#viewport;
|
|
997
1100
|
#readonly;
|
|
998
1101
|
#launcher;
|
|
999
1102
|
#input;
|
|
@@ -1002,22 +1105,32 @@ var BrowserMCPServer = class {
|
|
|
1002
1105
|
#abort = new AbortController();
|
|
1003
1106
|
#signal;
|
|
1004
1107
|
#vocabulary;
|
|
1005
|
-
#
|
|
1006
|
-
#removed;
|
|
1007
|
-
#cleared;
|
|
1108
|
+
#mirrored;
|
|
1008
1109
|
#pool;
|
|
1009
1110
|
#log;
|
|
1010
1111
|
#folders = /* @__PURE__ */ new Set();
|
|
1011
1112
|
#faults = /* @__PURE__ */ new Set();
|
|
1012
1113
|
#losses = /* @__PURE__ */ new WeakMap();
|
|
1114
|
+
#pings = /* @__PURE__ */ new WeakMap();
|
|
1013
1115
|
#watches = /* @__PURE__ */ new Map();
|
|
1014
|
-
#
|
|
1116
|
+
#shared = {
|
|
1117
|
+
id: "shared",
|
|
1118
|
+
purpose: "Shared browser",
|
|
1119
|
+
abort: this.#abort
|
|
1120
|
+
};
|
|
1121
|
+
#holders = /* @__PURE__ */ new Map([[this.#shared.id, this.#shared]]);
|
|
1122
|
+
#leases = /* @__PURE__ */ new Map();
|
|
1123
|
+
#grants = /* @__PURE__ */ new Map();
|
|
1124
|
+
#notices = /* @__PURE__ */ new Map();
|
|
1125
|
+
#disposals = /* @__PURE__ */ new Map();
|
|
1126
|
+
#drains = /* @__PURE__ */ new Map();
|
|
1127
|
+
#readers = /* @__PURE__ */ new Map();
|
|
1128
|
+
#writers = /* @__PURE__ */ new Set();
|
|
1129
|
+
#size;
|
|
1015
1130
|
#reference;
|
|
1016
1131
|
#references = 0;
|
|
1017
1132
|
#failure;
|
|
1018
1133
|
#stranded;
|
|
1019
|
-
#lease;
|
|
1020
|
-
#granting;
|
|
1021
1134
|
#starting;
|
|
1022
1135
|
#sweeping;
|
|
1023
1136
|
#closing;
|
|
@@ -1029,6 +1142,7 @@ var BrowserMCPServer = class {
|
|
|
1029
1142
|
constructor(options) {
|
|
1030
1143
|
const size = options?.pool?.size ?? 1;
|
|
1031
1144
|
if (!(0, _orkestrel_contract.isInteger)(size) || size < 1 || size > 3) throw new _src_core.BrowserError(`pool.size must be an integer from 1 through 3`, BROWSER_SERVER_OPTIONS);
|
|
1145
|
+
this.#size = size;
|
|
1032
1146
|
this.#log = options?.log ?? process.stderr;
|
|
1033
1147
|
this.#reference = this.#issue.bind(this);
|
|
1034
1148
|
this.#pool = (0, _orkestrel_pool.createPool)({
|
|
@@ -1043,15 +1157,32 @@ var BrowserMCPServer = class {
|
|
|
1043
1157
|
this.#root = (0, node_path.resolve)(options?.root ?? "tmp/browsers");
|
|
1044
1158
|
this.#headless = options?.headless ?? true;
|
|
1045
1159
|
this.#executable = options?.executable;
|
|
1160
|
+
this.#viewport = options?.viewport;
|
|
1046
1161
|
this.#readonly = options?.readonly ?? false;
|
|
1047
1162
|
this.#launcher = options?.launch ?? createBrowser;
|
|
1048
1163
|
this.#input = options?.stdio?.input ?? process.stdin;
|
|
1049
1164
|
this.#tools = (0, _orkestrel_tool.createToolManager)();
|
|
1050
1165
|
const names = [..._src_core.BROWSER_TOOL_NAMES, ..._src_core.BROWSER_JOURNEY_TOOL_NAMES];
|
|
1051
|
-
this.#vocabulary = new Set(names);
|
|
1166
|
+
this.#vocabulary = /* @__PURE__ */ new Set([...names, ...Object.keys(BROWSER_SERVER_COPY)]);
|
|
1052
1167
|
for (const name of names) this.#tools.add((0, _orkestrel_tool.createTool)({
|
|
1053
1168
|
..._src_core.BROWSER_TOOL_COPY[name],
|
|
1054
|
-
execute: this.#forward.bind(this, name)
|
|
1169
|
+
execute: this.#forward.bind(this, this.#shared, name)
|
|
1170
|
+
}));
|
|
1171
|
+
this.#tools.add((0, _orkestrel_tool.createTool)({
|
|
1172
|
+
...BROWSER_SERVER_COPY.acquire,
|
|
1173
|
+
execute: this.#acquire.bind(this)
|
|
1174
|
+
}));
|
|
1175
|
+
this.#tools.add((0, _orkestrel_tool.createTool)({
|
|
1176
|
+
...BROWSER_SERVER_COPY.execute,
|
|
1177
|
+
execute: this.#dispatch.bind(this)
|
|
1178
|
+
}));
|
|
1179
|
+
this.#tools.add((0, _orkestrel_tool.createTool)({
|
|
1180
|
+
...BROWSER_SERVER_COPY.tools,
|
|
1181
|
+
execute: this.#catalog.bind(this)
|
|
1182
|
+
}));
|
|
1183
|
+
this.#tools.add((0, _orkestrel_tool.createTool)({
|
|
1184
|
+
...BROWSER_SERVER_COPY.destroy,
|
|
1185
|
+
execute: this.#dispose.bind(this)
|
|
1055
1186
|
}));
|
|
1056
1187
|
const server = (0, _orkestrel_mcp.createMCPServer)({
|
|
1057
1188
|
identity: {
|
|
@@ -1067,9 +1198,6 @@ var BrowserMCPServer = class {
|
|
|
1067
1198
|
...options?.stdio?.output === void 0 ? {} : { output: options.stdio.output }
|
|
1068
1199
|
});
|
|
1069
1200
|
this.#signal = this.#end.bind(this);
|
|
1070
|
-
this.#added = this.#mirror.bind(this);
|
|
1071
|
-
this.#removed = this.#withdraw.bind(this);
|
|
1072
|
-
this.#cleared = this.#clear.bind(this);
|
|
1073
1201
|
}
|
|
1074
1202
|
async start() {
|
|
1075
1203
|
if (this.#closing !== void 0) throw this.#ended();
|
|
@@ -1090,7 +1218,7 @@ var BrowserMCPServer = class {
|
|
|
1090
1218
|
if (this.#closing !== void 0) return;
|
|
1091
1219
|
this.#sweeping = this.#sweep();
|
|
1092
1220
|
const warming = this.#pool.start().then(void 0, (error) => error);
|
|
1093
|
-
await this.#grant(this.#abort.signal);
|
|
1221
|
+
await this.#grant(this.#shared, this.#abort.signal);
|
|
1094
1222
|
if (this.#closing !== void 0) return;
|
|
1095
1223
|
warming.then(this.#exhaust.bind(this));
|
|
1096
1224
|
} catch (error) {
|
|
@@ -1106,13 +1234,16 @@ var BrowserMCPServer = class {
|
|
|
1106
1234
|
this.#transport.stop();
|
|
1107
1235
|
for (const tool of this.#tools.tools()) this.#tools.remove(tool.name);
|
|
1108
1236
|
this.#unmirror();
|
|
1109
|
-
this.#
|
|
1237
|
+
this.#leases.clear();
|
|
1238
|
+
for (const holder of this.#holders.values()) holder.abort.abort();
|
|
1110
1239
|
this.#abort.abort();
|
|
1111
1240
|
const barrier = this.#pool.destroy().catch((error) => {
|
|
1112
1241
|
for (const failure of (0, _orkestrel_pool.isPoolError)(error) ? error.context?.failures ?? [error] : [error]) this.#faults.add(failure);
|
|
1113
1242
|
});
|
|
1114
1243
|
await this.#starting?.catch(() => void 0);
|
|
1115
1244
|
await barrier;
|
|
1245
|
+
await Promise.allSettled(this.#disposals.values());
|
|
1246
|
+
this.#holders.clear();
|
|
1116
1247
|
await this.#sweeping;
|
|
1117
1248
|
if (this.#stranded !== void 0) this.#faults.add(this.#stranded);
|
|
1118
1249
|
for (const folder of this.#folders) {
|
|
@@ -1157,22 +1288,141 @@ var BrowserMCPServer = class {
|
|
|
1157
1288
|
process.exitCode = 1;
|
|
1158
1289
|
this.#write(`browse: ${describeBrowserServerLoss(BROWSER_SERVER_TEARDOWN, error)}`);
|
|
1159
1290
|
}
|
|
1160
|
-
async #
|
|
1291
|
+
async #acquire(args, context) {
|
|
1292
|
+
context.signal.throwIfAborted();
|
|
1293
|
+
if (this.#closing !== void 0) throw this.#ended();
|
|
1294
|
+
const purpose = args["purpose"];
|
|
1295
|
+
if (!(0, _orkestrel_contract.isString)(purpose) || purpose.trim().length === 0) throw new _src_core.BrowserError("purpose must describe the work", "BROWSER_TOOLSET_ARGUMENT");
|
|
1296
|
+
if (this.#holders.size >= this.#size) throw new _src_core.BrowserError(`${BROWSER_SERVER_BUSY}: ${[...this.#holders.values()].map((holder) => `${holder.id} (${holder.purpose})`).join(", ")}. Call destroy for a holder no longer needed or call the named tools to share the shared browser.`, BROWSER_SERVER_BUSY);
|
|
1297
|
+
const holder = {
|
|
1298
|
+
id: (0, node_crypto.randomUUID)(),
|
|
1299
|
+
purpose,
|
|
1300
|
+
abort: new AbortController()
|
|
1301
|
+
};
|
|
1302
|
+
this.#holders.set(holder.id, holder);
|
|
1303
|
+
const listener = (0, node_events.addAbortListener)(context.signal, () => holder.abort.abort(context.signal.reason));
|
|
1304
|
+
try {
|
|
1305
|
+
const slot = await this.#serve(holder, context.signal);
|
|
1306
|
+
context.signal.throwIfAborted();
|
|
1307
|
+
if (holder.abort.signal.aborted || this.#holders.get(holder.id) !== holder) throw this.#ended();
|
|
1308
|
+
if (this.#leases.get(holder)?.value !== slot) throw this.#unavailable(this.#losses.get(slot)?.cause);
|
|
1309
|
+
const catalog = {
|
|
1310
|
+
holder: holder.id,
|
|
1311
|
+
tools: slot.toolset.tools.definitions()
|
|
1312
|
+
};
|
|
1313
|
+
return JSON.stringify(catalog);
|
|
1314
|
+
} catch (error) {
|
|
1315
|
+
await this.#retire(holder).catch(this.#fault.bind(this));
|
|
1316
|
+
throw error;
|
|
1317
|
+
} finally {
|
|
1318
|
+
listener[Symbol.dispose]();
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
#holder(value) {
|
|
1322
|
+
const holder = (0, _orkestrel_contract.isString)(value) ? this.#holders.get(value) : void 0;
|
|
1323
|
+
if (holder === void 0 || holder === this.#shared || holder.abort.signal.aborted) throw new _src_core.BrowserError(`${BROWSER_SERVER_HOLDER}: unknown or ended holder`, BROWSER_SERVER_HOLDER);
|
|
1324
|
+
return holder;
|
|
1325
|
+
}
|
|
1326
|
+
async #dispatch(args, context) {
|
|
1327
|
+
const holder = this.#holder(args["holder"]);
|
|
1328
|
+
const name = args["name"];
|
|
1329
|
+
const parameters = args["arguments"];
|
|
1330
|
+
if (!(0, _orkestrel_contract.isString)(name) || !(0, _orkestrel_contract.isRecord)(parameters)) throw new _src_core.BrowserError("execute requires name and arguments", "BROWSER_TOOLSET_ARGUMENT");
|
|
1331
|
+
return this.#forward(holder, name, parameters, context);
|
|
1332
|
+
}
|
|
1333
|
+
async #catalog(args, context) {
|
|
1334
|
+
const holder = this.#holder(args["holder"]);
|
|
1335
|
+
const signal = AbortSignal.any([context.signal, holder.abort.signal]);
|
|
1336
|
+
try {
|
|
1337
|
+
const slot = await this.#serve(holder, signal);
|
|
1338
|
+
signal.throwIfAborted();
|
|
1339
|
+
if (this.#leases.get(holder)?.value !== slot) throw this.#unavailable(this.#losses.get(slot)?.cause);
|
|
1340
|
+
const catalog = {
|
|
1341
|
+
holder: holder.id,
|
|
1342
|
+
tools: slot.toolset.tools.definitions()
|
|
1343
|
+
};
|
|
1344
|
+
return this.#annotate(holder, JSON.stringify(catalog));
|
|
1345
|
+
} catch (error) {
|
|
1346
|
+
context.signal.throwIfAborted();
|
|
1347
|
+
if (holder.abort.signal.aborted) throw new _src_core.BrowserError(`${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`, BROWSER_SERVER_UNRESOLVED);
|
|
1348
|
+
if ((0, _src_core.isBrowserError)(error)) throw new _src_core.BrowserError(this.#annotate(holder, `${error.code}: ${error.message}`), error.code);
|
|
1349
|
+
throw error;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
async #dispose(args) {
|
|
1353
|
+
const id = args["holder"];
|
|
1354
|
+
await (((0, _orkestrel_contract.isString)(id) ? this.#disposals.get(id) : void 0) ?? this.#retire(this.#holder(id)));
|
|
1355
|
+
return "Holder destroyed.";
|
|
1356
|
+
}
|
|
1357
|
+
#retire(holder) {
|
|
1358
|
+
const pending = this.#disposals.get(holder.id);
|
|
1359
|
+
if (pending !== void 0) return pending;
|
|
1360
|
+
holder.abort.abort();
|
|
1361
|
+
const token = this.#leases.get(holder);
|
|
1362
|
+
this.#leases.delete(holder);
|
|
1363
|
+
const disposal = Promise.resolve().then(async () => {
|
|
1364
|
+
try {
|
|
1365
|
+
await this.#grants.get(holder)?.catch(() => void 0);
|
|
1366
|
+
await token?.destroy();
|
|
1367
|
+
await Promise.all(this.#drains.get(holder) ?? []);
|
|
1368
|
+
} catch (error) {
|
|
1369
|
+
this.#faults.add(error);
|
|
1370
|
+
} finally {
|
|
1371
|
+
if (this.#holders.get(holder.id) === holder) this.#holders.delete(holder.id);
|
|
1372
|
+
this.#notices.delete(holder);
|
|
1373
|
+
this.#drains.delete(holder);
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
this.#disposals.set(holder.id, disposal);
|
|
1377
|
+
return disposal;
|
|
1378
|
+
}
|
|
1379
|
+
async #forward(holder, name, args, context) {
|
|
1380
|
+
context.signal.throwIfAborted();
|
|
1381
|
+
const journey = args["journey"];
|
|
1382
|
+
if (name !== "replay" && name !== "forget" || !(0, _orkestrel_contract.isString)(journey)) return this.#perform(holder, name, args, context);
|
|
1383
|
+
if (this.#writers.has(journey) || name === "forget" && this.#readers.has(journey)) throw new _src_core.BrowserError(`BROWSER_JOURNEY_LOCKED: Journey ${journey} is locked; call ${name} again.`, "BROWSER_JOURNEY_LOCKED", { name: journey });
|
|
1384
|
+
const reader = Symbol(journey);
|
|
1385
|
+
if (name === "forget") this.#writers.add(journey);
|
|
1386
|
+
else {
|
|
1387
|
+
const readers = this.#readers.get(journey) ?? /* @__PURE__ */ new Set();
|
|
1388
|
+
readers.add(reader);
|
|
1389
|
+
this.#readers.set(journey, readers);
|
|
1390
|
+
}
|
|
1391
|
+
try {
|
|
1392
|
+
return await this.#perform(holder, name, args, context);
|
|
1393
|
+
} finally {
|
|
1394
|
+
if (name === "forget") this.#writers.delete(journey);
|
|
1395
|
+
else {
|
|
1396
|
+
const readers = this.#readers.get(journey);
|
|
1397
|
+
readers?.delete(reader);
|
|
1398
|
+
if (readers?.size === 0) this.#readers.delete(journey);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
async #perform(holder, name, args, context) {
|
|
1403
|
+
const signal = holder === this.#shared ? context.signal : AbortSignal.any([context.signal, holder.abort.signal]);
|
|
1161
1404
|
let slot;
|
|
1162
1405
|
try {
|
|
1163
|
-
slot = await this.#serve(
|
|
1406
|
+
slot = await this.#serve(holder, signal);
|
|
1164
1407
|
} catch (error) {
|
|
1165
1408
|
context.signal.throwIfAborted();
|
|
1166
|
-
if (
|
|
1409
|
+
if (holder !== this.#shared && holder.abort.signal.aborted) throw new _src_core.BrowserError(`${BROWSER_SERVER_UNRESOLVED}: the holder ended during the call`, BROWSER_SERVER_UNRESOLVED);
|
|
1410
|
+
if ((0, _src_core.isBrowserError)(error)) throw new _src_core.BrowserError(this.#annotate(holder, `${error.code}: ${error.message}`), error.code);
|
|
1167
1411
|
throw error;
|
|
1168
1412
|
}
|
|
1169
1413
|
let result;
|
|
1170
1414
|
try {
|
|
1171
|
-
|
|
1415
|
+
const execution = slot.toolset.tools.execute({
|
|
1172
1416
|
id: name,
|
|
1173
1417
|
name,
|
|
1174
1418
|
arguments: args
|
|
1175
|
-
},
|
|
1419
|
+
}, {
|
|
1420
|
+
...context,
|
|
1421
|
+
signal
|
|
1422
|
+
});
|
|
1423
|
+
const outcome = await (name === "replay" || name === "forget" ? execution : this.#race(execution, signal));
|
|
1424
|
+
if (outcome === void 0) throw this.#ended();
|
|
1425
|
+
result = outcome;
|
|
1176
1426
|
} catch (error) {
|
|
1177
1427
|
result = {
|
|
1178
1428
|
id: name,
|
|
@@ -1182,28 +1432,27 @@ var BrowserMCPServer = class {
|
|
|
1182
1432
|
};
|
|
1183
1433
|
}
|
|
1184
1434
|
context.signal.throwIfAborted();
|
|
1435
|
+
if (holder !== this.#shared && holder.abort.signal.aborted) throw new _src_core.BrowserError(BROWSER_SERVER_UNRESOLVED + ": the holder ended during the call", BROWSER_SERVER_UNRESOLVED);
|
|
1185
1436
|
if (!result.success) {
|
|
1186
|
-
if (this.#
|
|
1187
|
-
await
|
|
1188
|
-
} catch
|
|
1189
|
-
if (!context.signal.aborted && !this.#abort.signal.aborted) this.#lose(slot, error);
|
|
1190
|
-
}
|
|
1437
|
+
if (this.#leases.get(holder)?.value === slot && !context.signal.aborted) try {
|
|
1438
|
+
await this.#race(this.#ping(slot), context.signal);
|
|
1439
|
+
} catch {}
|
|
1191
1440
|
context.signal.throwIfAborted();
|
|
1192
|
-
if (this.#
|
|
1441
|
+
if (this.#leases.get(holder)?.value !== slot) {
|
|
1193
1442
|
const loss = this.#losses.get(slot);
|
|
1194
|
-
throw new _src_core.BrowserError(this.#annotate(describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url), slot), BROWSER_SERVER_UNRESOLVED);
|
|
1443
|
+
throw new _src_core.BrowserError(this.#annotate(holder, describeBrowserServerLoss(BROWSER_SERVER_UNRESOLVED, loss?.cause, loss?.url), slot), BROWSER_SERVER_UNRESOLVED);
|
|
1195
1444
|
}
|
|
1196
|
-
throw new _src_core.BrowserError(this.#annotate(result.error, slot));
|
|
1445
|
+
throw new _src_core.BrowserError(this.#annotate(holder, result.error, slot));
|
|
1197
1446
|
}
|
|
1198
|
-
return typeof result.value === "string" ? this.#annotate(result.value, slot) : result.value;
|
|
1447
|
+
return typeof result.value === "string" ? this.#annotate(holder, result.value, slot) : result.value;
|
|
1199
1448
|
}
|
|
1200
|
-
#annotate(value, slot) {
|
|
1449
|
+
#annotate(holder, value, slot) {
|
|
1201
1450
|
const notes = [];
|
|
1202
|
-
for (const lost of this.#
|
|
1451
|
+
for (const lost of this.#notices.get(holder) ?? []) {
|
|
1203
1452
|
if (lost === slot) continue;
|
|
1204
1453
|
const loss = this.#losses.get(lost);
|
|
1205
1454
|
notes.push(describeBrowserServerLoss(BROWSER_SERVER_CRASH, loss?.cause, loss?.url));
|
|
1206
|
-
this.#
|
|
1455
|
+
this.#notices.get(holder)?.delete(lost);
|
|
1207
1456
|
}
|
|
1208
1457
|
return [...notes, value].join("\n");
|
|
1209
1458
|
}
|
|
@@ -1227,52 +1476,66 @@ var BrowserMCPServer = class {
|
|
|
1227
1476
|
listener[Symbol.dispose]();
|
|
1228
1477
|
}
|
|
1229
1478
|
}
|
|
1230
|
-
async #grant(signal) {
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1479
|
+
async #grant(holder, signal) {
|
|
1480
|
+
const held = this.#leases.get(holder);
|
|
1481
|
+
if (held !== void 0) return held;
|
|
1482
|
+
let granting = this.#grants.get(holder);
|
|
1483
|
+
if (granting === void 0) {
|
|
1484
|
+
granting = this.#pool.acquire(holder.abort.signal).then(this.#hold.bind(this, holder));
|
|
1485
|
+
this.#grants.set(holder, granting);
|
|
1486
|
+
granting.finally(() => {
|
|
1487
|
+
if (this.#grants.get(holder) === granting) this.#grants.delete(holder);
|
|
1488
|
+
}).catch(() => void 0);
|
|
1489
|
+
}
|
|
1490
|
+
const token = await this.#race(granting, signal);
|
|
1234
1491
|
if (token === void 0) throw this.#ended();
|
|
1235
1492
|
return token;
|
|
1236
1493
|
}
|
|
1237
|
-
#hold(token) {
|
|
1238
|
-
this.#
|
|
1239
|
-
|
|
1494
|
+
async #hold(holder, token) {
|
|
1495
|
+
if (this.#closing !== void 0 || this.#holders.get(holder.id) !== holder || holder.abort.signal.aborted) {
|
|
1496
|
+
await token.destroy();
|
|
1497
|
+
throw this.#ended();
|
|
1498
|
+
}
|
|
1240
1499
|
if (this.#losses.has(token.value)) {
|
|
1241
|
-
token.destroy().catch(() => void 0);
|
|
1500
|
+
await token.destroy().catch(() => void 0);
|
|
1242
1501
|
throw this.#unavailable(this.#losses.get(token.value)?.cause);
|
|
1243
1502
|
}
|
|
1244
|
-
this.#
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1503
|
+
this.#leases.set(holder, token);
|
|
1504
|
+
if (holder === this.#shared) {
|
|
1505
|
+
const tools = token.value.toolset.tools;
|
|
1506
|
+
const mirrored = {
|
|
1507
|
+
token,
|
|
1508
|
+
added: this.#mirror.bind(this, token),
|
|
1509
|
+
removed: this.#withdraw.bind(this, token),
|
|
1510
|
+
cleared: this.#clear.bind(this, token)
|
|
1511
|
+
};
|
|
1512
|
+
this.#mirrored = mirrored;
|
|
1513
|
+
for (const tool of tools.tools()) this.#mirror(token, tool);
|
|
1514
|
+
tools.emitter.on("add", mirrored.added);
|
|
1515
|
+
tools.emitter.on("remove", mirrored.removed);
|
|
1516
|
+
tools.emitter.on("clear", mirrored.cleared);
|
|
1517
|
+
}
|
|
1250
1518
|
return token;
|
|
1251
1519
|
}
|
|
1252
|
-
#
|
|
1253
|
-
this.#
|
|
1254
|
-
throw error;
|
|
1255
|
-
}
|
|
1256
|
-
async #serve(signal) {
|
|
1257
|
-
if (this.#closing !== void 0) throw this.#ended();
|
|
1520
|
+
async #serve(holder, signal) {
|
|
1521
|
+
if (this.#closing !== void 0 || holder.abort.signal.aborted) throw this.#ended();
|
|
1258
1522
|
await this.#race(this.#starting, signal);
|
|
1259
|
-
if (this.#closing !== void 0) throw this.#ended();
|
|
1260
|
-
const held = this.#
|
|
1523
|
+
if (this.#closing !== void 0 || holder.abort.signal.aborted) throw this.#ended();
|
|
1524
|
+
const held = this.#leases.get(holder);
|
|
1261
1525
|
if (held !== void 0) try {
|
|
1262
|
-
await held.value
|
|
1263
|
-
if (this.#
|
|
1264
|
-
} catch
|
|
1526
|
+
await this.#race(this.#ping(held.value), signal);
|
|
1527
|
+
if (this.#leases.get(holder) === held) return held.value;
|
|
1528
|
+
} catch {
|
|
1265
1529
|
signal.throwIfAborted();
|
|
1266
|
-
if (
|
|
1267
|
-
this.#lose(held.value, error);
|
|
1530
|
+
if (holder.abort.signal.aborted) throw this.#ended();
|
|
1268
1531
|
}
|
|
1269
1532
|
try {
|
|
1270
|
-
const token = await this.#grant(signal);
|
|
1271
|
-
if (this.#
|
|
1533
|
+
const token = await this.#grant(holder, signal);
|
|
1534
|
+
if (this.#leases.get(holder) !== token) throw this.#unavailable(this.#losses.get(token.value)?.cause);
|
|
1272
1535
|
return token.value;
|
|
1273
1536
|
} catch (error) {
|
|
1274
1537
|
signal.throwIfAborted();
|
|
1275
|
-
if (
|
|
1538
|
+
if (holder.abort.signal.aborted || (0, _orkestrel_pool.isPoolError)(error) && error.code === "destroyed") throw this.#ended();
|
|
1276
1539
|
if ((0, _orkestrel_pool.isPoolError)(error) && (error.code === "create" || error.code === "cleanup")) {
|
|
1277
1540
|
this.#exhaust(error);
|
|
1278
1541
|
throw this.#unavailable(error.cause ?? this.#failure);
|
|
@@ -1280,6 +1543,15 @@ var BrowserMCPServer = class {
|
|
|
1280
1543
|
throw error;
|
|
1281
1544
|
}
|
|
1282
1545
|
}
|
|
1546
|
+
#ping(slot) {
|
|
1547
|
+
const pending = this.#pings.get(slot);
|
|
1548
|
+
if (pending !== void 0) return pending;
|
|
1549
|
+
const ping = slot.browser.ping({ signal: this.#abort.signal }).catch((error) => {
|
|
1550
|
+
if (!this.#abort.signal.aborted) this.#lose(slot, error);
|
|
1551
|
+
}).finally(() => this.#pings.delete(slot));
|
|
1552
|
+
this.#pings.set(slot, ping);
|
|
1553
|
+
return ping;
|
|
1554
|
+
}
|
|
1283
1555
|
async #validate(slot) {
|
|
1284
1556
|
try {
|
|
1285
1557
|
await slot.browser.ping({ signal: this.#abort.signal });
|
|
@@ -1296,12 +1568,19 @@ var BrowserMCPServer = class {
|
|
|
1296
1568
|
url: slot.toolset.view.url
|
|
1297
1569
|
});
|
|
1298
1570
|
this.#failure = cause;
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1571
|
+
for (const [holder, token] of this.#leases) {
|
|
1572
|
+
if (token.value !== slot || this.#holders.get(holder.id) !== holder) continue;
|
|
1573
|
+
if (holder === this.#shared) this.#unmirror();
|
|
1574
|
+
this.#leases.delete(holder);
|
|
1575
|
+
const notices = this.#notices.get(holder) ?? /* @__PURE__ */ new Set();
|
|
1576
|
+
notices.add(slot);
|
|
1577
|
+
this.#notices.set(holder, notices);
|
|
1578
|
+
const drain = token.destroy();
|
|
1579
|
+
const drains = this.#drains.get(holder) ?? /* @__PURE__ */ new Set();
|
|
1580
|
+
drains.add(drain);
|
|
1581
|
+
this.#drains.set(holder, drains);
|
|
1582
|
+
drain.finally(() => drains.delete(drain)).catch(() => void 0);
|
|
1583
|
+
}
|
|
1305
1584
|
}
|
|
1306
1585
|
#watch(slot, signal) {
|
|
1307
1586
|
const loss = Promise.withResolvers();
|
|
@@ -1374,6 +1653,7 @@ var BrowserMCPServer = class {
|
|
|
1374
1653
|
cdp: { discover: false },
|
|
1375
1654
|
signal: this.#abort.signal,
|
|
1376
1655
|
...this.#executable === void 0 ? {} : { executable: this.#executable },
|
|
1656
|
+
...this.#viewport === void 0 ? {} : { viewport: this.#viewport },
|
|
1377
1657
|
...process.platform === "linux" && process.getuid?.() === 0 ? { args: ["--no-sandbox"] } : {}
|
|
1378
1658
|
});
|
|
1379
1659
|
await browser.connect();
|
|
@@ -1385,7 +1665,11 @@ var BrowserMCPServer = class {
|
|
|
1385
1665
|
}));
|
|
1386
1666
|
await (0, node_fs_promises.rename)(`${record}.tmp`, record);
|
|
1387
1667
|
}
|
|
1388
|
-
const context = await browser.isolate({
|
|
1668
|
+
const context = await browser.isolate({
|
|
1669
|
+
reference: this.#reference,
|
|
1670
|
+
downloads: { path: (0, node_path.join)(profile, "downloads") },
|
|
1671
|
+
...this.#viewport === void 0 ? {} : { emulation: { viewport: this.#viewport } }
|
|
1672
|
+
});
|
|
1389
1673
|
const page = await context.create();
|
|
1390
1674
|
toolset = (0, _src_core.createBrowserToolset)(page, {
|
|
1391
1675
|
context,
|
|
@@ -1492,29 +1776,31 @@ var BrowserMCPServer = class {
|
|
|
1492
1776
|
#issue() {
|
|
1493
1777
|
return `e${++this.#references}`;
|
|
1494
1778
|
}
|
|
1495
|
-
#mirror(tool) {
|
|
1496
|
-
if (this.#vocabulary.has(tool.name)) return;
|
|
1779
|
+
#mirror(token, tool) {
|
|
1780
|
+
if (this.#leases.get(this.#shared) !== token || this.#vocabulary.has(tool.name)) return;
|
|
1497
1781
|
this.#tools.add((0, _orkestrel_tool.createTool)({
|
|
1498
1782
|
...(0, _orkestrel_tool.toolToDefinition)(tool),
|
|
1499
|
-
execute: this.#forward.bind(this, tool.name)
|
|
1783
|
+
execute: this.#forward.bind(this, this.#shared, tool.name)
|
|
1500
1784
|
}));
|
|
1501
1785
|
}
|
|
1502
|
-
#withdraw(tool) {
|
|
1503
|
-
if (this.#vocabulary.has(tool.name)) return;
|
|
1504
|
-
const current =
|
|
1786
|
+
#withdraw(token, tool) {
|
|
1787
|
+
if (this.#leases.get(this.#shared) !== token || this.#vocabulary.has(tool.name)) return;
|
|
1788
|
+
const current = token.value.toolset.tools.tool(tool.name);
|
|
1505
1789
|
if (current === void 0) this.#tools.remove(tool.name);
|
|
1506
|
-
else this.#mirror(current);
|
|
1790
|
+
else this.#mirror(token, current);
|
|
1507
1791
|
}
|
|
1508
|
-
#clear(tools) {
|
|
1509
|
-
for (const tool of tools) this.#withdraw(tool);
|
|
1792
|
+
#clear(token, tools) {
|
|
1793
|
+
for (const tool of tools) this.#withdraw(token, tool);
|
|
1510
1794
|
}
|
|
1511
1795
|
#unmirror() {
|
|
1512
|
-
const mirrored = this.#
|
|
1796
|
+
const mirrored = this.#mirrored;
|
|
1513
1797
|
if (mirrored === void 0) return;
|
|
1514
|
-
|
|
1515
|
-
mirrored.
|
|
1516
|
-
|
|
1517
|
-
|
|
1798
|
+
this.#mirrored = void 0;
|
|
1799
|
+
const tools = mirrored.token.value.toolset.tools;
|
|
1800
|
+
for (const tool of tools.tools()) if (!this.#vocabulary.has(tool.name)) this.#tools.remove(tool.name);
|
|
1801
|
+
tools.emitter.off("add", mirrored.added);
|
|
1802
|
+
tools.emitter.off("remove", mirrored.removed);
|
|
1803
|
+
tools.emitter.off("clear", mirrored.cleared);
|
|
1518
1804
|
}
|
|
1519
1805
|
#ended() {
|
|
1520
1806
|
return new _src_core.BrowserError("the browser session ended", "BROWSER_TOOLSET_ENDED");
|
|
@@ -1952,6 +2238,24 @@ var FileBrowserRunStore = class {
|
|
|
1952
2238
|
constructor(options) {
|
|
1953
2239
|
this.#files = new FileBrowserStore(options);
|
|
1954
2240
|
}
|
|
2241
|
+
async snapshot(bytes, options) {
|
|
2242
|
+
options?.signal?.throwIfAborted();
|
|
2243
|
+
const owned = new Uint8Array(bytes);
|
|
2244
|
+
const directory = await this.#files.allocate(this.#files.resolvePath(BROWSER_RUN_DIRECTORY), _src_core.generateBrowserRunId, options);
|
|
2245
|
+
const path = this.#files.resolvePath(directory.path, "view.png");
|
|
2246
|
+
try {
|
|
2247
|
+
await this.#files.check(path, options);
|
|
2248
|
+
options?.signal?.throwIfAborted();
|
|
2249
|
+
await (0, node_fs_promises.writeFile)(path, owned, {
|
|
2250
|
+
flag: "wx",
|
|
2251
|
+
signal: options?.signal
|
|
2252
|
+
});
|
|
2253
|
+
return path;
|
|
2254
|
+
} catch (error) {
|
|
2255
|
+
options?.signal?.throwIfAborted();
|
|
2256
|
+
throw this.#files.translateError(path, error);
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
1955
2259
|
async open(name, options) {
|
|
1956
2260
|
options?.signal?.throwIfAborted();
|
|
1957
2261
|
this.#files.validateName(name);
|
|
@@ -3006,8 +3310,11 @@ exports.BROWSER_PROCESS_EXIT_CAUSE = BROWSER_PROCESS_EXIT_CAUSE;
|
|
|
3006
3310
|
exports.BROWSER_PROFILE_PREFIX = BROWSER_PROFILE_PREFIX;
|
|
3007
3311
|
exports.BROWSER_RUN_DIRECTORY = BROWSER_RUN_DIRECTORY;
|
|
3008
3312
|
exports.BROWSER_RUN_FILE = BROWSER_RUN_FILE;
|
|
3313
|
+
exports.BROWSER_SERVER_BUSY = BROWSER_SERVER_BUSY;
|
|
3314
|
+
exports.BROWSER_SERVER_COPY = BROWSER_SERVER_COPY;
|
|
3009
3315
|
exports.BROWSER_SERVER_CRASH = BROWSER_SERVER_CRASH;
|
|
3010
3316
|
exports.BROWSER_SERVER_EXHAUSTED = BROWSER_SERVER_EXHAUSTED;
|
|
3317
|
+
exports.BROWSER_SERVER_HOLDER = BROWSER_SERVER_HOLDER;
|
|
3011
3318
|
exports.BROWSER_SERVER_LAUNCH = BROWSER_SERVER_LAUNCH;
|
|
3012
3319
|
exports.BROWSER_SERVER_OPTIONS = BROWSER_SERVER_OPTIONS;
|
|
3013
3320
|
exports.BROWSER_SERVER_POOL_LIMIT = BROWSER_SERVER_POOL_LIMIT;
|
|
@@ -3061,6 +3368,7 @@ exports.normalizeExecutablePath = normalizeExecutablePath;
|
|
|
3061
3368
|
exports.parseBrowserEngine = parseBrowserEngine;
|
|
3062
3369
|
exports.parseBrowserLockEntry = parseBrowserLockEntry;
|
|
3063
3370
|
exports.parseBrowserProfileRecord = parseBrowserProfileRecord;
|
|
3371
|
+
exports.parseBrowserViewport = parseBrowserViewport;
|
|
3064
3372
|
exports.probePathNames = probePathNames;
|
|
3065
3373
|
exports.probeProcess = probeProcess;
|
|
3066
3374
|
exports.readBrowserEndpoint = readBrowserEndpoint;
|