@deeeed/metamask-harness 0.69.0 → 0.70.1

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,236 +1,108 @@
1
1
  'use strict';
2
2
 
3
- const { execFileSync, spawn } = require('node:child_process');
4
- const fs = require('node:fs');
3
+ // macOS focus helpers for launchers that start a headed browser in the
4
+ // background. One owner (the launcher) and one decision: after the launch
5
+ // settles, if the frontmost process is one of OUR browser pids, give the front
6
+ // back to the app that had it, once, by pid. Anything else in front (the
7
+ // operator's own Chrome included, whatever its bundle) is left alone. A restore
8
+ // only activates an already-running app (never `open -a`, which can launch).
9
+ // MM_HARNESS_FOCUS_HOLD=0 makes every function here a no-op (tests).
5
10
 
6
- // Window activation is not sub-20ms; 80ms keeps the hold responsive while
7
- // spawning far fewer lsappinfo/open subprocesses per second.
8
- const HOLD_INTERVAL_MS = 80;
9
- // Upper bound for one Launch Services query or `open -a`. A short bound made a
10
- // loaded Mac time out the first query, which silently disabled the whole hold.
11
- const LS_TIMEOUT_MS = 2000;
12
- // Hard cap on a hold whose parent is alive but never released it.
13
- const HOLD_MAX_MS = 120_000;
14
- const toolEnv = () => ({ ...process.env });
11
+ const { execFileSync } = require('node:child_process');
15
12
 
