@deeeed/metamask-harness 0.91.0 → 0.91.2

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 CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.91.2 - 2026-10-13
6
+
7
+ ### Added
8
+
9
+ - Support the registered terminal-backend pack through the public provider, with pack-owned startup, invocation-owned teardown and indexed attempt logs. Backend recipes inherit the shared engine.
10
+ - Forward existing shared QA suite, recipe digest, owned-process and portability helpers through the public provider so dependency-free packs can use their installed tooling.
11
+
12
+ ## 0.91.1 - 2026-10-12
13
+
14
+ ### Fixed
15
+
16
+ - Restore the selected web-dapp venue policy, digest fence and MetaMask signer on each provider invocation, including live conformance after static preflight.
17
+ - Use adapter-web 0.7.2 so Terminal recipes record their owned browser through the shared recorder.
18
+
5
19
  ## 0.91.0 - 2026-10-12
6
20
 
7
21
  - Support Extension `ui.navigate page=settings` through the visible account menu and verify the Settings page before returning.
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://farmslot.io/schemas/action-manifest-v1.schema.json",
3
+ "actions": {
4
+ "end": { "description": "Finish a headless recipe.", "examples": [{ "action": "end", "status": "pass" }] }
5
+ }
6
+ }
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fencePolicy, POLICY_DIGEST_ENV, POLICY_ENV, PolicyFenceError } from "@farmslot/adapter-web/web-dapp";
4
- import { adapterPlugin, AdapterPluginError } from "@farmslot/recipe-cli/harness";
4
+ import { adapterPlugin, AdapterPluginError, harnessAdapter } from "@farmslot/recipe-cli/harness";
5
5
  import { libraryAdapterFiles } from "@farmslot/recipe-runner";
6
6
  import { configPath, declaredLibraryPathEnv, readConfig } from "../review/knowledge.js";
7
7
  import { bindWebDappPolicy } from "./web-dapp/surface.js";
@@ -41,7 +41,8 @@ async function fenceBoundPolicy() {
41
41
  throw error;
42
42
  }
43
43
  }
