@deeeed/metamask-harness 0.79.0 → 0.81.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 (252) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +1 -1
  3. package/adapters/extension/ensure-browser.sh +2 -2
  4. package/adapters/extension/inject.mjs +0 -1
  5. package/adapters/extension/launch-browser.cjs +39 -613
  6. package/adapters/extension/readiness.mjs +2 -5
  7. package/adapters/extension/reattach.sh +2 -2
  8. package/adapters/extension/stamp-runtime-title.cjs +1 -1
  9. package/adapters/manifest.json +16 -64
  10. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +57 -1358
  11. package/adapters/mobile/bridge-runtime/teaching.cjs +20 -0
  12. package/adapters/mobile/inject.sh +16 -0
  13. package/adapters/mobile/launch-console-forwarder.cjs +1 -2
  14. package/adapters/mobile/open-device.sh +2 -2
  15. package/adapters/mobile/reload-app.mjs +4 -4
  16. package/adapters/mobile/reset-app-data.sh +1 -1
  17. package/adapters/mobile/start-console-forwarder.sh +3 -2
  18. package/adapters/mobile/start-metro.sh +17 -12
  19. package/adapters/mobile/wait-for-bridge.cjs +2 -2
  20. package/adapters/shared/harness-source-fingerprint.mjs +13 -8
  21. package/adapters/shared/open-debug.mjs +2 -2
  22. package/adapters/shared/sync-wallet-fixture.sh +8 -5
  23. package/adapters/terminal/cleanup.mjs +2 -1
  24. package/adapters/terminal/inject.mjs +2 -1
  25. package/adapters/terminal/launch.mjs +17 -13
  26. package/adapters/terminal/lib/mainnet-guard.mjs +9 -0
  27. package/adapters/terminal/lib/readiness.mjs +20 -12
  28. package/adapters/terminal/policy.mjs +35 -0
  29. package/adapters/terminal/verify.mjs +10 -3
  30. package/adapters/terminal/wallet-host.mjs +43 -225
  31. package/bin/mm-harness +9 -28
  32. package/dist/adapters/checkout-files.js +16 -0
  33. package/dist/adapters/console-capture.js +14 -1
  34. package/dist/adapters/core/actions.js +8 -4
  35. package/dist/adapters/core/overlay.js +1 -1
  36. package/dist/adapters/core/readiness.js +28 -1
  37. package/dist/adapters/core/run.js +2 -2
  38. package/dist/adapters/core/surface.js +16 -1
  39. package/dist/adapters/extension/actions.js +6 -2
  40. package/dist/adapters/extension/browser-pid.js +56 -0
  41. package/dist/adapters/extension/console-capture.js +1 -1
  42. package/dist/adapters/extension/doctor-checks.js +1 -1
  43. package/dist/adapters/extension/ensure-ready.js +5 -4
  44. package/dist/adapters/extension/extension-id.js +1 -1
  45. package/dist/adapters/extension/feature-flags.js +1 -1
  46. package/dist/adapters/extension/fixtures.js +1 -2
  47. package/dist/adapters/extension/launch.js +4 -5
  48. package/dist/adapters/extension/launched-browser.js +1 -2
  49. package/dist/adapters/extension/network-observer.js +14 -1
  50. package/dist/adapters/extension/overlay.js +1 -1
  51. package/dist/adapters/extension/product-config.js +1 -1
  52. package/dist/adapters/extension/readiness.js +1 -1
  53. package/dist/adapters/extension/reload.js +1 -1
  54. package/dist/adapters/extension/run.js +135 -14
  55. package/dist/adapters/extension/runtime-decision.js +2 -1
  56. package/dist/adapters/extension/runtime.js +9 -5
  57. package/dist/adapters/extension/surface.js +15 -2
  58. package/dist/adapters/extension/verify.js +2 -2
  59. package/dist/adapters/harness-freshness.js +2 -1
  60. package/dist/adapters/mobile/actions.js +4 -1
  61. package/dist/adapters/mobile/android-gfxinfo-performance.js +1 -3
  62. package/dist/adapters/mobile/app-identity.js +10 -0
  63. package/dist/adapters/mobile/doctor-checks.js +1 -1
  64. package/dist/adapters/mobile/fixtures.js +1 -1
  65. package/dist/adapters/mobile/launch.js +27 -28
  66. package/dist/adapters/mobile/lifecycle.js +1 -1
  67. package/dist/adapters/mobile/metro-env.js +9 -59
  68. package/dist/adapters/mobile/network-observer.js +68 -0
  69. package/dist/adapters/mobile/performance-observer.js +2 -4
  70. package/dist/adapters/mobile/prepare-target.js +1 -1
  71. package/dist/adapters/mobile/prepare.js +3 -4
  72. package/dist/adapters/mobile/provision.js +1 -1
  73. package/dist/adapters/mobile/readiness.js +1 -1
  74. package/dist/adapters/mobile/recording-target.js +15 -0
  75. package/dist/adapters/mobile/release-artifact-state.js +1 -1
  76. package/dist/adapters/mobile/reload.js +1 -1
  77. package/dist/adapters/mobile/run.js +39 -15
  78. package/dist/adapters/mobile/runtime-decision.js +2 -4
  79. package/dist/adapters/mobile/source-freshness.js +22 -126
  80. package/dist/adapters/mobile/surface.js +26 -3
  81. package/dist/adapters/mobile/ui.js +16 -5
  82. package/dist/adapters/mobile/verify.js +2 -3
  83. package/dist/adapters/performance/cdp-trace.js +1 -1
  84. package/dist/adapters/performance/device-platform.js +1 -1
  85. package/dist/adapters/perps-order-inputs.js +36 -0
  86. package/dist/adapters/proof-failure.js +11 -0
  87. package/dist/adapters/slot-ports.js +1 -1
  88. package/dist/adapters/surface.js +1 -0
  89. package/dist/adapters/terminal/actions.js +4 -1
  90. package/dist/adapters/terminal/console-capture.js +1 -1
  91. package/dist/adapters/terminal/doctor-checks.js +2 -0
  92. package/dist/adapters/terminal/launch.js +4 -1
  93. package/dist/adapters/terminal/overlay.js +8 -1
  94. package/dist/adapters/terminal/run.js +33 -5
  95. package/dist/adapters/terminal/surface.js +9 -1
  96. package/dist/adapters.js +27 -7
  97. package/dist/advisor-policy.js +1 -1
  98. package/dist/checkout-view.js +1 -2
  99. package/dist/cli.js +59 -21
  100. package/dist/command-contract.js +1 -26
  101. package/dist/commands/check.js +33 -10
  102. package/dist/commands/checklist.js +1 -1
  103. package/dist/commands/completion-candidates.js +6 -14
  104. package/dist/commands/config.js +2 -2
  105. package/dist/commands/core-readiness.js +12 -7
  106. package/dist/commands/debug.js +3 -3
  107. package/dist/commands/device-target.js +5 -5
  108. package/dist/commands/doctor.js +2 -14
  109. package/dist/commands/domain.js +2 -2
  110. package/dist/commands/ensure-ready.js +1 -7
  111. package/dist/commands/fixtures.js +5 -6
  112. package/dist/commands/help.js +3 -4
  113. package/dist/commands/launch/device.js +10 -17
  114. package/dist/commands/launch/extension.js +2 -8
  115. package/dist/commands/launch/mobile.js +1 -1
  116. package/dist/commands/logs.js +3 -3
  117. package/dist/commands/mobile-device-view.js +4 -2
  118. package/dist/commands/overlay.js +110 -0
  119. package/dist/commands/pin.js +1 -1
  120. package/dist/commands/pr-body.js +2 -2
  121. package/dist/commands/prepare.js +4 -9
  122. package/dist/commands/provision.js +3 -12
  123. package/dist/commands/recipe-advice.js +1 -2
  124. package/dist/commands/recipe-quality.js +2 -3
  125. package/dist/commands/resolve-extension.js +1 -7
  126. package/dist/commands/review.js +4 -4
  127. package/dist/commands/runtime-decision.js +1 -6
  128. package/dist/commands/runtime-health.js +1 -7
  129. package/dist/commands/runtime-launch.js +1 -13
  130. package/dist/commands/self-test.js +5 -8
  131. package/dist/commands/status-watch.js +1 -2
  132. package/dist/commands/status.js +2 -5
  133. package/dist/commands/task-init.js +2 -3
  134. package/dist/commands/tutorial.js +2 -2
  135. package/dist/commands/update.js +1 -1
  136. package/dist/completions-cache.js +1 -1
  137. package/dist/console-classifier.js +19 -0
  138. package/dist/doctor.js +2 -3
  139. package/dist/engine.js +57 -0
  140. package/dist/funding-execution-context.js +96 -1
  141. package/dist/host.js +32 -0
  142. package/dist/index.js +1 -6
  143. package/dist/live-tsx.js +49 -0
  144. package/dist/mm-harness-cli.js +4 -6
  145. package/dist/paths.js +34 -64
  146. package/dist/performance-observation.js +3 -5
  147. package/dist/progress.js +1 -1
  148. package/dist/recipe-assistance-context.js +6 -9
  149. package/dist/recipe-security.js +2 -59
  150. package/dist/review/knowledge.js +2 -2
  151. package/dist/runner.js +7 -4
  152. package/dist/runtime-context.js +1 -1
  153. package/dist/task-view.js +2 -3
  154. package/docs/CONTRIBUTING.md +5 -3
  155. package/docs/RECIPES.md +31 -3
  156. package/docs/cli-contract.md +2 -2
  157. package/docs/feature-guard.md +90 -0
  158. package/library/README.md +3 -1
  159. package/library/actions/core/perps/_controller.mjs +153 -134
  160. package/library/actions/core/perps/_rewards.mjs +186 -0
  161. package/library/actions/core/perps/close_positions.mjs +1 -1
  162. package/library/actions/core/perps/place_order.mjs +167 -45
  163. package/library/actions/core/perps/quote_fees.mjs +61 -0
  164. package/library/actions/core/wallet/list_accounts.mjs +14 -57
  165. package/library/actions/extension/feature_flags/state.mjs +1 -1
  166. package/library/actions/extension/perps/close_visible_position.mjs +5 -0
  167. package/library/actions/extension/perps/edit_margin.mjs +4 -0
  168. package/library/actions/extension/perps/mutation-receipt.mjs +95 -3
  169. package/library/actions/extension/perps/perps.mjs +320 -3
  170. package/library/actions/extension/perps/place_order.mjs +3 -1
  171. package/library/actions/extension/perps/update_position_tpsl.mjs +4 -0
  172. package/library/actions/extension/platform/cdp.mjs +73 -11
  173. package/library/actions/harness-exports.mjs +1 -6
  174. package/library/actions/mobile/app/network-control.mjs +6 -8
  175. package/library/actions/mobile/perps/perps.mjs +216 -11
  176. package/library/actions/mobile/perps/place_order.mjs +3 -1
  177. package/library/actions/mobile/perps/prepare_local_snapshot_endpoint.mjs +1 -1
  178. package/library/actions/mobile/platform/bridge.mjs +8 -8
  179. package/library/actions/mobile/platform/native-session-name.mjs +21 -1
  180. package/library/actions/mobile/platform/native-session.mjs +3 -4
  181. package/library/actions/mobile/platform/observe-ui.mjs +12 -6
  182. package/library/actions/mobile/ui/native-navigation.mjs +4 -3
  183. package/library/actions/mobile/wallet/native-ui.mjs +3 -2
  184. package/library/actions/shared/analytics/_adapter.mjs +1 -1
  185. package/library/actions/shared/console-findings.mjs +1 -1
  186. package/library/actions/shared/perps/order-policy.mjs +177 -0
  187. package/library/actions/shared/wallet/fixture-accounts.mjs +84 -0
  188. package/library/actions/terminal/app/launch.mjs +3 -2
  189. package/library/actions/terminal/platform/hud.mjs +2 -1
  190. package/library/actions/terminal/platform/page.mjs +2 -1
  191. package/library/actions/terminal/platform/runtime.mjs +2 -1
  192. package/library/actions/terminal/wallet/_signature-log.mjs +40 -161
  193. package/library/actions/terminal/wallet/assert_signatures.mjs +6 -12
  194. package/library/actions/terminal/wallet/read_signatures.mjs +10 -9
  195. package/library/manifests/core.action-manifest.json +87 -13
  196. package/library/manifests/extension.action-manifest.json +4 -0
  197. package/library/manifests/mobile.action-manifest.json +12 -0
  198. package/package.json +8 -9
  199. package/site/assets/help-recipes.json +1 -0
  200. package/adapters/extension/lib/slot-title.cjs +0 -292
  201. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +0 -472
  202. package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +0 -125
  203. package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +0 -1055
  204. package/adapters/mobile/bridge-runtime/lib/cdp-eval.cjs +0 -110
  205. package/adapters/mobile/bridge-runtime/lib/config.cjs +0 -61
  206. package/adapters/mobile/bridge-runtime/lib/console-format.cjs +0 -53
  207. package/adapters/mobile/bridge-runtime/lib/devtools-proxy.cjs +0 -177
  208. package/adapters/mobile/bridge-runtime/lib/issue-capture.cjs +0 -446
  209. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +0 -84
  210. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +0 -403
  211. package/adapters/mobile/bridge-runtime/lib/ws-client.cjs +0 -116
  212. package/adapters/mobile/coalesce-metro-log.cjs +0 -24
  213. package/adapters/mobile/launch-metro.cjs +0 -77
  214. package/adapters/mobile/metro-config.cjs +0 -98
  215. package/adapters/mobile/metro-log-generation.cjs +0 -106
  216. package/adapters/terminal/lib/origin.mjs +0 -30
  217. package/adapters/terminal/lib/page-script.mjs +0 -246
  218. package/adapters/terminal/lib/strict-wallet.mjs +0 -138
  219. package/dist/adapters/mobile/frame-metrics.js +0 -118
  220. package/dist/adapters/mobile/video-recorder.js +0 -289
  221. package/dist/artifact-files.js +0 -92
  222. package/dist/checkout-lock.js +0 -149
  223. package/dist/cli-color.js +0 -98
  224. package/dist/command-journal.js +0 -308
  225. package/dist/commands/call.js +0 -685
  226. package/dist/commands/last.js +0 -60
  227. package/dist/commands/launch/index.js +0 -109
  228. package/dist/commands/list-executables.js +0 -247
  229. package/dist/commands/manifest.js +0 -488
  230. package/dist/commands/parse-args.js +0 -279
  231. package/dist/commands/reload.js +0 -41
  232. package/dist/commands/run-engine.js +0 -1340
  233. package/dist/commands/run-report.js +0 -241
  234. package/dist/commands/run.js +0 -751
  235. package/dist/commands/shared.js +0 -323
  236. package/dist/commands/stop.js +0 -60
  237. package/dist/devices.js +0 -100
  238. package/dist/discovery.js +0 -15
  239. package/dist/execution-provenance.js +0 -332
  240. package/dist/harness.js +0 -451
  241. package/dist/heal-bounds.js +0 -137
  242. package/dist/json-stream.js +0 -60
  243. package/dist/leaf-invoke.js +0 -19
  244. package/dist/library-provenance.js +0 -33
  245. package/dist/live-adapter-contract.js +0 -380
  246. package/dist/metamask-action-validation.js +0 -68
  247. package/dist/network-observation.js +0 -290
  248. package/dist/recording-target.js +0 -120
  249. package/dist/run-diagnostics.js +0 -364
  250. package/dist/run-recording.js +0 -481
  251. package/dist/runtime-proof.js +0 -90
  252. package/library/actions/mobile/platform/tool-paths.mjs +0 -123
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ // MetaMask Mobile recovery lines for the bridge error codes that
4
+ // @farmslot/adapter-rn classifies; cdp-bridge.cjs prints one after a coded failure.
5
+ //
6
+ // Every coded failure teaches the escape for the caller's ACTUAL situation — a
7
+ // down/transitioning target is recoverable, so the Next: line names the action
8
+ // that brings it back rather than leaving the operator to guess.
9
+ const TEACHING_BY_ERROR_CODE = {
10
+ NO_TARGET:
11
+ 'Next: bring the app to the foreground with `mm-harness launch <platform>` so a debug target is available.',
12
+ CDP_TIMEOUT:
13
+ 'Next: the app is reloading or busy — retry, or `mm-harness launch <platform>` to foreground it.',
14
+ WS_CLOSED:
15
+ 'Next: the Hermes debug page closed (app reload/backgrounded) — retry once the app settles.',
16
+ METRO_UNREACHABLE:
17
+ 'Next: start Metro with `mm-harness start-metro` (if it is already up, check the slot WATCHER_PORT).',
18
+ };
19
+
20
+ module.exports = { TEACHING_BY_ERROR_CODE };
@@ -77,6 +77,22 @@ rsync -a --delete "$RUNNER_DIR/library/recipes/" "$HARNESS_DIR/runner/recipes/"
77
77
  rm -rf "$HARNESS_DIR/runner/flows"