16
- function captureMacFrontmost() {
17
- if (process.platform !== 'darwin') return null;
18
- try {
19
- const asn = execFileSync('lsappinfo', ['front'], {
20
- encoding: 'utf8',
21
- env: toolEnv(),
22
- stdio: ['ignore', 'pipe', 'ignore'],
23
- timeout: LS_TIMEOUT_MS,
24
- }).trim();
25
- if (!asn) return null;
26
- return parseLsappinfo(execFileSync('lsappinfo', ['info', asn], {
27
- encoding: 'utf8',
28
- env: toolEnv(),
29
- stdio: ['ignore', 'pipe', 'ignore'],
30
- timeout: LS_TIMEOUT_MS,
31
- }));
32
- } catch {
33
- // Best effort: lsappinfo missing, timed out, or the app exited; no focus to preserve.
34
- return null;
35
- }
13
+ // Upper bound for one Launch Services query or activation.
14
+ const LS_TIMEOUT_MS = 2000;
15
+ // Activates a running app by pid; prints "missing" for a pid with no app.
16
+ const ACTIVATE_BY_PID = [
17
+ 'function run(argv) {',
18
+ " ObjC.import('AppKit');",
19
+ ' const app = $.NSRunningApplication.runningApplicationWithProcessIdentifier(Number(argv[0]));',
20
+ " if (!app || app.isNil()) return 'missing';",
21
+ " return app.activateWithOptions($.NSApplicationActivateIgnoringOtherApps) ? 'ok' : 'failed';",
22
+ '}',
23
+ ].join('\n');
24
+
25
+ function macFocusDisabled() {
26
+ return process.platform !== 'darwin' ||
27
+ process.env.MM_HARNESS_FOCUS_HOLD === '0' ||
28
+ process.env.MM_HARNESS_FOCUS_BROWSER === '1';
36
29
  }
37
30
 
38
- function captureMacFrontmostProcess() {
39
- return captureMacFrontmost()?.pid ?? null;
31
+ // At most one restore per launcher process.
32
+ let restored = false;
33
+
34
+ function lsappinfo(args) {
35
+ return execFileSync('lsappinfo', args, {
36
+ encoding: 'utf8',
37
+ env: { ...process.env },
38
+ stdio: ['ignore', 'pipe', 'ignore'],
39
+ timeout: LS_TIMEOUT_MS,
40
+ });
40
41
  }
41
42
 
42
43
  function parseLsappinfo(info) {
43
44
  const pid = Number.parseInt((info.match(/\bpid\s*=\s*(\d+)/u) || info.match(/"pid"=(\d+)/u) || [])[1], 10);
44
45
  const bundlePath = (info.match(/bundle path="([^"]+)"/u) || info.match(/"LSBundlePath"="([^"]+)"/u) || [])[1];
45
46
  const name = (info.match(/^"([^"]+)"/mu) || info.match(/"LSDisplayName"="([^"]+)"/u) || [])[1];
46
- if (!Number.isInteger(pid) || pid <= 0 || !bundlePath) return null;
47
- return { pid, bundlePath, name: name || bundlePath };
47
+ if (!Number.isInteger(pid) || pid <= 0) return null;
48
+ return { pid, bundlePath: bundlePath || null, name: name || bundlePath || `pid ${pid}` };
48
49
  }
49
50
 
50
- function currentFrontmostPid() {
51
+ // The frontmost app as { pid, bundlePath, name }, or null when unknown.
52
+ function captureMacFrontmost() {
53
+ if (macFocusDisabled()) return null;
51
54
  try {
52
- const asn = execFileSync('lsappinfo', ['front'], {
53
- encoding: 'utf8',
54
- env: toolEnv(),
55
- stdio: ['ignore', 'pipe', 'ignore'],
56
- timeout: LS_TIMEOUT_MS,
57
- }).trim();
58
- if (!asn) return null;
59
- const value = execFileSync('lsappinfo', ['info', '-only', 'pid', asn], {
60
- encoding: 'utf8',
61
- env: toolEnv(),
62
- stdio: ['ignore', 'pipe', 'ignore'],
63
- timeout: LS_TIMEOUT_MS,
64
- }).trim();
65
- const pid = Number.parseInt((value.match(/"pid"=(\d+)/u) || [])[1], 10);
66
- return Number.isInteger(pid) && pid > 0 ? pid : null;
55
+ const asn = lsappinfo(['front']).trim();
56
+ return asn ? parseLsappinfo(lsappinfo(['info', asn])) : null;
67
57
  } catch {
68
- // Best effort: lsappinfo unavailable or timed out; callers treat null as unknown.
58
+ // Best effort: lsappinfo missing, timed out, or the app exited.
69
59
  return null;
70
60
  }
71
61
  }
72
62
 
73
- function restoreMacFrontmost(target) {
74
- if (process.platform !== 'darwin' || !target?.bundlePath) return false;
75
- try {
76
- execFileSync('open', ['-a', target.bundlePath], {
77
- env: toolEnv(),
78
- stdio: 'ignore',
79
- timeout: LS_TIMEOUT_MS,
80
- });
81
- return true;
82
- } catch {
83
- // Best effort: `open -a` failed or timed out; the operator keeps whatever is in front.
84
- return false;
85
- }
63
+ function macFrontmostPid() {
64
+ return captureMacFrontmost()?.pid ?? null;
86
65
  }
87
66
 
88
- function restoreMacFrontmostProcess(pid) {
89
- if (process.platform !== 'darwin' || !Number.isInteger(pid) || pid <= 0) return false;
67
+ // Bring an already-running app to the front by pid. Never launches anything.
68
+ function activateMacAppByPid(pid) {
69
+ if (macFocusDisabled() || !Number.isInteger(pid) || pid <= 0) return false;
90
70
  try {
91
- const asn = execFileSync('lsappinfo', ['find', `pid=${pid}`], {
71
+ return execFileSync('osascript', ['-l', 'JavaScript', '-e', ACTIVATE_BY_PID, String(pid)], {
92
72
  encoding: 'utf8',
93
- env: toolEnv(),
73
+ env: { ...process.env },
94
74
  stdio: ['ignore', 'pipe', 'ignore'],
95
75
  timeout: LS_TIMEOUT_MS,
96
- }).trim();
97
- if (!asn) return false;
98
- return restoreMacFrontmost(parseLsappinfo(execFileSync('lsappinfo', ['info', asn], {
99
- encoding: 'utf8',
100
- env: toolEnv(),
101
- stdio: ['ignore', 'pipe', 'ignore'],
102
- timeout: LS_TIMEOUT_MS,
103
- })));
76
+ }).trim() === 'ok';
104
77
  } catch {
105
- // Best effort: lsappinfo unavailable, timed out, or the pid has no Launch Services entry; nothing to restore.
78
+ // Best effort: osascript failed or timed out; the operator keeps whatever is in front.
106
79
  return false;
107
80
  }
108
81
  }
109
82
 
110
- // Bring `target` back in front of the launched browser. With `launchedBundlePath`
111
- // given, any other frontmost app is an intentional operator switch and is kept.
112
- function preserveMacFrontmost(target, launchedBundlePath) {
113
- if (process.env.MM_HARNESS_FOCUS_BROWSER === '1') return false;
114
- if (process.platform !== 'darwin' || !target?.bundlePath || !Number.isInteger(target.pid) || target.pid <= 0) {
115
- return false;
116
- }
117
- const front = captureMacFrontmost();
118
- if (front && front.pid === target.pid) return true;
119
- if (front && launchedBundlePath && !sameBundle(front.bundlePath, launchedBundlePath)) return false;
120
- return restoreMacFrontmost(target);
121
- }
122
-
123
- function preserveMacFrontmostProcess(pid) {
124
- if (process.env.MM_HARNESS_FOCUS_BROWSER === '1') return false;
125
- if (!Number.isInteger(pid) || pid <= 0) return false;
126
- const current = currentFrontmostPid();
127
- if (current === pid) return true;
128
- return restoreMacFrontmostProcess(pid);
83
+ // One check, at most one restore: 'restored' when our browser had the front
84
+ // and `previous` got it back, 'kept' when anything else is in front (left as
85
+ // is), 'off' when focus handling is disabled or there is nothing to restore.
86
+ // A click on the slot window right after launch can therefore be undone once.
87
+ function restoreFrontmostIfOurs(previous, ourPids) {
88
+ if (macFocusDisabled() || restored || !Number.isInteger(previous?.pid) || previous.pid <= 0) return 'off';
89
+ const ours = new Set((ourPids || []).filter((pid) => Number.isInteger(pid) && pid > 0));
90
+ if (ours.size === 0 || ours.has(previous.pid)) return 'off';
91
+ const front = macFrontmostPid();
92
+ if (front === null || !ours.has(front)) return 'kept';
93
+ restored = true;
94
+ return activateMacAppByPid(previous.pid) ? 'restored' : 'kept';
129
95
  }
130
96
 
131
97
  function macBackgroundOpenArgs(application, chromeArgs) {
132
98
  return ['-g', '-n', '-a', application, '--args', ...chromeArgs];
133
99
  }
134
100
 
135
- // Keep the previously focused app in front while `launchedBundlePath` starts.
136
- // The hold only counters the launched browser: the moment any other app is
137
- // brought to the front, the operator switched on purpose and the hold ends.
138
- function startMacFocusHold(targetOrPid, launchedBundlePath) {
139
- if (process.env.MM_HARNESS_FOCUS_BROWSER === '1') return null;
140
- if (process.platform !== 'darwin') return null;
141
- const target = typeof targetOrPid === 'number'
142
- ? { pid: targetOrPid, bundlePath: bundlePathForPid(targetOrPid) }
143
- : targetOrPid;
144
- if (!target?.bundlePath || !Number.isInteger(target.pid) || target.pid <= 0) return null;
145
- return spawn(process.execPath, [
146
- __filename,
147
- '--hold',
148
- String(target.pid),
149
- target.bundlePath,
150
- String(process.pid),
151
- launchedBundlePath || '',
152
- ], { stdio: 'ignore', env: toolEnv() });
153
- }
154
-
155
- function sameBundle(left, right) {
156
- if (!left || !right) return false;
157
- const real = (value) => {
158
- try {
159
- return fs.realpathSync(value);
160
- } catch {
161
- // A path that cannot be resolved is compared as given.
162
- return value;
163
- }
164
- };
165
- return real(left) === real(right);
166
- }
167
-
168
- function bundlePathForPid(pid) {
169
- try {
170
- const asn = execFileSync('lsappinfo', ['find', `pid=${pid}`], {
171
- encoding: 'utf8',
172
- env: toolEnv(),
173
- stdio: ['ignore', 'pipe', 'ignore'],
174
- timeout: LS_TIMEOUT_MS,
175
- }).trim();
176
- return parseLsappinfo(execFileSync('lsappinfo', ['info', asn], {
177
- encoding: 'utf8',
178
- env: toolEnv(),
179
- stdio: ['ignore', 'pipe', 'ignore'],
180
- timeout: LS_TIMEOUT_MS,
181
- }))?.bundlePath ?? null;
182
- } catch {
183
- // Best effort: lsappinfo unavailable, timed out, or the pid has no Launch Services entry; the hold is skipped.
184
- return null;
185
- }
186
- }
187
-
188
- function stopMacFocusHold(child) {
189
- if (!child || !Number.isInteger(child.pid) || child.pid <= 0) return;
190
- try {
191
- process.kill(child.pid, 'SIGTERM');
192
- } catch (error) {
193
- if (error.code !== 'ESRCH') throw error;
194
- }
195
- }
196
-
197
- function parentAlive(pid) {
198
- if (!Number.isInteger(pid) || pid <= 0) return false;
199
- try {
200
- process.kill(pid, 0);
201
- return true;
202
- } catch {
203
- // ESRCH or EPERM: the parent is gone or not ours; either way the hold must end.
204
- return false;
205
- }
206
- }
207
-
208
- if (require.main === module && process.argv[2] === '--hold') {
209
- const pid = Number.parseInt(process.argv[3], 10);
210
- const bundlePath = process.argv[4];
211
- const parentPid = Number.parseInt(process.argv[5], 10);
212
- const launchedBundlePath = process.argv[6] || '';
213
- const target = { pid, bundlePath };
214
- const hold = () => {
215
- if (!parentAlive(parentPid)) process.exit(0);
216
- const front = captureMacFrontmost();
217
- if (front && front.pid === target.pid) return;
218
- if (front && launchedBundlePath && !sameBundle(front.bundlePath, launchedBundlePath)) process.exit(0);
219
- preserveMacFrontmost(target, launchedBundlePath);
220
- };
221
- hold();
222
- setInterval(hold, HOLD_INTERVAL_MS);
223
- setTimeout(() => process.exit(0), HOLD_MAX_MS);
224
- }
225
-
226
101
  module.exports = {
102
+ activateMacAppByPid,
227
103
  captureMacFrontmost,
228
- captureMacFrontmostProcess,
229
104
  macBackgroundOpenArgs,
230
- preserveMacFrontmost,
231
- preserveMacFrontmostProcess,
232
- restoreMacFrontmost,
233
- restoreMacFrontmostProcess,
234
- startMacFocusHold,
235
- stopMacFocusHold,
105
+ macFocusDisabled,
106
+ macFrontmostPid,
107
+ restoreFrontmostIfOurs,
236
108
  };
@@ -185,6 +185,8 @@ if $LAUNCH_EXISTING_DIST && [ -z "$PREPARE_CMD" ]; then
185
185
  # seeded wallet persists across relaunches. A per-run profile made every
186
186
  # relaunch "foreign" and let live/ pruning delete profiles still in use.
187
187
  PROFILE_ABS="${CHROME_USER_DATA_DIR:-$TARGET/$RUNTIME_DIR/$CHROME_PROFILE_NAME}"
188
+ # One spelling per profile: ownership checks compare it exactly.
189
+ while [ "${PROFILE_ABS%/}" != "$PROFILE_ABS" ]; do PROFILE_ABS="${PROFILE_ABS%/}"; done
188
190
  case "$PROFILE_ABS" in
189
191
  "$TARGET/$RUNTIME_DIR/"*) ;;
190
192
  *) echo "--chrome-user-data-dir must be a dedicated child of $TARGET/$RUNTIME_DIR" >&2; exit 2 ;;
@@ -238,53 +240,31 @@ NODE
238
240
  fi
239
241
  fi
240
242
 
241
- if [ -z "${CHROME_BIN:-}" ]; then
242
- # bash 3.2 (macOS /bin/bash) mis-parses heredocs inside $(...): its scanner
243
- # trips on quotes/backticks in the JS. Write the probe to a temp file and
244
- # command-substitute a plain `node` invocation instead.
245
- _chrome_probe="$(mktemp -t chrome-probe-XXXXXX)"
246
- cat > "$_chrome_probe" <<'NODE'
247
- const fs = require('fs');
248
- const path = require('path');
249
- const { createRequire } = require('module');
250
- const requireFromTarget = createRequire(path.join(process.cwd(), 'package.json'));
251
-
252
- let chromium = null;
253
- function shellQuote(value) {
254
- return `'${String(value).replace(/'/g, `'\\''`)}'`;
255
- }
256
- for (const pkg of ['@playwright/test', 'playwright']) {
257
- try {
258
- chromium = requireFromTarget(pkg).chromium;
259
- if (chromium) break;
260
- } catch (_error) {
261
- // Optional Playwright package unavailable; try the next package name.
262
- }
263
- }
264
- if (!chromium) {
265
- console.error('[recipe-harness] Playwright is not available from this checkout; install dependencies first, or set RECIPE_HARNESS_CHROME_BIN to an explicitly approved browser.');
266
- process.exit(1);
267
- }
268
-
269
- let executable = '';
270
- try {
271
- executable = chromium.executablePath();
272
- } catch (error) {
273
- const message = error && error.message ? error.message : String(error);
274
- console.error(`[recipe-harness] Could not resolve Playwright Chromium executable: ${message}. Manual approval required before installing the Playwright Chromium browser cache (no package.json changes); ask the user before running yarn playwright install chromium.`);
275
- process.exit(1);
276
- }
277
- if (!fs.existsSync(executable)) {
278
- console.error(`[recipe-harness] Playwright Chromium is not installed at ${executable}. Manual approval required before installing the Playwright Chromium browser cache (no package.json changes). Ask the user for approval; if they agree, run: cd ${shellQuote(process.cwd())} && yarn playwright install chromium`);
279
- console.error('[recipe-harness] To use a browser that is already installed, set RECIPE_HARNESS_CHROME_BIN=/path/to/chrome explicitly.');
280
- process.exit(1);
281
- }
282
-
283
- process.stdout.write(executable);
284
- NODE
285
- CHROME_PROBE_LOG="$ARTIFACTS/logs/chrome-probe.log"
286
- CHROME_BIN="$(cd "$TARGET" && node "$_chrome_probe" 2>"$CHROME_PROBE_LOG" || true)"
287
- rm -f "$_chrome_probe"
243
+ # The shared resolver picks the browser (RECIPE_HARNESS_BROWSER=auto|cft|
244
+ # chrome|<path>; RECIPE_HARNESS_CHROME_BIN wins) and records why, so the
245
+ # launcher knows how to load the extension and the run evidence names it.
246
+ # launch-browser.cjs starts a macOS .app through Launch Services, headful,
247
+ # so the probe does too.
248
+ BROWSER_RESOLVER="$SCRIPT_DIR/lib/browser-resolver.cjs"
249
+ [ -f "$BROWSER_RESOLVER" ] || BROWSER_RESOLVER="$SCRIPT_DIR/../shared/browser-resolver.cjs"
250
+ # A preserved profile keeps the browser recorded for it (auto never flips a
251
+ # profile between builds); the resolver bounds itself (probe + watchdog).
252
+ CHROME_PROBE_LOG="$ARTIFACTS/logs/chrome-probe.log"
253
+ BROWSER_RECORD="$TARGET/$RUNTIME_DIR/browser-resolution.json"
254
+ recorded_args=()
255
+ if ! $RESET_PROFILE && [ -f "$PROFILE_ABS/Local State" ] && [ -f "$BROWSER_RECORD" ]; then
256
+ recorded_args=(--recorded "$BROWSER_RECORD")
257
+ fi
258
+ CHROME_BIN="$(cd "$TARGET" && node "$BROWSER_RESOLVER" resolve --target "$TARGET" \
259
+ --launch-method launch-services --display headful ${recorded_args[@]+"${recorded_args[@]}"} \
260
+ --out "$BROWSER_RECORD" --out "$ARTIFACTS/logs/browser-resolution.json" \
261
+ 2>"$CHROME_PROBE_LOG" || true)"
262
+ if [ -n "${CHROME_BIN:-}" ]; then
263
+ cat "$CHROME_PROBE_LOG" >&2 2>/dev/null || true
264
+ elif grep -q 'BROWSER_UNLAUNCHABLE:' "$CHROME_PROBE_LOG" 2>/dev/null; then
265
+ cat "$CHROME_PROBE_LOG" >&2
266
+ echo "[recipe-harness] No launchable browser on this machine; stopping before build or live Extension launch." >&2
267
+ exit 1
288
268
  fi
289
269
 
290
270
  if [ -z "${CHROME_BIN:-}" ]; then
@@ -345,7 +325,8 @@ NODE
345
325
  quoted_wallet_fixture="$(printf '%q' "$WALLET_FIXTURE_ABS")"
346
326
  prepare_parts+=("bash ${quoted_seed_fixture} prefill --target ${quoted_target} --fixture ${quoted_wallet_fixture} --state ${quoted_fixture_state} --profile ${quoted_profile} --extension-dir ${quoted_runtime_dist} --extension-id-file ${quoted_extension_id_file}")
347
327
  fi
348
- chrome_launch_cmd="node ${quoted_chrome_launcher} --chrome-bin ${quoted_chrome} --profile ${quoted_profile} --cdp-port ${CDP_PORT} --extension-dir ${quoted_runtime_dist} --chrome-log ${quoted_chrome_log} --chrome-pid ${quoted_chrome_pid}"
328
+ quoted_browser_record="$(printf '%q' "$BROWSER_RECORD")"
329
+ chrome_launch_cmd="node ${quoted_chrome_launcher} --chrome-bin ${quoted_chrome} --browser-resolution ${quoted_browser_record} --profile ${quoted_profile} --cdp-port ${CDP_PORT} --extension-dir ${quoted_runtime_dist} --chrome-log ${quoted_chrome_log} --chrome-pid ${quoted_chrome_pid}"
349
330
  if [ -n "${START_URL:-}" ]; then
350
331
  chrome_launch_cmd="$chrome_launch_cmd --start-url $(printf '%q' "$START_URL")"
351
332
  fi
@@ -159,7 +159,7 @@
159
159
  "entry": "adapters/extension/live.sh",
160
160
  "kind": "bash",
161
161
  "purpose": "Sequencer: builds the prepare command from the named feature scripts, then launch + verify.",
162
- "inputs": "--target --cdp-port --launch-existing-dist|--start-watch|--build-lavamoat|--prepare-cmd --dist-dir --chrome-user-data-dir --out --artifacts-dir; env RECIPE_HARNESS_CHROME_BIN, RECIPE_WALLET_FIXTURE, RECIPE_HARNESS_LIVE_KEEP",
162
+ "inputs": "--target --cdp-port --launch-existing-dist|--start-watch|--build-lavamoat|--prepare-cmd --dist-dir --chrome-user-data-dir --out --artifacts-dir; env RECIPE_HARNESS_BROWSER, RECIPE_HARNESS_CHROME_BIN, RECIPE_WALLET_FIXTURE, RECIPE_HARNESS_LIVE_KEEP",
163
163
  "outputs": "<artifacts>/summary.json + launch/ + verify/ + logs/fixture-source.json; exit 0/1/2"
164
164
  },
165
165
  {
@@ -279,8 +279,8 @@
279
279
  "entry": "adapters/extension/launch-browser.cjs",
280
280
  "kind": "node",
281
281
  "purpose": "Low-level spawn primitive: detached Chrome with isolated profile, CDP on 127.0.0.1, unpacked dist.",
282
- "inputs": "--cdp-port --chrome-bin --profile --extension-dir --chrome-log --chrome-pid",
283
- "outputs": "pid file + append-log; exit 0 / non-zero"
282
+ "inputs": "--cdp-port --chrome-bin --profile --extension-dir --chrome-log --chrome-pid [--browser-resolution]",
283
+ "outputs": "pid file + append-log + <runtime>/browser-resolution.json; exit 0 / non-zero"
284
284
  },
285
285
  {
286
286
  "id": "extension/validation-process-ownership",
@@ -304,7 +304,7 @@
304
304
  "kind": "bash",
305
305
  "purpose": "Ensure the slot's Chromium is running against the existing dist/profile/port (kills + relaunches).",
306
306
  "inputs": "--slot-id --repo --cdp-port --runtime-dir --watcher-port; env CHROME_USER_DATA_DIR, RECIPE_HARNESS_ROOT",
307
- "outputs": "<runtime-dir>/{browser.pid,chromium.pid,extension.id}; exit 0/1"
307
+ "outputs": "<runtime-dir>/{browser.pid,chromium.pid,extension.id,browser-resolution.json}; exit 0/1"
308
308
  },
309
309
  {
310
310
  "id": "extension/inject",
@@ -506,6 +506,22 @@
506
506
  "inputs": "--target <checkout>",
507
507
  "outputs": "corepack yarn install output; exit 0/1/2"
508
508
  },
509
+ {
510
+ "id": "lib/browser-resolver",
511
+ "entry": "adapters/shared/browser-resolver.cjs",
512
+ "kind": "node",
513
+ "purpose": "Choose the extension browser (RECIPE_HARNESS_BROWSER=auto|cft|chrome|<path>; probe-launch Chrome for Testing, cached per machine and version, branded Chrome fallback) and load an unpacked extension over CDP for branded Chrome.",
514
+ "inputs": "resolve --target <checkout> [--launch-method spawn|launch-services] [--display headless|headful] [--recorded <file>] [--out <file>] [--json] | load-unpacked --port <cdp> --path <dist> --profile <dir> [--expect-id <id>] [--open-url <url>]; env RECIPE_HARNESS_BROWSER, RECIPE_HARNESS_CHROME_BIN, RECIPE_HARNESS_CHROME_CANDIDATES, RECIPE_HARNESS_BROWSER_CACHE",
515
+ "outputs": "executable path or resolution JSON on stdout; probe cache under ~/.cache/mm-harness; exit 0/1"
516
+ },
517
+ {
518
+ "id": "lib/browser-cdp",
519
+ "entry": "adapters/shared/browser-cdp.cjs",
520
+ "kind": "lib",
521
+ "purpose": "Browser-level CDP client with per-command deadlines; prove a CDP port belongs to a slot profile, load an unpacked extension (Extensions.loadUnpacked), audit for other user extensions.",
522
+ "inputs": "CDP port, extension dist, owning profile",
523
+ "outputs": "loaded extension id or an error; never loads into a browser it does not own"
524
+ },
509
525
  {
510
526
  "id": "lib/resolve-slot-ports",
511
527
  "entry": "adapters/shared/resolve-slot-ports.mjs",