@deeeed/metamask-harness 0.17.5 → 0.19.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 (169) hide show
  1. package/CHANGELOG.md +1059 -0
  2. package/README.md +91 -140
  3. package/adapters/core/inject.sh +1 -6
  4. package/adapters/extension/console-tail.mjs +55 -24
  5. package/adapters/extension/ensure-browser.sh +6 -1
  6. package/adapters/extension/inject.mjs +7 -13
  7. package/adapters/extension/launch-browser.cjs +7 -1
  8. package/adapters/extension/launch-webpack.cjs +83 -0
  9. package/adapters/extension/launch.sh +8 -11
  10. package/adapters/extension/lib/chrome-args.cjs +8 -1
  11. package/adapters/extension/lib/macos-focus.cjs +32 -0
  12. package/adapters/extension/live.sh +12 -21
  13. package/adapters/extension/reattach.sh +1 -0
  14. package/adapters/extension/seed-fixture.sh +4 -12
  15. package/adapters/extension/sidepanel-toggle.sh +4 -1
  16. package/adapters/extension/stamp-runtime-title.cjs +58 -0
  17. package/adapters/extension/start-watch.sh +17 -4
  18. package/adapters/extension/stop-viewers.sh +1 -1
  19. package/adapters/extension/sync-webpack-dist.cjs +107 -0
  20. package/adapters/extension/verify.sh +2 -2
  21. package/adapters/manifest.json +53 -37
  22. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
  23. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
  24. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
  25. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
  26. package/adapters/mobile/cleanup.sh +16 -176
  27. package/adapters/mobile/inject.sh +91 -671
  28. package/adapters/mobile/launch-console-forwarder.cjs +39 -0
  29. package/adapters/mobile/open-device.sh +45 -7
  30. package/adapters/mobile/start-console-forwarder.sh +70 -0
  31. package/adapters/mobile/start-metro.sh +0 -41
  32. package/adapters/mobile/verify.sh +34 -83
  33. package/adapters/mobile/wait-for-bridge.sh +22 -8
  34. package/adapters/shared/harness-source-fingerprint.mjs +14 -12
  35. package/adapters/shared/resolve-slot-ports-core.mjs +17 -6
  36. package/adapters/shared/resolve-slot-ports.sh +17 -5
  37. package/adapters/shared/update-check-worker.mjs +43 -0
  38. package/bin/mm-harness +21 -2
  39. package/dist/adapters/core/surface.js +4 -1
  40. package/dist/adapters/extension/console-capture.js +95 -0
  41. package/dist/adapters/extension/product-config.js +110 -0
  42. package/dist/adapters/extension/runtime-decision.js +20 -2
  43. package/dist/adapters/extension/surface.js +22 -3
  44. package/dist/adapters/harness-freshness.js +49 -0
  45. package/dist/adapters/mobile/perps-env.js +43 -3
  46. package/dist/adapters/mobile/prepare.js +39 -7
  47. package/dist/adapters/mobile/surface.js +5 -2
  48. package/dist/adapters/resolve-slot-ports.js +2 -2
  49. package/dist/adapters/slot-ports.js +13 -16
  50. package/dist/adapters.js +56 -16
  51. package/dist/checkout-lock.js +27 -2
  52. package/dist/cli-color.js +19 -0
  53. package/dist/cli-commands.js +1 -1
  54. package/dist/cli.js +7 -18
  55. package/dist/command-contract.js +462 -0
  56. package/dist/command-journal.js +263 -0
  57. package/dist/commands/call.js +252 -57
  58. package/dist/commands/check.js +10 -4
  59. package/dist/commands/completion-candidates.js +17 -22
  60. package/dist/commands/device-target.js +27 -12
  61. package/dist/commands/doctor.js +106 -25
  62. package/dist/commands/fixtures.js +142 -35
  63. package/dist/commands/last.js +60 -0
  64. package/dist/commands/launch/extension.js +40 -17
  65. package/dist/commands/launch/index.js +207 -63
  66. package/dist/commands/launch/mobile.js +2 -0
  67. package/dist/commands/list-executables.js +187 -29
  68. package/dist/commands/logs.js +8 -6
  69. package/dist/commands/manifest.js +286 -35
  70. package/dist/commands/parse-args.js +68 -4
  71. package/dist/commands/provision.js +10 -4
  72. package/dist/commands/run-engine.js +772 -264
  73. package/dist/commands/run-report.js +12 -3
  74. package/dist/commands/run.js +378 -85
  75. package/dist/commands/shared.js +76 -4
  76. package/dist/commands/status-probe.js +4 -1
  77. package/dist/commands/status.js +2 -1
  78. package/dist/commands/stop.js +7 -2
  79. package/dist/commands/update.js +16 -28
  80. package/dist/completions-cache.js +1 -1
  81. package/dist/doctor.js +57 -10
  82. package/dist/harness.js +47 -137
  83. package/dist/heal-bounds.js +7 -2
  84. package/dist/json-stream.js +57 -0
  85. package/dist/live-adapter-contract.js +138 -32
  86. package/dist/manifest.js +161 -1
  87. package/dist/mm-harness-cli.js +146 -73
  88. package/dist/paths.js +2 -5
  89. package/dist/recipe-security.js +178 -0
  90. package/dist/run-diagnostics.js +261 -0
  91. package/dist/run-recording.js +1 -1
  92. package/dist/runner.js +140 -9
  93. package/docs/CONTRIBUTING.md +136 -0
  94. package/docs/QA.md +184 -0
  95. package/docs/RECIPES.md +122 -0
  96. package/docs/SECURITY.md +88 -0
  97. package/library/README.md +7 -3
  98. package/library/actions/core/perps/_controller.mjs +43 -56
  99. package/library/actions/core/perps/assert_orders.mjs +6 -7
  100. package/library/actions/core/perps/assert_positions.mjs +6 -7
  101. package/library/actions/core/perps/close_orders.mjs +2 -0
  102. package/library/actions/core/perps/close_positions.mjs +2 -0
  103. package/library/actions/core/perps/ensure_orders.mjs +4 -2
  104. package/library/actions/core/perps/ensure_positions.mjs +4 -2
  105. package/library/actions/core/perps/place_order.mjs +7 -3
  106. package/library/actions/core/perps/read_account.mjs +2 -2
  107. package/library/actions/core/perps/read_orders.mjs +2 -1
  108. package/library/actions/core/perps/read_positions.mjs +2 -1
  109. package/library/actions/core/wallet/list_accounts.mjs +95 -0
  110. package/library/actions/extension/perps/assert_orders.mjs +2 -1
  111. package/library/actions/extension/perps/assert_positions.mjs +2 -1
  112. package/library/actions/extension/perps/perps.mjs +43 -14
  113. package/library/actions/extension/platform/cdp.mjs +1 -0
  114. package/library/actions/extension/wallet/list_accounts.mjs +41 -0
  115. package/library/actions/mobile/perps/assert_orders.mjs +2 -1
  116. package/library/actions/mobile/perps/assert_positions.mjs +2 -1
  117. package/library/actions/mobile/perps/perps.mjs +40 -12
  118. package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
  119. package/library/library.json +1 -1
  120. package/library/manifests/core.action-manifest.json +1214 -396
  121. package/library/manifests/extension.action-manifest.json +1635 -728
  122. package/library/manifests/mobile.action-manifest.json +1740 -727
  123. package/library/recipes/app/lifecycle.android-smoke.mobile.recipe.json +63 -81
  124. package/library/recipes/perps/clean-market-testnet.core.recipe.json +44 -0
  125. package/library/recipes/perps/clean-market-testnet.recipe.json +49 -0
  126. package/library/recipes/perps/lifecycle.recipe.json +136 -180
  127. package/library/recipes/perps/order-lifecycle.core.recipe.json +71 -67
  128. package/library/recipes/perps/performance.background-resume.mobile.recipe.json +51 -67
  129. package/library/recipes/perps/performance.cold-start.mobile.recipe.json +51 -67
  130. package/library/recipes/perps/performance.mobile.recipe.json +37 -51
  131. package/library/recipes/perps/performance.warm-start.mobile.recipe.json +44 -59
  132. package/library/recipes/perps/read-markets.core.recipe.json +29 -31
  133. package/library/recipes/perps/smoke.core.recipe.json +29 -32
  134. package/library/recipes/perps/smoke.extension.recipe.json +41 -44
  135. package/library/recipes/perps/smoke.mobile.recipe.json +42 -44
  136. package/library/recipes/perps/trading-lifecycle.core.recipe.json +69 -65
  137. package/library/recipes/runner/action-validation.extension.recipe.json +312 -398
  138. package/library/recipes/runner/action-validation.mobile.recipe.json +316 -402
  139. package/library/recipes/runner/smoke.core.recipe.json +25 -0
  140. package/library/recipes/runner/smoke.extension.recipe.json +23 -24
  141. package/library/recipes/runner/smoke.mobile.recipe.json +23 -24
  142. package/library/recipes/wallet/smoke.extension.recipe.json +40 -0
  143. package/library/recipes/wallet/smoke.mobile.recipe.json +40 -0
  144. package/package.json +12 -5
  145. package/scripts/completions.sh +8 -11
  146. package/scripts/validate-human-outcomes.mjs +169 -0
  147. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
  148. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
  149. package/adapters/mobile/overlay/compat/README.md +0 -28
  150. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
  151. package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
  152. package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
  153. package/adapters/shared/resolve-farmslot-ports.sh +0 -7
  154. package/dist/adapters/extension/harness-freshness.js +0 -39
  155. package/dist/commands/flows.js +0 -62
  156. package/docs/ADAPTER-SURFACE.md +0 -119
  157. package/docs/CHEATSHEET.md +0 -61
  158. package/docs/CLI-SPEC.md +0 -1098
  159. package/docs/CODE-MAP.md +0 -62
  160. package/docs/DEBUG-HANDOVER.md +0 -36
  161. package/docs/MENTAL-MODEL.md +0 -295
  162. package/docs/UX-PRINCIPLES.md +0 -64
  163. package/docs/architecture.md +0 -398
  164. package/docs/live-adapter-contract.md +0 -188
  165. package/docs/package-boundaries.md +0 -47
  166. package/docs/perps-flow-catalog.md +0 -235
  167. package/docs/recipe-libraries.md +0 -301
  168. package/docs/runtime-file-conventions.md +0 -36
  169. package/library/flows/perps.flows.json +0 -64
