@deeeed/metamask-harness 0.78.0 → 0.80.0

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.
Files changed (215) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +1 -1
  3. package/adapters/extension/ensure-browser.sh +2 -2
  4. package/adapters/extension/inject.mjs +0 -1
  5. package/adapters/extension/launch-browser.cjs +39 -613
  6. package/adapters/extension/readiness.mjs +2 -5
  7. package/adapters/extension/reattach.sh +2 -2
  8. package/adapters/extension/stamp-runtime-title.cjs +1 -1
  9. package/adapters/manifest.json +2 -34
  10. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +57 -1358
  11. package/adapters/mobile/bridge-runtime/teaching.cjs +20 -0
  12. package/adapters/mobile/inject.sh +16 -0
  13. package/adapters/mobile/launch-console-forwarder.cjs +1 -2
  14. package/adapters/mobile/open-device.sh +2 -2
  15. package/adapters/mobile/reload-app.mjs +4 -4
  16. package/adapters/mobile/reset-app-data.sh +1 -1
  17. package/adapters/mobile/start-console-forwarder.sh +3 -2
  18. package/adapters/mobile/start-metro.sh +17 -12
  19. package/adapters/mobile/wait-for-bridge.cjs +2 -2
  20. package/adapters/shared/ensure-runner-deps.sh +2 -2
  21. package/adapters/shared/harness-source-fingerprint.mjs +13 -8
  22. package/adapters/shared/open-debug.mjs +2 -2
  23. package/adapters/shared/runner-deps-ready.mjs +1 -1
  24. package/adapters/shared/sync-wallet-fixture.sh +8 -5
  25. package/adapters/terminal/cleanup.mjs +2 -1
  26. package/adapters/terminal/inject.mjs +2 -1
  27. package/adapters/terminal/lib/cdp-client.mjs +1 -1
  28. package/bin/mm-harness +9 -28
  29. package/dist/adapters/checkout-files.js +16 -0
  30. package/dist/adapters/console-capture.js +14 -1
  31. package/dist/adapters/core/actions.js +4 -3
  32. package/dist/adapters/core/overlay.js +1 -1
  33. package/dist/adapters/core/readiness.js +28 -1
  34. package/dist/adapters/core/run.js +3 -3
  35. package/dist/adapters/core/surface.js +22 -5
  36. package/dist/adapters/extension/browser-pid.js +56 -0
  37. package/dist/adapters/extension/console-capture.js +1 -1
  38. package/dist/adapters/extension/doctor-checks.js +1 -1
  39. package/dist/adapters/extension/ensure-ready.js +5 -4
  40. package/dist/adapters/extension/extension-id.js +1 -1
  41. package/dist/adapters/extension/feature-flags.js +1 -1
  42. package/dist/adapters/extension/fixtures.js +1 -2
  43. package/dist/adapters/extension/launch.js +4 -5
  44. package/dist/adapters/extension/launched-browser.js +1 -2
  45. package/dist/adapters/extension/network-observer.js +1 -1
  46. package/dist/adapters/extension/overlay.js +1 -1
  47. package/dist/adapters/extension/product-config.js +1 -1
  48. package/dist/adapters/extension/readiness.js +1 -1
  49. package/dist/adapters/extension/reload.js +1 -1
  50. package/dist/adapters/extension/run.js +24 -10
  51. package/dist/adapters/extension/runtime-decision.js +3 -2
  52. package/dist/adapters/extension/runtime.js +9 -5
  53. package/dist/adapters/extension/surface.js +20 -5
  54. package/dist/adapters/extension/verify.js +2 -2
  55. package/dist/adapters/harness-freshness.js +2 -1
  56. package/dist/adapters/mobile/android-gfxinfo-performance.js +1 -3
  57. package/dist/adapters/mobile/app-identity.js +10 -0
  58. package/dist/adapters/mobile/doctor-checks.js +1 -1
  59. package/dist/adapters/mobile/fixtures.js +1 -1
  60. package/dist/adapters/mobile/launch.js +27 -28
  61. package/dist/adapters/mobile/lifecycle.js +1 -1
  62. package/dist/adapters/mobile/metro-env.js +9 -59
  63. package/dist/adapters/mobile/performance-observer.js +1 -1
  64. package/dist/adapters/mobile/prepare-target.js +1 -1
  65. package/dist/adapters/mobile/prepare.js +4 -5
  66. package/dist/adapters/mobile/provision.js +1 -1
  67. package/dist/adapters/mobile/readiness.js +1 -1
  68. package/dist/adapters/mobile/recording-target.js +15 -0
  69. package/dist/adapters/mobile/release-artifact-state.js +1 -1
  70. package/dist/adapters/mobile/reload.js +1 -1
  71. package/dist/adapters/mobile/run.js +9 -8
  72. package/dist/adapters/mobile/runtime-decision.js +5 -7
  73. package/dist/adapters/mobile/source-freshness.js +22 -126
  74. package/dist/adapters/mobile/surface.js +31 -6
  75. package/dist/adapters/mobile/ui.js +16 -5
  76. package/dist/adapters/mobile/verify.js +2 -3
  77. package/dist/adapters/performance/cdp-trace.js +1 -1
  78. package/dist/adapters/performance/device-platform.js +1 -1
  79. package/dist/adapters/registry.js +2 -14
  80. package/dist/adapters/slot-ports.js +1 -1
  81. package/dist/adapters/surface.js +1 -0
  82. package/dist/adapters/terminal/console-capture.js +1 -1
  83. package/dist/adapters/terminal/launch.js +1 -1
  84. package/dist/adapters/terminal/run.js +25 -2
  85. package/dist/adapters/terminal/surface.js +12 -4
  86. package/dist/adapters.js +8 -4
  87. package/dist/advisor-policy.js +1 -1
  88. package/dist/checkout-view.js +1 -2
  89. package/dist/cli.js +28 -15
  90. package/dist/command-contract.js +1 -26
  91. package/dist/commands/call.js +10 -24
  92. package/dist/commands/check.js +3 -4
  93. package/dist/commands/checklist.js +1 -1
  94. package/dist/commands/completion-candidates.js +2 -10
  95. package/dist/commands/config.js +2 -2
  96. package/dist/commands/core-readiness.js +12 -7
  97. package/dist/commands/debug.js +3 -3
  98. package/dist/commands/device-target.js +5 -5
  99. package/dist/commands/doctor.js +2 -14
  100. package/dist/commands/domain.js +2 -2
  101. package/dist/commands/ensure-ready.js +1 -7
  102. package/dist/commands/fixtures.js +5 -6
  103. package/dist/commands/help.js +3 -4
  104. package/dist/commands/launch/device.js +10 -17
  105. package/dist/commands/launch/extension.js +3 -9
  106. package/dist/commands/launch/mobile.js +1 -1
  107. package/dist/commands/list-executables.js +1 -3
  108. package/dist/commands/logs.js +3 -3
  109. package/dist/commands/manifest.js +1 -12
  110. package/dist/commands/mobile-device-view.js +4 -2
  111. package/dist/commands/overlay.js +110 -0
  112. package/dist/commands/pin.js +1 -1
  113. package/dist/commands/pr-body.js +2 -2
  114. package/dist/commands/prepare.js +6 -11
  115. package/dist/commands/provision.js +3 -12
  116. package/dist/commands/recipe-advice.js +1 -2
  117. package/dist/commands/recipe-quality.js +2 -3
  118. package/dist/commands/resolve-extension.js +1 -7
  119. package/dist/commands/review.js +4 -4
  120. package/dist/commands/run-engine.js +12 -51
  121. package/dist/commands/run.js +4 -25
  122. package/dist/commands/runtime-decision.js +1 -6
  123. package/dist/commands/runtime-health.js +1 -7
  124. package/dist/commands/runtime-launch.js +1 -13
  125. package/dist/commands/self-test.js +2 -6
  126. package/dist/commands/status-watch.js +1 -2
  127. package/dist/commands/status.js +2 -5
  128. package/dist/commands/task-init.js +2 -3
  129. package/dist/commands/tutorial.js +2 -2
  130. package/dist/commands/update.js +1 -1
  131. package/dist/completions-cache.js +1 -1
  132. package/dist/console-classifier.js +19 -0
  133. package/dist/discovery.js +1 -1
  134. package/dist/doctor.js +3 -4
  135. package/dist/funding-execution-context.js +15 -1
  136. package/dist/host.js +32 -0
  137. package/dist/index.js +1 -6
  138. package/dist/live-tsx.js +49 -0
  139. package/dist/mm-harness-cli.js +2 -4
  140. package/dist/network-observation.js +4 -4
  141. package/dist/paths.js +48 -78
  142. package/dist/performance-observation.js +2 -2
  143. package/dist/progress.js +1 -1
  144. package/dist/recipe-assistance-context.js +2 -4
  145. package/dist/recipe-security.js +2 -59
  146. package/dist/review/knowledge.js +3 -3
  147. package/dist/runner.js +5 -3
  148. package/dist/runtime-context.js +1 -1
  149. package/dist/task-view.js +3 -4
  150. package/docs/CONTRIBUTING.md +3 -3
  151. package/docs/cli-contract.md +2 -2
  152. package/docs/feature-guard.md +90 -0
  153. package/library/README.md +3 -1
  154. package/library/actions/core/perps/_controller.mjs +153 -130
  155. package/library/actions/core/perps/quote_fees.mjs +58 -0
  156. package/library/actions/core/wallet/_fixture-accounts.mjs +69 -0
  157. package/library/actions/core/wallet/list_accounts.mjs +14 -57
  158. package/library/actions/extension/feature_flags/state.mjs +1 -1
  159. package/library/actions/extension/platform/cdp.mjs +2 -8
  160. package/library/actions/harness-exports.mjs +1 -6
  161. package/library/actions/mobile/app/network-control.mjs +6 -8
  162. package/library/actions/mobile/perps/prepare_local_snapshot_endpoint.mjs +1 -1
  163. package/library/actions/mobile/platform/bridge.mjs +8 -8
  164. package/library/actions/mobile/platform/native-session-name.mjs +21 -1
  165. package/library/actions/mobile/platform/native-session.mjs +3 -4
  166. package/library/actions/mobile/platform/observe-ui.mjs +12 -6
  167. package/library/actions/mobile/ui/native-navigation.mjs +4 -3
  168. package/library/actions/mobile/wallet/native-ui.mjs +3 -2
  169. package/library/actions/shared/analytics/_adapter.mjs +1 -1
  170. package/library/actions/shared/console-findings.mjs +1 -1
  171. package/library/actions/terminal/platform/runtime.mjs +2 -1
  172. package/library/manifests/core.action-manifest.json +83 -13
  173. package/package.json +8 -8
  174. package/adapters/extension/lib/slot-title.cjs +0 -292
  175. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +0 -472
  176. package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +0 -125
  177. package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +0 -1055
  178. package/adapters/mobile/bridge-runtime/lib/cdp-eval.cjs +0 -110
  179. package/adapters/mobile/bridge-runtime/lib/config.cjs +0 -61
  180. package/adapters/mobile/bridge-runtime/lib/console-format.cjs +0 -53
  181. package/adapters/mobile/bridge-runtime/lib/devtools-proxy.cjs +0 -177
  182. package/adapters/mobile/bridge-runtime/lib/issue-capture.cjs +0 -446
  183. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +0 -84
  184. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +0 -403
  185. package/adapters/mobile/bridge-runtime/lib/ws-client.cjs +0 -116
  186. package/adapters/mobile/coalesce-metro-log.cjs +0 -24
  187. package/adapters/mobile/launch-metro.cjs +0 -77
  188. package/adapters/mobile/metro-config.cjs +0 -98
  189. package/adapters/mobile/metro-log-generation.cjs +0 -106
  190. package/dist/adapters/mobile/frame-metrics.js +0 -118
  191. package/dist/adapters/mobile/video-recorder.js +0 -289
  192. package/dist/artifact-files.js +0 -92
  193. package/dist/checkout-lock.js +0 -149
  194. package/dist/cli-color.js +0 -98
  195. package/dist/command-journal.js +0 -308
  196. package/dist/commands/last.js +0 -60
  197. package/dist/commands/launch/index.js +0 -109
  198. package/dist/commands/parse-args.js +0 -279
  199. package/dist/commands/reload.js +0 -41
  200. package/dist/commands/run-report.js +0 -241
  201. package/dist/commands/shared.js +0 -323
  202. package/dist/commands/stop.js +0 -60
  203. package/dist/devices.js +0 -100
  204. package/dist/execution-provenance.js +0 -332
  205. package/dist/harness.js +0 -451
  206. package/dist/heal-bounds.js +0 -137
  207. package/dist/json-stream.js +0 -60
  208. package/dist/leaf-invoke.js +0 -19
  209. package/dist/library-provenance.js +0 -33
  210. package/dist/live-adapter-contract.js +0 -380
  211. package/dist/recording-target.js +0 -120
  212. package/dist/run-diagnostics.js +0 -364
  213. package/dist/run-recording.js +0 -481
  214. package/dist/runtime-proof.js +0 -90
  215. package/library/actions/mobile/platform/tool-paths.mjs +0 -123
