@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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Mind Harness",
4
- "version": "0.1.4",
4
+ "version": "0.1.6",
5
5
  "permissions": ["tabGroups"],
6
6
  "background": { "service_worker": "sw.js" }
7
7
  }
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
- for (const page of context.pages()) if (page.url() === "about:blank") await page.close().catch(() => void 0);
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 crashes than this within the window and the browser stays down. */
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
- relaunching;
5047
- crashes = [];
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.ready;
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.ready.then(() => void 0, (error) => error instanceof Error ? error.message : String(error));
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.ready;
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.ready;
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.ready;
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.ready;
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.relaunching = this.relaunch();
6339
+ if (this.closing || this.context !== context) return;
6340
+ this.onExit();
6325
6341
  });
6326
6342
  }
6327
- async relaunch() {
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.crashes.length > 0 && now - (this.crashes[0] ?? now) > RELAUNCH_WINDOW_MS) this.crashes.shift();
6333
- this.crashes.push(now);
6334
- if (this.crashes.length > RELAUNCH_LIMIT) {
6335
- const down = Promise.reject(new MhBrowserError(BROWSER_UNAVAILABLE, `the browser exited ${String(this.crashes.length)} times within ${String(RELAUNCH_WINDOW_MS / 1e3)} s and was not restarted`));
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.ready = this.launch();
6342
- await this.ready.catch(() => void 0);
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 Memory login or consent form, or any form that issues credentials, and whenever only the person can continue."
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.";
@@ -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":610,"column":9},
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":610,"column":9},
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
- private relaunching;
40
- private readonly crashes;
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
- private relaunch;
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;
@@ -3,5 +3,7 @@ export declare const testHooks: unique symbol;
3
3
  export interface TestHooks {
4
4
  crash(): Promise<void>;
5
5
  page(owner: string, tab: string): unknown;
6
+ /** A browser process is up. */
7
+ running(): boolean;
6
8
  }
7
9
  //# sourceMappingURL=test-hooks.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mind-harness/mh-browser",
3
- "version": "0.1.4",
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": [