@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
@@ -3,6 +3,8 @@
3
3
 
4
4
  import { pathToFileURL } from 'node:url';
5
5
  import { createHash } from 'node:crypto';
6
+ import { readFile, writeFile } from 'node:fs/promises';
7
+ import path from 'node:path';
6
8
  import { dataTestId, marketSymbol, normalizeMarketSymbol, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
7
9
  import {
8
10
  assertTrustedExternalMutationTarget,
@@ -14,11 +16,19 @@ import {
14
16
  trustedExternalMutationBudget,
15
17
  trustedExternalMutationFundingRequirement,
16
18
  trustedExternalMutationIdentity,
19
+ inFixturePolicyLane,
20
+ withFixturePolicyContext,
17
21
  withExternalMutationRecovery,
18
22
  } from './mutation-receipt.mjs';
19
23
  import { readSnapshot } from './read_snapshot.mjs';
20
24
  import { readPerpsRuntimeStateExpression } from './state.mjs';
21
25
  import { readWalletStateExpression } from '../wallet/state.mjs';
26
+ import {
27
+ assertFlatBeforeRoundTrip,
28
+ assertHouseLimitPosition,
29
+ closeImmediatelyRequested,
30
+ requireHouseNotional,
31
+ } from '../../shared/perps/order-policy.mjs';
22
32
  import {
23
33
  orderIdentityMatches,
24
34
  parseOrderCard,
@@ -44,6 +54,10 @@ function sideForItem(item) {
44
54
  const side = String(item?.side ?? item?.direction ?? '').toLowerCase();
45
55
  if (side === 'buy' || side === 'b') return 'long';
46
56
  if (side === 'sell' || side === 'a' || side === 's') return 'short';
57
+ if (side) return side;
58
+ // Positions carry no side, only a signed size (long > 0, short < 0).
59
+ const size = Number(item?.size ?? item?.szi);
60
+ if (Number.isFinite(size) && size !== 0) return size > 0 ? 'long' : 'short';
47
61
  return side;
48
62
  }
49
63
 
@@ -222,6 +236,9 @@ export async function closePositions(input) {
222
236
  }
223
237
 
224
238
  async function closeOrderThroughBackground(input, page, timeoutMs) {
239
+ ({ input } = await withFixturePolicyContext(input, page, {
240
+ operations: [{ operation: 'perps-cancel-order', amount: String(input.node?.amount ?? '') }],
241
+ }));
225
242
  await requirePerpsTradingContext(input, page);
226
243
  const symbol = marketSymbol(input);
227
244
  const side = String(input.node?.side ?? '').toLowerCase();
@@ -414,6 +431,9 @@ export async function closeOrders(input) {
414
431
  partialCloseAmount: null,
415
432
  roundingToleranceBaseUnits,
416
433
  };
434
+ ({ input } = await withFixturePolicyContext(input, page, {
435
+ operations: [{ operation: 'perps-cancel-order', amount }],
436
+ }));
417
437
  const priorLookupIdentity = trustedExternalMutationIdentity(input, page, operation);
418
438
  const priorPlace = await readPriorMutationReceipt(
419
439
  input,
@@ -791,8 +811,24 @@ async function observeBackgroundPlace(page, expected, baselineIds, timeoutMs) {
791
811
  throw new Error('Exact background Perps placement postcondition was not proven.');
792
812
  }
793
813
 
814
+ // Page source that returns { changed: true } unless the selected account and the
815
+ // Perps network are still the ones the order rule allowed. It runs in the same
816
+ // evaluation as the request it guards, with no await between the comparison and
817
+ // the request. The state reads themselves are awaited, so a switch inside that
818
+ // millisecond window can't be ruled out from the page; anything earlier is.
819
+ function boundContextGuard(bound) {
820
+ if (!bound) return '';
821
+ return `
822
+ const __wallet = await (${readWalletStateExpression()});
823
+ const __perps = await (${readPerpsRuntimeStateExpression()});
824
+ const __address = String(__wallet?.selectedAccount?.address ?? '').toLowerCase();
825
+ const __network = __perps?.networkKnown ? (__perps.isTestnet ? 'testnet' : 'mainnet') : null;
826
+ if (__address !== ${JSON.stringify(bound.address)} || __network !== ${JSON.stringify(bound.network)}) return { changed: true };`;
827
+ }
828
+
794
829
  async function dispatchBackgroundPlace(page, {
795
830
  amount,
831
+ bound = null,
796
832
  isBuy,
797
833
  leverage,
798
834
  limitPrice,
@@ -804,6 +840,7 @@ async function dispatchBackgroundPlace(page, {
804
840
  return page.evaluate(`(async () => {
805
841
  const request = globalThis.stateHooks?.submitRequestToBackground;
806
842
  if (typeof request !== 'function') throw new Error('stateHooks.submitRequestToBackground is unavailable; cannot place live Perps order.');
843
+ ${boundContextGuard(bound)}
807
844
  const currentPrice = Number(${JSON.stringify(resolvedPrice)});
808
845
  const usdAmount = ${JSON.stringify(amount)};
809
846
  const leverage = ${JSON.stringify(leverage)};
@@ -1167,6 +1204,8 @@ async function observeExactVisibleOrderResult(page, expected, receipt, timeoutMs
1167
1204
 
1168
1205
  async function placeOrderThroughBackground(input, page, {
1169
1206
  amount,
1207
+ bound = null,
1208
+ onDispatch = () => {},
1170
1209
  expected,
1171
1210
  identity,
1172
1211
  isBuy,
@@ -1224,8 +1263,10 @@ async function placeOrderThroughBackground(input, page, {
1224
1263
  } catch (error) {
1225
1264
  throw externalMutationNotDispatched(error instanceof Error ? error.message : String(error));
1226
1265
  }
1227
- return dispatchBackgroundPlace(page, {
1266
+ onDispatch();
1267
+ const accepted = await dispatchBackgroundPlace(page, {
1228
1268
  amount,
1269
+ bound,
1229
1270
  isBuy,
1230
1271
  leverage,
1231
1272
  limitPrice,
@@ -1234,6 +1275,10 @@ async function placeOrderThroughBackground(input, page, {
1234
1275
  resolvedPrice,
1235
1276
  symbol,
1236
1277
  });
1278
+ if (accepted?.changed) {
1279
+ throw externalMutationNotDispatched('the selected account or Perps network changed after the order rule allowed it');
1280
+ }
1281
+ return accepted;
1237
1282
  },
1238
1283
  observe: async () => observeBackgroundPlace(
1239
1284
  page,
@@ -1285,6 +1330,16 @@ export async function requirePerpsTradingContext(input, page) {
1285
1330
  throw new Error('Source-dev funded mutation selected account changed after its visible identity proof.');
1286
1331
  }
1287
1332
  }
1333
+ if (trusted?.proofLane === 'fixture-policy') {
1334
+ // The order rule allowed the account selected when the context was completed;
1335
+ // dispatch only from that same account.
1336
+ const wallet = await page.evaluate(readWalletStateExpression());
1337
+ const address = String(wallet?.selectedAccount?.address ?? '').toLowerCase();
1338
+ if (!/^0x[0-9a-f]{40}$/u.test(address)
1339
+ || createHash('sha256').update(address).digest('hex') !== trusted.selectedIdentitySha256) {
1340
+ throw new Error('Perps mutation selected account changed after the order rule allowed it.');
1341
+ }
1342
+ }
1288
1343
  if (actualProvider !== provider || actualNetwork !== network) {
1289
1344
  throw new Error(
1290
1345
  `Perps mutation expected ${provider}/${network}, got ${actualProvider || 'unknown'}/${actualNetwork}.`,
@@ -1565,7 +1620,7 @@ export async function placeOrder(input) {
1565
1620
  if (fromCurrentForm && (positionMode !== 'new' || submitAttempts !== 1)) {
1566
1621
  throw new Error('Prepared-form placement requires position_mode=new and submit_attempts=1.');
1567
1622
  }
1568
- const roundingToleranceBaseUnits = String(input.node?.identity_rounding_tolerance_base_units ?? '10000');
1623
+ let roundingToleranceBaseUnits = String(input.node?.identity_rounding_tolerance_base_units ?? '10000');
1569
1624
  if (!/^(?:0|[1-9][0-9]{0,6})$/u.test(roundingToleranceBaseUnits)
1570
1625
  || BigInt(roundingToleranceBaseUnits) > 1_000_000n) {
1571
1626
  throw new Error(`${input.action} identity_rounding_tolerance_base_units must be 0..1000000.`);
@@ -1581,7 +1636,55 @@ export async function placeOrder(input) {
1581
1636
  if (!Number.isInteger(submitAttempts) || submitAttempts < 1 || submitAttempts > 2) {
1582
1637
  throw new Error(`${input.action} submit_attempts must be 1 or 2.`);
1583
1638
  }
1584
- return withExtensionPage(input, async (page) => {
1639
+ const closeImmediately = closeImmediatelyRequested(input.node);
1640
+ if (closeImmediately && !inFixturePolicyLane(input)) {
1641
+ throw new Error('close_immediately runs without funding files; a funded run closes through its own authorized close node.');
1642
+ }
1643
+ if (closeImmediately && (positionMode !== 'new' || fromCurrentForm)) {
1644
+ throw new Error('close_immediately needs position_mode=new and a background placement.');
1645
+ }
1646
+ // Set once the market is proven flat in this action: only then is a position
1647
+ // in it after a failed placement this order's own.
1648
+ // Set at the actual dispatch (or by a prior receipt that may have dispatched):
1649
+ // only then can a failure have left a position, so only then may cleanup run.
1650
+ let dispatchMayHaveHappened = false;
1651
+ let houseNotional = null;
1652
+ // The account and network the order rule allowed, rechecked before any close.
1653
+ let bound = null;
1654
+ const place = () => withExtensionPage(input, async (page) => {
1655
+ const lane = await withFixturePolicyContext(input, page, {
1656
+ operations: [
1657
+ { operation: orderType === 'limit' ? 'perps-limit-order' : 'perps-market-order', amount, order: true },
1658
+ ],
1659
+ order: { amount, leverage, orderType, closeImmediately },
1660
+ });
1661
+ input = lane.input;
1662
+ if (lane.network) {
1663
+ const wallet = await page.evaluate(readWalletStateExpression());
1664
+ const address = String(wallet?.selectedAccount?.address ?? '').toLowerCase();
1665
+ if (createHash('sha256').update(address).digest('hex') !== input.context?.trustedMutation?.selectedIdentitySha256) {
1666
+ throw new Error('Perps mutation selected account changed after the order rule allowed it.');
1667
+ }
1668
+ bound = { address, network: lane.network };
1669
+ }
1670
+ if (closeImmediately) {
1671
+ if (input.node?.identity_rounding_tolerance_base_units == null) {
1672
+ // The fill's margin differs from the request by up to one size step plus
1673
+ // slippage, more than the default cent: check it fits the $1 cap.
1674
+ const price = await readCurrentMarketPrice(page, symbol, input.node?.current_price ?? input.node?.currentPrice);
1675
+ const szDecimals = await readSizeDecimals(page, symbol);
1676
+ const slippage = Number(input.node?.max_slippage_bps ?? input.node?.maxSlippageBps ?? 300) / 10_000;
1677
+ const margin = Number(amount);
1678
+ if (szDecimals == null) throw new Error(`close_immediately cannot read the ${symbol} size decimals; not dispatched.`);
1679
+ const tolerance = Math.ceil(((10 ** -Number(szDecimals)) * Number(price) / leverage + margin * slippage) * 1_000_000) + 10_000;
1680
+ if (!Number.isInteger(Number(szDecimals)) || !Number.isFinite(tolerance) || tolerance > 1_000_000) {
1681
+ throw new Error(`close_immediately cannot prove a ${symbol} fill within $1 (size step or slippage too coarse); pass identity_rounding_tolerance_base_units or choose another market.`);
1682
+ }
1683
+ // The receipt identity must not move with the price, so a resume finds it:
1684
+ // use the fixed $1 cap the estimate fits in.
1685
+ roundingToleranceBaseUnits = '1000000';
1686
+ }
1687
+ }
1585
1688
  const identity = trustedExternalMutationIdentity(input, page, {
1586
1689
  operation: orderType === 'limit' ? 'perps-limit-order' : 'perps-market-order',
1587
1690
  provider: input.node?.provider,
@@ -1621,6 +1724,18 @@ export async function placeOrder(input) {
1621
1724
  const hasBackgroundRequest = await page.evaluate(
1622
1725
  `typeof globalThis.stateHooks?.submitRequestToBackground === 'function'`,
1623
1726
  );
1727
+ if (closeImmediately) {
1728
+ // The close goes through the background request, so the order must too.
1729
+ if (!hasBackgroundRequest) throw new Error('close_immediately needs the background Perps request; it is unavailable.');
1730
+ const receipt = await readExternalMutationReceipt(input, identity);
1731
+ // Only a receipt that may have dispatched owns what is open in the market now.
1732
+ const owned = ['succeeded', 'outcome-unknown'].includes(receipt?.record?.state);
1733
+ if (owned) dispatchMayHaveHappened = true;
1734
+ if (!owned) {
1735
+ const book = await readBoundBook(page, bound);
1736
+ assertFlatBeforeRoundTrip(book.positions, symbol, symbolForItem, book.orders);
1737
+ }
1738
+ }
1624
1739
  if (hasBackgroundRequest && !fromCurrentForm) {
1625
1740
  await requirePerpsTradingContext(input, page);
1626
1741
  const resolvedPrice = await readCurrentMarketPrice(
@@ -1628,11 +1743,24 @@ export async function placeOrder(input) {
1628
1743
  symbol,
1629
1744
  input.node?.current_price ?? input.node?.currentPrice,
1630
1745
  );
1746
+ houseNotional = requireHouseNotional({
1747
+ network: lane.network,
1748
+ symbol,
1749
+ amount,
1750
+ leverage,
1751
+ price: resolvedPrice,
1752
+ szDecimals: lane.network === 'mainnet' ? await readSizeDecimals(page, symbol) : null,
1753
+ maxSlippageBps: Number(input.node?.max_slippage_bps ?? input.node?.maxSlippageBps ?? 300),
1754
+ });
1631
1755
  const backgroundLimitPrice = orderType === 'limit'
1632
1756
  ? resolveLimitPrice(input, isBuy, resolvedPrice)
1633
1757
  : null;
1634
1758
  return placeOrderThroughBackground(input, page, {
1635
1759
  amount,
1760
+ bound,
1761
+ onDispatch: () => {
1762
+ dispatchMayHaveHappened = true;
1763
+ },
1636
1764
  expected: {
1637
1765
  ...expectedVisibleIdentity,
1638
1766
  limitPriceBaseUnits: backgroundLimitPrice == null ? null : parseUsdBaseUnits(backgroundLimitPrice),
@@ -1952,6 +2080,195 @@ export async function placeOrder(input) {
1952
2080
  }
1953
2081
  throw new Error('Visible Perps placement did not return a terminal result.');
1954
2082
  });
2083
+ if (!closeImmediately) return place();
2084
+ // A resumed order checks its close record before anything else: it never
2085
+ // re-observes, cleans up or closes after its round trip has run.
2086
+ const marker = closeMarkerPath(input);
2087
+ const recorded = await readCloseMarker(marker);
2088
+ if (recorded?.state === 'closed') {
2089
+ if (recorded.error) throw new Error(recorded.error);
2090
+ return { ...recorded.placed, roundTrip: { ...recorded.roundTrip, resumed: true } };
2091
+ }
2092
+ if (recorded) {
2093
+ throw new Error(`close_immediately: an earlier close of ${symbol} may have run (record ${recorded.state}); check ${symbol} and rerun with a fresh artifacts dir.`);
2094
+ }
2095
+ let placed;
2096
+ try {
2097
+ placed = await place();
2098
+ } catch (error) {
2099
+ if (!dispatchMayHaveHappened || error?.knownNotDispatched) throw error;
2100
+ throw await closeAfterFailedPlacement(input, symbol, error, bound, marker);
2101
+ }
2102
+ placed = { ...placed, ...(houseNotional ? { houseNotional } : {}) };
2103
+ return { ...placed, roundTrip: await closeOpenedPosition(input, { symbol, bound, marker, placed }) };
2104
+ }
2105
+
2106
+ // The flat-market baseline: the bound account's positions and resting orders,
2107
+ // read in one evaluation with the context guard before, between and after the
2108
+ // reads, so a switch to another account's book and back can't pass as flat.
2109
+ async function readBoundBook(page, bound) {
2110
+ const book = await page.evaluate(`(async () => {
2111
+ const request = globalThis.stateHooks?.submitRequestToBackground;
2112
+ if (typeof request !== 'function') return { unavailable: true };
2113
+ const guard = async () => { ${boundContextGuard(bound)} return null; };
2114
+ if (await guard()) return { changed: true };
2115
+ const positions = await request('perpsGetPositions', []);
2116
+ if (await guard()) return { changed: true };
2117
+ const orders = await request('perpsGetOpenOrders', []);
2118
+ if (await guard()) return { changed: true };
2119
+ return { positions, orders };
2120
+ })()`);
2121
+ if (book?.changed) {
2122
+ throw Object.assign(
2123
+ new Error('Perps order not dispatched: the selected account or network changed while the market was checked flat.'),
2124
+ { knownNotDispatched: true },
2125
+ );
2126
+ }
2127
+ if (!Array.isArray(book?.positions) || !Array.isArray(book?.orders)) {
2128
+ throw new Error('Background Perps API unavailable; cannot check the market is flat.');
2129
+ }
2130
+ return book;
2131
+ }
2132
+
2133
+ async function readSizeDecimals(page, symbol) {
2134
+ const markets = await page.evaluate(
2135
+ `globalThis.stateHooks.submitRequestToBackground('perpsGetMarkets', [{ symbols: [${JSON.stringify(symbol)}] }])`,
2136
+ );
2137
+ const market = (Array.isArray(markets) ? markets : []).find((item) => normalizeMarketSymbol(item?.name ?? '') === symbol);
2138
+ return market?.szDecimals ?? null;
2139
+ }
2140
+
2141
+ // close_immediately started from a flat market (proven in the same action), so a
2142
+ // position in that market afterwards is this order's own. It is closed with the
2143
+ // product's own background close request and the market is proven flat again.
2144
+ // Every read and the close itself run guarded: if the selected account or the
2145
+ // Perps network is no longer the bound one, nothing is read as ours or closed.
2146
+ export async function closeOwnPosition(page, symbol, { houseLimits = false, bound = null } = {}) {
2147
+ const read = () => page.evaluate(`(async () => {
2148
+ ${boundContextGuard(bound)}
2149
+ const positions = await globalThis.stateHooks.submitRequestToBackground('perpsGetPositions', []);
2150
+ return { positions: Array.isArray(positions) ? positions : null };
2151
+ })()`);
2152
+ const own = async () => {
2153
+ const state = await read();
2154
+ if (state?.changed) return { changed: true };
2155
+ if (!state?.positions) throw new Error('Background Perps API unavailable.');
2156
+ return { position: state.positions.find((position) => symbolForItem(position) === symbol) ?? null };
2157
+ };
2158
+ const before = await own();
2159
+ if (before.changed) return { changed: true, closed: false };
2160
+ if (!before.position) return { wasOpen: false, closed: true };
2161
+ let limitError = null;
2162
+ if (houseLimits) {
2163
+ try {
2164
+ assertHouseLimitPosition(before.position, symbol);
2165
+ } catch (error) {
2166
+ limitError = error;
2167
+ }
2168
+ }
2169
+ const result = await page.evaluate(`(async () => {
2170
+ ${boundContextGuard(bound)}
2171
+ return globalThis.stateHooks.submitRequestToBackground('perpsClosePosition', [{ symbol: ${JSON.stringify(symbol)}, orderType: 'market' }]);
2172
+ })()`);
2173
+ if (result?.changed) return { changed: true, closed: false };
2174
+ const deadline = Date.now() + 30_000;
2175
+ let closed = false;
2176
+ while (Date.now() < deadline) {
2177
+ const after = await own();
2178
+ if (after.changed) return { changed: true, closed: false, closeSent: true };
2179
+ if (!after.position) {
2180
+ closed = true;
2181
+ break;
2182
+ }
2183
+ await sleep(1_000);
2184
+ }
2185
+ return {
2186
+ wasOpen: true,
2187
+ closed,
2188
+ limitError,
2189
+ opened: { symbol, size: before.position.size ?? null, leverage: before.position.leverage ?? null },
2190
+ result,
2191
+ };
2192
+ }
2193
+
2194
+ // The run's close record, next to its execution ledger: `closing` before the
2195
+ // close is sent, `closed` after. A resume reads it first, so it never closes a
2196
+ // position opened in that market later, and a crash between the two leaves a
2197
+ // record that refuses to guess.
2198
+ function closeMarkerPath(input) {
2199
+ const trusted = input.context?.trustedMutation;
2200
+ const root = trusted?.executionLedgerRoot;
2201
+ const nodeId = String(input.context?.nodeId ?? '');
2202
+ if (typeof root !== 'string' || !path.isAbsolute(root) || !/^[a-f0-9]{64}$/u.test(String(trusted?.runId ?? '')) || !nodeId) {
2203
+ throw new Error('close_immediately needs the run-owned execution ledger.');
2204
+ }
2205
+ // Same id as the receipt identity's executionId.
2206
+ const executionId = createHash('sha256').update(`${trusted.runId}\0${nodeId}`).digest('hex');
2207
+ return path.join(root, `${executionId}.close.json`);
2208
+ }
2209
+
2210
+ async function readCloseMarker(marker) {
2211
+ return readFile(marker, 'utf8').then(JSON.parse, (error) => {
2212
+ if (error?.code === 'ENOENT') return null;
2213
+ throw error;
2214
+ });
2215
+ }
2216
+
2217
+ async function writeCloseMarker(marker, record, { exclusive = false } = {}) {
2218
+ await writeFile(marker, `${JSON.stringify({ schemaVersion: 1, kind: 'perps-immediate-close', ...record })}\n`, {
2219
+ mode: 0o600,
2220
+ flag: exclusive ? 'wx' : 'w',
2221
+ });
2222
+ }
2223
+
2224
+ async function closeAfterFailedPlacement(input, symbol, error, bound, marker) {
2225
+ const message = error instanceof Error ? error.message : String(error);
2226
+ let outcome;
2227
+ try {
2228
+ await writeCloseMarker(marker, { state: 'closing', market: symbol }, { exclusive: true });
2229
+ // withExtensionPage merges the callback's result into its runtime fields.
2230
+ const { wasOpen, closed, changed } = await withExtensionPage(input, (page) => closeOwnPosition(page, symbol, { bound }));
2231
+ outcome = changed
2232
+ ? `the selected account or network changed, so nothing was closed; check ${symbol} on the original account now`
2233
+ : !wasOpen
2234
+ ? `no ${symbol} position is open`
2235
+ : closed
2236
+ ? `the ${symbol} position it opened was closed`
2237
+ : `the ${symbol} position is STILL OPEN after a close request; close it now`;
2238
+ // Nothing open, or closed now: the run is settled and a resume rethrows the failure.
2239
+ if (!changed && (closed || !wasOpen)) {
2240
+ await writeCloseMarker(marker, { state: 'closed', market: symbol, error: `${message} close_immediately: ${outcome}.` });
2241
+ }
2242
+ } catch (closeError) {
2243
+ outcome = `the ${symbol} position could not be checked or closed (${closeError instanceof Error ? closeError.message : String(closeError)}); check it now`;
2244
+ }
2245
+ return new Error(`${message} close_immediately: ${outcome}.`);
2246
+ }
2247
+
2248
+ async function closeOpenedPosition(input, { symbol, bound, marker, placed }) {
2249
+ await writeCloseMarker(marker, { state: 'closing', market: symbol }, { exclusive: true });
2250
+ const mainnet = bound?.network === 'mainnet';
2251
+ const { wasOpen, closed, changed, limitError, opened, result } = await withExtensionPage(
2252
+ input,
2253
+ (page) => closeOwnPosition(page, symbol, { houseLimits: mainnet, bound }),
2254
+ );
2255
+ if (changed) {
2256
+ throw new Error(`close_immediately did not finish closing ${symbol}: the selected account or network changed after the order. Check ${symbol} on the original account now and close it if it is open.`);
2257
+ }
2258
+ if (!wasOpen) throw new Error(`close_immediately could not find the ${symbol} position the order opened; check ${symbol} now.`);
2259
+ if (!closed) {
2260
+ throw new Error(`close_immediately could not close the ${symbol} position this order opened; it is still open. Close it now.`);
2261
+ }
2262
+ const roundTrip = { opened, closed: true, result, proofPath: 'background-close-request' };
2263
+ await writeCloseMarker(marker, {
2264
+ state: 'closed',
2265
+ market: symbol,
2266
+ placed,
2267
+ roundTrip,
2268
+ ...(limitError ? { error: limitError.message } : {}),
2269
+ });
2270
+ if (limitError) throw limitError;
2271
+ return roundTrip;
1955
2272
  }
1956
2273
 
1957
2274
  export async function ensurePositions(input) {
@@ -1,11 +1,13 @@
1
1
  // Place a Perps order and return visible-control evidence or assert the resulting order or position state.
2
+ // With close_immediately the order's position is already closed again, so the market must be flat.
2
3
  import { runAdapter } from '../platform/cdp.mjs';
4
+ import { closeImmediatelyRequested } from '../../shared/perps/order-policy.mjs';
3
5
  import { assertOrders, assertPositions, placeOrder } from './perps.mjs';
4
6
  runAdapter(async (input) => {
5
7
  const order = await placeOrder(input);
6
8
  if (order.proofPath === 'visible-product-controls') return order;
7
9
  const assertion = order.orderType === 'limit'
8
10
  ? await assertOrders(input, true)
9
- : await assertPositions(input, true);
11
+ : await assertPositions(input, !closeImmediatelyRequested(input.node));
10
12
  return { ...assertion, submitted: order.submitted === true, order };
11
13
  });
@@ -7,6 +7,7 @@ import {
7
7
  readExternalMutationReceipt,
8
8
  runGuardedExternalMutation,
9
9
  trustedExternalMutationIdentity,
10
+ withFixturePolicyContext,
10
11
  withExternalMutationRecovery,
11
12
  } from './mutation-receipt.mjs';
12
13
  import { requireCurrentVisibleFundingBalance, requirePerpsTradingContext } from './perps.mjs';
@@ -165,6 +166,9 @@ export async function updatePositionTpsl(input) {
165
166
  if (input.node?.confirm_live !== true) {
166
167
  throw new Error('metamask.perps.update_position_tpsl requires confirm_live=true for live submission.');
167
168
  }
169
+ ({ input } = await withFixturePolicyContext(input, page, {
170
+ operations: [{ operation: 'perps-tpsl-update', amount: fundingAmount }],
171
+ }));
168
172
  const identity = trustedExternalMutationIdentity(input, page, {
169
173
  operation: 'perps-tpsl-update',
170
174
  provider: input.node?.provider,
@@ -20,14 +20,8 @@ import { spawn } from 'node:child_process';
20
20
  import { createRequire } from 'node:module';
21
21
  import path from 'node:path';
22
22
 
23
- import {
24
- importRecipeHarnessRuntimeBrowserExtension,
25
- importRecipeHarnessRuntimeCdp,
26
- extensionIdPath,
27
- recipeHarnessPath,
28
- walletFixturePath,
29
- captureActiveRecipeRecordingSnapshot,
30
- } from '../../harness-exports.mjs';
23
+ import { captureActiveRecipeRecordingSnapshot, recipeHarnessPath } from '@farmslot/recipe-cli/harness';
24
+ import { importRecipeHarnessRuntimeBrowserExtension, importRecipeHarnessRuntimeCdp, extensionIdPath, walletFixturePath } from '../../harness-exports.mjs';
31
25
 
32
26
  // Resolve the Farmslot harness through normal package dependencies by default.
33
27
  // Local Farmslot source is only a dev override handled by the harness paths module.
@@ -201,6 +195,55 @@ function captureHelperPath() {
201
195
  return process.env.CAPTURE_HELPER_PATH || 'capture-helper';
202
196
  }
203
197
 
198
+ // A snapshot helper that outlived its harness process (a crashed run, or a
199
+ // timeout from before timed-out helpers were killed) is reparented to launchd
200
+ // and makes later snapshots hang. Kill those, and what they started (the npm
201
+ // wrapper `node …/capture-helper snapshot` runs the native binary as its
202
+ // child), before starting a new one. Only this user's processes are touched.
203
+ async function reapOrphanedCaptureHelpers(context) {
204
+ const ps = await runProcess('ps', ['-axo', 'pid=,ppid=,uid=,command='], {
205
+ cwd: context.projectRoot,
206
+ env: process.env,
207
+ timeoutMs: 5000,
208
+ });
209
+ if (ps.exitCode !== 0) throw new Error(`ps failed while reaping capture-helper: ${ps.stderr}`);
210
+ const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
211
+ const configured = captureHelperPath();
212
+ // The executable (or the script node runs) and its subcommand, never an
213
+ // argument. The configured path may contain spaces; other install paths may not.
214
+ const dir = configured.includes('/') ? `${escape(path.dirname(configured))}/|` : '';
215
+ const snapshotHelper = new RegExp(
216
+ `^(?:(?:\\S*/)?node\\s+)?(?:${dir}\\S*/)?${escape(path.basename(configured))} snapshot(?:\\s|$)`,
217
+ 'u',
218
+ );
219
+ const processes = ps.stdout.split('\n').flatMap((line) => {
220
+ const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(.*)$/u.exec(line);
221
+ return match && Number(match[3]) === process.getuid()
222
+ ? [{ pid: Number(match[1]), ppid: Number(match[2]), command: match[4] }]
223
+ : [];
224
+ });
225
+ const doomed = new Set(
226
+ processes.filter((entry) => entry.ppid === 1 && snapshotHelper.test(entry.command)).map((entry) => entry.pid),
227
+ );
228
+ for (let grew = true; grew;) {
229
+ grew = false;
230
+ for (const entry of processes) {
231
+ if (doomed.has(entry.ppid) && !doomed.has(entry.pid)) {
232
+ doomed.add(entry.pid);
233
+ grew = true;
234
+ }
235
+ }
236
+ }
237
+ for (const pid of doomed) {
238
+ try {
239
+ process.kill(pid, 'SIGKILL');
240
+ } catch (error) {
241
+ // It exited between ps and kill: already reaped.
242
+ if (error?.code !== 'ESRCH') throw error;
243
+ }
244
+ }
245
+ }
246
+
204
247
  function parsePositivePid(value) {
205
248
  const pid = Number(String(value ?? '').trim());
206
249
  return Number.isInteger(pid) && pid > 0 ? pid : null;
@@ -286,7 +329,7 @@ async function captureCdpSnapshot(page, context, relPath, metadata, captureHelpe
286
329
  provider: 'cdp',
287
330
  mode: 'Page.captureScreenshot',
288
331
  fullPage: metadata?.fullPage === true,
289
- ...(captureHelperError ? { fallbackFrom: 'capture-helper', captureHelperError } : {}),
332
+ ...(captureHelperError ? { fallbackFrom: 'capture-helper', fallbackReason: captureHelperError, captureHelperError } : {}),
290
333
  },
291
334
  };
292
335
  }
@@ -324,6 +367,7 @@ export async function captureHelperSnapshot(page, context, relPath, metadata) {
324
367
  'native snapshot skipped while full-run recording owns the browser window',
325
368
  );
326
369
  } else {
370
+ await reapOrphanedCaptureHelpers(context);
327
371
  const result = await runProcess(
328
372
  captureHelperPath(),
329
373
  ['snapshot', '--pid', String(pid), '--output', destination.stagedPath],
@@ -419,17 +463,31 @@ async function execNodeScript(script, args, options) {
419
463
 
420
464
  function runProcess(command, args, options) {
421
465
  return new Promise((resolve, reject) => {
466
+ // A bounded command runs in its own process group so a timeout kills
467
+ // everything it started: the npm capture-helper wrapper runs the native
468
+ // binary as a child, which would otherwise keep our pipes open.
422
469
  const child = spawn(command, args, {
423
470
  cwd: options.cwd,
424
471
  env: options.env || process.env,
425
472
  stdio: ['ignore', 'pipe', 'pipe'],
473
+ detached: Boolean(options.timeoutMs),
426
474
  });
427
475
  let timer = null;
428
476
  let stdout = '';
429
477
  let stderr = '';
430
478
  if (options.timeoutMs) {
431
479
  timer = setTimeout(() => {
432
- child.kill('SIGTERM');
480
+ timer = null;
481
+ // SIGKILL: capture-helper can ignore SIGTERM while it holds a window capture.
482
+ try {
483
+ process.kill(-child.pid, 'SIGKILL');
484
+ } catch (error) {
485
+ // ESRCH: the whole group already exited; only the pipe close is pending.
486
+ if (error?.code !== 'ESRCH') {
487
+ reject(error);
488
+ return;
489
+ }
490
+ }
433
491
  reject(new Error(`Command timed out after ${options.timeoutMs}ms: ${command} ${args.join(' ')}`));
434
492
  }, options.timeoutMs);
435
493
  }
@@ -656,7 +714,11 @@ export async function openExtensionHomePage(port, extensionId) {
656
714
 
657
715
  async function ensureExtensionTarget(input, port) {
658
716
  let launch = null;
659
- const trustedTargetId = input.context?.trustedMutation?.cdpTargetId ?? null;
717
+ // A fixture-policy run (F21) has no target until its first mutation pins the
718
+ // live one; every later page in that action uses it.
719
+ const trusted = input.context?.trustedMutation;
720
+ const unpinnedPolicyLane = trusted?.proofLane === 'fixture-policy' && trusted.cdpTargetId === '';
721
+ const trustedTargetId = unpinnedPolicyLane ? null : trusted?.cdpTargetId ?? null;
660
722
  if (trustedTargetId !== null && !/^[A-Za-z0-9._:-]{1,128}$/u.test(String(trustedTargetId))) {
661
723
  throw new Error('Funded Extension action has no valid trusted CDP target id.');
662
724
  }
@@ -1,4 +1,4 @@
1
- // Bridge from library actions to the harness path/recording helpers. Published
1
+ // Bridge from library actions to the harness path helpers. Published
2
2
  // installs load the compiled dist (node cannot type-strip .ts under
3
3
  // node_modules); source checkouts without dist fall back to src. Deliberately
4
4
  // narrow — importing the package entry here would close an import cycle
@@ -14,20 +14,15 @@ async function load(distRelative, srcRelative) {
14
14
  }
15
15
 
16
16
  const paths = await load('dist/paths.js', 'src/paths.ts');
17
- const recording = await load('dist/run-recording.js', 'src/run-recording.ts');
18
17
  const featureFlags = await load('dist/feature-flags.js', 'src/feature-flags.ts');
19
18
 
20
19
  export const {
21
20
  importRecipeHarnessRuntimeBrowserExtension,
22
21
  importRecipeHarnessRuntimeCdp,
23
22
  extensionIdPath,
24
- recipeHarnessPath,
25
- recipeRuntimeDir,
26
23
  walletFixturePath,
27
24
  } = paths;
28
25
 
29
- export const { captureActiveRecipeRecordingSnapshot } = recording;
30
-
31
26
  // One normalisation for --remote-flag specs, fixture objects, and the
32
27
  // doctor/status summary line. See src/feature-flags.ts.
33
28
  export const {
@@ -3,7 +3,9 @@
3
3
  import { execFile } from 'node:child_process';
4
4
  import { promisify } from 'node:util';
5
5
 
6
- import { resolveMobileToolPath } from '../platform/tool-paths.mjs';
6
+ import { resolveMobileToolPath } from '@farmslot/adapter-rn';
7
+
8
+ import { nativeAppId } from '../platform/native-session-name.mjs';
7
9
 
8
10
  const execFileAsync = promisify(execFile);
9
11
 
@@ -31,13 +33,9 @@ function selectedAndroidSerial(input) {
31
33
  }
32
34
 
33
35
  function selectedAndroidPackage(input) {
34
- return String(
35
- input.node?.package_id ??
36
- input.node?.app_id ??
37
- input.context?.env?.ANDROID_PACKAGE_ID ??
38
- process.env.ANDROID_PACKAGE_ID ??
39
- 'io.metamask',
40
- ).trim();
36
+ const explicit = input.node?.package_id ?? input.node?.app_id;
37
+ if (explicit != null) return String(explicit).trim();
38
+ return nativeAppId({ ...process.env, ...(input.context?.env ?? {}) }, 'android');
41
39
  }
42
40
 
43
41
  async function defaultRun(file, args) {