@deeeed/metamask-harness 0.2.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 (158) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/README.md +140 -0
  3. package/bin/mm-harness +99 -0
  4. package/docs/CHEATSHEET.md +61 -0
  5. package/docs/CLI-SPEC.md +915 -0
  6. package/docs/MENTAL-MODEL.md +295 -0
  7. package/docs/architecture.md +367 -0
  8. package/docs/extension-runtime-commands.md +60 -0
  9. package/docs/harness-cli.md +43 -0
  10. package/docs/live-adapter-contract.md +188 -0
  11. package/docs/package-boundaries.md +47 -0
  12. package/docs/perps-flow-catalog.md +235 -0
  13. package/docs/recipe-libraries.md +95 -0
  14. package/docs/runtime-file-conventions.md +36 -0
  15. package/library/actions/core/perps/_controller.mjs +727 -0
  16. package/library/actions/core/perps/assert_orders.mjs +53 -0
  17. package/library/actions/core/perps/assert_positions.mjs +52 -0
  18. package/library/actions/core/perps/close_orders.mjs +97 -0
  19. package/library/actions/core/perps/close_positions.mjs +118 -0
  20. package/library/actions/core/perps/ensure_orders.mjs +40 -0
  21. package/library/actions/core/perps/ensure_positions.mjs +37 -0
  22. package/library/actions/core/perps/place_order.mjs +201 -0
  23. package/library/actions/core/perps/read_account.mjs +30 -0
  24. package/library/actions/core/perps/read_orders.mjs +27 -0
  25. package/library/actions/core/perps/read_positions.mjs +27 -0
  26. package/library/actions/core/perps/start_state.mjs +92 -0
  27. package/library/actions/core/perps/teardown_state.mjs +86 -0
  28. package/library/actions/extension/perps/assert_orders.mjs +11 -0
  29. package/library/actions/extension/perps/assert_positions.mjs +11 -0
  30. package/library/actions/extension/perps/close_orders.mjs +8 -0
  31. package/library/actions/extension/perps/close_positions.mjs +8 -0
  32. package/library/actions/extension/perps/ensure_orders.mjs +4 -0
  33. package/library/actions/extension/perps/ensure_positions.mjs +4 -0
  34. package/library/actions/extension/perps/perps.mjs +730 -0
  35. package/library/actions/extension/perps/place_order.mjs +7 -0
  36. package/library/actions/extension/perps/read_orders.mjs +4 -0
  37. package/library/actions/extension/perps/read_positions.mjs +3 -0
  38. package/library/actions/extension/platform/cdp.mjs +541 -0
  39. package/library/actions/extension/ui/navigate.mjs +44 -0
  40. package/library/actions/extension/wallet/ensure_unlocked.mjs +36 -0
  41. package/library/actions/extension/wallet/read_state.mjs +27 -0
  42. package/library/actions/extension/wallet/select_account.mjs +48 -0
  43. package/library/actions/extension/wallet/setup.mjs +35 -0
  44. package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
  45. package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
  46. package/library/actions/mobile/bridge-runtime/cdp-bridge.cjs +686 -0
  47. package/library/actions/mobile/bridge-runtime/lib/cdp-eval.cjs +110 -0
  48. package/library/actions/mobile/bridge-runtime/lib/config.cjs +39 -0
  49. package/library/actions/mobile/bridge-runtime/lib/issue-capture.cjs +446 -0
  50. package/library/actions/mobile/bridge-runtime/lib/target-discovery.cjs +204 -0
  51. package/library/actions/mobile/bridge-runtime/lib/ws-client.cjs +108 -0
  52. package/library/actions/mobile/bridge-runtime/setup-wallet.sh +442 -0
  53. package/library/actions/mobile/perps/assert_orders.mjs +11 -0
  54. package/library/actions/mobile/perps/assert_positions.mjs +11 -0
  55. package/library/actions/mobile/perps/close_orders.mjs +8 -0
  56. package/library/actions/mobile/perps/close_positions.mjs +8 -0
  57. package/library/actions/mobile/perps/ensure_orders.mjs +4 -0
  58. package/library/actions/mobile/perps/ensure_positions.mjs +4 -0
  59. package/library/actions/mobile/perps/perps.mjs +709 -0
  60. package/library/actions/mobile/perps/place_order.mjs +7 -0
  61. package/library/actions/mobile/perps/read_orders.mjs +4 -0
  62. package/library/actions/mobile/perps/read_positions.mjs +3 -0
  63. package/library/actions/mobile/platform/bridge.mjs +283 -0
  64. package/library/actions/mobile/ui/navigate.mjs +38 -0
  65. package/library/actions/mobile/wallet/ensure_unlocked.mjs +107 -0
  66. package/library/actions/mobile/wallet/home.mjs +35 -0
  67. package/library/actions/mobile/wallet/read_state.mjs +40 -0
  68. package/library/actions/mobile/wallet/select_account.mjs +48 -0
  69. package/library/actions/mobile/wallet/setup.mjs +220 -0
  70. package/library/flows/perps.flows.json +64 -0
  71. package/library/library.json +7 -0
  72. package/library/manifests/core.action-manifest.json +1282 -0
  73. package/library/manifests/extension.action-manifest.json +1749 -0
  74. package/library/manifests/mobile.action-manifest.json +1753 -0
  75. package/library/recipes/action-validation.extension.recipe.json +417 -0
  76. package/library/recipes/action-validation.mobile.recipe.json +422 -0
  77. package/library/recipes/order-lifecycle.core.recipe.json +78 -0
  78. package/library/recipes/perps-lifecycle.recipe.json +194 -0
  79. package/library/recipes/read-markets.core.recipe.json +38 -0
  80. package/library/recipes/smoke.extension.recipe.json +31 -0
  81. package/library/recipes/smoke.mobile.recipe.json +31 -0
  82. package/library/recipes/trading-lifecycle.core.recipe.json +76 -0
  83. package/orchestration/compat-overlays/README.md +19 -0
  84. package/orchestration/compat-overlays/mobile/README.md +13 -0
  85. package/orchestration/compat-overlays/mobile/rn81-message-event-source.patch +42 -0
  86. package/orchestration/core/cleanup.sh +37 -0
  87. package/orchestration/core/inject.sh +154 -0
  88. package/orchestration/doctor.mjs +72 -0
  89. package/orchestration/extension/cleanup.mjs +60 -0
  90. package/orchestration/extension/console-tail.mjs +228 -0
  91. package/orchestration/extension/ensure-browser.sh +416 -0
  92. package/orchestration/extension/ensure-ready.ts +185 -0
  93. package/orchestration/extension/extension-id.ts +107 -0
  94. package/orchestration/extension/inject.mjs +266 -0
  95. package/orchestration/extension/launch-browser.cjs +216 -0
  96. package/orchestration/extension/launch.sh +175 -0
  97. package/orchestration/extension/live.sh +320 -0
  98. package/orchestration/extension/pin-remote-flags.cjs +45 -0
  99. package/orchestration/extension/readiness.mjs +414 -0
  100. package/orchestration/extension/refresh-build.sh +190 -0
  101. package/orchestration/extension/runtime-decision.ts +445 -0
  102. package/orchestration/extension/runtime.ts +407 -0
  103. package/orchestration/extension/seed-fixture.sh +177 -0
  104. package/orchestration/extension/sidepanel-toggle.sh +291 -0
  105. package/orchestration/extension/snapshot-dist.sh +84 -0
  106. package/orchestration/extension/start-watch.sh +339 -0
  107. package/orchestration/extension/wallet-fixture-state.cjs +1086 -0
  108. package/orchestration/lib/activate-repo-node.sh +144 -0
  109. package/orchestration/lib/cli-color.mjs +84 -0
  110. package/orchestration/lib/cli-commands.mjs +243 -0
  111. package/orchestration/lib/cli-home.mjs +354 -0
  112. package/orchestration/lib/cli-ux.sh +252 -0
  113. package/orchestration/lib/cli-version.mjs +123 -0
  114. package/orchestration/lib/ensure-runner-deps.sh +56 -0
  115. package/orchestration/lib/harness-path.sh +55 -0
  116. package/orchestration/lib/hash-helpers.sh +44 -0
  117. package/orchestration/lib/json-field.sh +23 -0
  118. package/orchestration/lib/log-tui.mjs +304 -0
  119. package/orchestration/lib/open-debug.mjs +317 -0
  120. package/orchestration/lib/path-defaults.json +4 -0
  121. package/orchestration/lib/progress.mjs +107 -0
  122. package/orchestration/lib/recipe-paths.mjs +26 -0
  123. package/orchestration/lib/resolve-farmslot-ports.sh +144 -0
  124. package/orchestration/manifest.json +358 -0
  125. package/orchestration/mobile/cleanup.sh +192 -0
  126. package/orchestration/mobile/deps-markers.ts +21 -0
  127. package/orchestration/mobile/inject.sh +681 -0
  128. package/orchestration/mobile/launch.sh +137 -0
  129. package/orchestration/mobile/live.sh +125 -0
  130. package/orchestration/mobile/runtime-decision.ts +292 -0
  131. package/orchestration/porcelain/metamask-recipe +99 -0
  132. package/orchestration/porcelain/mm-recipe +1591 -0
  133. package/orchestration/porcelain/mme-recipe +1181 -0
  134. package/package.json +59 -0
  135. package/runner/extension/verify.sh +511 -0
  136. package/runner/mobile/verify.sh +501 -0
  137. package/runner/src/adapters.ts +601 -0
  138. package/runner/src/cli.ts +1820 -0
  139. package/runner/src/commands/debug.ts +44 -0
  140. package/runner/src/commands/fixtures.ts +99 -0
  141. package/runner/src/commands/launch.ts +397 -0
  142. package/runner/src/commands/logs.ts +60 -0
  143. package/runner/src/commands/shared.ts +138 -0
  144. package/runner/src/completions-cache.ts +86 -0
  145. package/runner/src/doctor.ts +203 -0
  146. package/runner/src/harness.ts +516 -0
  147. package/runner/src/heal-bounds.ts +179 -0
  148. package/runner/src/index.ts +6 -0
  149. package/runner/src/live-adapter-contract.ts +274 -0
  150. package/runner/src/manifest.ts +47 -0
  151. package/runner/src/mm-harness-cli.ts +488 -0
  152. package/runner/src/paths.ts +198 -0
  153. package/runner/src/recording-target.ts +147 -0
  154. package/runner/src/run-recording.ts +329 -0
  155. package/runner/src/runner.ts +108 -0
  156. package/runner/src/types.ts +57 -0
  157. package/scripts/completions.sh +125 -0
  158. package/scripts/install-completions.sh +62 -0
