@deeeed/metamask-harness 0.38.1 → 0.40.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 (104) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/adapters/extension/lib/playwright-cdp.cjs +17 -1
  3. package/adapters/extension/sidepanel-toggle.sh +135 -13
  4. package/dist/adapters/mobile/release-artifact.js +65 -0
  5. package/dist/adapters.js +27 -17
  6. package/dist/command-contract.js +10 -2
  7. package/dist/commands/call.js +7 -4
  8. package/dist/commands/launch/index.js +55 -3
  9. package/dist/commands/parse-args.js +6 -0
  10. package/dist/commands/run-engine.js +75 -39
  11. package/dist/funding-execution-context.js +1386 -0
  12. package/dist/live-adapter-contract.js +3 -1
  13. package/dist/metamask-action-validation.js +170 -3
  14. package/dist/mm-harness-cli.js +8 -2
  15. package/dist/recipe-security.js +5 -12
  16. package/dist/runner.js +102 -6
  17. package/docs/RECIPES.md +74 -1
  18. package/docs/RELEASE-QA-CAPABILITY-MAP.md +6 -5
  19. package/library/actions/extension/assets/open_details.mjs +30 -0
  20. package/library/actions/extension/assets/set_token_visibility.mjs +5 -0
  21. package/library/actions/extension/networks/add_chainlist.mjs +4 -0
  22. package/library/actions/extension/networks/add_custom.mjs +4 -0
  23. package/library/actions/extension/networks/custom_network.mjs +48 -1
  24. package/library/actions/extension/perps/assert_visible_consistency.mjs +366 -0
  25. package/library/actions/extension/perps/close_visible_position.mjs +299 -37
  26. package/library/actions/extension/perps/compare_provider_market.mjs +77 -0
  27. package/library/actions/extension/perps/edit_margin.mjs +285 -44
  28. package/library/actions/extension/perps/mutation-receipt.mjs +781 -0
  29. package/library/actions/extension/perps/perps.mjs +1029 -363
  30. package/library/actions/extension/perps/read_funds_confirmation.mjs +126 -13
  31. package/library/actions/extension/perps/read_snapshot.mjs +124 -3
  32. package/library/actions/extension/perps/read_visible_state.mjs +63 -5
  33. package/library/actions/extension/perps/search_markets.mjs +32 -0
  34. package/library/actions/extension/perps/select_activity_filter.mjs +81 -18
  35. package/library/actions/extension/perps/select_market_filter.mjs +48 -4
  36. package/library/actions/extension/perps/set_market_favorite.mjs +6 -1
  37. package/library/actions/extension/perps/update_position_tpsl.mjs +251 -69
  38. package/library/actions/extension/perps/visible-mutation-identity.mjs +164 -0
  39. package/library/actions/extension/platform/cdp.mjs +88 -9
  40. package/library/actions/extension/settings/set_basic_functionality.mjs +35 -7
  41. package/library/actions/extension/swap_bridge/read_visible_state.mjs +2 -6
  42. package/library/actions/extension/swap_bridge/set_amount.mjs +11 -2
  43. package/library/actions/extension/ui/navigate.mjs +45 -5
  44. package/library/actions/extension/wallet/import.mjs +70 -29
  45. package/library/actions/extension/wallet/lock.mjs +62 -5
  46. package/library/actions/extension/wallet/visible-session.mjs +218 -0
  47. package/library/actions/mobile/assets/import_custom_token.mjs +181 -0
  48. package/library/actions/mobile/assets/open_details.mjs +43 -0
  49. package/library/actions/mobile/assets/read_details.mjs +96 -0
  50. package/library/actions/mobile/assets/set_token_visibility.mjs +302 -0
  51. package/library/actions/mobile/assets/verify_sorting.mjs +104 -0
  52. package/library/actions/mobile/networks/add_custom.mjs +125 -0
  53. package/library/actions/mobile/networks/network-management.mjs +132 -0
  54. package/library/actions/mobile/networks/read_visible_state.mjs +43 -0
  55. package/library/actions/mobile/networks/remove_custom.mjs +100 -0
  56. package/library/actions/mobile/perps/search_markets.mjs +46 -0
  57. package/library/actions/mobile/perps/set_market_favorite.mjs +117 -0
  58. package/library/actions/mobile/platform/native-session-name.mjs +14 -0
  59. package/library/actions/mobile/platform/native-session.mjs +59 -0
  60. package/library/actions/mobile/platform/observe-ui.mjs +9 -1
  61. package/library/actions/mobile/swap_bridge/native-session.mjs +10 -45
  62. package/library/actions/mobile/swap_bridge/select_assets.mjs +15 -1
  63. package/library/actions/mobile/swap_bridge/set_amount.mjs +1 -1
  64. package/library/actions/mobile/swap_bridge/set_max_amount.mjs +55 -0
  65. package/library/actions/mobile/swap_bridge/set_slippage.mjs +96 -0
  66. package/library/actions/mobile/swap_bridge/submit_transaction.mjs +141 -0
  67. package/library/actions/mobile/ui/native-navigation.mjs +120 -23
  68. package/library/actions/mobile/ui/navigate.mjs +13 -2
  69. package/library/actions/mobile/wallet/import.mjs +17 -10
  70. package/library/actions/mobile/wallet/lock.mjs +14 -0
  71. package/library/actions/mobile/wallet/native-ui.mjs +88 -15
  72. package/library/actions/shared/perps/visible-state.mjs +33 -11
  73. package/library/actions/shared/swap-bridge/visible-state.mjs +15 -0
  74. package/library/manifests/extension.action-manifest.json +1707 -1241
  75. package/library/manifests/mobile.action-manifest.json +308 -1
  76. package/library/recipes/assets/release-token-details.recipe.json +5 -4
  77. package/library/recipes/assets/release-token-hide-readd.recipe.json +2 -0
  78. package/library/recipes/{assets → extension/assets}/release-custom-network-token-persistence.recipe.json +1 -0
  79. package/library/recipes/{perps → extension/perps}/release-activity-filters.recipe.json +5 -5
  80. package/library/recipes/{perps → extension/perps}/release-funds-flows.recipe.json +3 -3
  81. package/library/recipes/extension/perps/release-live-limit-order.recipe.json +24 -19
  82. package/library/recipes/extension/perps/release-market-filters.recipe.json +97 -0
  83. package/library/recipes/extension/perps/release-order-entry.recipe.json +4 -1
  84. package/library/recipes/extension/perps/release-order-validation.recipe.json +53 -14
  85. package/library/recipes/extension/perps/release-visible-consistency.recipe.json +47 -0
  86. package/library/recipes/extension/perps/source-dev-snapshot-consistency.recipe.json +76 -0
  87. package/library/recipes/mobile/networks/release-custom-network-persistence.recipe.json +90 -0
  88. package/library/recipes/mobile/networks/release-multichain-visibility.recipe.json +58 -0
  89. package/library/recipes/perps/release-live-market-order.recipe.json +30 -13
  90. package/library/recipes/perps/release-market-details.recipe.json +1 -1
  91. package/library/recipes/perps/release-market-search.recipe.json +8 -4
  92. package/library/recipes/perps/release-watchlist.recipe.json +31 -19
  93. package/library/recipes/swap-bridge/release-custom-slippage.recipe.json +5 -13
  94. package/library/recipes/swap-bridge/release-quote.recipe.json +1 -0
  95. package/library/recipes/wallet/import.recipe.json +4 -14
  96. package/package.json +1 -1
  97. package/library/actions/extension/assets/finish_send.mjs +0 -128
  98. package/library/actions/extension/swap_bridge/submit_transaction.mjs +0 -363
  99. package/library/actions/extension/ui/activate_and_observe.mjs +0 -96
  100. package/library/recipes/assets/release-custom-gas-send.recipe.json +0 -88
  101. package/library/recipes/assets/release-native-max-cancel.recipe.json +0 -70
  102. package/library/recipes/assets/release-native-max-send.recipe.json +0 -78
  103. package/library/recipes/assets/release-own-account-send.recipe.json +0 -80
  104. package/library/recipes/extension/runner/action-validation.recipe.json +0 -343
