@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.
- package/CHANGELOG.md +19 -0
- package/adapters/extension/inject.mjs +0 -1
- package/adapters/extension/live.sh +11 -14
- package/adapters/manifest.json +7 -7
- package/adapters/mobile/inject.sh +4 -7
- package/adapters/shared/resolve-slot-ports-core.mjs +47 -19
- package/dist/adapters/extension/runtime-decision.js +1 -2
- package/dist/adapters/extension/runtime.js +4 -3
- package/dist/adapters/extension/verify.js +608 -0
- package/dist/adapters/mobile/verify.js +549 -0
- package/dist/adapters/resolve-slot-ports.js +2 -0
- package/dist/cli.js +1 -1
- package/dist/commands/run-engine.js +2 -2
- package/dist/execution-provenance.js +16 -28
- package/dist/harness.js +34 -18
- package/package.json +2 -2
- package/adapters/extension/verify.sh +0 -572
- package/adapters/mobile/verify.sh +0 -557
|
@@ -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)
|
|
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,
|