@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
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@deeeed/metamask-harness",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "mm-harness": "bin/mm-harness"
7
+ },
8
+ "scripts": {
9
+ "dev:link-farmslot": "node scripts/link-local-farmslot.mjs",
10
+ "check": "node scripts/check.mjs",
11
+ "self-test": "orchestration/porcelain/metamask-recipe self-test",
12
+ "manifest:mobile": "bin/mm-harness actions --raw --adapter mobile --json",
13
+ "manifest:extension": "bin/mm-harness actions --raw --adapter extension --json",
14
+ "check:syntax": "find . -name '*.mjs' -print0 | xargs -0 -n1 node --check"
15
+ },
16
+ "dependencies": {
17
+ "@farmslot/protocol": "^0.7.2",
18
+ "@farmslot/recipe-harness": "^0.3.3",
19
+ "commander": "^12.0.0",
20
+ "tsx": "^4.19.0",
21
+ "viem": "^2.52.2"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^22.0.0",
25
+ "typescript": "^5.6.0"
26
+ },
27
+ "main": "./runner/src/index.ts",
28
+ "types": "./runner/src/index.ts",
29
+ "exports": {
30
+ ".": {
31
+ "types": "./runner/src/index.ts",
32
+ "import": "./runner/src/index.ts"
33
+ }
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "files": [
39
+ "bin",
40
+ "runner",
41
+ "orchestration",
42
+ "library",
43
+ "scripts/completions.sh",
44
+ "scripts/install-completions.sh",
45
+ "!scripts/README.md",
46
+ "docs",
47
+ "README.md",
48
+ "CHANGELOG.md"
49
+ ],
50
+ "packageManager": "yarn@4.5.3",
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "git+https://github.com/MetaMask/experimental-metamask-harness.git"
54
+ },
55
+ "bugs": {
56
+ "url": "https://github.com/MetaMask/experimental-metamask-harness/issues"
57
+ },
58
+ "homepage": "https://github.com/MetaMask/experimental-metamask-harness#readme"
59
+ }
@@ -0,0 +1,511 @@
1
+ #!/usr/bin/env bash
2
+ # verify.sh — extension slot verification (readiness/proof layer)
3
+ #
4
+ # Purpose:
5
+ # Recipe-tree verification for ONE extension instance: static dist checks,
6
+ # live readiness over CDP, and the smoke recipe run, aggregated into a
7
+ # verify summary.
8
+ #
9
+ # Inputs (flags / env):
10
+ # --target <metamask-extension> (default $PWD), --cdp-port <port>
11
+ # (numeric; empty = static-only), --out <recipes-dir> (validated under
12
+ # target), --static-only, --artifacts-dir <dir>
13
+ # env RECIPE_HARNESS_ROOT, RECIPE_RUNTIME_DIR
14
+ #
15
+ # Outputs:
16
+ # <artifacts>/summary.json + logs/ (readiness, recipe runs).
17
+ # Exit 0 — verify pass; 1 — checks failed / helpers missing; 2 — bad args
18
+ # or unsafe/missing --out.
19
+ #
20
+ # Never touches: product source files; the dist build (read-only);
21
+ # anything outside the harness dir and artifacts dir.
22
+ set -euo pipefail
23
+
24
+ TARGET="$PWD"
25
+ CDP_PORT=""
26
+ ARTIFACTS=""
27
+ STATIC_ONLY=false
28
+ OUT=""
29
+ require_value() { [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; }
30
+ while [ "$#" -gt 0 ]; do
31
+ case "$1" in
32
+ --target) require_value "$@"; TARGET="$2"; shift 2 ;;
33
+ --out) require_value "$@"; OUT="$2"; shift 2 ;;
34
+ --cdp-port) require_value "$@"; CDP_PORT="$2"; shift 2 ;;
35
+ --artifacts-dir) require_value "$@"; ARTIFACTS="$2"; shift 2 ;;
36
+ --static-only) STATIC_ONLY=true; shift ;;
37
+ -h|--help) echo "Usage: verify.sh [--target <metamask-extension>] [--out <recipes-dir>] [--cdp-port <port>] [--static-only]"; exit 0 ;;
38
+ *) echo "Unknown arg: $1" >&2; exit 2 ;;
39
+ esac
40
+ done
41
+
42
+ # Reject a non-numeric --cdp-port before it is interpolated into shell/HTTP/CDP
43
+ # strings. Empty is allowed (static-only verify needs no port).
44
+ if [ -n "$CDP_PORT" ]; then
45
+ case "$CDP_PORT" in
46
+ *[!0-9]*) echo "Invalid --cdp-port (must be numeric): $CDP_PORT" >&2; exit 2 ;;
47
+ esac
48
+ fi
49
+
50
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
51
+ # shellcheck disable=SC1091
52
+ for _hp in "$SCRIPT_DIR/lib/harness-path.sh" "$SCRIPT_DIR/../../orchestration/lib/harness-path.sh" "$SCRIPT_DIR/../lib/harness-path.sh"; do
53
+ [ -f "$_hp" ] && { . "$_hp"; break; }
54
+ done
55
+ unset _hp
56
+ if ! command -v harness_root >/dev/null 2>&1; then
57
+ echo "extension verify: shared lib orchestration/lib/harness-path.sh not found; reinstall the harness." >&2
58
+ exit 1
59
+ fi
60
+
61
+ # Safe task-local --out resolution (kept from the legacy extension path.sh).
62
+ resolve_harness_out() {
63
+ local target="$1"
64
+ local out="$2"
65
+ node - "$target" "$out" <<'NODE'
66
+ const path = require('path');
67
+
68
+ const target = path.resolve(process.argv[2]);
69
+ const out = process.argv[3];
70
+ if (!out) process.exit(1);
71
+ if (out.split(/[\\/]+/).includes('..')) process.exit(1);
72
+
73
+ const resolved = path.resolve(target, out);
74
+ const relative = path.relative(target, resolved);
75
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
76
+ process.exit(1);
77
+ }
78
+
79
+ process.stdout.write(resolved);
80
+ NODE
81
+ }
82
+ TARGET="$(cd "$TARGET" && pwd)"
83
+ RUNTIME_DIR="$(recipe_runtime_dir)"
84
+ HARNESS_ROOT="$(harness_root)"
85
+ HARNESS_REL="$HARNESS_ROOT/extension"
86
+ HARNESS_DIR="$(harness_dir "$TARGET" extension)"
87
+ RUNNER_BIN="$HARNESS_DIR/runner/bin/metamask-recipe"
88
+ # --out (optional): a task-local recipes dir. Resolve it safely within the target
89
+ # (resolve_harness_out rejects absolute/.. escapes) and prefer its smoke recipe so
90
+ # `live --out <dir>` does not silently fall back to the installed default.
91
+ SMOKE_RECIPE="$HARNESS_DIR/runner/recipes/smoke.extension.recipe.json"
92
+ if [ -n "$OUT" ]; then
93
+ # Fail fast on a task-local --out that does not contain the requested recipe.
94
+ # Silently falling back to the installed default would validate a different
95
+ # (possibly stale) recipe than the caller explicitly asked for.
96
+ OUT_ABS="$(resolve_harness_out "$TARGET" "$OUT" 2>/dev/null || true)"
97
+ if [ -z "$OUT_ABS" ]; then
98
+ echo "extension verify: --out '$OUT' did not resolve to a safe path under the target." >&2
99
+ exit 2
100
+ fi
101
+ if [ ! -f "$OUT_ABS/smoke.extension.recipe.json" ]; then
102
+ echo "extension verify: --out '$OUT' (resolved: $OUT_ABS) has no smoke.extension.recipe.json. Refusing to fall back to the installed default recipe; place the recipe under --out or omit --out." >&2
103
+ exit 2
104
+ fi
105
+ SMOKE_RECIPE="$OUT_ABS/smoke.extension.recipe.json"
106
+ fi
107
+ ARTIFACTS="${ARTIFACTS:-$HARNESS_DIR/verify/$(date -u +%Y%m%dT%H%M%SZ)}"
108
+ mkdir -p "$ARTIFACTS/logs"
109
+ EXTENSION_ID_FILE="$TARGET/$RUNTIME_DIR/extension.id"
110
+ # Shared JSON reader: $SCRIPT_DIR/lib when running from the installed copy,
111
+ # else the runner canonical at orchestration/lib.
112
+ # shellcheck disable=SC1091
113
+ for _lib in "$SCRIPT_DIR/lib/json-field.sh" "$SCRIPT_DIR/../../orchestration/lib/json-field.sh"; do
114
+ [ -f "$_lib" ] && { . "$_lib"; break; }
115
+ done
116
+ unset _lib
117
+ # Fail fast with an actionable message if neither path loaded (e.g. an install that
118
+ # predates the lib co-location), instead of a cryptic later `set -e` abort.
119
+ if ! command -v read_runtime_context_field >/dev/null 2>&1; then
120
+ echo "extension verify: json-field helper missing (orchestration/lib/json-field.sh). Run metamask-recipe extension prepare --target $TARGET" >&2
121
+ exit 1
122
+ fi
123
+ # readiness probe is app control: co-located in installed copies, under
124
+ # orchestration/extension in a repo checkout.
125
+ READINESS_MJS=""
126
+ for _r in "$SCRIPT_DIR/readiness.mjs" "$SCRIPT_DIR/../../orchestration/extension/readiness.mjs"; do
127
+ [ -f "$_r" ] && { READINESS_MJS="$_r"; break; }
128
+ done
129
+ unset _r
130
+ if [ -z "$READINESS_MJS" ]; then
131
+ echo "extension verify: readiness.mjs missing next to $SCRIPT_DIR; reinstall the runner." >&2
132
+ exit 1
133
+ fi
134
+ refresh_extension_id() {
135
+ if [ -f "$EXTENSION_ID_FILE" ]; then
136
+ RECIPE_HARNESS_EXTENSION_ID="$(tr -d '[:space:]' < "$EXTENSION_ID_FILE")"
137
+ export RECIPE_HARNESS_EXTENSION_ID
138
+ else
139
+ unset RECIPE_HARNESS_EXTENSION_ID || true
140
+ fi
141
+ }
142
+ CONTEXT_PATH="${RECIPE_RUNTIME_CONTEXT:-$TARGET/$RUNTIME_DIR/agentic-runtime.json}"
143
+ if [ -f "$CONTEXT_PATH" ]; then
144
+ CONTEXT_EXTENSION_ID="$(read_runtime_context_field "$CONTEXT_PATH" extensionId || true)"
145
+ if [[ "$CONTEXT_EXTENSION_ID" =~ ^[a-p]{32}$ ]]; then
146
+ mkdir -p "$(dirname "$EXTENSION_ID_FILE")"
147
+ printf '%s\n' "$CONTEXT_EXTENSION_ID" > "$EXTENSION_ID_FILE"
148
+ RECIPE_HARNESS_EXTENSION_ID="$CONTEXT_EXTENSION_ID"
149
+ export RECIPE_HARNESS_EXTENSION_ID
150
+ else
151
+ refresh_extension_id
152
+ fi
153
+ unset CONTEXT_EXTENSION_ID
154
+ else
155
+ refresh_extension_id
156
+ fi
157
+
158
+ status="pass"
159
+ checks=()
160
+
161
+ fixture_status_json() {
162
+ TARGET_FOR_FIXTURE="$TARGET" RECIPE_RUNTIME_DIR="$RUNTIME_DIR" node <<'NODE'
163
+ const fs = require('fs');
164
+ const crypto = require('crypto');
165
+ const path = require('path');
166
+ const target = process.env.TARGET_FOR_FIXTURE;
167
+ const runtimeDir = process.env.RECIPE_RUNTIME_DIR;
168
+ if (!runtimeDir) throw new Error('RECIPE_RUNTIME_DIR is required');
169
+ const candidates = [
170
+ runtimeDir + '/wallet-fixture.json',
171
+ 'test/e2e/seeder/withFixtures.js',
172
+ 'test/e2e/fixtures',
173
+ 'fixtures',
174
+ ].map((rel) => path.join(target, rel));
175
+ const found = candidates.find((file) => fs.existsSync(file));
176
+ const extensionId = path.join(target, runtimeDir, 'extension.id');
177
+ const profileHints = [];
178
+ if (fs.existsSync(extensionId)) profileHints.push({ path: `${runtimeDir}/extension.id`, type: 'extension-id' });
179
+ if (!found) {
180
+ console.log(JSON.stringify({
181
+ status: 'MISSING_FIXTURES',
182
+ message: 'Fixture status: MISSING_FIXTURES. This run may depend on an inherited browser/profile state. Prefer a prepared debug profile or fixture seed before spending time repairing state manually.',
183
+ profileHints,
184
+ }));
185
+ process.exit(0);
186
+ }
187
+ const stat = fs.statSync(found);
188
+ const isFile = stat.isFile();
189
+ let sha256 = null;
190
+ let validJson = null;
191
+ let hasWalletPassword = false;
192
+ let mobileAccountShape = false;
193
+ const rel = path.relative(target, found);
194
+ const isWalletFixture = rel === runtimeDir + '/wallet-fixture.json';
195
+ if (isFile) {
196
+ const bytes = fs.readFileSync(found);
197
+ sha256 = crypto.createHash('sha256').update(bytes).digest('hex');
198
+ if (found.endsWith('.json')) {
199
+ try {
200
+ const parsed = JSON.parse(bytes.toString('utf8'));
201
+ validJson = true;
202
+ const accounts = Array.isArray(parsed.accounts) ? parsed.accounts : [];
203
+ hasWalletPassword = typeof parsed.password === 'string' && parsed.password.length > 0;
204
+ mobileAccountShape = accounts.some((account) => account?.type === 'mnemonic') &&
205
+ accounts.filter((account) => account?.type === 'privateKey').length >= 2;
206
+ } catch {
207
+ validJson = false;
208
+ }
209
+ }
210
+ }
211
+ const status = validJson === false
212
+ ? 'STALE_OR_INVALID'
213
+ : isWalletFixture && hasWalletPassword
214
+ ? 'READY'
215
+ : 'PROFILE_HINTS';
216
+ console.log(JSON.stringify({
217
+ status,
218
+ path: rel,
219
+ type: isFile ? 'file' : 'directory',
220
+ sha256,
221
+ modifiedAt: stat.mtime.toISOString(),
222
+ profileHints,
223
+ hasWalletPassword,
224
+ mobileAccountShape,
225
+ message: validJson === false
226
+ ? `Fixture status: STALE_OR_INVALID (${rel}). Fix before relying on a clean sandbox.`
227
+ : status === 'READY'
228
+ ? `Fixture status: READY (${rel}).`
229
+ : `Fixture status: PROFILE_HINTS (${rel}); no Mobile-shaped wallet fixture was found for automatic account parity validation.`,
230
+ }));
231
+ NODE
232
+ }
233
+
234
+ cdp_holder_json() {
235
+ local port="$1"
236
+ PORT_FOR_STATUS="$port" node <<'NODE'
237
+ const cp = require('child_process');
238
+ const http = require('http');
239
+ const port = process.env.PORT_FOR_STATUS;
240
+ function run(cmd) {
241
+ try { return cp.execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); }
242
+ catch { return ''; }
243
+ }
244
+ function getJson(path) {
245
+ return new Promise((resolve) => {
246
+ const req = http.get(`http://127.0.0.1:${port}${path}`, { timeout: 1000 }, (res) => {
247
+ let data = '';
248
+ res.on('data', (chunk) => { data += chunk; });
249
+ res.on('end', () => {
250
+ try { resolve(JSON.parse(data)); } catch { resolve(null); }
251
+ });
252
+ });
253
+ req.on('timeout', () => { req.destroy(); resolve(null); });
254
+ req.on('error', () => resolve(null));
255
+ });
256
+ }
257
+ (async () => {
258
+ const pid = run(`lsof -iTCP:${port} -sTCP:LISTEN -t | head -1`);
259
+ // Validate pid is numeric before interpolating it into the `ps -p` shell string.
260
+ const safePid = /^[0-9]+$/.test(pid) ? pid : '';
261
+ const command = safePid ? run(`ps -p ${safePid} -o command=`) : '';
262
+ const version = await getJson('/json/version');
263
+ const targets = await getJson('/json/list');
264
+ const extensionTargets = Array.isArray(targets)
265
+ ? targets.filter((target) => String(target.url || '').startsWith('chrome-extension://')).length
266
+ : 0;
267
+ console.log(JSON.stringify({
268
+ port,
269
+ listening: Boolean(pid),
270
+ pid: pid || null,
271
+ command: command || null,
272
+ cdpReachable: Boolean(version),
273
+ browser: version?.Browser || null,
274
+ targetCount: Array.isArray(targets) ? targets.length : null,
275
+ extensionTargets,
276
+ }));
277
+ })();
278
+ NODE
279
+ }
280
+
281
+ check_file() {
282
+ local rel="$1"
283
+ if [ -e "$TARGET/$rel" ]; then
284
+ checks+=("{\"name\":\"$rel\",\"status\":\"pass\"}")
285
+ else
286
+ checks+=("{\"name\":\"$rel\",\"status\":\"fail\"}")
287
+ status="fail"
288
+ fi
289
+ }
290
+
291
+ check_file "$HARNESS_REL/manifest.json"
292
+ check_file "$HARNESS_REL/action-manifest.json"
293
+ check_file "$HARNESS_REL/runner/bin/metamask-recipe"
294
+ check_file "$HARNESS_REL/runner/manifests/extension.action-manifest.json"
295
+
296
+ # dist-freshness + build-health now come from the runner's single source of
297
+ # truth (`runtime-decision`), which subsumes the probes this helper used to
298
+ # hand-roll — so the dist-id/source-dirty + webpack-log logic lives in ONE place
299
+ # (matching the runner readiness algorithm) and the three layers cannot disagree.
300
+ # git/fs only (no --cdp-port) so this stays harness-independent. The derived
301
+ # dist-freshness.json / build-health.json keep the same status vocab the case
302
+ # mappings + summary below already consume, so behavior is unchanged.
303
+ "$RUNNER_BIN" runtime-decision --adapter extension --target "$TARGET" --json \
304
+ > "$ARTIFACTS/logs/runtime-decision.json" 2>/dev/null \
305
+ || printf '%s' '{}' > "$ARTIFACTS/logs/runtime-decision.json"
306
+ # Surface an empty/{} decision loudly: without it, dist-freshness/build-health
307
+ # silently degrade to WARN and the runtime-proof gate is effectively bypassed.
308
+ if [ ! -s "$ARTIFACTS/logs/runtime-decision.json" ] || [ "$(cat "$ARTIFACTS/logs/runtime-decision.json")" = "{}" ]; then
309
+ echo "recipe-harness verify: runtime-decision returned empty/{} — dist-freshness and build-health cannot be derived from the runner and will degrade to WARN (runtime-proof gate NOT fully validated). Check the runner/build state." >&2
310
+ fi
311
+ node -e '
312
+ const fs = require("fs");
313
+ const dir = process.argv[process.argv.length - 1];
314
+ let r = {};
315
+ try { r = JSON.parse(fs.readFileSync(dir + "/runtime-decision.json", "utf8")); } catch {}
316
+ const c = r.checks || {};
317
+ const dist = c.dist || { status: "unknown" };
318
+ const distMsg = dist.status === "fresh" ? "dist id matches HEAD; no uncommitted source."
319
+ : dist.status === "stale" ? (dist.reason === "uncommitted-source"
320
+ ? ((dist.modified ? dist.modified.length : "some") + " uncommitted source file(s); rebuild or commit.")
321
+ : ("dist id " + (dist.distGitId || "?") + " != HEAD " + (dist.head || "?") + "; rebuild."))
322
+ : dist.status === "no-build" ? "no dist/chrome build."
323
+ : "no git id in dist or not a git checkout; cannot prove parity.";
324
+ fs.writeFileSync(dir + "/dist-freshness.json", JSON.stringify({ ...dist, message: distMsg }));
325
+ const bl = c.buildLog || { status: "unknown" };
326
+ const blMsg = bl.status === "ok" ? "webpack compiled."
327
+ : bl.status === "no-watch" ? "no webpack watch log; build-health n/a (e.g. one-shot build)."
328
+ : bl.status === "building" ? "webpack has not reported a successful compile yet."
329
+ : bl.status === "errors" ? (bl.reason === "stale-cache"
330
+ ? "webpack build failing on a stale cache (ENOENT on a deduped module). Run `metamask-recipe runtime-launch --adapter extension --target <repo> --cdp-port <port>` to auto-clear the cache and rebuild."
331
+ : "webpack build has errors; fix the source/build before validating.")
332
+ : "build-health unknown.";
333
+ fs.writeFileSync(dir + "/build-health.json", JSON.stringify({ status: bl.status, reason: bl.reason, excerpt: bl.excerpt, message: blMsg }));
334
+ ' "$ARTIFACTS/logs" 2>/dev/null \
335
+ || { printf '%s' '{"status":"unknown","message":"dist-freshness probe error"}' > "$ARTIFACTS/logs/dist-freshness.json"; printf '%s' '{"status":"unknown","message":"build-health probe error"}' > "$ARTIFACTS/logs/build-health.json"; }
336
+ df_read() { node -e 'const fs=require("fs");try{const [file,key]=process.argv.slice(-2);const v=JSON.parse(fs.readFileSync(file,"utf8"));process.stdout.write(String(v[key]??""))}catch{process.stdout.write("")}' "$ARTIFACTS/logs/dist-freshness.json" "$1"; }
337
+ df_status="$(df_read status)"
338
+ echo "dist-freshness: ${df_status:-unknown} — $(df_read message)" >&2
339
+ case "$df_status" in
340
+ fresh) checks+=("{\"name\":\"dist-freshness\",\"status\":\"pass\"}") ;;
341
+ stale)
342
+ # A stale dist only fails a LIVE verify (runtime proof would use the wrong
343
+ # build). --static-only checks install/idempotency shape, not runtime, so
344
+ # there it is a warning — don't regress install-only verification.
345
+ if [ "$STATIC_ONLY" = false ]; then
346
+ checks+=("{\"name\":\"dist-freshness\",\"status\":\"fail\",\"detail\":\"see logs/dist-freshness.json\"}"); status="fail"
347
+ else
348
+ checks+=("{\"name\":\"dist-freshness\",\"status\":\"warn\",\"detail\":\"stale (static-only); see logs/dist-freshness.json\"}")
349
+ fi
350
+ ;;
351
+ *) checks+=("{\"name\":\"dist-freshness\",\"status\":\"warn\",\"detail\":\"${df_status:-unknown}; see logs/dist-freshness.json\"}") ;;
352
+ esac
353
+
354
+ # build-health.json was produced by the runtime-decision derivation above (the
355
+ # runner reads the webpack watch log; same status vocab as before).
356
+ bh_read() { node -e 'const fs=require("fs");try{const [file,key]=process.argv.slice(-2);process.stdout.write(String(JSON.parse(fs.readFileSync(file,"utf8"))[key]??""))}catch{process.stdout.write("")}' "$ARTIFACTS/logs/build-health.json" "$1"; }
357
+ bh_status="$(bh_read status)"
358
+ echo "build-health: ${bh_status:-unknown} — $(bh_read message)" >&2
359
+ case "$bh_status" in
360
+ ok|no-watch) checks+=("{\"name\":\"build-health\",\"status\":\"pass\"}") ;;
361
+ errors)
362
+ # A broken build only fails a LIVE verify; in --static-only (install-shape only)
363
+ # it is a loud warning so install/idempotency checks aren't regressed.
364
+ if [ "$STATIC_ONLY" = false ]; then
365
+ checks+=("{\"name\":\"build-health\",\"status\":\"fail\",\"detail\":\"see logs/build-health.json\"}"); status="fail"
366
+ else
367
+ checks+=("{\"name\":\"build-health\",\"status\":\"warn\",\"detail\":\"build errors (static-only); see logs/build-health.json\"}")
368
+ fi
369
+ ;;
370
+ building) checks+=("{\"name\":\"build-health\",\"status\":\"warn\",\"detail\":\"still compiling; see logs/build-health.json\"}") ;;
371
+ *) checks+=("{\"name\":\"build-health\",\"status\":\"warn\",\"detail\":\"${bh_status:-unknown}; see logs/build-health.json\"}") ;;
372
+ esac
373
+
374
+ live_mode="static-only"
375
+ if [ "$STATIC_ONLY" = false ]; then
376
+ fixture_status_json > "$ARTIFACTS/logs/fixture-status.json"
377
+ node -e 'const fs=require("fs"); const file=process.argv[process.argv.length - 1]; const v=JSON.parse(fs.readFileSync(file,"utf8")); console.error(v.message || v.status);' "$ARTIFACTS/logs/fixture-status.json"
378
+ checks+=("{\"name\":\"fixture/profile status\",\"status\":\"$(node -e 'const fs=require("fs"); const file=process.argv[process.argv.length - 1]; const v=JSON.parse(fs.readFileSync(file,"utf8")); console.log(v.status === "READY" ? "pass" : "warn");' "$ARTIFACTS/logs/fixture-status.json")\"}")
379
+
380
+ if [ -z "$CDP_PORT" ]; then
381
+ echo "Live extension verify requires --cdp-port. Static checks may pass, but runtime proof is missing." > "$ARTIFACTS/logs/live-missing-cdp.log"
382
+ checks+=("{\"name\":\"live runtime CDP port\",\"status\":\"fail\",\"detail\":\"missing --cdp-port\"}")
383
+ status="fail"
384
+ live_mode="missing-cdp"
385
+ else
386
+ live_mode="live"
387
+ cdp_holder_json "$CDP_PORT" > "$ARTIFACTS/logs/cdp-holder.json"
388
+ if node "$READINESS_MJS" --target "$TARGET" --cdp-port "$CDP_PORT" --json > "$ARTIFACTS/logs/extension-readiness.json" 2>&1; then
389
+ checks+=("{\"name\":\"live extension readiness\",\"status\":\"pass\"}")
390
+ # extension-readiness.mjs may repair $RUNTIME_DIR/extension.id when the
391
+ # supplied Chrome profile loads a fresh extension ID. Reload it before
392
+ # running recipe smoke checks so the recipe bridge targets the live
393
+ # extension instead of a stale marker from a previous browser profile.
394
+ refresh_extension_id
395
+ else
396
+ checks+=("{\"name\":\"live extension readiness\",\"status\":\"fail\",\"detail\":\"see logs/extension-readiness.json\"}")
397
+ status="fail"
398
+ fi
399
+
400
+ if (
401
+ cd "$TARGET"
402
+ "$RUNNER_BIN" manifest --adapter extension --json
403
+ ) > "$ARTIFACTS/logs/runner-manifest.json" 2> "$ARTIFACTS/logs/runner-manifest.err"; then
404
+ checks+=("{\"name\":\"runner manifest\",\"status\":\"pass\"}")
405
+ else
406
+ checks+=("{\"name\":\"runner manifest\",\"status\":\"fail\",\"detail\":\"see logs/runner-manifest.err\"}")
407
+ status="fail"
408
+ fi
409
+
410
+ if (
411
+ cd "$TARGET"
412
+ "$RUNNER_BIN" run "$SMOKE_RECIPE" --adapter extension --project-root "$TARGET" --cdp-port "$CDP_PORT" --artifacts-dir "$ARTIFACTS/runner-smoke" --json
413
+ ) > "$ARTIFACTS/logs/runner-smoke.log" 2>&1; then
414
+ checks+=("{\"name\":\"runner v1 smoke\",\"status\":\"pass\"}")
415
+ else
416
+ checks+=("{\"name\":\"runner v1 smoke\",\"status\":\"fail\",\"detail\":\"see logs/runner-smoke.log\"}")
417
+ status="fail"
418
+ fi
419
+ fi
420
+ fi
421
+
422
+ if git -C "$TARGET" rev-parse --git-dir >/dev/null 2>&1; then
423
+ git -C "$TARGET" status --short -- . ":(exclude)$HARNESS_ROOT" > "$ARTIFACTS/logs/product-diff-excluding-harness.log" 2>&1 || true
424
+ fi
425
+
426
+ RECIPE_HARNESS_LIVE_MODE="$live_mode" RECIPE_HARNESS_ROOT_EXCLUDE="$HARNESS_ROOT" RECIPE_RUNTIME_DIR="$RUNTIME_DIR" node - "$ARTIFACTS" "$TARGET" "$status" "${checks[@]}" <<'NODE'
427
+ const fs = require('fs');
428
+ const path = require('path');
429
+ const cp = require('child_process');
430
+ const [artifacts, target, status, ...checks] = process.argv.slice(2);
431
+ const parsedChecks = checks.map((entry) => JSON.parse(entry));
432
+ const liveMode = process.env.RECIPE_HARNESS_LIVE_MODE || 'unknown';
433
+ let fixtureStatus = null;
434
+ let cdpHolder = null;
435
+ let readinessReport = null;
436
+ let distFreshness = null;
437
+ try { distFreshness = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/dist-freshness.json'), 'utf8')); } catch {}
438
+ try { fixtureStatus = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/fixture-status.json'), 'utf8')); } catch {}
439
+ try { cdpHolder = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/cdp-holder.json'), 'utf8')); } catch {}
440
+ try { readinessReport = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/extension-readiness.json'), 'utf8')); } catch {}
441
+ function runGit(args) {
442
+ try {
443
+ return cp.execFileSync('git', ['-C', target, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
444
+ } catch (error) {
445
+ // Git metadata is diagnostic-only; non-git targets still produce a usable verify summary.
446
+ return null;
447
+ }
448
+ }
449
+ const harnessRootExclude = process.env.RECIPE_HARNESS_ROOT_EXCLUDE;
450
+ if (!harnessRootExclude) throw new Error('RECIPE_HARNESS_ROOT_EXCLUDE is required');
451
+ const statusShort = runGit(['status', '--short', '--', '.', `:(exclude)${harnessRootExclude}`]);
452
+ const gitStatus = {
453
+ branch: runGit(['branch', '--show-current']),
454
+ head: runGit(['rev-parse', '--short', 'HEAD']),
455
+ dirtyCount: statusShort ? statusShort.split('\n').filter(Boolean).length : 0,
456
+ dirtyPreview: statusShort ? statusShort.split('\n').filter(Boolean).slice(0, 25) : [],
457
+ };
458
+ const readiness = parsedChecks.find((check) => check.name === 'live extension readiness');
459
+ const extensionIdPath = path.join(target, process.env.RECIPE_RUNTIME_DIR, 'extension.id');
460
+ let markerExtensionId = null;
461
+ try {
462
+ const value = fs.readFileSync(extensionIdPath, 'utf8').trim();
463
+ if (/^[a-p]{32}$/.test(value)) markerExtensionId = value;
464
+ } catch {}
465
+ const cdpTarget = readinessReport?.cdp ? {
466
+ selectedExtensionId: readinessReport.cdp.selectedExtensionId || null,
467
+ markerExtensionId,
468
+ markerMatched: readinessReport.cdp.markerMatched ?? null,
469
+ markerRepaired: readinessReport.cdp.markerRepaired ?? null,
470
+ extensionIds: readinessReport.cdp.extensionIds || [],
471
+ targetCount: readinessReport.cdp.targetCount ?? null,
472
+ browser: readinessReport.cdp.browser || cdpHolder?.browser || null,
473
+ } : null;
474
+ const runtimeOwner = liveMode === 'static-only'
475
+ ? 'static-only'
476
+ : liveMode === 'missing-cdp'
477
+ ? 'none'
478
+ : cdpHolder?.cdpReachable
479
+ ? (readiness?.status === 'pass' ? 'compatible-external-or-harness' : 'incompatible-external-or-stale')
480
+ : 'none';
481
+ fs.writeFileSync(path.join(artifacts, 'summary.json'), `${JSON.stringify({
482
+ adapter: 'extension',
483
+ status,
484
+ liveMode,
485
+ runtimeClassification: {
486
+ runtimeOwner,
487
+ recipeControllable: readiness?.status === 'pass',
488
+ startedByVerify: false,
489
+ },
490
+ cleanupOwnership: {
491
+ mayStop: false,
492
+ reason: 'extension verify inspects the supplied CDP runtime; wrapper/preflight ownership must be recorded by the caller before stopping processes',
493
+ },
494
+ gitStatus,
495
+ distFreshness,
496
+ runtimePolicy: {
497
+ runtimeReusePolicy: 'reuse a running harness-compatible CDP target when possible; wrapper auto-start must use a cached/watch-only prepare path unless the human explicitly permits a rebuild',
498
+ },
499
+ fixtureStatus,
500
+ cdpHolder,
501
+ cdpTarget,
502
+ checks: parsedChecks,
503
+ generatedAt: new Date().toISOString(),
504
+ }, null, 2)}\n`);
505
+ fs.writeFileSync(path.join(artifacts, 'artifact-manifest.json'), `${JSON.stringify({
506
+ artifacts: fs.readdirSync(artifacts).map((name) => ({ path: name })),
507
+ }, null, 2)}\n`);
508
+ NODE
509
+
510
+ echo "Extension harness verify $status: $ARTIFACTS/summary.json"
511
+ [ "$status" = "pass" ]