@deeeed/metamask-harness 0.57.0 → 0.59.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.
@@ -0,0 +1,549 @@
1
+ import { execFileSync, spawnSync } from "node:child_process";
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { readRuntimeContextField } from "../../harness.js";
6
+ import { recipeHarnessPath, recipeHarnessRoot, recipeRuntimeDir, runnerDir } from "../../paths.js";
7
+ const USAGE = "Usage: mm-harness verify --adapter mobile -- [--target <metamask-mobile>] [--artifacts-dir <dir>] [--static-only] [--platform ios|android] [--preflight-mode fast|auto|default|rebuild-native|clean] [--no-auto-start]";
8
+ const ENV_FILES = [".js.env", ".env", ".env.local"];
9
+ const PREFLIGHT_MODES = ["fast", "auto", "default", "rebuild-native", "clean"];
10
+ const AUTO_START_TRUE = ["1", "true", "TRUE", "True", "yes", "YES", "Yes", "on", "ON", "On"];
11
+ const AUTO_START_FALSE = ["0", "false", "FALSE", "False", "no", "NO", "No", "off", "OFF", "Off", ""];
12
+ const AUTO_START_REFUSAL = "Mobile auto-start is not allowed from product-local scripts. Start or prepare the app through the runner/slot runtime, then rerun verify with --no-auto-start.";
13
+ const EVM_READINESS_EXPRESSION = "(async function(){try{var engine=globalThis.Engine;var controller=engine&&engine.context&&engine.context.NetworkController;if(!controller||typeof controller.getSelectedNetworkClient!=='function')throw new Error('NetworkController selected client is unavailable');var client=controller.getSelectedNetworkClient();if(!client||!client.provider||typeof client.provider.request!=='function')throw new Error('selected EVM provider is unavailable');var result=await client.provider.request({method:'eth_blockNumber'});if(typeof result!=='string'||!/^0x[0-9a-f]+$/i.test(result))throw new Error('eth_blockNumber returned an invalid result');return {ok:true,result:result};}catch(error){return {ok:false,error:error&&(error.message||String(error))};}})()";
14
+ const STATUS_SMOKE_RECIPE = `{
15
+ "$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
16
+ "title": "Mobile runner status smoke",
17
+ "description": "Checks that the installed runner can read Mobile app status before live verification.",
18
+ "workflow": {
19
+ "entry": "status",
20
+ "nodes": {
21
+ "status": { "action": "app.status", "intent": "Read Mobile app status through the runner-owned bridge", "next": "done" },
22
+ "done": { "action": "end", "status": "pass" }
23
+ }
24
+ }
25
+ }
26
+ `;
27
+ const LIVE_SMOKE_RECIPE = `{
28
+ "$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
29
+ "title": "Mobile v1 runner live bridge smoke",
30
+ "description": "Verifies the installed MetaMask runner can read the React Native debug bridge without mutating wallet state.",
31
+ "workflow": {
32
+ "entry": "status",
33
+ "nodes": {
34
+ "status": { "action": "app.status", "intent": "Read Mobile app status through the v1 runner", "next": "cdp-probe" },
35
+ "cdp-probe": { "action": "cdp.target", "intent": "Verify the React Native debug bridge target is reachable", "require_reachable": true, "timeout_ms": 60000, "cdp_timeout_ms": 60000, "next": "done" },
36
+ "done": { "action": "end", "status": "pass" }
37
+ }
38
+ }
39
+ }
40
+ `;
41
+ function parseMobileVerifyArgs(argv, env = process.env) {
42
+ let target = process.cwd();
43
+ let artifactsDir;
44
+ let staticOnly = false;
45
+ let platform = env.RECIPE_HARNESS_PLATFORM || env.PLATFORM || "ios";
46
+ let preflightMode = env.RECIPE_HARNESS_MOBILE_PREFLIGHT_MODE || "fast";
47
+ let autoStart = env.RECIPE_HARNESS_MOBILE_AUTO_START ?? "false";
48
+ for (let i = 0; i < argv.length; i += 1) {
49
+ const arg = argv[i];
50
+ const needsValue = () => {
51
+ if (i + 1 >= argv.length) return void 0;
52
+ i += 1;
53
+ return argv[i];
54
+ };
55
+ switch (arg) {
56
+ case "--target": {
57
+ const value = needsValue();
58
+ if (value === void 0) return { kind: "error", message: "Missing value for --target", exitCode: 2 };
59
+ target = value;
60
+ break;
61
+ }
62
+ case "--artifacts-dir": {
63
+ const value = needsValue();
64
+ if (value === void 0) return { kind: "error", message: "Missing value for --artifacts-dir", exitCode: 2 };
65
+ artifactsDir = value;
66
+ break;
67
+ }
68
+ case "--static-only":
69
+ staticOnly = true;
70
+ break;
71
+ case "--platform": {
72
+ const value = needsValue();
73
+ if (value === void 0) return { kind: "error", message: "Missing value for --platform", exitCode: 2 };
74
+ platform = value;
75
+ break;
76
+ }
77
+ case "--preflight-mode": {
78
+ const value = needsValue();
79
+ if (value === void 0) return { kind: "error", message: "Missing value for --preflight-mode", exitCode: 2 };
80
+ preflightMode = value;
81
+ break;
82
+ }
83
+ case "--auto-start":
84
+ autoStart = "true";
85
+ break;
86
+ case "--no-auto-start":
87
+ autoStart = "false";
88
+ break;
89
+ case "-h":
90
+ case "--help":
91
+ return { kind: "help", usage: USAGE };
92
+ default:
93
+ return { kind: "error", message: `Unknown arg: ${arg}`, exitCode: 2 };
94
+ }
95
+ }
96
+ if (!PREFLIGHT_MODES.includes(preflightMode)) {
97
+ return { kind: "error", message: `Unknown --preflight-mode: ${preflightMode}`, exitCode: 2 };
98
+ }
99
+ if (platform !== "ios" && platform !== "android") {
100
+ return { kind: "error", message: `Unknown --platform: ${platform} (expected ios or android)`, exitCode: 2 };
101
+ }
102
+ if (!AUTO_START_TRUE.includes(autoStart) && !AUTO_START_FALSE.includes(autoStart)) {
103
+ return { kind: "error", message: `Unknown RECIPE_HARNESS_MOBILE_AUTO_START value: ${autoStart}`, exitCode: 2 };
104
+ }
105
+ return {
106
+ kind: "options",
107
+ options: {
108
+ target,
109
+ artifactsDir,
110
+ staticOnly,
111
+ platform,
112
+ preflightMode,
113
+ autoStart: AUTO_START_TRUE.includes(autoStart)
114
+ }
115
+ };
116
+ }
117
+ function isNumericWatcherPort(port) {
118
+ return port.length > 0 && /^[0-9]+$/u.test(port);
119
+ }
120
+ function classifyMobileRuntimeOwner(portHolder, startedByVerify, liveSmokeStatus) {
121
+ if (!portHolder) return "static-only";
122
+ if (startedByVerify) return "harness-owned";
123
+ if (!portHolder.listening) return "none";
124
+ return liveSmokeStatus === "pass" ? "compatible-external-or-harness" : "incompatible-external-or-stale";
125
+ }
126
+ function buildMobileVerifySummary(input) {
127
+ const liveRuntimeCheck = input.checks.find((check) => check.name === "runner v1 live bridge smoke");
128
+ const runtimeOwner = classifyMobileRuntimeOwner(input.portHolder, input.startedByVerify, liveRuntimeCheck?.status);
129
+ return {
130
+ adapter: "mobile",
131
+ status: input.status,
132
+ runtimeClassification: {
133
+ runtimeOwner,
134
+ recipeControllable: liveRuntimeCheck?.status === "pass",
135
+ startedByVerify: input.startedByVerify
136
+ },
137
+ cleanupOwnership: {
138
+ mayStop: input.startedByVerify,
139
+ reason: input.startedByVerify ? "verify launched the runtime through harness preflight" : "verify did not launch this runtime; do not stop human-owned or pre-existing processes automatically"
140
+ },
141
+ gitStatus: input.gitStatus,
142
+ runtimePolicy: {
143
+ preflightMode: input.preflightMode,
144
+ nativeBuildPolicy: input.preflightMode === "fast" ? "verify does not start product-local Mobile scripts; start/reuse a runner-owned or slot-owned runtime before live proof" : "verify did not start product-local Mobile scripts"
145
+ },
146
+ fixtureStatus: input.fixtureStatus,
147
+ portHolder: input.portHolder,
148
+ runtimeNotes: input.runtimeNotes,
149
+ checks: input.checks,
150
+ generatedAt: input.generatedAt
151
+ };
152
+ }
153
+ function resolveWatcherPort(target, env = process.env) {
154
+ const explicit = env.WATCHER_PORT || "";
155
+ if (explicit) return explicit;
156
+ for (const file of ENV_FILES) {
157
+ const full = path.join(target, file);
158
+ if (!fs.existsSync(full)) continue;
159
+ const match = fs.readFileSync(full, "utf8").match(/^\s*(?:export\s+)?WATCHER_PORT=(["']?)([0-9]+)\1/mu);
160
+ if (match) return match[2];
161
+ }
162
+ return "8081";
163
+ }
164
+ function resolveJsEnvValue(target, name, env = process.env) {
165
+ const fromProcess = env[name] || "";
166
+ if (fromProcess) return fromProcess;
167
+ const pattern = new RegExp(`^\\s*(?:export\\s+)?${name}=(["']?)([^"'\\n]+)\\1`, "mu");
168
+ for (const file of ENV_FILES) {
169
+ const full = path.join(target, file);
170
+ if (!fs.existsSync(full)) continue;
171
+ const match = fs.readFileSync(full, "utf8").match(pattern);
172
+ if (match) return match[2];
173
+ }
174
+ return "";
175
+ }
176
+ function runtimeContextValue(target, names) {
177
+ const file = path.join(target, recipeRuntimeDir(), "agentic-runtime.json");
178
+ for (const name of names) {
179
+ const value = readRuntimeContextField(file, name);
180
+ if (value !== void 0 && value !== "") return value;
181
+ }
182
+ return "";
183
+ }
184
+ function mobileDeviceTarget(target, platform, env) {
185
+ if (platform === "android") {
186
+ return env.ADB_SERIAL || env.ANDROID_SERIAL || env.ANDROID_DEVICE || runtimeContextValue(target, ["adbSerial", "androidDevice", "device"]);
187
+ }
188
+ return env.SIM_UDID || env.IOS_SIMULATOR || runtimeContextValue(target, ["simulator", "iosSimulator", "device"]);
189
+ }
190
+ function mobileFixtureStatus(target) {
191
+ const runtimeDir = recipeRuntimeDir();
192
+ const fixture = path.join(target, runtimeDir, "wallet-fixture.json");
193
+ if (!fs.existsSync(fixture)) {
194
+ return {
195
+ status: "MISSING_FIXTURES",
196
+ message: `No wallet fixture found. This run may spend time repairing wallet/perps state manually. For a clean isolated sandbox, create ${runtimeDir}/wallet-fixture.json.`,
197
+ setupCommand: `mkdir -p ${runtimeDir} && edit ${runtimeDir}/wallet-fixture.json with password + accounts`
198
+ };
199
+ }
200
+ const fixtureRaw = fs.readFileSync(fixture);
201
+ let valid = false;
202
+ let accountCount = null;
203
+ let hasPassword = false;
204
+ try {
205
+ const parsed = JSON.parse(fixtureRaw.toString("utf8"));
206
+ valid = true;
207
+ accountCount = Array.isArray(parsed.accounts) ? parsed.accounts.length : 0;
208
+ hasPassword = typeof parsed.password === "string" && parsed.password.length > 0;
209
+ } catch {
210
+ valid = false;
211
+ }
212
+ const relative = path.relative(target, fixture);
213
+ const ready = valid && hasPassword && (accountCount ?? 0) > 0;
214
+ return {
215
+ status: ready ? "READY" : "STALE_OR_INVALID",
216
+ path: relative,
217
+ sha256: crypto.createHash("sha256").update(fixtureRaw).digest("hex"),
218
+ modifiedAt: fs.statSync(fixture).mtime.toISOString(),
219
+ accountCount,
220
+ hasPassword,
221
+ message: ready ? `Fixture status: READY (${relative}, accounts=${accountCount}).` : `Fixture status: STALE_OR_INVALID (${relative}). Validate password/accounts before relying on a clean sandbox.`
222
+ };
223
+ }
224
+ function capture(command, args) {
225
+ try {
226
+ return execFileSync(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
227
+ } catch {
228
+ return "";
229
+ }
230
+ }
231
+ function mobilePortHolder(port) {
232
+ const pid = capture("lsof", [`-iTCP:${port}`, "-sTCP:LISTEN", "-t"]).split("\n")[0] ?? "";
233
+ const command = /^[0-9]+$/u.test(pid) ? capture("ps", ["-p", pid, "-o", "command="]) : "";
234
+ return {
235
+ port,
236
+ listening: Boolean(pid),
237
+ pid: pid || null,
238
+ command: command || null,
239
+ metroStatusReachable: null,
240
+ metroHttpProbeSkipped: true,
241
+ note: "HTTP /status probing is skipped during live verify because the React Native bridge is the authoritative readiness path for Mobile."
242
+ };
243
+ }
244
+ function spawnToLog(logPath, command, args, options = {}) {
245
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
246
+ const fd = fs.openSync(logPath, "w");
247
+ try {
248
+ const result = spawnSync(command, args, {
249
+ cwd: options.cwd,
250
+ env: options.env ?? process.env,
251
+ stdio: ["ignore", fd, fd],
252
+ ...options.timeoutMs === void 0 ? {} : { timeout: options.timeoutMs }
253
+ });
254
+ if (result.error) {
255
+ const timedOut = result.error.code === "ETIMEDOUT";
256
+ fs.writeSync(
257
+ fd,
258
+ timedOut ? `Timed out after ${(options.timeoutMs ?? 0) / 1e3}s: ${[command, ...args].join(" ")}
259
+ ` : `${result.error.message}
260
+ `
261
+ );
262
+ return timedOut ? 124 : 127;
263
+ }
264
+ return result.status ?? 1;
265
+ } finally {
266
+ fs.closeSync(fd);
267
+ }
268
+ }
269
+ function readJsonFile(file) {
270
+ try {
271
+ return JSON.parse(fs.readFileSync(file, "utf8"));
272
+ } catch {
273
+ return null;
274
+ }
275
+ }
276
+ function gitStatusSummary(target, harnessRootExclude) {
277
+ const runGit = (args) => {
278
+ try {
279
+ return execFileSync("git", ["-C", target, ...args], {
280
+ encoding: "utf8",
281
+ stdio: ["ignore", "pipe", "pipe"]
282
+ }).trim();
283
+ } catch {
284
+ return null;
285
+ }
286
+ };
287
+ const statusShort = runGit(["status", "--short", "--", ".", `:(exclude)${harnessRootExclude}`]);
288
+ const dirty = statusShort ? statusShort.split("\n").filter(Boolean) : [];
289
+ return {
290
+ branch: runGit(["branch", "--show-current"]),
291
+ head: runGit(["rev-parse", "--short", "HEAD"]),
292
+ dirtyCount: dirty.length,
293
+ dirtyPreview: dirty.slice(0, 25)
294
+ };
295
+ }
296
+ function utcStamp(date) {
297
+ return `${date.toISOString().replace(/[-:]/gu, "").split(".")[0]}Z`;
298
+ }
299
+ function sleep(ms) {
300
+ return new Promise((resolve) => {
301
+ setTimeout(resolve, ms);
302
+ });
303
+ }
304
+ const defaultIo = {
305
+ out: (line) => process.stdout.write(`${line}
306
+ `),
307
+ err: (line) => process.stderr.write(`${line}
308
+ `)
309
+ };
310
+ async function runMobileVerify(argv, io = defaultIo) {
311
+ const parsed = parseMobileVerifyArgs(argv);
312
+ if (parsed.kind === "help") {
313
+ io.out(parsed.usage);
314
+ return 0;
315
+ }
316
+ if (parsed.kind === "error") {
317
+ io.err(parsed.message);
318
+ return parsed.exitCode;
319
+ }
320
+ const { staticOnly, platform, preflightMode, autoStart } = parsed.options;
321
+ const target = path.resolve(parsed.options.target);
322
+ if (!fs.existsSync(target)) {
323
+ io.err(`Refusing mobile verify: target checkout does not exist: ${target}`);
324
+ io.err(" Next: pass --target <metamask-mobile checkout>");
325
+ return 1;
326
+ }
327
+ const harnessRoot = recipeHarnessRoot();
328
+ const harnessRel = `${harnessRoot}/mobile`;
329
+ const harnessDir = recipeHarnessPath(target, "mobile");
330
+ const runnerBin = path.join(harnessDir, "runner/bin/mm-harness");
331
+ const artifacts = parsed.options.artifactsDir ? path.resolve(parsed.options.artifactsDir) : path.join(harnessDir, "verify", utcStamp(/* @__PURE__ */ new Date()));
332
+ fs.mkdirSync(path.join(artifacts, "logs"), { recursive: true });
333
+ const notesPath = path.join(artifacts, "logs/runtime-notes.txt");
334
+ const addNote = (note) => {
335
+ fs.appendFileSync(notesPath, `${note}
336
+ `);
337
+ };
338
+ let status = "pass";
339
+ const checks = [];
340
+ const checkFile = (rel) => {
341
+ const ok = fs.existsSync(path.join(target, rel));
342
+ checks.push({ name: rel, status: ok ? "pass" : "fail" });
343
+ if (!ok) status = "fail";
344
+ };
345
+ const checkContains = (name, file, needle) => {
346
+ let ok = false;
347
+ try {
348
+ ok = fs.readFileSync(path.join(target, file), "utf8").includes(needle);
349
+ } catch {
350
+ ok = false;
351
+ }
352
+ checks.push({ name, status: ok ? "pass" : "fail" });
353
+ if (!ok) status = "fail";
354
+ };
355
+ checkFile(`${harnessRel}/manifest.json`);
356
+ checkFile(`${harnessRel}/action-manifest.json`);
357
+ checkFile(`${harnessRel}/runner/bin/mm-harness`);
358
+ checkFile("package.json");
359
+ checkFile("app/dev-tools/AgenticService/AgenticService.ts");
360
+ checkContains("NavigationService agentic bridge", "app/core/NavigationService/NavigationService.ts", "AgenticService.install");
361
+ checkContains("App AgentStepHud", "app/components/Nav/App/App.tsx", "AgentStepHud");
362
+ let fixtureStatus = null;
363
+ let portHolder = null;
364
+ if (!staticOnly) {
365
+ const port = resolveWatcherPort(target);
366
+ if (!isNumericWatcherPort(port)) {
367
+ io.err(`Refusing mobile verify: resolved watcher port is not numeric: '${port}' (check WATCHER_PORT)`);
368
+ return 2;
369
+ }
370
+ let deviceTarget = mobileDeviceTarget(target, platform, process.env);
371
+ if (!deviceTarget) {
372
+ deviceTarget = platform === "android" ? resolveJsEnvValue(target, "ADB_SERIAL") || resolveJsEnvValue(target, "ANDROID_DEVICE") : resolveJsEnvValue(target, "IOS_SIMULATOR");
373
+ }
374
+ const deviceArgs = deviceTarget ? ["--device", deviceTarget] : [];
375
+ const statusSmokeRecipe = path.join(artifacts, "mobile-status-smoke.recipe.json");
376
+ const precheckLog = path.join(artifacts, "logs/app-status-precheck.log");
377
+ let liveRuntimeOk = false;
378
+ for (let attempt = 1; attempt <= 5; attempt += 1) {
379
+ fs.writeFileSync(statusSmokeRecipe, STATUS_SMOKE_RECIPE);
380
+ const code = spawnToLog(precheckLog, runnerBin, [
381
+ "run",
382
+ statusSmokeRecipe,
383
+ "--adapter",
384
+ "mobile",
385
+ "--target",
386
+ target,
387
+ ...deviceArgs,
388
+ "--watcher-port",
389
+ port,
390
+ "--artifacts-dir",
391
+ path.join(artifacts, "runner-status-smoke"),
392
+ "--json"
393
+ ], { cwd: target });
394
+ if (code === 0) {
395
+ liveRuntimeOk = true;
396
+ break;
397
+ }
398
+ const deviceNotFound = /"code"[ \t]*:[ \t]*"DEVICE_NOT_FOUND"/u.test(
399
+ fs.readFileSync(precheckLog, "utf8")
400
+ );
401
+ if (attempt < 5 && deviceNotFound) {
402
+ io.err(`Waiting for Mobile device discovery (${attempt}/5): ${deviceTarget || "configured target"}`);
403
+ await sleep(1e3);
404
+ continue;
405
+ }
406
+ break;
407
+ }
408
+ if (!liveRuntimeOk && autoStart) io.err(AUTO_START_REFUSAL);
409
+ checks.push(liveRuntimeOk ? { name: "mobile runtime controllable precheck", status: "pass" } : { name: "mobile runtime controllable precheck", status: "fail", detail: "see logs/app-status-precheck.log" });
410
+ if (!liveRuntimeOk) status = "fail";
411
+ fixtureStatus = mobileFixtureStatus(target);
412
+ fs.writeFileSync(path.join(artifacts, "logs/fixture-status.json"), `${JSON.stringify(fixtureStatus)}
413
+ `);
414
+ const fixtureCheck = {
415
+ name: "fixture status",
416
+ status: fixtureStatus.status === "READY" ? "pass" : "warn",
417
+ detail: fixtureStatus.path || fixtureStatus.status || "",
418
+ message: fixtureStatus.message || fixtureStatus.status
419
+ };
420
+ io.err(fixtureCheck.message);
421
+ addNote(fixtureCheck.message);
422
+ checks.push(fixtureCheck);
423
+ portHolder = mobilePortHolder(port);
424
+ fs.writeFileSync(path.join(artifacts, "logs/port-holder.json"), `${JSON.stringify(portHolder)}
425
+ `);
426
+ if (status !== "fail" && portHolder.listening) {
427
+ const waiter = path.join(runnerDir, "adapters/mobile/wait-for-bridge.sh");
428
+ if (!fs.existsSync(waiter)) {
429
+ addNote("Mobile bridge settle skipped: wait-for-bridge.sh not found under the runner adapters; reinstall the runner.");
430
+ } else {
431
+ const waiterCode = spawnToLog(
432
+ path.join(artifacts, "logs/wait-for-bridge.log"),
433
+ "bash",
434
+ [waiter, "--target", target, "--port", port, "--platform", platform],
435
+ {
436
+ env: {
437
+ ...process.env,
438
+ MOBILE_BRIDGE_READY_TIMEOUT_MS: process.env.MOBILE_BRIDGE_READY_TIMEOUT_MS || "90000",
439
+ MOBILE_BRIDGE_UNRESPONSIVE_GRACE_MS: process.env.MOBILE_BRIDGE_UNRESPONSIVE_GRACE_MS || "90000"
440
+ }
441
+ }
442
+ );
443
+ addNote(waiterCode === 0 ? "Mobile bridge settled before the live smoke (logs/wait-for-bridge.log)." : `Mobile bridge settle did not complete (waiter exit ${waiterCode}); running the live smoke anyway (logs/wait-for-bridge.log).`);
444
+ }
445
+ }
446
+ const liveSmokeRecipe = path.join(artifacts, "mobile-v1-live-smoke.recipe.json");
447
+ fs.writeFileSync(liveSmokeRecipe, LIVE_SMOKE_RECIPE);
448
+ const iosSimulatorResolved = resolveJsEnvValue(target, "IOS_SIMULATOR");
449
+ const adbSerialResolved = resolveJsEnvValue(target, "ADB_SERIAL");
450
+ const liveSmokeCode = spawnToLog(
451
+ path.join(artifacts, "logs/runner-live-smoke.log"),
452
+ runnerBin,
453
+ [
454
+ "run",
455
+ liveSmokeRecipe,
456
+ "--adapter",
457
+ "mobile",
458
+ "--target",
459
+ target,
460
+ ...deviceArgs,
461
+ "--watcher-port",
462
+ port,
463
+ "--artifacts-dir",
464
+ path.join(artifacts, "runner-live-smoke"),
465
+ "--json"
466
+ ],
467
+ {
468
+ cwd: target,
469
+ env: {
470
+ ...process.env,
471
+ METAMASK_RECIPE_AUTO_HUD: "0",
472
+ IOS_SIMULATOR: process.env.IOS_SIMULATOR || iosSimulatorResolved,
473
+ ADB_SERIAL: process.env.ADB_SERIAL || adbSerialResolved,
474
+ ANDROID_SERIAL: process.env.ANDROID_SERIAL || adbSerialResolved
475
+ }
476
+ }
477
+ );
478
+ if (liveSmokeCode === 0) {
479
+ checks.push({ name: "runner v1 live bridge smoke", status: "pass" });
480
+ } else {
481
+ checks.push({ name: "runner v1 live bridge smoke", status: "fail", detail: "see logs/runner-live-smoke.log" });
482
+ addNote("Runner v1 live bridge smoke failed; inspect logs/runner-live-smoke.log and runner-live-smoke/trace.json.");
483
+ status = "fail";
484
+ }
485
+ const evmLog = path.join(artifacts, "logs/evm-rpc-readiness.log");
486
+ const bridge = path.join(harnessDir, "bridge-runtime/cdp-bridge.cjs");
487
+ let evmOk = false;
488
+ if (!fs.existsSync(bridge)) {
489
+ fs.writeFileSync(evmLog, `Mobile bridge is missing at ${bridge}
490
+ `);
491
+ } else {
492
+ const evmCode = spawnToLog(evmLog, process.execPath, [bridge, "eval-async", EVM_READINESS_EXPRESSION], {
493
+ timeoutMs: 25e3,
494
+ env: {
495
+ ...process.env,
496
+ APP_ROOT: target,
497
+ WATCHER_PORT: port,
498
+ IOS_SIMULATOR: process.env.IOS_SIMULATOR || iosSimulatorResolved,
499
+ ADB_SERIAL: process.env.ADB_SERIAL || adbSerialResolved,
500
+ ANDROID_SERIAL: process.env.ANDROID_SERIAL || adbSerialResolved
501
+ }
502
+ });
503
+ evmOk = evmCode === 0 && readJsonFile(evmLog)?.ok === true;
504
+ }
505
+ if (evmOk) {
506
+ checks.push({ name: "live EVM RPC readiness", status: "pass" });
507
+ } else {
508
+ checks.push({ name: "live EVM RPC readiness", status: "fail", detail: "see logs/evm-rpc-readiness.log" });
509
+ addNote(`EVM RPC readiness probe failed. Infura configured \u2260 RPC reachable. Next: confirm ${target}/.js.env has a real MM_INFURA_PROJECT_ID (not 00000000000), then rerun mm-harness launch ${platform} --verify --target '${target}'.`);
510
+ status = "fail";
511
+ }
512
+ }
513
+ const summary = buildMobileVerifySummary({
514
+ status,
515
+ preflightMode,
516
+ startedByVerify: fs.existsSync(path.join(artifacts, "logs/harness-started-runtime")),
517
+ fixtureStatus: readJsonFile(path.join(artifacts, "logs/fixture-status.json")),
518
+ portHolder: readJsonFile(path.join(artifacts, "logs/port-holder.json")),
519
+ runtimeNotes: readRuntimeNotes(notesPath),
520
+ checks,
521
+ gitStatus: gitStatusSummary(target, harnessRoot),
522
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
523
+ });
524
+ fs.writeFileSync(path.join(artifacts, "summary.json"), `${JSON.stringify(summary, null, 2)}
525
+ `);
526
+ fs.writeFileSync(
527
+ path.join(artifacts, "artifact-manifest.json"),
528
+ `${JSON.stringify({ artifacts: fs.readdirSync(artifacts).map((name) => ({ path: name })) }, null, 2)}
529
+ `
530
+ );
531
+ io.out(`Mobile harness verify ${status}: ${path.join(artifacts, "summary.json")}`);
532
+ return status === "pass" ? 0 : 1;
533
+ }
534
+ function readRuntimeNotes(notesPath) {
535
+ try {
536
+ return fs.readFileSync(notesPath, "utf8").trim().split("\n").filter(Boolean);
537
+ } catch {
538
+ return [];
539
+ }
540
+ }
541
+ export {
542
+ buildMobileVerifySummary,
543
+ classifyMobileRuntimeOwner,
544
+ isNumericWatcherPort,
545
+ parseMobileVerifyArgs,
546
+ resolveJsEnvValue,
547
+ resolveWatcherPort,
548
+ runMobileVerify
549
+ };
@@ -3,6 +3,7 @@ import {
3
3
  formatExtensionIdentityPorts,
4
4
  formatExtensionSlotPorts,
5
5
  inferSlotSuffix,
6
+ poolFilesForHost,
6
7
  realRepoPath,
7
8
  resolveDefaultExtensionPorts,
8
9
  resolveExtensionRuntimeContext,
@@ -17,6 +18,7 @@ export {
17
18
  formatExtensionSlotPorts,
18
19
  formatKvLines,
19
20
  inferSlotSuffix,
21
+ poolFilesForHost,
20
22
  realRepoPath,
21
23
  resolveDefaultExtensionPorts,
22
24
  resolveExtensionRuntimeContext,
package/dist/cli.js CHANGED
@@ -103,7 +103,7 @@ async function main(argv) {
103
103
  if (command === "completion-candidates") return handleCompletionCandidates(argv.slice(1));
104
104
  if (command === "provision") return handleProvision(parseArgs(argv.slice(1), command));
105
105
  if (OVERLAY_COMMANDS.includes(command)) {
106
- if (command === "verify") {
106
+ if (command === "verify" && process.env.MM_HARNESS_SKIP_OVERLAY_ENSURE !== "1") {
107
107
  try {
108
108
  const { options } = parseArgs(argv.slice(1), command);
109
109
  const target = targetPath(options);
@@ -89,7 +89,7 @@ async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManif
89
89
  preExecute
90
90
  );
91
91
  if (preExecuteDrift.length > 0) {
92
- const provenancePath2 = writeExecutionProvenance(
92
+ const provenancePath2 = await writeExecutionProvenance(
93
93
  absoluteArtifactsDir,
94
94
  provenanceSnapshots,
95
95
  preExecuteDrift
@@ -134,7 +134,7 @@ async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManif
134
134
  const end = await captureExecutionProvenance(provenanceInput, "end");
135
135
  provenanceSnapshots.push(end);
136
136
  const endDrift = executionProvenanceDrift(provenanceSnapshots[0], end);
137
- const provenancePath = writeExecutionProvenance(
137
+ const provenancePath = await writeExecutionProvenance(
138
138
  absoluteArtifactsDir,
139
139
  provenanceSnapshots,
140
140
  endDrift,
@@ -3,6 +3,7 @@ import { createHash } from "node:crypto";
3
3
  import fs, { constants as fsConstants } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { mobileSourceFingerprint } from "./adapters/mobile/source-freshness.js";
6
+ import { indexArtifactManifest } from "./artifact-files.js";
6
7
  import { importRecipeProtocol, runnerDir } from "./paths.js";
7
8
  const gitContexts = /* @__PURE__ */ new Map();
8
9
  let protocolPromise;
@@ -102,7 +103,7 @@ function executionProvenanceDrift(start, current) {
102
103
  }
103
104
  return drift;
104
105
  }
105
- function writeExecutionProvenance(artifactsDir, snapshots, drift, artifactManifestPath) {
106
+ async function writeExecutionProvenance(artifactsDir, snapshots, drift, artifactManifestPath) {
106
107
  const provenancePath = path.join(path.resolve(artifactsDir), "execution-provenance.json");
107
108
  const record = {
108
109
  schemaVersion: 1,
@@ -113,7 +114,20 @@ function writeExecutionProvenance(artifactsDir, snapshots, drift, artifactManife
113
114
  fs.mkdirSync(path.dirname(provenancePath), { recursive: true });
114
115
  fs.writeFileSync(provenancePath, `${JSON.stringify(record, null, 2)}
115
116
  `);
116
- if (artifactManifestPath) indexProvenanceArtifact(artifactManifestPath, record);
117
+ if (artifactManifestPath) {
118
+ await indexArtifactManifest(artifactManifestPath, [
119
+ {
120
+ path: "execution-provenance.json",
121
+ type: "json",
122
+ label: "Execution provenance",
123
+ category: "system",
124
+ metadata: {
125
+ valid: record.valid,
126
+ errorCode: record.valid ? null : "PROVENANCE_DRIFT"
127
+ }
128
+ }
129
+ ]);
130
+ }
117
131
  return provenancePath;
118
132
  }
119
133
  function recipeFileDigest(protocol, recipePath) {
@@ -309,32 +323,6 @@ function compareSource(drift, phase, prefix, start, current) {
309
323
  function compare(drift, phase, field, start, current) {
310
324
  if (start !== current) drift.push({ phase, field, start, current });
311
325
  }
312
- function indexProvenanceArtifact(artifactManifestPath, record) {
313
- let manifest;
314
- try {
315
- manifest = JSON.parse(fs.readFileSync(artifactManifestPath, "utf8"));
316
- } catch {
317
- return;
318
- }
319
- if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) return;
320
- const document = manifest;
321
- const artifacts = Array.isArray(document.artifacts) ? document.artifacts : [];
322
- document.artifacts = [
323
- ...artifacts.filter((artifact) => !artifact || typeof artifact !== "object" || Array.isArray(artifact) || artifact.path !== "execution-provenance.json"),
324
- {
325
- path: "execution-provenance.json",
326
- type: "json",
327
- label: "Execution provenance",
328
- category: "system",
329
- metadata: {
330
- valid: record.valid,
331
- errorCode: record.valid ? null : "PROVENANCE_DRIFT"
332
- }
333
- }
334
- ];
335
- fs.writeFileSync(artifactManifestPath, `${JSON.stringify(document, null, 2)}
336
- `);
337
- }
338
326
  export {
339
327
  ProvenanceDriftError,
340
328
  captureExecutionProvenance,