@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.
- package/CHANGELOG.md +28 -0
- package/adapters/core/inject.sh +1 -3
- package/adapters/extension/ensure-browser.sh +6 -4
- package/adapters/extension/inject.mjs +1 -1
- package/adapters/extension/launch-browser.cjs +6 -1
- package/adapters/extension/lib/chrome-args.cjs +11 -1
- package/adapters/extension/start-watch.sh +1 -1
- package/adapters/extension/verify.sh +17 -0
- package/adapters/mobile/inject.sh +1 -1
- package/adapters/mobile/open-device.sh +12 -1
- package/adapters/mobile/wait-for-bridge.sh +1 -1
- package/dist/adapters/extension/ensure-ready.js +7 -2
- package/dist/adapters/extension/runtime-decision.js +4 -6
- package/dist/adapters/extension/runtime.js +114 -17
- package/dist/adapters/mobile/prepare.js +40 -6
- package/dist/command-contract.js +58 -5
- package/dist/commands/call.js +7 -4
- package/dist/commands/check.js +9 -2
- package/dist/commands/checklist.js +117 -14
- package/dist/commands/launch/extension.js +8 -3
- package/dist/commands/launch/index.js +38 -25
- package/dist/commands/launch/mobile.js +2 -2
- package/dist/commands/manifest.js +86 -25
- package/dist/commands/run-engine.js +7 -2
- package/dist/heal-bounds.js +1 -1
- package/dist/manifest.js +85 -101
- package/dist/metamask-action-validation.js +45 -0
- package/dist/mm-harness-cli.js +7 -4
- package/dist/runner.js +9 -4
- package/docs/CONTRIBUTING.md +8 -0
- package/docs/RECIPES.md +2 -11
- package/library/actions/extension/platform/cdp.mjs +8 -3
- package/library/actions/extension/ui/navigate.mjs +239 -16
- package/library/actions/mobile/ui/navigate.mjs +1 -1
- package/library/manifests/core.action-manifest.json +357 -489
- package/library/manifests/extension.action-manifest.json +616 -885
- package/library/manifests/mobile.action-manifest.json +647 -931
- package/library/recipes/perps/lifecycle.recipe.json +3 -9
- package/library/recipes/runner/action-validation.extension.recipe.json +5 -4
- package/package.json +5 -4
- package/scripts/completions.sh +2 -2
- 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 = [
|
|
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) {
|
package/dist/heal-bounds.js
CHANGED
|
@@ -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 =
|
|
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
|
|
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.
|
|
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) =>
|
|
53
|
+
if (!manifests.some((entry) => entry.file === canonicalPath)) {
|
|
55
54
|
const manifest = loadActionManifest(adapter);
|
|
56
55
|
await validateManifest(manifest);
|
|
57
|
-
manifests.push({
|
|
56
|
+
manifests.push({
|
|
57
|
+
source: { name: "metamask", root: canonicalRoot },
|
|
58
|
+
file: canonicalPath,
|
|
59
|
+
manifest
|
|
60
|
+
});
|
|
58
61
|
}
|
|
59
|
-
const canonical = manifests.find((entry) => entry.
|
|
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
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
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 =
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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 ?? (
|
|
105
|
+
trust: entry.source.provenance?.trust ?? (isCanonical ? "trusted" : "unknown"),
|
|
91
106
|
...integrity
|
|
92
107
|
};
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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(
|
|
100
|
-
const existing = actionSources.get(
|
|
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.
|
|
107
|
-
|
|
108
|
-
actionSources.set(
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
+
};
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -60,11 +60,13 @@ Example:
|
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
62
|
name: "checklist",
|
|
63
|
-
summary: "Mark
|
|
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>
|
|
65
|
+
helpText: `mm-harness checklist <mark|closeout> <task-dir> [step] [options]
|
|
66
66
|
|
|
67
|
-
|
|
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
|
|
36
|
-
|
|
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",
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -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.
|
|
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
|
|
581
|
-
|
|
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() {
|