44
- async function afterAdapterLoad() {
44
+ async function afterAdapterLoad(adapterId) {
45
+ if (adapterId) bindPluginPolicy(harnessAdapter(adapterId));
45
46
  try {
46
47
  await fenceBoundPolicy();
47
48
  } catch (error) {
@@ -51,12 +52,16 @@ async function afterAdapterLoad() {
51
52
  throw error;
52
53
  }
53
54
  }
54
- function adoptPluginAdapter(adapter) {
55
+ function bindPluginPolicy(adapter) {
55
56
  const surface = bindWebDappPolicy(adapter);
56
57
  const policy = adapter;
57
58
  if (policy.extends === "web-dapp" && typeof policy.policy?.module === "string") {
58
59
  boundPolicy = { adapterId: adapter.id, file: policy.policy.module };
59
60
  }
61
+ return surface;
62
+ }
63
+ function adoptPluginAdapter(adapter) {
64
+ const surface = bindPluginPolicy(adapter);
60
65
  return {
61
66
  ...surface,
62
67
  run: {
@@ -0,0 +1,152 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { randomBytes } from "node:crypto";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { defineAdapter } from "@farmslot/adapter-sdk";
7
+ import { indexArtifactManifest, isRecipeExecution, usageError, writeContainedArtifact } from "@farmslot/recipe-cli/harness";
8
+ import { validatePackPortability } from "@farmslot/protocol/node/pack-portability";
9
+ async function createBackendProvider(context) {
10
+ if (!context.project?.root || !path.isAbsolute(context.project.root)) {
11
+ throw usageError("terminal-backend requires a registered project root.");
12
+ }
13
+ const root = fs.realpathSync(context.project.root);
14
+ const lifecycle = fs.realpathSync(path.join(root, "scripts/backend.mjs"));
15
+ if (!lifecycle.startsWith(root + path.sep)) throw usageError("Backend lifecycle must belong to the registered pack.");
16
+ const { configuration, health, main } = await import(pathToFileURL(lifecycle).href);
17
+ const target = context.target.value;
18
+ const ports = context.slot && "ports" in context.slot ? context.slot.ports : {};
19
+ const cfg = () => {
20
+ const state = path.join(target, ".agent/terminal-backend/service.json");
21
+ const record = fs.existsSync(state) ? JSON.parse(fs.readFileSync(state, "utf8")) : {};
22
+ return configuration(
23
+ target,
24
+ context.options?.watcherPort ?? ports.port ?? ports.devServerPort ?? record.api,
25
+ ports.redis_port ?? record.redis,
26
+ context.slot?.value ?? record.slot
27
+ );
28
+ };
29
+ const args = (action) => {
30
+ const c = cfg();
31
+ return [action, c.checkout, String(c.api), String(c.redis), c.slot];
32
+ };
33
+ const executing = isRecipeExecution(context.command, context.options ?? {});
34
+ if (executing) cfg();
35
+ const owner = executing ? randomBytes(16).toString("hex") : void 0;
36
+ const previous = /* @__PURE__ */ new Map();
37
+ const setEnv = (key, value) => {
38
+ if (!previous.has(key)) previous.set(key, process.env[key]);
39
+ process.env[key] = value;
40
+ };
41
+ const serviceLog = path.join(target, ".agent/terminal-backend/service.log");
42
+ const logOffset = fs.existsSync(serviceLog) ? fs.statSync(serviceLog).size : 0;
43
+ const output = path.resolve(target, String(context.options?.artifactsDir ?? context.project.artifactDir));
44
+ const manifest = path.join(output, "artifact-manifest.json");
45
+ const manifestIdentity = () => {
46
+ try {
47
+ const s = fs.statSync(manifest, { bigint: true });
48
+ return [s.dev, s.ino, s.size, s.mtimeNs, s.ctimeNs].join(":");
49
+ } catch (error) {
50
+ if (error.code === "ENOENT") return void 0;
51
+ throw error;
52
+ }
53
+ };
54
+ const previousManifest = manifestIdentity();
55
+ if (owner) {
56
+ setEnv("BACKEND_OWNER_ID", owner);
57
+ setEnv("BACKEND_LIFECYCLE_SCRIPT", lifecycle);
58
+ }
59
+ const cleanup = async () => {
60
+ if (owner) await main([...args("stop"), owner]);
61
+ };
62
+ const logSource = () => ({ label: "NestJS and monitor", path: serviceLog });
63
+ const hint = "Prepare the selected slot with the full profile.";
64
+ const runtime = defineAdapter({
65
+ id: "terminal-backend",
66
+ sdkVersion: 1,
67
+ targets: ["cli"],
68
+ headless: true,
69
+ resolveSlotPorts() {
70
+ const c = cfg();
71
+ setEnv("BACKEND_API_PORT", String(c.api));
72
+ setEnv("BACKEND_REDIS_PORT", String(c.redis));
73
+ setEnv("BACKEND_SLOT_ID", c.slot);
74
+ },
75
+ async runtimeStatus() {
76
+ try {
77
+ await health(cfg());
78
+ return { decision: "ready", reasons: ["Redis and a fresh complete v3 snapshot are ready."] };
79
+ } catch (error) {
80
+ return { decision: "blocked", reasons: [error.message], nextAction: hint };
81
+ }
82
+ },
83
+ readiness: { statusRuntime: true, captureProviders: [], readyIndicator: () => "OK" },
84
+ logSources: () => [logSource()],
85
+ appLogSource: logSource,
86
+ hints: { launch: hint, relaunch: hint, runtimeProbeRecovery: () => "Inspect the slot service.log and run farmslot doctor --conformance." },
87
+ devServer: {
88
+ label: "NestJS",
89
+ describe: () => "Slot backend",
90
+ stop() {
91
+ const result = spawnSync(process.execPath, [lifecycle, ...args("stop"), owner ?? ""], { encoding: "utf8", timeout: 3e4 });
92
+ if (result.error || result.status !== 0) throw result.error ?? new Error(result.stderr);
93
+ return { kind: "headless", message: "Backend stopped; Redis volume retained.", userAction: hint };
94
+ }
95
+ },
96
+ harness: {
97
+ // Installation and destructive cleanup belong to the pack's lifecycle hooks.
98
+ install: { entry: "", fallback: "" },
99
+ cleanup: { entry: "", fallback: "" },
100
+ verify: () => ({ inProcess: async () => async (_args, io) => {
101
+ const result = await validatePackPortability(root);
102
+ if (result.length) throw new Error(result.join("\n"));
103
+ io.out("Portable headless API provider; capture and HUD are unsupported.");
104
+ return 0;
105
+ } })
106
+ },
107
+ runtimeContext: { forbiddenFields: ["cdpPort", "metroPort", "simulator", "adbSerial", "extensionId"] },
108
+ launch: async () => {
109
+ await main(args("start"));
110
+ return 0;
111
+ },
112
+ actions: {
113
+ manifestPath: () => {
114
+ const product = path.join(target, "tests/recipes/action-manifest.json");
115
+ return fs.existsSync(product) ? product : fileURLToPath(new URL("../../../adapters/terminal-backend/action-manifest.json", import.meta.url));
116
+ },
117
+ semantic: [],
118
+ cdpTarget: { transport: "none", probePath: "" }
119
+ }
120
+ });
121
+ if (executing) runtime.resolveSlotPorts();
122
+ let finalized = false;
123
+ return {
124
+ runtime,
125
+ ...owner ? {
126
+ cancel: cleanup,
127
+ async finalize() {
128
+ if (finalized) return;
129
+ finalized = true;
130
+ try {
131
+ try {
132
+ await cleanup();
133
+ } finally {
134
+ const currentManifest = manifestIdentity();
135
+ if (fs.existsSync(serviceLog) && currentManifest !== void 0 && currentManifest !== previousManifest) {
136
+ await writeContainedArtifact(output, "logs/backend.log", fs.readFileSync(serviceLog).subarray(logOffset), "Backend logs");
137
+ await indexArtifactManifest(manifest, [{ category: "log", label: "Backend and monitor logs", path: "logs/backend.log", type: "text" }]);
138
+ }
139
+ }
140
+ } finally {
141
+ for (const [key, value] of previous) {
142
+ if (value === void 0) delete process.env[key];
143
+ else process.env[key] = value;
144
+ }
145
+ }
146
+ }
147
+ } : {}
148
+ };
149
+ }
150
+ export {
151
+ createBackendProvider
152
+ };
package/dist/provider.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  optionStrings,
10
10
  usageError
11
11
  } from "@farmslot/recipe-cli/harness";
12
+ import { createAdapterRegistry } from "@farmslot/adapter-sdk";
12
13
  import { MAINNET_RUN_OWNER_ENV } from "../library/actions/shared/perps/mainnet-order-ledger.mjs";
13
14
  import { adoptPluginAdapter, afterAdapterLoad, configuredLibrarySources, operatorLibraryEnv } from "./adapters/plugin-surface.js";
14
15
  import { abortRunMainnetOrders, cancelRunMainnetOrders } from "./adapters/perps-mainnet-cleanup.js";
@@ -20,14 +21,26 @@ import { providerHost } from "./host.js";
20
21
  import { configureProviderReadiness, doctorOptions } from "./provider-doctor.js";
21
22
  import { statusOptions, stopOptions } from "./provider-lifecycle.js";
22
23
  import { createLogsOptions } from "./provider-logs.js";
24
+ import { createBackendProvider } from "./adapters/terminal-backend/provider.js";
23
25
  import { providerHost as providerHost2 } from "./host.js";
24
26
  import { PUBLIC_COMMANDS } from "./command-contract.js";
27
+ import { digestRecipeDocument, RECIPE_SUITE_SCOPE_SCHEMA_URL, validateRecipeActionManifestDocument } from "@farmslot/protocol";
28
+ import { validatePackPortability, packMachineNames } from "@farmslot/protocol/node/pack-portability";
29
+ import { freezeRecipeSuiteScope, finalizeRecipeSuite, digestRecipeLibrary } from "@farmslot/recipe-runner";
30
+ import { runOwnedRecipeProcess } from "@farmslot/recipe-runner/adapters/core";
31
+ import { withRecipeSignals } from "@farmslot/recipe-cli/harness";
25
32
  async function createProvider(context) {
26
33
  configureHarnessHost(context.host ?? providerHost);
27
34
  configureHarnessAdapters(adapterRegistry);
28
35
  const selection = context.adapter?.value;
29
36
  if (!selection) throw usageError("The MetaMask provider requires a resolved adapter.");
30
37
  const adapter = adapterForPlatform(selection) ?? selection;
38
+ if (adapter === "terminal-backend") {
39
+ const registry = createAdapterRegistry();
40
+ for (const id of adapterRegistry.list()) registry.register(adapterRegistry.get(id));
41
+ configureHarnessAdapters(registry);
42
+ return createBackendProvider(context);
43
+ }
31
44
  const options = {
32
45
  ...context.options,
33
46
  ...!context.options?.platform && (selection === "ios" || selection === "android") ? { platform: selection } : {}
@@ -38,7 +51,7 @@ async function createProvider(context) {
38
51
  env: operatorLibraryEnv(),
39
52
  adopt: adoptPluginAdapter
40
53
  });
41
- await afterAdapterLoad();
54
+ await afterAdapterLoad(adapter);
42
55
  const runtime = getAdapterSurface(adapter);
43
56
  configureProviderReadiness(runtime);
44
57
  const executes = isRecipeExecution(context.command, options);
@@ -73,7 +86,17 @@ async function createProvider(context) {
73
86
  };
74
87
  }
75
88
  export {
89
+ RECIPE_SUITE_SCOPE_SCHEMA_URL,
76
90
  createProvider,
91
+ digestRecipeDocument,
92
+ digestRecipeLibrary,
93
+ finalizeRecipeSuite,
94
+ freezeRecipeSuiteScope,
95
+ packMachineNames,
77
96
  PUBLIC_COMMANDS as providerCommands,
78
- providerHost2 as providerHost
97
+ providerHost2 as providerHost,
98
+ runOwnedRecipeProcess,
99
+ validatePackPortability,
100
+ validateRecipeActionManifestDocument,
101
+ withRecipeSignals
79
102
  };
@@ -0,0 +1,21 @@
1
+ # Terminal backend provider
2
+
3
+ Registered backend packs select `@deeeed/metamask-harness/provider` and adapter `terminal-backend`. The pool supplies the installed package through `MM_HARNESS_PACKAGE_ROOT`. The shared project loader supplies `project.root`, the directory containing the registered pack's `project.json`.
4
+
5
+ The provider imports that pack's `scripts/backend.mjs` and uses its existing `configuration`, `health` and `main` functions. The pack owns service startup, health checks, Redis ports, startup recovery and volumes. Slot resources must supply the API port and `redis_port`; there is no default Redis port. Installation and destructive cleanup run through pack hooks, not harness overlay installation.
6
+
7
+ Backend recipes inherit the shared engine. Their action manifest lives at the selected checkout's `tests/recipes/action-manifest.json`; a packaged `end` manifest supports an empty checkout during planning. Recipe assets remain with the backend code. No backend recipes or generic runner implementations are bundled here.
8
+
9
+ Executing `run` and `call` invocations assign a unique `BACKEND_OWNER_ID` and expose `BACKEND_LIFECYCLE_SCRIPT` and slot ports to action children. Cancellation and finalization stop only that owner through the pack lifecycle. Finalization indexes the current attempt's service log when this invocation creates or updates the shared artifact manifest and restores caller environment values. Early startup failures leave previous evidence packages untouched; service diagnostics remain in the slot's runtime log. Attached services keep their existing ownership. Previews assign no owner and start no services. Capture and HUD are unsupported for this headless runtime.
10
+
11
+ The public provider forwards these existing shared APIs for dependency-free packs. Their contracts and versions remain owned by the listed packages:
12
+
13
+ | Package | Forwarded exports |
14
+ | --- | --- |
15
+ | `@farmslot/protocol` | `digestRecipeDocument`, `RECIPE_SUITE_SCOPE_SCHEMA_URL`, `validateRecipeActionManifestDocument` |
16
+ | `@farmslot/protocol/node/pack-portability` | `validatePackPortability`, `packMachineNames` |
17
+ | `@farmslot/recipe-runner` | `freezeRecipeSuiteScope`, `finalizeRecipeSuite`, `digestRecipeLibrary` |
18
+ | `@farmslot/recipe-runner/adapters/core` | `runOwnedRecipeProcess` |
19
+ | `@farmslot/recipe-cli/harness` | `withRecipeSignals` |
20
+
21
+ The backend pack's mandatory QA gate consumes the suite, digest, process and signal APIs. Its CI and prerequisite hooks consume portability. Packs import the public provider export and do not resolve private files or transitive package paths.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.91.0",
3
+ "version": "0.91.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -24,7 +24,7 @@
24
24
  "@farmslot/adapter-node": "0.1.14",
25
25
  "@farmslot/adapter-rn": "0.20.1",
26
26
  "@farmslot/adapter-sdk": "0.9.1",
27
- "@farmslot/adapter-web": "0.7.1",
27
+ "@farmslot/adapter-web": "0.7.2",
28
28
  "@farmslot/agent-runtime": "0.18.1",
29
29
  "@farmslot/handoff": "^0.3.1",
30
30
  "@farmslot/protocol": "0.40.0",