package/CHANGELOG.md CHANGED
@@ -2,6 +2,64 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.40.0 - 2026-08-16
6
+
7
+ - Remove Extension's public `metamask.perps.close_positions` and `metamask.perps.start_state` surfaces and its mutating `runner.action-validation` recipe; retain only state assertions it can prove without implicit readiness or unreceipted convergence.
8
+ - Make guarded Extension custom-network setup wait for an unobscured account-menu hit point before opening network management.
9
+
10
+ ### Added
11
+
12
+ - Add explicit Extension fullscreen/sidepanel launch selection and auto/show/hide recipe HUD policy.
13
+ - Bind external-mutation runs to independently validated release, onboarding, balance-evidence, request, and preflight bytes plus the same-run visible wallet/runtime identity.
14
+ - Add a packaged Perps market-filter release recipe with a deterministic market-list start state and every nonconditional category control.
15
+ - Add official-compatible visible Extension Perps market-list/activity consistency assertions, plus source-dev-only background snapshot sampling and activity-to-history identity expectations.
16
+ - Bind every live Extension action result to the selected CDP target and current browser runtime session.
17
+
18
+ ### Changed
19
+
20
+ - Canonicalize durable Extension Perps release journeys under one discovered recipe ID each, with activity and funds flows scoped to Extension and explicit official/source-development and live-mutation boundaries.
21
+ - Parameterize live values in the Extension Perps order-validation recipe and narrow existing market-details, activity-filter, and funds-preview claims to their asserted evidence.
22
+ - Require an explicit flat market for non-submitting Extension order-form recipes and make live-price movement an opt-in source-development snapshot assertion.
23
+
24
+ ### Fixed
25
+
26
+ - Trust only the current Perps-only funding registry plus the named frozen G005 digest; reject duplicate or aggregate-over-cap mutation operations and budgets, mutation-capable balance graphs, root-ref dependency shadowing, and non-visible or non-Home balance observations.
27
+ - Bind trusted funding and onboarding evidence to exact emitted call graphs, successful node/action order, and reachable root PASS terminals while preserving Mobile's adapter-owned external-mutation consent boundary.
28
+ - Stop advertising shared Perps clean-market convergence on Extension; its state helpers now expose only navigation and assertion inputs and reject ignored creation or cleanup parameters during planning.
29
+ - Remove unsupported Extension send completion, Swap/Bridge submission, and generic activate-and-observe capabilities plus four non-executable Assets submit recipes; retain read-only, prepare, review, quote, and Mobile-owned live Swap planning.
30
+ - Bind funded Extension Perps margin and TP/SL updates to the exact visible position size, enforce order caps against requested leveraged exposure before receipt publication, and default funded placements to one-times leverage.
31
+ - Fail closed on visible and background Perps financial mutations without an identity-bound durable receipt, current visible funding minimum, runner-owned artifact/runtime, and exact dispatch-boundary provider/network; terminally record known non-dispatch instead of retrying the same claim.
32
+ - Require approved visible wallet method and credential provenance plus a hash-bound emitted recipe, pass summary, artifact manifest, and exact reachable root-terminal PASS in funding evidence.
33
+ - Require every funded visible position edit or close to recheck current funding, provider/network, and exact target state after receipt publication and immediately before its first pointer dispatch.
34
+ - Invalidate funded-mutation wallet identity proof after account selection, reset, lock, unlock, setup, or any state-changing generic UI action until a complete fresh visible wallet import proof succeeds.
35
+ - Keep Extension Perps state wrappers observation-only so they cannot place, close, or cancel implicitly outside a leaf action's funding authorization.
36
+ - Preserve a final Perps navigation observation poll and require Side Panel convergence to one MetaMask product surface beside the explicitly requested dapp.
37
+ - Require the canonical non-submitting Swap quote recipe to prove a positive reviewable quote instead of accepting a no-route state.
38
+ - Refuse cleanup-bound custom-network setup when the target network already exists, preserving pre-existing wallet configuration.
39
+ - Bind Android release-artifact action results to the verified native app process session and redact Mobile wallet-import identity proofs to a matched digest.
40
+ - Distinguish obscured Perps controls from harmless edge intersections in visible layout proofs and include confirmation controls on funds surfaces.
41
+ - Reject duplicate rendered market, watchlist, or activity identities during optional Extension consistency proofs.
42
+ - Read Perps funds confirmation titles from their visible wallet-initiated header and support exact amount, token, network, percentage-control, and disabled-state assertions.
43
+ - Wait for Basic Functionality consent to enable its confirmation before submitting the visible settings change.
44
+ - Reach the Extension global wallet menu through the visible Home control before locking from headerless product routes.
45
+ - Keep Extension surface transitions singular and route the sidepanel launcher's obscuration hit test through hardened raw CDP when its visible notification badge overlaps the button.
46
+ - Namespace funded Perps claims by exact request and preflight bytes, persist identity-bound receipts before live pointer dispatch, and restrict over-cap order recovery to exact cleanup-only cancellation without upgrading placement success.
47
+ - Relaunch intact Extension release artifacts without source dependency repair, and support exact receipt-bound close-only Perps cleanup with primary-card and balance reconciliation proof.
48
+ - Validate Extension Perps activity filters against their singular route query while requiring the requested filter label to remain visible.
49
+ - Make Extension `wallet.import method=ui` idempotent on an onboarded profile by visibly unlocking and selecting the exact derived account without fixture or background-state probes.
50
+
51
+ ## 0.39.0 - 2026-08-14
52
+
53
+ ### Added
54
+
55
+ - Add opaque Android release actions for token details, sorting, visibility, custom-token import, custom-network lifecycle, Perps search and watchlists, Swap Max, slippage, guarded submission, and wallet locking through native accessibility.
56
+ - Make shared Assets, Perps, and Swap release recipes executable on untouched Extension and Android release artifacts with parameterized visible-state assertions.
57
+
58
+ ### Fixed
59
+
60
+ - Keep physical-device actions on isolated semantic sessions, bypass biometric authentication with password unlock, verify every recovery-phrase field, and reject incorrect token metadata as a product failure.
61
+ - Prove Extension token visibility changes and parse visible custom-slippage labels consistently across both clients.
62
+
5
63
  ## 0.38.1 - 2026-08-14
