@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,7 @@
1
+ import { runAdapter } from '../platform/bridge.mjs';
2
+ import { assertPositions, placeOrder } from './perps.mjs';
3
+ runAdapter(async (input) => {
4
+ const order = await placeOrder(input);
5
+ const assertion = await assertPositions(input, true);
6
+ return { ...assertion, submitted: order.submitted === true, order };
7
+ });
@@ -0,0 +1,4 @@
1
+ import { runAdapter } from '../platform/bridge.mjs';
2
+ import { readOrders } from './perps.mjs';
3
+
4
+ runAdapter(readOrders);
@@ -0,0 +1,3 @@
1
+ import { runAdapter } from '../platform/bridge.mjs';
2
+ import { readPerpsPositions } from './perps.mjs';
3
+ runAdapter(readPerpsPositions);
@@ -0,0 +1,283 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { spawn } from 'node:child_process';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ function sleep(ms) {
7
+ return new Promise((resolve) => setTimeout(resolve, ms));
8
+ }
9
+
10
+ export async function loadInput() {
11
+ const inputPath = process.argv[2] || process.env.METAMASK_RECIPE_ADAPTER_INPUT;
12
+ if (!inputPath) throw new Error('Missing live adapter input path.');
13
+ return JSON.parse(await readFile(inputPath, 'utf8'));
14
+ }
15
+
16
+ export async function writeOutput(input, output) {
17
+ await writeFile(input.outputPath, `${JSON.stringify(output, null, 2)}\n`);
18
+ }
19
+
20
+ export async function runAdapter(callback) {
21
+ const input = await loadInput();
22
+ const output = await callback(input);
23
+ await writeOutput(input, output);
24
+ }
25
+
26
+ function bridgeScript(input) {
27
+ if (process.env.METAMASK_RECIPE_MOBILE_BRIDGE_SCRIPT) {
28
+ return process.env.METAMASK_RECIPE_MOBILE_BRIDGE_SCRIPT;
29
+ }
30
+ return path.join(runtimeDir(), 'cdp-bridge.cjs');
31
+ }
32
+
33
+ function runtimeDir() {
34
+ return fileURLToPath(new URL('../bridge-runtime', import.meta.url));
35
+ }
36
+
37
+ export function bridgeEnv(input) {
38
+ /** @type {NodeJS.ProcessEnv} */
39
+ const env = {
40
+ ...process.env,
41
+ ...(input.context?.env || {}),
42
+ CDP_TIMEOUT: String(input.node?.cdp_timeout_ms ?? process.env.CDP_TIMEOUT ?? '10000'),
43
+ };
44
+ const target = resolveMobileTarget(input);
45
+ const watcherPort = target.watcherPort;
46
+ const simulator = target.iosSimulator;
47
+ const androidDevice = target.androidDevice;
48
+ const adbSerial = target.adbSerial;
49
+ if (watcherPort !== undefined && watcherPort !== null && String(watcherPort) !== '') env.WATCHER_PORT = String(watcherPort);
50
+ if (simulator !== undefined && simulator !== null && String(simulator) !== '') env.IOS_SIMULATOR = String(simulator);
51
+ if (androidDevice !== undefined && androidDevice !== null && String(androidDevice) !== '') env.ANDROID_DEVICE = String(androidDevice);
52
+ if (adbSerial !== undefined && adbSerial !== null && String(adbSerial) !== '') {
53
+ env.ADB_SERIAL = String(adbSerial);
54
+ env.ANDROID_SERIAL = String(adbSerial);
55
+ }
56
+ return env;
57
+ }
58
+
59
+ function resolveMobileTarget(input) {
60
+ const contextEnv = input.context?.env || {};
61
+ const watcherPort = input.node?.watcher_port ?? input.node?.metro_port ?? input.node?.cdp_port ?? contextEnv.WATCHER_PORT ?? contextEnv.CDP_PORT ?? contextEnv.RECIPE_CDP_PORT ?? process.env.WATCHER_PORT ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT;
62
+ const iosSimulator = input.node?.simulator ?? input.node?.ios_simulator ?? contextEnv.IOS_SIMULATOR ?? process.env.IOS_SIMULATOR;
63
+ const androidDevice = input.node?.android_device ?? contextEnv.ANDROID_DEVICE ?? process.env.ANDROID_DEVICE;
64
+ const adbSerial = input.node?.adb_serial ?? contextEnv.ADB_SERIAL ?? contextEnv.ANDROID_SERIAL ?? process.env.ADB_SERIAL ?? process.env.ANDROID_SERIAL ?? androidDevice;
65
+ return { watcherPort, iosSimulator, androidDevice, adbSerial };
66
+ }
67
+
68
+ export async function bridgeCommand(input, args) {
69
+ const script = bridgeScript(input);
70
+ const result = await new Promise((resolve, reject) => {
71
+ const timeoutMs = Number(input.node?.bridge_timeout_ms ?? input.node?.cdp_timeout_ms ?? process.env.CDP_TIMEOUT ?? 30000);
72
+ const child = spawn(process.execPath, [script, ...args], {
73
+ cwd: input.context.projectRoot,
74
+ env: { ...bridgeEnv(input), APP_ROOT: input.context.projectRoot },
75
+ stdio: ['ignore', 'pipe', 'pipe'],
76
+ });
77
+ let stdout = '';
78
+ let stderr = '';
79
+ let settled = false;
80
+ const timer = Number.isFinite(timeoutMs) && timeoutMs > 0
81
+ ? setTimeout(() => {
82
+ if (settled) return;
83
+ settled = true;
84
+ child.kill('SIGTERM');
85
+ setTimeout(() => {
86
+ if (!child.killed) child.kill('SIGKILL');
87
+ }, 1000);
88
+ resolve({ exitCode: null, stdout, stderr, timedOut: true, timeoutMs });
89
+ }, timeoutMs)
90
+ : null;
91
+ child.stdout.on('data', (chunk) => { stdout += chunk; });
92
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
93
+ child.on('error', (error) => {
94
+ if (settled) return;
95
+ settled = true;
96
+ if (timer) clearTimeout(timer);
97
+ reject(error);
98
+ });
99
+ child.on('close', (exitCode) => {
100
+ if (settled) return;
101
+ settled = true;
102
+ if (timer) clearTimeout(timer);
103
+ resolve({ exitCode, stdout, stderr, timedOut: false, timeoutMs });
104
+ });
105
+ });
106
+ if (result.timedOut) {
107
+ const command = ['node', path.relative(input.context.projectRoot, script), ...redactBridgeArgs(args)].join(' ');
108
+ throw new Error(`Mobile CDP bridge command timed out after ${result.timeoutMs}ms: ${command}`);
109
+ }
110
+ if (result.exitCode !== 0) {
111
+ const command = ['node', path.relative(input.context.projectRoot, script), ...redactBridgeArgs(args)].join(' ');
112
+ throw new Error(`Mobile CDP bridge command failed: ${command}\n${redactBridgeOutput(result.stderr || result.stdout, sensitiveBridgeArgs(args))}`);
113
+ }
114
+ try {
115
+ const parsed = JSON.parse(result.stdout);
116
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && parsed.ok === false) {
117
+ throw new Error(
118
+ `Mobile CDP bridge command reported failure for ${redactBridgeArgs(args).join(' ')}: ${redactBridgeOutput(JSON.stringify(parsed), sensitiveBridgeArgs(args))}`,
119
+ );
120
+ }
121
+ return parsed;
122
+ } catch (error) {
123
+ if (String(error?.message ?? '').startsWith('Mobile CDP bridge command reported failure')) {
124
+ throw error;
125
+ }
126
+ throw new Error(`Mobile CDP bridge returned non-JSON output for ${args[0]}: ${error.message}\n${result.stdout}`);
127
+ }
128
+ }
129
+
130
+ function redactBridgeArgs(args) {
131
+ const command = String(args[0] ?? '');
132
+ if (command === 'unlock' && args.length > 1) return [command, '<redacted-password>', ...args.slice(2)];
133
+ return args.map((arg) => String(arg));
134
+ }
135
+
136
+ function redactBridgeOutput(output, args) {
137
+ let redacted = String(output ?? '');
138
+ for (const arg of args) {
139
+ const value = String(arg ?? '');
140
+ if (value.length < 6) continue;
141
+ redacted = redacted.split(value).join('<redacted>');
142
+ }
143
+ return redacted;
144
+ }
145
+
146
+ function sensitiveBridgeArgs(args) {
147
+ const command = String(args[0] ?? '');
148
+ if (command === 'unlock' && args.length > 1) return [args[1]];
149
+ return [];
150
+ }
151
+
152
+ function parseMaybeJson(value) {
153
+ if (typeof value !== 'string') return value;
154
+ try {
155
+ return JSON.parse(value);
156
+ } catch {
157
+ // The bridge can legitimately return plain strings for eval results; keep
158
+ // the original value when it is not a JSON-encoded payload.
159
+ return value;
160
+ }
161
+ }
162
+
163
+ export async function evalAsync(input, expression) {
164
+ const deadline = Date.now() + Number(input.node?.controller_ready_timeout_ms ?? 30000);
165
+ let lastError = null;
166
+ while (Date.now() < deadline) {
167
+ try {
168
+ return parseMaybeJson(await bridgeCommand(input, ['eval-async', expression]));
169
+ } catch (error) {
170
+ lastError = error;
171
+ if (!String(error?.message ?? error).includes('CLIENT_NOT_INITIALIZED')) throw error;
172
+ await sleep(500);
173
+ }
174
+ }
175
+ throw lastError ?? new Error('Timed out waiting for Mobile async evaluation.');
176
+ }
177
+
178
+ export async function evalSync(input, expression) {
179
+ return parseMaybeJson(await bridgeCommand(input, ['eval', expression]));
180
+ }
181
+
182
+ export async function navigate(input, route, params = {}) {
183
+ const navigation = await bridgeCommand(input, ['navigate', route, JSON.stringify(params)]);
184
+ const verifiedRoute = navigation && typeof navigation === 'object' && navigation.navigated
185
+ ? String(navigation.navigated)
186
+ : String(route);
187
+ const currentRoute = await waitForRoute(input, verifiedRoute, Number(input.node?.navigation_timeout_ms ?? 15000));
188
+ return { ...navigation, currentRoute, verifiedRoute };
189
+ }
190
+
191
+ function routeName(route) {
192
+ return route && typeof route === 'object' ? String(route.name ?? '') : '';
193
+ }
194
+
195
+ export async function waitForRoute(input, expectedRoute, timeoutMs = 15000) {
196
+ const expected = String(expectedRoute);
197
+ const deadline = Date.now() + timeoutMs;
198
+ let lastRoute = null;
199
+ while (Date.now() < deadline) {
200
+ lastRoute = await bridgeCommand(input, ['get-route']);
201
+ if (routeName(lastRoute) === expected) return lastRoute;
202
+ await sleep(250);
203
+ }
204
+ throw new Error(`Timed out waiting for Mobile route ${expected}; last route was ${JSON.stringify(lastRoute)}`);
205
+ }
206
+
207
+ export async function simulatorScreenshot(input, relPath) {
208
+ const targetInfo = resolveMobileTarget(input);
209
+ const androidTarget = targetInfo.androidDevice;
210
+ const adbSerial = targetInfo.adbSerial;
211
+ const platform = String(input.node?.platform ?? process.env.PLATFORM ?? '').toLowerCase();
212
+ if (androidTarget || adbSerial || platform === 'android') {
213
+ return androidScreenshot(input, relPath, adbSerial);
214
+ }
215
+ const target = targetInfo.iosSimulator;
216
+ if (!target) {
217
+ throw new Error('iOS screenshot requires node.simulator, node.ios_simulator, or IOS_SIMULATOR so the proof is tied to the same device as the bridge commands.');
218
+ }
219
+ const { relative, absolute } = resolveArtifactPath(input.context.artifactsDir, relPath || `screenshots/${input.context.nodeId}.png`);
220
+ await mkdir(path.dirname(absolute), { recursive: true });
221
+ const result = await new Promise((resolve, reject) => {
222
+ const child = spawn('xcrun', ['simctl', 'io', String(target), 'screenshot', absolute], { stdio: ['ignore', 'pipe', 'pipe'] });
223
+ let stdout = '';
224
+ let stderr = '';
225
+ child.stdout.on('data', (chunk) => { stdout += chunk; });
226
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
227
+ child.on('error', reject);
228
+ child.on('close', (exitCode) => resolve({ exitCode, stdout, stderr }));
229
+ });
230
+ if (result.exitCode !== 0) {
231
+ throw new Error(`simctl screenshot failed for ${target}: ${result.stderr || result.stdout}`);
232
+ }
233
+ return {
234
+ path: relative,
235
+ type: 'screenshot',
236
+ nodeId: input.context.nodeId,
237
+ label: input.node?.description || `${input.action} screenshot`,
238
+ category: 'evidence',
239
+ };
240
+ }
241
+
242
+ async function androidScreenshot(input, relPath, androidDevice) {
243
+ const { relative, absolute } = resolveArtifactPath(input.context.artifactsDir, relPath || `screenshots/${input.context.nodeId}.png`);
244
+ await mkdir(path.dirname(absolute), { recursive: true });
245
+ const args = [];
246
+ if (androidDevice) args.push('-s', String(androidDevice));
247
+ args.push('exec-out', 'screencap', '-p');
248
+ const result = await new Promise((resolve, reject) => {
249
+ const child = spawn('adb', args, { stdio: ['ignore', 'pipe', 'pipe'] });
250
+ const stdout = [];
251
+ let stderr = '';
252
+ child.stdout.on('data', (chunk) => { stdout.push(Buffer.from(chunk)); });
253
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
254
+ child.on('error', reject);
255
+ child.on('close', (exitCode) => resolve({ exitCode, stdout: Buffer.concat(stdout), stderr }));
256
+ });
257
+ if (result.exitCode !== 0) {
258
+ throw new Error(`adb screenshot failed${androidDevice ? ` for ${androidDevice}` : ''}: ${result.stderr}`);
259
+ }
260
+ await writeFile(absolute, result.stdout);
261
+ return {
262
+ path: relative,
263
+ type: 'screenshot',
264
+ nodeId: input.context.nodeId,
265
+ label: input.node?.description || `${input.action} screenshot`,
266
+ category: 'evidence',
267
+ };
268
+ }
269
+
270
+ function resolveArtifactPath(artifactsDir, relativePath) {
271
+ const raw = String(relativePath);
272
+ if (path.isAbsolute(raw) || /^[A-Za-z]:/.test(raw)) {
273
+ throw new Error(`Artifact path must be relative: ${raw}`);
274
+ }
275
+ const normalized = path.normalize(raw);
276
+ if (normalized === '..' || normalized.startsWith(`..${path.sep}`)) {
277
+ throw new Error(`Artifact path must not escape artifacts directory: ${raw}`);
278
+ }
279
+ return {
280
+ relative: normalized.split(path.sep).join('/'),
281
+ absolute: path.join(artifactsDir, normalized),
282
+ };
283
+ }
@@ -0,0 +1,38 @@
1
+ import { navigate, runAdapter } from '../platform/bridge.mjs';
2
+
3
+ const PAGE_ROUTES = {
4
+ home: { route: 'WalletView', params: {} },
5
+ perps: { route: 'PerpsMarketListView', params: {} },
6
+ };
7
+
8
+ function text(value) {
9
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
10
+ }
11
+
12
+ function pageRoute(node) {
13
+ const page = text(node?.page);
14
+ if (!page) return undefined;
15
+ if (PAGE_ROUTES[page]) return { page, ...PAGE_ROUTES[page] };
16
+ if (page === 'perps-market') {
17
+ const market = text(node.market) ?? text(node.symbol) ?? text(node.params?.market?.symbol);
18
+ if (!market) throw new Error('mobile ui.navigate page=perps-market requires market or symbol.');
19
+ return { page, route: 'PerpsMarketDetails', params: { market: { symbol: market } } };
20
+ }
21
+ throw new Error('mobile ui.navigate supported page aliases: home, perps, perps-market.');
22
+ }
23
+
24
+ runAdapter(async (input) => {
25
+ const alias = pageRoute(input.node);
26
+ if (alias) {
27
+ const navigation = await navigate(input, alias.route, alias.params);
28
+ return { action: input.action, ...alias, navigation, proofPath: 'agentic-navigation' };
29
+ }
30
+
31
+ const route = input.node?.route ?? input.node?.screen;
32
+ if (typeof route !== 'string' || route.length === 0) {
33
+ throw new Error('mobile ui.navigate requires page or a raw React Navigation route in node.route or node.screen.');
34
+ }
35
+ const params = input.node?.params && typeof input.node.params === 'object' ? input.node.params : {};
36
+ const navigation = await navigate(input, route, params);
37
+ return { action: input.action, route, params, navigation, proofPath: 'agentic-navigation' };
38
+ });
@@ -0,0 +1,107 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { bridgeCommand, runAdapter } from '../platform/bridge.mjs';
3
+ import { walletFixturePath } from '../../../../runner/src/paths.ts';
4
+
5
+ async function fixturePassword(projectRoot) {
6
+ const candidates = [
7
+ walletFixturePath(projectRoot),
8
+ ];
9
+ for (const candidate of candidates) {
10
+ try {
11
+ const fixture = JSON.parse(await readFile(candidate, 'utf8'));
12
+ if (fixture.password) return fixture.password;
13
+ } catch (error) {
14
+ if (error?.code === 'ENOENT') continue;
15
+ throw error;
16
+ }
17
+ }
18
+ throw new Error('No wallet fixture password found for Mobile unlock.');
19
+ }
20
+
21
+ function selectedAccount(status, input) {
22
+ return selectedStatus(status, input)?.account ?? null;
23
+ }
24
+
25
+ function routeName(status, input) {
26
+ const route = selectedStatus(status, input)?.route ?? null;
27
+ return route && typeof route === 'object' ? String(route.name ?? '') : '';
28
+ }
29
+
30
+ function selectedStatus(status, input) {
31
+ if (!Array.isArray(status)) {
32
+ return status && typeof status === 'object' ? status : null;
33
+ }
34
+ const preferredDevices = [
35
+ input.node?.ios_simulator,
36
+ input.node?.simulator,
37
+ input.node?.android_device,
38
+ input.node?.adb_serial,
39
+ process.env.IOS_SIMULATOR,
40
+ process.env.ANDROID_DEVICE,
41
+ process.env.ADB_SERIAL,
42
+ ].filter((value) => typeof value === 'string' && value.length > 0);
43
+ for (const preferredDevice of preferredDevices) {
44
+ const match = status.find((entry) => entry?.deviceName === preferredDevice);
45
+ if (match) return match;
46
+ }
47
+ return (
48
+ status.find((entry) => entry?.account) ??
49
+ status.find((entry) => entry && typeof entry === 'object') ??
50
+ null
51
+ );
52
+ }
53
+
54
+ async function status(input) {
55
+ return bridgeCommand(input, ['status']);
56
+ }
57
+
58
+ async function waitForUnlocked(input, timeoutMs = 15000) {
59
+ const deadline = Date.now() + timeoutMs;
60
+ let last = null;
61
+ while (Date.now() < deadline) {
62
+ last = await status(input);
63
+ if (selectedAccount(last, input) && routeName(last, input) !== 'Login') return last;
64
+ await new Promise((resolve) => setTimeout(resolve, 250));
65
+ }
66
+ throw new Error(`Timed out waiting for Mobile wallet unlock; last status was ${JSON.stringify(last)}`);
67
+ }
68
+
69
+ runAdapter(async (input) => {
70
+ const before = await status(input);
71
+ if (selectedAccount(before, input) && routeName(before, input) !== 'Login') {
72
+ return {
73
+ action: input.action,
74
+ unlocked: true,
75
+ alreadyUnlocked: true,
76
+ account: selectedAccount(before, input),
77
+ redacted: true,
78
+ proofPath: 'agentic-wallet-status',
79
+ };
80
+ }
81
+
82
+ const password = input.node?.password ?? await fixturePassword(input.context.projectRoot);
83
+ try {
84
+ const result = await bridgeCommand(input, ['unlock', String(password)]);
85
+ const after = await waitForUnlocked(input, Number(input.node?.unlock_timeout_ms ?? 15000));
86
+ return {
87
+ action: input.action,
88
+ unlocked: Boolean(result?.ok ?? result?.unlocked ?? true),
89
+ account: selectedAccount(after, input),
90
+ route: selectedStatus(after, input)?.route ?? null,
91
+ redacted: true,
92
+ proofPath: 'agentic-wallet-unlock',
93
+ };
94
+ } catch (error) {
95
+ if (!String(error?.message ?? error).includes('login-password-input not found')) throw error;
96
+ const after = await status(input);
97
+ if (!selectedAccount(after, input) || routeName(after, input) === 'Login') throw error;
98
+ return {
99
+ action: input.action,
100
+ unlocked: true,
101
+ alreadyUnlocked: true,
102
+ account: selectedAccount(after, input),
103
+ redacted: true,
104
+ proofPath: 'agentic-wallet-status-after-missing-login-input',
105
+ };
106
+ }
107
+ });
@@ -0,0 +1,35 @@
1
+ import { bridgeCommand, navigate, waitForRoute } from '../platform/bridge.mjs';
2
+
3
+ /**
4
+ * Navigate to the wallet home across MetaMask Mobile navigation generations.
5
+ *
6
+ * Current checkouts can navigate directly to `WalletView`. Some historical
7
+ * checkouts only register `HomeNav` at the navigator boundary and then resolve
8
+ * to `WalletView` after React Navigation processes the parent route. The
9
+ * historical path is still a real app navigation command; it is not state
10
+ * injection.
11
+ */
12
+ export async function navigateWalletHome(input) {
13
+ try {
14
+ const direct = await navigate(input, 'WalletView', {});
15
+ return { navigation: direct, route: 'WalletView', proofPath: 'agentic-navigation' };
16
+ } catch (directError) {
17
+ const parent = await bridgeCommand(input, ['navigate', 'HomeNav', '{}']);
18
+ const currentRoute = await waitForRoute(
19
+ input,
20
+ 'WalletView',
21
+ Number(input.node?.navigation_timeout_ms ?? 15000),
22
+ );
23
+ return {
24
+ navigation: {
25
+ ...parent,
26
+ currentRoute,
27
+ verifiedRoute: 'WalletView',
28
+ fallbackFrom: 'WalletView',
29
+ fallbackReason: directError instanceof Error ? directError.message : String(directError),
30
+ },
31
+ route: 'HomeNav',
32
+ proofPath: 'agentic-navigation-home-nav',
33
+ };
34
+ }
35
+ }
@@ -0,0 +1,40 @@
1
+ import { bridgeCommand, runAdapter } from '../platform/bridge.mjs';
2
+
3
+ function selectedStatus(status, input) {
4
+ if (!Array.isArray(status)) {
5
+ return status && typeof status === 'object' ? status : null;
6
+ }
7
+ const preferredDevices = [
8
+ input.node?.ios_simulator,
9
+ input.node?.simulator,
10
+ input.node?.android_device,
11
+ input.node?.adb_serial,
12
+ process.env.IOS_SIMULATOR,
13
+ process.env.ANDROID_DEVICE,
14
+ process.env.ADB_SERIAL,
15
+ ].filter((value) => typeof value === 'string' && value.length > 0);
16
+ for (const preferredDevice of preferredDevices) {
17
+ const match = status.find((entry) => entry?.deviceName === preferredDevice);
18
+ if (match) return match;
19
+ }
20
+ return (
21
+ status.find((entry) => entry?.account) ??
22
+ status.find((entry) => entry && typeof entry === 'object') ??
23
+ null
24
+ );
25
+ }
26
+
27
+ runAdapter(async (input) => {
28
+ const status = selectedStatus(await bridgeCommand(input, ['status']), input);
29
+ const output = {
30
+ action: input.action,
31
+ source: 'mobile-agentic-status',
32
+ account: status?.account ?? null,
33
+ route: status?.route ?? null,
34
+ deviceName: status?.deviceName ?? null,
35
+ platform: status?.platform ?? 'mobile',
36
+ redacted: true,
37
+ proofPath: 'agentic-wallet-status',
38
+ };
39
+ return output;
40
+ });
@@ -0,0 +1,48 @@
1
+ import { bridgeCommand, evalAsync, runAdapter } from '../platform/bridge.mjs';
2
+
3
+ runAdapter(async (input) => {
4
+ const requestedAddress = input.node?.address ? String(input.node.address).toLowerCase() : null;
5
+ const requestedId = input.node?.id ? String(input.node.id) : null;
6
+ const requestedName = input.node?.name ? String(input.node.name) : null;
7
+ if (!requestedAddress && !requestedId && !requestedName) {
8
+ throw new Error('metamask.wallet.select_account mobile live adapter requires node.address, node.id, or node.name.');
9
+ }
10
+ const accounts = await bridgeCommand(input, ['list-accounts']);
11
+ if (!Array.isArray(accounts)) {
12
+ throw new Error(`metamask.wallet.select_account expected list-accounts to return an array, got ${typeof accounts}.`);
13
+ }
14
+ const match = accounts.find((account) => {
15
+ const address = String(account?.address ?? '').toLowerCase();
16
+ const id = String(account?.id ?? '');
17
+ const name = String(account?.name ?? '');
18
+ return (requestedAddress && address === requestedAddress) ||
19
+ (requestedId && id === requestedId) ||
20
+ (requestedName && name === requestedName);
21
+ });
22
+ if (!match?.address) {
23
+ throw new Error(`Requested mobile account was not found in list-accounts: ${JSON.stringify({ address: requestedAddress, id: requestedId, name: requestedName })}`);
24
+ }
25
+ const result = await bridgeCommand(input, ['switch-account', String(match.address)]);
26
+ const perps = await evalAsync(
27
+ input,
28
+ `(async function(){
29
+ var controller = Engine.context.PerpsController;
30
+ if (!controller) return JSON.stringify({ skipped: true, reason: 'PerpsController unavailable' });
31
+ if (typeof controller.disconnect === 'function') await controller.disconnect();
32
+ if (typeof controller.init === 'function') await controller.init();
33
+ var positions = typeof controller.getPositions === 'function' ? await controller.getPositions() : [];
34
+ var account = typeof controller.getAccountState === 'function' ? await controller.getAccountState() : null;
35
+ return JSON.stringify({ reinitialized: true, positions: positions.length, withdrawableBalance: account && account.withdrawableBalance });
36
+ })()`,
37
+ );
38
+ return {
39
+ action: input.action,
40
+ selected: {
41
+ address: result.address,
42
+ id: result.id,
43
+ name: result.name,
44
+ },
45
+ perps,
46
+ proofPath: 'agentic-account-selection',
47
+ };
48
+ });