package/README.md CHANGED
@@ -1,177 +1,128 @@
1
- # MetaMask Recipe Runner — `mm-harness`
1
+ # mm-harness
2
2
 
3
- Architecture: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
3
+ One CLI for operating MetaMask Extension, Mobile, and Core and producing
4
+ reviewable recipe evidence. Run it inside a checkout; the product, slot, ports,
5
+ and runtime paths are detected automatically.
4
6
 
5
- **`mm-harness` is built for the agent but easy for the human to use and
6
- understand.** It is the MetaMask recipe harness: one front door to launch the app,
7
- prove behavior with recipes, and manage the per-checkout runtime overlay. It is a
8
- thin MetaMask adapter over the generic `@farmslot/recipe-harness` engine — the
9
- runner never re-implements graph execution, it binds to it. The agent gets full
10
- depth (explicit flags, `--json`, ports, build tiers); the human gets short
11
- commands, positional targets, good defaults, completions, and teaching
12
- help/errors. Run it from inside a MetaMask Mobile/Extension/Core checkout and the
13
- platform is auto-detected.
7
+ - **Action:** one typed operation.
8
+ - **Recipe:** a reusable, parameterized graph of actions and called recipes.
14
9
 
15
- ```
16
- DAILY LOOP launch · logs · debug · fixtures what a teammate runs many times a day
17
- DISCOVER actions · call · flows compose recipes from the vocabulary + flow library
18
- PROVE run · doctor run recipes, check readiness
19
- RUNTIME OVERLAY install · verify · cleanup manage the per-checkout runtime overlay
20
- ```
21
-
22
- **One bin, no per-platform binaries.** `mm-harness` is the only command. Grammar:
23
- `mm-harness <command> [target] [flags]` — the human types the bare command
24
- (platform auto-detected) or a positional target (`mm-harness launch ios`); depth
25
- lives in FLAGS with good defaults, never in extra command names. It ships shell
26
- completions (zsh + bash) for commands, targets, and flags.
27
-
28
- Naming: **"harness" is the tool** (`mm-harness`) and the generic engine package
29
- (`@farmslot/recipe-harness`); the files it installs into a checkout are the
30
- **runtime overlay**. Read [MENTAL-MODEL.md](docs/MENTAL-MODEL.md) first (the
31
- before→after map), then [CLI-SPEC.md](docs/CLI-SPEC.md) for the full contract.
10
+ The generic graph engine and evidence schemas live in Farmslot packages.
11
+ `mm-harness` owns MetaMask runtime control and domain capabilities.
32
12
 