78
78
  rm -f "$HARNESS_DIR/runner/library.json"
79
79
  cp -R "$RUNNER_DIR/adapters/mobile/bridge-runtime" "$HARNESS_DIR/bridge-runtime"
80
+ # cdp-bridge.cjs runs from the target checkout, which cannot resolve the runner's
81
+ # packages; vendor the bridge libraries it requires next to it.
82
+ node - "$RUNNER_DIR" "$HARNESS_DIR/bridge-runtime/node_modules/@farmslot" <<'NODE' || exit 1
83
+ const fs = require('node:fs');
84
+ const path = require('node:path');
85
+ const { createRequire } = require('node:module');
86
+ const [runnerDir, dest] = process.argv.slice(2);
87
+ const requireFromRunner = createRequire(path.join(runnerDir, 'package.json'));
88
+ for (const [name, dirs] of [['adapter-rn', ['bridge-runtime']], ['recipe-runner', ['cdp-broker']]]) {
89
+ const packageJson = requireFromRunner.resolve(`@farmslot/${name}/package.json`);
90
+ const target = path.join(dest, name);
91
+ fs.mkdirSync(target, { recursive: true });
92
+ fs.copyFileSync(packageJson, path.join(target, 'package.json'));
93
+ for (const dir of dirs) fs.cpSync(path.join(path.dirname(packageJson), dir), path.join(target, dir), { recursive: true });
94
+ }
95
+ NODE
80
96
  chmod -R u+rwX,go+rX "$HARNESS_DIR"
