@deeeed/metamask-harness 0.81.0 → 0.82.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 (71) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/adapters/manifest.json +51 -51
  3. package/adapters/{terminal → web-dapp}/cleanup.mjs +9 -9
  4. package/adapters/{terminal → web-dapp}/inject.mjs +14 -14
  5. package/adapters/{terminal → web-dapp}/launch.mjs +23 -24
  6. package/adapters/web-dapp/lib/blocked-hosts.mjs +24 -0
  7. package/adapters/{terminal → web-dapp}/lib/cdp-client.mjs +1 -1
  8. package/adapters/{terminal → web-dapp}/lib/display.mjs +1 -1
  9. package/adapters/{terminal → web-dapp}/lib/hud.mjs +1 -1
  10. package/adapters/web-dapp/lib/policy.mjs +79 -0
  11. package/adapters/{terminal → web-dapp}/lib/processes.mjs +8 -8
  12. package/adapters/{terminal → web-dapp}/lib/readiness.mjs +15 -25
  13. package/adapters/{terminal → web-dapp}/stop.mjs +5 -4
  14. package/adapters/{terminal → web-dapp}/verify.mjs +11 -15
  15. package/adapters/{terminal → web-dapp}/wallet-host.mjs +10 -11
  16. package/dist/adapters/extension/run.js +5 -28
  17. package/dist/adapters/plugin-surface.js +98 -0
  18. package/dist/adapters/surface.js +1 -1
  19. package/dist/adapters/{terminal → web-dapp}/actions.js +7 -7
  20. package/dist/adapters/web-dapp/console-capture.js +63 -0
  21. package/dist/adapters/web-dapp/doctor-checks.js +33 -0
  22. package/dist/adapters/{terminal → web-dapp}/launch.js +21 -22
  23. package/dist/adapters/web-dapp/overlay.js +21 -0
  24. package/dist/adapters/web-dapp/readiness.js +10 -0
  25. package/dist/adapters/{terminal → web-dapp}/run.js +15 -15
  26. package/dist/adapters/{terminal → web-dapp}/stop.js +5 -4
  27. package/dist/adapters/web-dapp/surface.js +149 -0
  28. package/dist/cli.js +3 -0
  29. package/dist/command-contract.js +29 -7
  30. package/dist/commands/check.js +2 -2
  31. package/dist/commands/doctor.js +19 -2
  32. package/dist/commands/farmslot-ready.js +1 -0
  33. package/dist/commands/help.js +1 -1
  34. package/dist/commands/launch/device.js +5 -5
  35. package/dist/commands/self-test.js +9 -5
  36. package/dist/engine.js +6 -16
  37. package/dist/manifest.js +48 -24
  38. package/dist/mm-harness-cli.js +17 -0
  39. package/dist/performance-observation.js +7 -13
  40. package/dist/runner.js +26 -7
  41. package/docs/cli-contract.md +5 -3
  42. package/library/actions/shared/console-collector.mjs +1 -1
  43. package/library/actions/{terminal → web-dapp}/app/assert_no_console_errors.mjs +4 -4
  44. package/library/actions/{terminal → web-dapp}/app/launch.mjs +7 -7
  45. package/library/actions/{terminal → web-dapp}/platform/hud.mjs +5 -5
  46. package/library/actions/{terminal → web-dapp}/platform/page.mjs +11 -11
  47. package/library/actions/{terminal → web-dapp}/platform/runtime.mjs +16 -12
  48. package/library/actions/web-dapp/ui/navigate.mjs +24 -0
  49. package/library/actions/web-dapp/wallet/_signature-log.mjs +32 -0
  50. package/library/actions/{terminal → web-dapp}/wallet/assert_signatures.mjs +4 -4
  51. package/library/actions/{terminal → web-dapp}/wallet/list_accounts.mjs +2 -2
  52. package/library/actions/{terminal → web-dapp}/wallet/read_signatures.mjs +6 -6
  53. package/library/manifests/{terminal.action-manifest.json → web-dapp.action-manifest.json} +18 -702
  54. package/package.json +5 -5
  55. package/adapters/terminal/lib/mainnet-guard.mjs +0 -186
  56. package/adapters/terminal/policy.mjs +0 -35
  57. package/dist/adapters/terminal/console-capture.js +0 -59
  58. package/dist/adapters/terminal/doctor-checks.js +0 -24
  59. package/dist/adapters/terminal/overlay.js +0 -25
  60. package/dist/adapters/terminal/readiness.js +0 -11
  61. package/dist/adapters/terminal/surface.js +0 -135
  62. package/library/actions/terminal/perps/_venue.mjs +0 -242
  63. package/library/actions/terminal/perps/assert_orders.mjs +0 -54
  64. package/library/actions/terminal/perps/assert_positions.mjs +0 -40
  65. package/library/actions/terminal/perps/read_orders.mjs +0 -26
  66. package/library/actions/terminal/perps/read_positions.mjs +0 -25
  67. package/library/actions/terminal/perps/read_snapshot.mjs +0 -40
  68. package/library/actions/terminal/perps/revoke_agent.mjs +0 -58
  69. package/library/actions/terminal/perps/teardown_state.mjs +0 -104
  70. package/library/actions/terminal/ui/navigate.mjs +0 -31
  71. package/library/actions/terminal/wallet/_signature-log.mjs +0 -56
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.82.0 - 2026-10-07
6
+
7
+ ### Added
8
+
9
+ - Adapters a recipe library declares in `recipe-library.json` `adapters` (on `--library`, the `RECIPE_LIBRARY_PATH` you set or else the personal library, or `config set libraries.<name>`; never a task-local library, a sibling checkout or `<target>/.skills-cache`, which serve recipe discovery only) work as `--adapter <id>`: the command loads the plugin only when it selects it (the last `--adapter`, else the last `--platform`), composes it on the adapter it `extends` (a built-in or another plugin; a plugin that extends a built-in also offers the built-in's official actions, library actions and observers), runs its doctor checks after the shared ones (a check that throws is a required failure, `adapter-plugin:<id>:doctor`) and runs its in-code actions. `call <action> --adapter <id> --help` shows a plugin action's fields. `doctor --json` names each loaded plugin with its library and digest (`adapter-plugin:<id>`), and an approved plan (`--approve-plan`) binds that digest: every run on a plugin adapter carries it in `METAMASK_RECIPE_ADAPTER_PLUGIN`, so editing the plugin's module or a file beside it fails the old approval with `RECIPE_APPROVAL_MISMATCH`, whatever actions the recipe runs. A plugin's own manifest may declare official actions and observers for that plugin, but a plugin that extends it inherits only its custom actions: inherited official actions and observers come from the bundled library alone, and a parent manifest outside it that declares one fails with `ACTION_MANIFEST_INVALID` (a JSON envelope under `--json`), like any other action manifest the plugin can't use. A malformed `RECIPE_LIBRARY_PATH` never blocks a built-in adapter. A plugin may import from disk only its module's directory and the library's `actions/` (`RECIPE_SOURCE_INVALID` otherwise); a bare package it imports resolves from mm-harness's own install. Plugins need Node.js 22.15 or later. A refused plugin fails with `ADAPTER_SDK_UNSUPPORTED`, `ADAPTER_PLUGIN_INVALID`, `ADAPTER_PLUGIN_LOAD_FAILED`, `ADAPTER_ID_CONFLICT`, `ADAPTER_EXTENDS_UNKNOWN` or `RECIPE_SOURCE_INVALID`, exit 2. A plugin that extends nothing (or only other plugins) gets no Runway provisioning and no wallet fixture operations; one that extends a built-in keeps the built-in's (the terminal plugin keeps web-dapp's `fixtures init|sync|set`).
10
+
11
+ ### Changed
12
+
13
+ - **BREAKING:** the built-in `terminal` adapter becomes the generic `web-dapp` built-in, and the Web Terminal becomes the `terminal` plugin of the `experimental-metamask-recipe-terminal` library (`recipe-library.json` `adapters.terminal`, `extends: web-dapp`). `--adapter terminal` needs a revision of that library that declares `adapters.terminal` (8b409b6 or later, on its `feat/console-allowlist` branch; the library's `main` does not declare it yet) on `RECIPE_LIBRARY_PATH`, as on the Terminal farm slots. A host without it (an older checkout, or none, as on macpro and mini today) refuses every `--adapter terminal` command: `--adapter must be mobile, extension, core, or web-dapp; received 'terminal'`. Update the library before this harness; the hooks, runtime dir (`temp/recipe/runtime/terminal`), doctor check ids, evidence names and action ids are unchanged.
14
+ - The plugin holds everything Hyperliquid: the venue policy (blocked and served hosts, link hosts, the launch probe, the testnet typed-data refusal, the start page and product routes, the injected wallet's start chain, the checkout dependencies and detection, the dev-server testnet variable), the signature-log rules, and the 7 `metamask.perps.*` venue actions with their manifest. They leave this package: `adapters/terminal/lib/mainnet-guard.mjs`, `adapters/terminal/policy.mjs`, `library/actions/terminal/perps/`.
15
+ - `web-dapp` keeps the generic parts (`adapters/web-dapp/`, `library/actions/web-dapp/`, `library/manifests/web-dapp.action-manifest.json`): launch, stop, verify, inject, cleanup, the wallet host, console capture, `ui.*`, `app.hud`, `metamask.app.*` and `metamask.wallet.*`. It reads the venue policy from the module in `RECIPE_WEB_DAPP_POLICY`, which the harness sets when it adopts an adapter that extends `web-dapp`; the leaf scripts no longer take `--policy-module`.
16
+ - `web-dapp` selected on its own has no venue: launch, verify and runtime status refuse, and doctor fails its required `web-dapp-venue-policy` check. The policy module is checked when it loads (every member web-dapp reads, and `policyVersion: 1`).
17
+ - Without `--adapter`, a Terminal checkout is no longer auto-detected, and `actions --matrix` has no `terminal` column: the loader imports a plugin only when a command selects it (the ADR trust boundary), so the plugin's detection runs once `--adapter terminal` selects it. Every farm hook passes `--adapter terminal`.
18
+ - `check diff` takes any registered or library-declared adapter for `--adapter`/`--platform` (it listed the built-ins).
19
+ - Depend on `@farmslot/recipe-cli` 0.6.0 and `@farmslot/adapter-sdk` 0.5.0 (were 0.5.0 and 0.4.0), which carry recipe-library adapter plugins, adapter-aware trusted-mutation hooks and one environment scope for a run's ports (recipe-cli 0.6.0 also writes the task's acceptance ledger from a recipe's proof targets and warns at `run --plan` on command nodes whose exit code cannot prove the claim), and on `@farmslot/recipe-runner` 0.25.0 and `@farmslot/adapter-rn` 0.17.1 (were 0.24.0 and 0.17.0). `@farmslot/adapter-web` 0.3.1, `@farmslot/agent-runtime` 0.17.0 and `@farmslot/protocol` 0.34.0 are unchanged, and the install still holds one `@farmslot/recipe-runner`.
20
+ - The Extension proof-runtime check no longer pins `--cdp-port`/`--watcher-port` itself: `run` and `call` run it, the observers and the execution in one environment scope, so every platform's runtime check and observers see the run's ports, its dev-server port names (`METRO_PORT`) and its platform environment. The performance observers read their ports from that environment (`PerformanceObserver.backend(target, env)`), and `self-test` opens the scope around its smoke runs.
21
+
5
22
  ## 0.81.0 - 2026-10-07
6
23
 
7
24
  ### Added
@@ -584,115 +584,115 @@
584
584
  "outputs": "numbered checkouts, saved preferences, and run summary"
585
585
  },
586
586
  {
587
- "id": "terminal/launch",
588
- "entry": "adapters/terminal/launch.mjs",
587
+ "id": "web-dapp/launch",
588
+ "entry": "adapters/web-dapp/launch.mjs",
589
589
  "kind": "node",
590
- "purpose": "Start or reuse the Web Terminal 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 venue SDK's mainnet hosts are unresolvable for the whole browser.",
591
- "inputs": "--target --cdp-port --app-port --policy-module [--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",
592
- "outputs": "<runtime>/terminal/browser.json + pid/log files; JSON on stdout; exit 0/1/2"
590
+ "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.",
591
+ "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",
592
+ "outputs": "<runtime>/<adapter id>/browser.json + pid/log files; JSON on stdout; exit 0/1/2"
593
593
  },
594
594
  {
595
- "id": "terminal/wallet-host",
596
- "entry": "adapters/terminal/wallet-host.mjs",
595
+ "id": "web-dapp/wallet-host",
596
+ "entry": "adapters/web-dapp/wallet-host.mjs",
597
597
  "kind": "node",
598
- "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 host passes the testnet policy and the injected wallet's identity.",
599
- "inputs": "--cdp-port --runtime-dir --signer --app-origin --policy-module [--extension-id] [--target --account --start-chain] [--browser-app <.app>]",
600
- "outputs": "<runtime>/terminal/wallet-requests.jsonl, wallet-host.ready"
598
+ "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.",
599
+ "inputs": "--cdp-port --runtime-dir --signer --app-origin [--extension-id] [--target --account --start-chain] [--browser-app <.app>]",
600
+ "outputs": "<runtime>/<adapter id>/wallet-requests.jsonl, wallet-host.ready"
601
601
  },
602
602
  {
603
- "id": "terminal/stop",
604
- "entry": "adapters/terminal/stop.mjs",
603
+ "id": "web-dapp/stop",
604
+ "entry": "adapters/web-dapp/stop.mjs",
605
605
  "kind": "node",
606
- "purpose": "Stop the terminal slot browser, wallet host and console collector and prove the CDP port is free.",
606
+ "purpose": "Stop the web-dapp slot browser, wallet host and console collector and prove the CDP port is free.",
607
607
  "inputs": "--target [--cdp-port]",
608
608
  "outputs": "JSON on stdout; exit 0/1"
609
609
  },
610
610
  {
611
- "id": "terminal/inject",
612
- "entry": "adapters/terminal/inject.mjs",
611
+ "id": "web-dapp/inject",
612
+ "entry": "adapters/web-dapp/inject.mjs",
613
613
  "kind": "node",
614
- "purpose": "Terminal install: overlay marker, runtime dir and wallet fixture (0600); no product writes.",
614
+ "purpose": "web-dapp install: overlay marker, runtime dir and wallet fixture (0600); refuses a checkout the venue policy does not match; no product writes.",
615
615
  "inputs": "--target [--fixture] [--force]; env RECIPE_WALLET_FIXTURE",
616
- "outputs": "<harness>/terminal/install.json, <runtime>/wallet-fixture.json; exit 0/1"
616
+ "outputs": "<harness>/<adapter id>/install.json, <runtime>/wallet-fixture.json; exit 0/1"
617
617
  },
618
618
  {
619
- "id": "terminal/cleanup",
620
- "entry": "adapters/terminal/cleanup.mjs",
619
+ "id": "web-dapp/cleanup",
620
+ "entry": "adapters/web-dapp/cleanup.mjs",
621
621
  "kind": "node",
622
- "purpose": "Terminal cleanup: stop browser/host, remove terminal runtime state, overlay marker and installed fixture; profiles kept unless --reset-profile.",
622
+ "purpose": "web-dapp cleanup: stop browser/host, remove the slot's runtime state, overlay marker and installed fixture; profiles kept unless --reset-profile.",
623
623
  "inputs": "--target [--cdp-port] [--reset-profile]",
624
624
  "outputs": "JSON on stdout; exit 0/1"
625
625
  },
626
626
  {
627
- "id": "terminal/verify",
628
- "entry": "adapters/terminal/verify.mjs",
627
+ "id": "web-dapp/verify",
628
+ "entry": "adapters/web-dapp/verify.mjs",
629
629
  "kind": "node",
630
- "purpose": "Read-only terminal slot readiness: checkout, deps, dev server, forced testnet, fixture, browser start probe, extension copy, CDP port.",
631
- "inputs": "--policy-module --target [--cdp-port] [--watcher-port] [--signer] [--account] [--json]",
630
+ "purpose": "Read-only web-dapp slot readiness: checkout, deps, dev server, forced testnet, fixture, browser start probe, extension copy, CDP port.",
631
+ "inputs": "--target [--cdp-port] [--watcher-port] [--signer] [--account] [--json]",
632
632
  "outputs": "readiness report; exit 0 ready / 1 not ready"
633
633
  },
634
634
  {
635
- "id": "terminal/lib-cdp-client",
636
- "entry": "adapters/terminal/lib/cdp-client.mjs",
635
+ "id": "web-dapp/lib-cdp-client",
636
+ "entry": "adapters/web-dapp/lib/cdp-client.mjs",
637
637
  "kind": "lib",
638
638
  "purpose": "Browser-level CDP client with flattened target sessions for the wallet host.",
639
639
  "inputs": "CdpClient.connect(url)",
640
640
  "outputs": "send/on"
641
641
  },
642
642
  {
643
- "id": "terminal/lib-processes",
644
- "entry": "adapters/terminal/lib/processes.mjs",
643
+ "id": "web-dapp/lib-processes",
644
+ "entry": "adapters/web-dapp/lib/processes.mjs",
645
645
  "kind": "lib",
646
646
  "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.",
647
- "inputs": "stopTerminalBrowser(target, { cdpPort }), stopConsoleCollector(target), ownerMarker(kind, runtimeDir), ownsProcess(kind, pid, runtimeDir)",
647
+ "inputs": "stopWebDappBrowser(target, { cdpPort }), stopConsoleCollector(target), ownerMarker(kind, runtimeDir), ownsProcess(kind, pid, runtimeDir)",
648
648
  "outputs": "stopped processes"
649
649
  },
650
650
  {
651
- "id": "terminal/lib-readiness",
652
- "entry": "adapters/terminal/lib/readiness.mjs",
651
+ "id": "web-dapp/lib-readiness",
652
+ "entry": "adapters/web-dapp/lib/readiness.mjs",
653
653
  "kind": "lib",
654
- "purpose": "Terminal slot readiness checks shared by verify, doctor and the surface.",
655
- "inputs": "terminalReadiness({ policy, target, appPort, cdpPort, signer }), assertLaunchNetwork(target, { network, mainnetConfirmation }), devServerTestnetDiagnostic(target, { appPort, variable }) (advisory)",
654
+ "purpose": "web-dapp slot readiness checks shared by verify, doctor and the surface.",
655
+ "inputs": "webDappReadiness({ policy, target, appPort, cdpPort, signer }), assertLaunchNetwork(target, { network, mainnetConfirmation }), devServerTestnetDiagnostic(target, { appPort, variable }) (advisory)",
656
656
  "outputs": "checks[]"
657
657
  },
658
658
  {
659
- "id": "terminal/lib-display",
660
- "entry": "adapters/terminal/lib/display.mjs",
659
+ "id": "web-dapp/lib-display",
660
+ "entry": "adapters/web-dapp/lib/display.mjs",
661
661
  "kind": "lib",
662
662
  "purpose": "Headless/headful and slow-mo resolution, per-slot window placement and the profile zoom that keeps the desktop layout.",
663
663
  "inputs": "resolveHeadless, resolveSlowMo, windowPlacement, writeProfileZoom",
664
664
  "outputs": "browser display args"
665
665
  },
666
666
  {
667
- "id": "terminal/lib-hud",
668
- "entry": "adapters/terminal/lib/hud.mjs",
667
+ "id": "web-dapp/lib-hud",
668
+ "entry": "adapters/web-dapp/lib/hud.mjs",
669
669
  "kind": "lib",
670
670
  "purpose": "Recipe HUD state (<runtime>/hud.json) and the page expression that draws it on the app origin only.",
671
671
  "inputs": "nextHudState, hudRenderExpression",
672
672
  "outputs": "HUD state, expression source"
673
673
  },
674
674
  {
675
- "id": "terminal/lib-mainnet-guard",
676
- "entry": "adapters/terminal/lib/mainnet-guard.mjs",
675
+ "id": "web-dapp/lib-blocked-hosts",
676
+ "entry": "adapters/web-dapp/lib/blocked-hosts.mjs",
677
677
  "kind": "lib",
678
- "purpose": "Testnet enforcement: the venue endpoint hosts (from the venue SDK, @metamask/perps-controller and the app source in the checkout), the browser's host-resolver block, and the mainnet typed-data classifier used by the page script and the wallet host.",
679
- "inputs": "venueHosts(checkout), hostResolverRules(hosts), isMainnetVenueUrl(url, hosts), mainnetTypedDataReason(data), TESTNET_TYPED_DATA_POLICY",
680
- "outputs": "host list, launch switch, refusal reason"
678
+ "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.",
679
+ "inputs": "hostResolverRules(hosts), hostOf(url), isBlockedUrl(url, hosts)",
680
+ "outputs": "launch switch, booleans"
681
681
  },
682
682
  {
683
- "id": "terminal/policy",
684
- "entry": "adapters/terminal/policy.mjs",
683
+ "id": "web-dapp/lib-policy",
684
+ "entry": "adapters/web-dapp/lib/policy.mjs",
685
685
  "kind": "lib",
686
- "purpose": "The Web Terminal's venue policy: blocked and served venue hosts, link hosts, the launch probe, the testnet typed-data refusal, the start page, the injected wallet's start chain, the checkout dependencies and the dev-server testnet variable. launch, verify, doctor and the wallet host take it as an argument (--policy-module for the leaf processes).",
687
- "inputs": "policy.venueHosts(checkout)",
688
- "outputs": "policy"
686
+ "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.",
687
+ "inputs": "env RECIPE_WEB_DAPP_POLICY",
688
+ "outputs": "webDappPolicy(), webDappAdapterId()"
689
689
  },
690
690
  {
691
- "id": "terminal/surface",
692
- "entry": "src/adapters/terminal/surface.ts",
691
+ "id": "web-dapp/surface",
692
+ "entry": "src/adapters/web-dapp/surface.ts",
693
693
  "kind": "module",
694
- "purpose": "Terminal adapter surface (runtime status, browser stop, logs, hints).",
695
- "inputs": "terminalSurface",
694
+ "purpose": "The web-dapp adapter surface (runtime status, browser stop, logs, hints) and bindWebDappPolicy, which binds the venue policy of the adapter that extends it.",
695
+ "inputs": "webDappSurface",
696
696
  "outputs": "AdapterSurface"
697
697
  }
698
698
  ]
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- // cleanup.mjs — `mm-harness cleanup --adapter terminal`: stop the slot browser
3
- // and wallet host, then remove the terminal runtime state and the installed
2
+ // cleanup.mjs — `mm-harness cleanup --adapter <web-dapp adapter>`: stop the slot
3
+ // browser and wallet host, then remove the slot's runtime state and the installed
4
4
  // wallet fixture. Browser profiles are kept unless --reset-profile.
