@deeeed/metamask-harness 0.27.0 → 0.29.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.
- package/CHANGELOG.md +26 -0
- package/README.md +41 -0
- package/adapters/extension/build-lavamoat.sh +2 -1
- package/adapters/extension/ensure-browser.sh +82 -9
- package/adapters/extension/inject.mjs +1 -0
- package/adapters/extension/launch-browser.cjs +83 -1
- package/adapters/extension/lib/chrome-args.cjs +325 -1
- package/adapters/extension/lib/playwright-cdp.cjs +34 -0
- package/adapters/extension/lib/slot-title.cjs +2 -4
- package/adapters/extension/lib/validation-launch-supervisor.cjs +292 -0
- package/adapters/extension/lib/validation-process-ownership.cjs +69 -0
- package/adapters/extension/reattach.sh +2 -1
- package/adapters/extension/sidepanel-toggle.sh +14 -96
- package/adapters/extension/wallet-fixture-state.cjs +8 -31
- package/adapters/manifest.json +16 -0
- package/adapters/shared/private-atomic-write.cjs +47 -0
- package/adapters/shared/setup-base.sh +864 -0
- package/dist/adapters/extension/runtime.js +367 -24
- package/dist/adapters/extension/validation-process-ownership.js +10 -0
- package/dist/adapters.js +0 -11
- package/dist/cli-commands.js +2 -1
- package/dist/command-contract.js +13 -0
- package/dist/commands/call.js +38 -1
- package/dist/commands/launch/extension.js +130 -19
- package/dist/commands/manifest.js +201 -6
- package/dist/commands/parse-args.js +1 -0
- package/dist/commands/run.js +74 -8
- package/dist/commands/setup-base.js +24 -0
- package/dist/mm-harness-cli.js +30 -2
- package/dist/recipe-security.js +1 -0
- package/library/actions/extension/analytics/consent.mjs +203 -0
- package/library/actions/extension/analytics/set_consent.mjs +19 -143
- package/library/actions/extension/perps/perps.mjs +2 -16
- package/library/actions/extension/perps/state.mjs +20 -0
- package/library/actions/extension/ui/locators.mjs +13 -0
- package/library/actions/extension/wallet/list_accounts.mjs +3 -25
- package/library/actions/extension/wallet/read_state.mjs +3 -23
- package/library/actions/extension/wallet/select_account.mjs +6 -33
- package/library/actions/extension/wallet/setup.mjs +2 -20
- package/library/actions/extension/wallet/state.mjs +111 -0
- package/library/actions/mobile/platform/observe-ui.mjs +21 -0
- package/library/actions/mobile/ui/locators.mjs +17 -0
- package/library/actions/shared/ui/locators.mjs +160 -0
- package/library/manifests/extension.action-manifest.json +18 -0
- package/library/manifests/mobile.action-manifest.json +18 -0
- package/library/recipes/runner/action-validation.extension.recipe.json +1 -1
- package/library/recipes/runner/action-validation.mobile.recipe.json +1 -1
- package/package.json +7 -4
- package/scripts/site-contrast.mjs +538 -0
- package/site/architecture.html +415 -0
- package/site/assets/progress.mjs +272 -0
- package/site/assets/style.css +808 -0
- package/site/cheatsheet.html +305 -0
- package/site/index.html +643 -0
- package/site/recipes.html +396 -0
- package/site/reviewers.html +374 -0
- package/site/tutorials/index.html +180 -0
- package/site/tutorials/v1.html +211 -0
- package/site/tutorials/v2.html +207 -0
- package/site/tutorials/v3.html +214 -0
- package/site/tutorials/v4.html +195 -0
- package/site/tutorials/v5.html +163 -0
- package/site/tutorials/v6.html +165 -0
- package/site/tutorials/v7.html +184 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.29.0 - 2026-08-01
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Add `mm-harness setup-base` to bootstrap the shared default MetaMask checkout layout with interactive and scripted selection, persisted preferences, disk checks, dependency installation, and machine-readable summaries.
|
|
10
|
+
- Ship a dependency-free getting-started site with an interactive walkthrough, platform-filterable command reference, architecture guide, tutorials, reviewer evidence guide, and automated browser acceptance and contrast checks.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Upgrade the Farmslot runtime dependencies to their structured suite-evidence releases and converge on protocol 0.15.0.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Verify Extension MetaMetrics consent from the production background controller's flat state instead of relying on test-only page state hooks.
|
|
19
|
+
- Resolve Extension Perps network state from both flattened and nested controller stores.
|
|
20
|
+
- Update both supported `brace-expansion` lines to their patched releases for CVE-2026-14257.
|
|
21
|
+
- Build the non-test Extension proof runtime with its webpack sources and supported debug hooks, then read wallet actions from clean app state and wait for account selection to converge.
|
|
22
|
+
- Bind Extension runtime reuse to a per-launch nonce echoed by Chrome, with owner-only state, exact process-argument checks, and machine-global quarantine for unproven detached launches; keep validation launches on the invoking runner with checkout-contained runtime files; route Playwright-owned Extension injections through raw CDP; and require the hardened-page-safe recipe harness for page scrolling.
|
|
23
|
+
|
|
24
|
+
## 0.28.0 - 2026-07-31
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- Add `actions --matrix` and adapter-aware action refusals that name missing capabilities and list the adapters that satisfy them.
|
|
29
|
+
- Add `ui.locators` for ranked, copy-pasteable recipe targets derived from the current Extension or Mobile `ui.visible` observation.
|
|
30
|
+
|
|
5
31
|
## 0.27.0 - 2026-07-31
|
|
6
32
|
|
|
7
33
|
### Changed
|
package/README.md
CHANGED
|
@@ -10,6 +10,47 @@ and runtime paths are detected automatically.
|
|
|
10
10
|
The generic graph engine and evidence schemas live in Farmslot packages.
|
|
11
11
|
`mm-harness` owns MetaMask runtime control and domain capabilities.
|
|
12
12
|
|
|
13
|
+
## Getting started from zero
|
|
14
|
+
|
|
15
|
+
No checkouts yet? `mm-harness setup-base` clones the MetaMask product repos into one
|
|
16
|
+
standard layout and runs each repo's own dependency install — no install of this
|
|
17
|
+
package required to start:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx -p @deeeed/metamask-harness mm-harness setup-base
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
It produces, under `~/dev/metamask` by default:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
metamask-extension-1..N metamask-mobile-1..N core-1..N
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Run without flags in a terminal, it asks which projects you want and how many
|
|
30
|
+
copies of each. Pass flags to skip the prompt entirely — which is how scripts,
|
|
31
|
+
CI, and parent tools should invoke it:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
mm-harness setup-base --only core --counts core=1
|
|
35
|
+
mm-harness setup-base --dir ~/work/metamask --counts extension=3,mobile=1,core=2
|
|
36
|
+
mm-harness setup-base --only mobile --dry-run # plan only, changes nothing
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The base directory resolves by precedence: `--dir`, then `$MM_HARNESS_BASE_DIR`,
|
|
40
|
+
then saved preferences, then `~/dev/metamask`. A successful run saves the base
|
|
41
|
+
directory and counts it used, so later runs adopt them — inspect with
|
|
42
|
+
`--show-config`, clear with `--reset-config`. Re-running is safe: existing
|
|
43
|
+
clones are fetched, never reset and never deleted.
|
|
44
|
+
|
|
45
|
+
**Scope fence.** It clones and installs dependencies. Nothing else — no platform
|
|
46
|
+
toolchains, no simulators, no `.env` files, no builds. Workflow onboarding is a
|
|
47
|
+
prompt; environment readiness is `mm-harness doctor`, which is exactly where it
|
|
48
|
+
points you next.
|
|
49
|
+
|
|
50
|
+
`setup-base` is a normal command on the single `mm-harness` entrypoint. Its work
|
|
51
|
+
stays in one readable shell leaf, and `npx -p` makes that same command available
|
|
52
|
+
before a global install.
|
|
53
|
+
|
|
13
54
|
## Install
|
|
14
55
|
|
|
15
56
|
```bash
|
|
@@ -49,7 +49,8 @@ rm -f "$RUNTIME_DIR/webpack.log" "$RUNTIME_DIR/recipe-harness-webpack.log"
|
|
|
49
49
|
|
|
50
50
|
echo "[recipe-harness] clean non-test LavaMoat build"
|
|
51
51
|
rm -rf node_modules/.cache/webpack
|
|
52
|
-
yarn webpack:
|
|
52
|
+
yarn webpack:tsc
|
|
53
|
+
METAMASK_DEBUG=true yarn webpack:lavamoat:build
|
|
53
54
|
test -f dist/chrome/manifest.json || {
|
|
54
55
|
echo "build-lavamoat: build completed without dist/chrome/manifest.json" >&2
|
|
55
56
|
exit 1
|
|
@@ -200,6 +200,21 @@ kill_profile_processes() {
|
|
|
200
200
|
fi
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
# Invalidate the old browser identity before signaling it. A concurrent quick
|
|
204
|
+
# launch must fall through to process ownership while replacement is underway.
|
|
205
|
+
node - "$SCRIPT_DIR/lib/chrome-args.cjs" "$AGENT_DIR" <<'NODE'
|
|
206
|
+
const [modulePath, runtimeDir] = process.argv.slice(2);
|
|
207
|
+
try {
|
|
208
|
+
require(modulePath).removeRuntimeIdentity(runtimeDir);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
211
|
+
throw new Error(
|
|
212
|
+
'Failed to invalidate Extension runtime identity under ' + runtimeDir + ': ' + detail +
|
|
213
|
+
'. Next: remove the invalid runtime identity and fix owner permissions, then rerun: mm-harness launch --adapter extension --build'
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
NODE
|
|
217
|
+
|
|
203
218
|
# --- Kill existing browser (by PID only — don't touch other slots) ---
|
|
204
219
|
PREV_PID=$(cat "${AGENT_DIR}/browser.pid" 2>/dev/null || true)
|
|
205
220
|
if [ -n "$PREV_PID" ] && kill -0 "$PREV_PID" 2>/dev/null; then
|
|
@@ -275,12 +290,16 @@ exec node -e "
|
|
|
275
290
|
const path = require('path');
|
|
276
291
|
const fs = require('fs');
|
|
277
292
|
const http = require('http');
|
|
278
|
-
const {
|
|
293
|
+
const { execFileSync } = require('child_process');
|
|
279
294
|
let chromium; try { chromium = require('@playwright/test').chromium; } catch { chromium = require('playwright').chromium; }
|
|
280
295
|
const {
|
|
281
296
|
automationRuntimeArgs,
|
|
297
|
+
createRuntimeIdentityNonce,
|
|
282
298
|
isolatedProfileArgs,
|
|
283
299
|
remoteDebuggingArgs,
|
|
300
|
+
removeRuntimeIdentity,
|
|
301
|
+
runtimeIdentityArgs,
|
|
302
|
+
writeRuntimeIdentity,
|
|
284
303
|
} = require(path.join('${SCRIPT_DIR}', 'lib/chrome-args.cjs'));
|
|
285
304
|
const { captureMacFrontmostProcess, restoreMacFrontmostProcess } = require(path.join('${SCRIPT_DIR}', 'lib/macos-focus.cjs'));
|
|
286
305
|
|
|
@@ -304,11 +323,14 @@ const resumeWebpack = () => {
|
|
|
304
323
|
};
|
|
305
324
|
|
|
306
325
|
(async () => {
|
|
326
|
+
const runtimeNonce = createRuntimeIdentityNonce();
|
|
327
|
+
const startedAt = Date.now();
|
|
307
328
|
const args = [
|
|
308
329
|
'--user-data-dir=' + PROFILE,
|
|
309
330
|
'--disable-extensions-except=' + EXTENSION,
|
|
310
331
|
'--load-extension=' + EXTENSION,
|
|
311
332
|
...automationRuntimeArgs(),
|
|
333
|
+
...runtimeIdentityArgs(runtimeNonce),
|
|
312
334
|
'--no-first-run',
|
|
313
335
|
'--no-default-browser-check',
|
|
314
336
|
...isolatedProfileArgs(),
|
|
@@ -340,14 +362,63 @@ const resumeWebpack = () => {
|
|
|
340
362
|
}
|
|
341
363
|
const browser = await chromium.connectOverCDP('http://127.0.0.1:' + CDP_PORT);
|
|
342
364
|
const ctx = browser.contexts()[0];
|
|
365
|
+
let listenerPids;
|
|
343
366
|
try {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
367
|
+
listenerPids = execFileSync(
|
|
368
|
+
'lsof',
|
|
369
|
+
['-nP', '-iTCP:' + CDP_PORT, '-sTCP:LISTEN', '-t'],
|
|
370
|
+
{ encoding: 'utf8', timeout: 10000 },
|
|
371
|
+
).split(/\s+/).filter((value) => /^\d+$/.test(value));
|
|
372
|
+
} catch {
|
|
373
|
+
throw new Error(
|
|
374
|
+
'Reopened Chrome ownership could not be inspected with lsof. ' +
|
|
375
|
+
'Next: install or restore lsof, then rerun: mm-harness launch --adapter extension --build'
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
let browserPid = '';
|
|
379
|
+
for (const pid of listenerPids) {
|
|
380
|
+
try {
|
|
381
|
+
const command = execFileSync(
|
|
382
|
+
'ps',
|
|
383
|
+
['-ww', '-o', 'command=', '-p', pid],
|
|
384
|
+
{ encoding: 'utf8', timeout: 2000 },
|
|
385
|
+
);
|
|
386
|
+
if (
|
|
387
|
+
command.includes('--user-data-dir=' + PROFILE) &&
|
|
388
|
+
(
|
|
389
|
+
command.includes('--load-extension=' + EXTENSION) ||
|
|
390
|
+
command.includes('--disable-extensions-except=' + EXTENSION)
|
|
391
|
+
)
|
|
392
|
+
) {
|
|
393
|
+
browserPid = pid;
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
} catch {}
|
|
397
|
+
}
|
|
398
|
+
if (!browserPid) {
|
|
399
|
+
throw new Error(
|
|
400
|
+
'Reopened Chrome did not expose an owned CDP listener on port ' + CDP_PORT +
|
|
401
|
+
'. Next: inspect the slot browser process, then rerun: mm-harness launch --adapter extension --build'
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
fs.writeFileSync(path.join(AGENT_DIR, 'browser.pid'), browserPid);
|
|
405
|
+
fs.writeFileSync(path.join(AGENT_DIR, 'chromium.pid'), browserPid);
|
|
406
|
+
try {
|
|
407
|
+
writeRuntimeIdentity(AGENT_DIR, {
|
|
408
|
+
port: Number(CDP_PORT),
|
|
409
|
+
pid: Number(browserPid),
|
|
410
|
+
startedAt,
|
|
411
|
+
nonce: runtimeNonce,
|
|
412
|
+
});
|
|
413
|
+
} catch (error) {
|
|
414
|
+
try { process.kill(Number(browserPid), 'SIGTERM'); } catch {}
|
|
415
|
+
const detail = error && error.message ? error.message : String(error);
|
|
416
|
+
throw new Error(
|
|
417
|
+
'Failed to persist Extension runtime identity under ' + AGENT_DIR + ': ' + detail +
|
|
418
|
+
'. Next: fix owner write permissions for the runtime directory, then rerun: mm-harness launch --adapter extension --build'
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
console.log('[reopen] Chromium PID ' + browserPid);
|
|
351
422
|
|
|
352
423
|
// Detect extension ID via the recipe-runner — the single source of truth.
|
|
353
424
|
// Deterministic id from the loaded dist's manifest key; replaces a fragile
|
|
@@ -371,7 +442,8 @@ const resumeWebpack = () => {
|
|
|
371
442
|
|
|
372
443
|
// Set window title to slot ID (persists across SPA navigations)
|
|
373
444
|
const { applyPersistentSlotTitle } = require(path.join('${SCRIPT_DIR}', 'lib/slot-title.cjs'));
|
|
374
|
-
|
|
445
|
+
const { evaluatePageViaCdp } = require(path.join('${SCRIPT_DIR}', 'lib/playwright-cdp.cjs'));
|
|
446
|
+
await evaluatePageViaCdp(page, applyPersistentSlotTitle, SLOT_ID);
|
|
375
447
|
|
|
376
448
|
// Wait for runtime state to settle, then unlock if needed.
|
|
377
449
|
const unlockSelector = '[data-testid=\"unlock-password\"]';
|
|
@@ -438,6 +510,7 @@ const resumeWebpack = () => {
|
|
|
438
510
|
process.exit(0);
|
|
439
511
|
})().catch(e => {
|
|
440
512
|
resumeWebpack();
|
|
513
|
+
try { removeRuntimeIdentity(AGENT_DIR); } catch {}
|
|
441
514
|
try { fs.unlinkSync(path.join(AGENT_DIR, 'browser.pid')); } catch {}
|
|
442
515
|
try { fs.unlinkSync(path.join(AGENT_DIR, 'chromium.pid')); } catch {}
|
|
443
516
|
try { fs.unlinkSync(path.join(AGENT_DIR, 'extension.id')); } catch {}
|
|
@@ -126,6 +126,7 @@ copyFile(path.join(runnerDir, 'adapters/shared/tmux-session.sh'), path.join(harn
|
|
|
126
126
|
copyFile(path.join(runnerDir, 'adapters/shared/tmux-viewer.sh'), path.join(harnessDir, 'scripts/lib/tmux-viewer.sh'));
|
|
127
127
|
copyFile(path.join(runnerDir, 'adapters/shared/log-tui.mjs'), path.join(harnessDir, 'scripts/lib/log-tui.mjs'));
|
|
128
128
|
copyFile(path.join(runnerDir, 'adapters/extension/lib/extension-id.cjs'), path.join(harnessDir, 'scripts/lib/extension-id.cjs'));
|
|
129
|
+
copyFile(path.join(runnerDir, 'adapters/extension/lib/playwright-cdp.cjs'), path.join(harnessDir, 'scripts/lib/playwright-cdp.cjs'));
|
|
129
130
|
copyFile(path.join(runnerDir, 'adapters/extension/lib/slot-title.cjs'), path.join(harnessDir, 'scripts/lib/slot-title.cjs'));
|
|
130
131
|
copyFile(path.join(runnerDir, 'adapters/extension/lib/chrome-args.cjs'), path.join(harnessDir, 'scripts/lib/chrome-args.cjs'));
|
|
131
132
|
copyFile(path.join(runnerDir, 'adapters/extension/lib/macos-focus.cjs'), path.join(harnessDir, 'scripts/lib/macos-focus.cjs'));
|
|
@@ -26,8 +26,19 @@ const { execFileSync, spawn, spawnSync } = require('node:child_process');
|
|
|
26
26
|
const { extensionIdFromExtensionDir } = require('./lib/extension-id.cjs');
|
|
27
27
|
const {
|
|
28
28
|
automationRuntimeArgs,
|
|
29
|
+
clearDetachedLaunchUnproven,
|
|
30
|
+
clearValidationPortQuarantine,
|
|
31
|
+
createRuntimeIdentityNonce,
|
|
32
|
+
hasDetachedLaunchUnproven,
|
|
29
33
|
isolatedProfileArgs,
|
|
34
|
+
markDetachedLaunchUnproven,
|
|
35
|
+
markValidationPortLaunchUnproven,
|
|
30
36
|
remoteDebuggingArgs,
|
|
37
|
+
removeRuntimeIdentity,
|
|
38
|
+
runtimeIdentityArgs,
|
|
39
|
+
readValidationPortQuarantine,
|
|
40
|
+
validationLaunchQuarantineError,
|
|
41
|
+
writeRuntimeIdentity,
|
|
31
42
|
} = require('./lib/chrome-args.cjs');
|
|
32
43
|
const { captureMacFrontmostProcess, restoreMacFrontmostProcess } = require('./lib/macos-focus.cjs');
|
|
33
44
|
|
|
@@ -46,6 +57,7 @@ if (!Number.isInteger(cdpPort) || cdpPort <= 0) {
|
|
|
46
57
|
for (const key of ['chrome-bin', 'profile', 'extension-dir', 'chrome-log', 'chrome-pid']) {
|
|
47
58
|
if (!args[key]) throw new Error(`Missing --${key}`);
|
|
48
59
|
}
|
|
60
|
+
const runtimeDir = path.dirname(path.resolve(args['extension-dir']));
|
|
49
61
|
// --stop-only releases the profile before the dist snapshot exists on a first
|
|
50
62
|
// run, so launch-input validation applies only to a real launch.
|
|
51
63
|
if (args['stop-only'] === undefined) {
|
|
@@ -80,6 +92,15 @@ if (foreignPids.length > 0) {
|
|
|
80
92
|
const ownedPids = new Set(listenerPids);
|
|
81
93
|
const previousPid = readPidFile(args['chrome-pid']);
|
|
82
94
|
if (previousPid !== null && processIsOwnedHarnessBrowser(previousPid, args.profile, args['extension-dir'])) ownedPids.add(previousPid);
|
|
95
|
+
try {
|
|
96
|
+
removeRuntimeIdentity(runtimeDir);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Failed to invalidate Extension runtime identity under ${runtimeDir}: ${detail}. ` +
|
|
101
|
+
'Next: remove the invalid runtime identity and fix owner permissions, then rerun: mm-harness launch --adapter extension --build',
|
|
102
|
+
);
|
|
103
|
+
}
|
|
83
104
|
terminatePids([...ownedPids]);
|
|
84
105
|
if (args['reset-profile'] !== undefined) {
|
|
85
106
|
fs.rmSync(args.profile, { recursive: true, force: true });
|
|
@@ -95,7 +116,25 @@ if (args['stop-only'] !== undefined) {
|
|
|
95
116
|
process.exit(0);
|
|
96
117
|
}
|
|
97
118
|
|
|
119
|
+
const requestedProfileQuarantined = hasDetachedLaunchUnproven(args.profile);
|
|
120
|
+
const portQuarantine = readValidationPortQuarantine(cdpPort);
|
|
121
|
+
const validationPortLease = process.env.MM_HARNESS_VALIDATION_PORT_LEASE;
|
|
122
|
+
const activeValidationLease = Boolean(
|
|
123
|
+
portQuarantine?.profile === path.resolve(args.profile) &&
|
|
124
|
+
portQuarantine.lease === validationPortLease &&
|
|
125
|
+
/^[a-f0-9]{64}$/u.test(validationPortLease || ''),
|
|
126
|
+
);
|
|
127
|
+
if (requestedProfileQuarantined || (portQuarantine && !activeValidationLease)) {
|
|
128
|
+
throw validationLaunchQuarantineError(
|
|
129
|
+
cdpPort,
|
|
130
|
+
portQuarantine?.profile || (requestedProfileQuarantined ? args.profile : undefined),
|
|
131
|
+
requestedProfileQuarantined ? args.profile : undefined,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
98
135
|
const initialUrl = args['start-url'] || extensionHomeUrl(args['extension-dir']) || 'chrome://extensions/';
|
|
136
|
+
const runtimeNonce = createRuntimeIdentityNonce();
|
|
137
|
+
const startedAt = Date.now();
|
|
99
138
|
const chromeArgs = [
|
|
100
139
|
`--user-data-dir=${args.profile}`,
|
|
101
140
|
// Debug-port trio (address + port + scoped allow-origins) from the shared module,
|
|
@@ -104,6 +143,7 @@ const chromeArgs = [
|
|
|
104
143
|
'--no-first-run',
|
|
105
144
|
'--disable-first-run-ui',
|
|
106
145
|
...automationRuntimeArgs(),
|
|
146
|
+
...runtimeIdentityArgs(runtimeNonce),
|
|
107
147
|
...isolatedProfileArgs(),
|
|
108
148
|
'--disable-default-apps',
|
|
109
149
|
'--disable-popup-blocking',
|
|
@@ -123,6 +163,7 @@ let browserPid;
|
|
|
123
163
|
try {
|
|
124
164
|
const application = process.platform === 'darwin' ? macApplicationForExecutable(args['chrome-bin']) : null;
|
|
125
165
|
if (application) {
|
|
166
|
+
beginDetachedLaunch(cdpPort, args.profile, activeValidationLease);
|
|
126
167
|
execFileSync('open', ['-g', '-n', '-a', application, '--args', ...chromeArgs], {
|
|
127
168
|
env: sanitizedChildEnv(),
|
|
128
169
|
stdio: ['ignore', logFd, logFd],
|
|
@@ -132,18 +173,46 @@ try {
|
|
|
132
173
|
throw new Error(`Chrome launched but did not expose CDP on 127.0.0.1:${cdpPort}`);
|
|
133
174
|
}
|
|
134
175
|
} else {
|
|
176
|
+
beginDetachedLaunch(cdpPort, args.profile, activeValidationLease);
|
|
135
177
|
const child = spawn(args['chrome-bin'], chromeArgs, {
|
|
136
178
|
detached: true,
|
|
137
179
|
env: sanitizedChildEnv(),
|
|
138
180
|
stdio: ['ignore', logFd, logFd],
|
|
139
181
|
});
|
|
140
182
|
child.unref();
|
|
141
|
-
browserPid =
|
|
183
|
+
browserPid = waitForOwnedCdpPid(cdpPort, args.profile, args['extension-dir']);
|
|
184
|
+
if (browserPid === null) {
|
|
185
|
+
if (child.pid) terminatePids([child.pid]);
|
|
186
|
+
throw new Error(
|
|
187
|
+
`Chrome launched but did not expose an owned CDP listener on 127.0.0.1:${cdpPort}. ` +
|
|
188
|
+
`Next: inspect ${args['chrome-log']}, then rerun: mm-harness launch --adapter extension --build`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
142
191
|
}
|
|
143
192
|
} finally {
|
|
144
193
|
fs.closeSync(logFd);
|
|
145
194
|
}
|
|
146
195
|
fs.writeFileSync(args['chrome-pid'], `${browserPid}\n`);
|
|
196
|
+
try {
|
|
197
|
+
writeRuntimeIdentity(runtimeDir, {
|
|
198
|
+
port: cdpPort,
|
|
199
|
+
pid: browserPid,
|
|
200
|
+
startedAt,
|
|
201
|
+
nonce: runtimeNonce,
|
|
202
|
+
});
|
|
203
|
+
clearDetachedLaunchUnproven(args.profile);
|
|
204
|
+
if (!activeValidationLease) {
|
|
205
|
+
clearValidationPortQuarantine(cdpPort, args.profile);
|
|
206
|
+
}
|
|
207
|
+
} catch (error) {
|
|
208
|
+
terminatePids([browserPid]);
|
|
209
|
+
fs.rmSync(args['chrome-pid'], { force: true });
|
|
210
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
211
|
+
throw new Error(
|
|
212
|
+
`Failed to persist Extension runtime identity under ${runtimeDir}: ${detail}. ` +
|
|
213
|
+
'Next: fix owner write permissions for the runtime directory, then rerun: mm-harness launch --adapter extension --build',
|
|
214
|
+
);
|
|
215
|
+
}
|
|
147
216
|
if (process.platform === 'darwin') {
|
|
148
217
|
const wake = spawn('caffeinate', ['-dimsu', '-w', String(browserPid)], {
|
|
149
218
|
detached: true,
|
|
@@ -167,6 +236,19 @@ function sanitizedChildEnv() {
|
|
|
167
236
|
};
|
|
168
237
|
}
|
|
169
238
|
|
|
239
|
+
function beginDetachedLaunch(port, profile, activeLease) {
|
|
240
|
+
try {
|
|
241
|
+
if (!activeLease) markValidationPortLaunchUnproven(port, profile);
|
|
242
|
+
markDetachedLaunchUnproven(profile);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
if (error.code === 'EEXIST') {
|
|
245
|
+
const quarantine = readValidationPortQuarantine(port);
|
|
246
|
+
throw validationLaunchQuarantineError(port, quarantine?.profile || profile);
|
|
247
|
+
}
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
170
252
|
function macApplicationForExecutable(executable) {
|
|
171
253
|
let current = path.resolve(executable);
|
|
172
254
|
while (current !== path.dirname(current)) {
|