@deeeed/metamask-harness 0.2.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 (158) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/README.md +140 -0
  3. package/bin/mm-harness +99 -0
  4. package/docs/CHEATSHEET.md +61 -0
  5. package/docs/CLI-SPEC.md +915 -0
  6. package/docs/MENTAL-MODEL.md +295 -0
  7. package/docs/architecture.md +367 -0
  8. package/docs/extension-runtime-commands.md +60 -0
  9. package/docs/harness-cli.md +43 -0
  10. package/docs/live-adapter-contract.md +188 -0
  11. package/docs/package-boundaries.md +47 -0
  12. package/docs/perps-flow-catalog.md +235 -0
  13. package/docs/recipe-libraries.md +95 -0
  14. package/docs/runtime-file-conventions.md +36 -0
  15. package/library/actions/core/perps/_controller.mjs +727 -0
  16. package/library/actions/core/perps/assert_orders.mjs +53 -0
  17. package/library/actions/core/perps/assert_positions.mjs +52 -0
  18. package/library/actions/core/perps/close_orders.mjs +97 -0
  19. package/library/actions/core/perps/close_positions.mjs +118 -0
  20. package/library/actions/core/perps/ensure_orders.mjs +40 -0
  21. package/library/actions/core/perps/ensure_positions.mjs +37 -0
  22. package/library/actions/core/perps/place_order.mjs +201 -0
  23. package/library/actions/core/perps/read_account.mjs +30 -0
  24. package/library/actions/core/perps/read_orders.mjs +27 -0
  25. package/library/actions/core/perps/read_positions.mjs +27 -0
  26. package/library/actions/core/perps/start_state.mjs +92 -0
  27. package/library/actions/core/perps/teardown_state.mjs +86 -0
  28. package/library/actions/extension/perps/assert_orders.mjs +11 -0
  29. package/library/actions/extension/perps/assert_positions.mjs +11 -0
  30. package/library/actions/extension/perps/close_orders.mjs +8 -0
  31. package/library/actions/extension/perps/close_positions.mjs +8 -0
  32. package/library/actions/extension/perps/ensure_orders.mjs +4 -0
  33. package/library/actions/extension/perps/ensure_positions.mjs +4 -0
  34. package/library/actions/extension/perps/perps.mjs +730 -0
  35. package/library/actions/extension/perps/place_order.mjs +7 -0
  36. package/library/actions/extension/perps/read_orders.mjs +4 -0
  37. package/library/actions/extension/perps/read_positions.mjs +3 -0
  38. package/library/actions/extension/platform/cdp.mjs +541 -0
  39. package/library/actions/extension/ui/navigate.mjs +44 -0
  40. package/library/actions/extension/wallet/ensure_unlocked.mjs +36 -0
  41. package/library/actions/extension/wallet/read_state.mjs +27 -0
  42. package/library/actions/extension/wallet/select_account.mjs +48 -0
  43. package/library/actions/extension/wallet/setup.mjs +35 -0
  44. package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
  45. package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
  46. package/library/actions/mobile/bridge-runtime/cdp-bridge.cjs +686 -0
  47. package/library/actions/mobile/bridge-runtime/lib/cdp-eval.cjs +110 -0
  48. package/library/actions/mobile/bridge-runtime/lib/config.cjs +39 -0
  49. package/library/actions/mobile/bridge-runtime/lib/issue-capture.cjs +446 -0
  50. package/library/actions/mobile/bridge-runtime/lib/target-discovery.cjs +204 -0
  51. package/library/actions/mobile/bridge-runtime/lib/ws-client.cjs +108 -0
  52. package/library/actions/mobile/bridge-runtime/setup-wallet.sh +442 -0
  53. package/library/actions/mobile/perps/assert_orders.mjs +11 -0
  54. package/library/actions/mobile/perps/assert_positions.mjs +11 -0
  55. package/library/actions/mobile/perps/close_orders.mjs +8 -0
  56. package/library/actions/mobile/perps/close_positions.mjs +8 -0
  57. package/library/actions/mobile/perps/ensure_orders.mjs +4 -0
  58. package/library/actions/mobile/perps/ensure_positions.mjs +4 -0
  59. package/library/actions/mobile/perps/perps.mjs +709 -0
  60. package/library/actions/mobile/perps/place_order.mjs +7 -0
  61. package/library/actions/mobile/perps/read_orders.mjs +4 -0
  62. package/library/actions/mobile/perps/read_positions.mjs +3 -0
  63. package/library/actions/mobile/platform/bridge.mjs +283 -0
  64. package/library/actions/mobile/ui/navigate.mjs +38 -0
  65. package/library/actions/mobile/wallet/ensure_unlocked.mjs +107 -0
  66. package/library/actions/mobile/wallet/home.mjs +35 -0
  67. package/library/actions/mobile/wallet/read_state.mjs +40 -0
  68. package/library/actions/mobile/wallet/select_account.mjs +48 -0
  69. package/library/actions/mobile/wallet/setup.mjs +220 -0
  70. package/library/flows/perps.flows.json +64 -0
  71. package/library/library.json +7 -0
  72. package/library/manifests/core.action-manifest.json +1282 -0
  73. package/library/manifests/extension.action-manifest.json +1749 -0
  74. package/library/manifests/mobile.action-manifest.json +1753 -0
  75. package/library/recipes/action-validation.extension.recipe.json +417 -0
  76. package/library/recipes/action-validation.mobile.recipe.json +422 -0
  77. package/library/recipes/order-lifecycle.core.recipe.json +78 -0
  78. package/library/recipes/perps-lifecycle.recipe.json +194 -0
  79. package/library/recipes/read-markets.core.recipe.json +38 -0
  80. package/library/recipes/smoke.extension.recipe.json +31 -0
  81. package/library/recipes/smoke.mobile.recipe.json +31 -0
  82. package/library/recipes/trading-lifecycle.core.recipe.json +76 -0
  83. package/orchestration/compat-overlays/README.md +19 -0
  84. package/orchestration/compat-overlays/mobile/README.md +13 -0
  85. package/orchestration/compat-overlays/mobile/rn81-message-event-source.patch +42 -0
  86. package/orchestration/core/cleanup.sh +37 -0
  87. package/orchestration/core/inject.sh +154 -0
  88. package/orchestration/doctor.mjs +72 -0
  89. package/orchestration/extension/cleanup.mjs +60 -0
  90. package/orchestration/extension/console-tail.mjs +228 -0
  91. package/orchestration/extension/ensure-browser.sh +416 -0
  92. package/orchestration/extension/ensure-ready.ts +185 -0
  93. package/orchestration/extension/extension-id.ts +107 -0
  94. package/orchestration/extension/inject.mjs +266 -0
  95. package/orchestration/extension/launch-browser.cjs +216 -0
  96. package/orchestration/extension/launch.sh +175 -0
  97. package/orchestration/extension/live.sh +320 -0
  98. package/orchestration/extension/pin-remote-flags.cjs +45 -0
  99. package/orchestration/extension/readiness.mjs +414 -0
  100. package/orchestration/extension/refresh-build.sh +190 -0
  101. package/orchestration/extension/runtime-decision.ts +445 -0
  102. package/orchestration/extension/runtime.ts +407 -0
  103. package/orchestration/extension/seed-fixture.sh +177 -0
  104. package/orchestration/extension/sidepanel-toggle.sh +291 -0
  105. package/orchestration/extension/snapshot-dist.sh +84 -0
  106. package/orchestration/extension/start-watch.sh +339 -0
  107. package/orchestration/extension/wallet-fixture-state.cjs +1086 -0
  108. package/orchestration/lib/activate-repo-node.sh +144 -0
  109. package/orchestration/lib/cli-color.mjs +84 -0
  110. package/orchestration/lib/cli-commands.mjs +243 -0
  111. package/orchestration/lib/cli-home.mjs +354 -0
  112. package/orchestration/lib/cli-ux.sh +252 -0
  113. package/orchestration/lib/cli-version.mjs +123 -0
  114. package/orchestration/lib/ensure-runner-deps.sh +56 -0
  115. package/orchestration/lib/harness-path.sh +55 -0
  116. package/orchestration/lib/hash-helpers.sh +44 -0
  117. package/orchestration/lib/json-field.sh +23 -0
  118. package/orchestration/lib/log-tui.mjs +304 -0
  119. package/orchestration/lib/open-debug.mjs +317 -0
  120. package/orchestration/lib/path-defaults.json +4 -0
  121. package/orchestration/lib/progress.mjs +107 -0
  122. package/orchestration/lib/recipe-paths.mjs +26 -0
  123. package/orchestration/lib/resolve-farmslot-ports.sh +144 -0
  124. package/orchestration/manifest.json +358 -0
  125. package/orchestration/mobile/cleanup.sh +192 -0
  126. package/orchestration/mobile/deps-markers.ts +21 -0
  127. package/orchestration/mobile/inject.sh +681 -0
  128. package/orchestration/mobile/launch.sh +137 -0
  129. package/orchestration/mobile/live.sh +125 -0
  130. package/orchestration/mobile/runtime-decision.ts +292 -0
  131. package/orchestration/porcelain/metamask-recipe +99 -0
  132. package/orchestration/porcelain/mm-recipe +1591 -0
  133. package/orchestration/porcelain/mme-recipe +1181 -0
  134. package/package.json +59 -0
  135. package/runner/extension/verify.sh +511 -0
  136. package/runner/mobile/verify.sh +501 -0
  137. package/runner/src/adapters.ts +601 -0
  138. package/runner/src/cli.ts +1820 -0
  139. package/runner/src/commands/debug.ts +44 -0
  140. package/runner/src/commands/fixtures.ts +99 -0
  141. package/runner/src/commands/launch.ts +397 -0
  142. package/runner/src/commands/logs.ts +60 -0
  143. package/runner/src/commands/shared.ts +138 -0
  144. package/runner/src/completions-cache.ts +86 -0
  145. package/runner/src/doctor.ts +203 -0
  146. package/runner/src/harness.ts +516 -0
  147. package/runner/src/heal-bounds.ts +179 -0
  148. package/runner/src/index.ts +6 -0
  149. package/runner/src/live-adapter-contract.ts +274 -0
  150. package/runner/src/manifest.ts +47 -0
  151. package/runner/src/mm-harness-cli.ts +488 -0
  152. package/runner/src/paths.ts +198 -0
  153. package/runner/src/recording-target.ts +147 -0
  154. package/runner/src/run-recording.ts +329 -0
  155. package/runner/src/runner.ts +108 -0
  156. package/runner/src/types.ts +57 -0
  157. package/scripts/completions.sh +125 -0
  158. package/scripts/install-completions.sh +62 -0