6
64
 
7
65
  ### Fixed
@@ -1,5 +1,21 @@
1
1
  'use strict';
2
2
 
3
+ const path = require('node:path');
4
+ const { createRequire } = require('node:module');
5
+
6
+ function loadChromium() {
7
+ const runtimeRequire = createRequire(path.join(process.cwd(), 'package.json'));
8
+ try {
9
+ return runtimeRequire('@playwright/test').chromium;
10
+ } catch {
11
+ return runtimeRequire('playwright').chromium;
12
+ }
13
+ }
14
+
15
+ async function connectBrowserViaCdp(endpoint) {
16
+ return loadChromium().connectOverCDP(endpoint);
17
+ }
18
+
3
19
  function buildEvaluationExpression(callbackOrExpression, argument) {
4
20
  if (typeof callbackOrExpression === 'string') return callbackOrExpression;
5
21
  if (typeof callbackOrExpression !== 'function') {
@@ -31,4 +47,4 @@ async function evaluatePageViaCdp(page, callbackOrExpression, argument) {
31
47
  }
32
48
  }
33
49
 
34
- module.exports = { buildEvaluationExpression, evaluatePageViaCdp };
50
+ module.exports = { buildEvaluationExpression, connectBrowserViaCdp, evaluatePageViaCdp };
@@ -7,7 +7,8 @@
7
7
  #
8
8
  # Inputs: subcommand (status|open|close|toggle|cycle, default status);
9
9
  # --cdp-port <port> (required), --ext-id <id>, --agent-dir <dir>,
10
- # --settle-ms <ms>; env REPO, CDP_PORT, EXT_ID, SETTLE_MS, AGENT_DIR.
10
+ # --settle-ms <ms>, --dapp-url <url>; env REPO, CDP_PORT, EXT_ID,
11
+ # SETTLE_MS, AGENT_DIR, EXTENSION_START_URL.
11
12
  # Outputs: status/progress lines on stdout.
12
13
  # Exit 0 — action done; 1 — repo/args missing or action failed;
13
14
  # 2 — non-numeric port / CDP unreachable.
@@ -30,7 +31,7 @@ set -euo pipefail
30
31
 
31
32
  ACTION="${1:-status}"
32
33
  if [ "$ACTION" = "-h" ] || [ "$ACTION" = "--help" ]; then
33
- echo "Usage: sidepanel-toggle.sh <status|open|close|toggle|cycle> --cdp-port <port> [--ext-id <id>] [--agent-dir <dir>] [--settle-ms <ms>]"
34
+ echo "Usage: sidepanel-toggle.sh <status|open|close|toggle|cycle> --cdp-port <port> [--ext-id <id>] [--agent-dir <dir>] [--settle-ms <ms>] [--dapp-url <url>]"
34
35
  exit 0
35
36
  fi
36
37
  shift || true
@@ -68,6 +69,7 @@ CDP_PORT="${CDP_PORT:-}"
68
69
  EXT_ID="${EXT_ID:-}"
69
70
  SETTLE_MS="${SETTLE_MS:-10000}"
70
71
  AGENT_DIR="${AGENT_DIR:-$SCRIPT_DIR}"
72
+ DAPP_URL="${EXTENSION_START_URL:-https://metamask.github.io/test-dapp/}"
71
73
 
72
74
  while [[ $# -gt 0 ]]; do
73
75
  case "$1" in
@@ -75,8 +77,9 @@ while [[ $# -gt 0 ]]; do
75
77
  --ext-id) EXT_ID="$2"; shift 2 ;;
76
78
  --agent-dir) AGENT_DIR="$2"; shift 2 ;;
77
79
  --settle-ms) SETTLE_MS="$2"; shift 2 ;;
80
+ --dapp-url) DAPP_URL="$2"; shift 2 ;;
78
81
  -h|--help)
79
- echo "Usage: sidepanel-toggle.sh <status|open|close|toggle|cycle> --cdp-port <port> [--ext-id <id>] [--agent-dir <dir>] [--settle-ms <ms>]"
82
+ echo "Usage: sidepanel-toggle.sh <status|open|close|toggle|cycle> --cdp-port <port> [--ext-id <id>] [--agent-dir <dir>] [--settle-ms <ms>] [--dapp-url <url>]"
80
83
  exit 0
81
84
  ;;
82
85
  *) echo "unknown arg: $1" >&2; exit 1 ;;
@@ -91,6 +94,10 @@ if ! [[ "$CDP_PORT" =~ ^[0-9]+$ ]]; then
91
94
  echo "FAIL: CDP port \"$CDP_PORT\" must be numeric" >&2
