@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,177 @@
1
+ // The Perps order rule every adapter applies before dispatch (F21).
2
+ // Testnet orders need no funding proof. A mainnet order is allowed only from a
3
+ // synced fixture account within the house limits: 1x isolated, notional at
4
+ // most $15, closed again inside the same action (close_immediately: true).
5
+
6
+ import { isFixtureAccount } from '../wallet/fixture-accounts.mjs';
7
+
8
+ export const MAINNET_HOUSE_LIMITS = Object.freeze({
9
+ maxNotionalUsd: '15',
10
+ maxNotionalBaseUnits: 15_000_000n,
11
+ leverage: 1,
12
+ marginType: 'isolated',
13
+ });
14
+
15
+ const USDC_DECIMAL = /^(?:0|[1-9][0-9]{0,11})(?:\.([0-9]{1,6}))?$/u;
16
+
17
+ // USDC base units (6 decimals) of a non-negative decimal amount.
18
+ export function usdcBaseUnits(value) {
19
+ const text = String(value ?? '').trim();
20
+ const match = USDC_DECIMAL.exec(text);
21
+ if (!match) throw new Error(`Perps order amount must be a USDC decimal with at most 6 decimals, got ${JSON.stringify(text)}.`);
22
+ const [whole, fraction = ''] = text.split('.');
23
+ return BigInt(whole) * 1_000_000n + BigInt(fraction.padEnd(6, '0'));
24
+ }
25
+
26
+ // The order's network: the live app's, which must agree with the node's
27
+ // declared one. A mainnet order must declare network: mainnet, so a recipe
28
+ // written for testnet never trades real funds because the app moved.
29
+ export function orderNetwork({ declared, liveIsTestnet }) {
30
+ const live = liveIsTestnet === true ? 'testnet' : liveIsTestnet === false ? 'mainnet' : null;
31
+ if (!live) throw new Error('Perps order cannot verify whether the app is on testnet or mainnet.');
32
+ const wanted = declared == null || declared === '' ? null : String(declared).toLowerCase();
33
+ if (wanted && wanted !== 'testnet' && wanted !== 'mainnet') {
34
+ throw new Error(`Perps order network must be testnet or mainnet, got ${JSON.stringify(declared)}.`);
35
+ }
36
+ if (wanted && wanted !== live) throw new Error(`Perps order expected ${wanted}, but the app is on ${live}.`);
37
+ if (live === 'mainnet' && wanted !== 'mainnet') {
38
+ throw Object.assign(
39
+ new Error('PERPS_MAINNET_ORDER_REFUSED: Perps app is on mainnet but the order does not declare network: mainnet. Next: switch to testnet, or declare network: mainnet within the house limits.'),
40
+ { code: 'PERPS_MAINNET_ORDER_REFUSED' },
41
+ );
42
+ }
43
+ return live;
44
+ }
45
+
46
+ // Whether the node asks the action to close the position it opens.
47
+ export function closeImmediatelyRequested(node) {
48
+ return node?.close_immediately === true || node?.closeImmediately === true;
49
+ }
50
+
51
+ /**
52
+ * Decide whether a Perps order may be dispatched.
53
+ * @param {object} order
54
+ * @param {'testnet'|'mainnet'} order.network - The live app network, already cross-checked with the node.
55
+ * @param {string|number} order.amount - Margin in USDC.
56
+ * @param {number} order.leverage
57
+ * @param {'market'|'limit'} [order.orderType]
58
+ * @param {boolean} order.closeImmediately
59
+ * @param {boolean} order.fixtureAccount - The selected account is a synced fixture account.
60
+ * @returns {{ lane: 'testnet' } | { lane: 'mainnet-house-limits', notionalBaseUnits: string }}
61
+ */
62
+ export function decidePerpsOrder({ network, amount, leverage, orderType = 'market', closeImmediately, fixtureAccount }) {
63
+ if (closeImmediately && orderType !== 'market') {
64
+ throw new Error('close_immediately needs a market order: a resting limit order is not a position the action can close.');
65
+ }
66
+ // Testnet keeps whatever amount format the adapter accepts; only mainnet sizes are parsed strictly.
67
+ if (network === 'testnet') return { lane: 'testnet' };
68
+ if (network !== 'mainnet') throw new Error(`Perps order network must be testnet or mainnet, got ${JSON.stringify(network)}.`);
69
+ const leverageValue = Number(leverage);
70
+ const amountBaseUnits = usdcBaseUnits(amount);
71
+ const notionalBaseUnits = amountBaseUnits * BigInt(Number.isInteger(leverageValue) && leverageValue > 0 ? leverageValue : 1);
72
+ const broken = [];
73
+ if (amountBaseUnits <= 0n) broken.push('the amount is not positive');
74
+ if (!fixtureAccount) broken.push('the selected account is not one of the synced test accounts');
75
+ if (leverageValue !== MAINNET_HOUSE_LIMITS.leverage) broken.push(`leverage is ${leverage}, not 1`);
76
+ if (notionalBaseUnits > MAINNET_HOUSE_LIMITS.maxNotionalBaseUnits) {
77
+ broken.push(`notional ${formatUsd(notionalBaseUnits)} is above $${MAINNET_HOUSE_LIMITS.maxNotionalUsd}`);
78
+ }
79
+ if (orderType !== 'market') broken.push(`order type is ${orderType}, not market`);
80
+ if (!closeImmediately) broken.push('close_immediately is not true');
81
+ if (broken.length > 0) {
82
+ throw Object.assign(
83
+ new Error(
84
+ // The code leads the message: it is what survives a live adapter's process
85
+ // boundary, and the wording avoids the words bounded healing reads as wallet state.
86
+ `PERPS_MAINNET_ORDER_REFUSED: Mainnet Perps order refused before dispatch: ${broken.join('; ')}. ` +
87
+ `Mainnet orders need a synced test account, a 1x isolated market order, notional at most $${MAINNET_HOUSE_LIMITS.maxNotionalUsd} and close_immediately: true. ` +
88
+ 'Next: use testnet, or stay within those limits.',
89
+ ),
90
+ { code: 'PERPS_MAINNET_ORDER_REFUSED' },
91
+ );
92
+ }
93
+ return { lane: 'mainnet-house-limits', notionalBaseUnits: `${notionalBaseUnits}` };
94
+ }
95
+
96
+ // decidePerpsOrder with the fixture check resolved from the checkout's fixture.
97
+ export async function requirePerpsOrderAllowed({ projectRoot, selectedAddress, ...order }) {
98
+ const fixtureAccount = order.network === 'mainnet'
99
+ ? await isFixtureAccount(projectRoot, selectedAddress)
100
+ : false;
101
+ return decidePerpsOrder({ ...order, fixtureAccount });
102
+ }
103
+
104
+ // close_immediately closes the market's whole position (positions are netted
105
+ // per market), so it must start flat: no position and no resting order that
106
+ // could fill into the position it then closes.
107
+ export function assertFlatBeforeRoundTrip(positions, symbol, symbolOf, orders = []) {
108
+ if (positions.some((position) => symbolOf(position) === symbol)) {
109
+ throw new Error(`close_immediately needs no open ${symbol} position before the order; close it first.`);
110
+ }
111
+ if (orders.some((order) => symbolOf(order) === symbol)) {
112
+ throw new Error(`close_immediately needs no resting ${symbol} order before the order; cancel it first.`);
113
+ }
114
+ }
115
+
116
+ // The most a market order for `amount` USDC margin can fill at. The controller
117
+ // rounds the size to the market's szDecimals and adds one step when that falls
118
+ // short of the USD amount (perps-controller utils/orderCalculations.ts), and the
119
+ // fill may slip by maxSlippageBps: (amount × leverage + one step × price) × (1 + slippage).
120
+ export function worstCaseNotionalUsd({ amount, leverage, price, szDecimals, maxSlippageBps }) {
121
+ const usd = Number(amount) * Number(leverage);
122
+ const step = 10 ** -Number(szDecimals);
123
+ return (usd + step * Number(price)) * (1 + Number(maxSlippageBps) / 10_000);
124
+ }
125
+
126
+ // Mainnet only: refuse before dispatch when the size the controller would submit
127
+ // could fill above the house notional limit. Testnet needs no bound.
128
+ export function requireHouseNotional({ network, symbol, amount, leverage, price, szDecimals, maxSlippageBps }) {
129
+ if (network !== 'mainnet') return null;
130
+ // A missing value is unknown, never zero (Number(null) is 0).
131
+ const known = (value) => (value == null || value === '' ? Number.NaN : Number(value));
132
+ const inputs = { price: known(price), szDecimals: known(szDecimals), maxSlippageBps: known(maxSlippageBps) };
133
+ if (!(inputs.price > 0) || !Number.isInteger(inputs.szDecimals) || inputs.szDecimals < 0
134
+ || !Number.isFinite(inputs.maxSlippageBps) || inputs.maxSlippageBps < 0) {
135
+ throw Object.assign(
136
+ new Error(`PERPS_MAINNET_ORDER_REFUSED: Mainnet Perps order refused before dispatch: cannot bound the ${symbol} order size (price, size decimals or slippage unknown).`),
137
+ { code: 'PERPS_MAINNET_ORDER_REFUSED' },
138
+ );
139
+ }
140
+ const worst = worstCaseNotionalUsd({ amount, leverage, ...inputs });
141
+ if (worst > Number(MAINNET_HOUSE_LIMITS.maxNotionalUsd)) {
142
+ throw Object.assign(
143
+ new Error(
144
+ `PERPS_MAINNET_ORDER_REFUSED: Mainnet Perps order refused before dispatch: a ${symbol} order for $${Number(amount) * Number(leverage)} can fill at up to $${worst.toFixed(2)} ` +
145
+ `(size step ${10 ** -inputs.szDecimals} at $${inputs.price}, max slippage ${inputs.maxSlippageBps} bps), above $${MAINNET_HOUSE_LIMITS.maxNotionalUsd}. ` +
146
+ 'Next: order less, or lower max_slippage_bps.',
147
+ ),
148
+ { code: 'PERPS_MAINNET_ORDER_REFUSED' },
149
+ );
150
+ }
151
+ return { worstCaseNotionalUsd: Number(worst.toFixed(6)) };
152
+ }
153
+
154
+ // The opened position must be the isolated 1x one the policy allowed, worth at
155
+ // most the house limit, before the action closes it (mainnet), so a drift is
156
+ // reported, not hidden. The caller closes the position first either way.
157
+ export function assertHouseLimitPosition(position, symbol) {
158
+ const type = String(position?.leverage?.type ?? position?.marginType ?? '').toLowerCase();
159
+ const value = Number(position?.leverage?.value ?? position?.leverage);
160
+ if (type !== MAINNET_HOUSE_LIMITS.marginType || value !== MAINNET_HOUSE_LIMITS.leverage) {
161
+ throw new Error(
162
+ `Mainnet ${symbol} position opened as ${type || 'unknown'} ${Number.isFinite(value) ? `${value}x` : 'unknown leverage'}, not isolated 1x; it was closed.`,
163
+ );
164
+ }
165
+ const opened = Math.abs(Number(position?.size ?? position?.szi)) * Number(position?.entryPrice ?? position?.entryPx);
166
+ if (!Number.isFinite(opened) || opened > Number(MAINNET_HOUSE_LIMITS.maxNotionalUsd) + 1e-6) {
167
+ throw new Error(
168
+ `Mainnet ${symbol} position opened at ${Number.isFinite(opened) ? `$${opened.toFixed(2)}` : 'an unknown value'}, above $${MAINNET_HOUSE_LIMITS.maxNotionalUsd}; it was closed.`,
169
+ );
170
+ }
171
+ }
172
+
173
+ function formatUsd(baseUnits) {
174
+ const whole = baseUnits / 1_000_000n;
175
+ const fraction = `${baseUnits % 1_000_000n}`.padStart(6, '0').replace(/0+$/u, '');
176
+ return `$${whole}${fraction ? `.${fraction}` : ''}`;
177
+ }
@@ -0,0 +1,84 @@
1
+ // The checkout's synced wallet fixture (temp/recipe/runtime/wallet-fixture.json)
2
+ // expanded into accounts: a mnemonic entry with count N yields N accounts at
3
+ // address indexes 0..N-1, named dev1..devN from the entry's name. list_accounts
4
+ // shows them, Core Perps actions sign with them, and the Perps order rule
5
+ // (../perps/order-policy.mjs) checks a mainnet order's account against them.
6
+
7
+ import { readFile } from 'node:fs/promises';
8
+ import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts';
9
+
10
+ import { walletFixturePath } from '../../harness-exports.mjs';
11
+
12
+ function fixtureAccountName(base, index, count) {
13
+ if (count === 1) return base || 'account1';
14
+ const match = /^(.*?)(\d+)$/u.exec(base || 'account1');
15
+ return match
16
+ ? `${match[1]}${Number(match[2]) + index}`
17
+ : `${base || 'account'}${index + 1}`;
18
+ }
19
+
20
+ function mnemonicCount(entry, fixturePath, index) {
21
+ const count = Number(entry.count ?? entry.numberOfAccounts ?? 1);
22
+ if (!Number.isInteger(count) || count < 1 || count > 100) {
23
+ throw new Error(`${fixturePath} accounts[${index}] mnemonic count must be an integer from 1 through 100.`);
24
+ }
25
+ return count;
26
+ }
27
+
28
+ /**
29
+ * Expand fixture entries into `{ name, address, signer }`. `signer` is the viem
30
+ * account holding the key: use it to sign, never serialize it.
31
+ */
32
+ export function deriveFixtureAccounts(fixture, fixturePath) {
33
+ if (!Array.isArray(fixture.accounts) || fixture.accounts.length === 0) {
34
+ throw new Error(`wallet fixture at ${fixturePath} has no accounts array.`);
35
+ }
36
+ return fixture.accounts.flatMap((entry, fixtureIndex) => {
37
+ if (!entry || typeof entry !== 'object' || typeof entry.value !== 'string' || !entry.value.trim()) {
38
+ throw new Error(`${fixturePath} accounts[${fixtureIndex}] must contain a non-empty value.`);
39
+ }
40
+ if (entry.type === 'mnemonic') {
41
+ const count = mnemonicCount(entry, fixturePath, fixtureIndex);
42
+ return Array.from({ length: count }, (_, addressIndex) => {
43
+ const signer = mnemonicToAccount(entry.value.trim(), { addressIndex });
44
+ return { name: fixtureAccountName(entry.name, addressIndex, count), address: signer.address, signer };
45
+ });
46
+ }
47
+ if (entry.type === 'privateKey') {
48
+ const raw = entry.value.trim();
49
+ const normalized = raw.startsWith('0x') ? raw : `0x${raw}`;
50
+ if (!/^0x[0-9a-fA-F]{64}$/u.test(normalized)) {
51
+ throw new Error(`${fixturePath} accounts[${fixtureIndex}] privateKey is not a 32-byte hex key.`);
52
+ }
53
+ const signer = privateKeyToAccount(normalized);
54
+ return [{ name: entry.name || `account${fixtureIndex + 1}`, address: signer.address, signer }];
55
+ }
56
+ throw new Error(`${fixturePath} accounts[${fixtureIndex}].type must be mnemonic or privateKey.`);
57
+ });
58
+ }
59
+
60
+ /** The checkout's fixture accounts, or null when it has no wallet fixture. */
61
+ export async function readFixtureAccounts(projectRoot) {
62
+ const fixturePath = walletFixturePath(projectRoot);
63
+ let raw;
64
+ try {
65
+ raw = await readFile(fixturePath, 'utf8');
66
+ } catch (error) {
67
+ if (error?.code === 'ENOENT') return null;
68
+ throw error;
69
+ }
70
+ return { fixturePath, accounts: deriveFixtureAccounts(JSON.parse(raw), fixturePath) };
71
+ }
72
+
73
+ // Whether an address is one of the fixture's accounts. A missing or unreadable
74
+ // fixture is "not a fixture account": the caller refuses, it never assumes.
75
+ export async function isFixtureAccount(projectRoot, address) {
76
+ const wanted = String(address ?? '').toLowerCase();
77
+ if (!/^0x[0-9a-f]{40}$/u.test(wanted) || !projectRoot) return false;
78
+ try {
79
+ const loaded = await readFixtureAccounts(projectRoot);
80
+ return Boolean(loaded?.accounts.some((account) => account.address.toLowerCase() === wanted));
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { appPort, cdpPort, isDirectRun, resolveSigner, runAdapter } from '../platform/runtime.mjs';
7
7
  import { launchTerminalBrowser } from '../../../../adapters/terminal/launch.mjs';
8
+ import { policy } from '../../../../adapters/terminal/policy.mjs';
8
9
 
9
10
  export async function launch(input) {
10
11
  const node = input.node ?? {};
@@ -22,8 +23,8 @@ export async function launch(input) {
22
23
  'slow-mo': node.slow_mo_ms,
23
24
  network: node.network,
24
25
  'mainnet-confirmation': node.mainnet_confirmation,
25
- 'start-chain': node.start_chain ?? 42161,
26
- });
26
+ 'start-chain': node.start_chain ?? policy.startChain,
27
+ }, process.env, { policy });
27
28
  return {
28
29
  action: input.action,
29
30
  status: 'ready',
@@ -3,8 +3,9 @@
3
3
  // the same state after every navigation. The HUD is advisory: a missing
4
4
  // browser or a busy tab skips the update instead of failing the recipe.
5
5
 
6
+ import { isAppUrl } from '@farmslot/adapter-web/origin';
7
+
6
8
  import { clearHudState, hudRenderExpression, nextHudState, readHudState, writeHudState } from '../../../../adapters/terminal/lib/hud.mjs';
7
- import { isAppUrl } from '../../../../adapters/terminal/lib/origin.mjs';
8
9
  import { appOrigin, cdpPort, readBrowserState, terminalRuntimeDir } from './runtime.mjs';
9
10
  import { importRecipeHarnessRuntimeCdp } from '../../harness-exports.mjs';
10
11
 
@@ -6,9 +6,10 @@
6
6
  // app tab is the only tab of its window (the wallet host closes the others),
7
7
  // so it stays the window's active tab and keeps painting without it.
8
8
 
9
+ import { isAppUrl } from '@farmslot/adapter-web/origin';
10
+
9
11
  import { resolveSlowMo } from '../../../../adapters/terminal/lib/display.mjs';
10
12
  import { HUD_ELEMENT } from '../../../../adapters/terminal/lib/hud.mjs';
11
- import { isAppUrl } from '../../../../adapters/terminal/lib/origin.mjs';
12
13
  import { cdpListenerPids } from '../../../../adapters/terminal/lib/processes.mjs';
13
14
  import { appOrigin, cdpPort, readBrowserState } from './runtime.mjs';
14
15
  import { importRecipeHarnessRuntimeCdp } from '../../harness-exports.mjs';
@@ -9,7 +9,8 @@ import { pathToFileURL } from 'node:url';
9
9
 
10
10
  import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts';
11
11
 
12
- import { recipeRuntimeDir, walletFixturePath } from '../../harness-exports.mjs';
12
+ import { recipeRuntimeDir } from '@farmslot/recipe-cli/harness';
13
+ import { walletFixturePath } from '../../harness-exports.mjs';
13
14
 
14
15
  export const SIGNER_MODES = Object.freeze(['extension', 'injected']);
15
16
  export const DEFAULT_ACCOUNT = 'dev1';
@@ -1,177 +1,56 @@
1
- // Read, window and assert the terminal wallet request log the wallet host
2
- // writes: one JSON line per request the app sent to the main wallet (method,
3
- // EIP-712 primary type, domain/active chain, outcome) and, in extension mode,
4
- // one line per confirmation window MetaMask opened. Never holds params,
5
- // messages or signatures.
1
+ // The Terminal's wallet request log: where the wallet host writes it, and what
2
+ // the Terminal forbids in it. Reading, windowing and asserting the log are
3
+ // @farmslot/adapter-web/dapp's; the Hyperliquid rules are passed in as policy:
4
+ // - an L1 (agent-signed, Agent / chainId 1337) payload must never reach the
5
+ // main wallet (max_l1_requests, default 0);
6
+ // - a blocked mainnet venue request or a refused mainnet signature fails the
7
+ // assertion (testnet enforcement).
6
8
 
7
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
8
- import path from 'node:path';
9
+ import { shortPrimaryType } from '@farmslot/adapter-web/dapp';
9
10
 
10
11
  import { terminalRuntimePath } from '../platform/runtime.mjs';
11
12
 
12
13
  export const L1_PRIMARY_TYPE = 'Agent';
13
14
  export const L1_CHAIN_ID = 1337;
14
15
 
15
- export function signatureLogPath(projectRoot) {
16
- return terminalRuntimePath(projectRoot, 'wallet-requests.jsonl');
17
- }
18
-
19
- export function signatureCursorPath(projectRoot) {
20
- return terminalRuntimePath(projectRoot, 'wallet-requests.cursor.json');
21
- }
22
-
23
- export function parseLog(text) {
24
- return text
25
- .split('\n')
26
- .filter((line) => line.trim())
27
- .map((line, index) => {
28
- try {
29
- return { seq: index, ...JSON.parse(line) };
30
- } catch {
31
- return { seq: index, kind: 'unparseable' };
32
- }
33
- });
34
- }
35
-
36
- export async function readLog(projectRoot) {
37
- try {
38
- return parseLog(await readFile(signatureLogPath(projectRoot), 'utf8'));
39
- } catch (error) {
40
- if (error?.code === 'ENOENT') return [];
41
- throw error;
42
- }
43
- }
44
-
45
- export async function readCursor(projectRoot) {
46
- try {
47
- const cursor = JSON.parse(await readFile(signatureCursorPath(projectRoot), 'utf8'));
48
- return Number.isInteger(cursor.seq) ? cursor.seq : 0;
49
- } catch (error) {
50
- if (error?.code === 'ENOENT') return 0;
51
- throw error;
52
- }
53
- }
54
-
55
- export async function writeCursor(projectRoot, seq) {
56
- await writeFile(signatureCursorPath(projectRoot), `${JSON.stringify({ seq, at: new Date().toISOString() })}\n`, { mode: 0o600 });
57
- }
58
-
59
- // "HyperliquidTransaction:ApproveAgent" → "ApproveAgent".
60
- export function shortPrimaryType(primaryType) {
61
- if (primaryType == null) return null;
62
- const raw = String(primaryType);
63
- return raw.includes(':') ? raw.slice(raw.lastIndexOf(':') + 1) : raw;
64
- }
65
-
66
- export function isTypedDataRequest(entry) {
67
- return entry.kind === 'request' && /^eth_signTypedData/u.test(String(entry.method));
16
+ export function signatureLog(projectRoot) {
17
+ return {
18
+ logFile: terminalRuntimePath(projectRoot, 'wallet-requests.jsonl'),
19
+ cursorFile: terminalRuntimePath(projectRoot, 'wallet-requests.cursor.json'),
20
+ };
68
21
  }
69
22
 
70
23
  export function isL1Request(entry) {
71
- return isTypedDataRequest(entry) &&
24
+ return entry.kind === 'request' && /^eth_signTypedData/u.test(String(entry.method)) &&
72
25
  (shortPrimaryType(entry.primaryType) === L1_PRIMARY_TYPE || Number(entry.domainChainId) === L1_CHAIN_ID);
73
26
  }
74
27
 
75
- export function summarize(entries) {
76
- const requests = entries.filter((entry) => entry.kind === 'request');
77
- const typed = requests.filter(isTypedDataRequest);
78
- const byPrimaryType = {};
79
- for (const entry of typed) {
80
- const key = shortPrimaryType(entry.primaryType) ?? '<none>';
81
- byPrimaryType[key] ??= { requested: 0, signed: 0, rejected: 0, chainIds: [] };
82
- byPrimaryType[key].requested += 1;
83
- if (entry.outcome === 'signed') byPrimaryType[key].signed += 1;
84
- if (entry.outcome === 'rejected' || entry.outcome === 'error') byPrimaryType[key].rejected += 1;
85
- if (!byPrimaryType[key].chainIds.includes(entry.domainChainId)) byPrimaryType[key].chainIds.push(entry.domainChainId);
86
- }
87
- const byMethod = {};
88
- for (const entry of requests) byMethod[entry.method] = (byMethod[entry.method] ?? 0) + 1;
28
+ export const TERMINAL_SIGNATURE_POLICY = Object.freeze({
29
+ typedDataClasses: {
30
+ l1Requests: {
31
+ match: isL1Request,
32
+ param: 'max_l1_requests',
33
+ defaultMax: 0,
34
+ failure: (count, max) => `L1 requests reached the main wallet: ${count} > ${max}`,
35
+ },
36
+ },
37
+ forbiddenEntries: {
38
+ blockedMainnet: {
39
+ match: (entry) => entry.kind === 'blocked-mainnet' && !entry.probe,
40
+ failure: (count) => `the browser tried to reach a mainnet venue ${count} time(s) (blocked-mainnet)`,
41
+ },
42
+ refusedMainnet: {
43
+ match: (entry) => entry.kind === 'refused-mainnet',
44
+ failure: (count) => `the app asked the wallet to sign ${count} mainnet action(s) (refused-mainnet)`,
45
+ },
46
+ },
47
+ });
48
+
49
+ // Evidence goes to <artifacts>/terminal-wallet/<node>-signature-log.json.
50
+ export function logArtifactTarget(input) {
89
51
  return {
90
- requests: requests.length,
91
- typedDataRequests: typed.length,
92
- l1Requests: typed.filter(isL1Request).length,
93
- confirmationsShown: entries.filter((entry) => entry.kind === 'confirmation-shown').length,
94
- // Testnet enforcement and evidence gaps: each one fails the assertion.
95
- blockedMainnet: entries.filter((entry) => entry.kind === 'blocked-mainnet' && !entry.probe).length,
96
- refusedMainnet: entries.filter((entry) => entry.kind === 'refused-mainnet').length,
97
- unattributed: entries.filter((entry) => entry.kind === 'unattributed').length,
98
- // Calls from an iframe, a foreign page or a blank document: refused, kept as evidence.
99
- outsideAppFrame: entries.filter((entry) => entry.kind === 'outside-app-frame').length,
100
- byPrimaryType,
101
- byMethod,
52
+ artifactsDir: input.context?.artifactsDir,
53
+ nodeId: input.context?.nodeId ?? input.action,
54
+ folder: 'terminal-wallet',
102
55
  };
103
56
  }
104
-
105
- function exact(label, actual, expected, failures) {
106
- if (actual !== expected) failures.push(`${label}: expected ${expected}, got ${actual}`);
107
- }
108
-
109
- // Every expectation is optional; max_l1_requests defaults to 0 because an L1
110
- // (agent-signed) payload must never reach the main wallet.
111
- export function evaluateSignatureLog(entries, node = {}) {
112
- const summary = summarize(entries);
113
- const typed = entries.filter(isTypedDataRequest);
114
- const failures = [];
115
- if (summary.blockedMainnet > 0) failures.push(`the browser tried to reach a mainnet venue ${summary.blockedMainnet} time(s) (blocked-mainnet)`);
116
- if (summary.refusedMainnet > 0) failures.push(`the app asked the wallet to sign ${summary.refusedMainnet} mainnet action(s) (refused-mainnet)`);
117
- if (summary.unattributed > 0) failures.push(`${summary.unattributed} wallet log entr(y/ies) could not be attributed to the app's top frame (unattributed)`);
118
- const maxL1 = node.max_l1_requests == null ? 0 : Number(node.max_l1_requests);
119
- if (summary.l1Requests > maxL1) {
120
- failures.push(`L1 requests reached the main wallet: ${summary.l1Requests} > ${maxL1}`);
121
- }
122
- if (Array.isArray(node.allowed_primary_types)) {
123
- const allowed = new Set(node.allowed_primary_types.map(shortPrimaryType));
124
- for (const entry of typed) {
125
- const type = shortPrimaryType(entry.primaryType);
126
- if (!allowed.has(type)) failures.push(`primary type ${type} (seq ${entry.seq}) is not allowed`);
127
- }
128
- }
129
- if (node.counts && typeof node.counts === 'object') {
130
- for (const [type, expected] of Object.entries(node.counts)) {
131
- exact(`signed ${shortPrimaryType(type)}`, summary.byPrimaryType[shortPrimaryType(type)]?.signed ?? 0, Number(expected), failures);
132
- }
133
- }
134
- if (node.request_counts && typeof node.request_counts === 'object') {
135
- for (const [type, expected] of Object.entries(node.request_counts)) {
136
- exact(`requested ${shortPrimaryType(type)}`, summary.byPrimaryType[shortPrimaryType(type)]?.requested ?? 0, Number(expected), failures);
137
- }
138
- }
139
- if (node.methods && typeof node.methods === 'object') {
140
- for (const [method, expected] of Object.entries(node.methods)) {
141
- exact(`method ${method}`, summary.byMethod[method] ?? 0, Number(expected), failures);
142
- }
143
- }
144
- if (node.max_typed_data_requests != null && summary.typedDataRequests > Number(node.max_typed_data_requests)) {
145
- failures.push(`typed-data requests ${summary.typedDataRequests} > ${node.max_typed_data_requests}`);
146
- }
147
- if (node.expect_confirmations != null) {
148
- exact('confirmations shown', summary.confirmationsShown, Number(node.expect_confirmations), failures);
149
- }
150
- if (node.require_active_chain === true) {
151
- for (const entry of typed.filter((item) => item.outcome === 'signed')) {
152
- if (Number(entry.domainChainId) !== Number(entry.activeChainId)) {
153
- failures.push(`seq ${entry.seq} ${shortPrimaryType(entry.primaryType)} signed with domain chainId ${entry.domainChainId} while the wallet was on ${entry.activeChainId}`);
154
- }
155
- }
156
- }
157
- if (node.max_rejected != null) {
158
- const rejected = typed.filter((entry) => entry.outcome === 'rejected' || entry.outcome === 'error').length;
159
- if (rejected > Number(node.max_rejected)) failures.push(`rejected typed-data requests ${rejected} > ${node.max_rejected}`);
160
- }
161
- return { ok: failures.length === 0, failures, summary };
162
- }
163
-
164
- export async function windowSinceCursor(projectRoot) {
165
- const [entries, cursor] = await Promise.all([readLog(projectRoot), readCursor(projectRoot)]);
166
- return { cursor, entries: entries.filter((entry) => entry.seq >= cursor), total: entries.length };
167
- }
168
-
169
- export async function writeLogArtifact(input, payload) {
170
- const artifactsDir = input.context?.artifactsDir;
171
- if (!artifactsDir) return [];
172
- const nodeId = String(input.context?.nodeId ?? input.action).replaceAll('/', '__');
173
- const relative = `terminal-wallet/${nodeId}-signature-log.json`;
174
- await mkdir(path.join(artifactsDir, 'terminal-wallet'), { recursive: true });
175
- await writeFile(path.join(artifactsDir, relative), `${JSON.stringify(payload, null, 2)}\n`);
176
- return [{ path: relative, type: 'report', nodeId: String(input.context?.nodeId ?? input.action) }];
177
- }
@@ -3,22 +3,16 @@
3
3
  // payloads, active-chain matching and, in extension mode, confirmations shown.
4
4
  // The full window is retained as evidence whether or not the assertion holds.
5
5
 
6
+ import { awaitSignatureLog, writeLogArtifact } from '@farmslot/adapter-web/dapp';
7
+
6
8
  import { isDirectRun, runAdapter } from '../platform/runtime.mjs';
7
- import { evaluateSignatureLog, windowSinceCursor, writeLogArtifact } from './_signature-log.mjs';
9
+ import { TERMINAL_SIGNATURE_POLICY, logArtifactTarget, signatureLog } from './_signature-log.mjs';
8
10
 
9
11
  export async function assertSignatures(input) {
10
12
  const node = input.node ?? {};
11
- const projectRoot = input.context?.projectRoot;
12
- const deadline = Date.now() + Math.max(0, Number(node.timeout_ms ?? 0));
13
- let window = await windowSinceCursor(projectRoot);
14
- let result = evaluateSignatureLog(window.entries, node);
15
- // Entries arrive asynchronously from the wallet host; allow a short settle.
16
- while (!result.ok && Date.now() < deadline) {
17
- await new Promise((resolve) => setTimeout(resolve, 500));
18
- window = await windowSinceCursor(projectRoot);
19
- result = evaluateSignatureLog(window.entries, node);
20
- }
21
- const artifacts = await writeLogArtifact(input, {
13
+ // Entries arrive asynchronously from the wallet host; timeout_ms allows a short settle.
14
+ const { window, result } = await awaitSignatureLog(signatureLog(input.context?.projectRoot), node, TERMINAL_SIGNATURE_POLICY);
15
+ const artifacts = await writeLogArtifact(logArtifactTarget(input), {
22
16
  cursor: window.cursor,
23
17
  total: window.total,
24
18
  expectations: node,
@@ -2,26 +2,27 @@
2
2
  // evidence. reset=true moves the window start to the end of the log so the next
3
3
  // read covers only what follows (an explicit setup step, recorded in the trace).
4
4
 
5
+ import { resetWindow, summarize, windowSinceCursor, writeLogArtifact } from '@farmslot/adapter-web/dapp';
6
+
5
7
  import { isDirectRun, runAdapter } from '../platform/runtime.mjs';
6
- import { readLog, summarize, windowSinceCursor, writeCursor, writeLogArtifact } from './_signature-log.mjs';
8
+ import { TERMINAL_SIGNATURE_POLICY, logArtifactTarget, signatureLog } from './_signature-log.mjs';
7
9
 
8
10
  export async function readSignatures(input) {
9
- const projectRoot = input.context?.projectRoot;
11
+ const log = signatureLog(input.context?.projectRoot);
10
12
  if (input.node?.reset === true) {
11
- const entries = await readLog(projectRoot);
12
- await writeCursor(projectRoot, entries.length);
13
+ const { cursor, previousEntries } = await resetWindow(log);
13
14
  return {
14
15
  action: input.action,
15
16
  source: 'terminal-wallet-request-log',
16
17
  reset: true,
17
- cursor: entries.length,
18
- previousEntries: entries.length,
18
+ cursor,
19
+ previousEntries,
19
20
  proofPath: 'terminal-wallet-request-log',
20
21
  };
21
22
  }
22
- const { cursor, entries, total } = await windowSinceCursor(projectRoot);
23
- const summary = summarize(entries);
24
- const artifacts = await writeLogArtifact(input, { cursor, total, summary, entries });
23
+ const { cursor, entries, total } = await windowSinceCursor(log);
24
+ const summary = summarize(entries, TERMINAL_SIGNATURE_POLICY);
25
+ const artifacts = await writeLogArtifact(logArtifactTarget(input), { cursor, total, summary, entries });
25
26
  return {
26
27
  action: input.action,
27
28
  source: 'terminal-wallet-request-log',