33
- One package, two responsibilities:
13
+ ## Install
34
14
 
35
- - **Recipe layer:** manifests, recipes, Mobile/Extension adapters, proof output.
36
- - **Runtime layer:** start/reuse Metro or Chrome, seed fixtures, wait for readiness.
37
-
38
- Farmslot and skills are wrappers around this CLI; they should not copy runner
39
- logic.
40
-
41
- ## Quick start
15
+ ```bash
16
+ npm install -g @deeeed/metamask-harness@latest
17
+ mm-harness --version
18
+ mm-harness doctor
19
+ ```
42
20
 
43
- Run from inside a MetaMask checkout (platform auto-detected):
21
+ `doctor` is read-only. `doctor --fix` repairs harness-owned runtime state but
22
+ does not launch an app, invent credentials, or choose a wallet fixture.
44
23
 
45
24
  ```bash
46
- # 1) Launch the app (installs the runtime overlay on first use, boots Metro/build)
47
- mm-harness launch ios # mobile: ios | android; extension: just `launch`
48
-
49
- # 2) Run a recipe and write evidence
50
- mm-harness run recipe.json --adapter mobile --artifacts-dir /tmp/recipe-artifacts --json
25
+ mm-harness doctor --fix
26
+ mm-harness fixtures init --from /secure/path/wallet-fixture.json
27
+ # Disposable public testing only; never fund this wallet:
28
+ mm-harness fixtures init --dev
51
29
  ```
52
30
 
53
- Outputs: `summary.json`, `trace.json`, screenshots, logs, and an artifact
54
- manifest.
31
+ Use `mm-harness update` to update a published installation.
55
32
 
56
- ## Mental model
33
+ ## Operate
57
34
 
58
- ```text
59
- mm-harness launch # runtime/orchestration: the app is ready
60
- mm-harness run # runner/proof: actions execute and evidence is saved
35
+ ```bash
36
+ # Extension
37
+ mm-harness launch
38
+ mm-harness launch --sidepanel
39
+
40
+ # Mobile
41
+ mm-harness launch ios
42
+ mm-harness launch android
43
+
44
+ # Any checkout
45
+ mm-harness status
46
+ mm-harness stop
47
+ mm-harness logs
48
+ mm-harness debug
49
+ mm-harness fixtures set
61
50
  ```
62
51
 
63
- Do not mix those layers. library/ = what recipes ARE (recipes, actions,
64
- manifests the content). src/ = what EXECUTES them (TypeScript CLI/engine glue,
65
- typed adapters). adapters/ = what CONTROLS the app and its instances (start,
66
- windows, wallet state, health, ports, parallel).
52
+ Extension `launch` keeps its incremental watcher running. Refresh the active
53
+ page after a successful rebuild; use `launch --build` only for a clean rebuild.
67
54
 
68
- ## Useful commands
55
+ Log sources stay separate:
69
56
 
70
57
  ```bash