92
95
  exit 2
93
96
  fi
97
+ case "$DAPP_URL" in
98
+ http://*|https://*) ;;
99
+ *) echo "FAIL: --dapp-url must use http or https" >&2; exit 1 ;;
100
+ esac
94
101
  if ! curl -s -m 3 "http://127.0.0.1:${CDP_PORT}/json/version" >/dev/null 2>&1; then
95
102
  echo "FAIL: CDP not reachable on port ${CDP_PORT}" >&2
96
103
  exit 2
@@ -105,7 +112,44 @@ focus_browser() {
105
112
  }
106
113
 
107
114
  find_sidepanel_id() {
108
- json_list | python3 -c "import json,sys; d=json.load(sys.stdin); ids=[t.get('id','') for t in d if t.get('type')=='page' and 'sidepanel.html' in t.get('url','')]; print(ids[0] if ids else '')"
115
+ local ext_id
116
+ ext_id="$(resolve_ext_id)"
117
+ json_list | EXT_ID="$ext_id" python3 -c "import json,os,sys; d=json.load(sys.stdin); ext=os.environ.get('EXT_ID',''); ids=[t.get('id','') for t in d if t.get('type')=='page' and t.get('url','').startswith('chrome-extension://'+ext+'/sidepanel.html')]; print(ids[0] if ids else '')"
118
+ }
119
+
120
+ find_dapp_id() {
121
+ json_list | DAPP_URL="$DAPP_URL" python3 -c "import json,os,sys; d=json.load(sys.stdin); wanted=os.environ['DAPP_URL'].rstrip('/'); ids=[t.get('id','') for t in d if t.get('type')=='page' and t.get('url','').rstrip('/')==wanted]; print(ids[0] if ids else '')"
122
+ }
123
+
124
+ surface_counts() {
125
+ local ext_id
126
+ ext_id="$(resolve_ext_id)"
127
+ json_list | EXT_ID="$ext_id" DAPP_URL="$DAPP_URL" python3 -c "import json,os,sys; d=json.load(sys.stdin); ext=os.environ.get('EXT_ID',''); wanted=os.environ['DAPP_URL'].rstrip('/'); pages=[t for t in d if t.get('type')=='page']; homes=sum(t.get('url','').startswith('chrome-extension://'+ext+'/home.html') for t in pages); panels=sum(t.get('url','').startswith('chrome-extension://'+ext+'/sidepanel.html') for t in pages); dapps=sum(t.get('url','').rstrip('/')==wanted for t in pages); products=sum(t.get('url','').startswith('chrome-extension://'+ext+'/') for t in pages); print(homes,panels,dapps,products)"
128
+ }
129
+
130
+ ensure_dapp_tab() {
131
+ [ -n "$(find_dapp_id)" ] && return 0
132
+ local encoded
133
+ encoded="$(DAPP_URL="$DAPP_URL" python3 -c "import os,urllib.parse; print(urllib.parse.quote(os.environ['DAPP_URL'], safe=''))")"
134
+ curl -fsS -X PUT "http://127.0.0.1:${CDP_PORT}/json/new?${encoded}" >/dev/null
135
+ }
136
+
137
+ ensure_home_page() {
138
+ local ext_id encoded home_ids first
139
+ ext_id="$(resolve_ext_id)"
140
+ [ -n "$ext_id" ] || { echo "FAIL: could not resolve extension id for CDP ${CDP_PORT}" >&2; return 1; }
141
+ home_ids="$(json_list | EXT_ID="$ext_id" python3 -c "import json,os,sys; d=json.load(sys.stdin); ext=os.environ.get('EXT_ID',''); print('\\n'.join(t.get('id','') for t in d if t.get('type')=='page' and t.get('url','').startswith('chrome-extension://'+ext+'/home.html')))" )"
142
+ if [ -n "$home_ids" ]; then
143
+ first=1
144
+ while IFS= read -r target_id; do
145
+ [ -n "$target_id" ] || continue
146
+ if [ "$first" -eq 1 ]; then first=0; continue; fi
147
+ curl -fsS "http://127.0.0.1:${CDP_PORT}/json/close/${target_id}" >/dev/null
148
+ done <<< "$home_ids"
149
+ return 0
150
+ fi
151
+ encoded="$(EXT_ID="$ext_id" python3 -c "import os,urllib.parse; print(urllib.parse.quote('chrome-extension://'+os.environ['EXT_ID']+'/home.html#/', safe=''))")"
152
+ curl -fsS -X PUT "http://127.0.0.1:${CDP_PORT}/json/new?${encoded}" >/dev/null
109
153
  }
110
154
 