81
97
 
82
98
  add_git_exclude() {
@@ -2,7 +2,6 @@
2
2
  'use strict';
3
3
 
4
4
  const fs = require('node:fs');
5
- const path = require('node:path');
6
5
  const { spawn } = require('node:child_process');
7
6
 
8
7
  const args = { port: '', out: '', err: '' };
@@ -25,7 +24,7 @@ if (!args.port || !args.out || !args.err) {
25
24
  process.exit(2);
26
25
  }
27
26
 
28
- const forwarder = path.join(__dirname, 'bridge-runtime', 'console-forwarder.cjs');
27
+ const forwarder = require.resolve('@farmslot/adapter-rn/bridge-runtime/console-forwarder.cjs');
29
28
  const errFd = fs.openSync(args.err, 'a');
30
29
  try {
31
30
  const child = spawn(process.execPath, [forwarder, '--port', args.port, '--out', args.out], {
@@ -28,7 +28,7 @@ PLATFORM="ios"
28
28
  TARGET="$PWD"
29
29
  SIMULATOR="${SIM_UDID:-${IOS_SIMULATOR:-booted}}"
30
30
  ADB_SERIAL_ARG="${ADB_SERIAL:-${ANDROID_SERIAL:-}}"
31
- ADB_BIN="${MM_HARNESS_ADB_PATH:-}"
31
+ ADB_BIN="${RECIPE_RN_ADB_PATH:-}"
32
32
  PREFLIGHT_MODE="${MOBILE_PREFLIGHT_MODE:-fast}"
33
33
  PORT="${WATCHER_PORT:-${METRO_PORT:-8081}}"
34
34
  UI_ONLY=false
@@ -67,7 +67,7 @@ if [ "$PLATFORM" = "android" ]; then
67
67
  printf 'Next: brew install android-platform-tools\n' >&2
68
68
  exit 1
69
69
  fi
70
- export MM_HARNESS_ADB_PATH="$ADB_BIN"
70
+ export RECIPE_RN_ADB_PATH="$ADB_BIN"
71
71
  fi
72
72
 
73
73
  TARGET="$(cd "$TARGET" && pwd)"
@@ -4,13 +4,13 @@ import { spawnSync } from 'node:child_process';
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
6
  import { createRequire } from 'node:module';
7
- import { resolveMobileToolPath } from '../../library/actions/mobile/platform/tool-paths.mjs';
7
+ import { resolveMobileToolPath } from '@farmslot/adapter-rn';
8
8
 
9
9
  const require = createRequire(import.meta.url);
10
10
  const WebSocket = require('ws');
11
- const { discoverTarget, rankRuntimeCandidates } = require('./bridge-runtime/lib/target-discovery.cjs');
12
- const { brokerSocketPath, createBrokerClient, deviceIdFromUrl } = require('./bridge-runtime/lib/cdp-broker.cjs');
13
- const { createWSClient } = require('./bridge-runtime/lib/ws-client.cjs');
11
+ const { discoverTarget, rankRuntimeCandidates } = require('@farmslot/adapter-rn/bridge-runtime/lib/target-discovery.cjs');
12
+ const { brokerSocketPath, createBrokerClient, deviceIdFromUrl } = require('@farmslot/recipe-runner/cdp-broker');
13
+ const { createWSClient } = require('@farmslot/adapter-rn/bridge-runtime/lib/ws-client.cjs');
14
14
 
15
15
  async function observeRuntimeReset(port, target) {
16
16
  const socketPath = brokerSocketPath(
@@ -6,7 +6,7 @@ PLATFORM="${PLATFORM:-ios}"
6
6
  TARGET="$PWD"
7
7
  SIMULATOR="${SIM_UDID:-${IOS_SIMULATOR:-booted}}"
8
8
  ADB_SERIAL_ARG="${ADB_SERIAL:-${ANDROID_SERIAL:-}}"
9
- ADB_BIN="${MM_HARNESS_ADB_PATH:-}"
9
+ ADB_BIN="${RECIPE_RN_ADB_PATH:-}"
10
10
  MM_RESET_TEMP_ROOT=""
11
11
  MM_RESET_STAGING_DIR=""
12
12
 
@@ -23,13 +23,14 @@ done
23
23
  TARGET="$(cd "$TARGET" && pwd -P)"
24
24
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25
25
  if [ -z "$PORT" ] && command -v node >/dev/null 2>&1; then
26
- PORT="$(APP_ROOT="$TARGET" CONFIG_PATH="$SCRIPT_DIR/bridge-runtime/lib/config.cjs" node -e "process.stdout.write(String(require(process.env.CONFIG_PATH).resolvePort()))")"
26
+ PORT="$(cd "$SCRIPT_DIR" && APP_ROOT="$TARGET" node -e "process.stdout.write(String(require('@farmslot/adapter-rn/bridge-runtime/lib/config.cjs').resolvePort()))")"
27
27
  fi
28
28
  PORT="${PORT:-8081}"
29
29
  # shellcheck disable=SC1091
30
30
  . "$SCRIPT_DIR/../shared/harness-path.sh"
31
31
  LOG_DIR="$TARGET/$(recipe_runtime_dir)" || exit 1
32
- FORWARDER="$SCRIPT_DIR/bridge-runtime/console-forwarder.cjs"
32
+ command -v node >/dev/null 2>&1 || exit 0
33
+ FORWARDER="$(cd "$SCRIPT_DIR" && node -p "require.resolve('@farmslot/adapter-rn/bridge-runtime/console-forwarder.cjs')")" || exit 1
33
34
  PID_FILE="$LOG_DIR/console-forwarder.pid"
34
35
  APP_LOG_FILE="$LOG_DIR/app-console.log"
35
36
 
@@ -54,18 +54,23 @@ fi
54
54
  # only ever inspect or signal a Metro listening on $PORT.
55
55
  # shellcheck disable=SC1091
56
56
  . "$SCRIPT_DIR/lib/metro-listener.sh"
57
- HARNESS_METRO_CONFIG="$SCRIPT_DIR/metro-config.cjs"
57
+ # Metro config wrapper, launcher and log helpers ship in @farmslot/adapter-rn.
58
+ ADAPTER_RN_METRO_DIR="$(cd "$SCRIPT_DIR" && node -p "require('node:path').dirname(require.resolve('@farmslot/adapter-rn/metro/metro-config.cjs'))")" || {
59
+ echo "start-metro: @farmslot/adapter-rn is not installed next to the runner; reinstall the runner." >&2
60
+ exit 1
61
+ }
62
+ HARNESS_METRO_CONFIG="$ADAPTER_RN_METRO_DIR/metro-config.cjs"
58
63
  if [ "${EXPO_OVERRIDE_METRO_CONFIG:-}" = "$HARNESS_METRO_CONFIG" ]; then
59
- PRODUCT_METRO_CONFIG="${MM_HARNESS_METRO_BASE_CONFIG:-$TARGET/metro.config.js}"
64
+ PRODUCT_METRO_CONFIG="${RECIPE_RN_METRO_BASE_CONFIG:-$TARGET/metro.config.js}"
60
65
  else
61
66
  PRODUCT_METRO_CONFIG="${EXPO_OVERRIDE_METRO_CONFIG:-$TARGET/metro.config.js}"
62
67
  fi
63
- export MM_HARNESS_METRO_PROJECT_ROOT="$TARGET"
64
- export MM_HARNESS_METRO_BASE_CONFIG="$PRODUCT_METRO_CONFIG"
68
+ export RECIPE_RN_METRO_PROJECT_ROOT="$TARGET"
69
+ export RECIPE_RN_METRO_BASE_CONFIG="$PRODUCT_METRO_CONFIG"
65
70
  export EXPO_OVERRIDE_METRO_CONFIG="$HARNESS_METRO_CONFIG"
66
- MOBILE_METRO_REQUIRED_ENV="${MOBILE_METRO_REQUIRED_ENV:-MM_INFURA_PROJECT_ID METAMASK_BUILD_TYPE METAMASK_ENVIRONMENT} MM_HARNESS_METRO_PROJECT_ROOT MM_HARNESS_METRO_BASE_CONFIG EXPO_OVERRIDE_METRO_CONFIG"
67
- if [ -n "${MM_HARNESS_METRO_ENV_FINGERPRINT:-}" ]; then
68
- MOBILE_METRO_REQUIRED_ENV="$MOBILE_METRO_REQUIRED_ENV MM_HARNESS_METRO_ENV_FINGERPRINT"
71
+ MOBILE_METRO_REQUIRED_ENV="${MOBILE_METRO_REQUIRED_ENV:-MM_INFURA_PROJECT_ID METAMASK_BUILD_TYPE METAMASK_ENVIRONMENT} RECIPE_RN_METRO_PROJECT_ROOT RECIPE_RN_METRO_BASE_CONFIG EXPO_OVERRIDE_METRO_CONFIG"
72
+ if [ -n "${RECIPE_RN_METRO_ENV_FINGERPRINT:-}" ]; then
73
+ MOBILE_METRO_REQUIRED_ENV="$MOBILE_METRO_REQUIRED_ENV RECIPE_RN_METRO_ENV_FINGERPRINT"
69
74
  fi
70
75
 
71
76
  # RECIPE_RUNTIME_DIR (relative, validated) makes runtime state per-run so jobs
@@ -75,8 +80,8 @@ mkdir -p "$LOG_DIR"
75
80
  LOG_FILE="$LOG_DIR/metro.log"
76
81
  PID_FILE="$LOG_DIR/metro.pid"
77
82
  METRO_BUILD_ENV_FILE="$LOG_DIR/metro-build-env.sh"
78
- METRO_LOG_GENERATION="$SCRIPT_DIR/metro-log-generation.cjs"
79
- METRO_LOG_COALESCER="$SCRIPT_DIR/coalesce-metro-log.cjs"
83
+ METRO_LOG_GENERATION="$ADAPTER_RN_METRO_DIR/metro-log-generation.cjs"
84
+ METRO_LOG_COALESCER="$ADAPTER_RN_METRO_DIR/coalesce-metro-log.cjs"
80
85
 
81
86
  # --- helpers ------------------------------------------------------------------
82
87
 
@@ -106,8 +111,8 @@ write_metro_build_env() {
106
111
  done
107
112
  )" || { rm -f "$temporary"; return 1; }
108
113
  eval "$product_values"
109
- required_MM_HARNESS_METRO_PROJECT_ROOT="$MM_HARNESS_METRO_PROJECT_ROOT"
110
- required_MM_HARNESS_METRO_BASE_CONFIG="$MM_HARNESS_METRO_BASE_CONFIG"
114
+ required_RECIPE_RN_METRO_PROJECT_ROOT="$RECIPE_RN_METRO_PROJECT_ROOT"
115
+ required_RECIPE_RN_METRO_BASE_CONFIG="$RECIPE_RN_METRO_BASE_CONFIG"
111
116
  required_EXPO_OVERRIDE_METRO_CONFIG="$EXPO_OVERRIDE_METRO_CONFIG"
112
117
  [ "$CLEAR" = true ] && clear_value=1
113
118
  {
@@ -322,7 +327,7 @@ else
322
327
  )
323
328
  [ -z "$METRO_WORKERS" ] || launcher_args+=(--workers "$METRO_WORKERS")
324
329
  [ "$CLEAR" = true ] && launcher_args+=(--clear)
325
- node "$SCRIPT_DIR/launch-metro.cjs" "${launcher_args[@]}"
330
+ node "$ADAPTER_RN_METRO_DIR/launch-metro.cjs" "${launcher_args[@]}"
326
331
  ) || exit 1
327
332
  STARTED_METRO_PID="$(cat "$PID_FILE" 2>/dev/null || true)"
328
333
 
@@ -14,10 +14,10 @@ const {
14
14
  describeRequested,
15
15
  describeTargets,
16
16
  hasMatchingRoute,
17
- } = require('./bridge-runtime/lib/match-bridge-target.cjs');
17
+ } = require('@farmslot/adapter-rn/bridge-runtime/lib/match-bridge-target.cjs');
18
18
  const {
19
19
  rankRuntimeCandidates,
20
- } = require('./bridge-runtime/lib/target-discovery.cjs');
20
+ } = require('@farmslot/adapter-rn/bridge-runtime/lib/target-discovery.cjs');
21
21
 
22
22
  const DEFAULT_MAX_POLLS = 90;
23
23
  const DEFAULT_POLL_INTERVAL_MS = 2_000;
@@ -8,7 +8,7 @@
8
8
  // the next launch refreshes every slot from it.
9
9
  //
10
10
  // Source files count by size + mtime, so it is cheap enough to run on every launch;
11
- // the only file read is @farmslot/adapter-web's package.json, for its version. No
11
+ // the only files read are the vendored packages' package.json, for their versions. No
12
12
  // Date.now()/random is used (both would break determinism).
13
13
  import fs from 'node:fs';
14
14
  import path from 'node:path';
@@ -25,17 +25,22 @@ export function harnessSourceFingerprint(runnerDir, adapter = 'extension') {
25
25
  for (const rel of sourceDirs) {
26
26
  collect(path.join(runnerDir, rel), runnerDir, entries);
27
27
  }
28
- // inject.mjs copies the browser helpers from @farmslot/adapter-web, so its sources
29
- // are overlay inputs too; label them by package so the install location does not count.
30
- if (adapter === 'extension') {
31
- const packageJson = createRequire(path.join(runnerDir, 'package.json')).resolve('@farmslot/adapter-web/package.json');
28
+ // The injectors copy package files into the overlay (Extension: @farmslot/adapter-web's
29
+ // browser helpers; Mobile: the bridge libraries cdp-bridge.cjs requires), so those
30
+ // sources are overlay inputs too; label them by package so the install location
31
+ // does not count.
32
+ const vendored = adapter === 'extension'
33
+ ? [['@farmslot/adapter-web', ['src']]]
34
+ : [['@farmslot/adapter-rn', ['bridge-runtime']], ['@farmslot/recipe-runner', ['cdp-broker']]];
35
+ for (const [name, dirs] of vendored) {
36
+ const packageJson = createRequire(path.join(runnerDir, 'package.json')).resolve(`${name}/package.json`);
32
37
  const packageRoot = path.dirname(packageJson);
33
38
  const packageEntries = [];
34
- collect(path.join(packageRoot, 'src'), packageRoot, packageEntries);
35
- entries.push(...packageEntries.map((entry) => `@farmslot/adapter-web/${entry}`));
39
+ for (const dir of dirs) collect(path.join(packageRoot, dir), packageRoot, packageEntries);
40
+ entries.push(...packageEntries.map((entry) => `${name}/${entry}`));
36
41
  // npm tarballs carry fixed mtimes, so an upgrade can keep every size and mtime:
37
42
  // the version marks it.
38
- entries.push(`@farmslot/adapter-web@${JSON.parse(fs.readFileSync(packageJson, 'utf8')).version}`);
43
+ entries.push(`${name}@${JSON.parse(fs.readFileSync(packageJson, 'utf8')).version}`);
39
44
  }
40
45
  entries.sort();
41
46
  return crypto.createHash('sha256').update(entries.join('\n')).digest('hex');
@@ -15,10 +15,10 @@ import path from 'node:path';
15
15
  const require = createRequire(import.meta.url);
16
16
  const {
17
17
  discoverTarget,
18
- } = require('../mobile/bridge-runtime/lib/target-discovery.cjs');
18
+ } = require('@farmslot/adapter-rn/bridge-runtime/lib/target-discovery.cjs');
19
19
  const {
20
20
  deviceIdFromUrl,
21
- } = require('../mobile/bridge-runtime/lib/cdp-broker.cjs');
21
+ } = require('@farmslot/recipe-runner/cdp-broker');
22
22
 
23
23
  function usage() {
24
24
  console.error(`Usage:
@@ -60,12 +60,15 @@ fi
60
60
 
61
61
  # Farmslot slot sync (requires farmslot root and sync-fixtures.sh).
62
62
  if [ -n "$SLOT_ID" ]; then
63
+ # An explicit FARMSLOT_ROOT wins; otherwise the operator checkout's farm script.
63
64
  FARMSLOT_ROOT="${FARMSLOT_ROOT:-}"
64
- for candidate in "$HOME/dev/farmslot" "$HOME/farmslot"; do
65
- [ -d "$candidate/scripts" ] || continue
66
- FARMSLOT_ROOT="$candidate"
67
- break
68
- done
65
+ if [ -z "$FARMSLOT_ROOT" ]; then
66
+ for candidate in "$HOME/dev/farmslot" "$HOME/farmslot"; do
67
+ [ -f "$candidate/scripts/sync-fixtures.sh" ] || continue
68
+ FARMSLOT_ROOT="$candidate"
69
+ break
70
+ done
71
+ fi
69
72
  if [ -n "$FARMSLOT_ROOT" ] && [ -f "$FARMSLOT_ROOT/scripts/sync-fixtures.sh" ]; then
70
73
  if bash "$FARMSLOT_ROOT/scripts/sync-fixtures.sh" --slot "$SLOT_ID" 2>/dev/null; then
71
74
  [ -f "$CANONICAL" ] && exit 0
@@ -11,7 +11,8 @@ import { existsSync, readdirSync, rmSync } from 'node:fs';
11
11
  import path from 'node:path';
12
12
  import { fileURLToPath } from 'node:url';
13
13
 
14
- import { recipeHarnessPath, walletFixturePath } from '../../library/actions/harness-exports.mjs';
14
+ import { recipeHarnessPath } from '@farmslot/recipe-cli/harness';
15
+ import { walletFixturePath } from '../../library/actions/harness-exports.mjs';
15
16
  import { terminalRuntimeDir } from '../../library/actions/terminal/platform/runtime.mjs';
16
17
  import { stopConsoleCollector, stopTerminalBrowser } from './lib/processes.mjs';
17
18
 
@@ -13,7 +13,8 @@ import { copyFileSync, chmodSync, existsSync, mkdirSync, readFileSync, writeFile
13
13
  import path from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
15
 
16
- import { recipeHarnessPath, recipeRuntimeDir, walletFixturePath } from '../../library/actions/harness-exports.mjs';
16
+ import { recipeHarnessPath, recipeRuntimeDir } from '@farmslot/recipe-cli/harness';
17
+ import { walletFixturePath } from '../../library/actions/harness-exports.mjs';
17
18
  import { terminalRuntimeDir } from '../../library/actions/terminal/platform/runtime.mjs';
18
19
  import { isTerminalCheckout } from './lib/readiness.mjs';
19
20
 
@@ -29,7 +29,7 @@ import { createRequire } from 'node:module';
29
29
  import { appendFileSync, chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
30
30
  import os from 'node:os';
31
31
  import path from 'node:path';
32
- import { fileURLToPath } from 'node:url';
32
+ import { fileURLToPath, pathToFileURL } from 'node:url';
33
33
 
34
34
  import {
35
35
  fixtureEntry,
@@ -42,7 +42,7 @@ import { CdpClient, browserWebSocketUrl } from './lib/cdp-client.mjs';
42
42
  import { clearHudState } from './lib/hud.mjs';
43
43
  import { browserDisplayArgs, resolveHeadless, resolveSlowMo, windowPlacement, writeProfileZoom } from './lib/display.mjs';
44
44
  import { cdpListenerPids, ownedBrowserPids, ownerMarker, ownsProcess, pidAlive, stopTerminalBrowser, terminate } from './lib/processes.mjs';
45
- import { hostResolverRules, venueHosts } from './lib/mainnet-guard.mjs';
45
+ import { hostResolverRules } from './lib/mainnet-guard.mjs';
46
46
  import { assertLaunchNetwork, devServerTestnetDiagnostic } from './lib/readiness.mjs';
47
47
 
48
48
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
@@ -51,7 +51,7 @@ const HARNESS_ROOT = path.resolve(SCRIPT_DIR, '../..');
51
51
  const FIXTURE_STATE_SCRIPT = path.join(HARNESS_ROOT, 'adapters/extension/wallet-fixture-state.cjs');
52
52
 
53
53
  function usage() {
54
- return 'Usage: launch.mjs --target <checkout> --cdp-port <port> --app-port <port> [--signer extension|injected] [--account <name>] [--fresh-profile] [--headless | --headful] [--slow-mo <ms>] [--network testnet|mainnet --mainnet-confirmation REAL_FUNDS] [--start-chain <id>] [--json]';
54
+ return 'Usage: launch.mjs --target <checkout> --cdp-port <port> --app-port <port> --policy-module <venue policy module> [--signer extension|injected] [--account <name>] [--fresh-profile] [--headless | --headful] [--slow-mo <ms>] [--network testnet|mainnet --mainnet-confirmation REAL_FUNDS] [--start-chain <id>] [--json]';
55
55
  }
56
56
 
57
57
  export function parseLaunchArgs(argv) {
@@ -67,7 +67,7 @@ export function parseLaunchArgs(argv) {
67
67
  args[flag.slice(2)] = argv[index + 1];
68
68
  index += 1;
69
69
  }
70
- for (const key of ['target', 'cdp-port', 'app-port']) {
70
+ for (const key of ['target', 'cdp-port', 'app-port', 'policy-module']) {
71
71
  if (!args[key]) throw Object.assign(new Error(`Missing --${key}\n${usage()}`), { exitCode: 2 });
72
72
  }
73
73
  args.signer = resolveSigner(args.signer);
@@ -344,7 +344,9 @@ function writePrivateFile(file, text) {
344
344
  // which waits for a cold dev-server compile.
345
345
  export const LAUNCH_TIMEOUTS = Object.freeze({ browserStartMs: 90000, hostReadyMs: 180000 });
346
346
 
347
- export async function launchTerminalBrowser(args, env = process.env, { timeouts = LAUNCH_TIMEOUTS } = {}) {
347
+ // `policy`: the venue policy (adapters/terminal/policy.mjs).
348
+ export async function launchTerminalBrowser(args, env = process.env, { timeouts = LAUNCH_TIMEOUTS, policy } = {}) {
349
+ if (!policy?.module) throw new Error('launchTerminalBrowser needs the venue policy (adapters/terminal/policy.mjs).');
348
350
  const target = path.resolve(args.target);
349
351
  const cdpPort = Number(args['cdp-port']);
350
352
  const appPort = Number(args['app-port']);
@@ -353,12 +355,12 @@ export async function launchTerminalBrowser(args, env = process.env, { timeouts
353
355
  // browser: the SDK's mainnet venue hosts are unresolvable for the whole
354
356
  // browser, and the wallet host blocks, logs and refuses anything mainnet.
355
357
  const network = assertLaunchNetwork(target, { network: args.network, mainnetConfirmation: args['mainnet-confirmation'] });
356
- const venues = network === 'testnet' ? venueHosts(target) : { mainnet: [], testnet: [] };
357
- const mainnetHosts = venues.mainnet;
358
+ const venues = network === 'testnet' ? policy.venueHosts(target) : { blocked: [], served: [] };
359
+ const mainnetHosts = venues.blocked;
358
360
  // What the running browser enforces; a browser enforcing anything else is
359
361
  // never reused (its resolver rules are fixed at start).
360
- const enforcementFingerprint = createHash('sha256').update(JSON.stringify({ network, mainnetHosts, testnetHosts: venues.testnet })).digest('hex').slice(0, 16);
361
- const devServer = network === 'testnet' ? devServerTestnetDiagnostic(target, { appPort }) : null;
362
+ const enforcementFingerprint = createHash('sha256').update(JSON.stringify({ network, mainnetHosts, testnetHosts: venues.served })).digest('hex').slice(0, 16);
363
+ const devServer = network === 'testnet' ? devServerTestnetDiagnostic(target, { appPort, variable: policy.testnetVariable }) : null;
362
364
  if (devServer && !(devServer.known && devServer.forced)) {
363
365
  log(`WARN dev server testnet check: ${devServer.detail} (advisory; the browser enforces testnet itself)`);
364
366
  }
@@ -541,11 +543,12 @@ export async function launchTerminalBrowser(args, env = process.env, { timeouts
541
543
  '--app-origin', appOrigin,
542
544
  '--target', target,
543
545
  '--account', args.account,
544
- '--start-chain', String(args['start-chain'] ?? 42161),
546
+ '--start-chain', String(args['start-chain'] ?? policy.startChain),
545
547
  '--network', network,
548
+ '--policy-module', policy.module,
546
549
  ownerMarker('wallet-host', runtime),
547
550
  ];
548
- if (network === 'testnet') hostArgs.push('--mainnet-hosts', mainnetHosts.join(','), '--testnet-hosts', venues.testnet.join(','));
551
+ if (network === 'testnet') hostArgs.push('--mainnet-hosts', mainnetHosts.join(','), '--testnet-hosts', venues.served.join(','));
549
552
  if (extension?.id) hostArgs.push('--extension-id', extension.id);
550
553
  if (guardFocus) hostArgs.push('--observe-focus', '1');
551
554
  if (launchMethod === LAUNCH_SERVICES) hostArgs.push('--window', [windowBounds.x, windowBounds.y, windowBounds.width, windowBounds.height].join(','));
@@ -568,7 +571,7 @@ export async function launchTerminalBrowser(args, env = process.env, { timeouts
568
571
  signer,
569
572
  network,
570
573
  networkEnforcement: network === 'testnet'
571
- ? { mode: 'enforced', mainnetHosts, testnetHosts: venues.testnet, fingerprint: enforcementFingerprint, layers: ['host-resolver-rules', 'cdp-fetch-block', 'wallet-refusal', 'served-network-check'] }
574
+ ? { mode: 'enforced', mainnetHosts, testnetHosts: venues.served, fingerprint: enforcementFingerprint, layers: ['host-resolver-rules', 'cdp-fetch-block', 'wallet-refusal', 'served-network-check'] }
572
575
  : { mode: 'disabled', reason: 'network=mainnet with mainnet_confirmation=REAL_FUNDS', fingerprint: enforcementFingerprint },
573
576
  devServerDiagnostic: devServer,
574
577
  consoleOffsets,
@@ -618,7 +621,8 @@ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.me
618
621
  process.exit(0);
619
622
  }
620
623
  try {
621
- const state = await launchTerminalBrowser(args);
624
+ const { policy } = await import(pathToFileURL(path.resolve(args['policy-module'])).href);
625
+ const state = await launchTerminalBrowser(args, process.env, { policy });
622
626
  process.stdout.write(`${JSON.stringify({ status: 'pass', ...state }, null, args.json ? 0 : 2)}\n`);
623
627
  } catch (error) {
624
628
  const message = String(error?.stderr ?? '').trim() || error.message;
@@ -175,3 +175,12 @@ export function mainnetTypedDataReason(data) {
175
175
  if (data?.primaryType === 'Agent' && message.source === 'a') return 'L1 Agent source=a';
176
176
  return null;
177
177
  }
178
+
179
+ // The wallet refusal for testnet runs, as the @farmslot/adapter-web/dapp page
180
+ // script and wallet-request binding take it: mainnetTypedDataReason runs in
181
+ // the page too, so it stays self-contained.
182
+ export const TESTNET_TYPED_DATA_POLICY = Object.freeze({
183
+ reason: mainnetTypedDataReason,
184
+ kind: 'refused-mainnet',
185
+ message: 'Refused: this test run is held to Hyperliquid testnet and the request signs a mainnet action.',
186
+ });
@@ -34,8 +34,9 @@ async function httpStatus(url, timeoutMs = 20000) {
34
34
  }
35
35
 
36
36
  // Diagnostic only. Whether the slot's dev server looks configured for testnet:
37
- // the farm starts `next dev` (temp/farmslot/next-dev.pid) with
38
- // NEXT_PUBLIC_HYPERLIQUID_FORCE_TESTNET=true. Process inspection cannot prove
37
+ // the farm starts `next dev` (temp/farmslot/next-dev.pid) with the policy's
38
+ // testnet variable set to true (the Terminal's
39
+ // NEXT_PUBLIC_HYPERLIQUID_FORCE_TESTNET). Process inspection cannot prove
39
40
  // what the browser talks to (macOS reports a process's initial environment,
40
41
  // and lsof cannot see other users' sockets), so this only warns; the browser
41
42
  // is held to testnet by the network and wallet layers (mainnet-guard.mjs, the
@@ -45,7 +46,6 @@ async function httpStatus(url, timeoutMs = 20000) {
45
46
  // entries from sysctl KERN_PROCARGS2) and must carry the testnet variable. An
46
47
  // unreadable process makes the result unknown.
47
48
  export const DEV_SERVER_PID_FILE = 'temp/farmslot/next-dev.pid';
48
- const FORCE_VARIABLE = 'NEXT_PUBLIC_HYPERLIQUID_FORCE_TESTNET';
49
49
 
50
50
  function run(command, args) {
51
51
  try {
@@ -142,7 +142,7 @@ function chainTo(listener, root, parents) {
142
142
  return null;
143
143
  }
144
144
 
145
- export function devServerTestnetDiagnostic(target, { appPort, probe = systemProbe } = {}) {
145
+ export function devServerTestnetDiagnostic(target, { appPort, variable, probe = systemProbe } = {}) {
146
146
  const checkout = canonicalPath(target);
147
147
  const unknown = (detail) => ({ known: false, forced: false, detail });
148
148
  const pidFile = path.join(checkout, DEV_SERVER_PID_FILE);
@@ -170,14 +170,14 @@ export function devServerTestnetDiagnostic(target, { appPort, probe = systemProb
170
170
  const processMode = nextServerMode(args.argv);
171
171
  if (processMode === 'start') return { known: true, forced: false, detail: `pid ${pid} runs next start; its environment cannot prove what the build inlined` };
172
172
  if (processMode === 'dev') mode = 'dev';
173
- const value = environmentValue(args.env, FORCE_VARIABLE);
173
+ const value = environmentValue(args.env, variable);
174
174
  if (value !== 'true') {
175
- return { known: true, forced: false, detail: `pid ${pid} (between app port ${appPort} and next dev pid ${root}) has ${FORCE_VARIABLE}=${value ?? '<unset>'}` };
175
+ return { known: true, forced: false, detail: `pid ${pid} (between app port ${appPort} and next dev pid ${root}) has ${variable}=${value ?? '<unset>'}` };
176
176
  }
177
177
  }
178
178
  }
179
179
  if (mode !== 'dev') return { known: true, forced: false, detail: `no process serving app port ${appPort} runs next dev` };
180
- return { known: true, forced: true, detail: `every process from app port ${appPort}'s listeners (cwd ${checkout}) up to next dev pid ${root} has ${FORCE_VARIABLE}=true` };
180
+ return { known: true, forced: true, detail: `every process from app port ${appPort}'s listeners (cwd ${checkout}) up to next dev pid ${root} has ${variable}=true` };
181
181
  }
182
182
 
183
183
  export const MAINNET_CONFIRMATION = 'REAL_FUNDS';
@@ -195,7 +195,15 @@ export function assertLaunchNetwork(_target, { network, mainnetConfirmation } =
195
195
  return requested;
196
196
  }
197
197
 
198
+ // "a, b and c".
199
+ function listing(names) {
200
+ return names.length > 1 ? `${names.slice(0, -1).join(', ')} and ${names.at(-1)}` : names.join('');
201
+ }
202
+
203
+ // `policy`: the venue policy (adapters/terminal/policy.mjs): dependencies,
204
+ // startPath, testnetVariable.
198
205
  export async function terminalReadiness({
206
+ policy,
199
207
  target,
200
208
  appPort,
201
209
  cdpPort,
@@ -207,17 +215,17 @@ export async function terminalReadiness({
207
215
  const checks = [];
208
216
  const shape = isTerminalCheckout(target);
209
217
  checks.push(check('checkout', shape, shape ? 'Next.js checkout with src/features/perpetuals' : `${target} is not a Web Terminal checkout`));
210
- const deps = ['next', '@nktkas/hyperliquid', '@metamask/perps-controller'].filter((name) => !existsSync(path.join(target, 'node_modules', name, 'package.json')));
211
- checks.push(check('dependencies', deps.length === 0, deps.length === 0 ? 'next, @nktkas/hyperliquid and @metamask/perps-controller installed' : `missing: ${deps.join(', ')} (run npm ci on Node 22)`));
218
+ const deps = policy.dependencies.filter((name) => !existsSync(path.join(target, 'node_modules', name, 'package.json')));
219
+ checks.push(check('dependencies', deps.length === 0, deps.length === 0 ? `${listing(policy.dependencies)} installed` : `missing: ${deps.join(', ')} (run npm ci on Node 22)`));
212
220
 
213
221
  if (appPort) {
214
- const status = await httpStatus(`http://127.0.0.1:${appPort}/order/BTC`);
215
- checks.push(check('app-dev-server', status === 200, `GET http://localhost:${appPort}/order/BTC -> ${status}`));
222
+ const status = await httpStatus(`http://127.0.0.1:${appPort}${policy.startPath}`);
223
+ checks.push(check('app-dev-server', status === 200, `GET http://localhost:${appPort}${policy.startPath} -> ${status}`));
216
224
  } else {
217
225
  checks.push(check('app-dev-server', false, 'no dev-server port; pass --watcher-port <port> or set TERMINAL_APP_PORT'));
218
226
  }
219
227
  // Advisory: the browser enforces testnet itself (mainnet-guard.mjs).
220
- const testnet = devServerTestnetDiagnostic(target, { appPort });
228
+ const testnet = devServerTestnetDiagnostic(target, { appPort, variable: policy.testnetVariable });
221
229
  checks.push(check('testnet-forced', testnet.known && testnet.forced, testnet.detail, {
222
230
  required: false,
223
231
  status: testnet.known && testnet.forced ? 'pass' : 'warn',
@@ -0,0 +1,35 @@
1
+ // The Web Terminal's venue policy: what the generic web-dapp launch, wallet
2
+ // host and readiness need to know about Hyperliquid. They take it as an
3
+ // argument (the leaf scripts import it from --policy-module, reading its
4
+ // `policy` export), so none of them names the venue. It becomes the terminal
5
+ // plugin's `policy` once recipe libraries can declare adapters; the Terminal's
6
+ // detection and its signature-log rules (_signature-log.mjs) move with it.
7
+
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ import { LINK_HOSTS, TESTNET_TYPED_DATA_POLICY, venueHosts } from './lib/mainnet-guard.mjs';
11
+
12
+ export const policy = Object.freeze({
13
+ // network=testnet: the hosts the browser must never reach, and the hosts
14
+ // whose traffic proves the app serves testnet.
15
+ venueHosts(checkout) {
16
+ const { mainnet, testnet } = venueHosts(checkout);
17
+ return { blocked: mainnet, served: testnet };
18
+ },
19
+ // Venue web hosts (pages, coin icons): never blocked, never proof.
20
+ linkHosts: Object.freeze([...LINK_HOSTS]),
21
+ // The launch's proof that the block holds: requests to this blocked host
22
+ // (preferred; else the first blocked host) must fail.
23
+ probe: Object.freeze({ host: 'api.hyperliquid.xyz', httpPath: '/info', httpBody: '{"type":"meta"}', wsPath: '/ws' }),
24
+ // The chain the injected wallet starts on (Arbitrum One).
25
+ startChain: 42161,
26
+ // Typed data a testnet run never signs.
27
+ refuseTypedData: TESTNET_TYPED_DATA_POLICY,
28
+ // The app's first page: the launch opens it, readiness probes it.
29
+ startPath: '/order/BTC',
30
+ dependencies: Object.freeze(['next', '@nktkas/hyperliquid', '@metamask/perps-controller']),
31
+ // The dev-server environment variable that forces testnet (advisory check).
32
+ testnetVariable: 'NEXT_PUBLIC_HYPERLIQUID_FORCE_TESTNET',
33
+ // This module, for the wallet host (a separate process).
34
+ module: fileURLToPath(import.meta.url),
35
+ });
@@ -3,18 +3,18 @@
3
3
  // Web Terminal slot (checkout, deps, dev server, forced testnet, fixture,
4
4
  // browser start probe, MetaMask extension copy, CDP port).
5
5
  //
6
- // Inputs: --target <checkout> [--cdp-port <port>] [--watcher-port <app port>]
6
+ // Inputs: --policy-module <venue policy module> --target <checkout> [--cdp-port <port>] [--watcher-port <app port>]
7
7
  // [--signer extension|injected] [--account <name>] [--json]
8
8
  // Outputs: readiness JSON (or a human summary) on stdout. Exit 0 ready; 1 not ready.
9
9
  // Never launches the slot browser or the dev server.
10
10
 
11
11
  import path from 'node:path';
12
- import { fileURLToPath } from 'node:url';
12
+ import { fileURLToPath, pathToFileURL } from 'node:url';
13
13
 
14
14
  import { resolveBrowser } from './launch.mjs';
15
15
  import { terminalReadiness } from './lib/readiness.mjs';
16
16
 
17
- const usage = 'Usage: verify.mjs --target <checkout> [--cdp-port <port>] [--watcher-port <app port>] [--signer extension|injected] [--account <name>] [--json]';
17
+ const usage = 'Usage: verify.mjs --policy-module <venue policy module> --target <checkout> [--cdp-port <port>] [--watcher-port <app port>] [--signer extension|injected] [--account <name>] [--json]';
18
18
 
19
19
  if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
20
20
  const argv = process.argv.slice(2);
@@ -27,7 +27,14 @@ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.me
27
27
  return index >= 0 ? argv[index + 1] : undefined;
28
28
  };
29
29
  const num = (raw) => (raw && /^\d+$/u.test(raw) ? Number(raw) : undefined);
30
+ const policyModule = value('--policy-module');
31
+ if (!policyModule) {
32
+ process.stderr.write(`Missing --policy-module\n${usage}\n`);
33
+ process.exit(2);
34
+ }
35
+ const { policy } = await import(pathToFileURL(path.resolve(policyModule)).href);
30
36
  const report = await terminalReadiness({
37
+ policy,
31
38
  target: path.resolve(value('--target') ?? process.cwd()),
32
39
  appPort: num(value('--watcher-port') ?? process.env.TERMINAL_APP_PORT ?? process.env.WATCHER_PORT),
33
40
  cdpPort: num(value('--cdp-port') ?? process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT),