@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.
Files changed (93) hide show
  1. package/CHANGELOG.md +775 -0
  2. package/README.md +65 -71
  3. package/adapters/extension/ensure-browser.sh +12 -1
  4. package/adapters/extension/inject.mjs +7 -0
  5. package/adapters/extension/launch-browser.cjs +10 -3
  6. package/adapters/extension/lib/chrome-args.cjs +31 -0
  7. package/adapters/extension/lib/macos-focus.cjs +32 -0
  8. package/adapters/extension/live.sh +10 -20
  9. package/adapters/manifest.json +8 -0
  10. package/adapters/mobile/open-device.sh +45 -7
  11. package/adapters/mobile/verify.sh +15 -3
  12. package/adapters/shared/harness-source-fingerprint.mjs +49 -0
  13. package/adapters/shared/install-repo-deps.sh +1 -5
  14. package/adapters/shared/open-debug.mjs +184 -103
  15. package/adapters/shared/resolve-slot-ports-core.mjs +23 -6
  16. package/adapters/shared/resolve-slot-ports.sh +22 -5
  17. package/bin/mm-harness +15 -3
  18. package/dist/adapters/core/surface.js +4 -1
  19. package/dist/adapters/extension/console-capture.js +3 -1
  20. package/dist/adapters/extension/harness-freshness.js +39 -0
  21. package/dist/adapters/extension/product-config.js +110 -0
  22. package/dist/adapters/extension/runtime-decision.js +20 -71
  23. package/dist/adapters/extension/surface.js +19 -1
  24. package/dist/adapters/mobile/prepare.js +17 -0
  25. package/dist/adapters/mobile/source-freshness.js +26 -41
  26. package/dist/adapters/mobile/surface.js +4 -1
  27. package/dist/adapters/resolve-slot-ports.js +2 -0
  28. package/dist/adapters/slot-ports.js +13 -32
  29. package/dist/adapters.js +50 -17
  30. package/dist/checkout-lock.js +27 -2
  31. package/dist/cli-color.js +19 -0
  32. package/dist/cli-commands.js +1 -1
  33. package/dist/cli.js +2 -3
  34. package/dist/command-contract.js +13 -3
  35. package/dist/commands/call.js +115 -29
  36. package/dist/commands/checklist.js +4 -1
  37. package/dist/commands/completion-candidates.js +20 -13
  38. package/dist/commands/debug.js +31 -38
  39. package/dist/commands/doctor.js +33 -6
  40. package/dist/commands/fixtures.js +65 -17
  41. package/dist/commands/flows.js +39 -10
  42. package/dist/commands/launch/extension.js +40 -15
  43. package/dist/commands/launch/index.js +41 -5
  44. package/dist/commands/list-executables.js +151 -29
  45. package/dist/commands/manifest.js +127 -18
  46. package/dist/commands/parse-args.js +11 -1
  47. package/dist/commands/run-engine.js +384 -56
  48. package/dist/commands/run.js +112 -17
  49. package/dist/commands/shared.js +22 -2
  50. package/dist/commands/status-probe.js +3 -0
  51. package/dist/commands/status.js +1 -0
  52. package/dist/completions-cache.js +1 -1
  53. package/dist/doctor.js +56 -6
  54. package/dist/harness.js +6 -5
  55. package/dist/heal-bounds.js +1 -1
  56. package/dist/live-adapter-contract.js +132 -12
  57. package/dist/manifest.js +161 -1
  58. package/dist/mm-harness-cli.js +13 -7
  59. package/dist/recipe-security.js +178 -0
  60. package/dist/runner.js +87 -13
  61. package/dist/runtime-context.js +8 -26
  62. package/docs/CONTRIBUTING.md +137 -0
  63. package/docs/QA.md +185 -0
  64. package/docs/RECIPES.md +161 -0
  65. package/docs/SECURITY.md +88 -0
  66. package/library/actions/core/perps/read_account.mjs +2 -2
  67. package/library/actions/core/perps/read_orders.mjs +2 -1
  68. package/library/actions/core/perps/read_positions.mjs +2 -1
  69. package/library/actions/core/wallet/list_accounts.mjs +95 -0
  70. package/library/actions/extension/platform/cdp.mjs +1 -0
  71. package/library/actions/extension/wallet/list_accounts.mjs +41 -0
  72. package/library/actions/mobile/platform/bridge.mjs +1 -5
  73. package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
  74. package/library/manifests/core.action-manifest.json +61 -0
  75. package/library/manifests/extension.action-manifest.json +53 -0
  76. package/library/manifests/mobile.action-manifest.json +53 -0
  77. package/library/recipes/runner/action-validation.extension.recipe.json +8 -1
  78. package/library/recipes/runner/action-validation.mobile.recipe.json +8 -1
  79. package/package.json +10 -6
  80. package/scripts/completions.sh +7 -7
  81. package/docs/ADAPTER-SURFACE.md +0 -26
  82. package/docs/CHEATSHEET.md +0 -48
  83. package/docs/CLI-ERGONOMICS-AUDIT.md +0 -32
  84. package/docs/CLI-ERGONOMICS-HUMAN-QA.md +0 -64
  85. package/docs/CODE-MAP.md +0 -62
  86. package/docs/UX-PRINCIPLES.md +0 -66
  87. package/docs/VIDEO-DEMO-VALIDATION.md +0 -74
  88. package/docs/architecture.md +0 -88
  89. package/docs/live-adapter-contract.md +0 -190
  90. package/docs/package-boundaries.md +0 -47
  91. package/docs/perps-flow-catalog.md +0 -235
  92. package/docs/recipe-libraries.md +0 -71
  93. 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": "extension-account-list",
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": "select-account",
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.17.4",
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.8.0",
23
- "@farmslot/recipe-harness": "^0.5.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
- "esbuild": "0.28.1",
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": {
@@ -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|flows>` command (per-checkout cached):
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 + flow refs from the recipe library
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
- _mmh_flows() { local b; b="$(_mmh_bin)" || return 0; "$b" completion-candidates flows 2>/dev/null; }
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 "$(_mmh_flows)" -- "$cur")) ;;
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)$(_mmh_flows)}"); compadd -a candidates; _files ;;
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
  }
@@ -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.
@@ -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`.