111
155
  resolve_ext_id() {
@@ -172,6 +216,20 @@ wait_for_sidepanel() {
172
216
  done
173
217
  }
174
218
 
219
+ wait_for_surface_invariant() {
220
+ local expected="$1" start deadline now home_count panel_count dapp_count product_count
221
+ start="$(now_ms)"
222
+ deadline=$((start + SETTLE_MS))
223
+ while :; do
224
+ read -r home_count panel_count dapp_count product_count <<< "$(surface_counts)"
225
+ if [ "$expected" = "fullscreen" ] && [ "$home_count" -eq 1 ] && [ "$panel_count" -eq 0 ] && [ "$product_count" -eq 1 ]; then return 0; fi
226
+ if [ "$expected" = "sidepanel" ] && [ "$home_count" -eq 0 ] && [ "$panel_count" -eq 1 ] && [ "$dapp_count" -eq 1 ] && [ "$product_count" -eq 1 ]; then return 0; fi
227
+ now="$(now_ms)"
228
+ [ "$now" -lt "$deadline" ] || return 1
229
+ sleep 0.2
230
+ done
231
+ }
232
+
175
233
  print_targets() {
176
234
  json_list | python3 -c "import json,sys; d=json.load(sys.stdin); [print(f' {t.get(\"type\",\"?\")[:18]:18s} {t.get(\"url\",\"\")[:100]}') for t in d]"
177
235
  }
@@ -179,7 +237,7 @@ print_targets() {
179
237
  activate_dapp_tab() {
180
238
  local target_id
181
239
  target_id="$(
182
- json_list | python3 -c "import json,sys; d=json.load(sys.stdin); pages=[t for t in d if t.get('type')=='page']; target=next((t for t in pages if t.get('url','').startswith('http://') or t.get('url','').startswith('https://')), None); print(target.get('id','') if target else '')"
240
+ json_list | DAPP_URL="$DAPP_URL" python3 -c "import json,os,sys; d=json.load(sys.stdin); wanted=os.environ['DAPP_URL'].rstrip('/'); pages=[t for t in d if t.get('type')=='page']; target=next((t for t in pages if t.get('url','').rstrip('/')==wanted), None); print(target.get('id','') if target else '')"
183
241
  )"
184
242
  if [ -n "$target_id" ] && focus_browser; then
185
243
  curl -s "http://127.0.0.1:${CDP_PORT}/json/activate/${target_id}" >/dev/null || true
@@ -191,7 +249,7 @@ cleanup_sidepanel_tabs() {
191
249
  ext_id="$(resolve_ext_id)"
192
250
  [ -n "$ext_id" ] || return 0
193
251
  close_ids="$(
194
- json_list | EXT_ID="$ext_id" python3 -c "import json,os,sys; d=json.load(sys.stdin); ext=os.environ.get('EXT_ID',''); has_panel=any(t.get('type')=='page' and t.get('url','').startswith('chrome-extension://'+ext+'/') and 'sidepanel.html' in t.get('url','') for t in d); has_dapp=any(t.get('type')=='page' and (t.get('url','').startswith('http://') or t.get('url','').startswith('https://')) for t in d); print('\\n'.join(t.get('id','') for t in d if has_panel and has_dapp and t.get('type')=='page' and t.get('url','').startswith('chrome-extension://'+ext+'/') and 'home.html' in t.get('url','')))"
252
+ json_list | EXT_ID="$ext_id" DAPP_URL="$DAPP_URL" python3 -c "import json,os,sys; d=json.load(sys.stdin); ext=os.environ.get('EXT_ID',''); wanted=os.environ['DAPP_URL'].rstrip('/'); has_panel=any(t.get('type')=='page' and t.get('url','').startswith('chrome-extension://'+ext+'/sidepanel.html') for t in d); has_dapp=any(t.get('type')=='page' and t.get('url','').rstrip('/')==wanted for t in d); print('\\n'.join(t.get('id','') for t in d if has_panel and has_dapp and t.get('type')=='page' and t.get('url','').startswith('chrome-extension://'+ext+'/home.html')))"
195
253
  )"
196
254
  if [ -n "$close_ids" ]; then
197
255
  while IFS= read -r target_id; do
@@ -202,6 +260,18 @@ cleanup_sidepanel_tabs() {
202
260
  activate_dapp_tab
203
261
  }
204
262
 
263
+ dedupe_sidepanel_tabs() {
264
+ local ext_id sidepanel_ids first
265
+ ext_id="$(resolve_ext_id)"
266
+ sidepanel_ids="$(json_list | EXT_ID="$ext_id" python3 -c "import json,os,sys; d=json.load(sys.stdin); ext=os.environ.get('EXT_ID',''); print('\\n'.join(t.get('id','') for t in d if t.get('type')=='page' and t.get('url','').startswith('chrome-extension://'+ext+'/sidepanel.html')))" )"
267
+ first=1
268
+ while IFS= read -r target_id; do
269
+ [ -n "$target_id" ] || continue
270
+ if [ "$first" -eq 1 ]; then first=0; continue; fi
271
+ curl -fsS "http://127.0.0.1:${CDP_PORT}/json/close/${target_id}" >/dev/null
272
+ done <<< "$sidepanel_ids"
273
+ }
274
+
205
275
  status_sidepanel() {
206
276
  local sp
207
277
  sp="$(find_sidepanel_id)"
@@ -217,15 +287,22 @@ close_sidepanel() {
217
287
  sp="$(find_sidepanel_id)"
218
288
  if [ -z "$sp" ]; then
219
289
  echo "[sidepanel] already closed"
290
+ ensure_home_page
291
+ wait_for_surface_invariant fullscreen || { echo "FAIL: fullscreen surface did not converge to one Home target" >&2; return 1; }
220
292
  return 0
221
293
  fi
222
294
  curl -s "http://127.0.0.1:${CDP_PORT}/json/close/${sp}" >/dev/null
295
+ ensure_home_page
296
+ wait_for_surface_invariant fullscreen || { echo "FAIL: fullscreen surface did not converge to one Home target" >&2; return 1; }
223
297
  echo "[sidepanel] closed target ${sp}"
224
298
  }
225
299
 
226
300
  open_sidepanel() {
301
+ ensure_dapp_tab
227
302
  if [ -n "$(find_sidepanel_id)" ]; then
303
+ dedupe_sidepanel_tabs
228
304
  cleanup_sidepanel_tabs
305
+ wait_for_surface_invariant sidepanel || { echo "FAIL: sidepanel surface did not converge to one panel and a dapp host" >&2; return 1; }
229
306
  echo "[sidepanel] already open"
230
307
  return 0
231
308
  fi
@@ -239,13 +316,54 @@ open_sidepanel() {
239
316
  exit 4
240
317
  fi
241
318
 
242
- CDP_PORT="$CDP_PORT" EXT_ID="$ext_id" SCRIPT_DIR="$SCRIPT_DIR" node <<'NODE'
243
- const { chromium } = require('playwright');
319
+ CDP_PORT="$CDP_PORT" EXT_ID="$ext_id" SCRIPT_DIR="$SCRIPT_DIR" node <<'NODE'
320
+ const path = require('node:path');
321
+ const {
322
+ connectBrowserViaCdp,
323
+ evaluatePageViaCdp,
324
+ } = require(path.join(process.env.SCRIPT_DIR, 'lib/playwright-cdp.cjs'));
325
+
326
+ async function clickVisibleSafeHit(page, selector, successSelector, timeoutMs) {
327
+ const deadline = Date.now() + timeoutMs;
328
+ while (Date.now() < deadline) {
329
+ if (await page.locator(successSelector).isVisible().catch(() => false)) return;
330
+ const candidate = page.locator(selector);
331
+ const count = await candidate.count();
332
+ if (count > 1) throw new Error(`expected one visible ${selector} control, found ${count}`);
333
+ if (count === 1 && await candidate.isVisible().catch(() => false)) {
334
+ try {
335
+ const point = await evaluatePageViaCdp(page, (candidateSelector) => {
336
+ const matches = [...document.querySelectorAll(candidateSelector)];
337
+ if (matches.length !== 1) return null;
338
+ const [element] = matches;
339
+ const rect = element.getBoundingClientRect();
340
+ const points = [
341
+ [rect.left + rect.width / 2, rect.top + rect.height / 2],
342
+ [rect.left + rect.width / 4, rect.top + rect.height / 2],
343
+ [rect.left + rect.width * 3 / 4, rect.top + rect.height / 2],
344
+ [rect.left + rect.width / 2, rect.top + rect.height / 4],
345
+ [rect.left + rect.width / 2, rect.top + rect.height * 3 / 4],
346
+ ];
347
+ const safe = points.find(([x, y]) => {
348
+ const hit = document.elementFromPoint(x, y);
349
+ return hit === element || Boolean(hit && element.contains(hit));
350
+ });
351
+ return safe ? { x: safe[0], y: safe[1] } : null;
352
+ }, selector);
353
+ if (point) await page.mouse.click(point.x, point.y);
354
+ } catch (error) {
355
+ if (!/detached|not attached|execution context was destroyed/iu.test(String(error?.message || error))) throw error;
356
+ }
357
+ }
358
+ await page.waitForTimeout(50);
359
+ }
360
+ throw new Error(`visible ${selector} control did not activate within ${timeoutMs}ms`);
361
+ }
244
362
 
245
363
  (async () => {
246
364
  const port = process.env.CDP_PORT;
247
365
  const extId = process.env.EXT_ID;
248
- const browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`);
366
+ const browser = await connectBrowserViaCdp(`http://127.0.0.1:${port}`);
249
367
  const context = browser.contexts()[0];
250
368
  const page = await context.newPage();
251
369
  try {
@@ -256,11 +374,13 @@ const { chromium } = require('playwright');
256
374
  if (process.env.MM_HARNESS_FOCUS_BROWSER === '1') {
257
375
  await page.bringToFront();
258
376
  }
259
- const menu = page.locator('[data-testid="account-options-menu-button"]');
260
- await menu.waitFor({ state: 'visible', timeout: 15000 });
261
- await menu.click({ timeout: 5000 });
377
+ await clickVisibleSafeHit(
378
+ page,
379
+ '[data-testid="account-options-menu-button"]',
380
+ '[data-testid="global-menu-toggle-view"]',
381
+ 5000,
382
+ );
262
383
  const toggle = page.locator('[data-testid="global-menu-toggle-view"]');
263
- await toggle.waitFor({ state: 'visible', timeout: 10000 });
264
384
  await toggle.click({ timeout: 5000 });
265
385
  } finally {
266
386
  if (!page.isClosed()) await page.close();
@@ -277,7 +397,9 @@ const { chromium } = require('playwright');
277
397
  NODE
278
398
 
279
399
  if wait_for_sidepanel; then
400
+ dedupe_sidepanel_tabs
280
401
  cleanup_sidepanel_tabs
402
+ wait_for_surface_invariant sidepanel || { echo "FAIL: sidepanel surface did not converge to one panel and a dapp host" >&2; return 1; }
281
403
  echo "[sidepanel] opened"
282
404
  return 0
283
405
  fi
@@ -89,6 +89,70 @@ function assertAndroidReleaseArtifactRuntime(identity, deps = defaultDeps) {
89
89
  const adb = resolveAdb(deps);
90
90
  assertInstalledAndroidReleaseArtifact(identity, deps, adb);
91
91
  }
92
+ function readAndroidNativeRuntimeIdentity(identity, deps = defaultDeps) {
93
+ assertAndroidReleaseArtifactRuntime(identity, deps);
94
+ const adb = resolveAdb(deps);
95
+ const buildFingerprint = runChecked(deps, adb, [
96
+ "-s",
97
+ identity.deviceSerial,
98
+ "shell",
99
+ "getprop",
100
+ "ro.build.fingerprint"
101
+ ]).trim();
102
+ if (!/^[A-Za-z0-9._+:/=-]{1,512}$/u.test(buildFingerprint)) {
103
+ throw new Error(`Android device ${identity.deviceSerial} returned an invalid build fingerprint.`);
104
+ }
105
+ const bootId = runChecked(deps, adb, [
106
+ "-s",
107
+ identity.deviceSerial,
108
+ "shell",
109
+ "cat",
110
+ "/proc/sys/kernel/random/boot_id"
111
+ ]).trim().toLowerCase();
112
+ if (!/^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/u.test(bootId)) {
113
+ throw new Error(`Android device ${identity.deviceSerial} returned an invalid boot ID.`);
114
+ }
115
+ const pids = runChecked(deps, adb, [
116
+ "-s",
117
+ identity.deviceSerial,
118
+ "shell",
119
+ "pidof",
120
+ identity.packageId
121
+ ]).trim().split(/\s+/u).filter(Boolean);
122
+ if (pids.length !== 1 || !/^\d+$/u.test(pids[0])) {
123
+ throw new Error(`Android package ${identity.packageId} must have exactly one running app process on ${identity.deviceSerial}.`);
124
+ }
125
+ const pid = pids[0];
126
+ const processStat = runChecked(deps, adb, [
127
+ "-s",
128
+ identity.deviceSerial,
129
+ "shell",
130
+ "cat",
131
+ `/proc/${pid}/stat`
132
+ ]).trim();
133
+ const close = processStat.lastIndexOf(") ");
134
+ const processFields = close === -1 ? [] : processStat.slice(close + 2).trim().split(/\s+/u);
135
+ const startTimeTicks = processFields[19] ?? "";
136
+ if (!/^\d+$/u.test(startTimeTicks)) {
137
+ throw new Error(`Android package ${identity.packageId} returned an invalid process start time on ${identity.deviceSerial}.`);
138
+ }
139
+ const payload = {
140
+ schemaVersion: 1,
141
+ platform: "android",
142
+ deviceSerial: identity.deviceSerial,
143
+ buildFingerprint,
144
+ bootId,
145
+ packageId: identity.packageId,
146
+ versionName: identity.versionName,
147
+ versionCode: identity.versionCode,
148
+ sha256: identity.sha256,
149
+ appProcess: { pid, startTimeTicks }
150
+ };
151
+ return {
152
+ ...payload,
153
+ runtimeSessionId: createHash("sha256").update(JSON.stringify(payload)).digest("hex")
154
+ };
155
+ }
92
156
  async function relaunchAndroidReleaseArtifactRuntime(identity, deps = defaultDeps) {
93
157
  assertAndroidReleaseArtifactRuntime(identity, deps);
94
158
  const adb = resolveAdb(deps);
@@ -291,5 +355,6 @@ const defaultDeps = {
291
355
  export {
292
356
  assertAndroidReleaseArtifactRuntime,
293
357
  installAndroidReleaseArtifact,
358
+ readAndroidNativeRuntimeIdentity,
294
359
  relaunchAndroidReleaseArtifactRuntime
295
360
  };
package/dist/adapters.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import http from "node:http";
2
+ import { createHash } from "node:crypto";
2
3
  import { execFile } from "node:child_process";
3
4
  import { promisify } from "node:util";
4
5
  import { compatibilityMode, fixtureSummary, repoShape } from "./doctor.js";
@@ -8,6 +9,7 @@ import {
8
9
  import { withExtensionPage } from "../library/actions/extension/platform/cdp.mjs";
9
10
  import { bridgeCommand, evalAsync, evalSync, MOBILE_BRIDGE_ERROR_CODES, selectBridgeStatusEntry, simulatorScreenshot } from "../library/actions/mobile/platform/bridge.mjs";
10
11
  import { observeNativeUi } from "../library/actions/mobile/platform/observe-ui.mjs";
12
+ import { nativeAgentDeviceStateDir } from "../library/actions/mobile/platform/native-session-name.mjs";
11
13
  import { resolveMobileToolPath } from "../library/actions/mobile/platform/tool-paths.mjs";
12
14
  import { resolveWalletImportCredentials, validateWalletImportOptions } from "../library/actions/shared/wallet/import-source.mjs";
13
15
  const execFileAsync = promisify(execFile);
@@ -37,13 +39,13 @@ function simpleAdapter(action, executor) {
37
39
  }
38
40
  const LIVE_ONLY_ACTIONS = /* @__PURE__ */ new Set([
39
41
  "metamask.assets.read_visible_state",
42
+ "metamask.assets.open_details",
40
43
  "metamask.assets.read_details",
41
44
  "metamask.assets.verify_sorting",
42
45
  "metamask.assets.set_token_visibility",
43
46
  "metamask.assets.import_custom_token",
44
47
  "metamask.assets.prepare_send",
45
48
  "metamask.assets.review_send",
46
- "metamask.assets.finish_send",
47
49
  "metamask.assets.set_custom_gas",
48
50
  "metamask.assets.select_network_scope",
49
51
  "metamask.wallet.lock",
@@ -52,6 +54,7 @@ const LIVE_ONLY_ACTIONS = /* @__PURE__ */ new Set([
52
54
  "metamask.networks.add_chainlist",
53
55
  "metamask.networks.remove_custom",
54
56
  "metamask.perps.read_visible_state",
57
+ "metamask.perps.search_markets",
55
58
  "metamask.perps.compare_provider_market",
56
59
  "metamask.perps.read_eligibility",
57
60
  "metamask.perps.select_market_filter",
@@ -63,6 +66,7 @@ const LIVE_ONLY_ACTIONS = /* @__PURE__ */ new Set([
63
66
  "metamask.perps.edit_margin",
64
67
  "metamask.perps.update_position_tpsl",
65
68
  "metamask.perps.read_funds_confirmation",
69
+ "metamask.perps.assert_visible_consistency",
66
70
  "metamask.swap_bridge.read_visible_state",
67
71
  "metamask.swap_bridge.select_assets",
68
72
  "metamask.swap_bridge.set_amount",
@@ -161,7 +165,7 @@ async function semanticResult(platform, action, node, context, forceLive = false
161
165
  ...output,
162
166
  credentialSource: credentials.source,
163
167
  credentialSourceName: credentials.sourceName,
164
- expectedAddress: credentials.expectedAddress
168
+ expectedIdentitySha256: createHash("sha256").update(credentials.expectedAddress).digest("hex")
165
169
  }
166
170
  };
167
171
  }
@@ -250,45 +254,50 @@ function createMetaMaskSemanticAdapters(platform, declaredCustomActions = [], pr
250
254
  const bundledActions = [
251
255
  ...walletActions,
252
256
  "metamask.assets.read_visible_state",
253
- ...platform === "extension" ? ["metamask.assets.read_details"] : [],
254
- ...platform === "extension" ? ["metamask.assets.verify_sorting"] : [],
255
- ...platform === "extension" ? ["metamask.assets.set_token_visibility"] : [],
256
- ...platform === "extension" ? ["metamask.assets.import_custom_token"] : [],
257
+ "metamask.assets.open_details",
258
+ "metamask.assets.read_details",
259
+ "metamask.assets.verify_sorting",
260
+ "metamask.assets.set_token_visibility",
261
+ "metamask.assets.import_custom_token",
257
262
  ...platform === "extension" ? [
258
263
  "metamask.assets.prepare_send",
259
264
  "metamask.assets.review_send",
260
- "metamask.assets.finish_send",
261
265
  "metamask.assets.set_custom_gas",
262
266
  "metamask.assets.select_network_scope"
263
267
  ] : [],
264
- ...platform === "extension" ? ["metamask.wallet.lock"] : [],
265
- ...platform === "extension" ? ["metamask.networks.read_visible_state"] : [],
266
- ...platform === "extension" ? ["metamask.networks.add_custom", "metamask.networks.add_chainlist", "metamask.networks.remove_custom"] : [],
268
+ "metamask.wallet.lock",
269
+ "metamask.networks.read_visible_state",
270
+ "metamask.networks.add_custom",
271
+ ...platform === "extension" ? ["metamask.networks.add_chainlist"] : [],
272
+ "metamask.networks.remove_custom",
267
273
  "metamask.perps.read_visible_state",
274
+ "metamask.perps.search_markets",
275
+ "metamask.perps.set_market_favorite",
268
276
  ...platform === "extension" ? ["metamask.perps.compare_provider_market"] : [],
269
277
  ...platform === "extension" ? ["metamask.perps.read_eligibility"] : [],
270
278
  ...platform === "extension" ? [
271
279
  "metamask.perps.open_position_action",
272
280
  "metamask.perps.edit_margin",
273
- "metamask.perps.update_position_tpsl"
281
+ "metamask.perps.update_position_tpsl",
282
+ "metamask.perps.assert_visible_consistency"
274
283
  ] : [],
275
284
  "metamask.swap_bridge.read_visible_state",
276
285
  "metamask.swap_bridge.select_assets",
277
286
  "metamask.swap_bridge.set_amount",
278
- ...platform === "extension" ? ["metamask.swap_bridge.set_max_amount"] : [],
279
- ...platform === "extension" ? ["metamask.swap_bridge.set_slippage"] : [],
280
- ...platform === "extension" ? ["metamask.swap_bridge.submit_transaction"] : [],
287
+ "metamask.swap_bridge.set_max_amount",
288
+ "metamask.swap_bridge.set_slippage",
289
+ ...platform === "mobile" ? ["metamask.swap_bridge.submit_transaction"] : [],
281
290
  ...platform === "extension" ? ["metamask.settings.set_basic_functionality"] : [],
282
291
  "metamask.perps.read_positions",
283
292
  "metamask.perps.ensure_positions",
284
293
  "metamask.perps.assert_positions",
285
294
  "metamask.perps.place_order",
286
- "metamask.perps.close_positions",
295
+ ...platform === "mobile" || platform === "core" ? ["metamask.perps.close_positions"] : [],
287
296
  "metamask.perps.read_orders",
288
297
  "metamask.perps.close_orders",
289
298
  "metamask.perps.ensure_orders",
290
299
  "metamask.perps.assert_orders",
291
- "metamask.perps.start_state",
300
+ ...platform === "mobile" || platform === "core" ? ["metamask.perps.start_state"] : [],
292
301
  "metamask.perps.teardown_state",
293
302
  "metamask.perps.capture_performance",
294
303
  ...platform === "extension" || platform === "core" ? ["metamask.perps.read_snapshot"] : [],
@@ -900,9 +909,10 @@ async function executeNativeProviderAction(action, node, context, createTranspor
900
909
  }
901
910
  const app = platform === "android" ? context.env.ANDROID_PACKAGE_ID ?? process.env.ANDROID_PACKAGE_ID ?? "io.metamask" : context.env.IOS_BUNDLE_ID ?? process.env.IOS_BUNDLE_ID ?? "io.metamask.MetaMask";
902
911
  const session = `mm-harness-${process.pid}-${context.nodeId}`.replace(/[^a-zA-Z0-9._-]/gu, "-");
912
+ const stateDir = nativeAgentDeviceStateDir(context.env ?? {}, context.projectRoot, String(device));
903
913
  let transport;
904
914
  try {
905
- transport = await createTransport({ platform, device, app, session });
915
+ transport = await createTransport({ platform, device, app, session, stateDir });
906
916
  } catch (error) {
907
917
  throw new Error(
908
918
  `${action} could not start the MetaMask native UI provider. Next: npm install -g @deeeed/metamask-harness@latest`,
@@ -31,6 +31,12 @@ const RECIPE_RUNTIME = {
31
31
  "--source-digest": value(),
32
32
  "--approve-plan": value()
33
33
  };
34
+ const RUN_FUNDING = {
35
+ "--funding-request": value(),
36
+ "--funding-preflight": value(),
37
+ "--funding-release-proof": value(),
38
+ "--funding-balance-proof": value()
39
+ };
34
40
  const RUNWAY = {
35
41
  "--branch": value(),
36
42
  "--default-branch": value(),
@@ -158,10 +164,11 @@ const PUBLIC_COMMAND_CONTRACTS = {
158
164
  variadic: { label: "key=value", pattern: /^[^=\s]+=.*/u }
159
165
  },
160
166
  run: {
161
- options: options(HELP, JSON, JSON_STREAM, TARGET, ADAPTER, MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, RECIPE_RUNTIME, {
167
+ options: options(HELP, JSON, JSON_STREAM, TARGET, ADAPTER, MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, RECIPE_RUNTIME, RUN_FUNDING, {
162
168
  "--list": bool(),
163
169
  "--plan": bool(),
164
- "--describe": bool()
170
+ "--describe": bool(),
171
+ "--hud": value(["auto", "show", "hide"])
165
172
  }),
166
173
  positionals: [{ label: "recipe" }],
167
174
  minimumPositionals: 1,
@@ -232,6 +239,7 @@ const PUBLIC_COMMAND_CONTRACTS = {
232
239
  "--verify": bool(),
233
240
  "--runway": bool(),
234
241
  "--watch": bool(),
242
+ "--surface": value(["fullscreen", "sidepanel"]),
235
243
  "--sidepanel": bool(),
236
244
  "--fullscreen": bool(),
237
245
  "--url": value(),
@@ -208,16 +208,19 @@ async function handleCall(argv) {
208
208
  const artifactsDir = optionString(options, "artifactsDir") ?? defaultCallArtifactsDir(target, resolvedAction, args);
209
209
  recordCommandEvidence(artifactsDir);
210
210
  const requestedRuntimeOptions = runtimeOptionsFromCli(options);
211
+ const inheritedSource = process.env.FARMSLOT_RECIPE_SOURCE_TRUST || process.env.FARMSLOT_RECIPE_SOURCE_KIND || process.env.FARMSLOT_RECIPE_SOURCE_NAME || process.env.FARMSLOT_RECIPE_SOURCE_DIGEST;
211
212
  const callRuntimeOptions = {
212
213
  ...requestedRuntimeOptions,
213
214
  ...librarySources ? { librarySources } : {},
214
215
  autoHud: false,
215
216
  suppressLibraryResolutionLogs: true,
216
217
  stdoutIsMachineContract: json,
217
- source: requestedRuntimeOptions.source ?? {
218
- kind: "operator",
219
- trust: "trusted",
220
- name: "mm-harness call"
218
+ ...requestedRuntimeOptions.source ? { source: requestedRuntimeOptions.source } : inheritedSource ? {} : {
219
+ source: {
220
+ kind: "operator",
221
+ trust: "trusted",
222
+ name: "mm-harness call"
223
+ }
221
224
  }
222
225
  };
223
226
  let preflightedExecution;