@deeeed/metamask-harness 0.86.2 → 0.88.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 (61) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/adapters/extension/ensure-browser.sh +6 -10
  3. package/adapters/extension/inject.mjs +1 -0
  4. package/adapters/extension/launch-browser.cjs +31 -4
  5. package/adapters/extension/profile-extension-state.cjs +156 -0
  6. package/adapters/extension/readiness.mjs +21 -2
  7. package/adapters/manifest.json +8 -0
  8. package/adapters/mobile/wait-for-bridge.cjs +9 -3
  9. package/dist/adapters/extension/actions.js +2 -1
  10. package/dist/adapters/extension/run.js +6 -0
  11. package/dist/adapters/mobile/action-inputs.js +20 -0
  12. package/dist/adapters/mobile/actions.js +18 -1
  13. package/dist/adapters/mobile/android-screenrecord.js +274 -0
  14. package/dist/adapters/mobile/readiness.js +2 -1
  15. package/dist/adapters/mobile/recording-outcome.js +54 -0
  16. package/dist/adapters/mobile/recording-target.js +11 -0
  17. package/dist/adapters/mobile/run.js +58 -15
  18. package/dist/adapters/mobile/ui.js +96 -2
  19. package/dist/adapters/perps-mainnet-cleanup.js +122 -0
  20. package/dist/adapters/perps-order-inputs.js +43 -28
  21. package/dist/adapters.js +4 -1
  22. package/dist/commands/doctor.js +33 -14
  23. package/dist/commands/status-probe.js +2 -1
  24. package/dist/engine.js +11 -2
  25. package/dist/funding-execution-context.js +13 -8
  26. package/dist/host.js +6 -2
  27. package/dist/index.js +3 -0
  28. package/dist/mm-harness-cli.js +4 -2
  29. package/dist/provider.js +72 -0
  30. package/dist/recipe-security.js +1 -0
  31. package/docs/CONTRIBUTING.md +7 -0
  32. package/docs/RECIPES.md +96 -9
  33. package/docs/RELEASE-QA-CAPABILITY-MAP.md +5 -1
  34. package/docs/cli-contract.md +1 -1
  35. package/library/actions/core/perps/read_account.mjs +4 -2
  36. package/library/actions/core/perps/read_orders.mjs +4 -2
  37. package/library/actions/core/perps/read_positions.mjs +4 -2
  38. package/library/actions/extension/analytics/consent.mjs +7 -2
  39. package/library/actions/extension/perps/compare_provider_market.mjs +12 -8
  40. package/library/actions/extension/perps/mutation-receipt.mjs +124 -26
  41. package/library/actions/extension/perps/perps.mjs +298 -39
  42. package/library/actions/extension/perps/read_snapshot.mjs +6 -2
  43. package/library/actions/extension/perps/set_network.mjs +6 -0
  44. package/library/actions/extension/platform/cdp.mjs +6 -2
  45. package/library/actions/extension/ui/navigate.mjs +8 -0
  46. package/library/actions/mobile/perps/perps.mjs +264 -102
  47. package/library/actions/mobile/perps/place_order.mjs +7 -4
  48. package/library/actions/mobile/platform/bridge.mjs +171 -10
  49. package/library/actions/mobile/wallet/home.mjs +7 -2
  50. package/library/actions/mobile/wallet/select_account.mjs +4 -0
  51. package/library/actions/shared/app/live-adapter-deadline.mjs +8 -0
  52. package/library/actions/shared/perps/mainnet-order-ledger.mjs +378 -0
  53. package/library/actions/shared/perps/order-policy.mjs +56 -19
  54. package/library/actions/shared/perps/rate-limit.mjs +93 -0
  55. package/library/actions/shared/perps/visible-state.mjs +3 -2
  56. package/library/actions/shared/wallet/fixture-accounts.mjs +9 -3
  57. package/library/manifests/extension.action-manifest.json +36 -5
  58. package/library/manifests/mobile.action-manifest.json +48 -10
  59. package/package.json +14 -9
  60. package/scripts/validate-release-report.mjs +14 -2
  61. package/site/assets/help-recipes.json +2 -1
