@deeeed/metamask-harness 0.80.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.
- package/CHANGELOG.md +43 -0
- package/adapters/manifest.json +48 -64
- package/adapters/{terminal → web-dapp}/cleanup.mjs +9 -9
- package/adapters/{terminal → web-dapp}/inject.mjs +14 -14
- package/adapters/{terminal → web-dapp}/launch.mjs +27 -24
- package/adapters/web-dapp/lib/blocked-hosts.mjs +24 -0
- package/adapters/{terminal → web-dapp}/lib/cdp-client.mjs +1 -1
- package/adapters/{terminal → web-dapp}/lib/display.mjs +1 -1
- package/adapters/{terminal → web-dapp}/lib/hud.mjs +1 -1
- package/adapters/web-dapp/lib/policy.mjs +79 -0
- package/adapters/{terminal → web-dapp}/lib/processes.mjs +8 -8
- package/adapters/{terminal → web-dapp}/lib/readiness.mjs +32 -34
- package/adapters/{terminal → web-dapp}/stop.mjs +5 -4
- package/adapters/{terminal → web-dapp}/verify.mjs +9 -6
- package/adapters/{terminal → web-dapp}/wallet-host.mjs +44 -227
- package/dist/adapters/core/actions.js +4 -1
- package/dist/adapters/extension/actions.js +6 -2
- package/dist/adapters/extension/network-observer.js +13 -0
- package/dist/adapters/extension/run.js +115 -31
- package/dist/adapters/mobile/actions.js +4 -1
- package/dist/adapters/mobile/network-observer.js +68 -0
- package/dist/adapters/mobile/performance-observer.js +1 -3
- package/dist/adapters/mobile/run.js +32 -9
- package/dist/adapters/perps-order-inputs.js +36 -0
- package/dist/adapters/plugin-surface.js +98 -0
- package/dist/adapters/proof-failure.js +11 -0
- package/dist/adapters/surface.js +1 -1
- package/dist/adapters/{terminal → web-dapp}/actions.js +11 -8
- package/dist/adapters/web-dapp/console-capture.js +63 -0
- package/dist/adapters/web-dapp/doctor-checks.js +33 -0
- package/dist/adapters/{terminal → web-dapp}/launch.js +21 -19
- package/dist/adapters/web-dapp/overlay.js +21 -0
- package/dist/adapters/web-dapp/readiness.js +10 -0
- package/dist/adapters/{terminal → web-dapp}/run.js +22 -17
- package/dist/adapters/{terminal → web-dapp}/stop.js +5 -4
- package/dist/adapters/web-dapp/surface.js +149 -0
- package/dist/adapters.js +20 -4
- package/dist/cli.js +34 -6
- package/dist/command-contract.js +29 -7
- package/dist/commands/check.js +32 -8
- package/dist/commands/completion-candidates.js +5 -5
- package/dist/commands/doctor.js +19 -2
- package/dist/commands/farmslot-ready.js +1 -0
- package/dist/commands/help.js +1 -1
- package/dist/commands/launch/device.js +5 -5
- package/dist/commands/self-test.js +10 -5
- package/dist/engine.js +47 -0
- package/dist/funding-execution-context.js +81 -0
- package/dist/manifest.js +48 -24
- package/dist/mm-harness-cli.js +20 -3
- package/dist/performance-observation.js +7 -15
- package/dist/recipe-assistance-context.js +6 -7
- package/dist/runner.js +28 -8
- package/docs/CONTRIBUTING.md +4 -2
- package/docs/RECIPES.md +31 -3
- package/docs/cli-contract.md +5 -3
- package/library/actions/core/perps/_controller.mjs +24 -28
- package/library/actions/core/perps/_rewards.mjs +186 -0
- package/library/actions/core/perps/close_positions.mjs +1 -1
- package/library/actions/core/perps/place_order.mjs +167 -45
- package/library/actions/core/perps/quote_fees.mjs +7 -4
- package/library/actions/core/wallet/list_accounts.mjs +1 -1
- package/library/actions/extension/perps/close_visible_position.mjs +5 -0
- package/library/actions/extension/perps/edit_margin.mjs +4 -0
- package/library/actions/extension/perps/mutation-receipt.mjs +95 -3
- package/library/actions/extension/perps/perps.mjs +320 -3
- package/library/actions/extension/perps/place_order.mjs +3 -1
- package/library/actions/extension/perps/update_position_tpsl.mjs +4 -0
- package/library/actions/extension/platform/cdp.mjs +71 -3
- package/library/actions/mobile/perps/perps.mjs +216 -11
- package/library/actions/mobile/perps/place_order.mjs +3 -1
- package/library/actions/shared/console-collector.mjs +1 -1
- package/library/actions/shared/perps/order-policy.mjs +177 -0
- package/library/actions/{core/wallet/_fixture-accounts.mjs → shared/wallet/fixture-accounts.mjs} +18 -3
- package/library/actions/{terminal → web-dapp}/app/assert_no_console_errors.mjs +4 -4
- package/library/actions/{terminal → web-dapp}/app/launch.mjs +8 -7
- package/library/actions/{terminal → web-dapp}/platform/hud.mjs +7 -6
- package/library/actions/{terminal → web-dapp}/platform/page.mjs +13 -12
- package/library/actions/{terminal → web-dapp}/platform/runtime.mjs +16 -12
- package/library/actions/web-dapp/ui/navigate.mjs +24 -0
- package/library/actions/web-dapp/wallet/_signature-log.mjs +32 -0
- package/library/actions/{terminal → web-dapp}/wallet/assert_signatures.mjs +8 -14
- package/library/actions/{terminal → web-dapp}/wallet/list_accounts.mjs +2 -2
- package/library/actions/web-dapp/wallet/read_signatures.mjs +38 -0
- package/library/manifests/core.action-manifest.json +6 -2
- package/library/manifests/extension.action-manifest.json +4 -0
- package/library/manifests/mobile.action-manifest.json +12 -0
- package/library/manifests/{terminal.action-manifest.json → web-dapp.action-manifest.json} +18 -702
- package/package.json +6 -6
- package/site/assets/help-recipes.json +1 -0
- package/adapters/terminal/lib/mainnet-guard.mjs +0 -177
- package/adapters/terminal/lib/origin.mjs +0 -30
- package/adapters/terminal/lib/page-script.mjs +0 -246
- package/adapters/terminal/lib/strict-wallet.mjs +0 -138
- package/dist/adapters/terminal/console-capture.js +0 -59
- package/dist/adapters/terminal/doctor-checks.js +0 -22
- package/dist/adapters/terminal/overlay.js +0 -18
- package/dist/adapters/terminal/readiness.js +0 -11
- package/dist/adapters/terminal/surface.js +0 -133
- package/dist/commands/call.js +0 -671
- package/dist/commands/list-executables.js +0 -245
- package/dist/commands/manifest.js +0 -477
- package/dist/commands/run-engine.js +0 -1301
- package/dist/commands/run.js +0 -730
- package/dist/discovery.js +0 -15
- package/dist/metamask-action-validation.js +0 -68
- package/dist/network-observation.js +0 -290
- package/library/actions/terminal/perps/_venue.mjs +0 -242
- package/library/actions/terminal/perps/assert_orders.mjs +0 -54
- package/library/actions/terminal/perps/assert_positions.mjs +0 -40
- package/library/actions/terminal/perps/read_orders.mjs +0 -26
- package/library/actions/terminal/perps/read_positions.mjs +0 -25
- package/library/actions/terminal/perps/read_snapshot.mjs +0 -40
- package/library/actions/terminal/perps/revoke_agent.mjs +0 -58
- package/library/actions/terminal/perps/teardown_state.mjs +0 -104
- package/library/actions/terminal/ui/navigate.mjs +0 -31
- package/library/actions/terminal/wallet/_signature-log.mjs +0 -177
- package/library/actions/terminal/wallet/read_signatures.mjs +0 -37
package/dist/engine.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { getAdapterSurface } from "./adapters/surface.js";
|
|
3
|
+
import { consoleClassifier } from "./console-classifier.js";
|
|
4
|
+
import {
|
|
5
|
+
authorizeTrustedMutationPlan,
|
|
6
|
+
fixturePolicyMutationBase,
|
|
7
|
+
fundingOptionsFromCli,
|
|
8
|
+
isFixturePolicyBase,
|
|
9
|
+
loadTrustedMutationBase
|
|
10
|
+
} from "./funding-execution-context.js";
|
|
11
|
+
import { resolveActionManifest, validateManifest } from "./manifest.js";
|
|
12
|
+
import { LIBRARY_ACTION_NAMESPACE, runnerDir } from "./paths.js";
|
|
13
|
+
import { metaMaskActionExecutionCapabilities } from "./recipe-security.js";
|
|
14
|
+
const metaMaskEngine = {
|
|
15
|
+
bundledLibrary: {
|
|
16
|
+
name: "metamask",
|
|
17
|
+
root: path.resolve(runnerDir, "library"),
|
|
18
|
+
actionNamespace: LIBRARY_ACTION_NAMESPACE
|
|
19
|
+
},
|
|
20
|
+
resolveActionManifest,
|
|
21
|
+
validateManifest,
|
|
22
|
+
actionCapabilities: metaMaskActionExecutionCapabilities,
|
|
23
|
+
createRunner: async (adapter, manifest, options) => {
|
|
24
|
+
const { createMetaMaskRunner } = await import("./runner.js");
|
|
25
|
+
return createMetaMaskRunner(adapter, manifest, options);
|
|
26
|
+
},
|
|
27
|
+
// Funding files from the run's command line, bound to the run's plan. Without
|
|
28
|
+
// them, a platform whose actions take a trusted mutation (the Extension) runs a
|
|
29
|
+
// plan with external mutations in the fixture-policy lane, where each action
|
|
30
|
+
// applies the shared order policy; any other plan runs without one.
|
|
31
|
+
trustedMutation: {
|
|
32
|
+
load: async ({ cli, adapter, ...run }) => await loadTrustedMutationBase({ ...fundingOptionsFromCli(cli), ...run }) ?? (getAdapterSurface(adapter).actions.trustedMutation === true ? fixturePolicyMutationBase({ artifactsDir: run.artifactsDir, rootRecipe: run.rootRecipe }) : void 0),
|
|
33
|
+
// Nothing to bind (undefined): recipe-cli keeps the runner it planned with.
|
|
34
|
+
authorize: async (base, plan) => {
|
|
35
|
+
if (!base) return void 0;
|
|
36
|
+
if (isFixturePolicyBase(base) && !plan.nodes.some((node) => node.capabilities.includes("external-mutation"))) {
|
|
37
|
+
return void 0;
|
|
38
|
+
}
|
|
39
|
+
return authorizeTrustedMutationPlan(base, plan);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
console: consoleClassifier,
|
|
43
|
+
runnerIncludes: ["src", "adapters", "library", "bin", "dist", "package.json"]
|
|
44
|
+
};
|
|
45
|
+
export {
|
|
46
|
+
metaMaskEngine
|
|
47
|
+
};
|
|
@@ -35,6 +35,7 @@ const CLAIM_DIFF_FIELDS = [
|
|
|
35
35
|
"operationCounts"
|
|
36
36
|
];
|
|
37
37
|
const trustedActionContexts = /* @__PURE__ */ new WeakSet();
|
|
38
|
+
const fixturePolicySources = /* @__PURE__ */ new WeakMap();
|
|
38
39
|
const fundingSources = /* @__PURE__ */ new WeakMap();
|
|
39
40
|
const CURRENT_FUNDING_REGISTRY_SHA256 = "d4416e0e8a812a0e2f1798ae6f901a5820f7ee0b9783cfa5d515d0c7863ea80e";
|
|
40
41
|
const PREVIOUS_FUNDING_REGISTRY_SHA256 = "7e79ee2a5f7bdde0a5548208cb2eab55881575cc7d826ee5f7acdbf15d49609c";
|
|
@@ -141,7 +142,70 @@ async function loadTrustedMutationBase(options) {
|
|
|
141
142
|
});
|
|
142
143
|
return trusted;
|
|
143
144
|
}
|
|
145
|
+
const FIXTURE_POLICY_LANE = "fixture-policy";
|
|
146
|
+
const fixturePolicyMarker = (field) => sha256(`${FIXTURE_POLICY_LANE}:${field}`);
|
|
147
|
+
function fixturePolicyMutationBase(options) {
|
|
148
|
+
if (!path.isAbsolute(options.artifactsDir)) {
|
|
149
|
+
throw new Error("Runner artifactsDir must be absolute before a mutation context is created.");
|
|
150
|
+
}
|
|
151
|
+
const rootRecipeSha256 = normalizeRecipeDigest(digestRecipeDocument(options.rootRecipe));
|
|
152
|
+
const base = deepFreeze({
|
|
153
|
+
runId: sha256(canonical({ lane: FIXTURE_POLICY_LANE, artifactsDir: options.artifactsDir, rootRecipeSha256 })),
|
|
154
|
+
rootRecipeSha256,
|
|
155
|
+
requestSha256: fixturePolicyMarker("request"),
|
|
156
|
+
preflightSha256: fixturePolicyMarker("preflight"),
|
|
157
|
+
releaseProofSha256: fixturePolicyMarker("release-proof"),
|
|
158
|
+
balanceProofSha256: fixturePolicyMarker("balance-proof"),
|
|
159
|
+
fundingRequirementsSha256: fixturePolicyMarker("requirements"),
|
|
160
|
+
runtimeArtifactSha256: fixturePolicyMarker("runtime-artifact"),
|
|
161
|
+
runtimeTreeSha256: fixturePolicyMarker("runtime-tree"),
|
|
162
|
+
registrySha256: fixturePolicyMarker("registry"),
|
|
163
|
+
profileId: FIXTURE_POLICY_LANE,
|
|
164
|
+
domain: "perps",
|
|
165
|
+
client: "extension",
|
|
166
|
+
proofLane: FIXTURE_POLICY_LANE,
|
|
167
|
+
targetId: FIXTURE_POLICY_LANE,
|
|
168
|
+
subjectDigest: fixturePolicyMarker("subject"),
|
|
169
|
+
selectedIdentitySha256: "",
|
|
170
|
+
runtimeSessionId: "",
|
|
171
|
+
cdpPort: 0,
|
|
172
|
+
cdpTargetId: "",
|
|
173
|
+
extensionOrigin: "",
|
|
174
|
+
operations: [],
|
|
175
|
+
budgets: [],
|
|
176
|
+
requirements: []
|
|
177
|
+
});
|
|
178
|
+
fixturePolicySources.set(base, { artifactsDir: options.artifactsDir });
|
|
179
|
+
return base;
|
|
180
|
+
}
|
|
181
|
+
function isFixturePolicyBase(base) {
|
|
182
|
+
return base?.proofLane === FIXTURE_POLICY_LANE;
|
|
183
|
+
}
|
|
184
|
+
async function authorizeFixturePolicyPlan(base, plan) {
|
|
185
|
+
const source = fixturePolicySources.get(base);
|
|
186
|
+
if (!source) throw new Error("Fixture-policy plan authorization requires the runner-created context.");
|
|
187
|
+
const executionPlanSha256 = normalizeRecipeDigest(plan.digest);
|
|
188
|
+
await mkdir(source.artifactsDir, { recursive: true, mode: 448 });
|
|
189
|
+
const artifactsDir = await realpath(source.artifactsDir);
|
|
190
|
+
const artifactsIdentitySha256 = await directoryIdentity(artifactsDir);
|
|
191
|
+
const executionLedgerRoot = path.join(artifactsDir, "mutation-ledger");
|
|
192
|
+
await ensurePrivateDirectory(artifactsDir, executionLedgerRoot);
|
|
193
|
+
const runId = sha256(canonical({
|
|
194
|
+
lane: FIXTURE_POLICY_LANE,
|
|
195
|
+
rootRecipeSha256: base.rootRecipeSha256,
|
|
196
|
+
executionPlanSha256,
|
|
197
|
+
artifactsIdentitySha256
|
|
198
|
+
}));
|
|
199
|
+
const authorized = deepFreeze({ ...base, runId, executionPlanSha256, artifactsIdentitySha256 });
|
|
200
|
+
fixturePolicySources.set(authorized, {
|
|
201
|
+
...source,
|
|
202
|
+
executionLedgerRoot,
|
|
203
|
+
executionClaimSha256: sha256(canonical({ kind: "fixture-policy-execution-claim", runId, executionPlanSha256 }))
|
|
204
|
+
});
|
|
205
|
+
return authorized;
|
|
206
|
+
}
|
|
144
207
|
async function authorizeTrustedMutationPlan(base, plan, testIo = {}) {
|
|
208
|
+
if (isFixturePolicyBase(base)) return authorizeFixturePolicyPlan(base, plan);
|
|
145
209
|
const source = fundingSources.get(base);
|
|
146
210
|
if (!source) throw new Error("Funding plan authorization requires the original runner-verified context.");
|
|
147
211
|
const executionPlanSha256 = normalizeRecipeDigest(plan.digest);
|
|
@@ -228,6 +292,20 @@ async function authorizeTrustedMutationPlan(base, plan, testIo = {}) {
|
|
|
228
292
|
return authorized;
|
|
229
293
|
}
|
|
230
294
|
async function trustedMutationForAction(base, recipe, selectedIdentitySha256) {
|
|
295
|
+
if (isFixturePolicyBase(base)) {
|
|
296
|
+
const source2 = fixturePolicySources.get(base);
|
|
297
|
+
if (!source2?.executionLedgerRoot || !source2.executionClaimSha256) {
|
|
298
|
+
throw new Error("External mutation requires an authorized run-owned execution ledger.");
|
|
299
|
+
}
|
|
300
|
+
const trusted2 = deepFreeze({
|
|
301
|
+
...base,
|
|
302
|
+
recipeSha256: normalizeRecipeDigest(digestRecipeDocument(recipe)),
|
|
303
|
+
executionLedgerRoot: source2.executionLedgerRoot,
|
|
304
|
+
executionClaimSha256: source2.executionClaimSha256
|
|
305
|
+
});
|
|
306
|
+
trustedActionContexts.add(trusted2);
|
|
307
|
+
return trusted2;
|
|
308
|
+
}
|
|
231
309
|
if (!selectedIdentitySha256 || !SHA256.test(selectedIdentitySha256)) {
|
|
232
310
|
throw new Error(
|
|
233
311
|
"External mutation requires a successful same-run metamask.wallet.import selected identity proof. Next: run metamask.wallet.import before the mutation node."
|
|
@@ -1442,8 +1520,11 @@ function deepFreeze(value) {
|
|
|
1442
1520
|
return value;
|
|
1443
1521
|
}
|
|
1444
1522
|
export {
|
|
1523
|
+
FIXTURE_POLICY_LANE,
|
|
1445
1524
|
authorizeTrustedMutationPlan,
|
|
1525
|
+
fixturePolicyMutationBase,
|
|
1446
1526
|
fundingOptionsFromCli,
|
|
1527
|
+
isFixturePolicyBase,
|
|
1447
1528
|
loadTrustedMutationBase,
|
|
1448
1529
|
selectedIdentityFromWalletImport,
|
|
1449
1530
|
serializeTrustedMutationContext,
|
package/dist/manifest.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { OFFICIAL_RECIPE_ACTIONS } from "@farmslot/protocol";
|
|
4
4
|
import { getAdapterSurface } from "./adapters/surface.js";
|
|
5
|
-
import { manifestPath, readJson, importRecipeProtocol } from "./paths.js";
|
|
5
|
+
import { manifestPath, readJson, importRecipeProtocol, runnerDir } from "./paths.js";
|
|
6
6
|
function loadMetaMaskMobileActionManifest() {
|
|
7
7
|
return asActionManifest(readJson(manifestPath("mobile")));
|
|
8
8
|
}
|
|
@@ -12,14 +12,26 @@ function loadMetaMaskExtensionActionManifest() {
|
|
|
12
12
|
function loadMetaMaskCoreActionManifest() {
|
|
13
13
|
return asActionManifest(readJson(manifestPath("core")));
|
|
14
14
|
}
|
|
15
|
-
function loadMetaMaskTerminalActionManifest() {
|
|
16
|
-
return asActionManifest(readJson(manifestPath("terminal")));
|
|
17
|
-
}
|
|
18
15
|
function loadActionManifest(adapter, overridePath) {
|
|
19
16
|
if (overridePath) return asActionManifest(readJson(path.resolve(overridePath)));
|
|
20
17
|
return asActionManifest(readJson(getAdapterSurface(adapter).actions.manifestPath()));
|
|
21
18
|
}
|
|
19
|
+
class ActionManifestError extends Error {
|
|
20
|
+
code = "ACTION_MANIFEST_INVALID";
|
|
21
|
+
userAction;
|
|
22
|
+
constructor(adapter, cause) {
|
|
23
|
+
super(cause instanceof Error ? cause.message : String(cause));
|
|
24
|
+
this.userAction = `fix the ${adapter} action manifest the message names, then rerun`;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
22
27
|
async function resolveActionManifest(adapter, overridePath, librarySources, taskActionRoot) {
|
|
28
|
+
try {
|
|
29
|
+
return await resolveActionManifestSources(adapter, overridePath, librarySources, taskActionRoot);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
throw error instanceof ActionManifestError ? error : new ActionManifestError(adapter, error);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function resolveActionManifestSources(adapter, overridePath, librarySources, taskActionRoot) {
|
|
23
35
|
if (overridePath) {
|
|
24
36
|
const manifestPath2 = path.resolve(overridePath);
|
|
25
37
|
const implementationRoot = path.resolve(
|
|
@@ -38,9 +50,12 @@ async function resolveActionManifest(adapter, overridePath, librarySources, task
|
|
|
38
50
|
})
|
|
39
51
|
};
|
|
40
52
|
}
|
|
41
|
-
const
|
|
53
|
+
const surfaceActions = getAdapterSurface(adapter).actions;
|
|
54
|
+
const canonicalPath = path.resolve(surfaceActions.manifestPath());
|
|
42
55
|
const canonicalRoot = path.dirname(path.dirname(canonicalPath));
|
|
43
|
-
const
|
|
56
|
+
const bundledRoot = path.resolve(runnerDir, "library");
|
|
57
|
+
const canonicalName = canonicalRoot === bundledRoot ? "metamask" : path.basename(canonicalRoot);
|
|
58
|
+
const sources = librarySources?.length ? librarySources : [{ name: canonicalName, root: canonicalRoot }];
|
|
44
59
|
const manifests = [];
|
|
45
60
|
for (const source of sources) {
|
|
46
61
|
const file = path.resolve(source.root, "manifests", `${adapter}.action-manifest.json`);
|
|
@@ -49,31 +64,39 @@ async function resolveActionManifest(adapter, overridePath, librarySources, task
|
|
|
49
64
|
await validateManifest(manifest);
|
|
50
65
|
manifests.push({ source, file, manifest });
|
|
51
66
|
}
|
|
67
|
+
for (const declared of surfaceActions.manifestPaths?.() ?? []) {
|
|
68
|
+
const file = path.resolve(declared);
|
|
69
|
+
if (file === canonicalPath || manifests.some((entry) => entry.file === file)) continue;
|
|
70
|
+
const manifest = asActionManifest(readJson(file));
|
|
71
|
+
await validateManifest(manifest);
|
|
72
|
+
const root = path.dirname(path.dirname(file));
|
|
73
|
+
const source = sources.find((candidate) => path.resolve(candidate.root) === root) ?? { name: path.basename(root), root };
|
|
74
|
+
manifests.push({ source, file, manifest });
|
|
75
|
+
}
|
|
52
76
|
if (!manifests.some((entry) => entry.file === canonicalPath)) {
|
|
53
77
|
const manifest = loadActionManifest(adapter);
|
|
54
78
|
await validateManifest(manifest);
|
|
55
79
|
manifests.push({
|
|
56
|
-
source: { name:
|
|
80
|
+
source: { name: canonicalName, root: canonicalRoot },
|
|
57
81
|
file: canonicalPath,
|
|
58
82
|
manifest
|
|
59
83
|
});
|
|
60
84
|
}
|
|
61
85
|
const canonical = manifests.find((entry) => entry.file === canonicalPath);
|
|
62
86
|
if (!canonical) throw new Error(`No ${adapter} action manifest could be resolved.`);
|
|
87
|
+
const isBundled = (entry) => path.resolve(entry.source.root) === bundledRoot;
|
|
88
|
+
const bundledParents = manifests.filter((entry) => entry !== canonical && isBundled(entry));
|
|
63
89
|
const official = new Set(OFFICIAL_RECIPE_ACTIONS);
|
|
64
|
-
const canonicalActions = actionsOf(canonical.manifest);
|
|
65
90
|
const mergedActions = {};
|
|
66
|
-
for (const [name, entry] of Object.entries(canonicalActions)) {
|
|
67
|
-
if (official.has(name)) mergedActions[name] = entry;
|
|
68
|
-
}
|
|
69
91
|
const actionSources = /* @__PURE__ */ new Map();
|
|
70
|
-
for (const
|
|
71
|
-
|
|
72
|
-
name
|
|
73
|
-
|
|
74
|
-
manifestPath:
|
|
75
|
-
}
|
|
92
|
+
for (const entry of [canonical, ...bundledParents]) {
|
|
93
|
+
for (const [name, action] of Object.entries(actionsOf(entry.manifest))) {
|
|
94
|
+
if (!official.has(name) || name in mergedActions) continue;
|
|
95
|
+
mergedActions[name] = action;
|
|
96
|
+
actionSources.set(name, { name: "official", tier: "official", manifestPath: entry.file });
|
|
97
|
+
}
|
|
76
98
|
}
|
|
99
|
+
const observers = bundledParents.length ? [canonical, ...bundledParents].flatMap((entry) => entry.manifest.observers ?? []).filter((observer, index, all) => all.findIndex((other) => other.ref === observer.ref) === index) : canonical.manifest.observers;
|
|
77
100
|
const custom = /* @__PURE__ */ new Set();
|
|
78
101
|
const ordered = [
|
|
79
102
|
...manifests.filter((entry) => entry.file !== canonicalPath),
|
|
@@ -81,24 +104,25 @@ async function resolveActionManifest(adapter, overridePath, librarySources, task
|
|
|
81
104
|
];
|
|
82
105
|
for (const entry of ordered) {
|
|
83
106
|
const isCanonical = entry.file === canonicalPath;
|
|
84
|
-
|
|
107
|
+
const bundled = isBundled(entry);
|
|
108
|
+
if (!isCanonical && !bundled && entry.manifest.observers?.length) {
|
|
85
109
|
throw new Error(
|
|
86
110
|
`Library manifest ${entry.file} declares observers, but external library observers are not executable; remove observers or use an explicit task action manifest.`
|
|
87
111
|
);
|
|
88
112
|
}
|
|
89
|
-
const sourceName =
|
|
90
|
-
const tier =
|
|
113
|
+
const sourceName = bundled ? "metamask" : entry.source.name ?? path.basename(entry.source.root);
|
|
114
|
+
const tier = bundled ? "canonical" : sourceName === "personal" ? "personal" : "team";
|
|
91
115
|
const implementationRoot = path.join(entry.source.root, "actions");
|
|
92
116
|
const sourceInfo = {
|
|
93
117
|
name: sourceName,
|
|
94
118
|
tier,
|
|
95
119
|
manifestPath: entry.file,
|
|
96
120
|
implementationRoot,
|
|
97
|
-
trust: entry.source.provenance?.trust ?? (
|
|
121
|
+
trust: entry.source.provenance?.trust ?? (bundled ? "trusted" : "unknown")
|
|
98
122
|
};
|
|
99
123
|
for (const [name, action] of Object.entries(actionsOf(entry.manifest))) {
|
|
100
124
|
if (official.has(name)) {
|
|
101
|
-
if (!isCanonical) {
|
|
125
|
+
if (!isCanonical && !bundled) {
|
|
102
126
|
throw new Error(
|
|
103
127
|
`Library action ${name} from ${entry.file} conflicts with an official action; team libraries may declare namespaced custom actions only.`
|
|
104
128
|
);
|
|
@@ -121,7 +145,7 @@ async function resolveActionManifest(adapter, overridePath, librarySources, task
|
|
|
121
145
|
manifest: {
|
|
122
146
|
$schema: canonical.manifest.$schema,
|
|
123
147
|
actions: mergedActions,
|
|
124
|
-
...
|
|
148
|
+
...observers ? { observers } : {}
|
|
125
149
|
},
|
|
126
150
|
actionSources
|
|
127
151
|
};
|
|
@@ -163,11 +187,11 @@ function asActionManifest(value) {
|
|
|
163
187
|
return value;
|
|
164
188
|
}
|
|
165
189
|
export {
|
|
190
|
+
ActionManifestError,
|
|
166
191
|
loadActionManifest,
|
|
167
192
|
loadMetaMaskCoreActionManifest,
|
|
168
193
|
loadMetaMaskExtensionActionManifest,
|
|
169
194
|
loadMetaMaskMobileActionManifest,
|
|
170
|
-
loadMetaMaskTerminalActionManifest,
|
|
171
195
|
resolveActionManifest,
|
|
172
196
|
validateManifest
|
|
173
197
|
};
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -4,14 +4,16 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { Command } from "commander";
|
|
7
|
-
import { color, withCommandJournal, JsonStreamWriter, detectAdapter } from "@farmslot/recipe-cli/harness";
|
|
7
|
+
import { color, withCommandJournal, JsonStreamWriter, detectAdapter, handleCallHelp } from "@farmslot/recipe-cli/harness";
|
|
8
8
|
import { handleUpdate, maybeNudge } from "./commands/update.js";
|
|
9
9
|
import { handlePin } from "./commands/pin.js";
|
|
10
|
-
import {
|
|
10
|
+
import { metaMaskEngine } from "./engine.js";
|
|
11
11
|
import { handleHelp } from "./commands/help.js";
|
|
12
12
|
import { handleTutorial } from "./commands/tutorial.js";
|
|
13
13
|
import { handleSetupBase } from "./commands/setup-base.js";
|
|
14
|
+
import { loadSelectedAdapter } from "./adapters/plugin-surface.js";
|
|
14
15
|
import { getAdapterSurface } from "./adapters/surface.js";
|
|
16
|
+
import { ActionManifestError } from "./manifest.js";
|
|
15
17
|
import { configPath, hydrateLibraryPathEnv, readConfig } from "./review/knowledge.js";
|
|
16
18
|
import {
|
|
17
19
|
PUBLIC_COMMAND_CONTRACTS,
|
|
@@ -1088,6 +1090,17 @@ async function delegate(argv) {
|
|
|
1088
1090
|
try {
|
|
1089
1091
|
return await recipeMain(argv);
|
|
1090
1092
|
} catch (error) {
|
|
1093
|
+
const own = argv.includes("--") ? argv.slice(0, argv.indexOf("--")) : argv;
|
|
1094
|
+
if (error instanceof ActionManifestError && own.includes("--json")) {
|
|
1095
|
+
console.log(JSON.stringify({
|
|
1096
|
+
schemaVersion: 1,
|
|
1097
|
+
command: argv[0],
|
|
1098
|
+
status: "fail",
|
|
1099
|
+
exitCode: 1,
|
|
1100
|
+
error: { code: error.code, message: error.message, userAction: error.userAction }
|
|
1101
|
+
}, null, 2));
|
|
1102
|
+
return 1;
|
|
1103
|
+
}
|
|
1091
1104
|
console.error(error instanceof Error ? error.message : String(error));
|
|
1092
1105
|
return error !== null && typeof error === "object" && "exitCode" in error && typeof error.exitCode === "number" ? error.exitCode : 1;
|
|
1093
1106
|
}
|
|
@@ -1226,11 +1239,15 @@ if (!preflightBypass.has(rawArgv[0] ?? "")) {
|
|
|
1226
1239
|
}
|
|
1227
1240
|
}
|
|
1228
1241
|
await hydrateLibraryPathEnv({ target: targetFromArgv(rawArgv), config: readConfig(configPath()) });
|
|
1242
|
+
{
|
|
1243
|
+
const refused = await loadSelectedAdapter(rawArgv[0] ?? "", rawArgv.slice(1));
|
|
1244
|
+
if (refused !== void 0) process.exit(refused);
|
|
1245
|
+
}
|
|
1229
1246
|
if (hasPassthroughHelp(rawArgv)) {
|
|
1230
1247
|
process.exit(await delegate(rawArgv));
|
|
1231
1248
|
}
|
|
1232
1249
|
if (isCallActionHelp(rawArgv)) {
|
|
1233
1250
|
const callHelp = REAL.find((command) => command.name === "call")?.helpText ?? "";
|
|
1234
|
-
process.exit(await handleCallHelp(rawArgv.slice(1), callHelp));
|
|
1251
|
+
process.exit(await handleCallHelp(rawArgv.slice(1), callHelp, { catalog: metaMaskEngine }));
|
|
1235
1252
|
}
|
|
1236
1253
|
await program.parseAsync(process.argv);
|
|
@@ -15,19 +15,13 @@ const sessions = /* @__PURE__ */ new Map();
|
|
|
15
15
|
const DEFAULT_MAX_DURATION_MS = 5 * 60 * 1e3;
|
|
16
16
|
const MAX_DURATION_MS = 60 * 60 * 1e3;
|
|
17
17
|
const gzipAsync = promisify(gzip);
|
|
18
|
-
function startRunPerformanceObservation(
|
|
19
|
-
const observer = getAdapterSurface(adapter).observation?.performance;
|
|
20
|
-
if (!observer) return void 0;
|
|
18
|
+
function startRunPerformanceObservation(observer, { target, artifactsDir, env }) {
|
|
21
19
|
const key = path.resolve(artifactsDir);
|
|
22
20
|
const session = {
|
|
23
21
|
observer,
|
|
24
22
|
artifactsDir: key,
|
|
25
|
-
env
|
|
26
|
-
...env,
|
|
27
|
-
...ports.watcherPort ? { WATCHER_PORT: ports.watcherPort } : {}
|
|
28
|
-
},
|
|
23
|
+
env,
|
|
29
24
|
nodeEvents: [],
|
|
30
|
-
ports,
|
|
31
25
|
target,
|
|
32
26
|
windows: /* @__PURE__ */ new Map()
|
|
33
27
|
};
|
|
@@ -154,7 +148,7 @@ async function performanceBackend(session) {
|
|
|
154
148
|
if (session.backend) return session.backend;
|
|
155
149
|
if (session.backendError) throw new Error(session.backendError);
|
|
156
150
|
try {
|
|
157
|
-
session.backend = await session.observer.backend(session.target, session.env
|
|
151
|
+
session.backend = await session.observer.backend(session.target, session.env);
|
|
158
152
|
return session.backend;
|
|
159
153
|
} catch (error) {
|
|
160
154
|
session.backendError = errorMessage(error);
|
|
@@ -517,13 +511,11 @@ function unavailableCapture(session, window, reason) {
|
|
|
517
511
|
session.nodeEvents
|
|
518
512
|
);
|
|
519
513
|
}
|
|
520
|
-
async function createExtensionPerformanceObserver(target, env
|
|
521
|
-
return createExtensionPerformanceBackend(extensionCdpPort(
|
|
514
|
+
async function createExtensionPerformanceObserver(target, env) {
|
|
515
|
+
return createExtensionPerformanceBackend(extensionCdpPort(env), target);
|
|
522
516
|
}
|
|
523
|
-
function extensionCdpPort(
|
|
524
|
-
const port = Number(
|
|
525
|
-
ports.cdpPort ?? env.CDP_PORT ?? env.RECIPE_CDP_PORT
|
|
526
|
-
);
|
|
517
|
+
function extensionCdpPort(env) {
|
|
518
|
+
const port = Number(env.CDP_PORT ?? env.RECIPE_CDP_PORT);
|
|
527
519
|
if (!Number.isInteger(port) || port <= 0) {
|
|
528
520
|
throw new Error("Extension performance observation requires CDP_PORT.");
|
|
529
521
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { loadRecipeLibraries, resolveRecipeDependencies, rootResolutionRef } from "@farmslot/recipe-runner";
|
|
4
|
-
import { isSensitiveKey, CliError, isRecord, optionStrings, EXIT } from "@farmslot/recipe-cli/harness";
|
|
4
|
+
import { isSensitiveKey, CliError, describeManifestActions, isRecord, optionStrings, EXIT, resolveLibrarySources, validateRunRecipeStatic } from "@farmslot/recipe-cli/harness";
|
|
5
5
|
import { resolveActionManifest } from "./manifest.js";
|
|
6
|
-
import { describeManifestActions } from "./commands/manifest.js";
|
|
7
6
|
import { recipeComposition } from "@farmslot/recipe-cli";
|
|
8
|
-
import {
|
|
7
|
+
import { metaMaskEngine } from "./engine.js";
|
|
9
8
|
import { MAX_ADVICE_STATE_BYTES } from "./recipe-assistance-rubric.js";
|
|
10
9
|
function digest(value) {
|
|
11
10
|
return createHash("sha256").update(value).digest("hex");
|
|
@@ -46,7 +45,7 @@ async function buildAdviceContext(input) {
|
|
|
46
45
|
const task = readInput(input.task);
|
|
47
46
|
if (!input.recipe) throw new CliError("Advice requires a recipe. Next: use recipe-quality plan before authoring.", EXIT.usage);
|
|
48
47
|
readInput(input.recipe);
|
|
49
|
-
const validated = await validateRunRecipeStatic(input.recipe, input.adapter, input.options ?? {}, input.params);
|
|
48
|
+
const validated = await validateRunRecipeStatic(metaMaskEngine, input.recipe, input.adapter, input.options ?? {}, input.params);
|
|
50
49
|
if (validated.usageError || !validated.schemaValid || !isRecord(validated.recipe)) {
|
|
51
50
|
const detail = validated.usageError?.message ?? validated.findings.map((finding) => finding.message).join("; ");
|
|
52
51
|
throw new CliError(`Recipe is not statically valid: ${detail}. Next: run mm-harness run ${input.recipe} --plan --adapter ${input.adapter}.`, EXIT.validation);
|
|
@@ -56,7 +55,7 @@ async function buildAdviceContext(input) {
|
|
|
56
55
|
const dependencies = resolveRecipeDependencies({ rootRef: rootResolutionRef(rootDigest), root: validated.recipe, rootSource: { kind: "recipe-file", trust: "unknown", digest: rootDigest }, recipes: library.recipes });
|
|
57
56
|
const composition = recipeComposition(validated.recipe, dependencies.recipes);
|
|
58
57
|
const { manifest, actionSources } = await resolveActionManifest(input.adapter, void 0, validated.librarySources);
|
|
59
|
-
const catalog = describeManifestActions(manifest, actionSources);
|
|
58
|
+
const catalog = describeManifestActions(metaMaskEngine, manifest, actionSources);
|
|
60
59
|
const used = new Set(composition.actions);
|
|
61
60
|
const actions = catalog.filter((action) => used.has(action.name)).map(({ name, description, schema, capabilities }) => ({ name, description, schema, capabilities }));
|
|
62
61
|
const domains = new Set(composition.actions.map((name) => name.split(".").slice(0, -1).join(".")));
|
|
@@ -108,12 +107,12 @@ async function buildPlanningContext(input) {
|
|
|
108
107
|
throw new CliError("Planning requires 1\u201320 non-empty acceptanceCriteria strings. Next: supply a focused JSON task.", EXIT.validation);
|
|
109
108
|
}
|
|
110
109
|
const criteria = task.acceptanceCriteria;
|
|
111
|
-
const sources = await
|
|
110
|
+
const sources = await resolveLibrarySources(metaMaskEngine, optionStrings(input.options ?? {}, "library"));
|
|
112
111
|
const library = await loadRecipeLibraries(sources ?? [], { adapter: input.adapter });
|
|
113
112
|
const { manifest, actionSources } = await resolveActionManifest(input.adapter, void 0, sources);
|
|
114
113
|
const candidates = [
|
|
115
114
|
...[...library.recipes.values()].map((recipe) => ({ id: `recipe:${recipe.ref}`, kind: "recipe", ref: recipe.ref, description: typeof recipe.document.description === "string" ? recipe.document.description : "", parameters: recipe.document.paramsSchema, source: recipe.source, provenance: recipe.provenance, shadows: recipe.shadows })),
|
|
116
|
-
...describeManifestActions(manifest, actionSources).map((action) => ({ id: `action:${action.name}`, kind: "action", ref: action.name, description: action.description, parameters: action.schema, capabilities: action.capabilities, source: action.source, sourceTier: action.sourceTier, shadows: action.shadows }))
|
|
115
|
+
...describeManifestActions(metaMaskEngine, manifest, actionSources).map((action) => ({ id: `action:${action.name}`, kind: "action", ref: action.name, description: action.description, parameters: action.schema, capabilities: action.capabilities, source: action.source, sourceTier: action.sourceTier, shadows: action.shadows }))
|
|
117
116
|
];
|
|
118
117
|
const terms = new Set(criteria.join(" ").toLowerCase().match(/[a-z][a-z_]{3,}/gu) ?? []);
|
|
119
118
|
const score = (candidate) => [...new Set(`${candidate.ref} ${candidate.description}`.toLowerCase().match(/[a-z][a-z_]{3,}/gu) ?? [])].filter((word) => terms.has(word)).length;
|
package/dist/runner.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { OFFICIAL_RECIPE_ACTIONS } from "@farmslot/protocol";
|
|
3
4
|
import { createMetaMaskAdapters, createMetaMaskUiTransport } from "./adapters.js";
|
|
4
5
|
import { getAdapterSurface } from "./adapters/surface.js";
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
resolveMetaMaskMobileLifecycleTarget
|
|
9
10
|
} from "./app-lifecycle.js";
|
|
10
11
|
import { loadMetaMaskExtensionActionManifest, loadMetaMaskMobileActionManifest } from "./manifest.js";
|
|
12
|
+
import { adapterPlugin } from "@farmslot/recipe-cli/harness";
|
|
11
13
|
import { actionSourceIntegrity, metaMaskActionExecutionCapabilities } from "./recipe-security.js";
|
|
12
14
|
import {
|
|
13
15
|
createRecordingTargetProvider,
|
|
@@ -23,6 +25,7 @@ import {
|
|
|
23
25
|
runnerDir
|
|
24
26
|
} from "./paths.js";
|
|
25
27
|
import {
|
|
28
|
+
isFixturePolicyBase,
|
|
26
29
|
selectedIdentityFromWalletImport,
|
|
27
30
|
trustedMutationForAction
|
|
28
31
|
} from "./funding-execution-context.js";
|
|
@@ -49,7 +52,12 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
49
52
|
const { createReactNativeBridgeUiTransport } = await importRecipeHarnessRuntimeReactNativeBridge();
|
|
50
53
|
const { createAppLifecycleAdapters } = await importRecipeHarnessAppLifecycle();
|
|
51
54
|
const actions = Object.keys(actionManifest.actions);
|
|
52
|
-
const
|
|
55
|
+
const surface = getAdapterSurface(adapter);
|
|
56
|
+
const coded = await surface.actions.adapters?.() ?? [];
|
|
57
|
+
const codedActions = new Set(coded.map((entry) => entry.action));
|
|
58
|
+
const customActions = actions.filter(
|
|
59
|
+
(action) => !OFFICIAL_ACTIONS.has(action) && !codedActions.has(action)
|
|
60
|
+
);
|
|
53
61
|
const actionSources = options.actionSources ? new Map([...options.actionSources].map(([action, source]) => [action, { ...source }])) : void 0;
|
|
54
62
|
const preparedLiveAdapters = await prepareLiveAdapterImplementations(
|
|
55
63
|
adapter,
|
|
@@ -57,6 +65,7 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
57
65
|
actionSources,
|
|
58
66
|
options.trustTaskActions
|
|
59
67
|
);
|
|
68
|
+
bindPluginDigest(actionSources, codedActions, adapterPlugin(adapter)?.digest);
|
|
60
69
|
const declaredActions = new Set(actions);
|
|
61
70
|
const core = createStandardCoreAdapters({ actions });
|
|
62
71
|
const projectOwnedOfficialActions = /* @__PURE__ */ new Set(["app.status", "app.lifecycle"]);
|
|
@@ -72,7 +81,6 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
72
81
|
}, preparedLiveAdapters, { hudPolicy: options.autoHud === true ? "show" : "auto" })
|
|
73
82
|
});
|
|
74
83
|
const existing = new Set([...core, ...ui].map((entry) => entry.action));
|
|
75
|
-
const surface = getAdapterSurface(adapter);
|
|
76
84
|
const lifecycleAdapters = bindMetaMaskMobileLifecycleDeadline(
|
|
77
85
|
createAppLifecycleAdapters({
|
|
78
86
|
actions,
|
|
@@ -81,11 +89,12 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
|
|
|
81
89
|
})
|
|
82
90
|
);
|
|
83
91
|
const lifecycle = surface.actions.lifecycle ? await surface.actions.lifecycle(lifecycleAdapters) : lifecycleAdapters;
|
|
84
|
-
const custom =
|
|
85
|
-
|
|
86
|
-
customActions,
|
|
87
|
-
|
|
88
|
-
|
|
92
|
+
const custom = [
|
|
93
|
+
...coded,
|
|
94
|
+
...createMetaMaskAdapters(adapter, customActions, preparedLiveAdapters).filter(
|
|
95
|
+
(entry) => !codedActions.has(entry.action)
|
|
96
|
+
)
|
|
97
|
+
].filter(
|
|
89
98
|
(entry) => declaredActions.has(entry.action) && !existing.has(entry.action) && !lifecycle.some((lifecycleEntry) => lifecycleEntry.action === entry.action)
|
|
90
99
|
);
|
|
91
100
|
const autoHudDisabled = options.autoHud === false || options.autoHud === void 0 && (process.env.METAMASK_RECIPE_AUTO_HUD === "0" || process.env.METAMASK_RECIPE_AUTO_HUD === "false");
|
|
@@ -166,7 +175,7 @@ function withTrustedMutationContext(adapters, base, externalMutationActions) {
|
|
|
166
175
|
throw error;
|
|
167
176
|
});
|
|
168
177
|
if (invalidatesIdentity) selectedIdentitySha256 = void 0;
|
|
169
|
-
const selected = selectedIdentityFromWalletImport(entry.action, result, base);
|
|
178
|
+
const selected = isFixturePolicyBase(base) ? void 0 : selectedIdentityFromWalletImport(entry.action, result, base);
|
|
170
179
|
if (selected) {
|
|
171
180
|
if (base && base.selectedIdentitySha256 !== selected) {
|
|
172
181
|
throw new Error("Same-run wallet identity changed after its trusted import proof.");
|
|
@@ -196,6 +205,17 @@ function deduplicateLibraryResolutionLogs(logger, suppress = false) {
|
|
|
196
205
|
}
|
|
197
206
|
};
|
|
198
207
|
}
|
|
208
|
+
function bindPluginDigest(sources, actions, pluginDigest) {
|
|
209
|
+
if (!sources || !pluginDigest) return;
|
|
210
|
+
const bind = (digest) => `sha256:${createHash("sha256").update(`${digest}\0${pluginDigest}`).digest("hex")}`;
|
|
211
|
+
for (const action of actions) {
|
|
212
|
+
const source = sources.get(action);
|
|
213
|
+
if (!source) continue;
|
|
214
|
+
const { digest, resolveDigest } = source;
|
|
215
|
+
source.digest = bind(digest ?? "");
|
|
216
|
+
source.resolveDigest = resolveDigest ? async () => bind(await resolveDigest()) : void 0;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
199
219
|
async function prepareLiveAdapterImplementations(platform, actions, sources, trustTaskActions = false) {
|
|
200
220
|
const prepared = /* @__PURE__ */ new Map();
|
|
201
221
|
if (!sources) return prepared;
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -79,8 +79,10 @@ context, prepare) and `readiness.ts` (doctor and status). Do not add
|
|
|
79
79
|
command-local platform ladders for surface-owned behavior:
|
|
80
80
|
- `launch` dispatches through `AdapterSurface.launch` (the device platforms
|
|
81
81
|
share `src/commands/launch/device.ts` through a `DeviceLaunchProfile`);
|
|
82
|
-
- `run`/`call`
|
|
83
|
-
`sourceFingerprint`;
|
|
82
|
+
- `run`/`call` come from `@farmslot/recipe-cli/harness` and read `run`,
|
|
83
|
+
`recording`, `diagnostics`, `observation` and `sourceFingerprint`;
|
|
84
|
+
mm-harness hands them its engine (`src/engine.ts`) and its device targeting
|
|
85
|
+
and plan steps (`src/cli.ts`);
|
|
84
86
|
- the runner reads `actions` (manifest, bundled semantic actions, live adapter
|
|
85
87
|
scripts, `cdp.target`, the `ui.*` transport, adapter wrappers, input checks);
|
|
86
88
|
- `install`/`verify`/`cleanup` read `harness`, `agentic-runtime.json` reads
|
package/docs/RECIPES.md
CHANGED
|
@@ -245,9 +245,37 @@ Use the snapshot recipe's `history` object to bound existing record dates and
|
|
|
245
245
|
`capture_path` to separate evidence when composing calls. Market sampling does
|
|
246
246
|
not repeatedly fetch history.
|
|
247
247
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
248
|
+
Perps orders follow one rule on Mobile, Extension and Core
|
|
249
|
+
(`library/actions/shared/perps/order-policy.mjs`), checked before dispatch:
|
|
250
|
+
|
|
251
|
+
- Testnet: allowed with no funding proof.
|
|
252
|
+
- Mainnet: only from a synced fixture account (`wallet-fixture.json`), as a 1x
|
|
253
|
+
isolated market order with `close_immediately: true`, and the node must
|
|
254
|
+
declare `network: mainnet`. The $15 limit bounds the size the controller would
|
|
255
|
+
submit: (amount + one size step × price) × (1 + max slippage). Anything else
|
|
256
|
+
is refused with `PERPS_MAINNET_ORDER_REFUSED`, naming each broken limit, and
|
|
257
|
+
no order is sent.
|
|
258
|
+
- `close_immediately: true` needs a market order from a flat market: no
|
|
259
|
+
position and no resting order in it. Positions are netted per market, so the
|
|
260
|
+
close takes the market's whole position; don't share a fixture account's
|
|
261
|
+
market with a concurrent run. The action closes the opened position itself
|
|
262
|
+
and proves the market flat. On mainnet it also checks the position opened
|
|
263
|
+
isolated 1x and within $15. A placement that fails after dispatch has its
|
|
264
|
+
position closed too.
|
|
265
|
+
- The account and network the rule allowed are rechecked around the flat-market
|
|
266
|
+
reads, before dispatch and before any close; if either changed, nothing is
|
|
267
|
+
sent or closed.
|
|
268
|
+
- Core still needs `CORE_PERPS_ALLOW_MAINNET_WRITES=1` for any mainnet write.
|
|
269
|
+
|
|
270
|
+
On the Extension, a run without funding files gets that rule through the
|
|
271
|
+
fixture-policy lane: the runner binds the run and a run-owned execution ledger
|
|
272
|
+
(`<artifacts>/mutation-ledger`), and each mutation completes the context from
|
|
273
|
+
the live app, so receipts and resume work as in a funded run. The funding files
|
|
274
|
+
below remain the stricter release lane.
|
|
275
|
+
|
|
276
|
+
The two live recipes require explicit `confirm_live=true`. A funded run's
|
|
277
|
+
trusted runner must also verify the exact consented funding request, preflight,
|
|
278
|
+
release proof, and contained balance proof. Market placement declares the exact provider and
|
|
251
279
|
network; background placement and cancellation refuse a mismatch without
|
|
252
280
|
toggling it. Immediately before publication and dispatch, the action also reads
|
|
253
281
|
the current visible Perps Home balance and requires the runner-trusted minimum.
|
package/docs/cli-contract.md
CHANGED
|
@@ -92,9 +92,11 @@ These are real farm bugs. The goldens freeze the current rejections, so fixing e
|
|
|
92
92
|
|
|
93
93
|
## Terminal adapter
|
|
94
94
|
|
|
95
|
-
`goldens-terminal` freezes the `va-mmcx-terminal-farm` hooks with their exact argv (`recipe_action_manifest`, `recipe_doctor`, `recipe_harness_{install,verify,cleanup}`, `recipe_run`, the browser `boot`/`shutdown` resources and `teardown`), against a fresh fixture slot: no slot dev server, no Extension dist, and the farm's wallet fixture outside the slot, so `install` copies it in. `launch` is frozen on its cold path: it refuses at the missing dist before any browser starts. The slot's CDP port is the one port no port holder reserves: a farm slot with no browser has it free, and the Terminal's port checks probe it.
|
|
95
|
+
`goldens-terminal` freezes the `va-mmcx-terminal-farm` hooks with their exact argv (`recipe_action_manifest`, `recipe_doctor`, `recipe_harness_{install,verify,cleanup}`, `recipe_run`, the browser `boot`/`shutdown` resources and `teardown`), against a fresh fixture slot: no slot dev server, no Extension dist, and the farm's wallet fixture outside the slot, so `install` copies it in. `launch` is frozen on its cold path: it refuses at the missing dist before any browser starts. The slot's CDP port is the one port no port holder reserves: a farm slot with no browser has it free, and the Terminal's port checks probe it.
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
`terminal` is the plugin the `experimental-metamask-recipe-terminal` library declares (`adapters.terminal`, extends the built-in `web-dapp`), so the family loads it: set `GOLDENS_TERMINAL_LIBRARY` to a checkout of that library, which the family copies into its sandbox and puts on `RECIPE_LIBRARY_PATH`. CI has no access to the private library and skips the family (`CI` set); local runs need the variable, and without it outside CI the family fails. A release gate must set it too (proposed for the release checklist). `goldens-discovery` covers the built-in `web-dapp` in its per-adapter loop.
|
|
98
|
+
|
|
99
|
+
Outside this boundary: a successful `launch` and the `temp/recipe/runtime/terminal/` layout (`browser.pid`), the library's recipes, `run terminal.perps.<name> --plan`, and the recipe-terminal `console-guard.mjs` import paths.
|
|
98
100
|
|
|
99
101
|
## Running the goldens
|
|
100
102
|
|
|
@@ -142,7 +144,7 @@ The goldens need the harness to be a git checkout: `execution-provenance.json` `
|
|
|
142
144
|
2. UUIDs become `<UUID>`. Git object ids (40 hex, or a short hex value under a `*head`/`*gitRef`/`*commit` key) become `<GIT_REF>`. ISO-8601 timestamps become `<TIMESTAMP>`, and compact stamps in names `<STAMP>`. Path-derived slot ids `local-<adapter>-<8 hex>` become `local-<adapter>-<HASH>`.
|
|
143
145
|
3. The package version becomes `<HARNESS_VERSION>`. Any other `x.y.z` under a key ending in `version` becomes `<VERSION>`.
|
|
144
146
|
4. Numbers under volatile keys: `*pid` becomes `<PID>`, `inode` `<INODE>`, `*port` `<PORT>`, `*At`/`*Time`/`*timestamp`/`mtime*` `<TIMESTAMP>`, and `*Ms`/`duration*`/`elapsed*` `<DURATION>`. Values inside `schema` and `examples` subtrees (static manifest content) are never rewritten.
|
|
145
|
-
5. `invocationDigest` becomes `<DIGEST>`: it hashes `recipe-invocation.json`, which carries sandbox paths and timestamps and is frozen itself in normalised form. `runner.sourceFingerprint` becomes `<FINGERPRINT>`: it hashes the harness source tree, so any harness edit changes it. A `status` holding `git status --porcelain` output (provenance `runner.status` and `libraries[].status`, empty when clean) becomes `<GIT_STATUS>`. Content-only digests (recipe and dependency digests) stay frozen.
|
|
147
|
+
5. `invocationDigest` becomes `<DIGEST>`: it hashes `recipe-invocation.json`, which carries sandbox paths and timestamps and is frozen itself in normalised form. `runner.sourceFingerprint` becomes `<FINGERPRINT>`: it hashes the harness source tree, so any harness edit changes it. A `status` holding `git status --porcelain` output (provenance `runner.status` and `libraries[].status`, empty when clean) becomes `<GIT_STATUS>`. A loaded adapter plugin's code digest in doctor's `adapter-plugin:<id>` message becomes `sha256:<PLUGIN_DIGEST>`: it hashes the recipe library's plugin files, so any library edit changes it. Content-only digests (recipe and dependency digests) stay frozen.
|
|
146
148
|
6. Durations in text (`0.3s`, `123ms`) become `<DURATION>`.
|
|
147
149
|
|
|
148
150
|
In tree listings, files whose names differ only by a volatile token collapse into one row: a count and the union of their key shapes, or the sorted list of their values in a value tree.
|