@@ -1,59 +1,26 @@
1
1
  #!/usr/bin/env node
2
- // launch-browser.cjs — per-instance isolated detached Chrome spawn
2
+ // launch-browser.cjs — the Extension's isolated detached Chrome launch
3
3
  //
4
- // Purpose:
5
- // Spawns a detached Chrome bound to 127.0.0.1:<cdp-port> with its own
6
- // profile and the runtime dist loaded as an unpacked extension: by
7
- // --load-extension, or over CDP (Extensions.loadUnpacked) for branded Chrome,
8
- // which ignores that flag. The browser choice comes from
9
- // <runtime>/browser-resolution.json when it names --chrome-bin.
4
+ // Runs @farmslot/adapter-web's launchBrowser with what MetaMask owns: the
5
+ // build-handoff lock (no build swaps the dist mid-launch), home.html and its
6
+ // "MetaMask" title, the harness runtime layouts that prove a browser is ours by
7
+ // its --load-extension path, and the mm-harness rerun command.
10
8
  //
11
- // Inputs (flags, all required):
9
+ // Inputs (flags, all required unless noted):
12
10
  // --cdp-port <port> --chrome-bin <path> --profile <dir>
13
11
  // --extension-dir <dist> --chrome-log <file> --chrome-pid <file>
14
- // Env is sanitized for the child (DYLD_*, PYTHON*, BUNDLED_DEBUGPY_PATH).
12
+ // [--browser-resolution <resolver record>] [--start-url <url>]
13
+ // [--stop-only 1] [--reset-profile 1]
14
+ // MM_HARNESS_FOCUS_SETTLE_MS: wait before the one macOS focus check (default 1000).
15
15
  //
