@deeeed/metamask-harness 0.19.1 → 0.21.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 (42) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/adapters/core/inject.sh +1 -3
  3. package/adapters/extension/ensure-browser.sh +6 -4
  4. package/adapters/extension/inject.mjs +1 -1
  5. package/adapters/extension/launch-browser.cjs +6 -1
  6. package/adapters/extension/lib/chrome-args.cjs +11 -1
  7. package/adapters/extension/start-watch.sh +1 -1
  8. package/adapters/extension/verify.sh +17 -0
  9. package/adapters/mobile/inject.sh +1 -1
  10. package/adapters/mobile/open-device.sh +12 -1
  11. package/adapters/mobile/wait-for-bridge.sh +1 -1
  12. package/dist/adapters/extension/ensure-ready.js +7 -2
  13. package/dist/adapters/extension/runtime-decision.js +4 -6
  14. package/dist/adapters/extension/runtime.js +114 -17
  15. package/dist/adapters/mobile/prepare.js +40 -6
  16. package/dist/command-contract.js +58 -5
  17. package/dist/commands/call.js +7 -4
  18. package/dist/commands/check.js +9 -2
  19. package/dist/commands/checklist.js +117 -14
  20. package/dist/commands/launch/extension.js +8 -3
  21. package/dist/commands/launch/index.js +38 -25
  22. package/dist/commands/launch/mobile.js +2 -2
  23. package/dist/commands/manifest.js +86 -25
  24. package/dist/commands/run-engine.js +7 -2
  25. package/dist/heal-bounds.js +1 -1
  26. package/dist/manifest.js +85 -101
  27. package/dist/metamask-action-validation.js +45 -0
  28. package/dist/mm-harness-cli.js +7 -4
  29. package/dist/runner.js +9 -4
  30. package/docs/CONTRIBUTING.md +8 -0
  31. package/docs/RECIPES.md +2 -11
  32. package/library/actions/extension/platform/cdp.mjs +8 -3
  33. package/library/actions/extension/ui/navigate.mjs +239 -16
  34. package/library/actions/mobile/ui/navigate.mjs +1 -1
  35. package/library/manifests/core.action-manifest.json +357 -489
  36. package/library/manifests/extension.action-manifest.json +616 -885
  37. package/library/manifests/mobile.action-manifest.json +647 -931
  38. package/library/recipes/perps/lifecycle.recipe.json +3 -9
  39. package/library/recipes/runner/action-validation.extension.recipe.json +5 -4
  40. package/package.json +5 -4
  41. package/scripts/completions.sh +2 -2
  42. package/library/library.json +0 -7
@@ -20,6 +20,7 @@ import {
20
20
  } from "../paths.js";
21
21
  import { captureHelperSupportsRecordSessionSnapshots } from "../recording-target.js";
22
22
  import { startRecipeRecording, stopRecipeRecording } from "../run-recording.js";
23
+ import { validateMetaMaskActionInputs } from "../metamask-action-validation.js";
23
24
  import {
24
25
  beginRunDiagnostics,
25
26
  finishRunDiagnostics
@@ -276,7 +277,10 @@ async function validateRecipeAdapterAware(adapter, recipe, manifest, librarySour
276
277
  manifest,
277
278
  validationOptions
278
279
  );
279
- const findings = [...withManifest.findings];
280
+ const findings = [
281
+ ...withManifest.findings,
282
+ ...validateMetaMaskActionInputs(recipe)
283
+ ];
280
284
  if (withManifest.status === "valid" && isRecord(recipe) && libraryResolution) {
281
285
  try {
282
286
  const digest = protocol.digestRecipeDocument(recipe);
@@ -304,7 +308,8 @@ async function validateRecipeAdapterAware(adapter, recipe, manifest, librarySour
304
308
  dependency.document,
305
309
  manifest,
306
310
  validationOptions
307
- ).findings
311
+ ).findings,
312
+ ...validateMetaMaskActionInputs(dependency.document)
308
313
  );
309
314
  }