71
- # Capabilities (compose recipes from the vocabulary + flow library)
72
- mm-harness actions --adapter mobile --json # the action vocabulary
73
- mm-harness actions --adapter mobile --raw # raw action-manifest dump
74
- mm-harness call unlock --adapter extension # run one action via the real engine path
75
- mm-harness flows --json # reusable library flows, with provenance
76
-
77
- # Readiness + proof
78
- mm-harness doctor # read-only readiness check (no launch)
79
- mm-harness doctor --fix # heal the overlay without launching
80
- mm-harness run recipe.json --plan --adapter mobile # validate + plan, touch nothing
81
-
82
- # Daily loop (run from inside a checkout)
83
- mm-harness launch ios # launch (mobile: ios | android)
84
- mm-harness logs # tail Metro/webpack + app logs
85
- mm-harness debug # open the debug console
86
- mm-harness fixtures sync # sync fixtures; `fixtures set` seeds the wallet
87
-
88
- # Keep the install current
89
- mm-harness update # upgrade to the published latest (prints old → new)
90
- mm-harness update --check --json # report only — { current, latest, updateAvailable }
58
+ mm-harness logs --source extension # Extension page
59
+ mm-harness logs --source dapp # active dapp
60
+ mm-harness logs --source webpack # compiler
61
+ mm-harness logs --source watcher # watcher lifecycle
62
+ mm-harness logs --source rebuild # incremental rebuilds
63
+ mm-harness logs --source app # Mobile app
64
+ mm-harness logs --source metro # Mobile bundler
91
65
  ```
92
66
 
93
- ### Human validation rule
94
-
95
- An exit code or `--json` result is not sufficient for a UI/debug command. After
96
- `launch --verify`, confirm the debugger is visibly connected to the current
97
- slot's app/profile. Repeat this check on every slot/platform that will be used
98
- before release.
99
-
100
- A passive once-a-day nudge prints one `stderr` line when a newer version is
101
- published (`mm-harness <cur> → <latest> available · run: mm-harness update`). It
102
- never blocks and never auto-updates. Silence it with `MM_HARNESS_NO_UPDATE_CHECK=1`
103
- (auto-off in CI).
104
-
105
- Set `RECIPE_LOG_UI=compact|full|quiet` (default `compact`) and `RECIPE_LOG_EVENTS=10`
106
- to tune the compact log view during launch and Metro/webpack startup.
107
-
108
- ## Layout
109
-
110
- ```text
111
- bin/ the mm-harness entrypoint
112
- src/ what EXECUTES recipes: TypeScript CLI/engine glue +
113
- typed per-adapter logic (src/adapters/)
114
- adapters/ what CONTROLS the app and its instances: launch/live/
115
- watch/windows/wallet state/health/inject/cleanup per
116
- platform, shared glue, porcelain, overlay payload +
117
- manifest.json
118
- library/ what recipes ARE: recipes/ (recipe JSONs), actions/
119
- (per-platform implementations), manifests/ (capability)
120
- scripts/ dev tooling (yarn check, local farmslot link, e2e
121
- validation, adapter-surface-doctor)
122
- docs/ details when this README is not enough
123
- ```
67
+ ## Discover and prove
124
68
 
125
- Defaults for installed harness/runtime paths live in
126
- `adapters/shared/path-defaults.json`.
69
+ ```bash
70
+ mm-harness actions positions
71
+ mm-harness actions --action metamask.wallet.ensure_unlocked
72
+ mm-harness call metamask.wallet.ensure_unlocked
73
+
74
+ mm-harness run --list
75
+ mm-harness run perps.clean-market-testnet --describe
76
+ mm-harness run wallet.smoke --describe
77
+ mm-harness run path/to/recipe.json market=ETH --plan
78
+ mm-harness run path/to/recipe.json
79
+ ```
127
80
 
128
- ## Runtime-readiness ownership
81
+ `run` selects a checkout-local artifact directory unless
82
+ `--artifacts-dir <dir>` overrides it. Human output prints diagnostics and
83
+ absolute paths to the report, trace, executed recipe, and artifact manifest.
84
+ Use `mm-harness last --json` to resume without repeating the last operation.
129
85
 
130
- Generic readiness mechanics live in `@farmslot/recipe-harness` (0.3+):
86
+ For automation, `--json` emits one stable document. `run --json-stream` emits
87
+ line-buffered JSONL progress and a terminal event.
131
88
 
132
- - `@farmslot/recipe-harness/runtime/deps-readiness` — install fingerprint + baseline
133
- - `@farmslot/recipe-harness/runtime/log-analysis` — bundle-log boundaries, unresolved-module scoping
134
- - `@farmslot/recipe-harness/runtime/metro-probe` — Metro `/status` reachability
89
+ ## Team libraries
135
90
 
136
- This repo adds **MetaMask adapters only**: `adapters/mobile/deps-markers.ts`,
137
- `adapters/mobile/runtime-decision.ts`, `adapters/extension/runtime-decision.ts`,
138
- and shell launch in `adapters/porcelain/mm-recipe` / `adapters/porcelain/mme-recipe`. Do not re-copy harness
139
- primitives into `adapters/shared/`. CI installs harness from npm — publish
140
- `@farmslot/recipe-harness` before bumping the dependency here.
91
+ ```bash
92
+ export RECIPE_LIBRARY_PATH="team=$HOME/shared-library/team-recipes"
93
+ mm-harness run --list
94
+ ```
141
95
 
142
- ## Dev/prod harness (`MM_HARNESS_BIN`)
96
+ Shared libraries hold durable actions and composable recipes. Task acceptance
97
+ criteria remain task-local. See [Recipes](docs/RECIPES.md).
143
98
 
144
- The global npm install owns the `mm-harness` on `PATH` — that is the **prod**
145
- harness. To run a **dev** checkout with active pre-release changes alongside it,
146
- set `MM_HARNESS_BIN` to that checkout's `bin/mm-harness`:
99
+ ## Recover
147
100
 
148
101
  ```bash
149
- export MM_HARNESS_BIN=/path/to/checkout/bin/mm-harness # dev: this checkout owns the run
150
- unset MM_HARNESS_BIN # prod: the installed/global bin
102
+ mm-harness doctor
103
+ mm-harness doctor --fix
104
+ mm-harness verify
105
+ mm-harness cleanup
151
106
  ```
152
107
 
153
- `MM_HARNESS_BIN` is the single override point: when set, the entrypoint hands the
154
- whole invocation to that path before doing any dependency work; when unset it
155
- resolves the installed bin. A dev checkout is never installed globally, so there is
156
- no `PATH` collision dev is reached only through `MM_HARNESS_BIN` (or a shell
157
- alias). A per-farm environment can export it so the dev farm dispatches through the
158
- dev harness while the prod farm leaves it unset.
108
+ Failures name one exact next action. Core is headless; browser, device, logs,
109
+ and debugger capabilities are reported as unavailable instead of fabricated.
110
+ Stable exit codes are `1` runtime/action failure, `2` invalid CLI usage, `3`
111
+ infrastructure failure, `4` bounded recovery refusal, and `5` validation/trust
112
+ failure.
159
113
 
160
- ## Runtime notes
114
+ ## Reference
161
115
 
162
- - `tmux` is recommended for long-lived Metro/webpack processes; standalone use
163
- falls back to detached `nohup` where possible.
164
- - Mobile may inject a local development bridge/HUD into older checkouts. Do not
165
- commit those product patches.
166
- - Extension does not patch product source; it drives `dist/chrome` through Chrome
167
- CDP.
116
+ - [Recipes](docs/RECIPES.md) discover, compose, author, and share proof.
117
+ - [Security](docs/SECURITY.md) trust, approval, fixtures, and evidence safety.
118
+ - [QA](docs/QA.md) clean-machine and human release checks.
119
+ - [Contributing](docs/CONTRIBUTING.md) ownership, layout, and change gates.
168
120
 
169
- ## Validate changes
121
+ For development, point the installed command at a source checkout:
170
122
 
171
123
  ```bash