16
- // Outputs:
17
- // <chrome-pid> pid file; <chrome-log> append-log;
18
- // <runtime>/browser-resolution.json (browser, version, loading method, pid).
19
- // Exit 0 — spawned; non-zero — invalid/missing args, missing chrome binary,
20
- // or missing dist manifest (thrown error message on stderr).
21
- //
22
- // Never touches: the extension dist (read-only), anything outside the
23
- // profile/log/pid paths it was given.
16
+ // Outputs: see launchBrowser. Exit 0 — launched (or released with --stop-only);
17
+ // non-zero — invalid/missing args, missing chrome binary or dist manifest, or a
18
+ // failed launch (thrown error message on stderr).
24
19
  'use strict';
25
20
 
26
- const fs = require('node:fs');
27
- const os = require('node:os');
28
21
  const path = require('node:path');
29
- const { execFileSync, spawn, spawnSync } = require('node:child_process');
30
- const { extensionIdFromExtensionDir } = require('@farmslot/adapter-web/extension-id');
31
- // The resolver CLI runs as a bounded child for CDP calls the launcher must not hang on.
32
- const browserResolverPath = require.resolve('@farmslot/adapter-web/browser-resolver');
33
- const { CDP_LOAD_UNPACKED, describeBrowser, extensionLaunchArgs, waitForCdpOwner } = require(browserResolverPath);
34
- const { profileProcessPids, stopProfileProcessesSync } = require('@farmslot/adapter-web/validation-process-ownership');
22
+ const { launchBrowser } = require('@farmslot/adapter-web/launch-browser');
35
23
  const { acquireBuildHandoff } = require('./build-handoff.cjs');
36
- const {
37
- automationRuntimeArgs,
38
- clearDetachedLaunchUnproven,
39
- clearValidationPortQuarantine,
40
- createRuntimeIdentityNonce,
41
- hasDetachedLaunchUnproven,
42
- isolatedProfileArgs,
43
- markDetachedLaunchUnproven,
44
- markValidationPortLaunchUnproven,
45
- remoteDebuggingArgs,
46
- removeRuntimeIdentity,
47
- runtimeIdentityArgs,
48
- readValidationPortQuarantine,
49
- validationLaunchQuarantineError,
50
- writeRuntimeIdentity,
51
- } = require('@farmslot/adapter-web/chrome-args');
52
- const {
53
- captureMacFrontmost,
54
- macBackgroundOpenArgs,
55
- restoreFrontmostIfOurs,
56
- } = require('@farmslot/adapter-web/macos-focus');
57
24
 