310
315
  } catch (error) {
@@ -62,7 +62,7 @@ async function ensureOverlay(adapter, target, heal, state, json) {
62
62
  }
63
63
  function classifyFailure(output) {
64
64
  if (/wallet|fixture|keyring|not seeded|\bsrp\b|password|onboard/iu.test(output)) return "wallet";
65
- if (/metro|cdp|chrome|bundle|packager|port\b|econnrefused|not reachable|watcher|dev client|websocket/iu.test(output)) {
65
+ if (/metro|cdp|chrome|bridge|bundle|packager|port\b|econnrefused|not reachable|watcher|dev client|websocket/iu.test(output)) {
66
66
  return "infra";
67
67
  }
68
68
  return "app";
package/dist/manifest.js CHANGED
@@ -1,10 +1,8 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { OFFICIAL_RECIPE_ACTIONS } from "@farmslot/protocol";
3
4
  import { manifestPath, readJson, importRecipeProtocol } from "./paths.js";
4
- import {
5
- actionSourceIntegrity,
6
- metaMaskActionExecutionCapabilities
7
- } from "./recipe-security.js";
5
+ import { actionSourceIntegrity } from "./recipe-security.js";
8
6
  function loadMetaMaskMobileActionManifest() {
9
7
  return asActionManifest(readJson(manifestPath("mobile")));
10
8
  }
@@ -26,12 +24,12 @@ async function resolveActionManifest(adapter, overridePath, librarySources, task
26
24
  const implementationRoot = path.resolve(
27
25
  taskActionRoot ?? defaultImplementationRoot(manifestPath2)
28
26
  );
29
- const manifest = withMetaMaskExecutionCapabilities(loadActionManifest(adapter, manifestPath2));
27
+ const manifest = loadActionManifest(adapter, manifestPath2);
30
28
  await validateManifest(manifest);
31
29
  const integrity = await actionSourceIntegrity(manifestPath2, "task", implementationRoot);
32
30
  return {
33
31
  manifest,
34
- actionSources: describeManifestSources(manifest, {
32
+ actionSources: await describeManifestSources(manifest, {
35
33
  name: "task",
36
34
  tier: "task",
37
35
  manifestPath: manifestPath2,
@@ -41,109 +39,125 @@ async function resolveActionManifest(adapter, overridePath, librarySources, task
41
39
  })
42
40
  };
43
41
  }
44
- const canonicalPath = manifestPath(adapter);
45
- const sources = librarySources?.length ? librarySources : [{ name: "metamask", root: path.dirname(path.dirname(canonicalPath)) }];
42
+ const canonicalPath = path.resolve(manifestPath(adapter));
43
+ const canonicalRoot = path.dirname(path.dirname(canonicalPath));
44
+ const sources = librarySources?.length ? librarySources : [{ name: "metamask", root: canonicalRoot }];
46
45
  const manifests = [];
47
46
  for (const source of sources) {
48
- const file = path.join(source.root, "manifests", `${adapter}.action-manifest.json`);
47
+ const file = path.resolve(source.root, "manifests", `${adapter}.action-manifest.json`);
49
48
  if (!fs.existsSync(file)) continue;
50
49
  const manifest = asActionManifest(readJson(file));
51
50
  await validateManifest(manifest);
52
51
  manifests.push({ source, file, manifest });
53
52
  }
54
- if (!manifests.some((entry) => path.resolve(entry.file) === path.resolve(canonicalPath))) {
53
+ if (!manifests.some((entry) => entry.file === canonicalPath)) {
55
54
  const manifest = loadActionManifest(adapter);
56
55
  await validateManifest(manifest);
57
- manifests.push({ source: { name: "metamask", root: path.dirname(path.dirname(canonicalPath)) }, file: canonicalPath, manifest });
56
+ manifests.push({
57
+ source: { name: "metamask", root: canonicalRoot },
58
+ file: canonicalPath,
59
+ manifest
60
+ });
58
61
  }
59
- const canonical = manifests.find((entry) => entry.source.name === "metamask") ?? manifests.at(-1);
62
+ const canonical = manifests.find((entry) => entry.file === canonicalPath);
60
63
  if (!canonical) throw new Error(`No ${adapter} action manifest could be resolved.`);
61
- const base = cloneManifest(canonical.manifest);
62
- const official = new Set(stringArray(recordOf(base).supported_official_actions));
63
- const custom = /* @__PURE__ */ new Map();
64
- const customMetadata = /* @__PURE__ */ new Map();
65
- const bindings = /* @__PURE__ */ new Map();
64
+ const official = new Set(OFFICIAL_RECIPE_ACTIONS);
65
+ const canonicalActions = actionsOf(canonical.manifest);
66
+ const mergedActions = {};
67
+ for (const [name, entry] of Object.entries(canonicalActions)) {
68
+ if (official.has(name)) mergedActions[name] = entry;
69
+ }
66
70
  const canonicalImplementationRoot = path.join(canonical.source.root, "actions");
67
71
  const canonicalIntegrity = await actionSourceIntegrity(
68
72
  canonical.file,
69
73
  "canonical",
70
74
  canonicalImplementationRoot
71
75
  );
72
- const actionSources = describeManifestSources(base, {
73
- name: "metamask",
74
- tier: "canonical",
75
- manifestPath: canonical.file,
76
- implementationRoot: canonicalImplementationRoot,
77
- trust: "trusted",
78
- ...canonicalIntegrity
79
- });
80
- for (const entry of manifests) {
81
- const sourceName = entry.source.name ?? path.basename(entry.source.root);
82
- const tier = sourceName === "metamask" ? "canonical" : sourceName === "personal" ? "personal" : "team";
76
+ const actionSources = /* @__PURE__ */ new Map();
77
+ for (const name of Object.keys(mergedActions)) {
78
+ actionSources.set(name, {
79
+ name: "official",
80
+ tier: "official",
81
+ manifestPath: canonical.file
82
+ });
83
+ }
84
+ const custom = /* @__PURE__ */ new Set();
85
+ const ordered = [
86
+ ...manifests.filter((entry) => entry.file !== canonicalPath),
87
+ canonical
88
+ ];
89
+ for (const entry of ordered) {
90
+ const isCanonical = entry.file === canonicalPath;
91
+ if (!isCanonical && entry.manifest.observers?.length) {
92
+ throw new Error(
93
+ `Library manifest ${entry.file} declares observers, but external library observers are not executable; remove observers or use an explicit task action manifest.`
94
+ );
95
+ }
96
+ const sourceName = isCanonical ? "metamask" : entry.source.name ?? path.basename(entry.source.root);
97
+ const tier = isCanonical ? "canonical" : sourceName === "personal" ? "personal" : "team";
83
98
  const implementationRoot = path.join(entry.source.root, "actions");
84
- const integrity = await actionSourceIntegrity(entry.file, tier, implementationRoot);
99
+ const integrity = isCanonical ? canonicalIntegrity : await actionSourceIntegrity(entry.file, tier, implementationRoot);
85
100
  const sourceInfo = {
86
101
  name: sourceName,
87
102
  tier,
88
103
  manifestPath: entry.file,
89
104
  implementationRoot,
90
- trust: entry.source.provenance?.trust ?? (tier === "canonical" ? "trusted" : "unknown"),
105
+ trust: entry.source.provenance?.trust ?? (isCanonical ? "trusted" : "unknown"),
91
106
  ...integrity
92
107
  };
93
- const manifestRecord = recordOf(entry.manifest);
94
- const metadata = recordOf(manifestRecord.action_metadata);
95
- for (const action of customActions(manifestRecord.custom_actions)) {
96
- if (official.has(action.name)) {
97
- throw new Error(`Library action ${action.name} from ${entry.file} conflicts with an official action.`);
108
+ for (const [name, action] of Object.entries(actionsOf(entry.manifest))) {
109
+ if (official.has(name)) {
110
+ if (!isCanonical) {
111
+ throw new Error(
112
+ `Library action ${name} from ${entry.file} conflicts with an official action; team libraries may declare namespaced custom actions only.`
113
+ );
114
+ }
115
+ continue;
98
116
  }
99
- if (custom.has(action.name)) {
100
- const existing = actionSources.get(action.name);
117
+ if (custom.has(name)) {
118
+ const existing = actionSources.get(name);
101
119
  if (existing && existing.name !== sourceName) {
102
120
  existing.shadows = [...existing.shadows ?? [], sourceName];
103
121
  }
104
122
  continue;
105
123
  }
106
- custom.set(action.name, action.value);
107
- customMetadata.set(action.name, metadata[action.name]);
108
- actionSources.set(action.name, { ...sourceInfo });
109
- }
110
- for (const binding of objectArray(manifestRecord.native_bindings)) {
111
- const action = typeof binding.action === "string" ? binding.action : void 0;
112
- if (action && !bindings.has(action)) bindings.set(action, binding);
124
+ custom.add(name);
125
+ mergedActions[name] = action;
126
+ actionSources.set(name, { ...sourceInfo });
113
127
  }
114
128
  }
115
- const baseRecord = recordOf(base);
116
- baseRecord.custom_actions = [...custom.values()];
117
- const baseMetadata = recordOf(baseRecord.action_metadata);
118
- for (const [name, metadata] of customMetadata) {
119
- if (metadata !== void 0) baseMetadata[name] = metadata;
129
+ return {
130
+ manifest: {
131
+ $schema: canonical.manifest.$schema,
132
+ actions: mergedActions,
133
+ ...canonical.manifest.observers ? { observers: canonical.manifest.observers } : {}
134
+ },
135
+ actionSources
136
+ };
137
+ }
138
+ function actionsOf(manifest) {
139
+ return manifest.actions;
140
+ }
141
+ async function describeManifestSources(manifest, customSource) {
142
+ const official = new Set(OFFICIAL_RECIPE_ACTIONS);
143
+ const sources = /* @__PURE__ */ new Map();
144
+ for (const name of Object.keys(actionsOf(manifest))) {
145
+ if (official.has(name)) {
146
+ sources.set(name, {
147
+ name: "official",
148
+ tier: "official",
149
+ manifestPath: customSource.manifestPath
150
+ });
151
+ continue;
152
+ }
153
+ sources.set(name, { ...customSource });
120
154
  }
121
- baseRecord.action_metadata = baseMetadata;
122
- baseRecord.native_bindings = [...bindings.values()];
123
- return { manifest: withMetaMaskExecutionCapabilities(base), actionSources };
155
+ return sources;
124
156
  }
125
157
  function defaultImplementationRoot(selectedManifestPath) {
126
158
  const manifestDirectory = path.dirname(selectedManifestPath);
127
159
  return path.basename(manifestDirectory) === "manifests" ? path.join(path.dirname(manifestDirectory), "actions") : path.join(manifestDirectory, "actions");
128
160
  }
129
- function withMetaMaskExecutionCapabilities(manifest) {
130
- const result = cloneManifest(manifest);
131
- const record = recordOf(result);
132
- if (!Array.isArray(record.custom_actions)) return result;
133
- record.custom_actions = record.custom_actions.map((entry) => {
134
- const name = typeof entry === "string" ? entry : recordOf(entry).name;
135
- if (typeof name !== "string") return entry;
136
- const capabilities = metaMaskActionExecutionCapabilities(name);
137
- if (capabilities.length === 0) return entry;
138
- const declaredValue = typeof entry === "string" ? void 0 : recordOf(entry).execution_capabilities;
139
- const declared = Array.isArray(declaredValue) ? declaredValue.filter(
140
- (capability) => typeof capability === "string"
141
- ) : [];
142
- const executionCapabilities = [.../* @__PURE__ */ new Set([...declared, ...capabilities])];
143
- return typeof entry === "string" ? { name, execution_capabilities: executionCapabilities } : { ...recordOf(entry), execution_capabilities: executionCapabilities };
144
- });
145
- return result;
146
- }
147
161
  async function validateManifest(manifest) {
148
162
  const { validateRecipeActionManifestDocument } = await importRecipeProtocol();
149
163
  const result = validateRecipeActionManifestDocument(manifest);
@@ -157,41 +171,11 @@ async function validateManifest(manifest) {
157
171
  function asActionManifest(value) {
158
172
  return value;
159
173
  }
160
- function cloneManifest(value) {
161
- return JSON.parse(JSON.stringify(value));
162
- }
163
- function recordOf(value) {
164
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
165
- }
166
- function stringArray(value) {
167
- return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
168
- }
169
- function objectArray(value) {
170
- return Array.isArray(value) ? value.filter((entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry)) : [];
171
- }
172
- function customActions(value) {
173
- if (!Array.isArray(value)) return [];
174
- return value.flatMap((entry) => {
175
- if (typeof entry === "string") return [{ name: entry, value: entry }];
176
- const record = recordOf(entry);
177
- return typeof record.name === "string" ? [{ name: record.name, value: entry }] : [];
178
- });
179
- }
180
- function describeManifestSources(manifest, customSource) {
181
- const record = recordOf(manifest);
182
- const sources = /* @__PURE__ */ new Map();
183
- for (const name of stringArray(record.supported_official_actions)) {
184
- sources.set(name, { name: "official", tier: "official", manifestPath: customSource.manifestPath });
185
- }
186
- for (const action of customActions(record.custom_actions)) sources.set(action.name, { ...customSource });
187
- return sources;
188
- }
189
174
  export {
190
175
  loadActionManifest,
191
176
  loadMetaMaskCoreActionManifest,
192
177
  loadMetaMaskExtensionActionManifest,
193
178
  loadMetaMaskMobileActionManifest,
194
179
  resolveActionManifest,
195
- validateManifest,
196
- withMetaMaskExecutionCapabilities
180
+ validateManifest
197
181
  };
@@ -0,0 +1,45 @@
1
+ function isRecord(value) {
2
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
3
+ }
4
+ function validateMetaMaskActionInputs(recipe) {
5
+ if (!isRecord(recipe) || !isRecord(recipe.workflow)) return [];
6
+ const nodes = isRecord(recipe.workflow.nodes) ? recipe.workflow.nodes : void 0;
7
+ if (!nodes) return [];
8
+ const findings = [];
9
+ for (const [nodeId, node] of Object.entries(nodes)) {
10
+ if (!isRecord(node)) continue;
11
+ const action = node.action;
12
+ const state = typeof node.state === "string" ? node.state.toLowerCase() : void 0;
13
+ const createsOrder = action === "metamask.perps.place_order" || (action === "metamask.perps.ensure_positions" || action === "metamask.perps.ensure_orders") && (state === "open" || state === "present");
14
+ if (!createsOrder) continue;
15
+ const requireOneOf = (names, message) => {
16
+ const present = names.some((name) => {
17
+ const value = node[name];
18
+ return value !== void 0 && value !== null && value !== "";
19
+ });
20
+ if (present) return;
21
+ findings.push({
22
+ severity: "error",
23
+ code: "recipe.missing_param",
24
+ path: `workflow.nodes.${nodeId}.${names[0]}`,
25
+ message
26
+ });
27
+ };
28
+ requireOneOf(
29
+ ["market", "symbol"],
30
+ `${action} requires market=<symbol> (or symbol=<symbol>) before it may create an order`
31
+ );
32
+ requireOneOf(
33
+ ["side"],
34
+ `${action} requires side=long or side=short before it may create an order`
35
+ );
36
+ requireOneOf(
37
+ ["amount", "notional"],
38
+ `${action} requires amount=<usd> or notional=<usd> before it may create an order`
39
+ );
40
+ }
41
+ return findings;
42
+ }
43
+ export {
44
+ validateMetaMaskActionInputs
45
+ };
@@ -60,11 +60,13 @@ Example:
60
60
  },
61
61
  {
62
62
  name: "checklist",
63
- summary: "Mark task-local checklist progress through the bundled agent runtime.",
63
+ summary: "Mark checklist progress and close out scrubbed learning packages.",
64
64
  example: "mm-harness checklist mark <task-dir> start",
65
- helpText: `mm-harness checklist mark <task-dir> <step> [options]
65
+ helpText: `mm-harness checklist <mark|closeout> <task-dir> [step] [options]
66
66
 
67
- Mark checklist progress through mm-harness's bundled @farmslot/agent-runtime.
67
+ mark delegates checklist state to @farmslot/agent-runtime. Successful complete
68
+ and no-change marks stage a learning package through @farmslot/handoff.
69
+ closeout exposes that same Handoff boundary directly; --share remains explicit.
68
70
  The task directory must contain CHECKLIST.md and checklist-target.json.
69
71
 
70
72
  Steps: start | 1 | 2 | ... | complete | no-change | blocked
@@ -72,7 +74,8 @@ Example:
72
74
  Example:
73
75
  mm-harness checklist mark temp/tasks/recipe-cook/<task> start
74
76
  mm-harness checklist mark temp/tasks/recipe-cook/<task> 1
75
- mm-harness checklist mark temp/tasks/recipe-cook/<task> complete --mark-last`
77
+ mm-harness checklist mark temp/tasks/recipe-cook/<task> complete --mark-last
78
+ mm-harness checklist closeout temp/tasks/recipe-cook/<task> --share`
76
79
  },
77
80
  {
78
81
  name: "actions",
package/dist/runner.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { execSync } from "node:child_process";
2
+ import { OFFICIAL_RECIPE_ACTIONS } from "@farmslot/protocol";
2
3
  import { createMetaMaskAdapters, createMetaMaskUiTransport } from "./adapters.js";
3
4
  import {
4
5
  mobileSourceFingerprint,
@@ -6,11 +7,13 @@ import {
6
7
  } from "./adapters/mobile/source-freshness.js";
7
8
  import { resolveMetaMaskMobileLifecycleTarget } from "./app-lifecycle.js";
8
9
  import { loadMetaMaskExtensionActionManifest, loadMetaMaskMobileActionManifest } from "./manifest.js";
10
+ import { metaMaskActionExecutionCapabilities } from "./recipe-security.js";
9
11
  import {
10
12
  prepareLiveAdapterScript
11
13
  } from "./live-adapter-contract.js";
12
14
  import { createMetaMaskRecordingTargetProvider } from "./recording-target.js";
13
15
  import { importRecipeHarness, importRecipeHarnessAppLifecycle, importRecipeHarnessRuntimeCdp, importRecipeHarnessRuntimeReactNativeBridge, runnerDir } from "./paths.js";
16
+ const OFFICIAL_ACTIONS = new Set(OFFICIAL_RECIPE_ACTIONS);
14
17
  async function createMetaMaskMobileRunner(options = {}) {
15
18
  return createMetaMaskRunner(
16
19
  "mobile",
@@ -32,9 +35,8 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
32
35
  const { createCdpWebUiTransport } = await importRecipeHarnessRuntimeCdp();
33
36
  const { createReactNativeBridgeUiTransport } = await importRecipeHarnessRuntimeReactNativeBridge();
34
37
  const { createAppLifecycleAdapters } = await importRecipeHarnessAppLifecycle();
35
- const customActions = (actionManifest.custom_actions ?? []).map(
36
- (entry) => typeof entry === "string" ? entry : entry.name
37
- );
38
+ const actions = Object.keys(actionManifest.actions);
39
+ const customActions = actions.filter((action) => !OFFICIAL_ACTIONS.has(action));
38
40
  const actionSources = options.actionSources ? new Map([...options.actionSources].map(([action, source]) => [action, { ...source }])) : void 0;
39
41
  const preparedLiveAdapters = await prepareLiveAdapterImplementations(
40
42
  adapter,
@@ -42,7 +44,6 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
42
44
  actionSources,
43
45
  options.trustTaskActions
44
46
  );
45
- const actions = [...actionManifest.supported_official_actions, ...customActions];
46
47
  const declaredActions = new Set(actions);
47
48
  const core = createStandardCoreAdapters({ actions });
48
49
  const projectOwnedOfficialActions = /* @__PURE__ */ new Set(["app.status", "app.lifecycle"]);
@@ -144,8 +145,10 @@ async function prepareLiveAdapterImplementations(platform, actions, sources, tru
144
145
  }
145
146
  function adapterSecurity(action, sources, trustTaskActions = false) {
146
147
  const source = sources?.get(action);
148
+ const capabilities = !OFFICIAL_ACTIONS.has(action) ? { capabilities: metaMaskActionExecutionCapabilities(action) } : {};
147
149
  if (source?.tier === "task") {
148
150
  return {
151
+ ...capabilities,
149
152
  source: {
150
153
  kind: "custom-adapter",
151
154
  trust: trustTaskActions ? "trusted" : "untrusted",
@@ -158,6 +161,7 @@ function adapterSecurity(action, sources, trustTaskActions = false) {
158
161
  }
159
162
  if (source && source.tier !== "official") {
160
163
  return {
164
+ ...capabilities,
161
165
  source: {
162
166
  kind: source.tier === "canonical" ? "bundled" : "custom-adapter",
163
167
  trust: source.trust ?? (source.tier === "canonical" ? "trusted" : "unknown"),
@@ -169,6 +173,7 @@ function adapterSecurity(action, sources, trustTaskActions = false) {
169
173
  };
170
174
  }
171
175
  return {
176
+ ...capabilities,
172
177
  source: {
173
178
  kind: "bundled",
174
179
  trust: "trusted",
@@ -8,6 +8,8 @@ both the machine contract and the visible human result.
8
8
  ```text
9
9
  bin/mm-harness
10
10
  -> src/ typed CLI and product decisions
11
+ -> @farmslot/agent-runtime checklist state
12
+ -> @farmslot/handoff scrubbed learning capture/share
11
13
  -> @farmslot/recipe-harness generic execution, UI transports, evidence
12
14
  -> @farmslot/protocol schemas
13
15
  -> adapters/ focused host/browser/device leaves
@@ -18,6 +20,8 @@ bin/mm-harness
18
20
  | Layer | Owns |
19
21
  |---|---|
20
22
  | `@farmslot/protocol` | recipe and evidence schemas |
23
+ | `@farmslot/agent-runtime` | task-local checklist state and terminal contracts |
24
+ | `@farmslot/handoff` | learning assembly, scrubbing, validation, approval, and publication |
21
25
  | `@farmslot/recipe-harness` | generic execution, recovery, traces, artifacts, `ui.*` |
22
26
  | `mm-harness` | MetaMask runtime control, diagnostics, durable domain actions |
23
27
  | skills/checklists | task workflow and proof expectations |
@@ -27,6 +31,10 @@ Generic bootstrap, trust, receipts, and recovery belong upstream. MetaMask
27
31
  platform behavior and risk classification belong here. Ticket assertions stay
28
32
  task-local.
29
33
 
34
+ `mm-harness checklist` is only the MetaMask-facing dispatch layer. Terminal
35
+ marks stage learnings through Handoff; sharing always requires a separate
36
+ explicit `--share` call.
37
+
30
38
  ## Repository map
31
39
 
32
40
  | Path | Responsibility |
package/docs/RECIPES.md CHANGED
@@ -89,21 +89,11 @@ The protocol is authoritative:
89
89
 
90
90
  ```text
91
91
  team-recipes/
92
- library.json
93
92
  manifests/extension.action-manifest.json
94
93
  actions/extension/wallet/ensure_ready.mjs
95
94
  recipes/onboarding/smoke.extension.recipe.json
96
95
  ```
97
96
 
98
- ```json
99
- {
100
- "kind": "recipe-library",
101
- "schema_version": 1,
102
- "name": "wallet-team",
103
- "owner": "wallet-team"
104
- }
105
- ```
106
-
107
97
  ```bash
108
98
  export RECIPE_LIBRARY_PATH="wallet=$HOME/shared-library/wallet-team"
109
99
  mm-harness run --list
@@ -111,7 +101,8 @@ mm-harness run onboarding.smoke --describe
111
101
  mm-harness run onboarding.smoke --plan
112
102
  ```
113
103
 
114
- Use `--library wallet=/path/to/library` for one command. Resolution follows the
104
+ Use `--library wallet=/path/to/library` for one command. The alias is the
105
+ source name; without one, the directory name is used. Resolution follows the
115
106
  explicit library order, then bundled MetaMask. Adapter-specific variants are
116
107
  selected deterministically. Every run records the root recipe, exact resolved
117
108
  dependency documents, their digests, call edges, selected sources, and shadows.
@@ -575,10 +575,15 @@ export class ExtensionPage extends CdpWebPage {
575
575
  this.port = port;
576
576
  }
577
577
 
578
- async navigateHash(hash) {
578
+ async navigateHash(hash, timeoutMs = 10000) {
579
579
  const normalizedHash = String(hash || '').startsWith('#') ? hash : `#${hash || '/'}`;
580
- const href = `${this.origin}/home.html${normalizedHash}`;
581
- return this.navigate(href);
580
+ const navigation = await this.evaluate(`(() => {
581
+ const before = location.href;
582
+ location.hash = ${JSON.stringify(normalizedHash)};
583
+ return { before, href: location.href };
584
+ })()`);
585
+ await this.waitForDocumentReady({ expectedUrl: navigation.href, timeoutMs });
586
+ return { ...navigation, sameDocument: true };
582
587
  }
583
588
 
584
589
  async readPositions() {