@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
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts';
|
|
4
|
+
|
|
5
|
+
import { walletFixturePath } from '../../harness-exports.mjs';
|
|
6
|
+
|
|
7
|
+
function fixtureAccountName(base, index, count) {
|
|
8
|
+
if (count === 1) return base || 'account1';
|
|
9
|
+
const match = /^(.*?)(\d+)$/u.exec(base || 'account1');
|
|
10
|
+
return match
|
|
11
|
+
? `${match[1]}${Number(match[2]) + index}`
|
|
12
|
+
: `${base || 'account'}${index + 1}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function mnemonicCount(entry, fixturePath, index) {
|
|
16
|
+
const count = Number(entry.count ?? entry.numberOfAccounts ?? 1);
|
|
17
|
+
if (!Number.isInteger(count) || count < 1 || count > 100) {
|
|
18
|
+
throw new Error(`${fixturePath} accounts[${index}] mnemonic count must be an integer from 1 through 100.`);
|
|
19
|
+
}
|
|
20
|
+
return count;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function deriveFixtureAccounts(fixture, fixturePath) {
|
|
24
|
+
if (!Array.isArray(fixture.accounts) || fixture.accounts.length === 0) {
|
|
25
|
+
throw new Error(`wallet fixture at ${fixturePath} has no accounts array.`);
|
|
26
|
+
}
|
|
27
|
+
return fixture.accounts.flatMap((entry, fixtureIndex) => {
|
|
28
|
+
if (!entry || typeof entry !== 'object' || typeof entry.value !== 'string' || !entry.value.trim()) {
|
|
29
|
+
throw new Error(`${fixturePath} accounts[${fixtureIndex}] must contain a non-empty value.`);
|
|
30
|
+
}
|
|
31
|
+
if (entry.type === 'mnemonic') {
|
|
32
|
+
const count = mnemonicCount(entry, fixturePath, fixtureIndex);
|
|
33
|
+
return Array.from({ length: count }, (_, addressIndex) => {
|
|
34
|
+
const account = mnemonicToAccount(entry.value.trim(), { addressIndex });
|
|
35
|
+
return {
|
|
36
|
+
id: `fixture:${fixtureAccountName(entry.name, addressIndex, count)}`,
|
|
37
|
+
address: account.address,
|
|
38
|
+
name: fixtureAccountName(entry.name, addressIndex, count),
|
|
39
|
+
type: 'eip155:eoa',
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (entry.type === 'privateKey') {
|
|
44
|
+
const raw = entry.value.trim();
|
|
45
|
+
const normalized = raw.startsWith('0x') ? raw : `0x${raw}`;
|
|
46
|
+
if (!/^0x[0-9a-fA-F]{64}$/u.test(normalized)) {
|
|
47
|
+
throw new Error(`${fixturePath} accounts[${fixtureIndex}] privateKey is not a 32-byte hex key.`);
|
|
48
|
+
}
|
|
49
|
+
const account = privateKeyToAccount(normalized);
|
|
50
|
+
const name = entry.name || `account${fixtureIndex + 1}`;
|
|
51
|
+
return [{ id: `fixture:${name}`, address: account.address, name, type: 'eip155:eoa' }];
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`${fixturePath} accounts[${fixtureIndex}].type must be mnemonic or privateKey.`);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function listAccounts(input) {
|
|
58
|
+
const scope = input.node?.scope == null ? 'named' : String(input.node.scope);
|
|
59
|
+
if (scope !== 'selected' && scope !== 'named' && scope !== 'evm' && scope !== 'all') {
|
|
60
|
+
throw new Error('metamask.wallet.list_accounts scope must be selected, named, evm, or all.');
|
|
61
|
+
}
|
|
62
|
+
const fixturePath = walletFixturePath(input.context.projectRoot);
|
|
63
|
+
const fixture = JSON.parse(await readFile(fixturePath, 'utf8'));
|
|
64
|
+
const selectedName = String(input.node?.account_name ?? input.node?.account ?? 'dev1');
|
|
65
|
+
const accounts = deriveFixtureAccounts(fixture, fixturePath)
|
|
66
|
+
.map((account) => ({
|
|
67
|
+
...account,
|
|
68
|
+
selected: account.name === selectedName,
|
|
69
|
+
}))
|
|
70
|
+
.filter((account) => {
|
|
71
|
+
if (scope === 'selected') return account.selected;
|
|
72
|
+
if (scope === 'all') return true;
|
|
73
|
+
if (scope === 'evm') return /^0x[0-9a-f]{40}$/iu.test(account.address);
|
|
74
|
+
return account.selected || Boolean(account.name.trim());
|
|
75
|
+
});
|
|
76
|
+
return {
|
|
77
|
+
action: input.action,
|
|
78
|
+
source: 'core-headless-wallet-fixture',
|
|
79
|
+
scope,
|
|
80
|
+
accounts,
|
|
81
|
+
count: accounts.length,
|
|
82
|
+
redacted: true,
|
|
83
|
+
selectionSource: 'explicit-controller-account-input',
|
|
84
|
+
proofPath: 'core-wallet-fixture-accounts',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
89
|
+
const inputPath = process.argv[2] || process.env.METAMASK_RECIPE_ADAPTER_INPUT;
|
|
90
|
+
if (!inputPath) throw new Error('Adapter input path is required.');
|
|
91
|
+
const input = JSON.parse(await readFile(inputPath, 'utf8'));
|
|
92
|
+
const result = await listAccounts(input);
|
|
93
|
+
if (input.outputPath) await writeFile(input.outputPath, `${JSON.stringify(result, null, 2)}\n`);
|
|
94
|
+
else process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
95
|
+
}
|
|
@@ -397,6 +397,7 @@ async function launchExistingDistRuntime(input, port) {
|
|
|
397
397
|
try {
|
|
398
398
|
child = spawn(chrome, [
|
|
399
399
|
`--user-data-dir=${profileDir}`,
|
|
400
|
+
...(process.platform === 'darwin' ? ['--use-mock-keychain'] : []),
|
|
400
401
|
'--remote-debugging-address=127.0.0.1',
|
|
401
402
|
`--remote-debugging-port=${port}`,
|
|
402
403
|
'--no-first-run',
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
|
|
3
|
+
runAdapter((input) => withExtensionPage(input, async (page) => {
|
|
4
|
+
const scope = input.node?.scope == null ? 'named' : String(input.node.scope);
|
|
5
|
+
if (scope !== 'selected' && scope !== 'named' && scope !== 'evm' && scope !== 'all') {
|
|
6
|
+
throw new Error('metamask.wallet.list_accounts scope must be selected, named, evm, or all.');
|
|
7
|
+
}
|
|
8
|
+
const result = await page.evaluate(`(() => {
|
|
9
|
+
const store = globalThis.stateHooks?.store;
|
|
10
|
+
if (!store || typeof store.getState !== 'function') {
|
|
11
|
+
throw new Error('stateHooks.store.getState is unavailable.');
|
|
12
|
+
}
|
|
13
|
+
const internal = store.getState()?.metamask?.internalAccounts || {};
|
|
14
|
+
const selectedId = internal.selectedAccount || null;
|
|
15
|
+
const scope = ${JSON.stringify(scope)};
|
|
16
|
+
const accounts = Object.entries(internal.accounts || {})
|
|
17
|
+
.map(([id, account]) => ({
|
|
18
|
+
id,
|
|
19
|
+
address: account?.address || null,
|
|
20
|
+
name: account?.metadata?.name || null,
|
|
21
|
+
type: account?.type || null,
|
|
22
|
+
selected: id === selectedId
|
|
23
|
+
}))
|
|
24
|
+
.filter((account) => {
|
|
25
|
+
if (scope === 'selected') return account.selected;
|
|
26
|
+
if (scope === 'all') return true;
|
|
27
|
+
if (scope === 'evm') return /^0x[0-9a-f]{40}$/iu.test(account.address || '');
|
|
28
|
+
return account.selected || Boolean(String(account.name || '').trim());
|
|
29
|
+
});
|
|
30
|
+
return { accounts };
|
|
31
|
+
})()`);
|
|
32
|
+
return {
|
|
33
|
+
action: input.action,
|
|
34
|
+
source: 'extension-stateHooks-store',
|
|
35
|
+
scope,
|
|
36
|
+
...result,
|
|
37
|
+
count: result.accounts.length,
|
|
38
|
+
redacted: true,
|
|
39
|
+
proofPath: 'extension-wallet-accounts',
|
|
40
|
+
};
|
|
41
|
+
}));
|
|
@@ -529,14 +529,10 @@ async function androidScreenshot(input, relPath) {
|
|
|
529
529
|
async function resolveAndroidScreenshotSerial(input) {
|
|
530
530
|
const contextEnv = input.context?.env || {};
|
|
531
531
|
const explicit = input.node?.adb_serial ??
|
|
532
|
-
input.node?.android_device ??
|
|
533
|
-
input.node?.device ??
|
|
534
532
|
contextEnv.ADB_SERIAL ??
|
|
535
533
|
contextEnv.ANDROID_SERIAL ??
|
|
536
|
-
contextEnv.ANDROID_DEVICE ??
|
|
537
534
|
process.env.ADB_SERIAL ??
|
|
538
|
-
process.env.ANDROID_SERIAL
|
|
539
|
-
process.env.ANDROID_DEVICE;
|
|
535
|
+
process.env.ANDROID_SERIAL;
|
|
540
536
|
if (explicit !== undefined && explicit !== null && String(explicit).trim()) return String(explicit).trim();
|
|
541
537
|
|
|
542
538
|
let result;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { bridgeCommand, runAdapter } from '../platform/bridge.mjs';
|
|
2
|
+
|
|
3
|
+
runAdapter(async (input) => {
|
|
4
|
+
const scope = input.node?.scope == null ? 'named' : String(input.node.scope);
|
|
5
|
+
if (scope !== 'selected' && scope !== 'named' && scope !== 'evm' && scope !== 'all') {
|
|
6
|
+
throw new Error('metamask.wallet.list_accounts scope must be selected, named, evm, or all.');
|
|
7
|
+
}
|
|
8
|
+
const rawAccounts = await bridgeCommand(input, ['list-accounts']);
|
|
9
|
+
if (!Array.isArray(rawAccounts)) {
|
|
10
|
+
throw new Error(`metamask.wallet.list_accounts expected an array, got ${typeof rawAccounts}.`);
|
|
11
|
+
}
|
|
12
|
+
const selected = await bridgeCommand(input, ['get-selected-account']);
|
|
13
|
+
const selectedAddress = String(selected?.address ?? selected ?? '').toLowerCase();
|
|
14
|
+
const accounts = rawAccounts
|
|
15
|
+
.map((account) => ({
|
|
16
|
+
id: account?.id ?? null,
|
|
17
|
+
address: account?.address ?? null,
|
|
18
|
+
name: account?.name ?? null,
|
|
19
|
+
type: account?.type ?? null,
|
|
20
|
+
selected: Boolean(selectedAddress && String(account?.address ?? '').toLowerCase() === selectedAddress),
|
|
21
|
+
}))
|
|
22
|
+
.filter((account) => {
|
|
23
|
+
if (scope === 'selected') return account.selected;
|
|
24
|
+
if (scope === 'all') return true;
|
|
25
|
+
if (scope === 'evm') return /^0x[0-9a-f]{40}$/iu.test(String(account.address ?? ''));
|
|
26
|
+
return account.selected || Boolean(String(account.name ?? '').trim());
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
action: input.action,
|
|
30
|
+
source: 'mobile-accounts-controller',
|
|
31
|
+
scope,
|
|
32
|
+
accounts,
|
|
33
|
+
count: accounts.length,
|
|
34
|
+
redacted: true,
|
|
35
|
+
proofPath: 'agentic-wallet-accounts',
|
|
36
|
+
};
|
|
37
|
+
});
|
|
@@ -162,6 +162,18 @@
|
|
|
162
162
|
}
|
|
163
163
|
]
|
|
164
164
|
},
|
|
165
|
+
"metamask.wallet.list_accounts": {
|
|
166
|
+
"description": "List redacted headless fixture accounts available to Core controller actions.",
|
|
167
|
+
"examples": [
|
|
168
|
+
{
|
|
169
|
+
"description": "List configured Core accounts",
|
|
170
|
+
"node": {
|
|
171
|
+
"action": "metamask.wallet.list_accounts",
|
|
172
|
+
"intent": "Discover the accounts available to headless controller actions"
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
},
|
|
165
177
|
"metamask.perps.read_positions": {
|
|
166
178
|
"description": "Read live Perps positions directly from the perps controller (HyperLiquid testnet, HTTP read-only) and return the selected subset.",
|
|
167
179
|
"examples": [
|
|
@@ -392,6 +404,51 @@
|
|
|
392
404
|
}
|
|
393
405
|
},
|
|
394
406
|
"custom_actions": [
|
|
407
|
+
{
|
|
408
|
+
"name": "metamask.wallet.list_accounts",
|
|
409
|
+
"owner": "metamask",
|
|
410
|
+
"description": "List redacted headless fixture accounts available to Core controller actions.",
|
|
411
|
+
"schema": {
|
|
412
|
+
"type": "object",
|
|
413
|
+
"properties": {
|
|
414
|
+
"action": {
|
|
415
|
+
"const": "metamask.wallet.list_accounts"
|
|
416
|
+
},
|
|
417
|
+
"scope": {
|
|
418
|
+
"type": "string",
|
|
419
|
+
"enum": [
|
|
420
|
+
"selected",
|
|
421
|
+
"named",
|
|
422
|
+
"evm",
|
|
423
|
+
"all"
|
|
424
|
+
],
|
|
425
|
+
"description": "Account scope. Core fixtures are EVM-only today; selected filters to the explicit controller account input."
|
|
426
|
+
},
|
|
427
|
+
"account": {
|
|
428
|
+
"type": "string",
|
|
429
|
+
"description": "Optional account name to mark as selected for the next controller operation; defaults to dev1."
|
|
430
|
+
},
|
|
431
|
+
"account_name": {
|
|
432
|
+
"type": "string",
|
|
433
|
+
"description": "Alias for account."
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
"required": [
|
|
437
|
+
"action"
|
|
438
|
+
]
|
|
439
|
+
},
|
|
440
|
+
"examples": [
|
|
441
|
+
{
|
|
442
|
+
"description": "List configured Core accounts",
|
|
443
|
+
"node": {
|
|
444
|
+
"action": "metamask.wallet.list_accounts",
|
|
445
|
+
"intent": "Discover the accounts available to headless controller actions"
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
],
|
|
449
|
+
"proof_effect": "Records the redacted addresses derived from the same wallet fixture used by headless controller actions.",
|
|
450
|
+
"safety_notes": "Read-only fixture metadata; must never return private keys, mnemonics, passwords, or vault data."
|
|
451
|
+
},
|
|
395
452
|
{
|
|
396
453
|
"name": "metamask.perps.read_positions",
|
|
397
454
|
"owner": "metamask",
|
|
@@ -1486,6 +1543,10 @@
|
|
|
1486
1543
|
"action": "app.status",
|
|
1487
1544
|
"implementation": "@metamask/recipe-runner/platform-status"
|
|
1488
1545
|
},
|
|
1546
|
+
{
|
|
1547
|
+
"action": "metamask.wallet.list_accounts",
|
|
1548
|
+
"implementation": "@metamask/recipe-runner/core/wallet"
|
|
1549
|
+
},
|
|
1489
1550
|
{
|
|
1490
1551
|
"action": "metamask.perps.read_positions",
|
|
1491
1552
|
"implementation": "@metamask/recipe-runner/core/perps"
|
|
@@ -516,6 +516,18 @@
|
|
|
516
516
|
}
|
|
517
517
|
]
|
|
518
518
|
},
|
|
519
|
+
"metamask.wallet.list_accounts": {
|
|
520
|
+
"description": "List redacted wallet accounts and identify the selected account without mutating the app.",
|
|
521
|
+
"examples": [
|
|
522
|
+
{
|
|
523
|
+
"description": "List wallet accounts",
|
|
524
|
+
"node": {
|
|
525
|
+
"action": "metamask.wallet.list_accounts",
|
|
526
|
+
"intent": "Discover the available wallet accounts"
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
]
|
|
530
|
+
},
|
|
519
531
|
"metamask.wallet.read_state": {
|
|
520
532
|
"description": "Read the redacted wallet state (selected account, route, and device) from the running app.",
|
|
521
533
|
"examples": [
|
|
@@ -911,6 +923,43 @@
|
|
|
911
923
|
],
|
|
912
924
|
"owner": "metamask"
|
|
913
925
|
},
|
|
926
|
+
{
|
|
927
|
+
"name": "metamask.wallet.list_accounts",
|
|
928
|
+
"owner": "metamask",
|
|
929
|
+
"description": "List redacted wallet accounts and identify the selected account without mutating the app.",
|
|
930
|
+
"schema": {
|
|
931
|
+
"type": "object",
|
|
932
|
+
"properties": {
|
|
933
|
+
"action": {
|
|
934
|
+
"const": "metamask.wallet.list_accounts"
|
|
935
|
+
},
|
|
936
|
+
"scope": {
|
|
937
|
+
"type": "string",
|
|
938
|
+
"enum": [
|
|
939
|
+
"selected",
|
|
940
|
+
"named",
|
|
941
|
+
"evm",
|
|
942
|
+
"all"
|
|
943
|
+
],
|
|
944
|
+
"description": "Account scope. Defaults to named human-visible accounts; use selected for the active account, evm for EVM accounts, or all for the full inventory."
|
|
945
|
+
}
|
|
946
|
+
},
|
|
947
|
+
"required": [
|
|
948
|
+
"action"
|
|
949
|
+
]
|
|
950
|
+
},
|
|
951
|
+
"examples": [
|
|
952
|
+
{
|
|
953
|
+
"description": "List wallet accounts",
|
|
954
|
+
"node": {
|
|
955
|
+
"action": "metamask.wallet.list_accounts",
|
|
956
|
+
"intent": "Discover the available wallet accounts"
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
],
|
|
960
|
+
"proof_effect": "E2E validation must record this action in trace.json; live-proof actions include liveAdapter/proofPath or target runtime output.",
|
|
961
|
+
"safety_notes": "Read-only account metadata; must never expose keys, mnemonics, vault data, or credentials."
|
|
962
|
+
},
|
|
914
963
|
{
|
|
915
964
|
"name": "metamask.wallet.read_state",
|
|
916
965
|
"owner": "metamask",
|
|
@@ -2045,6 +2094,10 @@
|
|
|
2045
2094
|
"action": "metamask.wallet.select_account",
|
|
2046
2095
|
"implementation": "@metamask/recipe-runner/extension/wallet"
|
|
2047
2096
|
},
|
|
2097
|
+
{
|
|
2098
|
+
"action": "metamask.wallet.list_accounts",
|
|
2099
|
+
"implementation": "@metamask/recipe-runner/extension/wallet"
|
|
2100
|
+
},
|
|
2048
2101
|
{
|
|
2049
2102
|
"action": "metamask.wallet.read_state",
|
|
2050
2103
|
"implementation": "@metamask/recipe-runner/extension/wallet"
|
|
@@ -550,6 +550,18 @@
|
|
|
550
550
|
}
|
|
551
551
|
]
|
|
552
552
|
},
|
|
553
|
+
"metamask.wallet.list_accounts": {
|
|
554
|
+
"description": "List redacted wallet accounts and identify the selected account without mutating the app.",
|
|
555
|
+
"examples": [
|
|
556
|
+
{
|
|
557
|
+
"description": "List wallet accounts",
|
|
558
|
+
"node": {
|
|
559
|
+
"action": "metamask.wallet.list_accounts",
|
|
560
|
+
"intent": "Discover the available wallet accounts"
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
]
|
|
564
|
+
},
|
|
553
565
|
"metamask.wallet.read_state": {
|
|
554
566
|
"description": "Read the redacted wallet state (selected account, route, and device) from the running app.",
|
|
555
567
|
"examples": [
|
|
@@ -947,6 +959,43 @@
|
|
|
947
959
|
"proof_effect": "E2E validation must record this action in trace.json; live-proof actions include liveAdapter/proofPath or target runtime output.",
|
|
948
960
|
"safety_notes": "Must not inject mid-recipe UI/app state to fabricate proof."
|
|
949
961
|
},
|
|
962
|
+
{
|
|
963
|
+
"name": "metamask.wallet.list_accounts",
|
|
964
|
+
"owner": "metamask",
|
|
965
|
+
"description": "List redacted wallet accounts and identify the selected account without mutating the app.",
|
|
966
|
+
"schema": {
|
|
967
|
+
"type": "object",
|
|
968
|
+
"properties": {
|
|
969
|
+
"action": {
|
|
970
|
+
"const": "metamask.wallet.list_accounts"
|
|
971
|
+
},
|
|
972
|
+
"scope": {
|
|
973
|
+
"type": "string",
|
|
974
|
+
"enum": [
|
|
975
|
+
"selected",
|
|
976
|
+
"named",
|
|
977
|
+
"evm",
|
|
978
|
+
"all"
|
|
979
|
+
],
|
|
980
|
+
"description": "Account scope. Defaults to named human-visible accounts; use selected for the active account, evm for EVM accounts, or all for the full inventory."
|
|
981
|
+
}
|
|
982
|
+
},
|
|
983
|
+
"required": [
|
|
984
|
+
"action"
|
|
985
|
+
]
|
|
986
|
+
},
|
|
987
|
+
"examples": [
|
|
988
|
+
{
|
|
989
|
+
"description": "List wallet accounts",
|
|
990
|
+
"node": {
|
|
991
|
+
"action": "metamask.wallet.list_accounts",
|
|
992
|
+
"intent": "Discover the available wallet accounts"
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
],
|
|
996
|
+
"proof_effect": "E2E validation must record this action in trace.json; live-proof actions include liveAdapter/proofPath or target runtime output.",
|
|
997
|
+
"safety_notes": "Read-only account metadata; must never expose keys, mnemonics, vault data, or credentials."
|
|
998
|
+
},
|
|
950
999
|
{
|
|
951
1000
|
"name": "metamask.wallet.read_state",
|
|
952
1001
|
"owner": "metamask",
|
|
@@ -2092,6 +2141,10 @@
|
|
|
2092
2141
|
"action": "metamask.wallet.select_account",
|
|
2093
2142
|
"implementation": "@metamask/recipe-runner/mobile/wallet"
|
|
2094
2143
|
},
|
|
2144
|
+
{
|
|
2145
|
+
"action": "metamask.wallet.list_accounts",
|
|
2146
|
+
"implementation": "@metamask/recipe-runner/mobile/wallet"
|
|
2147
|
+
},
|
|
2095
2148
|
{
|
|
2096
2149
|
"action": "metamask.wallet.read_state",
|
|
2097
2150
|
"implementation": "@metamask/recipe-runner/mobile/wallet"
|
|
@@ -125,11 +125,18 @@
|
|
|
125
125
|
},
|
|
126
126
|
"wallet-read-state": {
|
|
127
127
|
"action": "metamask.wallet.read_state",
|
|
128
|
-
"next": "
|
|
128
|
+
"next": "wallet-list-accounts",
|
|
129
129
|
"intent": "Read wallet state for proof",
|
|
130
130
|
"detail": "Capture selected account and network state without mutating UI.",
|
|
131
131
|
"flow": "wallet"
|
|
132
132
|
},
|
|
133
|
+
"wallet-list-accounts": {
|
|
134
|
+
"action": "metamask.wallet.list_accounts",
|
|
135
|
+
"next": "extension-account-list",
|
|
136
|
+
"intent": "List wallet accounts for proof",
|
|
137
|
+
"detail": "Capture redacted account metadata and the selected account without mutating the app.",
|
|
138
|
+
"flow": "wallet"
|
|
139
|
+
},
|
|
133
140
|
"app-navigate-wallet": {
|
|
134
141
|
"action": "ui.navigate",
|
|
135
142
|
"next": "app-navigate-perps",
|
|
@@ -125,11 +125,18 @@
|
|
|
125
125
|
},
|
|
126
126
|
"wallet-read-state": {
|
|
127
127
|
"action": "metamask.wallet.read_state",
|
|
128
|
-
"next": "
|
|
128
|
+
"next": "wallet-list-accounts",
|
|
129
129
|
"intent": "Read wallet state for proof",
|
|
130
130
|
"detail": "Capture selected account and network state without mutating UI.",
|
|
131
131
|
"flow": "wallet"
|
|
132
132
|
},
|
|
133
|
+
"wallet-list-accounts": {
|
|
134
|
+
"action": "metamask.wallet.list_accounts",
|
|
135
|
+
"next": "select-account",
|
|
136
|
+
"intent": "List wallet accounts for proof",
|
|
137
|
+
"detail": "Capture redacted account metadata and the selected account without mutating the app.",
|
|
138
|
+
"flow": "wallet"
|
|
139
|
+
},
|
|
133
140
|
"select-account": {
|
|
134
141
|
"action": "metamask.wallet.select_account",
|
|
135
142
|
"address": "0x8dc623e964475d4d669da601fd15ea9125469003",
|
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deeeed/metamask-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mm-harness": "bin/mm-harness"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
9
|
"build": "node scripts/build-dist.mjs",
|
|
10
|
-
"audit:cli-ergonomics": "node scripts/generate-cli-ergonomics-audit.mjs",
|
|
11
10
|
"prepack": "npm run build",
|
|
12
11
|
"dev:link-farmslot": "node scripts/link-local-farmslot.mjs",
|
|
13
12
|
"check": "node scripts/check.mjs",
|
|
13
|
+
"test:unit": "vitest run --config scripts/vitest.config.mjs",
|
|
14
|
+
"test:coverage": "vitest run --coverage --config scripts/vitest.config.mjs",
|
|
14
15
|
"qa:human": "node scripts/validate-human-outcomes.mjs",
|
|
15
16
|
"self-test": "bin/mm-harness self-test",
|
|
16
17
|
"manifest:mobile": "bin/mm-harness actions --raw --adapter mobile --json",
|
|
@@ -19,9 +20,11 @@
|
|
|
19
20
|
},
|
|
20
21
|
"dependencies": {
|
|
21
22
|
"@farmslot/agent-runtime": "^0.2.0",
|
|
22
|
-
"@farmslot/protocol": "^0.
|
|
23
|
-
"@farmslot/recipe-harness": "^0.
|
|
23
|
+
"@farmslot/protocol": "^0.10.0",
|
|
24
|
+
"@farmslot/recipe-harness": "^0.8.0",
|
|
24
25
|
"commander": "^12.0.0",
|
|
26
|
+
"es-module-lexer": "2.3.1",
|
|
27
|
+
"esbuild": "0.28.1",
|
|
25
28
|
"viem": "^2.54.3",
|
|
26
29
|
"ws": "8.21.0"
|
|
27
30
|
},
|
|
@@ -33,12 +36,13 @@
|
|
|
33
36
|
"devDependencies": {
|
|
34
37
|
"@eslint/js": "^9",
|
|
35
38
|
"@types/node": "^22.0.0",
|
|
36
|
-
"
|
|
39
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
37
40
|
"eslint": "^9",
|
|
38
41
|
"globals": "^17.7.0",
|
|
39
42
|
"tsx": "^4.23.0",
|
|
40
43
|
"typescript": "^5.6.0",
|
|
41
|
-
"typescript-eslint": "^8"
|
|
44
|
+
"typescript-eslint": "^8",
|
|
45
|
+
"vitest": "^4.1.10"
|
|
42
46
|
},
|
|
43
47
|
"main": "./dist/index.js",
|
|
44
48
|
"exports": {
|
package/scripts/completions.sh
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
#
|
|
4
4
|
# Static parts (commands, positional targets, per-command flags) mirror the
|
|
5
5
|
# mm-harness surface. Dynamic parts are sourced live from the hidden
|
|
6
|
-
# `mm-harness completion-candidates <actions|
|
|
6
|
+
# `mm-harness completion-candidates <actions|recipes>` command (per-checkout cached):
|
|
7
7
|
# call <TAB> → action names from the adapter manifest
|
|
8
|
-
# run <TAB> → recipe files +
|
|
8
|
+
# run <TAB> → recipe files + named recipes from the recipe library
|
|
9
9
|
# so completion reflects the installed overlay's real vocabulary. A candidate read
|
|
10
10
|
# that fails is silently empty — completion never blocks the shell.
|
|
11
11
|
#
|
|
@@ -41,9 +41,9 @@ _mmh_flags_for() {
|
|
|
41
41
|
fixtures) printf '%s' "--from --dev --force --fixture --adapter --target --json" ;;
|
|
42
42
|
checklist) printf '%s' "" ;;
|
|
43
43
|
actions) printf '%s' "--adapter --action --kind --raw --action-manifest --target --json" ;;
|
|
44
|
-
call) printf '%s' "--arg --adapter --target --artifacts-dir --action-manifest --heal --json" ;;
|
|
44
|
+
call) printf '%s' "--list --arg --adapter --target --artifacts-dir --action-manifest --heal --json" ;;
|
|
45
45
|
flows) printf '%s' "--library --json" ;;
|
|
46
|
-
run) printf '%s' "--plan --adapter --artifacts-dir --target --library --cdp-port --heal --record-video --json" ;;
|
|
46
|
+
run) printf '%s' "--list --describe --plan --adapter --artifacts-dir --target --library --cdp-port --heal --record-video --json" ;;
|
|
47
47
|
doctor) printf '%s' "--adapter --target --fix --json" ;;
|
|
48
48
|
install|verify|cleanup) printf '%s' "--adapter --target --json" ;;
|
|
49
49
|
*) printf '%s' "" ;;
|
|
@@ -52,7 +52,7 @@ _mmh_flags_for() {
|
|
|
52
52
|
|
|
53
53
|
# Dynamic candidates from the DISCOVER layer (silent empty on any failure).
|
|
54
54
|
_mmh_actions() { local b; b="$(_mmh_bin)" || return 0; "$b" completion-candidates actions 2>/dev/null; }
|
|
55
|
-
|
|
55
|
+
_mmh_recipes() { local b; b="$(_mmh_bin)" || return 0; "$b" completion-candidates recipes 2>/dev/null; }
|
|
56
56
|
|
|
57
57
|
# ── Bash ────────────────────────────────────────────────────────────────
|
|
58
58
|
if [ -n "${BASH_VERSION:-}" ]; then
|
|
@@ -82,7 +82,7 @@ if [ -n "${BASH_VERSION:-}" ]; then
|
|
|
82
82
|
completions) [ "$cword" -eq 2 ] && COMPREPLY=($(compgen -W "install bash zsh status" -- "$cur")) ;;
|
|
83
83
|
flows) [ "$cword" -eq 2 ] && COMPREPLY=($(compgen -W "list promote" -- "$cur")) ;;
|
|
84
84
|
call) [ "$cword" -eq 2 ] && COMPREPLY=($(compgen -W "$(_mmh_actions)" -- "$cur")) ;;
|
|
85
|
-
run) COMPREPLY=($(compgen -f -- "$cur"; compgen -W "$(
|
|
85
|
+
run) COMPREPLY=($(compgen -f -- "$cur"; compgen -W "$(_mmh_recipes)" -- "$cur")) ;;
|
|
86
86
|
*) COMPREPLY=($(compgen -W "$(_mmh_flags_for "$cmd")" -- "$cur")) ;;
|
|
87
87
|
esac
|
|
88
88
|
}
|
|
@@ -117,7 +117,7 @@ if [ -n "${ZSH_VERSION:-}" ]; then
|
|
|
117
117
|
completions) (( CURRENT == 3 )) && { candidates=(install bash zsh status); compadd -a candidates; } ;;
|
|
118
118
|
flows) (( CURRENT == 3 )) && { candidates=(list promote); compadd -a candidates; } ;;
|
|
119
119
|
call) (( CURRENT == 3 )) && { candidates=("${(@f)$(_mmh_actions)}"); compadd -a candidates; } ;;
|
|
120
|
-
run) candidates=("${(@f)$(
|
|
120
|
+
run) candidates=("${(@f)$(_mmh_recipes)}"); compadd -a candidates; _files ;;
|
|
121
121
|
*) candidates=(${=$(_mmh_flags_for "$cmd")}); compadd -a candidates ;;
|
|
122
122
|
esac
|
|
123
123
|
}
|
package/docs/ADAPTER-SURFACE.md
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# Adapter surface
|
|
2
|
-
|
|
3
|
-
`src/adapters/surface.ts` is the typed seam for Mobile, Extension, and Core.
|
|
4
|
-
Commands ask it for platform behavior instead of re-deriving platform rules.
|
|
5
|
-
|
|
6
|
-
Each adapter implements:
|
|
7
|
-
|
|
8
|
-
- runtime readiness;
|
|
9
|
-
- slot-owned ports;
|
|
10
|
-
- dev-server lifecycle;
|
|
11
|
-
- log sources;
|
|
12
|
-
- launch/recovery hints;
|
|
13
|
-
- headless capability.
|
|
14
|
-
|
|
15
|
-
Implementations live in `src/adapters/{mobile,extension,core}/surface.ts`.
|
|
16
|
-
Platform-specific shell and Node operations live under `adapters/<platform>/`.
|
|
17
|
-
|
|
18
|
-
When adding behavior:
|
|
19
|
-
|
|
20
|
-
1. Extend `AdapterSurface` only when every platform needs the concept.
|
|
21
|
-
2. Implement all three adapters; Core should explicitly report headless/N/A.
|
|
22
|
-
3. Call the surface from the command instead of adding adapter conditionals.
|
|
23
|
-
4. Add a registry-completeness contract and a real affected-platform check.
|
|
24
|
-
|
|
25
|
-
Mechanisms unique to one command may remain platform-specific. Shared readiness,
|
|
26
|
-
ports, logs, lifecycle, and recovery language belong on the surface.
|
package/docs/CHEATSHEET.md
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
# mm-harness cheatsheet
|
|
2
|
-
|
|
3
|
-
Run inside a MetaMask checkout. Product, slot, ports, and runtime paths are
|
|
4
|
-
auto-detected.
|
|
5
|
-
|
|
6
|
-
## Operate
|
|
7
|
-
|
|
8
|
-
| Need | Command |
|
|
9
|
-
|---|---|
|
|
10
|
-
| Extension | `mm-harness launch` |
|
|
11
|
-
| Extension sidepanel | `mm-harness launch --sidepanel` |
|
|
12
|
-
| iOS / Android | `mm-harness launch ios` / `mm-harness launch android` |
|
|
13
|
-
| Clean rebuild | `mm-harness launch --build` |
|
|
14
|
-
| Runtime status | `mm-harness status` |
|
|
15
|
-
| Logs | `mm-harness logs` |
|
|
16
|
-
| Debugger | `mm-harness debug` |
|
|
17
|
-
| Apply wallet fixture | `mm-harness fixtures set` |
|
|
18
|
-
|
|
19
|
-
Extension log sources: `extension`, `dapp`, `webpack`, `watcher`, `rebuild`.
|
|
20
|
-
Mobile log sources: `app`, `metro`.
|
|
21
|
-
|
|
22
|
-
## Discover and prove
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
mm-harness actions positions
|
|
26
|
-
mm-harness actions --action metamask.wallet.ensure_unlocked
|
|
27
|
-
mm-harness call metamask.wallet.ensure_unlocked
|
|
28
|
-
mm-harness flows
|
|
29
|
-
mm-harness run recipe.json --plan
|
|
30
|
-
mm-harness run recipe.json --artifacts-dir temp/recipe-proof
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
The run result prints diagnostics plus absolute evidence paths. Resume safely with
|
|
34
|
-
`mm-harness last --json`.
|
|
35
|
-
|
|
36
|
-
## Recover
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
mm-harness doctor
|
|
40
|
-
mm-harness doctor --fix
|
|
41
|
-
mm-harness verify
|
|
42
|
-
mm-harness cleanup
|
|
43
|
-
mm-harness update
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
For automation, add `--json`. Use `--json-stream` for line-delimited run progress.
|
|
47
|
-
Bad input exits `2`; recipe/action failure exits `1`; recipe validation failure
|
|
48
|
-
exits `5`.
|