58
25
  if (process.argv.includes('--help') || process.argv.includes('-h')) {
59
26
  console.log(
@@ -64,368 +31,28 @@ if (process.argv.includes('--help') || process.argv.includes('-h')) {
64
31
 
65
32
  const args = parseArgs(process.argv.slice(2));
66
33
  const cdpPort = Number(args['cdp-port']);
67
- if (!Number.isInteger(cdpPort) || cdpPort <= 0) {
68
- throw new Error(`Invalid --cdp-port: ${args['cdp-port'] || ''}`);
69
- }
70
- for (const key of ['chrome-bin', 'profile', 'extension-dir', 'chrome-log', 'chrome-pid']) {
71
- if (!args[key]) throw new Error(`Missing --${key}`);
72
- }
73
- const runtimeDir = path.dirname(path.resolve(args['extension-dir']));
74
- // --stop-only releases the profile before the dist snapshot exists on a first
75
- // run, so launch-input validation applies only to a real launch.
76
- if (args['stop-only'] === undefined) {
77
- if (!fs.existsSync(args['chrome-bin'])) throw new Error(`Chrome binary not found: ${args['chrome-bin']}`);
78
- if (!fs.existsSync(path.join(args['extension-dir'], 'manifest.json'))) {
79
- throw new Error(`Extension dist manifest not found: ${path.join(args['extension-dir'], 'manifest.json')}`);
80
- }
81
- }
82
-
83
- // Isolation guard: a standalone launch must use a per-checkout profile, never a
84
- // shared (e.g. ~/.chrome-farmslot) or the OS-default browser profile. Farm dispatch
85
- // injects its own already-isolated profile via runtime-context, so this only trips
86
- // on a genuinely personal/shared path — protecting the user's browser data.
87
- assertIsolatedProfile(args.profile);
88
- if (args['reset-profile'] !== undefined) assertResettableProfile(args.profile, runtimeDir);
89
-
90
- fs.mkdirSync(args.profile, { recursive: true });
91
- fs.mkdirSync(path.dirname(args['chrome-log']), { recursive: true });
92
- fs.mkdirSync(path.dirname(args['chrome-pid']), { recursive: true });
93
- const releaseBuildHandoff = acquireBuildHandoff(runtimeDir);
94
- process.on('exit', releaseBuildHandoff);
95
-
96
- // Ownership guard: only ever take over a CDP endpoint we provably launched. A pid
97
- // on this port is ours iff its command line loads our --user-data-dir; anything
98
- // else is a browser this harness did not launch (possibly the user's personal
99
- // Chrome), so we refuse rather than kill it or attach to it.
100
- const listenerPids = cdpListenerPids(cdpPort);
101
- const foreignPids = listenerPids.filter((pid) => !processIsOwnedHarnessBrowser(pid, args.profile, args['extension-dir']));
102
- if (foreignPids.length > 0) {
103
- throw new Error(
104
- `Refusing to launch on CDP port ${cdpPort}: it is held by a browser this harness did not launch (pid ${foreignPids.join(', ')}). ` +
105
- `Next: pick a free --cdp-port, or stop that browser.`,
106
- );
107
- }
108
- const ownedPids = new Set(listenerPids);
109
- for (const pid of profileProcessPids(args.profile)) ownedPids.add(pid);
110
- const previousPid = readPidFile(args['chrome-pid']);
111
- if (previousPid !== null && processIsOwnedHarnessBrowser(previousPid, args.profile, args['extension-dir'])) ownedPids.add(previousPid);
112
- try {
113
- removeRuntimeIdentity(runtimeDir);
114
- } catch (error) {
115
- const detail = error instanceof Error ? error.message : String(error);
116
- throw new Error(
117
- `Failed to invalidate Extension runtime identity under ${runtimeDir}: ${detail}. ` +
118
- 'Next: remove the invalid runtime identity and fix owner permissions, then rerun: mm-harness launch --adapter extension --build',
119
- );
120
- }
121
- stopProfileProcessesSync(args.profile, { extraPids: [...ownedPids] });
122
- if (args['reset-profile'] !== undefined) {
123
- fs.rmSync(args.profile, { recursive: true, force: true });
124
- fs.mkdirSync(args.profile, { recursive: true });
125
- }
126
- removeProfileSingletonLocks(args.profile);
127
-
128
- // --stop-only: release the profile (owned Chrome terminated, singleton locks
129
- // cleared) without launching. The prefill step needs the profile's LevelDB
130
- // unlocked BEFORE it writes; a prior run's Chrome would hold the lock and the
131
- // write fails with "Database is not open".
132
- if (args['stop-only'] !== undefined) {
133
- process.exit(0);
134
- }
135
-
136
- const requestedProfileQuarantined = hasDetachedLaunchUnproven(args.profile);
137
- const portQuarantine = readValidationPortQuarantine(cdpPort);
138
- const validationPortLease = process.env.FARMSLOT_VALIDATION_PORT_LEASE;
139
- const activeValidationLease = Boolean(
140
- portQuarantine?.profile === path.resolve(args.profile) &&
141
- portQuarantine.lease === validationPortLease &&
142
- /^[a-f0-9]{64}$/u.test(validationPortLease || ''),
143
- );
144
- if (requestedProfileQuarantined || (portQuarantine && !activeValidationLease)) {
145
- throw validationLaunchQuarantineError(
146
- cdpPort,
147
- portQuarantine?.profile || (requestedProfileQuarantined ? args.profile : undefined),
148
- requestedProfileQuarantined ? args.profile : undefined,
149
- );
150
- }
151
-
152
- const initialUrl = args['start-url'] || extensionHomeUrl(args['extension-dir']) || 'chrome://extensions/';
153
- const browser = launchBrowserResolution(args['browser-resolution'], args['chrome-bin']);
154
- const loadsOverCdp = browser.extensionLoading === CDP_LOAD_UNPACKED;
155
- let cdpLoad = null;
156
- let owner = null;
157
- const runtimeNonce = createRuntimeIdentityNonce();
158
- const startedAt = Date.now();
159
- const chromeArgs = [
160
- `--user-data-dir=${args.profile}`,
161
- // Debug-port trio (address + port + scoped allow-origins) from the shared module,
162
- // so the DevTools origin allow-list can never drift away from the port.
163
- ...remoteDebuggingArgs(cdpPort),
164
- '--no-first-run',
165
- '--disable-first-run-ui',
166
- ...automationRuntimeArgs(),
167
- ...runtimeIdentityArgs(runtimeNonce),
168
- ...isolatedProfileArgs(),
169
- '--disable-default-apps',
170
- '--disable-popup-blocking',
171
- '--disable-extensions-file-access-check',
172
- '--disable-extensions-content-verification',
173
- '--disable-features=ExtensionContentVerification,DisableLoadExtensionCommandLineSwitch',
174
- ...extensionLaunchArgs(args['extension-dir'], browser.extensionLoading),
175
- ];
176
- // Initial tab: caller's dapp URL when provided, else the extension home. The
177
- // extension home avoids leaving disposable chrome://newtab/ or extensions
178
- // manager tabs in the operator's slot browser. On macOS the browser starts
179
- // through Launch Services in the background with no startup window (Chrome
180
- // activates itself when it opens one), and the first window is opened over
181
- // CDP as a background window. A CDP-loaded extension does not exist yet at
182
- // startup, so its window is opened after the load.
183
- const application = process.platform === 'darwin' ? macApplicationForExecutable(args['chrome-bin']) : null;
184
- if (application) chromeArgs.push('--no-startup-window');
185
- else chromeArgs.push(loadsOverCdp ? 'about:blank' : initialUrl);
186
- const logFd = fs.openSync(args['chrome-log'], 'a');
187
- const previousFrontmost = captureMacFrontmost();
188
- let browserPid;
189
- try {
190
- try {
191
- if (application) {
192
- beginDetachedLaunch(cdpPort, args.profile, activeValidationLease);
193
- execFileSync('open', macBackgroundOpenArgs(application, chromeArgs), {
194
- env: sanitizedChildEnv(),
195
- stdio: ['ignore', logFd, logFd],
196
- });
197
- browserPid = waitForOwnedCdpPid(cdpPort, args.profile, args['extension-dir']);
198
- if (browserPid === null) {
199
- stopProfileProcessesSync(args.profile, { waitForAppearanceMs: 400 });
200
- throw new Error(
201
- `Chrome launched but did not expose an owned CDP listener on 127.0.0.1:${cdpPort}. ` +
202
- `Next: inspect ${args['chrome-log']}, then rerun: mm-harness launch --adapter extension --build`,
203
- );
204
- }
205
- } else {
206
- beginDetachedLaunch(cdpPort, args.profile, activeValidationLease);
207
- const child = spawn(args['chrome-bin'], chromeArgs, {
208
- detached: true,
209
- env: sanitizedChildEnv(),
210
- stdio: ['ignore', logFd, logFd],
211
- });
212
- child.unref();
213
- browserPid = waitForOwnedCdpPid(cdpPort, args.profile, args['extension-dir']);
214
- if (browserPid === null) {
215
- stopProfileProcessesSync(args.profile, { extraPids: child.pid ? [child.pid] : [] });
216
- throw new Error(
217
- `Chrome launched but did not expose an owned CDP listener on 127.0.0.1:${cdpPort}. ` +
218
- `Next: inspect ${args['chrome-log']}, then rerun: mm-harness launch --adapter extension --build`,
219
- );
220
- }
221
- }
222
- if (loadsOverCdp) cdpLoad = loadExtensionOverCdp(browserPid);
223
- else if (application) openBackgroundWindow(browserPid, initialUrl);
224
- owner = cdpLoad?.owner ?? ownerIdentity(browserPid);
225
- for (const problem of pruneExtraHomeTabs(cdpPort, args['extension-dir'])) {
226
- process.stderr.write(`[launch] home-tab pruning: ${problem}\n`);
227
- }
228
- } finally {
229
- fs.closeSync(logFd);
230
- }
231
- fs.writeFileSync(args['chrome-pid'], `${browserPid}\n`);
232
- writeBrowserResolution(runtimeDir, {
233
- ...browser,
234
- launch: {
235
- pid: browserPid,
236
- cdpPort,
237
- startedAt: new Date(startedAt).toISOString(),
238
- ...(owner ? { profile: owner.profile, processStartedAt: owner.startedAt } : {}),
239
- ...(cdpLoad ? { extensionId: cdpLoad.id, otherExtensions: cdpLoad.otherExtensions } : {}),
240
- },
241
- });
242
- try {
243
- writeRuntimeIdentity(runtimeDir, {
244
- port: cdpPort,
245
- pid: browserPid,
246
- startedAt,
247
- nonce: runtimeNonce,
248
- });
249
- clearDetachedLaunchUnproven(args.profile);
250
- if (!activeValidationLease) {
251
- clearValidationPortQuarantine(cdpPort, args.profile);
252
- }
253
- } catch (error) {
254
- terminatePids([browserPid]);
255
- fs.rmSync(args['chrome-pid'], { force: true });
256
- const detail = error instanceof Error ? error.message : String(error);
257
- throw new Error(
258
- `Failed to persist Extension runtime identity under ${runtimeDir}: ${detail}. ` +
259
- 'Next: fix owner write permissions for the runtime directory, then rerun: mm-harness launch --adapter extension --build',
260
- );
261
- }
262
- if (process.platform === 'darwin') {
263
- const wake = spawn('caffeinate', ['-dims', '-w', String(browserPid)], {
264
- detached: true,
265
- stdio: 'ignore',
266
- });
267
- wake.unref();
268
- // Let the first window settle before the one focus check below.
269
- const settleMs = Number(process.env.MM_HARNESS_FOCUS_SETTLE_MS);
270
- const waitSec = Number.isFinite(settleMs) && settleMs >= 0 ? settleMs / 1000 : 1;
271
- if (waitSec > 0) spawnSync('sleep', [String(waitSec)]);
272
- }
273
- } finally {
274
- // One check, at most one restore, and only if OUR browser (this launch's
275
- // CDP owner pid) holds the front; any other app in front is left alone.
276
- if (browserPid) {
277
- const outcome = restoreFrontmostIfOurs(previousFrontmost, [Number(browserPid)]);
278
- if (outcome === 'restored') process.stderr.write(`[launch] focus: the slot browser took the front; restored ${previousFrontmost.name}\n`);
279
- }
280
- }
281
-
282
- // The first window of a browser started without one, opened in the background.
283
- // Without it the slot has no window at all, so a failure fails the launch:
284
- // the owned browser is stopped (its markers kept if it cannot be).
285
- function openBackgroundWindow(pid, url) {
286
- const result = spawnSync(process.execPath, [browserResolverPath, 'open-window', '--port', String(cdpPort), '--url', url], {
287
- encoding: 'utf8',
288
- timeout: 30_000,
289
- });
290
- if (result.status === 0) return;
291
- const openError = (result.stderr || result.error?.message || `exit ${result.status}`).trim();
292
- try {
293
- stopProfileProcessesSync(args.profile, { extraPids: [pid] });
294
- } catch (stopError) {
295
- throw new Error(
296
- `Chrome started but its start window could not be opened (${openError}), and stopping it failed: ${stopError.message}. ` +
297
- `The launch markers for port ${cdpPort} and ${args.profile} are kept. Next: stop pid ${pid}, then rerun.`,
298
- );
299
- }
300
- clearDetachedLaunchUnproven(args.profile);
301
- if (!activeValidationLease) clearValidationPortQuarantine(cdpPort, args.profile);
302
- throw new Error(`Chrome started but its start window could not be opened: ${openError}. Next: inspect ${args['chrome-log']}, then rerun.`);
303
- }
304
-
305
- // live.sh passes the record its resolver just wrote (--browser-resolution);
306
- // reuse it when it names the same binary. A direct --chrome-bin without it is
307
- // the caller's own explicit choice and is recorded as such, whatever an older
308
- // record of this profile says.
309
- function launchBrowserResolution(file, chromeBin) {
310
- if (!file) return explicitResolution(chromeBin);
311
- try {
312
- const recorded = JSON.parse(fs.readFileSync(file, 'utf8'));
313
- if (recorded && recorded.bin === chromeBin && typeof recorded.extensionLoading === 'string') {
314
- const { launch: _previousLaunch, ...resolution } = recorded;
315
- return resolution;
316
- }
317
- } catch {
318
- // No resolver record for this launch; fall through to a direct description.
319
- }
320
- return explicitResolution(chromeBin);
321
- }
322
-
323
- function explicitResolution(chromeBin) {
324
- return { schemaVersion: 2, ...describeBrowser(chromeBin), source: '--chrome-bin', mode: 'explicit', selection: 'explicit', resolvedAt: new Date().toISOString() };
325
- }
326
-
327
- function writeBrowserResolution(runtime, resolution) {
328
- fs.writeFileSync(path.join(runtime, 'browser-resolution.json'), `${JSON.stringify(resolution, null, 2)}\n`);
329
- }
330
-
331
- function loadExtensionOverCdp(pid) {
332
- const result = spawnSync(process.execPath, [
333
- browserResolverPath,
334
- 'load-unpacked',
335
- '--port', String(cdpPort),
336
- '--path', args['extension-dir'],
337
- '--profile', args.profile,
338
- '--open-url', initialUrl,
339
- ], { encoding: 'utf8', timeout: 130_000 });
340
- if (result.status === 0) {
341
- const loaded = JSON.parse(result.stdout);
342
- process.stderr.write(`[launch] extension ${loaded.id} loaded over CDP\n`);
343
- for (const entry of loaded.otherExtensions?.disabled ?? []) {
344
- process.stderr.write(`[launch] disabled ${entry.id} (${entry.name}) in the slot profile\n`);
345
- }
346
- for (const entry of loaded.otherExtensions?.policyPinned ?? []) {
347
- process.stderr.write(`[launch] WARN: enterprise policy keeps ${entry.id} (${entry.name}) enabled in the slot profile\n`);
348
- }
349
- return loaded;
350
- }
351
- // Ownership was proven; once the browser is stopped the launch markers would
352
- // only quarantine a port and profile that nothing holds. If it cannot be
353
- // stopped, the markers stay and both failures are reported.
354
- const loadError = (result.stderr || result.error?.message || '').trim();
355
- try {
356
- stopProfileProcessesSync(args.profile, { extraPids: [pid] });
357
- } catch (stopError) {
358
- throw new Error(
359
- `Chrome started but the extension did not load over CDP (${loadError}), and stopping it failed: ${stopError.message}. ` +
360
- `The launch markers for port ${cdpPort} and ${args.profile} are kept. Next: stop pid ${pid}, then rerun.`,
361
- );
362
- }
363
- clearDetachedLaunchUnproven(args.profile);
364
- if (!activeValidationLease) clearValidationPortQuarantine(cdpPort, args.profile);
365
- throw new Error(
366
- `Chrome started but the extension did not load over CDP: ${loadError}. ` +
367
- `Next: inspect ${args['chrome-log']}, or rerun with RECIPE_HARNESS_BROWSER=cft to use Chrome for Testing.`,
368
- );
369
- }
370
-
371
- // Identity of the browser that holds the profile (pid + start time), recorded
372
- // so run evidence can later prove it names this exact process.
373
- // Best effort, so a short budget: a browser that never proves ownership (a
374
- // stub without a SingletonLock, say) costs seconds, not the full default.
375
- function ownerIdentity(pid) {
376
- try {
377
- const identity = waitForCdpOwner(cdpPort, args.profile, { timeoutMs: 15000 });
378
- return identity.pid === pid ? identity : null;
379
- } catch (error) {
380
- process.stderr.write(`[launch] could not record the browser identity: ${error.message}\n`);
381
- return null;
382
- }
383
- }
384
-
385
- function sanitizedChildEnv() {
386
- return {
387
- ...process.env,
388
- BUNDLED_DEBUGPY_PATH: undefined,
389
- PYTHONHOME: undefined,
390
- PYTHONPATH: undefined,
391
- DYLD_LIBRARY_PATH: undefined,
392
- DYLD_FALLBACK_LIBRARY_PATH: undefined,
393
- DYLD_INSERT_LIBRARIES: undefined,
394
- };
395
- }
396
-
397
- function beginDetachedLaunch(port, profile, activeLease) {
398
- try {
399
- if (!activeLease) markValidationPortLaunchUnproven(port, profile);
400
- markDetachedLaunchUnproven(profile);
401
- } catch (error) {
402
- if (error.code === 'EEXIST') {
403
- const quarantine = readValidationPortQuarantine(port);
404
- throw validationLaunchQuarantineError(port, quarantine?.profile || profile);
405
- }
406
- throw error;
407
- }
408
- }
409
-
410
- function macApplicationForExecutable(executable) {
411
- let current = path.resolve(executable);
412
- while (current !== path.dirname(current)) {
413
- if (current.endsWith('.app')) return current;
414
- current = path.dirname(current);
415
- }
416
- return null;
417
- }
418
-
419
- function waitForOwnedCdpPid(port, profile, extensionDir) {
420
- for (let i = 0; i < 300; i += 1) {
421
- const pid = cdpListenerPids(port).find((candidate) =>
422
- processIsOwnedHarnessBrowser(candidate, profile, extensionDir),
423
- );
424
- if (pid !== undefined) return pid;
425
- spawnSync('sleep', ['0.1']);
426
- }
427
- return null;
428
- }
34
+ // Checked here so the error echoes the flag as typed.
35
+ if (!Number.isInteger(cdpPort) || cdpPort <= 0) throw new Error(`Invalid --cdp-port: ${args['cdp-port'] || ''}`);
36
+ launchBrowser({
37
+ cdpPort,
38
+ chromeBin: args['chrome-bin'],
39
+ profile: args.profile,
40
+ extensionDir: args['extension-dir'],
41
+ runtimeDir: args['extension-dir'] ? path.dirname(path.resolve(args['extension-dir'])) : '',
42
+ chromeLog: args['chrome-log'],
43
+ chromePid: args['chrome-pid'],
44
+ browserResolution: args['browser-resolution'],
45
+ startUrl: args['start-url'],
46
+ stopOnly: args['stop-only'] !== undefined,
47
+ resetProfile: args['reset-profile'] !== undefined,
48
+ homePage: 'home.html',
49
+ defaultTitle: 'MetaMask',
50
+ extensionOwnerRoot: harnessOwnerRootFromExtensionDir,
51
+ acquireRuntimeLock: acquireBuildHandoff,
52
+ rerunCommand: 'mm-harness launch --adapter extension --build',
53
+ // Unset or invalid falls back to the package default (1000 ms).
54
+ focusSettleMs: Number(process.env.MM_HARNESS_FOCUS_SETTLE_MS),
55
+ });
429
56
 
430
57
  function parseArgs(argv) {
431
58
  const parsed = {};
@@ -439,216 +66,15 @@ function parseArgs(argv) {
439
66
  return parsed;
440
67
  }
441
68
 
442
- function assertIsolatedProfile(profile) {
443
- const resolved = path.resolve(profile);
444
- const home = os.homedir();
445
- const osDefaults = [
446
- path.join(home, 'Library/Application Support/Google/Chrome'),
447
- path.join(home, 'Library/Application Support/Chromium'),
448
- path.join(home, 'Library/Application Support/Microsoft Edge'),
449
- path.join(home, '.config/google-chrome'),
450
- path.join(home, '.config/chromium'),
451
- path.join(home, '.config/microsoft-edge'),
452
- ];
453
- const underDefault = (dir) => resolved === dir || resolved.startsWith(`${dir}${path.sep}`);
454
- if (resolved === home || resolved.includes(`${path.sep}.chrome-farmslot`) || osDefaults.some(underDefault)) {
455
- throw new Error(
456
- `Refusing to launch: --profile ${resolved} is a shared or default browser profile; ` +
457
- `standalone launches must use a per-checkout profile under the harness runtime dir.`,
458
- );
459
- }
460
- }
461
-
462
- function assertResettableProfile(profile, expectedRuntimeDir) {
463
- const resolved = path.resolve(profile);
464
- const runtime = path.resolve(expectedRuntimeDir);
465
- if (!resolved.startsWith(`${runtime}${path.sep}`)) {
466
- throw new Error(`Refusing to reset --profile ${resolved}: it must be a dedicated child of ${runtime}.`);
467
- }
468
- const runtimeStat = fs.lstatSync(runtime);
469
- if (!runtimeStat.isDirectory() || runtimeStat.isSymbolicLink()) {
470
- throw new Error(`Refusing to reset --profile: runtime directory is not a regular directory: ${runtime}.`);
471
- }
472
- let current = runtime;
473
- for (const segment of path.relative(runtime, resolved).split(path.sep)) {
474
- current = path.join(current, segment);
475
- if (!fs.existsSync(current)) break;
476
- const stat = fs.lstatSync(current);
477
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
478
- throw new Error(`Refusing to reset --profile: path contains an unsafe entry: ${current}.`);
479
- }
480
- }
481
- }
482
-
483
- // Proof of ownership: a process is ours iff its command line loads the exact
484
- // --user-data-dir we launch with. If the process can't be inspected we treat it
485
- // as NOT ours (fail safe — never assume ownership of an opaque process).
486
- function processIsOwnedHarnessBrowser(pid, profile, extensionDir) {
487
- const command = processCommand(pid);
488
- if (!command) return false;
489
- if (command.includes(`--user-data-dir=${profile}`)) return true;
490
- const ownerRoot = harnessOwnerRootFromExtensionDir(extensionDir);
491
- return Boolean(ownerRoot && command.includes(`--load-extension=${ownerRoot}`));
492
- }
493
-
69
+ // The overlay's live dist and the runtime snapshot both sit under a harness
70
+ // runtime root; a browser loading an extension from there is ours.
494
71
  function harnessOwnerRootFromExtensionDir(extensionDir) {
495
- const resolved = path.resolve(extensionDir);
496
72
  for (const marker of [
497
73
  `${path.sep}temp${path.sep}recipe${path.sep}harness${path.sep}extension${path.sep}live${path.sep}`,
498
74
  `${path.sep}temp${path.sep}recipe${path.sep}runtime${path.sep}runtime-dist`,
499
75
  ]) {
500
- const markerIndex = resolved.indexOf(marker);
501
- if (markerIndex !== -1) return resolved.slice(0, markerIndex + marker.length);
76
+ const markerIndex = extensionDir.indexOf(marker);
77
+ if (markerIndex !== -1) return extensionDir.slice(0, markerIndex + marker.length);
502
78
  }
503
79
  return null;
504
80
  }
505
-
506
- function processCommand(pid) {
507
- if (!Number.isInteger(pid) || pid <= 0) return null;
508
- try {
509
- return execFileSync('ps', ['-ww', '-o', 'command=', '-p', String(pid)], {
510
- encoding: 'utf8',
511
- stdio: ['ignore', 'pipe', 'ignore'],
512
- });
513
- } catch {
514
- return null;
515
- }
516
- }
517
-
518
- function readPidFile(file) {
519
- if (!fs.existsSync(file)) return null;
520
- const pid = Number.parseInt(fs.readFileSync(file, 'utf8').trim(), 10);
521
- return Number.isInteger(pid) && pid > 0 ? pid : null;
522
- }
523
-
524
- function cdpListenerPids(port) {
525
- try {
526
- return execFileSync('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-t'], {
527
- encoding: 'utf8',
528
- stdio: ['ignore', 'pipe', 'ignore'],
529
- })
530
- .split(/\s+/u)
531
- .map((value) => Number.parseInt(value, 10))
532
- .filter((pid) => Number.isInteger(pid) && pid > 0);
533
- } catch (error) {
534
- if (error.status === 1) return [];
535
- throw error;
536
- }
537
- }
538
-
539
- function terminatePids(pids) {
540
- const unique = [...new Set(pids)].filter((pid) => pid !== process.pid);
541
- for (const pid of unique) signalPid(pid, 'SIGTERM');
542
- for (let i = 0; i < 10 && unique.some(processRespondsToSignal); i += 1) {
543
- spawnSync('sleep', ['0.2']);
544
- }
545
- for (const pid of unique) {
546
- if (processRespondsToSignal(pid)) signalPid(pid, 'SIGKILL');
547
- }
548
- }
549
-
550
- function signalPid(pid, signal) {
551
- try {
552
- process.kill(pid, signal);
553
- return true;
554
- } catch (error) {
555
- if (error.code === 'ESRCH') return false;
556
- throw error;
557
- }
558
- }
559
-
560
- function processRespondsToSignal(pid) {
561
- try {
562
- process.kill(pid, 0);
563
- return true;
564
- } catch (error) {
565
- if (error.code === 'ESRCH') return false;
566
- throw error;
567
- }
568
- }
569
-
570
- function removeProfileSingletonLocks(profile) {
571
- for (const name of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) {
572
- const file = path.join(profile, name);
573
- if (fs.existsSync(file)) fs.rmSync(file, { force: true });
574
- }
575
- }
576
-
577
- function extensionHomeUrl(extensionDir) {
578
- const id = extensionIdFromExtensionDir(extensionDir);
579
- return id ? `chrome-extension://${id}/home.html` : '';
580
- }
581
-
582
- function cdpHttp(port, pathname) {
583
- const result = spawnSync(process.execPath, ['-e', `
584
- const http = require('http');
585
- http.get('http://127.0.0.1:${Number(port)}${pathname}', (res) => {
586
- let body = '';
587
- res.on('data', (chunk) => { body += chunk; });
588
- res.on('end', () => {
589
- process.stdout.write(body);
590
- process.exitCode = res.statusCode === 200 ? 0 : 1;
591
- });
592
- }).on('error', () => process.exit(1));
593
- `], { encoding: 'utf8', timeout: 4000 });
594
- if (result.status !== 0) return null;
595
- return result.stdout;
596
- }
597
-
598
- // Best effort by design: CDP ownership is already proven and ensure-ready
599
- // converges home tabs again during verify, so a pruning failure must not fail
600
- // the launch. Every failure is returned so the launch log says why a duplicate
601
- // tab survived instead of hiding it.
602
- function pruneExtraHomeTabs(port, extensionDir) {
603
- const id = extensionIdFromExtensionDir(extensionDir);
604
- if (!id) return ['extension id unavailable; skipped'];
605
- const raw = cdpHttp(port, '/json/list');
606
- if (raw === null) return [`GET http://127.0.0.1:${port}/json/list failed`];
607
- let targets;
608
- try {
609
- targets = JSON.parse(raw);
610
- } catch (error) {
611
- return [`/json/list returned malformed JSON: ${error.message}`];
612
- }
613
- if (!Array.isArray(targets)) return ['/json/list did not return a target list'];
614
- const problems = [];
615
- const close = (target) => {
616
- if (cdpHttp(port, `/json/close/${target.id}`) === null) problems.push(`could not close ${target.url} (${target.id})`);
617
- };
618
- const homes = targets.filter((target) => (
619
- target &&
620
- target.type === 'page' &&
621
- typeof target.id === 'string' &&
622
- typeof target.url === 'string' &&
623
- target.url.includes(`chrome-extension://${id}/`) &&
624
- target.url.includes('/home.html')
625
- ));
626
- const strays = targets.filter((target) => (
627
- target &&
628
- target.type === 'page' &&
629
- typeof target.id === 'string' &&
630
- typeof target.url === 'string' &&
631
- (
632
- target.url === 'about:blank' ||
633
- target.url === 'chrome://newtab/' ||
634
- target.url.startsWith('chrome://new-tab-page')
635
- )
636
- ));
637
- if (homes.length === 0) return problems;
638
- const keeper = homes.find((target) => (
639
- typeof target.title === 'string' &&
640
- target.title.trim() !== '' &&
641
- target.title.trim() !== 'MetaMask'
642
- )) ?? homes[0];
643
- if (homes.length > 1) {
644
- for (const extra of homes) {
645
- if (keeper && extra.id === keeper.id) continue;
646
- close(extra);
647
- }
648
- }
649
- for (const stray of strays) {
650
- if (keeper && stray.id === keeper.id) continue;
651
- close(stray);
652
- }
653
- return problems;
654
- }