@deeeed/metamask-harness 0.17.4 → 0.18.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 +775 -0
- package/README.md +65 -71
- package/adapters/extension/ensure-browser.sh +12 -1
- package/adapters/extension/inject.mjs +7 -0
- package/adapters/extension/launch-browser.cjs +10 -3
- package/adapters/extension/lib/chrome-args.cjs +31 -0
- package/adapters/extension/lib/macos-focus.cjs +32 -0
- package/adapters/extension/live.sh +10 -20
- package/adapters/manifest.json +8 -0
- package/adapters/mobile/open-device.sh +45 -7
- package/adapters/mobile/verify.sh +15 -3
- package/adapters/shared/harness-source-fingerprint.mjs +49 -0
- package/adapters/shared/install-repo-deps.sh +1 -5
- package/adapters/shared/open-debug.mjs +184 -103
- package/adapters/shared/resolve-slot-ports-core.mjs +23 -6
- package/adapters/shared/resolve-slot-ports.sh +22 -5
- package/bin/mm-harness +15 -3
- package/dist/adapters/core/surface.js +4 -1
- package/dist/adapters/extension/console-capture.js +3 -1
- package/dist/adapters/extension/harness-freshness.js +39 -0
- package/dist/adapters/extension/product-config.js +110 -0
- package/dist/adapters/extension/runtime-decision.js +20 -71
- package/dist/adapters/extension/surface.js +19 -1
- package/dist/adapters/mobile/prepare.js +17 -0
- package/dist/adapters/mobile/source-freshness.js +26 -41
- package/dist/adapters/mobile/surface.js +4 -1
- package/dist/adapters/resolve-slot-ports.js +2 -0
- package/dist/adapters/slot-ports.js +13 -32
- package/dist/adapters.js +50 -17
- package/dist/checkout-lock.js +27 -2
- package/dist/cli-color.js +19 -0
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +2 -3
- package/dist/command-contract.js +13 -3
- package/dist/commands/call.js +115 -29
- package/dist/commands/checklist.js +4 -1
- package/dist/commands/completion-candidates.js +20 -13
- package/dist/commands/debug.js +31 -38
- package/dist/commands/doctor.js +33 -6
- package/dist/commands/fixtures.js +65 -17
- package/dist/commands/flows.js +39 -10
- package/dist/commands/launch/extension.js +40 -15
- package/dist/commands/launch/index.js +41 -5
- package/dist/commands/list-executables.js +151 -29
- package/dist/commands/manifest.js +127 -18
- package/dist/commands/parse-args.js +11 -1
- package/dist/commands/run-engine.js +384 -56
- package/dist/commands/run.js +112 -17
- package/dist/commands/shared.js +22 -2
- package/dist/commands/status-probe.js +3 -0
- package/dist/commands/status.js +1 -0
- package/dist/completions-cache.js +1 -1
- package/dist/doctor.js +56 -6
- package/dist/harness.js +6 -5
- package/dist/heal-bounds.js +1 -1
- package/dist/live-adapter-contract.js +132 -12
- package/dist/manifest.js +161 -1
- package/dist/mm-harness-cli.js +13 -7
- package/dist/recipe-security.js +178 -0
- package/dist/runner.js +87 -13
- package/dist/runtime-context.js +8 -26
- package/docs/CONTRIBUTING.md +137 -0
- package/docs/QA.md +185 -0
- package/docs/RECIPES.md +161 -0
- package/docs/SECURITY.md +88 -0
- package/library/actions/core/perps/read_account.mjs +2 -2
- package/library/actions/core/perps/read_orders.mjs +2 -1
- package/library/actions/core/perps/read_positions.mjs +2 -1
- package/library/actions/core/wallet/list_accounts.mjs +95 -0
- package/library/actions/extension/platform/cdp.mjs +1 -0
- package/library/actions/extension/wallet/list_accounts.mjs +41 -0
- package/library/actions/mobile/platform/bridge.mjs +1 -5
- package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
- package/library/manifests/core.action-manifest.json +61 -0
- package/library/manifests/extension.action-manifest.json +53 -0
- package/library/manifests/mobile.action-manifest.json +53 -0
- package/library/recipes/runner/action-validation.extension.recipe.json +8 -1
- package/library/recipes/runner/action-validation.mobile.recipe.json +8 -1
- package/package.json +10 -6
- package/scripts/completions.sh +7 -7
- package/docs/ADAPTER-SURFACE.md +0 -26
- package/docs/CHEATSHEET.md +0 -48
- package/docs/CLI-ERGONOMICS-AUDIT.md +0 -32
- package/docs/CLI-ERGONOMICS-HUMAN-QA.md +0 -64
- package/docs/CODE-MAP.md +0 -62
- package/docs/UX-PRINCIPLES.md +0 -66
- package/docs/VIDEO-DEMO-VALIDATION.md +0 -74
- package/docs/architecture.md +0 -88
- package/docs/live-adapter-contract.md +0 -190
- package/docs/package-boundaries.md +0 -47
- package/docs/perps-flow-catalog.md +0 -235
- package/docs/recipe-libraries.md +0 -71
- package/docs/runtime-file-conventions.md +0 -36
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
* open-debug.mjs — open human-facing debugger UIs for recipe runtimes.
|
|
4
4
|
*
|
|
5
5
|
* Mobile: Metro /open-debugger or /open-dev-menu HTTP endpoints.
|
|
6
|
-
* Extension: DevTools
|
|
6
|
+
* Extension: Chrome DevTools frontend for an extension page or service worker.
|
|
7
7
|
*/
|
|
8
8
|
import { spawnSync } from 'node:child_process';
|
|
9
|
-
import
|
|
9
|
+
import http from 'node:http';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
10
11
|
|
|
11
12
|
function usage() {
|
|
12
13
|
console.error(`Usage:
|
|
@@ -97,8 +98,8 @@ async function fetchOk(url, init) {
|
|
|
97
98
|
return response.ok;
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
async function fetchJson(url
|
|
101
|
-
const response = await fetch(url
|
|
101
|
+
async function fetchJson(url) {
|
|
102
|
+
const response = await fetch(url);
|
|
102
103
|
if (!response.ok) {
|
|
103
104
|
throw new Error(`HTTP ${response.status} for ${url}`);
|
|
104
105
|
}
|
|
@@ -106,52 +107,109 @@ async function fetchJson(url, init) {
|
|
|
106
107
|
return body;
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
|
|
110
|
+
// Probe whether the debug server will accept a browser DevTools frontend
|
|
111
|
+
// WebSocket for `targetId` under `origin`. Chrome 403s every browser Origin
|
|
112
|
+
// unless the slot launched with --remote-allow-origins for it; a slot launched
|
|
113
|
+
// before that flag existed fails here, so we can tell the user to relaunch
|
|
114
|
+
// instead of opening a window that shows "Debugging connection was closed".
|
|
115
|
+
// Transient: the socket is destroyed the instant the handshake resolves.
|
|
116
|
+
function probeAttach(port, targetId, origin) {
|
|
110
117
|
return new Promise((resolve) => {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
params,
|
|
123
|
-
}));
|
|
118
|
+
const req = http.request({
|
|
119
|
+
host: '127.0.0.1',
|
|
120
|
+
port,
|
|
121
|
+
path: `/devtools/page/${targetId}`,
|
|
122
|
+
headers: {
|
|
123
|
+
Connection: 'Upgrade',
|
|
124
|
+
Upgrade: 'websocket',
|
|
125
|
+
'Sec-WebSocket-Version': '13',
|
|
126
|
+
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
|
|
127
|
+
Origin: origin,
|
|
128
|
+
},
|
|
124
129
|
});
|
|
125
|
-
|
|
126
|
-
|
|
130
|
+
let settled = false;
|
|
131
|
+
const finish = (value) => {
|
|
132
|
+
if (settled) return;
|
|
133
|
+
settled = true;
|
|
127
134
|
try {
|
|
128
|
-
|
|
135
|
+
req.destroy();
|
|
129
136
|
} catch {
|
|
130
|
-
|
|
131
|
-
return;
|
|
137
|
+
// Best-effort teardown of the probe socket.
|
|
132
138
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
});
|
|
136
|
-
|
|
139
|
+
resolve(value);
|
|
140
|
+
};
|
|
141
|
+
req.on('upgrade', () => finish({ ok: true }));
|
|
142
|
+
req.on('response', (res) => finish({ ok: false, status: res.statusCode }));
|
|
143
|
+
req.on('error', (err) => finish({ ok: false, error: err.message }));
|
|
144
|
+
req.setTimeout(3000, () => finish({ ok: false, error: 'timeout' }));
|
|
145
|
+
req.end();
|
|
137
146
|
});
|
|
138
147
|
}
|
|
139
148
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (!devtoolsId) throw new Error('Chrome did not return a native DevTools target');
|
|
148
|
-
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
149
|
-
const targets = await fetchJson(`http://127.0.0.1:${cdpPort}/json/list`);
|
|
150
|
-
const devtools = targets.find((candidate) => candidate.id === devtoolsId);
|
|
151
|
-
if (devtools && String(devtools.url || '').startsWith('devtools://devtools/')) return devtools;
|
|
152
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
149
|
+
function resolveWebSocketImpl() {
|
|
150
|
+
if (typeof WebSocket === 'function') return WebSocket;
|
|
151
|
+
try {
|
|
152
|
+
const require = createRequire(import.meta.url);
|
|
153
|
+
return require(require.resolve('ws', { paths: [process.cwd()] }));
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
153
156
|
}
|
|
154
|
-
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Open the locally-served bundled DevTools frontend as its own window inside the
|
|
160
|
+
// slot's own Chromium, via a browser-level CDP call (the Node client sends no
|
|
161
|
+
// Origin header, so it is never origin-rejected). Returns the new target id.
|
|
162
|
+
async function openDevtoolsWindow(browserWsUrl, url) {
|
|
163
|
+
const WebSocketImpl = resolveWebSocketImpl();
|
|
164
|
+
if (!WebSocketImpl) throw new Error('WebSocket unavailable in this Node runtime to open the DevTools window');
|
|
165
|
+
return new Promise((resolve, reject) => {
|
|
166
|
+
const ws = new WebSocketImpl(browserWsUrl);
|
|
167
|
+
const timer = setTimeout(() => {
|
|
168
|
+
try {
|
|
169
|
+
ws.close();
|
|
170
|
+
} catch {
|
|
171
|
+
// Best-effort cleanup on timeout.
|
|
172
|
+
}
|
|
173
|
+
reject(new Error('timeout creating the DevTools window over CDP'));
|
|
174
|
+
}, 8000);
|
|
175
|
+
const onOpen = () => {
|
|
176
|
+
ws.send(JSON.stringify({
|
|
177
|
+
id: 1,
|
|
178
|
+
method: 'Target.createTarget',
|
|
179
|
+
params: { url, newWindow: true, width: 1200, height: 800 },
|
|
180
|
+
}));
|
|
181
|
+
};
|
|
182
|
+
const onMessage = (event) => {
|
|
183
|
+
const raw = event?.data ?? event;
|
|
184
|
+
let msg;
|
|
185
|
+
try {
|
|
186
|
+
msg = JSON.parse(Buffer.isBuffer(raw) ? raw.toString('utf8') : String(raw));
|
|
187
|
+
} catch {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (msg.id !== 1) return;
|
|
191
|
+
clearTimeout(timer);
|
|
192
|
+
ws.close();
|
|
193
|
+
if (msg.error) {
|
|
194
|
+
reject(new Error(msg.error.message || 'Target.createTarget failed'));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
resolve(msg.result?.targetId || null);
|
|
198
|
+
};
|
|
199
|
+
const onError = (err) => {
|
|
200
|
+
clearTimeout(timer);
|
|
201
|
+
reject(new Error(`browser CDP websocket error while opening DevTools: ${err?.message || err || 'unknown'}`));
|
|
202
|
+
};
|
|
203
|
+
if (typeof ws.on === 'function') {
|
|
204
|
+
ws.on('open', onOpen);
|
|
205
|
+
ws.on('message', onMessage);
|
|
206
|
+
ws.on('error', onError);
|
|
207
|
+
} else {
|
|
208
|
+
ws.addEventListener('open', onOpen);
|
|
209
|
+
ws.addEventListener('message', onMessage);
|
|
210
|
+
ws.addEventListener('error', onError);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
155
213
|
}
|
|
156
214
|
|
|
157
215
|
// CDP-eval fallback: invoke DevSettings native module directly via the bridge
|
|
@@ -256,8 +314,7 @@ function pickExtensionTarget(targets, targetKind) {
|
|
|
256
314
|
|
|
257
315
|
const pages = extensionTargets.filter((target) => target.type === 'page');
|
|
258
316
|
return (
|
|
259
|
-
pages.find((target) => String(target.url || '').includes('/
|
|
260
|
-
|| pages.find((target) => String(target.url || '').includes('/home.html'))
|
|
317
|
+
pages.find((target) => String(target.url || '').includes('/home.html'))
|
|
261
318
|
|| pages.find((target) => !String(target.url || '').includes('/popup-init.html'))
|
|
262
319
|
|| pages[0]
|
|
263
320
|
|| null
|
|
@@ -270,7 +327,6 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
|
|
|
270
327
|
return { ok: false, adapter: 'extension', error: 'invalid-cdp-port', cdpPort };
|
|
271
328
|
}
|
|
272
329
|
|
|
273
|
-
const inspectUrl = 'chrome://inspect/#devices';
|
|
274
330
|
const configureHint = `127.0.0.1:${numericPort}`;
|
|
275
331
|
const versionEndpoint = `http://127.0.0.1:${numericPort}/json/version`;
|
|
276
332
|
const listEndpoint = `http://127.0.0.1:${numericPort}/json/list`;
|
|
@@ -286,9 +342,8 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
|
|
|
286
342
|
cdpPort: numericPort,
|
|
287
343
|
error: 'cdp-unreachable',
|
|
288
344
|
endpoint: versionEndpoint,
|
|
289
|
-
inspectUrl,
|
|
290
345
|
configureHint,
|
|
291
|
-
hint:
|
|
346
|
+
hint: 'Start the extension runtime first (mm-harness launch).',
|
|
292
347
|
detail: error instanceof Error ? error.message : String(error),
|
|
293
348
|
};
|
|
294
349
|
}
|
|
@@ -304,9 +359,8 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
|
|
|
304
359
|
cdpPort: numericPort,
|
|
305
360
|
error: 'cdp-list-failed',
|
|
306
361
|
endpoint: listEndpoint,
|
|
307
|
-
inspectUrl,
|
|
308
362
|
configureHint,
|
|
309
|
-
hint:
|
|
363
|
+
hint: 'CDP is up but /json/list failed. Relaunch the runtime (mm-harness launch).',
|
|
310
364
|
detail: error instanceof Error ? error.message : String(error),
|
|
311
365
|
};
|
|
312
366
|
}
|
|
@@ -319,80 +373,107 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
|
|
|
319
373
|
cdpPort: numericPort,
|
|
320
374
|
error: 'cdp-list-invalid',
|
|
321
375
|
endpoint: listEndpoint,
|
|
322
|
-
inspectUrl,
|
|
323
376
|
configureHint,
|
|
324
|
-
hint:
|
|
377
|
+
hint: 'Relaunch the runtime (mm-harness launch).',
|
|
325
378
|
};
|
|
326
379
|
}
|
|
327
380
|
|
|
328
381
|
const picked = pickExtensionTarget(targets, targetKind);
|
|
329
|
-
if (picked
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
382
|
+
if (!picked) {
|
|
383
|
+
return {
|
|
384
|
+
ok: false,
|
|
385
|
+
adapter: 'extension',
|
|
386
|
+
targetKind,
|
|
387
|
+
cdpPort: numericPort,
|
|
388
|
+
error: 'no-target',
|
|
389
|
+
endpoint: listEndpoint,
|
|
390
|
+
configureHint,
|
|
391
|
+
hint: 'No extension targets are registered yet. Start the runtime (mm-harness launch).',
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Locally-served bundled DevTools frontend (offline, same host:port as the ws
|
|
396
|
+
// target so its Origin is allowed). Built from the target id — Chrome omits
|
|
397
|
+
// webSocketDebuggerUrl for already-attached targets, but the id is always present.
|
|
398
|
+
const wsRef = `127.0.0.1:${numericPort}/devtools/page/${picked.id}`;
|
|
399
|
+
const devtoolsUrl = `http://127.0.0.1:${numericPort}/devtools/inspector.html?ws=${wsRef}`;
|
|
400
|
+
const targetInfo = { type: picked.type, url: picked.url, title: picked.title || null };
|
|
401
|
+
|
|
402
|
+
if (!shouldOpen) {
|
|
347
403
|
return {
|
|
348
404
|
ok: true,
|
|
349
405
|
adapter: 'extension',
|
|
350
406
|
targetKind,
|
|
351
|
-
method: '
|
|
352
|
-
endpoint:
|
|
407
|
+
method: 'devtools-window',
|
|
408
|
+
endpoint: devtoolsUrl,
|
|
353
409
|
cdpPort: numericPort,
|
|
354
|
-
target:
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
},
|
|
359
|
-
debuggerTargetId: devtoolsTarget.id,
|
|
360
|
-
attached: true,
|
|
361
|
-
opened: true,
|
|
410
|
+
target: targetInfo,
|
|
411
|
+
devtoolsUrl,
|
|
412
|
+
opened: false,
|
|
413
|
+
detail: 'Open action not invoked (--no-open).',
|
|
362
414
|
};
|
|
363
415
|
}
|
|
364
416
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
:
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
417
|
+
// Honest gate: confirm the debug server will accept a browser DevTools Origin
|
|
418
|
+
// before opening a window. A slot launched before --remote-allow-origins support
|
|
419
|
+
// 403s here; tell the user to relaunch rather than open a dead console.
|
|
420
|
+
const probe = await probeAttach(numericPort, picked.id, `http://127.0.0.1:${numericPort}`);
|
|
421
|
+
if (!probe.ok) {
|
|
422
|
+
return {
|
|
423
|
+
ok: false,
|
|
424
|
+
adapter: 'extension',
|
|
425
|
+
targetKind,
|
|
426
|
+
cdpPort: numericPort,
|
|
427
|
+
error: 'origin-rejected',
|
|
428
|
+
endpoint: devtoolsUrl,
|
|
429
|
+
target: targetInfo,
|
|
430
|
+
hint: 'This slot was launched before debugger support (Chrome rejects the DevTools WebSocket origin). Relaunch it: mm-harness launch',
|
|
431
|
+
detail: probe.status ? `HTTP ${probe.status}` : probe.error || 'handshake rejected',
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const browserWsUrl = version?.webSocketDebuggerUrl;
|
|
436
|
+
if (!browserWsUrl) {
|
|
437
|
+
return {
|
|
438
|
+
ok: false,
|
|
439
|
+
adapter: 'extension',
|
|
440
|
+
targetKind,
|
|
441
|
+
cdpPort: numericPort,
|
|
442
|
+
error: 'no-browser-ws',
|
|
443
|
+
endpoint: devtoolsUrl,
|
|
444
|
+
target: targetInfo,
|
|
445
|
+
hint: 'CDP /json/version did not expose a browser websocket. Relaunch the runtime (mm-harness launch).',
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
let createdTargetId;
|
|
450
|
+
try {
|
|
451
|
+
createdTargetId = await openDevtoolsWindow(browserWsUrl, devtoolsUrl);
|
|
452
|
+
} catch (error) {
|
|
453
|
+
return {
|
|
454
|
+
ok: false,
|
|
455
|
+
adapter: 'extension',
|
|
456
|
+
targetKind,
|
|
457
|
+
cdpPort: numericPort,
|
|
458
|
+
error: 'devtools-window-failed',
|
|
459
|
+
endpoint: devtoolsUrl,
|
|
460
|
+
target: targetInfo,
|
|
461
|
+
hint: 'Could not open the DevTools window over CDP. Relaunch the runtime (mm-harness launch).',
|
|
462
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
463
|
+
};
|
|
464
|
+
}
|
|
382
465
|
|
|
383
466
|
return {
|
|
384
467
|
ok: true,
|
|
385
468
|
adapter: 'extension',
|
|
386
469
|
targetKind,
|
|
387
|
-
method:
|
|
388
|
-
endpoint:
|
|
470
|
+
method: 'devtools-window',
|
|
471
|
+
endpoint: devtoolsUrl,
|
|
389
472
|
cdpPort: numericPort,
|
|
390
|
-
target:
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
opened: false,
|
|
395
|
-
detail: 'Open action not invoked (--no-open).',
|
|
473
|
+
target: targetInfo,
|
|
474
|
+
devtoolsUrl,
|
|
475
|
+
createdTargetId,
|
|
476
|
+
opened: true,
|
|
396
477
|
};
|
|
397
478
|
}
|
|
398
479
|
|
|
@@ -172,10 +172,10 @@ function extensionRuntimeContextPath(repo) {
|
|
|
172
172
|
const repoRoot = path.resolve(repo);
|
|
173
173
|
const envCtx = process.env.RECIPE_RUNTIME_CONTEXT;
|
|
174
174
|
if (envCtx) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
175
|
+
// Farmslot may manage runtime context outside the checkout. Treat the
|
|
176
|
+
// explicit environment override as authoritative; containment is not a
|
|
177
|
+
// security boundary and silently dropping it selects the wrong port.
|
|
178
|
+
return path.resolve(envCtx);
|
|
179
179
|
}
|
|
180
180
|
return path.join(repoRoot, recipeRuntimeDir(), 'agentic-runtime.json');
|
|
181
181
|
}
|
|
@@ -198,12 +198,22 @@ export function resolveExtensionRuntimeContext(repo) {
|
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
//
|
|
202
|
-
|
|
201
|
+
// Context and pool describe checkout identity and therefore replace stale
|
|
202
|
+
// inherited env values. The suffix formula is only a fallback and is applied
|
|
203
|
+
// separately by callers so an explicitly supplied env port can still win.
|
|
204
|
+
export function formatExtensionIdentityPorts(repo) {
|
|
203
205
|
const realRepo = path.resolve(repo);
|
|
204
206
|
return mergeKvLayers(
|
|
205
207
|
{ source: resolveExtensionRuntimeContext(realRepo), overwrite: true },
|
|
206
208
|
{ source: resolveSlotPortsByRepo(realRepo), overwrite: false },
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Checkout-scoped extension ports: runtime context (authoritative) → pool → suffix formula.
|
|
213
|
+
export function formatExtensionSlotPorts(repo) {
|
|
214
|
+
const realRepo = path.resolve(repo);
|
|
215
|
+
return mergeKvLayers(
|
|
216
|
+
{ source: formatExtensionIdentityPorts(realRepo), overwrite: true },
|
|
207
217
|
{ source: resolveDefaultExtensionPorts(realRepo), overwrite: false },
|
|
208
218
|
);
|
|
209
219
|
}
|
|
@@ -276,9 +286,16 @@ export function resolveMobileRuntimePorts(repo) {
|
|
|
276
286
|
);
|
|
277
287
|
}
|
|
278
288
|
|
|
289
|
+
// Back-compat: the previous name for resolveSlotPortsByRepo, kept as an alias for
|
|
290
|
+
// one release so existing importers keep resolving.
|
|
291
|
+
export const resolveFarmslotPortsByRepo = resolveSlotPortsByRepo;
|
|
292
|
+
|
|
279
293
|
export const cliFns = {
|
|
280
294
|
resolve_slot_ports_by_repo: resolveSlotPortsByRepo,
|
|
295
|
+
// Back-compat cli key for one release.
|
|
296
|
+
resolve_farmslot_ports_by_repo: resolveSlotPortsByRepo,
|
|
281
297
|
resolve_default_extension_ports: resolveDefaultExtensionPorts,
|
|
298
|
+
resolve_extension_identity_ports: (repo) => formatExtensionIdentityPorts(repo) || null,
|
|
282
299
|
resolve_extension_runtime_context: resolveExtensionRuntimeContext,
|
|
283
300
|
resolve_extension_slot_ports: (repo) => formatExtensionSlotPorts(repo) || null,
|
|
284
301
|
resolve_extension_runtime_ports: (repo) => resolveExtensionRuntimePorts(repo) || null,
|
|
@@ -20,6 +20,11 @@ resolve_slot_ports_by_repo() {
|
|
|
20
20
|
_resolve_ports_cli resolve_slot_ports_by_repo "$1"
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
# Back-compat alias for the previous function name (one release).
|
|
24
|
+
resolve_farmslot_ports_by_repo() {
|
|
25
|
+
resolve_slot_ports_by_repo "$1"
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
infer_extension_slot_suffix() {
|
|
24
29
|
local repo="$1" base
|
|
25
30
|
base="$(basename "$repo")"
|
|
@@ -47,12 +52,13 @@ apply_extension_slot_ports() {
|
|
|
47
52
|
local repo="${1:-.}" line key val resolved="" ctx_file="" runtime_rel="${RECIPE_RUNTIME_DIR:-temp/recipe/runtime}"
|
|
48
53
|
repo="$(cd "$repo" && pwd)"
|
|
49
54
|
ctx_file="$repo/$runtime_rel/agentic-runtime.json"
|
|
50
|
-
if [ -f "$ctx_file" ]; then
|
|
51
|
-
|
|
52
|
-
else
|
|
53
|
-
unset RECIPE_RUNTIME_CONTEXT
|
|
55
|
+
if [ -z "${RECIPE_RUNTIME_CONTEXT:-}" ] && [ -f "$ctx_file" ]; then
|
|
56
|
+
RECIPE_RUNTIME_CONTEXT="$ctx_file"
|
|
54
57
|
fi
|
|
55
|
-
|
|
58
|
+
export RECIPE_RUNTIME_CONTEXT
|
|
59
|
+
# Context/pool are authoritative checkout identity. Formula ports are only
|
|
60
|
+
# fallback values and must not replace explicitly supplied env ports.
|
|
61
|
+
resolved="$(_resolve_ports_cli resolve_extension_identity_ports "$repo" 2>/dev/null)" || resolved=""
|
|
56
62
|
while IFS= read -r line; do
|
|
57
63
|
[ -n "$line" ] || continue
|
|
58
64
|
key="${line%%=*}"
|
|
@@ -63,6 +69,17 @@ apply_extension_slot_ports() {
|
|
|
63
69
|
SLOT_ID) RECIPE_SLOT_ID="$val" ;;
|
|
64
70
|
esac
|
|
65
71
|
done <<< "$resolved"
|
|
72
|
+
resolved="$(_resolve_ports_cli resolve_default_extension_ports "$repo" 2>/dev/null)" || return 0
|
|
73
|
+
while IFS= read -r line; do
|
|
74
|
+
[ -n "$line" ] || continue
|
|
75
|
+
key="${line%%=*}"
|
|
76
|
+
val="${line#*=}"
|
|
77
|
+
case "$key" in
|
|
78
|
+
CDP_PORT) [ -n "${CDP_PORT:-}" ] || CDP_PORT="$val" ;;
|
|
79
|
+
WATCHER_PORT) [ -n "${WATCHER_PORT:-}" ] || WATCHER_PORT="$val" ;;
|
|
80
|
+
SLOT_ID) [ -n "${RECIPE_SLOT_ID:-}" ] || RECIPE_SLOT_ID="$val" ;;
|
|
81
|
+
esac
|
|
82
|
+
done <<< "$resolved"
|
|
66
83
|
}
|
|
67
84
|
|
|
68
85
|
apply_resolved_extension_ports() {
|
package/bin/mm-harness
CHANGED
|
@@ -5,6 +5,17 @@
|
|
|
5
5
|
# end-state surface; it resolves node/tsx/dist the same way whether the package is
|
|
6
6
|
# installed as a dependency or run from a source checkout.
|
|
7
7
|
set -euo pipefail
|
|
8
|
+
INVOKED_AS="$0"
|
|
9
|
+
if [[ "$INVOKED_AS" == */* ]]; then
|
|
10
|
+
INVOKED_AS="$(cd "$(dirname "$INVOKED_AS")" && pwd -L)/${INVOKED_AS##*/}"
|
|
11
|
+
else
|
|
12
|
+
RESOLVED_INVOKED_AS="$(command -v "$INVOKED_AS" 2>/dev/null || true)"
|
|
13
|
+
if [[ -z "$RESOLVED_INVOKED_AS" ]]; then
|
|
14
|
+
echo "ERROR: Cannot resolve the invoked mm-harness executable: $INVOKED_AS" >&2
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
INVOKED_AS="$RESOLVED_INVOKED_AS"
|
|
18
|
+
fi
|
|
8
19
|
SOURCE="${BASH_SOURCE[0]}"
|
|
9
20
|
while [ -L "$SOURCE" ]; do
|
|
10
21
|
SOURCE_DIR="$(cd "$(dirname "$SOURCE")" && pwd -P)"
|
|
@@ -26,7 +37,7 @@ RUNNER_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
26
37
|
# already points at this script — including via a symlink (both sides are resolved
|
|
27
38
|
# to their real paths before comparing).
|
|
28
39
|
if [ -n "${MM_HARNESS_BIN:-}" ]; then
|
|
29
|
-
self_real="$SCRIPT_DIR/$
|
|
40
|
+
self_real="$SCRIPT_DIR/${SOURCE##*/}"
|
|
30
41
|
# Resolve MM_HARNESS_BIN through any symlink chain so a symlink-to-self is caught.
|
|
31
42
|
mm_bin_src="$MM_HARNESS_BIN"
|
|
32
43
|
while [ -L "$mm_bin_src" ]; do
|
|
@@ -39,7 +50,7 @@ if [ -n "${MM_HARNESS_BIN:-}" ]; then
|
|
|
39
50
|
fi
|
|
40
51
|
done
|
|
41
52
|
bin_real_dir="$(cd "$(dirname "$mm_bin_src")" 2>/dev/null && pwd -P || true)"
|
|
42
|
-
bin_real="${bin_real_dir:+$bin_real_dir/$
|
|
53
|
+
bin_real="${bin_real_dir:+$bin_real_dir/${mm_bin_src##*/}}"
|
|
43
54
|
if [ "$bin_real" != "$self_real" ]; then
|
|
44
55
|
if [ ! -x "$MM_HARNESS_BIN" ]; then
|
|
45
56
|
echo "mm-harness: MM_HARNESS_BIN is not an executable: $MM_HARNESS_BIN" >&2
|
|
@@ -55,7 +66,8 @@ fi
|
|
|
55
66
|
# mm-harness is not on PATH (task-local installs). Set AFTER the MM_HARNESS_BIN
|
|
56
67
|
# delegation above so the final executable entered always overwrites any
|
|
57
68
|
# inherited/stale value with its own resolved self path.
|
|
58
|
-
export MM_HARNESS_EXECUTABLE="$SCRIPT_DIR/$
|
|
69
|
+
export MM_HARNESS_EXECUTABLE="$SCRIPT_DIR/${SOURCE##*/}"
|
|
70
|
+
export MM_HARNESS_INVOKED_AS="$INVOKED_AS"
|
|
59
71
|
|
|
60
72
|
ENTRY_TS="$RUNNER_DIR/src/mm-harness-cli.ts"
|
|
61
73
|
ENTRY_DIST="$RUNNER_DIR/dist/mm-harness-cli.js"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { depsCheck } from "@farmslot/recipe-harness/runtime/deps-readiness";
|
|
3
|
+
import { shellQuote } from "../../commands/parse-args.js";
|
|
3
4
|
const coreSurface = {
|
|
4
5
|
adapter: "core",
|
|
5
6
|
headless: true,
|
|
@@ -14,6 +15,7 @@ const coreSurface = {
|
|
|
14
15
|
reasons: [
|
|
15
16
|
ready ? "Core is headless; dependencies are installed. Run recipes with mm-harness run." : "Core is headless; dependencies are not fully installed."
|
|
16
17
|
],
|
|
18
|
+
nextAction: ready ? void 0 : `cd ${shellQuote(path.resolve(target))} && yarn install --immutable`,
|
|
17
19
|
deps: deps.status
|
|
18
20
|
};
|
|
19
21
|
},
|
|
@@ -49,7 +51,8 @@ const coreSurface = {
|
|
|
49
51
|
},
|
|
50
52
|
hints: {
|
|
51
53
|
launch: "mm-harness run <recipe> # run recipes against the headless core",
|
|
52
|
-
relaunch: "mm-harness verify"
|
|
54
|
+
relaunch: "mm-harness verify",
|
|
55
|
+
runtimeProbeRecovery: (target) => `mm-harness verify --adapter core --target ${shellQuote(path.resolve(target))}`
|
|
53
56
|
}
|
|
54
57
|
};
|
|
55
58
|
export {
|
|
@@ -5,7 +5,9 @@ import { recipeRuntimePath, runnerDir } from "../../paths.js";
|
|
|
5
5
|
async function ensureExtensionConsoleCapture(target) {
|
|
6
6
|
const cdpPort = process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT;
|
|
7
7
|
if (!cdpPort) throw new Error("Extension console capture requires a resolved CDP port.");
|
|
8
|
-
const response = await fetch(`http://127.0.0.1:${cdpPort}/json/version
|
|
8
|
+
const response = await fetch(`http://127.0.0.1:${cdpPort}/json/version`, {
|
|
9
|
+
signal: AbortSignal.timeout(3e3)
|
|
10
|
+
});
|
|
9
11
|
if (!response.ok) throw new Error(`Extension CDP ${cdpPort} is not ready for console capture.`);
|
|
10
12
|
const extensionLog = recipeRuntimePath(target, "extension-console.log");
|
|
11
13
|
const dappLog = recipeRuntimePath(target, "dapp-console.log");
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { recipeHarnessPath, runnerDir } from "../../paths.js";
|
|
6
|
+
async function ensureExtensionHarnessFresh(target) {
|
|
7
|
+
if (!fs.existsSync(path.join(target, "package.json"))) return;
|
|
8
|
+
let current;
|
|
9
|
+
try {
|
|
10
|
+
const mod = await import(pathToFileURL(path.join(runnerDir, "adapters/shared/harness-source-fingerprint.mjs")).href);
|
|
11
|
+
current = mod.harnessSourceFingerprint(runnerDir);
|
|
12
|
+
} catch {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const manifestPath = recipeHarnessPath(target, "extension", "manifest.json");
|
|
16
|
+
const installed = readInstalledFingerprint(manifestPath);
|
|
17
|
+
if (installed === current) return;
|
|
18
|
+
const reason = installed === null ? "harness not installed" : "runner changed";
|
|
19
|
+
try {
|
|
20
|
+
execFileSync(process.execPath, [path.join(runnerDir, "adapters/extension/inject.mjs"), "--target", target], {
|
|
21
|
+
stdio: ["ignore", "ignore", "inherit"]
|
|
22
|
+
});
|
|
23
|
+
console.error(`\u2192 refreshed extension harness overlay (${reason})`);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
const detail = error instanceof Error ? `: ${error.message}` : "";
|
|
26
|
+
throw new Error(`could not refresh the extension harness overlay (${reason})${detail}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function readInstalledFingerprint(manifestPath) {
|
|
30
|
+
try {
|
|
31
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
32
|
+
return typeof manifest.sourceFingerprint === "string" ? manifest.sourceFingerprint : null;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
ensureExtensionHarnessFresh
|
|
39
|
+
};
|