172
- yarn check
173
- bash -n bin/mm-harness adapters/porcelain/{metamask-recipe,mm-recipe,mme-recipe} adapters/{mobile,extension,core}/*.sh
174
- node --check adapters/extension/inject.mjs adapters/extension/cleanup.mjs adapters/extension/readiness.mjs adapters/extension/launch-browser.cjs adapters/shared/open-debug.mjs adapters/shared/cli-home.mjs adapters/shared/log-tui.mjs adapters/shared/progress.mjs adapters/shared/recipe-paths.mjs scripts/check.mjs
124
+ export MM_HARNESS_BIN=/path/to/metamask-harness/bin/mm-harness
125
+ mm-harness --version
175
126
  ```
176
127
 
177
- More detail: [Architecture](docs/architecture.md), [Package boundaries](docs/package-boundaries.md), [Runtime file conventions](docs/runtime-file-conventions.md).
128
+ Unset `MM_HARNESS_BIN` to return to the published installation.
@@ -110,12 +110,7 @@ install_v1_runner_assets() {
110
110
  if [ -d "$METAMASK_RUNNER_DIR/library/recipes" ]; then
111
111
  rsync -a --delete "$METAMASK_RUNNER_DIR/library/recipes/" "$HARNESS_DIR/runner/recipes/"
112
112
  fi
113
- # Canonical recipe-library snapshot: flows/ + library.json so the installed
114
- # harness mirrors the runner's library layout.
115
- if [ -d "$METAMASK_RUNNER_DIR/library/flows" ]; then
116
- mkdir -p "$HARNESS_DIR/runner/flows"
117
- rsync -a --delete "$METAMASK_RUNNER_DIR/library/flows/" "$HARNESS_DIR/runner/flows/"
118
- fi
113
+ rm -rf "$HARNESS_DIR/runner/flows"
119
114
  if [ -f "$METAMASK_RUNNER_DIR/library/library.json" ]; then
120
115
  cp "$METAMASK_RUNNER_DIR/library/library.json" "$HARNESS_DIR/runner/library.json"
121
116
  fi
@@ -8,7 +8,7 @@
8
8
  // the tab shows live extension logs (the equivalent of the Metro/webpack tabs).
9
9
  //
10
10
  // Usage:
11
- // console-tail.mjs --cdp-port <port> [--log <file>]
11
+ // console-tail.mjs --cdp-port <port> [--extension-log <file>] [--dapp-log <file>]
12
12
  //
13
13
  // Resilience: MV3 service workers cycle on idle and UI pages open/close, so we
14
14
  // re-poll /json/list every ~2s and (re)attach to any extension target that is
@@ -19,9 +19,10 @@
19
19
  'use strict';
20
20
 
21
21
  import { appendFileSync } from 'node:fs';
22
+ import path from 'node:path';
22
23
 
23
24
  function parseArgs(argv) {
24
- const args = { cdpPort: null, log: null };
25
+ const args = { cdpPort: null, extensionLog: null, dappLog: null };
25
26
  for (let i = 0; i < argv.length; i += 1) {
26
27
  const arg = argv[i];
27
28
  switch (arg) {
@@ -29,12 +30,16 @@ function parseArgs(argv) {
29
30
  args.cdpPort = argv[++i];
30
31
  break;
31
32
  case '--log':
32
- args.log = argv[++i];
33
+ case '--extension-log':
34
+ args.extensionLog = argv[++i];
35
+ break;
36
+ case '--dapp-log':
37
+ args.dappLog = argv[++i];
33
38
  break;
34
39
  case '-h':
35
40
  case '--help':
36
41
  process.stdout.write(
37
- 'Usage: console-tail.mjs --cdp-port <port> [--log <file>]\n',
42
+ 'Usage: console-tail.mjs --cdp-port <port> [--extension-log <file>] [--dapp-log <file>]\n',
38
43
  );
39
44
  process.exit(0);
40
45
  break;
@@ -51,6 +56,9 @@ if (!Number.isInteger(cdpPort) || cdpPort <= 0) {
51
56
  throw new Error('console-tail requires a numeric --cdp-port.');
52
57
  }
53
58
  const HOST = `127.0.0.1:${cdpPort}`;
59
+ if (args.extensionLog && !args.dappLog) {
60
+ args.dappLog = path.join(path.dirname(path.resolve(args.extensionLog)), 'dapp-console.log');
61
+ }
54
62
 
55
63
  if (typeof WebSocket === 'undefined') {
56
64
  throw new Error('console-tail requires Node 22+ with a global WebSocket.');
@@ -65,9 +73,13 @@ const cooldown = new Map();
65
73
  // Writes formatted console-event strings (never raw HTTP response bodies) to
66
74
  // stdout and optionally to a caller-supplied log file. The source is the local
67
75
  // Chrome CDP WebSocket (127.0.0.1:<cdpPort>) — a trusted-local endpoint.
68
- function out(line) {
76
+ function out(line, destination = null) {
69
77
  process.stdout.write(`${line}\n`);
70
- if (args.log) appendFileSync(args.log, `${line}\n`);
78
+ if (destination) {
79
+ // This file is explicitly an untrusted application-console transcript; it is never executed or parsed as configuration.
80
+ // codeql[js/http-to-file-access]
81
+ appendFileSync(destination, `${line}\n`);
82
+ }
71
83
  }
72
84
 
73
85
  function stamp() {
@@ -95,6 +107,22 @@ function isExtensionTarget(target) {
95
107
  return String(target.url || '').startsWith('chrome-extension://');
96
108
  }
97
109
 
110
+ function isDappTarget(target) {
111
+ if (!target || !target.webSocketDebuggerUrl || target.type !== 'page') return false;
112
+ try {
113
+ const url = new URL(String(target?.url || ''));
114
+ return url.protocol === 'http:' || url.protocol === 'https:';
115
+ } catch {
116
+ return false;
117
+ }
118
+ }
119
+
120
+ function targetStream(target) {
121
+ if (isExtensionTarget(target)) return { kind: 'extension', log: args.extensionLog };
122
+ if (isDappTarget(target)) return { kind: 'dapp', log: args.dappLog };
123
+ return null;
124
+ }
125
+
98
126
  function previewArg(arg) {
99
127
  if (!arg || typeof arg !== 'object') return String(arg);
100
128
  if ('value' in arg && arg.value !== undefined) {
@@ -108,29 +136,29 @@ function previewArg(arg) {
108
136
  return arg.subtype || arg.type || '?';
109
137
  }
110
138
 
111
- function emitConsole(label, params) {
139
+ function emitConsole(label, params, destination) {
112
140
  const level = String(params.type || 'log').toUpperCase();
113
141
  const text = (params.args || []).map(previewArg).join(' ');
114
- out(`${stamp()} [${label}] ${level} ${text}`);
142
+ out(`${stamp()} [${label}] ${level} ${text}`, destination);
115
143
  }
116
144
 
117
- function emitException(label, params) {
145
+ function emitException(label, params, destination) {
118
146
  const d = params.exceptionDetails || {};
119
147
  const text =
120
148
  d.exception?.description ||
121
149
  d.exception?.value ||
122
150
  d.text ||
123
151
  'uncaught exception';
124
- out(`${stamp()} [${label}] EXCEPTION ${text}`);
152
+ out(`${stamp()} [${label}] EXCEPTION ${text}`, destination);
125
153
  }
126
154
 
127
- function emitLogEntry(label, params) {
155
+ function emitLogEntry(label, params, destination) {
128
156
  const e = params.entry || {};
129
157
  const level = String(e.level || 'log').toUpperCase();
130
- out(`${stamp()} [${label}] ${level} ${e.text || ''}`);
158
+ out(`${stamp()} [${label}] ${level} ${e.text || ''}`, destination);
131
159
  }
132
160
 
133
- function handleMessage(label, data) {
161
+ function handleMessage(label, destination, data) {
134
162
  let message;
135
163
  try {
136
164
  message = JSON.parse(data);
@@ -141,13 +169,13 @@ function handleMessage(label, data) {
141
169
  if (message.id != null) return; // command ack, not an event
142
170
  switch (message.method) {
143
171
  case 'Runtime.consoleAPICalled':
144
- emitConsole(label, message.params || {});
172
+ emitConsole(label, message.params || {}, destination);
145
173
  break;
146
174
  case 'Runtime.exceptionThrown':
147
- emitException(label, message.params || {});
175
+ emitException(label, message.params || {}, destination);
148
176
  break;
149
177
  case 'Log.entryAdded':
150
- emitLogEntry(label, message.params || {});
178
+ emitLogEntry(label, message.params || {}, destination);
151
179
  break;
152
180
  default:
153
181
  break;
@@ -155,9 +183,11 @@ function handleMessage(label, data) {
155
183
  }
156
184
 
157
185
  function attach(target) {
158
- const label = shortLabel(target);
186
+ const stream = targetStream(target);
187
+ if (!stream) return;
188
+ const label = `${stream.kind}:${shortLabel(target)}`;
159
189
  const ws = new WebSocket(target.webSocketDebuggerUrl);
160
- const entry = { ws, label, opened: false };
190
+ const entry = { ws, label, destination: stream.log, opened: false };
161
191
  attached.set(target.id, entry);
162
192
 
163
193
  ws.addEventListener('open', () => {
@@ -166,16 +196,16 @@ function attach(target) {
166
196
  for (const method of ['Runtime.enable', 'Log.enable']) {
167
197
  ws.send(JSON.stringify({ id: ++id, method, params: {} }));
168
198
  }
169
- out(`${stamp()} [${label}] [attached]`);
199
+ out(`${stamp()} [${label}] [attached]`, stream.log);
170
200
  });
171
201
  ws.addEventListener('message', (event) => {
172
- handleMessage(label, typeof event.data === 'string' ? event.data : '');
202
+ handleMessage(label, stream.log, typeof event.data === 'string' ? event.data : '');
173
203
  });
174
204
  const drop = () => {
175
205
  if (attached.get(target.id) !== entry) return;
176
206
  attached.delete(target.id);
177
207
  if (entry.opened) {
178
- out(`${stamp()} [${label}] [reconnect] target detached; will re-attach`);
208
+ out(`${stamp()} [${label}] [reconnect] target detached; will re-attach`, stream.log);
179
209
  } else {
180
210
  // Never connected (target gone / not debuggable yet): back off so a dead
181
211
  // target is not retried — and logged — on every 2s poll.
@@ -196,8 +226,9 @@ async function poll() {
196
226
  // CDP not reachable this tick (browser starting/restarting); retry next poll.
197
227
  return;
198
228
  }
199
- for (const target of Array.isArray(targets) ? targets : []) {
200
- if (!isExtensionTarget(target)) continue;
229
+ const targetList = Array.isArray(targets) ? targets : [];
230
+ for (const target of targetList) {
231
+ if (!targetStream(target)) continue;
201
232
  if (attached.has(target.id)) continue;
202
233
  const until = cooldown.get(target.id);
203
234
  if (until !== undefined) {
@@ -226,6 +257,6 @@ function shutdown() {
226
257
  process.on('SIGINT', shutdown);
227
258
  process.on('SIGTERM', shutdown);
228
259
 
229
- out(`${stamp()} [console-tail] streaming extension console via cdp ${cdpPort}`);
260
+ out(`${stamp()} [console-tail] streaming extension and dapp consoles via cdp ${cdpPort}`);
230
261
  await poll();
231
262
  setInterval(poll, 2000);
@@ -277,7 +277,8 @@ const fs = require('fs');
277
277
  const http = require('http');
278
278
  const { execSync, execFileSync } = require('child_process');
279
279
  let chromium; try { chromium = require('@playwright/test').chromium; } catch { chromium = require('playwright').chromium; }
280
- const { remoteDebuggingArgs } = require(path.join('${SCRIPT_DIR}', 'lib/chrome-args.cjs'));
280
+ const { isolatedProfileArgs, remoteDebuggingArgs } = require(path.join('${SCRIPT_DIR}', 'lib/chrome-args.cjs'));
281
+ const { captureMacFrontmostProcess, restoreMacFrontmostProcess } = require(path.join('${SCRIPT_DIR}', 'lib/macos-focus.cjs'));
281
282
 
282
283
  const SLOT_ID = '${SLOT_ID}';
283
284
  const AGENT_DIR = '${AGENT_DIR}';
@@ -308,6 +309,7 @@ const resumeWebpack = () => {
308
309
  '--disable-renderer-backgrounding',
309
310
  '--no-first-run',
310
311
  '--no-default-browser-check',
312
+ ...isolatedProfileArgs(),
311
313
  '--window-size=420,800',
312
314
  ];
313
315
  // Debug-port trio (address + port + scoped allow-origins) from the shared module
@@ -318,6 +320,7 @@ const resumeWebpack = () => {
318
320
  }
319
321
 
320
322
  const chromiumApp = path.dirname(path.dirname(path.dirname(chromium.executablePath())));
323
+ const previousFrontmostPid = captureMacFrontmostProcess();
321
324
  execFileSync('open', ['-g', '-n', '-a', chromiumApp, '--args', ...args], { stdio: 'ignore' });
322
325
  for (let i = 0; i < 60; i++) {
323
326
  try {
@@ -420,6 +423,8 @@ const resumeWebpack = () => {
420
423
  }
421
424
  if (process.env.MM_HARNESS_FOCUS_BROWSER === '1') {
422
425
  await page.bringToFront().catch(() => {});
426
+ } else {
427
+ restoreMacFrontmostProcess(previousFrontmostPid);
423
428
  }
424
429
 
425
430
  fs.writeFileSync(path.join(AGENT_DIR, 'extension.id'), extId);
@@ -90,21 +90,18 @@ copyFile(path.join(runnerDir, 'library/manifests/mobile.action-manifest.json'),
90
90
  copyFile(path.join(runnerDir, 'library/manifests/extension.action-manifest.json'), path.join(harnessDir, 'runner/manifests/extension.action-manifest.json'));
91
91
  copyFile(path.join(runnerDir, 'library/manifests/extension.action-manifest.json'), path.join(harnessDir, 'action-manifest.json'));
92
92
  copyDir(path.join(runnerDir, 'library/recipes'), path.join(harnessDir, 'runner/recipes'));
93
- // Canonical recipe-library snapshot: flows/ + library.json so the installed
94
- // harness mirrors the runner's library layout.
95
93
  copyFile(path.join(runnerDir, 'library/library.json'), path.join(harnessDir, 'runner/library.json'));
96
- fs.mkdirSync(path.join(harnessDir, 'runner/flows'), { recursive: true });
97
- copyDir(path.join(runnerDir, 'library/flows'), path.join(harnessDir, 'runner/flows'));
98
- // Installed-layout back-compat (INTENTIONAL): targets keep a flat
99
- // <harness>/scripts/ subtree assembled from the runner trees, so installed
100
- // consumers are unaffected by runner-repo layout changes.
94
+ fs.rmSync(path.join(harnessDir, 'runner/flows'), { recursive: true, force: true });
95
+ // The installed overlay has a flat scripts/ runtime API independent of the
96
+ // package's source layout.
101
97
  copyFile(path.join(runnerDir, 'adapters/extension/launch-browser.cjs'), path.join(harnessDir, 'scripts/launch-browser.cjs'));
102
- // installed back-compat alias: callers of the pre-rename installed name
103
- copyFile(path.join(runnerDir, 'adapters/extension/launch-browser.cjs'), path.join(harnessDir, 'scripts/launch-chrome-detached.cjs'));
104
98
  copyFile(path.join(runnerDir, 'adapters/extension/console-tail.mjs'), path.join(harnessDir, 'scripts/console-tail.mjs'));
105
99
  copyFile(path.join(runnerDir, 'adapters/extension/launch.sh'), path.join(harnessDir, 'scripts/launch.sh'));
106
100
  copyFile(path.join(runnerDir, 'adapters/extension/live.sh'), path.join(harnessDir, 'scripts/live.sh'));
107
101
  copyFile(path.join(runnerDir, 'adapters/extension/start-watch.sh'), path.join(harnessDir, 'scripts/start-watch.sh'));
102
+ copyFile(path.join(runnerDir, 'adapters/extension/launch-webpack.cjs'), path.join(harnessDir, 'scripts/launch-webpack.cjs'));
103
+ copyFile(path.join(runnerDir, 'adapters/extension/sync-webpack-dist.cjs'), path.join(harnessDir, 'scripts/sync-webpack-dist.cjs'));
104
+ copyFile(path.join(runnerDir, 'adapters/extension/stamp-runtime-title.cjs'), path.join(harnessDir, 'scripts/stamp-runtime-title.cjs'));
108
105
  copyFile(path.join(runnerDir, 'adapters/extension/stop-viewers.sh'), path.join(harnessDir, 'scripts/stop-viewers.sh'));
109
106
  copyFile(path.join(runnerDir, 'adapters/extension/snapshot-dist.sh'), path.join(harnessDir, 'scripts/snapshot-dist.sh'));
110
107
  copyFile(path.join(runnerDir, 'adapters/extension/pin-remote-flags.cjs'), path.join(harnessDir, 'scripts/pin-remote-flags.cjs'));
@@ -121,10 +118,6 @@ copyFile(path.join(runnerDir, 'adapters/shared/activate-repo-node.sh'), path.joi
121
118
  copyFile(path.join(runnerDir, 'adapters/shared/resolve-slot-ports.sh'), path.join(harnessDir, 'scripts/lib/resolve-slot-ports.sh'));
122
119
  copyFile(path.join(runnerDir, 'adapters/shared/resolve-slot-ports.mjs'), path.join(harnessDir, 'scripts/lib/resolve-slot-ports.mjs'));
123
120
  copyFile(path.join(runnerDir, 'adapters/shared/resolve-slot-ports-core.mjs'), path.join(harnessDir, 'scripts/lib/resolve-slot-ports-core.mjs'));
124
- // Back-compat shims (one release) so an overlay consumer using the old leaf names still resolves.
125
- copyFile(path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports.sh'), path.join(harnessDir, 'scripts/lib/resolve-farmslot-ports.sh'));
126
- copyFile(path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports.mjs'), path.join(harnessDir, 'scripts/lib/resolve-farmslot-ports.mjs'));
127
- copyFile(path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports-core.mjs'), path.join(harnessDir, 'scripts/lib/resolve-farmslot-ports-core.mjs'));
128
121
  copyFile(path.join(runnerDir, 'adapters/shared/path-defaults.json'), path.join(harnessDir, 'scripts/lib/path-defaults.json'));
129
122
  copyFile(path.join(runnerDir, 'adapters/shared/json-field.sh'), path.join(harnessDir, 'scripts/lib/json-field.sh'));
130
123
  copyFile(path.join(runnerDir, 'adapters/shared/tmux-session.sh'), path.join(harnessDir, 'scripts/lib/tmux-session.sh'));
@@ -133,6 +126,7 @@ copyFile(path.join(runnerDir, 'adapters/shared/log-tui.mjs'), path.join(harnessD
133
126
  copyFile(path.join(runnerDir, 'adapters/extension/lib/extension-id.cjs'), path.join(harnessDir, 'scripts/lib/extension-id.cjs'));
134
127
  copyFile(path.join(runnerDir, 'adapters/extension/lib/slot-title.cjs'), path.join(harnessDir, 'scripts/lib/slot-title.cjs'));
135
128
  copyFile(path.join(runnerDir, 'adapters/extension/lib/chrome-args.cjs'), path.join(harnessDir, 'scripts/lib/chrome-args.cjs'));
129
+ copyFile(path.join(runnerDir, 'adapters/extension/lib/macos-focus.cjs'), path.join(harnessDir, 'scripts/lib/macos-focus.cjs'));
136
130
  makeExecutableTree(path.join(harnessDir, 'scripts'));
137
131
  fs.writeFileSync(path.join(harnessDir, 'installed-scripts.sha256'), `${dirContentHash(path.join(harnessDir, 'scripts'))}\n`);
138
132
 
@@ -24,7 +24,8 @@ const os = require('node:os');
24
24
  const path = require('node:path');
25
25
  const { execFileSync, spawn, spawnSync } = require('node:child_process');
26
26
  const { extensionIdFromExtensionDir } = require('./lib/extension-id.cjs');
27
- const { remoteDebuggingArgs } = require('./lib/chrome-args.cjs');
27
+ const { isolatedProfileArgs, remoteDebuggingArgs } = require('./lib/chrome-args.cjs');
28
+ const { captureMacFrontmostProcess, restoreMacFrontmostProcess } = require('./lib/macos-focus.cjs');
28
29
 
29
30
  if (process.argv.includes('--help') || process.argv.includes('-h')) {
30
31
  console.log(
@@ -98,6 +99,7 @@ const chromeArgs = [
98
99
  ...remoteDebuggingArgs(cdpPort),
99
100
  '--no-first-run',
100
101
  '--disable-first-run-ui',
102
+ ...isolatedProfileArgs(),
101
103
  '--disable-default-apps',
102
104
  '--disable-popup-blocking',
103
105
  '--disable-extensions-file-access-check',
@@ -111,6 +113,7 @@ const chromeArgs = [
111
113
  initialUrl,
112
114
  ];
113
115
  const logFd = fs.openSync(args['chrome-log'], 'a');
116
+ const previousFrontmostPid = captureMacFrontmostProcess();
114
117
  let browserPid;
115
118
  try {
116
119
  const application = process.platform === 'darwin' ? macApplicationForExecutable(args['chrome-bin']) : null;
@@ -136,6 +139,9 @@ try {
136
139
  fs.closeSync(logFd);
137
140
  }
138
141
  fs.writeFileSync(args['chrome-pid'], `${browserPid}\n`);
142
+ if (process.env.MM_HARNESS_FOCUS_BROWSER !== '1') {
143
+ restoreMacFrontmostProcess(previousFrontmostPid);
144
+ }
139
145
 
140
146
  function sanitizedChildEnv() {
141
147
  return {