@@ -0,0 +1,48 @@
1
+ import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
+
3
+ function normalize(value) {
4
+ return String(value ?? '').toLowerCase();
5
+ }
6
+
7
+ runAdapter((input) => withExtensionPage(input, async (page) => {
8
+ const requestedAddress = normalize(input.node?.address);
9
+ const requestedId = input.node?.id ? String(input.node.id) : null;
10
+ const requestedName = input.node?.name ? String(input.node.name) : null;
11
+ if (!requestedAddress && !requestedId && !requestedName) {
12
+ throw new Error('metamask.wallet.select_account extension live adapter requires node.address, node.id, or node.name.');
13
+ }
14
+ const selected = await page.evaluate(`(async () => {
15
+ const hooks = globalThis.stateHooks;
16
+ const store = hooks && hooks.store;
17
+ const request = hooks && hooks.submitRequestToBackground;
18
+ if (!store || typeof store.getState !== 'function') throw new Error('stateHooks.store.getState is unavailable.');
19
+ if (typeof request !== 'function') throw new Error('stateHooks.submitRequestToBackground is unavailable.');
20
+ const state = store.getState() || {};
21
+ const internal = state.metamask?.internalAccounts || {};
22
+ const accounts = internal.accounts || {};
23
+ const requestedAddress = ${JSON.stringify(requestedAddress)};
24
+ const requestedId = ${JSON.stringify(requestedId)};
25
+ const requestedName = ${JSON.stringify(requestedName)};
26
+ const entries = Object.entries(accounts);
27
+ const match = entries.find(([id, account]) => {
28
+ const metadata = account?.metadata || {};
29
+ return (requestedId && id === requestedId) ||
30
+ (requestedAddress && String(account?.address || '').toLowerCase() === requestedAddress) ||
31
+ (requestedName && metadata.name === requestedName);
32
+ });
33
+ if (!match) throw new Error('Requested account was not found in internalAccounts.');
34
+ const [id, account] = match;
35
+ await request('setSelectedInternalAccount', [id]);
36
+ const after = store.getState()?.metamask?.internalAccounts || {};
37
+ const selectedId = after.selectedAccount;
38
+ const selected = after.accounts?.[selectedId];
39
+ if (selectedId !== id) throw new Error('Expected selected account ' + id + ', got ' + selectedId);
40
+ return {
41
+ id: selectedId,
42
+ address: selected?.address || null,
43
+ name: selected?.metadata?.name || null,
44
+ type: selected?.type || null
45
+ };
46
+ })()`, { awaitPromise: true });
47
+ return { action: input.action, selected, proofPath: 'extension-account-selection' };
48
+ }));
@@ -0,0 +1,35 @@
1
+ import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
+
3
+ runAdapter((input) => withExtensionPage(input, async (page) => {
4
+ const state = await page.evaluate(`(() => {
5
+ const passwordInput = Boolean(document.querySelector('input[type="password"]'));
6
+ const hooks = globalThis.stateHooks;
7
+ const store = hooks && hooks.store;
8
+ const root = store && typeof store.getState === 'function' ? store.getState() : {};
9
+ const metamask = root.metamask || {};
10
+ const internal = metamask.internalAccounts || {};
11
+ const accounts = internal.accounts || {};
12
+ const selectedId = internal.selectedAccount || null;
13
+ const selected = selectedId && accounts[selectedId] ? accounts[selectedId] : null;
14
+ return {
15
+ href: String(globalThis.location && globalThis.location.href || ''),
16
+ passwordInput,
17
+ completedOnboarding: Boolean(metamask.completedOnboarding),
18
+ selectedAccount: selected ? {
19
+ id: selectedId,
20
+ address: selected.address || null,
21
+ } : null,
22
+ };
23
+ })()`);
24
+ const seededProfilePresent = Boolean(state?.passwordInput || (state?.completedOnboarding && state?.selectedAccount?.address));
25
+ if (!seededProfilePresent) {
26
+ throw new Error(`Extension fixture profile is not ready; expected a locked password screen or completed onboarding with a selected account, got ${JSON.stringify(state)}`);
27
+ }
28
+ return {
29
+ action: input.action,
30
+ setup: 'preseeded-profile-verified',
31
+ targetUrl: page.target.url,
32
+ runtimeState: state,
33
+ proofPath: 'extension-fixture-profile',
34
+ };
35
+ }));
@@ -0,0 +1,185 @@
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import { StyleSheet, Text, View } from 'react-native';
3
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
4
+ import { FullWindowOverlay } from 'react-native-screens';
5
+
6
+ interface Step {
7
+ id: string;
8
+ intent: string;
9
+ status?: string;
10
+ progress?: { current?: number; total?: number };
11
+ detail?: string;
12
+ error?: string;
13
+ nodeId?: string;
14
+ debug?: { nodeId?: string; proofTarget?: unknown };
15
+ }
16
+
17
+ // Step bus. The HUD owns the registry so nothing outside this leaf module needs
18
+ // to import AgenticService — keeping the bridge off the static import graph, so
19
+ // it is dead-code-eliminated from release builds. AgenticService emits through
20
+ // `emitStepHud`; the mounted HUD registers its setState as the sink.
21
+ let stepSink: ((step: Step | null) => void) | null = null;
22
+
23
+ export function emitStepHud(step: Step | null) {
24
+ stepSink?.(step);
25
+ }
26
+
27
+ function registerStepHudCallback(sink: ((step: Step | null) => void) | null) {
28
+ stepSink = sink;
29
+ }
30
+
31
+ function statusForStep(step: Step) {
32
+ const id = typeof step.id === 'string' ? step.id : '';
33
+ return String(step.status ?? id.split(/\s+/)[0] ?? '').toLowerCase();
34
+ }
35
+
36
+ function progressForStep(step: Step) {
37
+ if (
38
+ typeof step.progress?.current === 'number' &&
39
+ typeof step.progress?.total === 'number'
40
+ ) {
41
+ return `${step.progress.current}/${step.progress.total}`;
42
+ }
43
+ const progressPattern = /\b\d+\s*\/\s*\d+\b/;
44
+ const id = typeof step.id === 'string' ? step.id : '';
45
+ const match = progressPattern.exec(id);
46
+ return match ? match[0].replace(/\s+/g, '') : null;
47
+ }
48
+
49
+ function badgeTextForStep(step: Step) {
50
+ const rawStatus = statusForStep(step);
51
+ const status = rawStatus === 'running' ? 'run' : rawStatus;
52
+ const progress = progressForStep(step);
53
+ return [status || 'run', progress].filter(Boolean).join(' ').toUpperCase();
54
+ }
55
+
56
+ function statusToneForStep(step: Step) {
57
+ const status = statusForStep(step);
58
+ if (status === 'fail' || status === 'failed' || status === 'error') {
59
+ return 'fail';
60
+ }
61
+ if (status === 'pass' || status === 'passed' || status === 'success') {
62
+ return 'pass';
63
+ }
64
+ return 'running';
65
+ }
66
+
67
+ function displayStateForStep(step: Step) {
68
+ const intent = step.intent.trim();
69
+ const secondary = [
70
+ step.error ? `error: ${step.error}` : null,
71
+ step.detail && step.detail !== intent ? step.detail : null,
72
+ ].filter((part): part is string => Boolean(part));
73
+ return {
74
+ intent,
75
+ secondary: secondary.filter(
76
+ (part, index) => secondary.indexOf(part) === index,
77
+ ),
78
+ };
79
+ }
80
+
81
+ // Debug-only overlay — intentionally uses hardcoded colors for guaranteed
82
+ // contrast on both light and dark themes. Design tokens would defeat the purpose.
83
+ /* eslint-disable react-native/no-color-literals, @metamask/design-tokens/color-no-hex */
84
+ const styles = StyleSheet.create({
85
+ container: {
86
+ position: 'absolute',
87
+ left: 0,
88
+ right: 0,
89
+ zIndex: 9999,
90
+ backgroundColor: 'rgba(0, 0, 0, 0.58)',
91
+ paddingVertical: 3,
92
+ },
93
+ line: {
94
+ color: '#FFFFFF',
95
+ fontSize: 11,
96
+ fontWeight: '700',
97
+ lineHeight: 14,
98
+ },
99
+ badgeText: {
100
+ fontFamily: 'Courier',
101
+ fontSize: 9,
102
+ fontWeight: '800',
103
+ },
104
+ badgeTextRunning: {
105
+ color: '#00FF88',
106
+ },
107
+ badgeTextPass: {
108
+ color: '#00FF88',
109
+ },
110
+ badgeTextFail: {
111
+ color: '#FF4D4F',
112
+ },
113
+ secondary: {
114
+ color: '#E6E6E6',
115
+ fontSize: 10,
116
+ fontWeight: '400',
117
+ lineHeight: 12,
118
+ },
119
+ });
120
+ /* eslint-enable react-native/no-color-literals, @metamask/design-tokens/color-no-hex */
121
+
122
+ // Inner component — hooks always called unconditionally, per rules of React.
123
+ const AgentStepHudInner = () => {
124
+ const [step, setStep] = useState<Step | null>(null);
125
+ const insets = useSafeAreaInsets();
126
+
127
+ const containerStyle = useMemo(
128
+ () => [
129
+ styles.container,
130
+ {
131
+ bottom: Math.max(insets.bottom, 0),
132
+ paddingLeft: Math.max(insets.left, 10),
133
+ paddingRight: Math.max(insets.right, 10),
134
+ },
135
+ ],
136
+ [insets.left, insets.right, insets.bottom],
137
+ );
138
+
139
+ useEffect(() => {
140
+ registerStepHudCallback(setStep);
141
+ return () => {
142
+ registerStepHudCallback(null);
143
+ };
144
+ }, []);
145
+
146
+ if (!step) return null;
147
+
148
+ const { intent, secondary } = displayStateForStep(step);
149
+ const badge = badgeTextForStep(step);
150
+ const tone = statusToneForStep(step);
151
+ const badgeTextStyle =
152
+ tone === 'fail'
153
+ ? styles.badgeTextFail
154
+ : tone === 'pass'
155
+ ? styles.badgeTextPass
156
+ : styles.badgeTextRunning;
157
+
158
+ // FullWindowOverlay (iOS) renders the HUD in a UIWindow above every native
159
+ // layer — including native-stack modal screens (perps close-position/TPSL,
160
+ // etc.), which a plain absolute/zIndex View in the JS root cannot reach. On
161
+ // Android it is a passthrough, so root-level rendering is preserved.
162
+ return (
163
+ <FullWindowOverlay>
164
+ <View style={containerStyle} pointerEvents="none">
165
+ <Text style={styles.line}>
166
+ <Text style={[styles.badgeText, badgeTextStyle]}>{badge}</Text>
167
+ {intent ? ` ${intent}` : ''}
168
+ </Text>
169
+ {secondary.map((detail) => (
170
+ <Text key={detail} style={styles.secondary}>
171
+ {detail}
172
+ </Text>
173
+ ))}
174
+ </View>
175
+ </FullWindowOverlay>
176
+ );
177
+ };
178
+
179
+ // Outer guard — never calls hooks, so the __DEV__ early return is fine.
180
+ const AgentStepHud = () => {
181
+ if (!__DEV__) return null;
182
+ return <AgentStepHudInner />;
183
+ };
184
+
185
+ export default AgentStepHud;