@@ -0,0 +1,220 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { spawn } from 'node:child_process';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { bridgeCommand, bridgeEnv, runAdapter } from '../platform/bridge.mjs';
6
+ import { walletFixturePath } from '../../../../runner/src/paths.ts';
7
+
8
+ async function fixtureProfile(projectRoot) {
9
+ const candidates = [
10
+ walletFixturePath(projectRoot),
11
+ ];
12
+ for (const candidate of candidates) {
13
+ try {
14
+ const fixture = JSON.parse(await readFile(candidate, 'utf8'));
15
+ if (
16
+ typeof fixture.password !== 'string' ||
17
+ fixture.password.length === 0
18
+ ) {
19
+ throw new Error(
20
+ `Mobile wallet fixture ${path.relative(projectRoot, candidate)} is missing password.`,
21
+ );
22
+ }
23
+ if (!Array.isArray(fixture.accounts) || fixture.accounts.length === 0) {
24
+ throw new Error(
25
+ `Mobile wallet fixture ${path.relative(projectRoot, candidate)} must define at least one account.`,
26
+ );
27
+ }
28
+ const expectedEvmAccounts = validateFixtureAccounts(
29
+ fixture.accounts,
30
+ path.relative(projectRoot, candidate),
31
+ );
32
+ return {
33
+ absolutePath: candidate,
34
+ path: path.relative(projectRoot, candidate),
35
+ password: fixture.password,
36
+ accounts: fixture.accounts.length,
37
+ expectedEvmAccounts,
38
+ hasPassword: true,
39
+ };
40
+ } catch (error) {
41
+ if (error?.code === 'ENOENT') continue;
42
+ throw error;
43
+ }
44
+ }
45
+ throw new Error('No wallet fixture found for Mobile setup.');
46
+ }
47
+
48
+ function validateFixtureAccounts(accounts, fixturePath) {
49
+ return accounts.reduce((total, account, index) => {
50
+ if (!account || typeof account !== 'object') {
51
+ throw new Error(`${fixturePath} accounts[${index}] must be an object.`);
52
+ }
53
+ if (account.type !== 'mnemonic' && account.type !== 'privateKey') {
54
+ throw new Error(
55
+ `${fixturePath} accounts[${index}].type must be mnemonic or privateKey.`,
56
+ );
57
+ }
58
+ if (typeof account.value !== 'string' || account.value.length === 0) {
59
+ throw new Error(
60
+ `${fixturePath} accounts[${index}].value must be a non-empty string.`,
61
+ );
62
+ }
63
+ if (account.type === 'mnemonic')
64
+ return total + mnemonicAccountCount(account, fixturePath, index);
65
+ return total + 1;
66
+ }, 0);
67
+ }
68
+
69
+ function mnemonicAccountCount(account, fixturePath, index) {
70
+ const raw = account.count ?? account.numberOfAccounts ?? 1;
71
+ const count = Number(raw);
72
+ if (!Number.isInteger(count) || count < 1 || count > 100) {
73
+ throw new Error(
74
+ `${fixturePath} accounts[${index}] mnemonic count must be an integer from 1 through 100.`,
75
+ );
76
+ }
77
+ return count;
78
+ }
79
+
80
+ function selectedStatus(status, input) {
81
+ if (!Array.isArray(status)) {
82
+ return status && typeof status === 'object' ? status : null;
83
+ }
84
+ const preferredDevices = [
85
+ input.node?.ios_simulator,
86
+ input.node?.simulator,
87
+ input.node?.android_device,
88
+ input.node?.adb_serial,
89
+ process.env.IOS_SIMULATOR,
90
+ process.env.ANDROID_DEVICE,
91
+ process.env.ADB_SERIAL,
92
+ ].filter((value) => typeof value === 'string' && value.length > 0);
93
+ for (const preferredDevice of preferredDevices) {
94
+ const match = status.find((entry) => entry?.deviceName === preferredDevice);
95
+ if (match) return match;
96
+ }
97
+ return (
98
+ status.find((entry) => entry?.account) ??
99
+ status.find((entry) => entry && typeof entry === 'object') ??
100
+ null
101
+ );
102
+ }
103
+
104
+ function hasSelectedAccount(status, input) {
105
+ return Boolean(selectedStatus(status, input)?.account);
106
+ }
107
+
108
+ function setupWalletScript() {
109
+ if (process.env.METAMASK_RECIPE_MOBILE_SETUP_WALLET_SCRIPT) {
110
+ return process.env.METAMASK_RECIPE_MOBILE_SETUP_WALLET_SCRIPT;
111
+ }
112
+ return fileURLToPath(new URL('../bridge-runtime/setup-wallet.sh', import.meta.url));
113
+ }
114
+
115
+ function runSetupWallet(input, fixture) {
116
+ return new Promise((resolve, reject) => {
117
+ const timeoutMs = Number(
118
+ input.node?.setup_timeout_ms ?? input.node?.timeout_ms ?? 120000,
119
+ );
120
+ const child = spawn(
121
+ 'bash',
122
+ [setupWalletScript(), '--fixture', fixture.absolutePath],
123
+ {
124
+ cwd: input.context.projectRoot,
125
+ env: { ...bridgeEnv(input), CDP_TIMEOUT: String(timeoutMs), APP_ROOT: input.context.projectRoot },
126
+ stdio: ['ignore', 'pipe', 'pipe'],
127
+ },
128
+ );
129
+ let stdout = '';
130
+ let stderr = '';
131
+ let settled = false;
132
+ const timer =
133
+ Number.isFinite(timeoutMs) && timeoutMs > 0
134
+ ? setTimeout(() => {
135
+ if (settled) return;
136
+ settled = true;
137
+ child.kill('SIGTERM');
138
+ setTimeout(() => {
139
+ if (!child.killed) child.kill('SIGKILL');
140
+ }, 1000);
141
+ reject(
142
+ new Error(
143
+ `Mobile setup-wallet.sh timed out after ${timeoutMs}ms.`,
144
+ ),
145
+ );
146
+ }, timeoutMs)
147
+ : null;
148
+ child.stdout.on('data', (chunk) => {
149
+ stdout += chunk;
150
+ });
151
+ child.stderr.on('data', (chunk) => {
152
+ stderr += chunk;
153
+ });
154
+ child.on('error', (error) => {
155
+ if (settled) return;
156
+ settled = true;
157
+ if (timer) clearTimeout(timer);
158
+ reject(error);
159
+ });
160
+ child.on('close', (exitCode) => {
161
+ if (settled) return;
162
+ settled = true;
163
+ if (timer) clearTimeout(timer);
164
+ if (exitCode !== 0) {
165
+ reject(
166
+ new Error(
167
+ `Mobile setup-wallet.sh exited ${exitCode}: ${stderr || stdout}`,
168
+ ),
169
+ );
170
+ return;
171
+ }
172
+ resolve({ stdout, stderr });
173
+ });
174
+ });
175
+ }
176
+
177
+ async function verifiedStatus(input, password) {
178
+ const before = await bridgeCommand(input, ['status']);
179
+ if (hasSelectedAccount(before, input))
180
+ return { status: before, unlockedDuringSetup: false };
181
+
182
+ await bridgeCommand(input, ['unlock', String(password)]);
183
+ const after = await bridgeCommand(input, ['status']);
184
+ if (!hasSelectedAccount(after, input)) {
185
+ throw new Error(
186
+ `Mobile wallet setup did not expose a selected account after unlock; status=${JSON.stringify(after)}`,
187
+ );
188
+ }
189
+ return { status: after, unlockedDuringSetup: true };
190
+ }
191
+
192
+ runAdapter(async (input) => {
193
+ const fixture = await fixtureProfile(input.context.projectRoot);
194
+ const setupResult = await runSetupWallet(input, fixture);
195
+ const runtime = await verifiedStatus(input, fixture.password);
196
+ return {
197
+ action: input.action,
198
+ setup: 'preseeded-mobile-fixture-verified',
199
+ fixture: {
200
+ path: fixture.path,
201
+ accounts: fixture.accounts,
202
+ expectedEvmAccounts: fixture.expectedEvmAccounts,
203
+ hasPassword: fixture.hasPassword,
204
+ },
205
+ runtimeState: selectedStatus(runtime.status, input),
206
+ unlockedDuringSetup: runtime.unlockedDuringSetup,
207
+ setupWalletProof: summarizeSetupOutput(setupResult.stdout),
208
+ proofPath: 'mobile-fixture-profile',
209
+ };
210
+ });
211
+
212
+ function summarizeSetupOutput(stdout) {
213
+ return String(stdout ?? '')
214
+ .split('\n')
215
+ .filter((line) =>
216
+ /^(Fixture OK|=== Wallet Ready ===|Route:|Unlocked:|Accounts:|Selected:)/.test(
217
+ line,
218
+ ),
219
+ );
220
+ }
@@ -0,0 +1,64 @@
1
+ {
2
+ "schema_version": 1,
3
+ "kind": "recipe-flow-catalog",
4
+ "owner": "metamask.perps",
5
+ "flows": {
6
+ "perps.clean_market_testnet": {
7
+ "version": 1,
8
+ "description": "Converge Perps to a clean testnet baseline before a proof window: wallet unlocked, requested market selected, no open positions or orders for that market.",
9
+ "paramsSchema": {
10
+ "type": "object",
11
+ "required": ["market"],
12
+ "properties": {
13
+ "market": {
14
+ "type": "string",
15
+ "description": "Market symbol to converge, e.g. BTC."
16
+ },
17
+ "provider": {
18
+ "type": "string",
19
+ "description": "Optional Perps provider, e.g. hyperliquid; omit for the app default."
20
+ }
21
+ }
22
+ },
23
+ "requiresActions": ["metamask.wallet.ensure_unlocked", "metamask.perps.start_state"],
24
+ "defaultPhase": "start_state",
25
+ "defaultRecord": "trace_only",
26
+ "postcondition": { "path": "$.status", "operator": "eq", "value": "pass" },
27
+ "workflow": {
28
+ "entry": "ensure-wallet",
29
+ "nodes": {
30
+ "ensure-wallet": {
31
+ "action": "metamask.wallet.ensure_unlocked",
32
+ "intent": "Ensure the wallet is unlocked before converging the Perps baseline",
33
+ "next": "start-state"
34
+ },
35
+ "start-state": {
36
+ "action": "metamask.perps.start_state",
37
+ "intent": "Converge Perps to a clean {{params.market}} testnet baseline",
38
+ "profile": "clean_market_testnet",
39
+ "network": "testnet",
40
+ "provider": "{{params.provider}}",
41
+ "market": "{{params.market}}",
42
+ "positions": { "state": "none", "mode": "matching" },
43
+ "orders": { "state": "none", "mode": "matching" },
44
+ "next": "done"
45
+ },
46
+ "done": { "action": "end", "status": "pass" }
47
+ }
48
+ },
49
+ "examples": [
50
+ {
51
+ "description": "Clean BTC testnet baseline before a visual proof",
52
+ "node": {
53
+ "action": "call",
54
+ "ref": "perps.clean_market_testnet",
55
+ "params": { "market": "BTC" },
56
+ "phase": "start_state",
57
+ "record": "trace_only",
58
+ "intent": "Converge Perps to a clean BTC testnet baseline before proof"
59
+ }
60
+ }
61
+ ]
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "kind": "recipe-library",
3
+ "schema_version": 1,
4
+ "name": "metamask",
5
+ "owner": "metamask-recipe-runner",
6
+ "description": "Canonical MetaMask recipe library: minimal, stable flows every recipe can compose. Team and personal libraries layer on top and win resolution by default."
7
+ }