@@ -0,0 +1,122 @@
1
+ import {
2
+ VENUE_ORDER_FIELDS,
3
+ cleanupOwns,
4
+ clearMainnetSwitch,
5
+ ledgerOrderTargets,
6
+ mainnetOrderLeftOpenError,
7
+ markRunAborting,
8
+ readHyperliquidVenue,
9
+ readMainnetOrderLedger,
10
+ settleMainnetOrder
11
+ } from "../../library/actions/shared/perps/mainnet-order-ledger.mjs";
12
+ const PENDING_SETTLE_MS = 1e4;
13
+ async function platformPerps(platform) {
14
+ if (platform === "mobile") return import("../../library/actions/mobile/perps/perps.mjs");
15
+ if (platform === "extension") return import("../../library/actions/extension/perps/perps.mjs");
16
+ throw new Error(`no mainnet order cleanup for platform ${platform}`);
17
+ }
18
+ async function currentEntry(projectRoot, id) {
19
+ return (await readMainnetOrderLedger(projectRoot)).entries.find((entry) => entry.id === id);
20
+ }
21
+ async function venueRead(entry) {
22
+ const venue = await readHyperliquidVenue(entry.address, entry.symbol);
23
+ const open = ledgerOrderTargets(entry, venue.openOrders, VENUE_ORDER_FIELDS);
24
+ return { source: venue.source, open: open.map((order) => order.orderId), positionSize: venue.positionSize };
25
+ }
26
+ async function closeOut(projectRoot, env, via, id) {
27
+ const deadline = Date.now() + PENDING_SETTLE_MS;
28
+ for (; ; ) {
29
+ const entry = await currentEntry(projectRoot, id);
30
+ if (!entry) return;
31
+ const cancelInApp = async () => (await platformPerps(entry.platform)).cancelMainnetLedgerOrder(entry, { projectRoot, env });
32
+ let proof = entry.state === "cancelled" ? { canceled: entry.cancelProof.canceled, read: null } : await cancelInApp();
33
+ let venue = await venueRead(entry);
34
+ if (entry.state === "cancelled" && venue.open.length > 0) {
35
+ proof = await cancelInApp();
36
+ venue = await venueRead(entry);
37
+ }
38
+ for (let wait = 0; venue.open.length > 0 && proof.canceled.length > 0 && wait < 5; wait += 1) {
39
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
40
+ venue = await venueRead(entry);
41
+ }
42
+ if (venue.open.length > 0) {
43
+ throw new Error(`the venue still lists order(s) ${venue.open.join(", ")} after the app ${proof.read?.notOpen ? "reported none open" : "cancelled"}`);
44
+ }
45
+ if (proof.read?.notOpen && entry.state === "pending" && Date.now() < deadline) {
46
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
47
+ continue;
48
+ }
49
+ const recorded = { via: entry.cancelProof?.via ?? via, canceled: proof.canceled, app: proof.read, venue };
50
+ const evidencePath = await settleMainnetOrder(projectRoot, entry, recorded);
51
+ process.stderr.write(
52
+ `mainnet ${entry.symbol} order ${entry.orderId ?? "(pending)"}: ${proof.canceled.length > 0 ? "cancelled" : "not open"} at run ${via}, gone at the venue; proof in ${evidencePath}
53
+ `
54
+ );
55
+ if (venue.positionSize !== Number(entry.baselinePositionSize ?? 0)) {
56
+ throw new Error(`the ${entry.symbol} position is ${venue.positionSize}, was ${entry.baselinePositionSize ?? 0} before the order: it filled; close that position now`);
57
+ }
58
+ return;
59
+ }
60
+ }
61
+ async function cleanup(projectRoot, env, via) {
62
+ const ledger = await readMainnetOrderLedger(projectRoot);
63
+ const failures = [];
64
+ for (const entry of ledger.entries.filter((record) => cleanupOwns(record, process.pid))) {
65
+ try {
66
+ await closeOut(projectRoot, env, via, entry.id);
67
+ } catch (error) {
68
+ failures.push({ entry, error });
69
+ }
70
+ }
71
+ if (failures.length > 0) throw mainnetOrderLeftOpenError(failures);
72
+ if (ledger.networkSwitch && cleanupOwns(ledger.networkSwitch, process.pid)) {
73
+ const { restoreTestnetAfterRun } = await import("../../library/actions/extension/perps/perps.mjs");
74
+ await restoreTestnetAfterRun(ledger.networkSwitch, { projectRoot });
75
+ await clearMainnetSwitch(projectRoot);
76
+ process.stderr.write(`Perps network switched back to testnet at run ${via}.
77
+ `);
78
+ }
79
+ }
80
+ let inFlight;
81
+ function cancelRunMainnetOrders(projectRoot, env, via) {
82
+ inFlight ??= cleanup(projectRoot, env, via).finally(() => {
83
+ inFlight = void 0;
84
+ });
85
+ return inFlight;
86
+ }
87
+ const ABORT_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
88
+ async function abortRunMainnetOrders(projectRoot) {
89
+ await markRunAborting(projectRoot, process.pid);
90
+ await cancelRunMainnetOrders(projectRoot, process.env, "abort");
91
+ }
92
+ async function withMainnetOrderAbort(projectRoot, run) {
93
+ let aborting;
94
+ const remove = () => {
95
+ for (const signal of ABORT_SIGNALS) process.off(signal, onSignal);
96
+ };
97
+ function onSignal(signal) {
98
+ if (aborting) return;
99
+ aborting = abortRunMainnetOrders(projectRoot).then(
100
+ () => void 0,
101
+ (error) => {
102
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
103
+ `);
104
+ }
105
+ ).then(() => {
106
+ remove();
107
+ process.kill(process.pid, signal);
108
+ });
109
+ }
110
+ for (const signal of ABORT_SIGNALS) process.on(signal, onSignal);
111
+ try {
112
+ return await run();
113
+ } finally {
114
+ if (aborting) await aborting;
115
+ else remove();
116
+ }
117
+ }
118
+ export {
119
+ abortRunMainnetOrders,
120
+ cancelRunMainnetOrders,
121
+ withMainnetOrderAbort
122
+ };
@@ -1,35 +1,50 @@
1
+ const ORDER_INPUTS = [
2
+ [["market", "symbol"], "market=<symbol> (or symbol=<symbol>)"],
3
+ [["side"], "side=long or side=short"],
4
+ [["amount", "notional"], "amount=<usd> or notional=<usd>"]
5
+ ];
6
+ function isRecord(value) {
7
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
8
+ }
9
+ function opensState(value) {
10
+ const state = typeof value === "string" ? value.toLowerCase() : void 0;
11
+ return state === "open" || state === "present";
12
+ }
13
+ function hasValue(source, names) {
14
+ return names.some((name) => {
15
+ const value = source[name];
16
+ return value !== void 0 && value !== null && value !== "";
17
+ });
18
+ }
19
+ function missingOrderInputs(label, has, pathFor) {
20
+ return ORDER_INPUTS.filter(([names]) => !has(names)).map(([names, wanted]) => ({
21
+ severity: "error",
22
+ code: "recipe.missing_param",
23
+ path: pathFor(names[0]),
24
+ message: `${label} requires ${wanted} before it may create an order`
25
+ }));
26
+ }
1
27
  function perpsOrderInputFindings(nodeId, node, { ensureChecksOnly }) {
2
28
  const action = node.action;
3
- const state = typeof node.state === "string" ? node.state.toLowerCase() : void 0;
4
- const createsOrder = action === "metamask.perps.place_order" || !ensureChecksOnly && (action === "metamask.perps.ensure_positions" || action === "metamask.perps.ensure_orders") && (state === "open" || state === "present");
5
- if (!createsOrder) return [];
6
- const findings = [];
7
- const requireOneOf = (names, message) => {
8
- const present = names.some((name) => {
9
- const value = node[name];
10
- return value !== void 0 && value !== null && value !== "";
29
+ if (action === "metamask.perps.start_state") {
30
+ if (ensureChecksOnly) return [];
31
+ return ["positions", "orders"].flatMap((key) => {
32
+ const block = node[key];
33
+ if (!isRecord(block) || !opensState(block.state)) return [];
34
+ return missingOrderInputs(
35
+ `${action} ${key}.state=${String(block.state)}`,
36
+ (names) => hasValue(block, names) || names[0] !== "amount" && hasValue(node, names),
37
+ (name) => `workflow.nodes.${nodeId}.${key}.${name}`
38
+ );
11
39
  });
12
- if (present) return;
13
- findings.push({
14
- severity: "error",
15
- code: "recipe.missing_param",
16
- path: `workflow.nodes.${nodeId}.${names[0]}`,
17
- message
18
- });
19
- };
20
- requireOneOf(
21
- ["market", "symbol"],
22
- `${action} requires market=<symbol> (or symbol=<symbol>) before it may create an order`
23
- );
24
- requireOneOf(
25
- ["side"],
26
- `${action} requires side=long or side=short before it may create an order`
27
- );
28
- requireOneOf(
29
- ["amount", "notional"],
30
- `${action} requires amount=<usd> or notional=<usd> before it may create an order`
40
+ }
41
+ const createsOrder = action === "metamask.perps.place_order" || !ensureChecksOnly && (action === "metamask.perps.ensure_positions" || action === "metamask.perps.ensure_orders") && opensState(node.state);
42
+ if (!createsOrder) return [];
43
+ return missingOrderInputs(
44
+ String(action),
45
+ (names) => hasValue(node, names),
46
+ (name) => `workflow.nodes.${nodeId}.${name}`
31
47
  );
32
- return findings;
33
48
  }
34
49
  export {
35
50
  perpsOrderInputFindings
package/dist/adapters.js CHANGED
@@ -118,7 +118,10 @@ async function runLiveFirst(platform, action, node, context, forceLive = false,
118
118
  platform,
119
119
  action,
120
120
  namespace: LIBRARY_ACTION_NAMESPACE,
121
- node: withImmediateCloseBudget(action, node),
121
+ node: withImmediateCloseBudget(
122
+ action,
123
+ await getAdapterSurface(platform).actions.liveProcessNode?.(action, node, { ...process.env, ...context.env }) ?? node
124
+ ),
122
125
  context,
123
126
  prepared: preparedLiveAdapters?.get(action),
124
127
  contextExtras: trustedMutationContextExtras(context),
@@ -85,11 +85,14 @@ async function handleDoctor({ options }) {
85
85
  } catch (error2) {
86
86
  runtimeProbeError2 = error2 instanceof Error ? error2.message : String(error2);
87
87
  }
88
- const ready = runtime2?.decision === "ready";
88
+ const recorder2 = runtime2?.decision === "ready" ? await readiness.recorder?.() : void 0;
89
+ const ready2 = doctorReady(runtime2, recorder2);
89
90
  const block = readiness.runtimeBlock?.(target, runtime2);
90
91
  if (block) failed.push(block.id);
92
+ if (recorder2?.status === "fail") failed.push(recorder2.id);
91
93
  const nextActions = doctorFixNextActions(failed, result2.fixture, adapter, target, block);
92
- if (failed.length === 0 && !ready && runtime2?.nextAction) nextActions.push(runtime2.nextAction);
94
+ if (recorder2?.status === "fail" && recorder2.userAction) nextActions.push(recorder2.userAction);
95
+ if (failed.length === 0 && runtime2?.decision !== "ready" && runtime2?.nextAction) nextActions.push(runtime2.nextAction);
93
96
  const status = result2.status === "pass" && failed.length === 0 ? "pass" : "fail";
94
97
  const retryDoctor = `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
95
98
  const fixUserAction = failed.includes("wallet-fixture") ? `choose one wallet fixture source from nextActions, run it, then retry: ${retryDoctor}` : nextActions[0] ?? retryDoctor;
@@ -98,15 +101,16 @@ async function handleDoctor({ options }) {
98
101
  message: "doctor --fix could not make every required check pass",
99
102
  userAction: fixUserAction
100
103
  } : void 0;
101
- if (json) console.log(JSON.stringify({ ...result2, status, fixed, failed, ready, runtime: runtime2, ...runtimeProbeError2 ? { runtimeProbeError: runtimeProbeError2 } : {}, nextActions, ...error ? { error } : {} }, null, 2));
104
+ if (json) console.log(JSON.stringify({ ...result2, status, fixed, failed, ready: ready2, runtime: runtime2, ...recorder2 ? { recorder: recorder2 } : {}, ...runtimeProbeError2 ? { runtimeProbeError: runtimeProbeError2 } : {}, nextActions, ...error ? { error } : {} }, null, 2));
102
105
  else {
103
- console.log(`${status} ${adapter} ${result2.compatibilityMode} ready=${ready} manifest=${actionManifestPath} fixed=[${fixed.join(",")}] failed=[${failed.join(",")}]`);
106
+ console.log(`${status} ${adapter} ${result2.compatibilityMode} ready=${ready2} manifest=${actionManifestPath} fixed=[${fixed.join(",")}] failed=[${failed.join(",")}]`);
104
107
  if (runtime2) {
105
108
  console.log(`runtime: decision=${runtime2.decision}${runtime2.reasonCode ? ` (${runtime2.reasonCode})` : ""}`);
106
109
  for (const reason of runtime2.reasons) console.log(` ${reason}`);
107
110
  } else if (runtimeProbeError2) {
108
111
  console.log(`runtime probe failed: ${runtimeProbeError2}`);
109
112
  }
113
+ if (recorder2) console.log(`recorder: ${recorder2.status} ${recorder2.message}`);
110
114
  for (const action of nextActions) console.error(` Next: ${action}`);
111
115
  }
112
116
  return status === "pass" ? 0 : 1;
@@ -154,10 +158,12 @@ async function handleDoctor({ options }) {
154
158
  runtimeProbeError = error instanceof Error ? error.message : String(error);
155
159
  }
156
160
  const runtimeCheck = doctorRuntimeCheck(runtime, runtimeProbeError, expectLive);
161
+ const recorder = runtime?.decision === "ready" ? await readiness.recorder?.() : void 0;
157
162
  const checks = [
158
163
  ...result.checks,
159
164
  runtimeCheck,
160
165
  ...await readiness.liveChecks?.(target) ?? [],
166
+ ...recorder ? [recorder] : [],
161
167
  ...await platformDoctorChecks(adapter, target),
162
168
  // Which library plugin code this process loaded: library, module and digest.
163
169
  ...adapterPluginChecks()
@@ -182,6 +188,7 @@ async function handleDoctor({ options }) {
182
188
  adapter,
183
189
  target,
184
190
  runtime,
191
+ recorder,
185
192
  doctorResult,
186
193
  orphanMetros,
187
194
  capture,
@@ -201,7 +208,8 @@ async function handleDoctor({ options }) {
201
208
  )?.userAction ?? `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
202
209
  const doctorError = doctorResult.status === "fail" ? { code: "DOCTOR_CHECKS_FAILED", message: "one or more required doctor checks failed", userAction: doctorUserAction } : void 0;
203
210
  const next = doctorError?.userAction ?? discovery.deviceDiscoveryErrors?.[0]?.userAction ?? (runtime?.decision === "ready" ? void 0 : runtime?.nextAction);
204
- if (json) console.log(JSON.stringify({ ...doctorResult, ready: runtime?.decision === "ready", runtime, view, orphanMetros, capture, skills, update, featureFlags, devices, additionalReachableDevices, ...discovery, ...next ? { next } : {}, ...doctorError ? { error: doctorError } : {} }, null, 2));
211
+ const ready = doctorReady(runtime, recorder);
212
+ if (json) console.log(JSON.stringify({ ...doctorResult, ready, runtime, ...recorder ? { recorder } : {}, view, orphanMetros, capture, skills, update, featureFlags, devices, additionalReachableDevices, ...discovery, ...next ? { next } : {}, ...doctorError ? { error: doctorError } : {} }, null, 2));
205
213
  else {
206
214
  const out = (style, text) => color(style, text, { stream: process.stdout });
207
215
  const stateStyle = (value, good) => value === good ? "ok" : "warn";
@@ -222,6 +230,7 @@ async function handleDoctor({ options }) {
222
230
  } else if (runtimeProbeError) {
223
231
  console.log(`${out("err", "runtime probe failed:")} ${out("dim", runtimeProbeError)}`);
224
232
  }
233
+ if (recorder) console.log(`${out("label", "recorder:")} ${out(recorder.status === "pass" ? "ok" : "err", recorder.status)} ${recorder.message}`);
225
234
  console.log(renderCheckoutView(view));
226
235
  console.log(`${out("label", "feature flags:")} ${out(
227
236
  featureFlags.error || (featureFlags.overrideCount ?? 0) > 0 ? "warn" : "dim",
@@ -246,6 +255,9 @@ async function handleDoctor({ options }) {
246
255
  }
247
256
  return doctorResult.status === "pass" ? 0 : 1;
248
257
  }
258
+ function doctorReady(runtime, recorder) {
259
+ return runtime?.decision === "ready" && recorder?.status !== "fail";
260
+ }
249
261
  async function platformDoctorChecks(adapter, target) {
250
262
  try {
251
263
  return await getAdapterSurface(adapter).doctor?.(target) ?? [];
@@ -297,17 +309,23 @@ function doctorFixNextActions(failed, fixture, adapter, target, block) {
297
309
  if (block?.userAction && failed.includes(block.id)) actions.push(block.userAction);
298
310
  return actions;
299
311
  }
300
- function emitExpectLiveVerdict(adapter, target, runtime, verdict) {
312
+ function emitExpectLiveVerdict(adapter, target, runtime, verdict, recorder) {
301
313
  const out = (style, text) => color(style, text, { stream: process.stderr });
302
314
  const detail = runtime ? `${runtime.decision}${runtime.reasonCode ? ` (${runtime.reasonCode})` : ""}` : "runtime probe unavailable";
303
315
  console.error(`${out("err", verdict)} ${out("bold", adapter)} ${detail} ${out("dim", target)}`);
304
316
  for (const reason of runtime?.reasons ?? []) console.error(` ${out("dim", reason)}`);
317
+ if (recorder?.status === "fail") {
318
+ console.error(` ${out("err", "recorder:")} ${recorder.message}${recorder.detail ? ` ${out("dim", recorder.detail)}` : ""}`);
319
+ console.error(` Next: ${recorder.userAction}`);
320
+ return;
321
+ }
305
322
  console.error(` Next: ${runtime?.nextAction ?? getAdapterSurface(adapter).hints.runtimeProbeRecovery(target)}`);
306
323
  }
307
- function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture, devices, additionalReachableDevices, json, printReady, discovery) {
324
+ function emitExpectLive(adapter, target, runtime, recorder, result, orphanMetros, capture, devices, additionalReachableDevices, json, printReady, discovery) {
308
325
  const live = runtime?.decision === "ready";
326
+ const ready = doctorReady(runtime, recorder);
309
327
  if (printReady) {
310
- if (live) {
328
+ if (ready) {
311
329
  const indicator = farmslotReadyIndicator(adapter, devices);
312
330
  if (indicator) {
313
331
  console.log(indicator);
@@ -316,21 +334,21 @@ function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture,
316
334
  emitExpectLiveVerdict(adapter, target, runtime, "not-ready");
317
335
  return EXIT.runtime;
318
336
  }
319
- emitExpectLiveVerdict(adapter, target, runtime, "not-live");
337
+ emitExpectLiveVerdict(adapter, target, runtime, live ? "not-ready" : "not-live", recorder);
320
338
  return EXIT.runtime;
321
339
  }
322
340
  if (json) {
323
341
  const userAction = runtime?.nextAction ?? getAdapterSurface(adapter).hints.runtimeProbeRecovery(target);
324
- const error = live ? void 0 : { code: "RUNTIME_NOT_LIVE", message: `${adapter} runtime is not live`, userAction };
325
- console.log(JSON.stringify({ ...result, ready: live, runtime, orphanMetros, capture, devices, additionalReachableDevices, ...discovery, ...error ? { error } : {} }, null, 2));
326
- return live ? EXIT.ok : EXIT.runtime;
342
+ const error = !live ? { code: "RUNTIME_NOT_LIVE", message: `${adapter} runtime is not live`, userAction } : !ready ? { code: "RECORDER_UNAVAILABLE", message: recorder.message, userAction: recorder.userAction } : void 0;
343
+ console.log(JSON.stringify({ ...result, ready, runtime, ...recorder ? { recorder } : {}, orphanMetros, capture, devices, additionalReachableDevices, ...discovery, ...error ? { error } : {} }, null, 2));
344
+ return ready ? EXIT.ok : EXIT.runtime;
327
345
  }
328
346
  const out = (style, text) => color(style, text, { stream: process.stdout });
329
- if (live) {
347
+ if (ready) {
330
348
  console.log(`${out("ok", "live")} ${out("bold", adapter)} runtime ready`);
331
349
  return EXIT.ok;
332
350
  }
333
- emitExpectLiveVerdict(adapter, target, runtime, "not-live");
351
+ emitExpectLiveVerdict(adapter, target, runtime, live ? "not-ready" : "not-live", recorder);
334
352
  return EXIT.runtime;
335
353
  }
336
354
  function captureHealth(providers) {
@@ -457,6 +475,7 @@ async function runDoctorFix(adapter, target, manifestValidation, json) {
457
475
  }
458
476
  export {
459
477
  doctorFixNextActions,
478
+ doctorReady,
460
479
  handleDoctor,
461
480
  skillsView
462
481
  };
@@ -3,6 +3,7 @@ import fs from "node:fs";
3
3
  import { fixtureSummary } from "../doctor.js";
4
4
  import { normalizeFeatureFlagRecord } from "../feature-flags.js";
5
5
  import { mobileBridgeScript } from "../adapters/mobile/bridge-script.js";
6
+ import { isAndroidEmulatorSerial } from "../../library/actions/mobile/platform/bridge.mjs";
6
7
  const PROBE_TIMEOUT_MS = 2e3;
7
8
  function truncateAddress(address) {
8
9
  if (!address || address.length <= 10) return address;
@@ -51,7 +52,7 @@ function parseBridgeEntries(raw) {
51
52
  function androidModelMatches(deviceName, model, device, devices) {
52
53
  if (deviceName === model || deviceName.startsWith(`${model} -`)) return true;
53
54
  if (!deviceName.endsWith(` ${model}`) && !deviceName.includes(` ${model} -`)) return false;
54
- const physical = (d) => d.platform === "android" && !d.id.startsWith("emulator-");
55
+ const physical = (d) => d.platform === "android" && !isAndroidEmulatorSerial(d.id);
55
56
  if (!physical(device)) return false;
56
57
  return devices.filter((d) => physical(d) && d.name === device.name).length === 1;
57
58
  }
package/dist/engine.js CHANGED
@@ -6,11 +6,15 @@ import {
6
6
  fixturePolicyMutationBase,
7
7
  fundingOptionsFromCli,
8
8
  isFixturePolicyBase,
9
- loadTrustedMutationBase
9
+ loadTrustedMutationBase,
10
+ validateTrustedMutationPlan
10
11
  } from "./funding-execution-context.js";
11
12
  import { resolveActionManifest, validateManifest } from "./manifest.js";
12
13
  import { LIBRARY_ACTION_NAMESPACE, runnerDir } from "./paths.js";
13
14
  import { metaMaskActionExecutionCapabilities } from "./recipe-security.js";
15
+ async function loadEngineMutation({ cli, adapter, ...run }) {
16
+ return await loadTrustedMutationBase({ ...fundingOptionsFromCli(cli), ...run }) ?? (getAdapterSurface(adapter).actions.trustedMutation === true ? fixturePolicyMutationBase({ artifactsDir: run.artifactsDir, rootRecipe: run.rootRecipe }) : void 0);
17
+ }
14
18
  const metaMaskEngine = {
15
19
  bundledLibrary: {
16
20
  name: "metamask",
@@ -24,12 +28,17 @@ const metaMaskEngine = {
24
28
  const { createMetaMaskRunner } = await import("./runner.js");
25
29
  return createMetaMaskRunner(adapter, manifest, options);
26
30
  },
31
+ // Preflight checks policy; execution also claims the plan and checks claim conflicts.
27
32
  // Funding files from the run's command line, bound to the run's plan. Without
28
33
  // them, a platform whose actions take a trusted mutation (the Extension) runs a
29
34
  // plan with external mutations in the fixture-policy lane, where each action
30
35
  // applies the shared order policy; any other plan runs without one.
31
36
  trustedMutation: {
32
- load: async ({ cli, adapter, ...run }) => await loadTrustedMutationBase({ ...fundingOptionsFromCli(cli), ...run }) ?? (getAdapterSurface(adapter).actions.trustedMutation === true ? fixturePolicyMutationBase({ artifactsDir: run.artifactsDir, rootRecipe: run.rootRecipe }) : void 0),
37
+ preflight: async (input, plan) => {
38
+ const base = await loadEngineMutation(input);
39
+ if (base) validateTrustedMutationPlan(base, plan);
40
+ },
41
+ load: loadEngineMutation,
33
42
  // Nothing to bind (undefined): recipe-cli keeps the runner it planned with.
34
43
  authorize: async (base, plan) => {
35
44
  if (!base) return void 0;
@@ -181,10 +181,9 @@ function fixturePolicyMutationBase(options) {
181
181
  function isFixturePolicyBase(base) {
182
182
  return base?.proofLane === FIXTURE_POLICY_LANE;
183
183
  }
184
- async function authorizeFixturePolicyPlan(base, plan) {
184
+ async function authorizeFixturePolicyPlan(base, executionPlanSha256) {
185
185
  const source = fixturePolicySources.get(base);
186
186
  if (!source) throw new Error("Fixture-policy plan authorization requires the runner-created context.");
187
- const executionPlanSha256 = normalizeRecipeDigest(plan.digest);
188
187
  await mkdir(source.artifactsDir, { recursive: true, mode: 448 });
189
188
  const artifactsDir = await realpath(source.artifactsDir);
190
189
  const artifactsIdentitySha256 = await directoryIdentity(artifactsDir);
@@ -204,11 +203,9 @@ async function authorizeFixturePolicyPlan(base, plan) {
204
203
  });
205
204
  return authorized;
206
205
  }
207
- async function authorizeTrustedMutationPlan(base, plan, testIo = {}) {
208
- if (isFixturePolicyBase(base)) return authorizeFixturePolicyPlan(base, plan);
209
- const source = fundingSources.get(base);
210
- if (!source) throw new Error("Funding plan authorization requires the original runner-verified context.");
206
+ function validateTrustedMutationPlan(base, plan) {
211
207
  const executionPlanSha256 = normalizeRecipeDigest(plan.digest);
208
+ if (isFixturePolicyBase(base)) return { executionPlanSha256, operationCounts: [] };
212
209
  const nodes = Array.isArray(plan.nodes) ? plan.nodes : [];
213
210
  const actual = /* @__PURE__ */ new Map();
214
211
  for (const node of nodes) {
@@ -235,9 +232,16 @@ async function authorizeTrustedMutationPlan(base, plan, testIo = {}) {
235
232
  }
236
233
  }
237
234
  if (actual.size === 0) throw new Error("Funding documents require a resolved external-mutation plan.");
235
+ const operationCounts = [...expected.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([action, count]) => ({ action, count }));
236
+ return { executionPlanSha256, operationCounts };
237
+ }
238
+ async function authorizeTrustedMutationPlan(base, plan, testIo = {}) {
239
+ const { executionPlanSha256, operationCounts } = validateTrustedMutationPlan(base, plan);
240
+ if (isFixturePolicyBase(base)) return authorizeFixturePolicyPlan(base, executionPlanSha256);
241
+ const source = fundingSources.get(base);
242
+ if (!source) throw new Error("Funding plan authorization requires the original runner-verified context.");
238
243
  await mkdir(source.artifactsDir, { recursive: true, mode: 448 });
239
244
  const artifactsIdentitySha256 = await directoryIdentity(source.artifactsDir);
240
- const operationCounts = [...expected.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([action, count]) => ({ action, count }));
241
245
  const runId = sha256(canonical({
242
246
  requestSha256: base.requestSha256,
243
247
  preflightSha256: base.preflightSha256,
@@ -1529,5 +1533,6 @@ export {
1529
1533
  selectedIdentityFromWalletImport,
1530
1534
  serializeTrustedMutationContext,
1531
1535
  trustedMutationContextExtras,
1532
- trustedMutationForAction
1536
+ trustedMutationForAction,
1537
+ validateTrustedMutationPlan
1533
1538
  };
package/dist/host.js CHANGED
@@ -3,7 +3,7 @@ import { fileURLToPath } from "node:url";
3
3
  import { configureHarnessAdapters, configureHarnessHost } from "@farmslot/recipe-cli/harness";
4
4
  import { adapterRegistry } from "./adapters/registry.js";
5
5
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
6
- configureHarnessHost({
6
+ const providerHost = {
7
7
  name: "mm-harness",
8
8
  product: "MetaMask",
9
9
  envPrefix: "MM_HARNESS",
@@ -28,5 +28,9 @@ configureHarnessHost({
28
28
  "stop",
29
29
  "verify"
30
30
  ]
31
- });
31
+ };
32
+ configureHarnessHost(providerHost);
32
33
  configureHarnessAdapters(adapterRegistry);
34
+ export {
35
+ providerHost
36
+ };
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import "./host.js";
2
+ import { createProvider, providerHost } from "./provider.js";
2
3
  import { createDoctorReport } from "./doctor.js";
3
4
  import { loadActionManifest, validateManifest } from "./manifest.js";
4
5
  import { createMetaMaskExtensionRunner, createMetaMaskMobileRunner, createMetaMaskRunner } from "./runner.js";
@@ -13,10 +14,12 @@ export {
13
14
  createMetaMaskExtensionRunner,
14
15
  createMetaMaskMobileRunner,
15
16
  createMetaMaskRunner,
17
+ createProvider,
16
18
  extensionIdPath,
17
19
  importRecipeHarnessRuntimeBrowserExtension,
18
20
  importRecipeHarnessRuntimeCdp,
19
21
  loadActionManifest,
22
+ providerHost,
20
23
  validateManifest,
21
24
  walletFixturePath
22
25
  };
@@ -22,6 +22,7 @@ import {
22
22
  } from "@farmslot/recipe-cli/harness";
23
23
  import { stopProjectCollector } from "../library/actions/shared/analytics/collector.mjs";
24
24
  import { adoptPluginAdapter, afterAdapterLoad, configuredLibrarySources } from "./adapters/plugin-surface.js";
25
+ import { withMainnetOrderAbort } from "./adapters/perps-mainnet-cleanup.js";
25
26
  import { adapterRegistry } from "./adapters/registry.js";
26
27
  import {
27
28
  explainMissingAdapterLibrary,
@@ -155,8 +156,9 @@ const PUBLIC_RUN = {
155
156
  "execution-template": { run: handleExecutionTemplate },
156
157
  actions: { run: (args) => handleActions(parseArgs(args), { catalog: metaMaskEngine }) },
157
158
  stop: { run: (args) => handleStop(args, stopOptions) },
158
- call: { run: (args) => handleCall(args, callOptions) },
159
- run: { run: (args) => handleRun(args, runOptions) },
159
+ // A signal during run or call still cancels the run's mainnet orders.
160
+ call: { run: (args) => withMainnetOrderAbort(targetPath(parseArgs(args).options), () => handleCall(args, callOptions)) },
161
+ run: { run: (args) => withMainnetOrderAbort(targetPath(parseArgs(args).options), () => handleRun(args, runOptions)) },
160
162
  last: { run: (args) => handleLast(parseArgs(args)) },
161
163
  doctor: { run: (args) => handleDoctor(parseArgs(args)) },
162
164
  check: { run: handleCheck },
@@ -0,0 +1,72 @@
1
+ import "./adapters/surface.js";
2
+ import {
3
+ configureHarnessAdapters,
4
+ adapterForPlatform,
5
+ applyRuntimeDirOption,
6
+ configureHarnessHost,
7
+ ensureAdapterLoaded,
8
+ optionFlag,
9
+ optionStrings,
10
+ usageError
11
+ } from "@farmslot/recipe-cli/harness";
12
+ import { MAINNET_RUN_OWNER_ENV } from "../library/actions/shared/perps/mainnet-order-ledger.mjs";
13
+ import { adoptPluginAdapter, afterAdapterLoad, configuredLibrarySources, operatorLibraryEnv } from "./adapters/plugin-surface.js";
14
+ import { abortRunMainnetOrders, cancelRunMainnetOrders } from "./adapters/perps-mainnet-cleanup.js";
15
+ import { adapterRegistry } from "./adapters/registry.js";
16
+ import { getAdapterSurface } from "./adapters/surface.js";
17
+ import { applyDeviceTargeting } from "./commands/device-target.js";
18
+ import { metaMaskEngine } from "./engine.js";
19
+ import { providerHost } from "./host.js";
20
+ import { providerHost as providerHost2 } from "./host.js";
21
+ async function createProvider(context) {
22
+ configureHarnessHost(context.host ?? providerHost);
23
+ configureHarnessAdapters(adapterRegistry);
24
+ const selection = context.adapter?.value;
25
+ if (!selection) throw usageError("The MetaMask provider requires a resolved adapter.");
26
+ const adapter = adapterForPlatform(selection) ?? selection;
27
+ const options = {
28
+ ...context.options,
29
+ ...!context.options?.platform && (selection === "ios" || selection === "android") ? { platform: selection } : {}
30
+ };
31
+ await ensureAdapterLoaded(adapter, {
32
+ libraries: optionStrings(options, "library"),
33
+ configured: context.libraries ?? configuredLibrarySources(),
34
+ env: operatorLibraryEnv(),
35
+ adopt: adoptPluginAdapter
36
+ });
37
+ await afterAdapterLoad();
38
+ const runtime = getAdapterSurface(adapter);
39
+ const executes = (context.command === "run" || context.command === "call") && !["plan", "list", "describe", "help"].some((flag) => optionFlag(options, flag));
40
+ if (executes) {
41
+ applyRuntimeDirOption(options);
42
+ runtime.resolveSlotPorts(context.target.value);
43
+ const selected = applyDeviceTargeting(context.command, adapter, {
44
+ ...options,
45
+ target: context.target.value
46
+ }, { gate: true, rerun: "" });
47
+ if (selected.ok === false) throw Object.assign(usageError(selected.message), { code: selected.code, userAction: selected.userAction });
48
+ }
49
+ const previousOwner = process.env[MAINNET_RUN_OWNER_ENV];
50
+ if (executes) process.env[MAINNET_RUN_OWNER_ENV] = String(process.pid);
51
+ return {
52
+ runtime,
53
+ engine: metaMaskEngine,
54
+ ...executes ? {
55
+ async cancel() {
56
+ await abortRunMainnetOrders(context.target.value);
57
+ },
58
+ async finalize() {
59
+ try {
60
+ await cancelRunMainnetOrders(context.target.value, process.env, "teardown");
61
+ } finally {
62
+ if (previousOwner === void 0) delete process.env[MAINNET_RUN_OWNER_ENV];
63
+ else process.env[MAINNET_RUN_OWNER_ENV] = previousOwner;
64
+ }
65
+ }
66
+ } : {}
67
+ };
68
+ }
69
+ export {
70
+ createProvider,
71
+ providerHost2 as providerHost
72
+ };
@@ -76,6 +76,7 @@ const APP_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
76
76
  "metamask.perps.open_balance_action",
77
77
  "metamask.perps.assert_visible_consistency",
78
78
  "metamask.perps.start_state",
79
+ "metamask.perps.set_network",
79
80
  "metamask.perps.teardown_state",
80
81
  // Consent changes wallet state and is therefore an app mutation.
81
82
  "metamask.analytics.set_consent",
@@ -199,6 +199,13 @@ when packed, so `agent-runtime` and `adapter-rn` can retain older copies until
199
199
  republished. Verify the complete install with
200
200
  `bash tests/contract/packaging-dist-install.test.sh` before releasing the harness.
201
201
 
202
+ `yarn install` hardlinks every `node_modules` file to Yarn's store
203
+ (`~/.yarn/berry/store`), shared by all worktrees. Never edit an installed
204
+ dependency in place: a write into an existing file reaches the store and every
205
+ other worktree, and a reinstall does not undo it. Use `yarn dev:link-farmslot`
206
+ for local Farmslot builds. To repair a package, remove `node_modules/<package>`
207
+ in one worktree and run `yarn install`.
208
+
202
209
  Docs describe only current behavior. History belongs in the changelog and Git;
203
210
  do not add handovers, audits, speculative roadmaps, or duplicate command guides
204
211
  under `docs/`.