5
5
  //
6
6
  // Inputs: --target <checkout> [--cdp-port <port>] [--reset-profile]
@@ -13,16 +13,16 @@ import { fileURLToPath } from 'node:url';
13
13
 
14
14
  import { recipeHarnessPath } from '@farmslot/recipe-cli/harness';
15
15
  import { walletFixturePath } from '../../library/actions/harness-exports.mjs';
16
- import { terminalRuntimeDir } from '../../library/actions/terminal/platform/runtime.mjs';
17
- import { stopConsoleCollector, stopTerminalBrowser } from './lib/processes.mjs';
16
+ import { webDappPolicy, webDappRuntimeDir } from '../../library/actions/web-dapp/platform/runtime.mjs';
17
+ import { stopConsoleCollector, stopWebDappBrowser } from './lib/processes.mjs';
18
18
 
19
19
  const usage = 'Usage: cleanup.mjs --target <checkout> [--cdp-port <port>] [--reset-profile]';
20
20
 
21
- export async function cleanupTerminalRuntime({ target, cdpPort, resetProfile = false }) {
21
+ export async function cleanupWebDappRuntime({ target, cdpPort, resetProfile = false }) {
22
22
  const root = path.resolve(target);
23
- const stop = await stopTerminalBrowser(root, { cdpPort });
23
+ const stop = await stopWebDappBrowser(root, { cdpPort });
24
24
  await stopConsoleCollector(root);
25
- const runtime = terminalRuntimeDir(root);
25
+ const runtime = webDappRuntimeDir(root);
26
26
  const removed = [];
27
27
  if (existsSync(runtime)) {
28
28
  for (const entry of readdirSync(runtime)) {
@@ -31,7 +31,7 @@ export async function cleanupTerminalRuntime({ target, cdpPort, resetProfile = f
31
31
  removed.push(entry);
32
32
  }
33
33
  }
34
- const overlay = recipeHarnessPath(root, 'terminal');
34
+ const overlay = recipeHarnessPath(root, webDappPolicy().adapterId);
35
35
  if (existsSync(overlay)) {
36
36
  rmSync(overlay, { recursive: true, force: true });
37
37
  removed.push(path.relative(root, overlay));
@@ -56,7 +56,7 @@ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.me
56
56
  };
57
57
  const rawPort = value('--cdp-port') ?? process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT;
58
58
  try {
59
- const result = await cleanupTerminalRuntime({
59
+ const result = await cleanupWebDappRuntime({
60
60
  target: value('--target') ?? process.cwd(),
61
61
  cdpPort: rawPort ? Number(rawPort) : undefined,
62
62
  resetProfile: argv.includes('--reset-profile'),
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- // inject.mjs — `mm-harness install --adapter terminal`: prepare the slot runtime
3
- // directory and install the wallet fixture the terminal actions read.
2
+ // inject.mjs — `mm-harness install --adapter <web-dapp adapter>`: prepare the slot
3
+ // runtime directory and install the wallet fixture the web-dapp actions read.
4
4
  //
5
- // Inputs: --target <terminal checkout> [--fixture <wallet-fixture.json>] [--force]
5
+ // Inputs: --target <checkout> [--fixture <wallet-fixture.json>] [--force]
6
6
  // env RECIPE_WALLET_FIXTURE (fixture source when --fixture is absent)
7
- // Outputs: <target>/<harness root>/terminal/install.json (overlay marker),
8
- // <target>/<runtime>/terminal/ (0700), <runtime>/wallet-fixture.json (0600);
7
+ // Outputs: <target>/<harness root>/<adapter id>/install.json (overlay marker),
8
+ // <target>/<runtime>/<adapter id>/ (0700), <runtime>/wallet-fixture.json (0600);
9
9
  // JSON summary on stdout (account names only). Exit 0 installed; 1 failure; 2 usage.
10
10
  // Never touches: product source files; never prints key material.
11
11
 
@@ -15,16 +15,16 @@ import { fileURLToPath } from 'node:url';
15
15
 
16
16
  import { recipeHarnessPath, recipeRuntimeDir } from '@farmslot/recipe-cli/harness';
17
17
  import { walletFixturePath } from '../../library/actions/harness-exports.mjs';
18
- import { terminalRuntimeDir } from '../../library/actions/terminal/platform/runtime.mjs';
19
- import { isTerminalCheckout } from './lib/readiness.mjs';
18
+ import { webDappPolicy, webDappRuntimeDir } from '../../library/actions/web-dapp/platform/runtime.mjs';
20
19
 
21
- const usage = 'Usage: inject.mjs --target <terminal checkout> [--fixture <wallet-fixture.json>] [--force]';
20
+ const usage = 'Usage: inject.mjs --target <checkout> [--fixture <wallet-fixture.json>] [--force]';
22
21
 
23
- export function installTerminalRuntime({ target, fixture, force = false }) {
22
+ export function installWebDappRuntime({ target, fixture, force = false }) {
24
23
  const root = path.resolve(target);
25
- if (!isTerminalCheckout(root)) throw new Error(`${root} is not a Web Terminal checkout (needs next + src/features/perpetuals).`);
24
+ const { adapterId, checkout } = webDappPolicy();
25
+ if (!checkout.matches(root)) throw new Error(`${root} is not a ${checkout.name} checkout (needs ${checkout.needs}).`);
26
26
  mkdirSync(path.join(root, recipeRuntimeDir()), { recursive: true, mode: 0o700 });
27
- mkdirSync(terminalRuntimeDir(root), { recursive: true, mode: 0o700 });
27
+ mkdirSync(webDappRuntimeDir(root), { recursive: true, mode: 0o700 });
28
28
  const destination = walletFixturePath(root);
29
29
  let fixtureAction = 'kept';
30
30
  if (fixture && (force || !existsSync(destination))) {
@@ -39,9 +39,9 @@ export function installTerminalRuntime({ target, fixture, force = false }) {
39
39
  const accounts = existsSync(destination)
40
40
  ? JSON.parse(readFileSync(destination, 'utf8')).accounts.map((account) => account?.name).filter(Boolean)
41
41
  : [];
42
- const marker = { schemaVersion: 1, adapter: 'terminal', installedAt: new Date().toISOString(), fixture: fixtureAction };
42
+ const marker = { schemaVersion: 1, adapter: adapterId, installedAt: new Date().toISOString(), fixture: fixtureAction };
43
43
  // The overlay directory is the install marker run/call auto-ensure checks.
44
- const overlay = recipeHarnessPath(root, 'terminal');
44
+ const overlay = recipeHarnessPath(root, adapterId);
45
45
  mkdirSync(overlay, { recursive: true, mode: 0o700 });
46
46
  writeFileSync(path.join(overlay, 'install.json'), `${JSON.stringify(marker, null, 2)}\n`, { mode: 0o600 });
47
47
  return { status: fixtureAction === 'missing' ? 'fail' : 'pass', target: root, fixture: fixtureAction, fixturePath: destination, accounts };
@@ -58,7 +58,7 @@ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.me
58
58
  return index >= 0 ? argv[index + 1] : undefined;
59
59
  };
60
60
  try {
61
- const result = installTerminalRuntime({
61
+ const result = installWebDappRuntime({
62
62
  target: value('--target') ?? process.cwd(),
63
63
  fixture: value('--fixture') ?? process.env.RECIPE_WALLET_FIXTURE,
64
64
  force: argv.includes('--force'),
@@ -1,25 +1,25 @@
1
1
  #!/usr/bin/env node
2
- // launch.mjs — start (or reuse) the terminal slot browser and its wallet host.
2
+ // launch.mjs — start (or reuse) the web-dapp slot browser and its wallet host.
3
3
  //
4
4
  // Purpose:
5
5
  // One Chromium-family browser per slot, bound to 127.0.0.1:<cdp-port>, with a
6
- // profile under <runtime>/terminal/. signer=extension loads the slot's copy of
6
+ // profile under <runtime>/<adapter id>/. signer=extension loads the slot's copy of
7
7
  // the MetaMask extension and imports the fixture account through the Extension
8
8
  // adapter's fixture flow (wallet-fixture-state.cjs generate → prefill-profile →
9
9
  // seed-cdp). signer=injected loads no extension; the wallet host provides the
10
10
  // strict test wallet. Either way wallet-host.mjs then records wallet requests.
11
11
  //
12
- // Inputs (flags): --target <terminal checkout> --cdp-port <port> --app-port <port>
12
+ // Inputs (flags): --target <checkout> --cdp-port <port> --app-port <port>
13
13
  // [--signer extension|injected] [--account <fixture name>] [--fresh-profile]
14
14
  // [--headless | --headful] [--slow-mo <ms>] [--start-chain <id>] [--json]
15
- // [--network mainnet --mainnet-confirmation REAL_FUNDS] (required unless the slot
16
- // dev server is known to force Hyperliquid testnet)
15
+ // [--network mainnet --mainnet-confirmation REAL_FUNDS] (testnet otherwise,
16
+ // enforced in the browser from the venue policy)
17
17
  // env: TERMINAL_CHROME_BIN, TERMINAL_EXTENSION_DIR,
18
18
  // TERMINAL_EXTENSION_CHECKOUT, TERMINAL_EXTENSION_NODE, RECIPE_WALLET_FIXTURE,
19
19
  // TERMINAL_HEADLESS, TERMINAL_SLOW_MO, TERMINAL_WINDOW, TERMINAL_SCREEN
20
20
  // The browser is headful by default, in a window placed per slot (see
21
21
  // lib/display.mjs); --headless or TERMINAL_HEADLESS=1 hides it.
22
- // Outputs: <runtime>/terminal/browser.json (+ pid files, logs); JSON summary on stdout.
22
+ // Outputs: <runtime>/<adapter id>/browser.json (+ pid files, logs); JSON summary on stdout.
23
23
  // Exit 0 ready; 1 launch/seed failure; 2 usage.
24
24
  // Never prints: mnemonics, private keys, the wallet password.
25
25
 
@@ -29,20 +29,21 @@ import { createRequire } from 'node:module';
29
29
  import { appendFileSync, chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
30
30
  import os from 'node:os';
31
31
  import path from 'node:path';
32
- import { fileURLToPath, pathToFileURL } from 'node:url';
32
+ import { fileURLToPath } from 'node:url';
33
33
 
34
34
  import {
35
35
  fixtureEntry,
36
36
  loadWalletFixture,
37
37
  resolveSigner,
38
- terminalRuntimeDir,
39
38
  viemAccountFromEntry,
40
- } from '../../library/actions/terminal/platform/runtime.mjs';
39
+ webDappPolicy,
40
+ webDappRuntimeDir,
41
+ } from '../../library/actions/web-dapp/platform/runtime.mjs';
41
42
  import { CdpClient, browserWebSocketUrl } from './lib/cdp-client.mjs';
42
43
  import { clearHudState } from './lib/hud.mjs';
43
44
  import { browserDisplayArgs, resolveHeadless, resolveSlowMo, windowPlacement, writeProfileZoom } from './lib/display.mjs';
44
- import { cdpListenerPids, ownedBrowserPids, ownerMarker, ownsProcess, pidAlive, stopTerminalBrowser, terminate } from './lib/processes.mjs';
45
- import { hostResolverRules } from './lib/mainnet-guard.mjs';
45
+ import { cdpListenerPids, ownedBrowserPids, ownerMarker, ownsProcess, pidAlive, stopWebDappBrowser, terminate } from './lib/processes.mjs';
46
+ import { hostResolverRules } from './lib/blocked-hosts.mjs';
46
47
  import { assertLaunchNetwork, devServerTestnetDiagnostic } from './lib/readiness.mjs';
47
48
 
48
49
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
@@ -51,7 +52,7 @@ const HARNESS_ROOT = path.resolve(SCRIPT_DIR, '../..');
51
52
  const FIXTURE_STATE_SCRIPT = path.join(HARNESS_ROOT, 'adapters/extension/wallet-fixture-state.cjs');
52
53
 
53
54
  function usage() {
54
- return 'Usage: launch.mjs --target <checkout> --cdp-port <port> --app-port <port> --policy-module <venue policy module> [--signer extension|injected] [--account <name>] [--fresh-profile] [--headless | --headful] [--slow-mo <ms>] [--network testnet|mainnet --mainnet-confirmation REAL_FUNDS] [--start-chain <id>] [--json]';
55
+ return 'Usage: launch.mjs --target <checkout> --cdp-port <port> --app-port <port> [--signer extension|injected] [--account <name>] [--fresh-profile] [--headless | --headful] [--slow-mo <ms>] [--network testnet|mainnet --mainnet-confirmation REAL_FUNDS] [--start-chain <id>] [--json]';
55
56
  }
56
57
 
57
58
  export function parseLaunchArgs(argv) {
@@ -67,7 +68,7 @@ export function parseLaunchArgs(argv) {
67
68
  args[flag.slice(2)] = argv[index + 1];
68
69
  index += 1;
69
70
  }
70
- for (const key of ['target', 'cdp-port', 'app-port', 'policy-module']) {
71
+ for (const key of ['target', 'cdp-port', 'app-port']) {
71
72
  if (!args[key]) throw Object.assign(new Error(`Missing --${key}\n${usage()}`), { exitCode: 2 });
72
73
  }
73
74
  args.signer = resolveSigner(args.signer);
@@ -76,7 +77,7 @@ export function parseLaunchArgs(argv) {
76
77
  }
77
78
 
78
79
  function log(message) {
79
- process.stderr.write(`[terminal/launch] ${message}\n`);
80
+ process.stderr.write(`[${webDappPolicy().adapterId}/launch] ${message}\n`);
80
81
  }
81
82
 
82
83
  function browserStarts(bin) {
@@ -113,7 +114,7 @@ export async function resolveBrowser(env = process.env, candidates = chromeForTe
113
114
  if (!existsSync(explicit)) throw new Error(`TERMINAL_CHROME_BIN does not exist: ${explicit}`);
114
115
  return { bin: explicit, version: browserStarts(explicit), source: 'TERMINAL_CHROME_BIN' };
115
116
  }
116
- if (env.RECIPE_HARNESS_CHROME_BIN) log(`ignoring RECIPE_HARNESS_CHROME_BIN (${env.RECIPE_HARNESS_CHROME_BIN}) for the terminal; set TERMINAL_CHROME_BIN to override Chrome for Testing`);
117
+ if (env.RECIPE_HARNESS_CHROME_BIN) log(`ignoring RECIPE_HARNESS_CHROME_BIN (${env.RECIPE_HARNESS_CHROME_BIN}) for the ${webDappPolicy().adapterId}; set TERMINAL_CHROME_BIN to override Chrome for Testing`);
117
118
  const bin = candidates.find((candidate) => existsSync(candidate));
118
119
  if (bin) return { bin, version: browserStarts(bin), source: 'chrome-for-testing' };
119
120
  throw new Error(
@@ -344,9 +345,9 @@ function writePrivateFile(file, text) {
344
345
  // which waits for a cold dev-server compile.
345
346
  export const LAUNCH_TIMEOUTS = Object.freeze({ browserStartMs: 90000, hostReadyMs: 180000 });
346
347
 
347
- // `policy`: the venue policy (adapters/terminal/policy.mjs).
348
- export async function launchTerminalBrowser(args, env = process.env, { timeouts = LAUNCH_TIMEOUTS, policy } = {}) {
349
- if (!policy?.module) throw new Error('launchTerminalBrowser needs the venue policy (adapters/terminal/policy.mjs).');
348
+ // The venue policy comes from RECIPE_WEB_DAPP_POLICY (webDappPolicy), as for every web-dapp leaf.
349
+ export async function launchWebDappBrowser(args, env = process.env, { timeouts = LAUNCH_TIMEOUTS } = {}) {
350
+ const policy = webDappPolicy(env);
350
351
  const target = path.resolve(args.target);
351
352
  const cdpPort = Number(args['cdp-port']);
352
353
  const appPort = Number(args['app-port']);
@@ -364,7 +365,7 @@ export async function launchTerminalBrowser(args, env = process.env, { timeouts
364
365
  if (devServer && !(devServer.known && devServer.forced)) {
365
366
  log(`WARN dev server testnet check: ${devServer.detail} (advisory; the browser enforces testnet itself)`);
366
367
  }
367
- const runtime = terminalRuntimeDir(target);
368
+ const runtime = webDappRuntimeDir(target);
368
369
  mkdirSync(runtime, { recursive: true, mode: 0o700 });
369
370
  chmodSync(runtime, 0o700);
370
371
  const stateFile = path.join(runtime, 'browser.json');
@@ -394,7 +395,7 @@ export async function launchTerminalBrowser(args, env = process.env, { timeouts
394
395
  if (previous.slowMoMs !== slowMoMs) writePrivateFile(stateFile, `${JSON.stringify(state, null, 2)}\n`);
395
396
  return { ...state, reused: true };
396
397
  }
397
- await stopTerminalBrowser(target, { cdpPort, log });
398
+ await stopWebDappBrowser(target, { cdpPort, log });
398
399
 
399
400
  const browser = await resolveBrowser(env);
400
401
  const profile = path.join(runtime, `profile-${signer}-${args.account.replace(/[^A-Za-z0-9_-]/gu, '_')}`);
@@ -545,7 +546,6 @@ export async function launchTerminalBrowser(args, env = process.env, { timeouts
545
546
  '--account', args.account,
546
547
  '--start-chain', String(args['start-chain'] ?? policy.startChain),
547
548
  '--network', network,
548
- '--policy-module', policy.module,
549
549
  ownerMarker('wallet-host', runtime),
550
550
  ];
551
551
  if (network === 'testnet') hostArgs.push('--mainnet-hosts', mainnetHosts.join(','), '--testnet-hosts', venues.served.join(','));
@@ -598,7 +598,7 @@ export async function launchTerminalBrowser(args, env = process.env, { timeouts
598
598
  } catch (error) {
599
599
  if (started) {
600
600
  log(`launch failed, stopping what it started: ${String(error?.message ?? error).split('\n')[0]}`);
601
- await stopTerminalBrowser(target, { cdpPort, log }).catch((stopError) => log(`rollback incomplete: ${stopError.message}`));
601
+ await stopWebDappBrowser(target, { cdpPort, log }).catch((stopError) => log(`rollback incomplete: ${stopError.message}`));
602
602
  for (const pid of ownedBrowserPids(runtime)) await terminate(pid, 3000);
603
603
  }
604
604
  throw error;
@@ -621,8 +621,7 @@ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.me
621
621
  process.exit(0);
622
622
  }
623
623
  try {
624
- const { policy } = await import(pathToFileURL(path.resolve(args['policy-module'])).href);
625
- const state = await launchTerminalBrowser(args, process.env, { policy });
624
+ const state = await launchWebDappBrowser(args);
626
625
  process.stdout.write(`${JSON.stringify({ status: 'pass', ...state }, null, args.json ? 0 : 2)}\n`);
627
626
  } catch (error) {
628
627
  const message = String(error?.stderr ?? '').trim() || error.message;
@@ -0,0 +1,24 @@
1
+ // Testnet enforcement mechanics for the web-dapp slot browser: the hosts a
2
+ // venue policy blocks become unresolvable for the whole browser (pages,
3
+ // workers, WebSockets), and the wallet host fails and logs each request it
4
+ // sees to one of them. Which hosts is the venue policy's (venueHosts).
5
+
6
+ // A URL's host as the network sees it: lowercase, no trailing dot. null when
7
+ // it does not parse.
8
+ export function hostOf(url) {
9
+ try {
10
+ return new URL(String(url)).hostname.toLowerCase().replace(/\.$/u, '');
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+
16
+ export function isBlockedUrl(url, hosts) {
17
+ const host = hostOf(url);
18
+ return host !== null && hosts.includes(host);
19
+ }
20
+
21
+ export function hostResolverRules(hosts) {
22
+ // The host and its fully qualified form (trailing dot) alike.
23
+ return `--host-resolver-rules=${hosts.flatMap((host) => [`MAP ${host} ~NOTFOUND`, `MAP ${host}. ~NOTFOUND`]).join(', ')}`;
24
+ }
@@ -1,5 +1,5 @@
1
1
  // Minimal browser-level Chrome DevTools Protocol client with flattened target
2
- // sessions, for the long-running terminal wallet host. Page-level UI actions use
2
+ // sessions, for the long-running web-dapp wallet host. Page-level UI actions use
3
3
  // @farmslot/recipe-runner/runtime/cdp instead.
4
4
 
5
5
  import WebSocket from 'ws';
@@ -1,5 +1,5 @@
1
1
  // How the slot browser is shown: headful (default) or headless, where its
2
- // window sits on screen, the page zoom that keeps the terminal's desktop layout
2
+ // window sits on screen, the page zoom that keeps the app's desktop layout
3
3
  // inside that window, and the optional slow-mo between UI actions.
4
4
 
5
5
  import { execFileSync } from 'node:child_process';
@@ -1,4 +1,4 @@
1
- // Recipe HUD for the terminal app tab: state handling and the page expression
1
+ // Recipe HUD for the web-dapp app tab: state handling and the page expression
2
2
  // that draws it.
3
3
  //
4
4
  // The overlay is an <mm-harness-hud> element under <html> (outside <body>) with
@@ -0,0 +1,79 @@
1
+ // The venue policy of the adapter that extends web-dapp (e.g. the Terminal
2
+ // plugin's plugins/terminal/policy.mjs): what web-dapp's launch, wallet host,
3
+ // readiness and actions know about the app's venue. mm-harness sets its module
4
+ // path in RECIPE_WEB_DAPP_POLICY when it binds such an adapter, so every
5
+ // web-dapp leaf process and live action it starts reads the same policy.
6
+
7
+ import { createRequire } from 'node:module';
8
+ import path from 'node:path';
9
+
10
+ export const POLICY_ENV = 'RECIPE_WEB_DAPP_POLICY';
11
+ // The policy contract this web-dapp reads; a policy declares the version it was written for.
12
+ export const POLICY_VERSION = 1;
13
+ let loaded;
14
+
15
+ // Every member web-dapp reads, checked once when the policy loads: a missing or mistyped
16
+ // member fails here, naming the module, instead of deep in a launch or an assertion (a
17
+ // missing signatureLog would silently drop the venue's L1 and mainnet rules).
18
+ const MEMBERS = {
19
+ adapterId: 'string',
20
+ checkout: 'object',
21
+ venueHosts: 'function',
22
+ linkHosts: 'array',
23
+ probe: 'object',
24
+ startChain: 'number',
25
+ refuseTypedData: 'object',
26
+ signatureLog: 'object',
27
+ startPath: 'string',
28
+ pagePath: 'function',
29
+ dependencies: 'array',
30
+ testnetVariable: 'string',
31
+ module: 'string',
32
+ };
33
+
34
+ function checkPolicy(policy, file) {
35
+ const problems = [];
36
+ if (!policy || typeof policy !== 'object') problems.push('exports no policy object');
37
+ else {
38
+ if (policy.policyVersion !== POLICY_VERSION) problems.push(`policyVersion is ${policy.policyVersion ?? 'missing'}, this web-dapp reads ${POLICY_VERSION}`);
39
+ for (const [name, type] of Object.entries(MEMBERS)) {
40
+ const value = policy[name];
41
+ const ok = type === 'array' ? Array.isArray(value) : type === 'object' ? value !== null && typeof value === 'object' : typeof value === type;
42
+ if (!ok) problems.push(`${name} must be ${type === 'array' || type === 'object' ? `an ${type}` : `a ${type}`}`);
43
+ }
44
+ const { checkout, refuseTypedData, signatureLog } = policy;
45
+ if (checkout && typeof checkout.matches !== 'function') problems.push('checkout.matches must be a function');
46
+ for (const name of ['name', 'label', 'needs']) if (checkout && typeof checkout[name] !== 'string') problems.push(`checkout.${name} must be a string`);
47
+ if (refuseTypedData && typeof refuseTypedData.reason !== 'function') problems.push('refuseTypedData.reason must be a function');
48
+ for (const name of ['kind', 'message']) if (refuseTypedData && typeof refuseTypedData[name] !== 'string') problems.push(`refuseTypedData.${name} must be a string`);
49
+ // The venue's request-log rules: both maps are required (empty only on purpose), so a
50
+ // policy cannot drop its L1 or mainnet rules by omission.
51
+ for (const name of ['typedDataClasses', 'forbiddenEntries']) {
52
+ if (signatureLog && (signatureLog[name] === null || typeof signatureLog[name] !== 'object')) problems.push(`signatureLog.${name} must be an object`);
53
+ }
54
+ }
55
+ if (problems.length) {
56
+ throw new Error(`web-dapp venue policy ${file}: ${problems.join('; ')}.\nNext: update the library that declares the adapter to a revision written for this mm-harness.`);
57
+ }
58
+ return policy;
59
+ }
60
+
61
+ export function webDappPolicy(env = process.env) {
62
+ const file = env[POLICY_ENV];
63
+ if (!file) {
64
+ throw new Error(
65
+ 'web-dapp needs a venue policy: select an adapter that extends it.\n' +
66
+ 'Next: put the library that declares it on RECIPE_LIBRARY_PATH and pass its id, e.g. --adapter terminal.',
67
+ );
68
+ }
69
+ if (loaded?.file !== file) {
70
+ // The policy module uses node built-ins only and has no top-level await, so it loads synchronously.
71
+ loaded = { file, policy: checkPolicy(createRequire(import.meta.url)(path.resolve(file)).policy, file) };
72
+ }
73
+ return loaded.policy;
74
+ }
75
+
76
+ // The bound adapter's id, for names and paths; web-dapp's own when none is bound.
77
+ export function webDappAdapterId(env = process.env) {
78
+ return env[POLICY_ENV] ? webDappPolicy(env).adapterId : 'web-dapp';
79
+ }