@mind-harness/mh-browser 0.1.4 → 0.1.6
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/extension/manifest.json +1 -1
- package/lib/index.js +56 -28
- package/lib/typert.host.js +9 -2
- package/lib/typert.remote-client.js +1 -1
- package/lib/types/service/browser.d.ts +7 -0
- package/lib/types/service/service.d.ts +13 -3
- package/lib/types/service/test-hooks.d.ts +2 -0
- package/package.json +1 -1
package/extension/manifest.json
CHANGED
package/lib/index.js
CHANGED
|
@@ -2952,6 +2952,15 @@ function missingExecutable(error) {
|
|
|
2952
2952
|
return /ENOENT|doesn.t exist|Executable doesn't exist|not found/i.test(message);
|
|
2953
2953
|
}
|
|
2954
2954
|
/**
|
|
2955
|
+
* Playwright appends `--no-sandbox` unless this is true, and branded Chrome
|
|
2956
|
+
* then warns that stability and security will suffer. An installed browser
|
|
2957
|
+
* sandboxes on every platform. Playwright's own Chromium does too, except on
|
|
2958
|
+
* Linux, where that build has no usable sandbox helper and would fail to start.
|
|
2959
|
+
*/
|
|
2960
|
+
function chromiumSandboxEnabled(browser, platform = process.platform) {
|
|
2961
|
+
return platform !== "linux" || browser !== "chromium";
|
|
2962
|
+
}
|
|
2963
|
+
/**
|
|
2955
2964
|
* One persistent context for the whole process. `chromium` omits `channel`
|
|
2956
2965
|
* so Playwright uses its own browser; the others use a local install.
|
|
2957
2966
|
* A missing browser is reported and not downloaded.
|
|
@@ -2962,9 +2971,10 @@ async function launchBrowser(options) {
|
|
|
2962
2971
|
return await chromium.launchPersistentContext(options.profileDir, {
|
|
2963
2972
|
headless: options.headless,
|
|
2964
2973
|
acceptDownloads: true,
|
|
2974
|
+
chromiumSandbox: chromiumSandboxEnabled(options.browser),
|
|
2965
2975
|
...options.headless ? {} : {
|
|
2966
2976
|
ignoreDefaultArgs: ["--disable-extensions"],
|
|
2967
|
-
args: ["--enable-unsafe-extension-debugging"]
|
|
2977
|
+
args: ["--enable-unsafe-extension-debugging", "--keep-alive-for-test"]
|
|
2968
2978
|
},
|
|
2969
2979
|
...options.browser === "chromium" ? {} : { channel: options.browser },
|
|
2970
2980
|
...options.executablePath !== void 0 ? { executablePath: options.executablePath } : {}
|
|
@@ -4297,8 +4307,9 @@ var Windows = class {
|
|
|
4297
4307
|
if (browser === null) throw new Error("persistent context has no browser");
|
|
4298
4308
|
this.browserSession = await browser.newBrowserCDPSession();
|
|
4299
4309
|
if (options.headed) this.extension = await Extension.load(context, this.browserSession);
|
|
4300
|
-
|
|
4310
|
+
const launched = context.pages().filter((page) => page.url() === "about:blank");
|
|
4301
4311
|
context.on("page", (page) => {
|
|
4312
|
+
for (const blank of launched.splice(0)) blank.close().catch(() => void 0);
|
|
4302
4313
|
this.ingest(page);
|
|
4303
4314
|
});
|
|
4304
4315
|
}
|
|
@@ -4995,7 +5006,7 @@ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializ
|
|
|
4995
5006
|
const NAVIGATION_MS = 1e4;
|
|
4996
5007
|
/** How long an open that started a download waits for the download event. */
|
|
4997
5008
|
const DOWNLOAD_EVENT_MS = 3e3;
|
|
4998
|
-
/** More
|
|
5009
|
+
/** More exits than this within the window and the browser is not started again until one ages out. */
|
|
4999
5010
|
const RELAUNCH_LIMIT = 3;
|
|
5000
5011
|
const RELAUNCH_WINDOW_MS = 6e4;
|
|
5001
5012
|
const STOP_WAIT_MS = 5e3;
|
|
@@ -5043,8 +5054,9 @@ let MhBrowserService = (() => {
|
|
|
5043
5054
|
context;
|
|
5044
5055
|
ready;
|
|
5045
5056
|
closing = false;
|
|
5046
|
-
|
|
5047
|
-
|
|
5057
|
+
/** The browser exited. The next call that needs a page starts it again. */
|
|
5058
|
+
exited = false;
|
|
5059
|
+
exits = [];
|
|
5048
5060
|
/** One queue per owner: a slow wait in one Session does not hold up another. */
|
|
5049
5061
|
queues = /* @__PURE__ */ new Map();
|
|
5050
5062
|
shown = /* @__PURE__ */ new Map();
|
|
@@ -5099,7 +5111,7 @@ let MhBrowserService = (() => {
|
|
|
5099
5111
|
}
|
|
5100
5112
|
async open(owner, items, signal) {
|
|
5101
5113
|
return this.exclusive(owner, async () => {
|
|
5102
|
-
await this.
|
|
5114
|
+
await this.up(owner === PERSON_OWNER);
|
|
5103
5115
|
const views = [];
|
|
5104
5116
|
for (const item of items) {
|
|
5105
5117
|
if (signal?.aborted) {
|
|
@@ -5159,7 +5171,7 @@ let MhBrowserService = (() => {
|
|
|
5159
5171
|
async presenceCheck(owner, which, opts) {
|
|
5160
5172
|
if (this.book.entries(owner).length === 0 && this.book.peekRemovals(owner).length === 0) return;
|
|
5161
5173
|
return this.exclusive(owner, async () => {
|
|
5162
|
-
const down = await this.
|
|
5174
|
+
const down = await this.up().then(() => void 0, (error) => error instanceof Error ? error.message : String(error));
|
|
5163
5175
|
const pages = this.book.entries(owner);
|
|
5164
5176
|
const removals = this.book.peekRemovals(owner);
|
|
5165
5177
|
const wanted = new Set(which === "all" ? pages.map((page) => page.url) : which.map(canonical));
|
|
@@ -5215,7 +5227,7 @@ let MhBrowserService = (() => {
|
|
|
5215
5227
|
}
|
|
5216
5228
|
async fetch(url, opts) {
|
|
5217
5229
|
try {
|
|
5218
|
-
await this.
|
|
5230
|
+
await this.up();
|
|
5219
5231
|
const context = this.context;
|
|
5220
5232
|
if (context === void 0) return {
|
|
5221
5233
|
kind: "error",
|
|
@@ -5261,14 +5273,18 @@ let MhBrowserService = (() => {
|
|
|
5261
5273
|
async started() {
|
|
5262
5274
|
await this.ready;
|
|
5263
5275
|
}
|
|
5276
|
+
/** The person asked for the browser: start it if it exited, past the exit limit. */
|
|
5277
|
+
startForPerson() {
|
|
5278
|
+
return this.up(true);
|
|
5279
|
+
}
|
|
5264
5280
|
[testHooks]() {
|
|
5265
5281
|
return {
|
|
5266
5282
|
crash: async () => {
|
|
5267
5283
|
this.windows.suppressPersonInput();
|
|
5268
5284
|
await (this.context?.browser())?.close();
|
|
5269
|
-
await this.relaunching;
|
|
5270
5285
|
},
|
|
5271
|
-
page: (owner, tab) => this.windows.get(owner, tab)?.page
|
|
5286
|
+
page: (owner, tab) => this.windows.get(owner, tab)?.page,
|
|
5287
|
+
running: () => this.context !== void 0
|
|
5272
5288
|
};
|
|
5273
5289
|
}
|
|
5274
5290
|
async act(owner, tab, op, signal) {
|
|
@@ -5356,7 +5372,7 @@ let MhBrowserService = (() => {
|
|
|
5356
5372
|
}
|
|
5357
5373
|
invoke(owner, _page, call, opts) {
|
|
5358
5374
|
return this.exclusive(owner, async () => {
|
|
5359
|
-
await this.
|
|
5375
|
+
await this.up();
|
|
5360
5376
|
if (opts?.signal?.aborted) throw new MhBrowserError(INTERRUPTED, WAITING_STOPPED);
|
|
5361
5377
|
const signal = opts?.signal;
|
|
5362
5378
|
const sent = await runInvoke({
|
|
@@ -5376,7 +5392,7 @@ let MhBrowserService = (() => {
|
|
|
5376
5392
|
* Session's other calls. `root` is the directory the session may write.
|
|
5377
5393
|
*/
|
|
5378
5394
|
async download(_owner, url, root, signal) {
|
|
5379
|
-
await this.
|
|
5395
|
+
await this.up();
|
|
5380
5396
|
return this.saveResponse(url, root, signal);
|
|
5381
5397
|
}
|
|
5382
5398
|
controlOf(owner) {
|
|
@@ -5395,7 +5411,7 @@ let MhBrowserService = (() => {
|
|
|
5395
5411
|
*/
|
|
5396
5412
|
handover(owner, req, signal) {
|
|
5397
5413
|
return this.exclusive(owner, async () => {
|
|
5398
|
-
await this.
|
|
5414
|
+
await this.up();
|
|
5399
5415
|
if (signal?.aborted === true) throw new MhBrowserError(INTERRUPTED, WAITING_STOPPED);
|
|
5400
5416
|
if (req.tab !== void 0) {
|
|
5401
5417
|
const live = this.windows.get(owner, req.tab);
|
|
@@ -6320,27 +6336,39 @@ let MhBrowserService = (() => {
|
|
|
6320
6336
|
this.context = context;
|
|
6321
6337
|
await this.windows.attach(context, this.launchOptions.profileDir, { headed: !this.launchOptions.headless });
|
|
6322
6338
|
context.browser()?.on("disconnected", () => {
|
|
6323
|
-
if (this.closing) return;
|
|
6324
|
-
this.
|
|
6339
|
+
if (this.closing || this.context !== context) return;
|
|
6340
|
+
this.onExit();
|
|
6325
6341
|
});
|
|
6326
6342
|
}
|
|
6327
|
-
|
|
6343
|
+
/** No window opens on exit: the person may have quit it. Calls on old tabs find none. */
|
|
6344
|
+
onExit() {
|
|
6328
6345
|
this.windows.suppressPersonInput();
|
|
6329
6346
|
this.windows.dropPages();
|
|
6330
6347
|
this.context = void 0;
|
|
6348
|
+
this.exits.push(Date.now());
|
|
6349
|
+
this.exited = true;
|
|
6350
|
+
this.ready = Promise.resolve();
|
|
6351
|
+
}
|
|
6352
|
+
/**
|
|
6353
|
+
* The browser for a call that needs a page. One that exited starts again
|
|
6354
|
+
* here. The exit limit stops unattended restarts; a person's request is
|
|
6355
|
+
* one start per click and skips it.
|
|
6356
|
+
*/
|
|
6357
|
+
up(byPerson = false) {
|
|
6358
|
+
if (!this.exited || this.closing) return this.ready;
|
|
6331
6359
|
const now = Date.now();
|
|
6332
|
-
while (this.
|
|
6333
|
-
this.
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
down.catch(() => void 0);
|
|
6337
|
-
this.ready = down;
|
|
6338
|
-
this.ctx.logger.warn("browser keeps exiting; not restarting it");
|
|
6339
|
-
return;
|
|
6360
|
+
while (this.exits.length > 0 && now - (this.exits[0] ?? now) > RELAUNCH_WINDOW_MS) this.exits.shift();
|
|
6361
|
+
if (!byPerson && this.exits.length > RELAUNCH_LIMIT) {
|
|
6362
|
+
this.ctx.logger.warn("browser keeps exiting; not starting it again yet");
|
|
6363
|
+
return Promise.reject(new MhBrowserError(BROWSER_UNAVAILABLE, `the browser exited ${String(this.exits.length)} times within ${String(RELAUNCH_WINDOW_MS / 1e3)} s; try again in a minute`));
|
|
6340
6364
|
}
|
|
6341
|
-
this.
|
|
6342
|
-
|
|
6343
|
-
this.windows.resumePersonInput()
|
|
6365
|
+
this.exited = false;
|
|
6366
|
+
const started = this.launch();
|
|
6367
|
+
started.then(() => this.windows.resumePersonInput(), () => {
|
|
6368
|
+
this.exited = true;
|
|
6369
|
+
});
|
|
6370
|
+
this.ready = started;
|
|
6371
|
+
return started;
|
|
6344
6372
|
}
|
|
6345
6373
|
async stop() {
|
|
6346
6374
|
this.windows.suppressPersonInput();
|
|
@@ -6649,7 +6677,7 @@ function browserDownloadTool(ctx) {
|
|
|
6649
6677
|
const HANDOVER = [
|
|
6650
6678
|
"Hand the browser to the person and wait until they hand it back.",
|
|
6651
6679
|
"The person can take over a tab at any time; that stops the action in progress on that tab, and a tab they have touched stays with them until they hand it back. Other tabs keep working.",
|
|
6652
|
-
"Call it instead of filling or submitting a login form, a
|
|
6680
|
+
"Call it instead of filling or submitting a login form, a Mind Harness login or consent form, or any form that issues credentials, and whenever only the person can continue."
|
|
6653
6681
|
].join(" ");
|
|
6654
6682
|
const REASON = "The sentence the person sees on the handover card, unchanged.";
|
|
6655
6683
|
const TAB = "Tab id of the human page to show the person, copied from a page element's tab attribute. Omit it to hand over without naming a tab; that does not pick the latest result. A page with an Agent View has no tab until you open it with snapshot.";
|
package/lib/typert.host.js
CHANGED
|
@@ -44,7 +44,7 @@ const $remote = createTypertRemoteInfo({
|
|
|
44
44
|
create: _mind_harness_mh_browser_mhBrowser_revealHandover_result$schema,
|
|
45
45
|
jsonSchema: {"type":"object","properties":{"status":{"type":"string","enum":["revealed","not-handing-over","no-tab"]}},"required":["status"],"additionalProperties":true},
|
|
46
46
|
},
|
|
47
|
-
sourceLocation: {"file":"packages/mh-browser/src/service/service.ts","line":
|
|
47
|
+
sourceLocation: {"file":"packages/mh-browser/src/service/service.ts","line":616,"column":9},
|
|
48
48
|
},
|
|
49
49
|
},
|
|
50
50
|
}, z)
|
|
@@ -160,6 +160,13 @@ export const TYPERT = {
|
|
|
160
160
|
"name": "started",
|
|
161
161
|
"signature": "async started(): Promise<void>"
|
|
162
162
|
},
|
|
163
|
+
{
|
|
164
|
+
"kind": "method",
|
|
165
|
+
"name": "startForPerson",
|
|
166
|
+
"signature": "startForPerson(): Promise<void>",
|
|
167
|
+
"summary": "The person asked for the browser: start it if it exited, past the exit limit.",
|
|
168
|
+
"jsDoc": "/** The person asked for the browser: start it if it exited, past the exit limit. */"
|
|
169
|
+
},
|
|
163
170
|
{
|
|
164
171
|
"kind": "method",
|
|
165
172
|
"name": "[testHooks]",
|
|
@@ -352,7 +359,7 @@ export const TYPERT = {
|
|
|
352
359
|
},
|
|
353
360
|
{
|
|
354
361
|
"name": "TestHooks",
|
|
355
|
-
"declaration": "export interface TestHooks {\n crash(): Promise<void>;\n page(owner: string, tab: string): unknown;\n}"
|
|
362
|
+
"declaration": "export interface TestHooks {\n crash(): Promise<void>;\n page(owner: string, tab: string): unknown;\n running(): boolean;\n}"
|
|
356
363
|
},
|
|
357
364
|
{
|
|
358
365
|
"name": "WaitFor",
|
|
@@ -44,7 +44,7 @@ export const TYPERT_REMOTE = createTypertRemoteInfo({
|
|
|
44
44
|
create: _mind_harness_mh_browser_mhBrowser_revealHandover_result$schema,
|
|
45
45
|
jsonSchema: {"type":"object","properties":{"status":{"type":"string","enum":["revealed","not-handing-over","no-tab"]}},"required":["status"],"additionalProperties":true},
|
|
46
46
|
},
|
|
47
|
-
sourceLocation: {"file":"packages/mh-browser/src/service/service.ts","line":
|
|
47
|
+
sourceLocation: {"file":"packages/mh-browser/src/service/service.ts","line":616,"column":9},
|
|
48
48
|
},
|
|
49
49
|
},
|
|
50
50
|
}, z)
|
|
@@ -7,6 +7,13 @@ export interface LaunchOptions {
|
|
|
7
7
|
/** Test hook: point at a missing executable to exercise the not-found message. */
|
|
8
8
|
executablePath?: string;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Playwright appends `--no-sandbox` unless this is true, and branded Chrome
|
|
12
|
+
* then warns that stability and security will suffer. An installed browser
|
|
13
|
+
* sandboxes on every platform. Playwright's own Chromium does too, except on
|
|
14
|
+
* Linux, where that build has no usable sandbox helper and would fail to start.
|
|
15
|
+
*/
|
|
16
|
+
export declare function chromiumSandboxEnabled(browser: BrowserName, platform?: NodeJS.Platform): boolean;
|
|
10
17
|
/**
|
|
11
18
|
* One persistent context for the whole process. `chromium` omits `channel`
|
|
12
19
|
* so Playwright uses its own browser; the others use a local install.
|
|
@@ -36,8 +36,9 @@ export declare class MhBrowserService extends TypertRemoteService {
|
|
|
36
36
|
private context;
|
|
37
37
|
private ready;
|
|
38
38
|
private closing;
|
|
39
|
-
|
|
40
|
-
private
|
|
39
|
+
/** The browser exited. The next call that needs a page starts it again. */
|
|
40
|
+
private exited;
|
|
41
|
+
private readonly exits;
|
|
41
42
|
/** One queue per owner: a slow wait in one Session does not hold up another. */
|
|
42
43
|
private readonly queues;
|
|
43
44
|
private readonly shown;
|
|
@@ -105,6 +106,8 @@ export declare class MhBrowserService extends TypertRemoteService {
|
|
|
105
106
|
}>;
|
|
106
107
|
pushPath(owner: string, tab: string, path: string): Promise<void>;
|
|
107
108
|
started(): Promise<void>;
|
|
109
|
+
/** The person asked for the browser: start it if it exited, past the exit limit. */
|
|
110
|
+
startForPerson(): Promise<void>;
|
|
108
111
|
[testHooks](): TestHooks;
|
|
109
112
|
act(owner: string, tab: string, op: PageOp, signal?: AbortSignal): Promise<PageView>;
|
|
110
113
|
holdHandover(owner: string, tab: string): void;
|
|
@@ -219,7 +222,14 @@ export declare class MhBrowserService extends TypertRemoteService {
|
|
|
219
222
|
/** A failed launch is logged here; calls that need the browser see it as `BROWSER_UNAVAILABLE`. */
|
|
220
223
|
private launch;
|
|
221
224
|
private start;
|
|
222
|
-
|
|
225
|
+
/** No window opens on exit: the person may have quit it. Calls on old tabs find none. */
|
|
226
|
+
private onExit;
|
|
227
|
+
/**
|
|
228
|
+
* The browser for a call that needs a page. One that exited starts again
|
|
229
|
+
* here. The exit limit stops unattended restarts; a person's request is
|
|
230
|
+
* one start per click and skips it.
|
|
231
|
+
*/
|
|
232
|
+
private up;
|
|
223
233
|
private stop;
|
|
224
234
|
private refuseIfPerson;
|
|
225
235
|
private takenOver;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mind-harness/mh-browser",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Opens pages in a real browser for the agent, keeps chosen Agent Views in context, and hands the browser to you when a person is needed.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"files": [
|