@deeeed/metamask-harness 0.83.0 → 0.85.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 +62 -0
- package/README.md +8 -1
- package/adapters/extension/console-tail.mjs +11 -7
- package/adapters/extension/ensure-browser.sh +17 -18
- package/adapters/manifest.json +14 -110
- package/adapters/mobile/start-console-forwarder.sh +8 -0
- package/adapters/shared/resolve-slot-ports-core.mjs +48 -6
- package/adapters/shared/session-lock.mjs +43 -0
- package/adapters/web-dapp/metamask-signer.mjs +212 -0
- package/dist/adapters/extension/actions.js +5 -0
- package/dist/adapters/extension/console-capture.js +18 -0
- package/dist/adapters/extension/fixtures.js +2 -0
- package/dist/adapters/extension/overlay.js +5 -4
- package/dist/adapters/extension/run.js +4 -2
- package/dist/adapters/extension/runtime.js +73 -18
- package/dist/adapters/mobile/devices.js +5 -1
- package/dist/adapters/mobile/launch.js +19 -2
- package/dist/adapters/mobile/provision.js +4 -2
- package/dist/adapters/mobile/run.js +3 -2
- package/dist/adapters/mobile/surface.js +1 -2
- package/dist/adapters/mobile/verify.js +15 -7
- package/dist/adapters/plugin-surface.js +14 -42
- package/dist/adapters/session-lock.js +23 -0
- package/dist/adapters/web-dapp/actions.js +7 -5
- package/dist/adapters/web-dapp/console-capture.js +2 -20
- package/dist/adapters/web-dapp/run.js +25 -24
- package/dist/adapters/web-dapp/surface.js +32 -108
- package/dist/adapters.js +5 -3
- package/dist/command-contract.js +1351 -643
- package/dist/commands/check.js +5 -7
- package/dist/commands/debug.js +2 -2
- package/dist/commands/doctor.js +3 -4
- package/dist/commands/domain.js +4 -3
- package/dist/commands/fixtures.js +3 -3
- package/dist/commands/help.js +6 -5
- package/dist/commands/launch/device.js +1 -1
- package/dist/commands/launch/extension.js +9 -7
- package/dist/commands/logs.js +2 -2
- package/dist/commands/prepare.js +4 -4
- package/dist/commands/provision.js +2 -2
- package/dist/commands/review.js +8 -10
- package/dist/commands/runtime-launch.js +2 -0
- package/dist/commands/status.js +4 -2
- package/dist/commands/task-init.js +2 -3
- package/dist/console-classifier.js +3 -2
- package/dist/doctor.js +11 -4
- package/dist/mm-harness-cli.js +241 -1210
- package/dist/paths.js +9 -4
- package/dist/recipe-security.js +1 -1
- package/dist/review/knowledge.js +10 -2
- package/dist/runner.js +18 -3
- package/docs/CONTRIBUTING.md +14 -4
- package/docs/RECIPES.md +14 -1
- package/docs/cli-contract.md +8 -7
- package/docs/feature-guard.md +1 -1
- package/library/actions/extension/app/assert_no_console_errors.mjs +95 -0
- package/library/actions/extension/platform/cdp.mjs +26 -0
- package/library/actions/mobile/app/assert_no_console_errors.mjs +129 -0
- package/library/actions/shared/console-assert.mjs +174 -0
- package/library/actions/shared/console-collector.mjs +11 -4
- package/library/actions/shared/console-findings.mjs +108 -7
- package/library/actions/web-dapp/app/assert_no_console_errors.mjs +10 -54
- package/library/actions/web-dapp/app/launch.mjs +1 -1
- package/library/actions/web-dapp/platform/adapter-web-lib.mjs +18 -0
- package/library/actions/web-dapp/platform/hud.mjs +3 -10
- package/library/actions/web-dapp/platform/page.mjs +63 -18
- package/library/actions/web-dapp/platform/runtime.mjs +30 -179
- package/library/diagnostics/console-allowlist.json +13 -0
- package/library/manifests/extension.action-manifest.json +36 -0
- package/library/manifests/mobile.action-manifest.json +36 -0
- package/library/manifests/web-dapp.action-manifest.json +1 -1
- package/library/recipes/core/health/check.recipe.json +40 -0
- package/library/recipes/extension/health/check.recipe.json +71 -0
- package/library/recipes/mobile/health/check.recipe.json +71 -0
- package/package.json +9 -9
- package/scripts/completions.sh +1 -1
- package/site/assets/help-recipes.json +2 -1
- package/adapters/web-dapp/cleanup.mjs +0 -69
- package/adapters/web-dapp/inject.mjs +0 -75
- package/adapters/web-dapp/launch.mjs +0 -631
- package/adapters/web-dapp/lib/blocked-hosts.mjs +0 -24
- package/adapters/web-dapp/lib/cdp-client.mjs +0 -84
- package/adapters/web-dapp/lib/display.mjs +0 -115
- package/adapters/web-dapp/lib/hud.mjs +0 -148
- package/adapters/web-dapp/lib/policy-fence.mjs +0 -119
- package/adapters/web-dapp/lib/policy.mjs +0 -84
- package/adapters/web-dapp/lib/processes.mjs +0 -187
- package/adapters/web-dapp/lib/readiness.mjs +0 -272
- package/adapters/web-dapp/stop.mjs +0 -38
- package/adapters/web-dapp/verify.mjs +0 -51
- package/adapters/web-dapp/wallet-host.mjs +0 -520
- package/dist/adapters/web-dapp/doctor-checks.js +0 -33
- package/dist/adapters/web-dapp/launch.js +0 -87
- package/dist/adapters/web-dapp/overlay.js +0 -21
- package/dist/adapters/web-dapp/readiness.js +0 -10
- package/dist/adapters/web-dapp/stop.js +0 -20
- package/dist/cli.js +0 -202
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,68 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.85.0 - 2026-10-09
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `health.check` recipes for Mobile, Extension and Core: a read-only readiness check to run before ticket work. A failed precondition stops at a node named after its error (for example `WALLET_LOCKED`, `NOT_ON_HOME` or, on Mobile and Extension, `CONSOLE_ERRORS`), whose intent gives the recovery command. `mm-harness help` lists it under the target step.
|
|
10
|
+
- `metamask.app.assert_no_console_errors` on Mobile and Extension, as on `web-dapp`: it fails on a console error or exception that no console allowlist covers, judging the events since the launch began by their own timestamps, and fails as unavailable unless the capture is proven (the slot's console forwarder or collector is running and a control line logged in the app or MetaMask reaches the log). `launch` records when it starts, and console lines replayed from before then (by an app runtime or browser the launch kept) are left out. Mobile reads `app-console.log`, which every launch now restarts empty along with the forwarder's replay cursor, and judges only the device the bridge selects; Extension reads `extension-console.log` from the offset `launch` records, and its collector now stamps each line with the event's own time (UTC, with milliseconds). Known app noise is accepted only when it also appears on the app's unchanged base build: the bundled library ships `diagnostics/console-allowlist.json` (the Extension unlock page's `UNSAFE_componentWillMount` warning), read ahead of other libraries' by these actions and by run diagnostics, and the classifier carries pattern rules for base-reproduced errors one exact entry cannot cover: Mobile's SES lockdown intrinsic reports and its notifications startup failure (`createOnChainTriggers`), whose message carries the bundle line; Extension's `getBearerToken` while locked (MetaMask/metamask-extension#46688) and the Money query adapter's `mutations` TypeError (TAT-4063), whose stacks name build-specific chunks. Allowlist entries can name the `app` source Mobile lines carry, and Mobile console lines match without the forwarder's time and device prefix.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **BREAKING:** `run --hud` takes `show` or `hide`, default `show`. `auto` is gone: the CLI refuses it as an invalid choice (`--hud must be show or hide; received 'auto'.`); drop the flag to get the default. The recipe HUD, each node's intent, now draws on every adapter that implements `app.hud`, headless browsers included. A headless web-dapp (Terminal) browser gets it where `auto` skipped it; Mobile and Extension already drew it by default. A Mobile release artifact still runs without it, since it has no dev bridge to draw with. `METAMASK_RECIPE_AUTO_HUD=0` stays the internal off switch for self-test, verify and the test sandboxes, and `--hud show` wins over it.
|
|
15
|
+
- **BREAKING:** web-dapp `ui.screenshot` keeps the HUD in the capture (it hid it while capturing), so Terminal screenshot evidence shows the node's intent as Mobile and Extension evidence does. `--hud hide` is the way to a HUD-free screenshot.
|
|
16
|
+
- A `surface=wallet` web-dapp `ui.screenshot` (the MetaMask confirmation window or side panel) shows the running node's intent too: the HUD is a bar docked to that MetaMask page's bottom edge for the capture only, which reserves its own height so the request and the footer buttons move up rather than being covered (pointer-events none), and is removed after it.
|
|
17
|
+
- A run with the HUD off (`--hud hide`, a `call`, an untrusted source) first clears the HUD an earlier run left on the slot, so its screenshots never show a previous recipe's intent: web-dapp deletes `hud.json` (the wallet host redraws from it) and the drawn HUD on the app tabs; Extension removes the runner HUD from its pages. Nothing is launched to do it, `METAMASK_RECIPE_AUTO_HUD=0` skips it, and a failed clear prints one `WARN:` line on stderr.
|
|
18
|
+
- Inside a checkout, every command resolves its adapter, target and slot once, through recipe-cli's context resolver, so `status`, `doctor`, `run` and the rest need no `--adapter`. The order is: flag, then the checkout's runtime binding, then its slot, then a unique detect match among the built-ins and the operator's library plugins (matched by their declared `detect`, before any plugin code loads).
|
|
19
|
+
- A checkout that more than one adapter matches stops with `ADAPTER_AMBIGUOUS` (exit 2) and lists the candidates. With no match, a command keeps its "could not detect the MetaMask repo type" error.
|
|
20
|
+
- `status`, `doctor`, `run`, `call` and discovery (`actions`, `run --list`/`--describe`, `call --list`) carry `context` in their `--json` envelopes (each value and its source), as does a command's own `USAGE` error once its context has resolved. Grammar refusals (raised before resolution), the task view (`status --task`/`--watch`) and the other commands' envelopes, `launch` on every adapter included, are unchanged. Human output adds one `context:` line on stderr when the adapter was not given by a flag.
|
|
21
|
+
- Terminal detection needs a recipe-terminal library that declares `detect` (recipe-terminal #8, 8a44aee); with an older library, pass `--adapter terminal`.
|
|
22
|
+
- `status` accepts `--adapter` and `--platform` (it refused them with `CLI_UNKNOWN_OPTION`).
|
|
23
|
+
- The slot comes from mm-harness's own pool resolution (the configured Farmslot root, else the nearest Farmslot checkout's `pool/`) unless `FARMSLOT_POOL_DIR` or `FARMSLOT_ROOT` names one, so a slot checkout beside a Farmslot checkout resolves its slot, and its ports fill `--cdp-port`/`--watcher-port`, with no env.
|
|
24
|
+
- Commands no longer detect the adapter themselves: status, doctor, check, provision, prepare, help, task init, `verify`'s overlay, debug, logs, fixtures, review and domain all read the resolved one. `check` still refuses `--platform ios|android`.
|
|
25
|
+
- `review` and `domain` scope to the resolved adapter when review covers it (mobile, extension, core, terminal), else run unscoped. Only an explicit `--adapter` that names another adapter is refused.
|
|
26
|
+
- Depend on `@farmslot/recipe-cli` 0.11.0 and `@farmslot/adapter-sdk` 0.8.0 (were 0.10.0 and 0.7.1) for the context resolver (deeeed/farmslot#864), on `@farmslot/recipe-runner` 0.28.0, `@farmslot/protocol` 0.35.0 and `@farmslot/agent-runtime` 0.17.1 (were 0.27.0, 0.34.0 and 0.17.0), and on `@farmslot/adapter-web` 0.6.4, `@farmslot/adapter-rn` 0.19.3 and `@farmslot/adapter-node` 0.1.6 (were 0.6.2, 0.19.1 and 0.1.4). With them, a web-dapp launch stops waiting for CDP when its browser dies (deeeed/farmslot#865). The install holds one `@farmslot/adapter-sdk` and one `@farmslot/recipe-runner`.
|
|
27
|
+
- Help lists the selectable adapters (the registered ones, then those the operator's libraries and the invocation's `--library` entries declare) wherever it names `--adapter` or `--platform`, instead of a fixed `mobile|extension|core|terminal`.
|
|
28
|
+
- **BREAKING:** Mobile `verify` takes `--watcher-port` (the public `verify` grammar accepted it; the Mobile leaf refused it) and reads `RECIPE_WATCHER_PORT`: the Metro port is `--watcher-port`, then `RECIPE_WATCHER_PORT`, then `WATCHER_PORT`, then the slot's `.js.env`. With no port env set, a checkout the slot pool maps gets the pool slot's port through `WATCHER_PORT`, so that port now wins over the checkout's `.js.env` `WATCHER_PORT`.
|
|
29
|
+
- **BREAKING:** A `RECIPE_RUNTIME_CONTEXT` that belongs to another checkout no longer sets the Extension slot's ports (`CDP_PORT`, `WATCHER_PORT`, `SLOT_ID`), Extension `verify`'s `RECIPE_SLOT_ID` and `RECIPE_HARNESS_EXTENSION_ID` (it reads the checkout's own context instead), or the Mobile provision slot (simulator, slot id). It counts when its `repoRoot` is the checkout's Git top level (so a clone nested inside another checkout never takes that checkout's context), or, without a `repoRoot`, when the file sits inside the checkout: the rule recipe-cli applies to the runtime binding. A context outside every checkout that names no `repoRoot` used to be authoritative; an orchestrator that keeps the file elsewhere sets `repoRoot`.
|
|
30
|
+
- Contributors: `bash tests/contract/run.sh` runs half the cores' worth of tests at once (`-j N`), each in its own process group, `TMPDIR` and output file, and prints each test's duration and the 15 slowest. A test gets 60 s (`CONTRACT_TEST_TIMEOUT`, or `# contract: timeout=<s>` in its header) and fails as `timed out`; what it leaves running is killed and named on a `LEAK` line; a test that rebuilds `dist/` declares `# contract: serial`. An unfiltered run over `CONTRACT_BUDGET` (180 s) fails, and a stopped run first lets each running test clean up (its EXIT trap reaps the leaves its CLI detached), then kills every process group left in the test's tree. A process a test leaves behind can no longer stall the run.
|
|
31
|
+
- Contributors: `yarn test:goldens` runs its families through `run.sh` and fails over `GOLDENS_BUDGET` (120 s); a golden case gets 60 s (`GOLDENS_CASE_TIMEOUT`, was 600 s). Docs: CONTRIBUTING and cli-contract describe both.
|
|
32
|
+
- Contributors: `AGENTS.md` has a `## Test speed` section: the default and CI suite must take 5 minutes or less (the contract and goldens runs check their own budgets; build and unit are not checked), and a new or changed test uses no fixed waits of 1 s or more, moves to `tests/live/` when it takes over 5 s (a golden case over 30 s), tears down every background process, and never rebuilds `dist/` under concurrent readers.
|
|
33
|
+
- Site: the platform map (`site/platform.html`) is a static page instead of the zoomable canvas. It shows the five layers (Libraries, MetaMask preset, Adapters, CLI, Engine) with their packages, one `mm-harness run` traced through them, and who changes what. It renders from `file://` (no `fetch`), so `site/assets/platform.json` is removed.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- Extension reopen (`adapters/extension/ensure-browser.sh`) no longer builds its `node -e` script or its `python3` profile cleanup from the slot id, checkout, runtime, profile, extension and runner paths, CDP port, webpack pid or wallet fixture path. The scripts are fixed text and get those values as argv. Before, a checkout path with an apostrophe (`/tmp/Arthur's checkout`) broke the reopen script's syntax and silently skipped the profile cleanup, and a crafted value could change the code the reopen ran.
|
|
38
|
+
|
|
39
|
+
## 0.84.0 - 2026-10-08
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- `mm-harness` is the MetaMask preset over `@farmslot/recipe-cli`'s `createHarnessCli`: the CLI program, help, the strict command grammar, library hydration, adapter loading and dispatch come from recipe-cli; mm-harness supplies its commands, handlers and help text. Apart from the removals under Removed and the `--json-stream` refusal change below, every command, flag, help text and JSON envelope is unchanged.
|
|
44
|
+
- `mm-harness --version` (and `-v`) still prints only the harness version. `mm-harness --version --verbose` adds a second line, `@farmslot/recipe-cli <version>`, the recipe-cli it runs on, and `doctor --json` reports it as `runner.recipeCliVersion`.
|
|
45
|
+
- `src/command-contract.ts` holds the one public command table (`PUBLIC_COMMANDS`: name, aliases, summary, example, help text, grammar) and `HIDDEN_COMMANDS`; `src/cli.ts` is gone.
|
|
46
|
+
- `mm-harness --help -- --help` prints the grouped help (it printed an older usage text), and a failure inside `update`, `pin`, `help`, `tutorial`, `setup-base`, `completions` or `call <action> --help` prints its message and exits with its code instead of a stack trace.
|
|
47
|
+
- Docs: the site's tutorial template snapshots (`site/assets/template-examples.json`) are regenerated from the recipe-cook templates at Consensys/skills b115bd2, which pass `call` inputs as bare `key=value` (`call app.lifecycle command=restart`) now that `call --arg` is refused.
|
|
48
|
+
- Docs: the README's Operate section and the feature-guard machine rules say a headful run, call or web-dapp launch on a locked macOS session refuses with `SESSION_LOCKED` (and how to proceed).
|
|
49
|
+
- `run` and `call` apply `--runtime-dir` before the slot resolves, as `doctor` does: the slot context, the checkout lock and the run's runtime state come from that directory. The flag was accepted and ignored.
|
|
50
|
+
- `call` never loads a funded mutation from its command line (it never accepted the `--funding-*` flags); funded mutations run through `run`.
|
|
51
|
+
- Depend on `@farmslot/recipe-cli` 0.10.0 and `@farmslot/adapter-sdk` 0.7.1 (were 0.8.0 and 0.6.0): `createHarnessCli`'s `afterAdapterLoad` and `explainInvalidChoice` hooks (deeeed/farmslot#846), which the preset uses for the web-dapp policy fence and the missing Terminal library, one `--watcher-port` (#806) and key=value-only `call` inputs (#808). Also `@farmslot/recipe-runner` 0.27.0, `@farmslot/adapter-web` 0.6.2, `@farmslot/adapter-rn` 0.19.1 and `@farmslot/adapter-node` 0.1.4 (were 0.26.0, 0.4.1, 0.18.0 and 0.1.2). `@farmslot/agent-runtime` 0.17.0 and `@farmslot/protocol` 0.34.0 are unchanged, and the install holds one `@farmslot/adapter-sdk` and one `@farmslot/recipe-runner`. With them, `launch` prints its setup stage on stderr (a line at least every 15 s, and a no-progress notice after 60 s); `run`, `run --plan` and `call` refuse `--record-video` on an adapter without recording before anything runs (`RECORDING_UNSUPPORTED`, exit 2); and `run` lists evidence an action produced through a fallback provider (`fallbacks` in `--json` and the `--json-stream` completion, a marked line in human output).
|
|
52
|
+
- Under `--json-stream`, a refused adapter plugin (a loader or venue-policy refusal) prints NDJSON on stdout, an `error` event then `complete` with `status: fail`, where 0.83.0 printed the human line on stderr. Human and `--json` output and the exit code (2) are unchanged.
|
|
53
|
+
- `call` writes its artifacts by `run`'s rule: inside a task (`RECIPE_TASK_DIR`/`FARMSLOT_TASK_DIR`) under `<task>/artifacts/calls/<action>-<uuid>`, else `temp/recipe/calls/<action>-<uuid>`; `--artifacts-dir` still wins.
|
|
54
|
+
- The built-in `web-dapp` is adapter-web's `createWebDappAdapter` (`@farmslot/adapter-web/web-dapp`): the slot browser, wallet host, venue policy and its fence, readiness and the launch, verify, stop, inject and cleanup leaves moved there. MetaMask is its extension signer (`adapters/web-dapp/metamask-signer.mjs`: dist and fixture import, seeding, confirmation surfaces, the two readiness checks, and the MetaMask identity of the injected wallet), loaded by every web-dapp process through `RECIPE_WEB_DAPP_SIGNER_MODULE`. The launch, verify, stop, install and cleanup flags and runtime files are unchanged.
|
|
55
|
+
|
|
56
|
+
### Removed
|
|
57
|
+
|
|
58
|
+
- **BREAKING:** the web-dapp leaf scripts (`adapters/web-dapp/{launch,verify,stop,inject,cleanup,wallet-host}.mjs`) and `adapters/web-dapp/lib/*` (policy, policy fence, readiness, processes, display, HUD, CDP client, blocked hosts) are no longer in the package. They live in `@farmslot/adapter-web/web-dapp`: use its `webDappPolicy` (same name; `POLICY_VERSION` is no longer exported, and the loader enforces the version) and `createWebDappAdapter`. `mm-harness launch|verify|stop` for web-dapp targets are unchanged. recipe-terminal's own `checks/terminal-policy.test.mjs` imports the old path before its #7 (d3a98d2).
|
|
59
|
+
- **BREAKING:** `--metro-port` is gone from `run`, `call`, `doctor`, `launch` and `verify`; pass `--watcher-port`. The CLI answers `--metro-port` with `Did you mean '--watcher-port'`. `doctor`'s `metroPort` runtime-context row reads `mirrors watcherPort`.
|
|
60
|
+
- **BREAKING:** `call` no longer takes `--arg k=v`; pass action inputs as `key=value` (the documented form, which carries every value `--arg` did, leading `-`/`--` included). The CLI answers `--arg` with `Did you mean 'key=value'`. The Mobile stale-source next step is `mm-harness call app.lifecycle command=restart …`.
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
|
|
64
|
+
- Unattended Extension and web-dapp (Terminal) runs on a Mac whose login session is locked fail fast with `SESSION_LOCKED` instead of hanging until a screenshot or unlock timeout. macOS stops painting windows while the session is locked; the probe reads the on-console session's `CGSSessionScreenIsLocked` from `ioreg` (bounded to 3 s). `run` and `call` refuse before they launch or drive a headful browser (exit 3; the Extension always, web-dapp unless its running slot browser, or else `TERMINAL_HEADLESS`, is headless; a saved `browser.json` counts only while its browser process is alive and this slot's), and `run --json-stream` carries `SESSION_LOCKED` as an `error` event ahead of `RUN_PREPARE_FAILED`. The web-dapp launcher refuses to start or reuse a headful browser on a locked session (`SESSION_LOCKED: …` in the launch error), so a recipe launch step can't start one either. The Extension health check reports a stalled compositor as `SESSION_LOCKED` rather than `UI_COMPOSITOR_SUSPENDED` when the session is locked. The next step says to unlock the Mac; caffeinate keeps it awake but does not unlock it. Nothing changes when the session is unlocked, off macOS, or when the lock state can't be read.
|
|
65
|
+
- Extension validation now stops a CDP listener that is this slot's own Chrome when no runtime identity proves it (for example after a reload loses CDP): a single Chrome listener whose `--user-data-dir` resolves, by real path, inside the checkout's recipe runtime dir gets the same TERM-then-KILL as an identity-proven one, after its command line is re-read just before the kill. Any other listener (a profile outside the checkout, a non-Chrome process, several listeners, or a command line whose `--user-data-dir` can't be parsed unambiguously) is still refused, and the refusal now names each pid and the `--user-data-dir` it saw, with the next step: stop that pid if it is this slot's browser, or free the port.
|
|
66
|
+
|
|
5
67
|
## 0.83.0 - 2026-10-07
|
|
6
68
|
|
|
7
69
|
### Added
|
package/README.md
CHANGED
|
@@ -113,6 +113,13 @@ Extension `launch` keeps its incremental watcher running. Refresh the active
|
|
|
113
113
|
page after a successful rebuild; use `launch --build` only for an explicit
|
|
114
114
|
production-like LavaMoat rebuild.
|
|
115
115
|
|
|
116
|
+
macOS stops painting visible windows while the login session is locked. On a locked Mac:
|
|
117
|
+
|
|
118
|
+
- a headful Extension or web-dapp (Terminal) `run`/`call` refuses with `SESSION_LOCKED` at its runtime check, before it launches or drives the browser, instead of stalling on screenshots and UI actions;
|
|
119
|
+
- a headful web-dapp launch, including reuse of a running browser, refuses the same way.
|
|
120
|
+
|
|
121
|
+
The Extension has no headless mode, so unlock the Mac. For the web-dapp, unlock, or relaunch its browser headless (`mm-harness launch --adapter terminal --headless`, or set `TERMINAL_HEADLESS=1` before launching): `TERMINAL_HEADLESS=1` alone doesn't help while a headful slot browser is still running. caffeinate keeps a Mac awake but does not unlock it.
|
|
122
|
+
|
|
116
123
|
### Extension browser
|
|
117
124
|
|
|
118
125
|
`RECIPE_HARNESS_BROWSER` picks the browser that hosts the unpacked extension:
|
|
@@ -259,7 +266,7 @@ how `prepare --remote-flag` keeps its pin through the fixtures step.
|
|
|
259
266
|
|
|
260
267
|
```bash
|
|
261
268
|
mm-harness call metamask.feature_flags.read
|
|
262
|
-
mm-harness call metamask.feature_flags.set
|
|
269
|
+
mm-harness call metamask.feature_flags.set 'flags={"perpsEnabled":true}' # mobile
|
|
263
270
|
mm-harness call metamask.feature_flags.clear # mobile
|
|
264
271
|
```
|
|
265
272
|
|
|
@@ -90,10 +90,14 @@ function out(event, destination = null) {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
// A record's time: the UTC time of day of the event's own timestamp (CDP
|
|
94
|
+
// Runtime.Timestamp, ms since the epoch), as the Mobile console forwarder
|
|
95
|
+
// writes it, or of the write when the event carries none. Events the browser
|
|
96
|
+
// replays when a collector attaches keep their own time, so
|
|
97
|
+
// metamask.app.assert_no_console_errors can leave out those from before a
|
|
98
|
+
// launch.
|
|
99
|
+
function stamp(timestamp) {
|
|
100
|
+
return new Date(Number.isFinite(timestamp) ? timestamp : Date.now()).toISOString().slice(11, 23);
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
function shortLabel(target) {
|
|
@@ -160,7 +164,7 @@ function emitConsole(label, params, destination) {
|
|
|
160
164
|
const level = String(params.type || 'log').toUpperCase();
|
|
161
165
|
const text = (params.args || []).map(previewArg).join(' ');
|
|
162
166
|
const [first, ...rest] = text.split('\n');
|
|
163
|
-
out(`${stamp()} [${label}] ${level} ${first}${atSuffix(frameText(params.stackTrace?.callFrames?.[0]))}${rest.length ? `\n${rest.join('\n')}` : ''}`, destination);
|
|
167
|
+
out(`${stamp(params.timestamp)} [${label}] ${level} ${first}${atSuffix(frameText(params.stackTrace?.callFrames?.[0]))}${rest.length ? `\n${rest.join('\n')}` : ''}`, destination);
|
|
164
168
|
}
|
|
165
169
|
|
|
166
170
|
function emitException(label, params, destination) {
|
|
@@ -172,7 +176,7 @@ function emitException(label, params, destination) {
|
|
|
172
176
|
'uncaught exception';
|
|
173
177
|
const frame = frameText(d.stackTrace?.callFrames?.[0]) || frameText({ url: d.url, lineNumber: d.lineNumber, columnNumber: d.columnNumber });
|
|
174
178
|
const [first, ...rest] = String(text).split('\n');
|
|
175
|
-
out(`${stamp()} [${label}] EXCEPTION ${first}${atSuffix(frame)}${rest.length ? `\n${rest.join('\n')}` : ''}`, destination);
|
|
179
|
+
out(`${stamp(params.timestamp)} [${label}] EXCEPTION ${first}${atSuffix(frame)}${rest.length ? `\n${rest.join('\n')}` : ''}`, destination);
|
|
176
180
|
}
|
|
177
181
|
|
|
178
182
|
// Browser log entries (network failures, interventions, deprecations) are
|
|
@@ -186,7 +190,7 @@ function emitLogEntry(label, params, destination) {
|
|
|
186
190
|
// A Log entry's location is its URL as is: a line number appended to a URL
|
|
187
191
|
// could not be told apart from the URL's own text.
|
|
188
192
|
const where = e.url ? e.url : frameText(e.stackTrace?.callFrames?.[0]);
|
|
189
|
-
out(`${stamp()} [${label}] [browser:${source}] ${level} ${e.text || ''}${atSuffix(where)}`, destination);
|
|
193
|
+
out(`${stamp(e.timestamp)} [${label}] [browser:${source}] ${level} ${e.text || ''}${atSuffix(where)}`, destination);
|
|
190
194
|
}
|
|
191
195
|
|
|
192
196
|
function handleMessage(label, destination, data) {
|
|
@@ -235,9 +235,10 @@ echo "[cleanup] Reusing profile ${PROFILE}"
|
|
|
235
235
|
# shutdowns; --load-extension re-advertises the path but does not flip it back,
|
|
236
236
|
# leaving MV3 service workers inert).
|
|
237
237
|
if [ -f "${PROFILE}/Default/Preferences" ]; then
|
|
238
|
-
python3 -
|
|
238
|
+
python3 - "${PROFILE}/Default/Preferences" 2>/dev/null <<'PY' && echo "[cleanup] Cleared crash recovery state + re-enabled extensions"
|
|
239
239
|
import json
|
|
240
|
-
|
|
240
|
+
import sys
|
|
241
|
+
pf = sys.argv[1]
|
|
241
242
|
prefs = json.load(open(pf))
|
|
242
243
|
prefs['profile'] = prefs.get('profile', {})
|
|
243
244
|
prefs['profile']['exit_type'] = 'Normal'
|
|
@@ -248,7 +249,7 @@ for ext_id, ext_prefs in ext_settings.items():
|
|
|
248
249
|
ext_prefs['state'] = 1
|
|
249
250
|
ext_prefs.pop('disable_reasons', None)
|
|
250
251
|
json.dump(prefs, open(pf, 'w'))
|
|
251
|
-
|
|
252
|
+
PY
|
|
252
253
|
fi
|
|
253
254
|
|
|
254
255
|
# Readiness gate: wait until webpack emits a loadable background entry before
|
|
@@ -289,10 +290,13 @@ echo "[reopen] ${SLOT_ID} — CDP:${CDP_PORT:-off}"
|
|
|
289
290
|
cd "${REPO}"
|
|
290
291
|
|
|
291
292
|
trap - EXIT
|
|
292
|
-
|
|
293
|
+
# The reopen script is fixed text; paths, ids and ports reach it as argv, never as source.
|
|
294
|
+
IFS= read -r -d '' REOPEN_SCRIPT <<'NODE' || true
|
|
295
|
+
const [SCRIPT_DIR, SLOT_ID, AGENT_DIR, EXTENSION, REPO, RUNNER_BIN, PROFILE, cdpPortArg, webpackPidArg, WALLET_FIXTURE] =
|
|
296
|
+
process.argv.slice(1);
|
|
293
297
|
const path = require('path');
|
|
294
298
|
const fs = require('fs');
|
|
295
|
-
const adapterWeb = (name) => require('module').createRequire(path.join(
|
|
299
|
+
const adapterWeb = (name) => require('module').createRequire(path.join(SCRIPT_DIR, 'package.json'))('@farmslot/adapter-web/' + name);
|
|
296
300
|
const http = require('http');
|
|
297
301
|
const { execFileSync } = require('child_process');
|
|
298
302
|
let chromium; try { chromium = require('@playwright/test').chromium; } catch { chromium = require('playwright').chromium; }
|
|
@@ -318,15 +322,9 @@ const {
|
|
|
318
322
|
resolveBrowser,
|
|
319
323
|
} = adapterWeb('browser-resolver');
|
|
320
324
|
|
|
321
|
-
const
|
|
322
|
-
const
|
|
323
|
-
const
|
|
324
|
-
const REPO = '${REPO}';
|
|
325
|
-
const RUNNER_BIN = '${RUNNER_BIN}';
|
|
326
|
-
const PROFILE = '${PROFILE}';
|
|
327
|
-
const CDP_PORT = '${CDP_PORT}' || null;
|
|
328
|
-
const WEBPACK_PID = '${WEBPACK_PID}' || null;
|
|
329
|
-
const walletFixture = JSON.parse(fs.readFileSync('${WALLET_FIXTURE}', 'utf8'));
|
|
325
|
+
const CDP_PORT = cdpPortArg || null;
|
|
326
|
+
const WEBPACK_PID = webpackPidArg || null;
|
|
327
|
+
const walletFixture = JSON.parse(fs.readFileSync(WALLET_FIXTURE, 'utf8'));
|
|
330
328
|
const PASSWORD = walletFixture.password;
|
|
331
329
|
|
|
332
330
|
if (!PASSWORD) {
|
|
@@ -522,8 +520,8 @@ const releaseFocusHold = () => {
|
|
|
522
520
|
await evaluatePageViaCdp(page, applyPersistentSlotTitle, { slotId: SLOT_ID, defaultTitle: 'MetaMask' });
|
|
523
521
|
|
|
524
522
|
// Wait for runtime state to settle, then unlock if needed.
|
|
525
|
-
const unlockSelector = '[data-testid
|
|
526
|
-
const homeSelector = '[data-testid
|
|
523
|
+
const unlockSelector = '[data-testid="unlock-password"]';
|
|
524
|
+
const homeSelector = '[data-testid="account-menu-icon"]';
|
|
527
525
|
let detectedState = 'unknown';
|
|
528
526
|
for (let i = 0; i < 15; i++) {
|
|
529
527
|
if (await page.locator(homeSelector).count()) {
|
|
@@ -544,7 +542,7 @@ const releaseFocusHold = () => {
|
|
|
544
542
|
if (detectedState === 'locked') {
|
|
545
543
|
console.log('[reopen] Unlocking wallet...');
|
|
546
544
|
await page.fill(unlockSelector, PASSWORD);
|
|
547
|
-
await page.click('[data-testid
|
|
545
|
+
await page.click('[data-testid="unlock-submit"]');
|
|
548
546
|
await page.waitForSelector(homeSelector, { timeout: 30000 });
|
|
549
547
|
console.log('[reopen] Unlocked');
|
|
550
548
|
} else if (detectedState === 'onboarding') {
|
|
@@ -611,4 +609,5 @@ const releaseFocusHold = () => {
|
|
|
611
609
|
console.error(e);
|
|
612
610
|
process.exit(1);
|
|
613
611
|
});
|
|
614
|
-
|
|
612
|
+
NODE
|
|
613
|
+
exec node -e "$REOPEN_SCRIPT" "$SCRIPT_DIR" "$SLOT_ID" "$AGENT_DIR" "$EXTENSION" "$REPO" "$RUNNER_BIN" "$PROFILE" "$CDP_PORT" "$WEBPACK_PID" "$WALLET_FIXTURE"
|
package/adapters/manifest.json
CHANGED
|
@@ -498,6 +498,14 @@
|
|
|
498
498
|
"inputs": "ESM import; env RECIPE_HARNESS_ROOT optional override",
|
|
499
499
|
"outputs": "recipeHarnessRoot() relative path string"
|
|
500
500
|
},
|
|
501
|
+
{
|
|
502
|
+
"id": "lib/session-lock",
|
|
503
|
+
"entry": "adapters/shared/session-lock.mjs",
|
|
504
|
+
"kind": "lib",
|
|
505
|
+
"purpose": "Bounded macOS locked-session probe (ioreg CGSSessionScreenIsLocked) and the SESSION_LOCKED wording, shared by the run/call preflight and the Extension health check.",
|
|
506
|
+
"inputs": "ESM import from src/adapters/session-lock.ts or src/adapters/extension/runtime.ts",
|
|
507
|
+
"outputs": "macosSessionLocked() true/false/null; parseSessionLocked(); SESSION_LOCKED_MESSAGE and SESSION_LOCKED_USER_ACTION"
|
|
508
|
+
},
|
|
501
509
|
{
|
|
502
510
|
"id": "lib/resolve-slot-ports-sh",
|
|
503
511
|
"entry": "adapters/shared/resolve-slot-ports.sh",
|
|
@@ -592,122 +600,18 @@
|
|
|
592
600
|
"outputs": "numbered checkouts, saved preferences, and run summary"
|
|
593
601
|
},
|
|
594
602
|
{
|
|
595
|
-
"id": "web-dapp/
|
|
596
|
-
"entry": "adapters/web-dapp/
|
|
597
|
-
"kind": "node",
|
|
598
|
-
"purpose": "Start or reuse the web-dapp slot browser (Chrome for Testing unless TERMINAL_CHROME_BIN; MetaMask extension with the fixture imported, or the injected strict wallet) and its wallet host. On macOS the browser starts through LaunchServices with no startup window; in testnet mode the hosts the venue policy blocks are unresolvable for the whole browser.",
|
|
599
|
-
"inputs": "--target --cdp-port --app-port [--signer extension|injected] [--account] [--fresh-profile] [--headless|--headful] [--slow-mo] [--network mainnet --mainnet-confirmation REAL_FUNDS] [--json]; env TERMINAL_CHROME_BIN, TERMINAL_EXTENSION_DIR, TERMINAL_EXTENSION_CHECKOUT, RECIPE_WALLET_FIXTURE, TERMINAL_HEADLESS, TERMINAL_SLOW_MO, TERMINAL_WINDOW, TERMINAL_SCREEN",
|
|
600
|
-
"outputs": "<runtime>/<adapter id>/browser.json + pid/log files; JSON on stdout; exit 0/1/2"
|
|
601
|
-
},
|
|
602
|
-
{
|
|
603
|
-
"id": "web-dapp/wallet-host",
|
|
604
|
-
"entry": "adapters/web-dapp/wallet-host.mjs",
|
|
605
|
-
"kind": "node",
|
|
606
|
-
"purpose": "Long-running CDP observer: opens the app tab blank, hooks it once per target, navigates it to the app and verifies the log binding before ready; logs every wallet request from the app top frame and every MetaMask confirmation shown; answers the injected strict wallet in injected mode (other frames and blank popups get 4100). The page script, the frame judgement and the strict wallet are @farmslot/adapter-web/dapp's; the venue policy (RECIPE_WEB_DAPP_POLICY) gives the testnet refusal, hosts, probe and start page.",
|
|
607
|
-
"inputs": "--cdp-port --runtime-dir --signer --app-origin [--extension-id] [--target --account --start-chain] [--browser-app <.app>]",
|
|
608
|
-
"outputs": "<runtime>/<adapter id>/wallet-requests.jsonl, wallet-host.ready"
|
|
609
|
-
},
|
|
610
|
-
{
|
|
611
|
-
"id": "web-dapp/stop",
|
|
612
|
-
"entry": "adapters/web-dapp/stop.mjs",
|
|
613
|
-
"kind": "node",
|
|
614
|
-
"purpose": "Stop the web-dapp slot browser, wallet host and console collector and prove the CDP port is free.",
|
|
615
|
-
"inputs": "--target [--cdp-port]",
|
|
616
|
-
"outputs": "JSON on stdout; exit 0/1"
|
|
617
|
-
},
|
|
618
|
-
{
|
|
619
|
-
"id": "web-dapp/inject",
|
|
620
|
-
"entry": "adapters/web-dapp/inject.mjs",
|
|
621
|
-
"kind": "node",
|
|
622
|
-
"purpose": "web-dapp install: overlay marker, runtime dir and wallet fixture (0600); refuses a checkout the venue policy does not match; no product writes.",
|
|
623
|
-
"inputs": "--target [--fixture] [--force]; env RECIPE_WALLET_FIXTURE",
|
|
624
|
-
"outputs": "<harness>/<adapter id>/install.json, <runtime>/wallet-fixture.json; exit 0/1"
|
|
625
|
-
},
|
|
626
|
-
{
|
|
627
|
-
"id": "web-dapp/cleanup",
|
|
628
|
-
"entry": "adapters/web-dapp/cleanup.mjs",
|
|
629
|
-
"kind": "node",
|
|
630
|
-
"purpose": "web-dapp cleanup: stop browser/host, remove the slot's runtime state, overlay marker and installed fixture; profiles kept unless --reset-profile.",
|
|
631
|
-
"inputs": "--target [--cdp-port] [--reset-profile]",
|
|
632
|
-
"outputs": "JSON on stdout; exit 0/1"
|
|
633
|
-
},
|
|
634
|
-
{
|
|
635
|
-
"id": "web-dapp/verify",
|
|
636
|
-
"entry": "adapters/web-dapp/verify.mjs",
|
|
637
|
-
"kind": "node",
|
|
638
|
-
"purpose": "Read-only web-dapp slot readiness: checkout, deps, dev server, forced testnet, fixture, browser start probe, extension copy, CDP port.",
|
|
639
|
-
"inputs": "--target [--cdp-port] [--watcher-port] [--signer] [--account] [--json]",
|
|
640
|
-
"outputs": "readiness report; exit 0 ready / 1 not ready"
|
|
641
|
-
},
|
|
642
|
-
{
|
|
643
|
-
"id": "web-dapp/lib-cdp-client",
|
|
644
|
-
"entry": "adapters/web-dapp/lib/cdp-client.mjs",
|
|
645
|
-
"kind": "lib",
|
|
646
|
-
"purpose": "Browser-level CDP client with flattened target sessions for the wallet host.",
|
|
647
|
-
"inputs": "CdpClient.connect(url)",
|
|
648
|
-
"outputs": "send/on"
|
|
649
|
-
},
|
|
650
|
-
{
|
|
651
|
-
"id": "web-dapp/lib-processes",
|
|
652
|
-
"entry": "adapters/web-dapp/lib/processes.mjs",
|
|
653
|
-
"kind": "lib",
|
|
654
|
-
"purpose": "Pid-file process stop with owner-marker checks (--mm-harness-owner=<kind>-<id>, exact token; live foreign pids kept and reported, dead ones dropped), process-group termination, CDP port release check.",
|
|
655
|
-
"inputs": "stopWebDappBrowser(target, { cdpPort }), stopConsoleCollector(target), ownerMarker(kind, runtimeDir), ownsProcess(kind, pid, runtimeDir)",
|
|
656
|
-
"outputs": "stopped processes"
|
|
657
|
-
},
|
|
658
|
-
{
|
|
659
|
-
"id": "web-dapp/lib-readiness",
|
|
660
|
-
"entry": "adapters/web-dapp/lib/readiness.mjs",
|
|
661
|
-
"kind": "lib",
|
|
662
|
-
"purpose": "web-dapp slot readiness checks shared by verify, doctor and the surface.",
|
|
663
|
-
"inputs": "webDappReadiness({ policy, target, appPort, cdpPort, signer }), assertLaunchNetwork(target, { network, mainnetConfirmation }), devServerTestnetDiagnostic(target, { appPort, variable }) (advisory)",
|
|
664
|
-
"outputs": "checks[]"
|
|
665
|
-
},
|
|
666
|
-
{
|
|
667
|
-
"id": "web-dapp/lib-display",
|
|
668
|
-
"entry": "adapters/web-dapp/lib/display.mjs",
|
|
669
|
-
"kind": "lib",
|
|
670
|
-
"purpose": "Headless/headful and slow-mo resolution, per-slot window placement and the profile zoom that keeps the desktop layout.",
|
|
671
|
-
"inputs": "resolveHeadless, resolveSlowMo, windowPlacement, writeProfileZoom",
|
|
672
|
-
"outputs": "browser display args"
|
|
673
|
-
},
|
|
674
|
-
{
|
|
675
|
-
"id": "web-dapp/lib-hud",
|
|
676
|
-
"entry": "adapters/web-dapp/lib/hud.mjs",
|
|
677
|
-
"kind": "lib",
|
|
678
|
-
"purpose": "Recipe HUD state (<runtime>/hud.json) and the page expression that draws it on the app origin only.",
|
|
679
|
-
"inputs": "nextHudState, hudRenderExpression",
|
|
680
|
-
"outputs": "HUD state, expression source"
|
|
681
|
-
},
|
|
682
|
-
{
|
|
683
|
-
"id": "web-dapp/lib-blocked-hosts",
|
|
684
|
-
"entry": "adapters/web-dapp/lib/blocked-hosts.mjs",
|
|
685
|
-
"kind": "lib",
|
|
686
|
-
"purpose": "Testnet enforcement mechanics: the host-resolver rule that makes the policy's blocked hosts unresolvable, and the canonical host check the wallet host's request block uses.",
|
|
687
|
-
"inputs": "hostResolverRules(hosts), hostOf(url), isBlockedUrl(url, hosts)",
|
|
688
|
-
"outputs": "launch switch, booleans"
|
|
689
|
-
},
|
|
690
|
-
{
|
|
691
|
-
"id": "web-dapp/lib-policy",
|
|
692
|
-
"entry": "adapters/web-dapp/lib/policy.mjs",
|
|
693
|
-
"kind": "lib",
|
|
694
|
-
"purpose": "The venue policy of the adapter that extends web-dapp (e.g. the Terminal plugin's), loaded from the module RECIPE_WEB_DAPP_POLICY names; the host sets it when it binds that adapter.",
|
|
695
|
-
"inputs": "env RECIPE_WEB_DAPP_POLICY",
|
|
696
|
-
"outputs": "webDappPolicy(), webDappAdapterId()"
|
|
697
|
-
},
|
|
698
|
-
{
|
|
699
|
-
"id": "web-dapp/lib-policy-fence",
|
|
700
|
-
"entry": "adapters/web-dapp/lib/policy-fence.mjs",
|
|
603
|
+
"id": "web-dapp/metamask-signer",
|
|
604
|
+
"entry": "adapters/web-dapp/metamask-signer.mjs",
|
|
701
605
|
"kind": "lib",
|
|
702
|
-
"purpose": "
|
|
703
|
-
"inputs": "
|
|
704
|
-
"outputs": "
|
|
606
|
+
"purpose": "MetaMask as @farmslot/adapter-web web-dapp's extension signer (load the slot's MetaMask dist, import the fixture account through wallet-fixture-state.cjs generate, prefill-profile and seed-cdp, judge MetaMask's confirmation surfaces, readiness checks for the dist and fixture tooling) and the MetaMask identity the injected strict wallet presents.",
|
|
607
|
+
"inputs": "loaded by the web-dapp leaves through RECIPE_WEB_DAPP_SIGNER_MODULE or --signer-module; env TERMINAL_EXTENSION_DIR, TERMINAL_EXTENSION_CHECKOUT, TERMINAL_EXTENSION_NODE",
|
|
608
|
+
"outputs": "signers.extension (prepareProfile, confirm, readinessChecks) and signers.injected.identity"
|
|
705
609
|
},
|
|
706
610
|
{
|
|
707
611
|
"id": "web-dapp/surface",
|
|
708
612
|
"entry": "src/adapters/web-dapp/surface.ts",
|
|
709
613
|
"kind": "module",
|
|
710
|
-
"purpose": "The web-dapp adapter
|
|
614
|
+
"purpose": "The web-dapp adapter: @farmslot/adapter-web's createWebDappAdapter with the MetaMask signer module, the web-dapp action set, console capture, the locked-session preflight and observation, plus the MetaMask-only surface members; bindWebDappPolicy binds the venue policy and the signer module of the adapter that extends it.",
|
|
711
615
|
"inputs": "webDappSurface",
|
|
712
616
|
"outputs": "AdapterSurface"
|
|
713
617
|
}
|
|
@@ -72,7 +72,15 @@ if [ -f "$PID_FILE" ]; then
|
|
|
72
72
|
rm -f "$PID_FILE"
|
|
73
73
|
fi
|
|
74
74
|
|
|
75
|
+
# Each launch starts a fresh log. The replay cursor goes with it: it marks
|
|
76
|
+
# lines as written that are no longer in the file, such as the relaunched
|
|
77
|
+
# app's first lines the previous forwarder wrote, so the new forwarder replays
|
|
78
|
+
# the app runtime's console buffer into the new log instead of skipping them.
|
|
79
|
+
# A runtime the launch kept replays lines from before the launch too;
|
|
80
|
+
# metamask.app.assert_no_console_errors leaves those out by their own time
|
|
81
|
+
# against the mark each launch attempt records (app-console.launch.json).
|
|
75
82
|
: > "$APP_LOG_FILE"
|
|
83
|
+
rm -f "$APP_LOG_FILE.forwarder-state.json"
|
|
76
84
|
pid="$(node "$SCRIPT_DIR/launch-console-forwarder.cjs" \
|
|
77
85
|
--port "$PORT" \
|
|
78
86
|
--out "$APP_LOG_FILE" \
|
|
@@ -198,15 +198,57 @@ function mergeKvLayers(...layers) {
|
|
|
198
198
|
return Object.keys(result).length ? formatKvLines(result) : '';
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
// Whether a runtime context belongs to the checkout at `repo` (recipe-cli's
|
|
202
|
+
// binding rule): its repoRoot is the checkout's Git top level, so a clone
|
|
203
|
+
// nested inside another checkout never takes that checkout's context; without
|
|
204
|
+
// a repoRoot, the file sits inside that top level. An orchestrator may keep the
|
|
205
|
+
// file elsewhere as long as it names the checkout. Twin of recipe-cli's
|
|
206
|
+
// runtimeContextOwned: this module stays dependency-free for the shell paths
|
|
207
|
+
// that load it, so the rule is repeated here rather than imported.
|
|
208
|
+
export function ownsRuntimeContext(file, repo) {
|
|
209
|
+
let repoRoot;
|
|
210
|
+
try {
|
|
211
|
+
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
212
|
+
repoRoot = typeof value?.repoRoot === 'string' ? value.repoRoot : undefined;
|
|
213
|
+
} catch {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
if (repoRoot === undefined) return within(gitTopLevel(repo), file);
|
|
217
|
+
try {
|
|
218
|
+
return fs.realpathSync(repoRoot) === fs.realpathSync(gitTopLevel(repo));
|
|
219
|
+
} catch {
|
|
220
|
+
// A path that does not exist owns nothing.
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function gitTopLevel(dir) {
|
|
226
|
+
try {
|
|
227
|
+
return execFileSync('git', ['-C', dir, 'rev-parse', '--show-toplevel'], {
|
|
228
|
+
encoding: 'utf8',
|
|
229
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
230
|
+
}).trim();
|
|
231
|
+
} catch {
|
|
232
|
+
// Not a Git checkout: the directory is its own root.
|
|
233
|
+
return dir;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function within(outer, inner) {
|
|
238
|
+
try {
|
|
239
|
+
const relative = path.relative(fs.realpathSync(outer), fs.realpathSync(inner));
|
|
240
|
+
return !(relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative));
|
|
241
|
+
} catch {
|
|
242
|
+
// A path that does not exist owns nothing.
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
201
247
|
function extensionRuntimeContextPath(repo) {
|
|
202
248
|
const repoRoot = path.resolve(repo);
|
|
203
249
|
const envCtx = process.env.RECIPE_RUNTIME_CONTEXT;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
// explicit environment override as authoritative; containment is not a
|
|
207
|
-
// security boundary and silently dropping it selects the wrong port.
|
|
208
|
-
return path.resolve(envCtx);
|
|
209
|
-
}
|
|
250
|
+
// Another checkout's context would set this checkout's ports.
|
|
251
|
+
if (envCtx && ownsRuntimeContext(path.resolve(envCtx), repoRoot)) return path.resolve(envCtx);
|
|
210
252
|
return path.join(repoRoot, recipeRuntimeDir(), 'agentic-runtime.json');
|
|
211
253
|
}
|
|
212
254
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Whether the macOS login session is locked. macOS stops painting visible
|
|
2
|
+
// windows while it is, so a headful browser's screenshots, video and UI actions
|
|
3
|
+
// stall until their own timeouts. The run/call preflight (src/adapters/
|
|
4
|
+
// session-lock.ts) and the web-dapp launcher refuse up front instead.
|
|
5
|
+
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
export const SESSION_LOCKED_MESSAGE =
|
|
9
|
+
"The macOS login session is locked, so the headful browser can't paint (screenshots, video and UI actions stall).";
|
|
10
|
+
export const SESSION_LOCKED_USER_ACTION =
|
|
11
|
+
'Unlock the Mac and keep its login session unlocked while the run drives the browser, then rerun. ' +
|
|
12
|
+
'caffeinate keeps the Mac awake but does not unlock it.';
|
|
13
|
+
|
|
14
|
+
const LOCKED = /<key>CGSSessionScreenIsLocked<\/key>\s*<true\/>/u;
|
|
15
|
+
const ON_CONSOLE = /<key>kCGSSessionOnConsoleKey<\/key>\s*<true\/>/u;
|
|
16
|
+
|
|
17
|
+
// `ioreg -n Root -d1 -a` output: the lock flag of the on-console session in
|
|
18
|
+
// IOConsoleUsers (any session's when none is on console); false for a plist
|
|
19
|
+
// without it, null for anything else.
|
|
20
|
+
export function parseSessionLocked(output) {
|
|
21
|
+
if (!/<plist[\s>]/u.test(output)) return null;
|
|
22
|
+
const users = /<key>IOConsoleUsers<\/key>\s*<array>([\s\S]*?)<\/array>/u.exec(output)?.[1] ?? '';
|
|
23
|
+
const onConsole = users.match(/<dict>[\s\S]*?<\/dict>/gu)?.find((session) => ON_CONSOLE.test(session));
|
|
24
|
+
return LOCKED.test(onConsole ?? output);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// true or false on macOS; null when unknown (another platform, or ioreg failed
|
|
28
|
+
// or took longer than 3 s). Never throws.
|
|
29
|
+
export function macosSessionLocked() {
|
|
30
|
+
if (process.platform !== 'darwin') return null;
|
|
31
|
+
try {
|
|
32
|
+
const result = spawnSync('ioreg', ['-n', 'Root', '-d1', '-a'], {
|
|
33
|
+
encoding: 'utf8',
|
|
34
|
+
timeout: 3_000,
|
|
35
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
36
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
37
|
+
});
|
|
38
|
+
if (result.error || result.status !== 0) return null;
|
|
39
|
+
return parseSessionLocked(result.stdout);
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|