@deeeed/metamask-harness 0.87.0 → 0.88.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 +23 -0
- package/adapters/extension/ensure-browser.sh +6 -10
- package/adapters/extension/inject.mjs +1 -0
- package/adapters/extension/launch-browser.cjs +31 -4
- package/adapters/extension/profile-extension-state.cjs +156 -0
- package/adapters/extension/readiness.mjs +21 -2
- package/adapters/manifest.json +8 -0
- package/adapters/mobile/wait-for-bridge.cjs +9 -3
- package/dist/adapters/extension/actions.js +2 -1
- package/dist/adapters/extension/run.js +6 -0
- package/dist/adapters/mobile/android-screenrecord.js +10 -6
- package/dist/adapters/mobile/recording-outcome.js +2 -51
- package/dist/adapters/mobile/run.js +10 -4
- package/dist/adapters/perps-mainnet-cleanup.js +122 -0
- package/dist/engine.js +11 -2
- package/dist/funding-execution-context.js +13 -8
- package/dist/host.js +6 -2
- package/dist/index.js +3 -0
- package/dist/mm-harness-cli.js +4 -2
- package/dist/provider.js +72 -0
- package/dist/recipe-security.js +1 -0
- package/docs/CONTRIBUTING.md +7 -0
- package/docs/RECIPES.md +38 -13
- package/docs/RELEASE-QA-CAPABILITY-MAP.md +5 -1
- package/docs/cli-contract.md +1 -1
- package/library/actions/extension/perps/mutation-receipt.mjs +4 -3
- package/library/actions/extension/perps/perps.mjs +200 -9
- package/library/actions/extension/perps/set_network.mjs +6 -0
- package/library/actions/mobile/perps/perps.mjs +159 -11
- package/library/actions/mobile/perps/place_order.mjs +7 -4
- package/library/actions/shared/perps/mainnet-order-ledger.mjs +378 -0
- package/library/actions/shared/perps/order-policy.mjs +56 -19
- package/library/actions/shared/wallet/fixture-accounts.mjs +9 -3
- package/library/manifests/extension.action-manifest.json +34 -4
- package/library/manifests/mobile.action-manifest.json +18 -5
- package/package.json +14 -9
- package/scripts/validate-release-report.mjs +14 -2
- package/site/assets/help-recipes.json +1 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
VENUE_ORDER_FIELDS,
|
|
3
|
+
cleanupOwns,
|
|
4
|
+
clearMainnetSwitch,
|
|
5
|
+
ledgerOrderTargets,
|
|
6
|
+
mainnetOrderLeftOpenError,
|
|
7
|
+
markRunAborting,
|
|
8
|
+
readHyperliquidVenue,
|
|
9
|
+
readMainnetOrderLedger,
|
|
10
|
+
settleMainnetOrder
|
|
11
|
+
} from "../../library/actions/shared/perps/mainnet-order-ledger.mjs";
|
|
12
|
+
const PENDING_SETTLE_MS = 1e4;
|
|
13
|
+
async function platformPerps(platform) {
|
|
14
|
+
if (platform === "mobile") return import("../../library/actions/mobile/perps/perps.mjs");
|
|
15
|
+
if (platform === "extension") return import("../../library/actions/extension/perps/perps.mjs");
|
|
16
|
+
throw new Error(`no mainnet order cleanup for platform ${platform}`);
|
|
17
|
+
}
|
|
18
|
+
async function currentEntry(projectRoot, id) {
|
|
19
|
+
return (await readMainnetOrderLedger(projectRoot)).entries.find((entry) => entry.id === id);
|
|
20
|
+
}
|
|
21
|
+
async function venueRead(entry) {
|
|
22
|
+
const venue = await readHyperliquidVenue(entry.address, entry.symbol);
|
|
23
|
+
const open = ledgerOrderTargets(entry, venue.openOrders, VENUE_ORDER_FIELDS);
|
|
24
|
+
return { source: venue.source, open: open.map((order) => order.orderId), positionSize: venue.positionSize };
|
|
25
|
+
}
|
|
26
|
+
async function closeOut(projectRoot, env, via, id) {
|
|
27
|
+
const deadline = Date.now() + PENDING_SETTLE_MS;
|
|
28
|
+
for (; ; ) {
|
|
29
|
+
const entry = await currentEntry(projectRoot, id);
|
|
30
|
+
if (!entry) return;
|
|
31
|
+
const cancelInApp = async () => (await platformPerps(entry.platform)).cancelMainnetLedgerOrder(entry, { projectRoot, env });
|
|
32
|
+
let proof = entry.state === "cancelled" ? { canceled: entry.cancelProof.canceled, read: null } : await cancelInApp();
|
|
33
|
+
let venue = await venueRead(entry);
|
|
34
|
+
if (entry.state === "cancelled" && venue.open.length > 0) {
|
|
35
|
+
proof = await cancelInApp();
|
|
36
|
+
venue = await venueRead(entry);
|
|
37
|
+
}
|
|
38
|
+
for (let wait = 0; venue.open.length > 0 && proof.canceled.length > 0 && wait < 5; wait += 1) {
|
|
39
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
40
|
+
venue = await venueRead(entry);
|
|
41
|
+
}
|
|
42
|
+
if (venue.open.length > 0) {
|
|
43
|
+
throw new Error(`the venue still lists order(s) ${venue.open.join(", ")} after the app ${proof.read?.notOpen ? "reported none open" : "cancelled"}`);
|
|
44
|
+
}
|
|
45
|
+
if (proof.read?.notOpen && entry.state === "pending" && Date.now() < deadline) {
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const recorded = { via: entry.cancelProof?.via ?? via, canceled: proof.canceled, app: proof.read, venue };
|
|
50
|
+
const evidencePath = await settleMainnetOrder(projectRoot, entry, recorded);
|
|
51
|
+
process.stderr.write(
|
|
52
|
+
`mainnet ${entry.symbol} order ${entry.orderId ?? "(pending)"}: ${proof.canceled.length > 0 ? "cancelled" : "not open"} at run ${via}, gone at the venue; proof in ${evidencePath}
|
|
53
|
+
`
|
|
54
|
+
);
|
|
55
|
+
if (venue.positionSize !== Number(entry.baselinePositionSize ?? 0)) {
|
|
56
|
+
throw new Error(`the ${entry.symbol} position is ${venue.positionSize}, was ${entry.baselinePositionSize ?? 0} before the order: it filled; close that position now`);
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function cleanup(projectRoot, env, via) {
|
|
62
|
+
const ledger = await readMainnetOrderLedger(projectRoot);
|
|
63
|
+
const failures = [];
|
|
64
|
+
for (const entry of ledger.entries.filter((record) => cleanupOwns(record, process.pid))) {
|
|
65
|
+
try {
|
|
66
|
+
await closeOut(projectRoot, env, via, entry.id);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
failures.push({ entry, error });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (failures.length > 0) throw mainnetOrderLeftOpenError(failures);
|
|
72
|
+
if (ledger.networkSwitch && cleanupOwns(ledger.networkSwitch, process.pid)) {
|
|
73
|
+
const { restoreTestnetAfterRun } = await import("../../library/actions/extension/perps/perps.mjs");
|
|
74
|
+
await restoreTestnetAfterRun(ledger.networkSwitch, { projectRoot });
|
|
75
|
+
await clearMainnetSwitch(projectRoot);
|
|
76
|
+
process.stderr.write(`Perps network switched back to testnet at run ${via}.
|
|
77
|
+
`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
let inFlight;
|
|
81
|
+
function cancelRunMainnetOrders(projectRoot, env, via) {
|
|
82
|
+
inFlight ??= cleanup(projectRoot, env, via).finally(() => {
|
|
83
|
+
inFlight = void 0;
|
|
84
|
+
});
|
|
85
|
+
return inFlight;
|
|
86
|
+
}
|
|
87
|
+
const ABORT_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
88
|
+
async function abortRunMainnetOrders(projectRoot) {
|
|
89
|
+
await markRunAborting(projectRoot, process.pid);
|
|
90
|
+
await cancelRunMainnetOrders(projectRoot, process.env, "abort");
|
|
91
|
+
}
|
|
92
|
+
async function withMainnetOrderAbort(projectRoot, run) {
|
|
93
|
+
let aborting;
|
|
94
|
+
const remove = () => {
|
|
95
|
+
for (const signal of ABORT_SIGNALS) process.off(signal, onSignal);
|
|
96
|
+
};
|
|
97
|
+
function onSignal(signal) {
|
|
98
|
+
if (aborting) return;
|
|
99
|
+
aborting = abortRunMainnetOrders(projectRoot).then(
|
|
100
|
+
() => void 0,
|
|
101
|
+
(error) => {
|
|
102
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}
|
|
103
|
+
`);
|
|
104
|
+
}
|
|
105
|
+
).then(() => {
|
|
106
|
+
remove();
|
|
107
|
+
process.kill(process.pid, signal);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
for (const signal of ABORT_SIGNALS) process.on(signal, onSignal);
|
|
111
|
+
try {
|
|
112
|
+
return await run();
|
|
113
|
+
} finally {
|
|
114
|
+
if (aborting) await aborting;
|
|
115
|
+
else remove();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export {
|
|
119
|
+
abortRunMainnetOrders,
|
|
120
|
+
cancelRunMainnetOrders,
|
|
121
|
+
withMainnetOrderAbort
|
|
122
|
+
};
|
package/dist/engine.js
CHANGED
|
@@ -6,11 +6,15 @@ import {
|
|
|
6
6
|
fixturePolicyMutationBase,
|
|
7
7
|
fundingOptionsFromCli,
|
|
8
8
|
isFixturePolicyBase,
|
|
9
|
-
loadTrustedMutationBase
|
|
9
|
+
loadTrustedMutationBase,
|
|
10
|
+
validateTrustedMutationPlan
|
|
10
11
|
} from "./funding-execution-context.js";
|
|
11
12
|
import { resolveActionManifest, validateManifest } from "./manifest.js";
|
|
12
13
|
import { LIBRARY_ACTION_NAMESPACE, runnerDir } from "./paths.js";
|
|
13
14
|
import { metaMaskActionExecutionCapabilities } from "./recipe-security.js";
|
|
15
|
+
async function loadEngineMutation({ cli, adapter, ...run }) {
|
|
16
|
+
return await loadTrustedMutationBase({ ...fundingOptionsFromCli(cli), ...run }) ?? (getAdapterSurface(adapter).actions.trustedMutation === true ? fixturePolicyMutationBase({ artifactsDir: run.artifactsDir, rootRecipe: run.rootRecipe }) : void 0);
|
|
17
|
+
}
|
|
14
18
|
const metaMaskEngine = {
|
|
15
19
|
bundledLibrary: {
|
|
16
20
|
name: "metamask",
|
|
@@ -24,12 +28,17 @@ const metaMaskEngine = {
|
|
|
24
28
|
const { createMetaMaskRunner } = await import("./runner.js");
|
|
25
29
|
return createMetaMaskRunner(adapter, manifest, options);
|
|
26
30
|
},
|
|
31
|
+
// Preflight checks policy; execution also claims the plan and checks claim conflicts.
|
|
27
32
|
// Funding files from the run's command line, bound to the run's plan. Without
|
|
28
33
|
// them, a platform whose actions take a trusted mutation (the Extension) runs a
|
|
29
34
|
// plan with external mutations in the fixture-policy lane, where each action
|
|
30
35
|
// applies the shared order policy; any other plan runs without one.
|
|
31
36
|
trustedMutation: {
|
|
32
|
-
|
|
37
|
+
preflight: async (input, plan) => {
|
|
38
|
+
const base = await loadEngineMutation(input);
|
|
39
|
+
if (base) validateTrustedMutationPlan(base, plan);
|
|
40
|
+
},
|
|
41
|
+
load: loadEngineMutation,
|
|
33
42
|
// Nothing to bind (undefined): recipe-cli keeps the runner it planned with.
|
|
34
43
|
authorize: async (base, plan) => {
|
|
35
44
|
if (!base) return void 0;
|
|
@@ -181,10 +181,9 @@ function fixturePolicyMutationBase(options) {
|
|
|
181
181
|
function isFixturePolicyBase(base) {
|
|
182
182
|
return base?.proofLane === FIXTURE_POLICY_LANE;
|
|
183
183
|
}
|
|
184
|
-
async function authorizeFixturePolicyPlan(base,
|
|
184
|
+
async function authorizeFixturePolicyPlan(base, executionPlanSha256) {
|
|
185
185
|
const source = fixturePolicySources.get(base);
|
|
186
186
|
if (!source) throw new Error("Fixture-policy plan authorization requires the runner-created context.");
|
|
187
|
-
const executionPlanSha256 = normalizeRecipeDigest(plan.digest);
|
|
188
187
|
await mkdir(source.artifactsDir, { recursive: true, mode: 448 });
|
|
189
188
|
const artifactsDir = await realpath(source.artifactsDir);
|
|
190
189
|
const artifactsIdentitySha256 = await directoryIdentity(artifactsDir);
|
|
@@ -204,11 +203,9 @@ async function authorizeFixturePolicyPlan(base, plan) {
|
|
|
204
203
|
});
|
|
205
204
|
return authorized;
|
|
206
205
|
}
|
|
207
|
-
|
|
208
|
-
if (isFixturePolicyBase(base)) return authorizeFixturePolicyPlan(base, plan);
|
|
209
|
-
const source = fundingSources.get(base);
|
|
210
|
-
if (!source) throw new Error("Funding plan authorization requires the original runner-verified context.");
|
|
206
|
+
function validateTrustedMutationPlan(base, plan) {
|
|
211
207
|
const executionPlanSha256 = normalizeRecipeDigest(plan.digest);
|
|
208
|
+
if (isFixturePolicyBase(base)) return { executionPlanSha256, operationCounts: [] };
|
|
212
209
|
const nodes = Array.isArray(plan.nodes) ? plan.nodes : [];
|
|
213
210
|
const actual = /* @__PURE__ */ new Map();
|
|
214
211
|
for (const node of nodes) {
|
|
@@ -235,9 +232,16 @@ async function authorizeTrustedMutationPlan(base, plan, testIo = {}) {
|
|
|
235
232
|
}
|
|
236
233
|
}
|
|
237
234
|
if (actual.size === 0) throw new Error("Funding documents require a resolved external-mutation plan.");
|
|
235
|
+
const operationCounts = [...expected.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([action, count]) => ({ action, count }));
|
|
236
|
+
return { executionPlanSha256, operationCounts };
|
|
237
|
+
}
|
|
238
|
+
async function authorizeTrustedMutationPlan(base, plan, testIo = {}) {
|
|
239
|
+
const { executionPlanSha256, operationCounts } = validateTrustedMutationPlan(base, plan);
|
|
240
|
+
if (isFixturePolicyBase(base)) return authorizeFixturePolicyPlan(base, executionPlanSha256);
|
|
241
|
+
const source = fundingSources.get(base);
|
|
242
|
+
if (!source) throw new Error("Funding plan authorization requires the original runner-verified context.");
|
|
238
243
|
await mkdir(source.artifactsDir, { recursive: true, mode: 448 });
|
|
239
244
|
const artifactsIdentitySha256 = await directoryIdentity(source.artifactsDir);
|
|
240
|
-
const operationCounts = [...expected.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([action, count]) => ({ action, count }));
|
|
241
245
|
const runId = sha256(canonical({
|
|
242
246
|
requestSha256: base.requestSha256,
|
|
243
247
|
preflightSha256: base.preflightSha256,
|
|
@@ -1529,5 +1533,6 @@ export {
|
|
|
1529
1533
|
selectedIdentityFromWalletImport,
|
|
1530
1534
|
serializeTrustedMutationContext,
|
|
1531
1535
|
trustedMutationContextExtras,
|
|
1532
|
-
trustedMutationForAction
|
|
1536
|
+
trustedMutationForAction,
|
|
1537
|
+
validateTrustedMutationPlan
|
|
1533
1538
|
};
|
package/dist/host.js
CHANGED
|
@@ -3,7 +3,7 @@ import { fileURLToPath } from "node:url";
|
|
|
3
3
|
import { configureHarnessAdapters, configureHarnessHost } from "@farmslot/recipe-cli/harness";
|
|
4
4
|
import { adapterRegistry } from "./adapters/registry.js";
|
|
5
5
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
6
|
-
|
|
6
|
+
const providerHost = {
|
|
7
7
|
name: "mm-harness",
|
|
8
8
|
product: "MetaMask",
|
|
9
9
|
envPrefix: "MM_HARNESS",
|
|
@@ -28,5 +28,9 @@ configureHarnessHost({
|
|
|
28
28
|
"stop",
|
|
29
29
|
"verify"
|
|
30
30
|
]
|
|
31
|
-
}
|
|
31
|
+
};
|
|
32
|
+
configureHarnessHost(providerHost);
|
|
32
33
|
configureHarnessAdapters(adapterRegistry);
|
|
34
|
+
export {
|
|
35
|
+
providerHost
|
|
36
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import "./host.js";
|
|
2
|
+
import { createProvider, providerHost } from "./provider.js";
|
|
2
3
|
import { createDoctorReport } from "./doctor.js";
|
|
3
4
|
import { loadActionManifest, validateManifest } from "./manifest.js";
|
|
4
5
|
import { createMetaMaskExtensionRunner, createMetaMaskMobileRunner, createMetaMaskRunner } from "./runner.js";
|
|
@@ -13,10 +14,12 @@ export {
|
|
|
13
14
|
createMetaMaskExtensionRunner,
|
|
14
15
|
createMetaMaskMobileRunner,
|
|
15
16
|
createMetaMaskRunner,
|
|
17
|
+
createProvider,
|
|
16
18
|
extensionIdPath,
|
|
17
19
|
importRecipeHarnessRuntimeBrowserExtension,
|
|
18
20
|
importRecipeHarnessRuntimeCdp,
|
|
19
21
|
loadActionManifest,
|
|
22
|
+
providerHost,
|
|
20
23
|
validateManifest,
|
|
21
24
|
walletFixturePath
|
|
22
25
|
};
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "@farmslot/recipe-cli/harness";
|
|
23
23
|
import { stopProjectCollector } from "../library/actions/shared/analytics/collector.mjs";
|
|
24
24
|
import { adoptPluginAdapter, afterAdapterLoad, configuredLibrarySources } from "./adapters/plugin-surface.js";
|
|
25
|
+
import { withMainnetOrderAbort } from "./adapters/perps-mainnet-cleanup.js";
|
|
25
26
|
import { adapterRegistry } from "./adapters/registry.js";
|
|
26
27
|
import {
|
|
27
28
|
explainMissingAdapterLibrary,
|
|
@@ -155,8 +156,9 @@ const PUBLIC_RUN = {
|
|
|
155
156
|
"execution-template": { run: handleExecutionTemplate },
|
|
156
157
|
actions: { run: (args) => handleActions(parseArgs(args), { catalog: metaMaskEngine }) },
|
|
157
158
|
stop: { run: (args) => handleStop(args, stopOptions) },
|
|
158
|
-
|
|
159
|
-
|
|
159
|
+
// A signal during run or call still cancels the run's mainnet orders.
|
|
160
|
+
call: { run: (args) => withMainnetOrderAbort(targetPath(parseArgs(args).options), () => handleCall(args, callOptions)) },
|
|
161
|
+
run: { run: (args) => withMainnetOrderAbort(targetPath(parseArgs(args).options), () => handleRun(args, runOptions)) },
|
|
160
162
|
last: { run: (args) => handleLast(parseArgs(args)) },
|
|
161
163
|
doctor: { run: (args) => handleDoctor(parseArgs(args)) },
|
|
162
164
|
check: { run: handleCheck },
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import "./adapters/surface.js";
|
|
2
|
+
import {
|
|
3
|
+
configureHarnessAdapters,
|
|
4
|
+
adapterForPlatform,
|
|
5
|
+
applyRuntimeDirOption,
|
|
6
|
+
configureHarnessHost,
|
|
7
|
+
ensureAdapterLoaded,
|
|
8
|
+
optionFlag,
|
|
9
|
+
optionStrings,
|
|
10
|
+
usageError
|
|
11
|
+
} from "@farmslot/recipe-cli/harness";
|
|
12
|
+
import { MAINNET_RUN_OWNER_ENV } from "../library/actions/shared/perps/mainnet-order-ledger.mjs";
|
|
13
|
+
import { adoptPluginAdapter, afterAdapterLoad, configuredLibrarySources, operatorLibraryEnv } from "./adapters/plugin-surface.js";
|
|
14
|
+
import { abortRunMainnetOrders, cancelRunMainnetOrders } from "./adapters/perps-mainnet-cleanup.js";
|
|
15
|
+
import { adapterRegistry } from "./adapters/registry.js";
|
|
16
|
+
import { getAdapterSurface } from "./adapters/surface.js";
|
|
17
|
+
import { applyDeviceTargeting } from "./commands/device-target.js";
|
|
18
|
+
import { metaMaskEngine } from "./engine.js";
|
|
19
|
+
import { providerHost } from "./host.js";
|
|
20
|
+
import { providerHost as providerHost2 } from "./host.js";
|
|
21
|
+
async function createProvider(context) {
|
|
22
|
+
configureHarnessHost(context.host ?? providerHost);
|
|
23
|
+
configureHarnessAdapters(adapterRegistry);
|
|
24
|
+
const selection = context.adapter?.value;
|
|
25
|
+
if (!selection) throw usageError("The MetaMask provider requires a resolved adapter.");
|
|
26
|
+
const adapter = adapterForPlatform(selection) ?? selection;
|
|
27
|
+
const options = {
|
|
28
|
+
...context.options,
|
|
29
|
+
...!context.options?.platform && (selection === "ios" || selection === "android") ? { platform: selection } : {}
|
|
30
|
+
};
|
|
31
|
+
await ensureAdapterLoaded(adapter, {
|
|
32
|
+
libraries: optionStrings(options, "library"),
|
|
33
|
+
configured: context.libraries ?? configuredLibrarySources(),
|
|
34
|
+
env: operatorLibraryEnv(),
|
|
35
|
+
adopt: adoptPluginAdapter
|
|
36
|
+
});
|
|
37
|
+
await afterAdapterLoad();
|
|
38
|
+
const runtime = getAdapterSurface(adapter);
|
|
39
|
+
const executes = (context.command === "run" || context.command === "call") && !["plan", "list", "describe", "help"].some((flag) => optionFlag(options, flag));
|
|
40
|
+
if (executes) {
|
|
41
|
+
applyRuntimeDirOption(options);
|
|
42
|
+
runtime.resolveSlotPorts(context.target.value);
|
|
43
|
+
const selected = applyDeviceTargeting(context.command, adapter, {
|
|
44
|
+
...options,
|
|
45
|
+
target: context.target.value
|
|
46
|
+
}, { gate: true, rerun: "" });
|
|
47
|
+
if (selected.ok === false) throw Object.assign(usageError(selected.message), { code: selected.code, userAction: selected.userAction });
|
|
48
|
+
}
|
|
49
|
+
const previousOwner = process.env[MAINNET_RUN_OWNER_ENV];
|
|
50
|
+
if (executes) process.env[MAINNET_RUN_OWNER_ENV] = String(process.pid);
|
|
51
|
+
return {
|
|
52
|
+
runtime,
|
|
53
|
+
engine: metaMaskEngine,
|
|
54
|
+
...executes ? {
|
|
55
|
+
async cancel() {
|
|
56
|
+
await abortRunMainnetOrders(context.target.value);
|
|
57
|
+
},
|
|
58
|
+
async finalize() {
|
|
59
|
+
try {
|
|
60
|
+
await cancelRunMainnetOrders(context.target.value, process.env, "teardown");
|
|
61
|
+
} finally {
|
|
62
|
+
if (previousOwner === void 0) delete process.env[MAINNET_RUN_OWNER_ENV];
|
|
63
|
+
else process.env[MAINNET_RUN_OWNER_ENV] = previousOwner;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} : {}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export {
|
|
70
|
+
createProvider,
|
|
71
|
+
providerHost2 as providerHost
|
|
72
|
+
};
|
package/dist/recipe-security.js
CHANGED
|
@@ -76,6 +76,7 @@ const APP_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
76
76
|
"metamask.perps.open_balance_action",
|
|
77
77
|
"metamask.perps.assert_visible_consistency",
|
|
78
78
|
"metamask.perps.start_state",
|
|
79
|
+
"metamask.perps.set_network",
|
|
79
80
|
"metamask.perps.teardown_state",
|
|
80
81
|
// Consent changes wallet state and is therefore an app mutation.
|
|
81
82
|
"metamask.analytics.set_consent",
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -199,6 +199,13 @@ when packed, so `agent-runtime` and `adapter-rn` can retain older copies until
|
|
|
199
199
|
republished. Verify the complete install with
|
|
200
200
|
`bash tests/contract/packaging-dist-install.test.sh` before releasing the harness.
|
|
201
201
|
|
|
202
|
+
`yarn install` hardlinks every `node_modules` file to Yarn's store
|
|
203
|
+
(`~/.yarn/berry/store`), shared by all worktrees. Never edit an installed
|
|
204
|
+
dependency in place: a write into an existing file reaches the store and every
|
|
205
|
+
other worktree, and a reinstall does not undo it. Use `yarn dev:link-farmslot`
|
|
206
|
+
for local Farmslot builds. To repair a package, remove `node_modules/<package>`
|
|
207
|
+
in one worktree and run `yarn install`.
|
|
208
|
+
|
|
202
209
|
Docs describe only current behavior. History belongs in the changelog and Git;
|
|
203
210
|
do not add handovers, audits, speculative roadmaps, or duplicate command guides
|
|
204
211
|
under `docs/`.
|
package/docs/RECIPES.md
CHANGED
|
@@ -261,12 +261,34 @@ Perps orders follow one rule on Mobile, Extension and Core
|
|
|
261
261
|
(`library/actions/shared/perps/order-policy.mjs`), checked before dispatch:
|
|
262
262
|
|
|
263
263
|
- Testnet: allowed with no funding proof.
|
|
264
|
-
- Mainnet: only from a synced fixture account (`wallet-fixture.json`),
|
|
265
|
-
isolated
|
|
266
|
-
|
|
267
|
-
submit: (amount + one size step × price) × (1 + max
|
|
268
|
-
|
|
269
|
-
|
|
264
|
+
- Mainnet: only from a synced fixture account (`wallet-fixture.json`), 1x
|
|
265
|
+
isolated, and the node must declare `network: mainnet`. Either a market order
|
|
266
|
+
with `close_immediately: true`, where the $15 limit bounds the size the
|
|
267
|
+
controller would submit: (amount + one size step × price) × (1 + max
|
|
268
|
+
slippage); or, on Mobile and Extension `place_order`, a `dev1` Hyperliquid
|
|
269
|
+
limit order away from the live market price, sent post-only and bounded at its limit
|
|
270
|
+
price: amount + one size step × limit price. Mainnet orders take no
|
|
271
|
+
`current_price`. Anything else is refused with `PERPS_MAINNET_ORDER_REFUSED`,
|
|
272
|
+
naming each broken limit, and no order is sent: more than 1x or $15, a limit
|
|
273
|
+
that would fill now, another account, Core, and `ensure_orders` or
|
|
274
|
+
`ensure_positions` opening state.
|
|
275
|
+
- A mainnet limit order lasts one run. It is tracked before dispatch in
|
|
276
|
+
`temp/recipe/runtime/perps-mainnet-orders.json`, and the run cancels whatever
|
|
277
|
+
is still open when it ends (success, failure, or abort by signal; after a
|
|
278
|
+
kill, the next run on the checkout). The order counts as gone only when the
|
|
279
|
+
Hyperliquid API, read for the placing address, no longer lists it. Each
|
|
280
|
+
order's ID, cancel result and both reads (app and venue) land in
|
|
281
|
+
`<artifacts>/perps-mainnet-orders.json`. An order
|
|
282
|
+
that cannot be proven cancelled, or that filled (the market's position moved),
|
|
283
|
+
fails the run with `PERPS_MAINNET_ORDER_LEFT_OPEN`; a fill is reported, never
|
|
284
|
+
closed for you. On mainnet, `close_orders` cancels only orders the run placed,
|
|
285
|
+
and leaving mainnet while the run holds one is refused
|
|
286
|
+
(`PERPS_MAINNET_ORDER_OPEN`).
|
|
287
|
+
- Switching to mainnet: Mobile `metamask.perps.start_state network=mainnet`;
|
|
288
|
+
Extension `metamask.perps.set_network network=mainnet`, which the run switches
|
|
289
|
+
back to testnet at its end (the fixture loads testnet). Mobile stays on
|
|
290
|
+
mainnet after the run: switch back with `start_state network=testnet` once
|
|
291
|
+
the run's orders are settled.
|
|
270
292
|
- `close_immediately: true` needs a market order from a flat market: no
|
|
271
293
|
position and no resting order in it. Positions are netted per market, so the
|
|
272
294
|
close takes the market's whole position; don't share a fixture account's
|
|
@@ -342,9 +364,11 @@ rather than refuse. Extension `ensure_*` and `teardown_state` only assert, so
|
|
|
342
364
|
converge there through an explicitly funded leaf action. Never broaden cleanup
|
|
343
365
|
onto an account or market the task does not own; obtain an owned one instead
|
|
344
366
|
of blocking. On mainnet, orders follow the Perps order rule above, Core needs
|
|
345
|
-
`CORE_PERPS_ALLOW_MAINNET_WRITES=1` for any write, Mobile
|
|
346
|
-
whatever network the app is on,
|
|
347
|
-
orders and
|
|
367
|
+
`CORE_PERPS_ALLOW_MAINNET_WRITES=1` for any write, Mobile `close_positions`
|
|
368
|
+
acts on whatever network the app is on, `close_orders` there cancels only the
|
|
369
|
+
run's own orders, and Extension refuses mainnet writes other than orders,
|
|
370
|
+
`close_visible_position` and cancelling the run's own orders in the
|
|
371
|
+
fixture-policy lane.
|
|
348
372
|
|
|
349
373
|
### Mobile advanced-order setup
|
|
350
374
|
|
|
@@ -445,10 +469,11 @@ A recording that fails fails the run: the `record.video` trace entry starts
|
|
|
445
469
|
with `RECORDING_FAILED` or `RECORDER_INTERRUPTED`, names the cause, and says the
|
|
446
470
|
run has no video. When capture stopped mid-run but a finalized partial file was
|
|
447
471
|
kept (capture-helper 0.3.1's `stream_interrupted`, or the screenrecord files
|
|
448
|
-
finished before a failure), the video is published
|
|
449
|
-
`
|
|
450
|
-
|
|
451
|
-
|
|
472
|
+
finished before a failure), the video is published, its manifest entry's
|
|
473
|
+
`interruption` gives the frames, the video time up to the last captured frame
|
|
474
|
+
(`mediaTimeMs`) and the cause, and the run fails as `CAPTURE_INTERRUPTED`, a
|
|
475
|
+
capture (environment) failure, not a product one. `mm-harness doctor` shows the
|
|
476
|
+
recorder for the selected device and is not ready while it cannot record.
|
|
452
477
|
|
|
453
478
|
### Mobile setup and command budgets
|
|
454
479
|
|
|
@@ -63,7 +63,11 @@ outside the declared root, incomplete run directories, and any non-empty
|
|
|
63
63
|
|
|
64
64
|
Supported verdicts are `PASS`, `FAIL`, `BLOCKED_FUNDS`, `BLOCKED_COST`, and
|
|
65
65
|
`BLOCKED_ENVIRONMENT`. Product failures must remain failures; infrastructure,
|
|
66
|
-
funding, and cost limits must not be reported as passes.
|
|
66
|
+
funding, and cost limits must not be reported as passes. If every run cited by a
|
|
67
|
+
case failed only on `CAPTURE_INTERRUPTED`, the gate requires `BLOCKED_ENVIRONMENT`
|
|
68
|
+
with "evidence incomplete (capture interrupted)" in its summary and the partial
|
|
69
|
+
video in its run directory. Retain interrupted attempts alongside the retry's
|
|
70
|
+
evidence. A passing retry can be `PASS`; a product-failing retry remains `FAIL`.
|
|
67
71
|
|
|
68
72
|
## Capability matrix
|
|
69
73
|
|
package/docs/cli-contract.md
CHANGED
|
@@ -146,7 +146,7 @@ The goldens need the harness to be a git checkout: `execution-provenance.json` `
|
|
|
146
146
|
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>`.
|
|
147
147
|
3. The package version becomes `<HARNESS_VERSION>`. Any other `x.y.z` under a key ending in `version` becomes `<VERSION>`.
|
|
148
148
|
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.
|
|
149
|
-
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.
|
|
149
|
+
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. Within `executionPlan`, SHA-256 `digest` and `executionContextDigest` values become `sha256:<DIGEST>` because plans bind sandbox paths, environment and loaded source bytes. `schema` and `examples` remain exempt. Other content-only digests (recipe and dependency digests) stay frozen.
|
|
150
150
|
6. Durations in text (`0.3s`, `123ms`) become `<DURATION>`.
|
|
151
151
|
|
|
152
152
|
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.
|
|
@@ -228,8 +228,9 @@ export function inFixturePolicyLane(input) {
|
|
|
228
228
|
* non-order mutation (allowed on testnet; on mainnet only a close from a fixture account).
|
|
229
229
|
* @returns {{ input, network, policy }} input with the completed context.
|
|
230
230
|
*/
|
|
231
|
-
export async function withFixturePolicyContext(input, page, { operations, order = null, mainnetAllowed = false }) {
|
|
232
|
-
// mainnetAllowed: a non-order mutation (a close
|
|
231
|
+
export async function withFixturePolicyContext(input, page, { operations, order = null, mainnetAllowed = false, mainnetRefusal = null }) {
|
|
232
|
+
// mainnetAllowed: a non-order mutation (a close, or a cancel of an order this
|
|
233
|
+
// run placed) a fixture account may run on mainnet; mainnetRefusal says why not.
|
|
233
234
|
if (!inFixturePolicyLane(input)) return { input, network: null, policy: null };
|
|
234
235
|
const state = await page.evaluate(readPerpsRuntimeStateExpression());
|
|
235
236
|
if (!state?.available || !state?.networkKnown) {
|
|
@@ -246,7 +247,7 @@ export async function withFixturePolicyContext(input, page, { operations, order
|
|
|
246
247
|
if (!order && network === 'mainnet'
|
|
247
248
|
&& (!mainnetAllowed || !(await isFixtureAccount(projectRoot, address)))) {
|
|
248
249
|
throw Object.assign(
|
|
249
|
-
new Error(`PERPS_MAINNET_ORDER_REFUSED: ${input.action} on mainnet needs the funding-file release lane${mainnetAllowed ? ' or a synced test account' : ''}; refused before dispatch.`),
|
|
250
|
+
new Error(`PERPS_MAINNET_ORDER_REFUSED: ${input.action} on mainnet ${mainnetRefusal && !mainnetAllowed ? `${mainnetRefusal} without funding files` : `needs the funding-file release lane${mainnetAllowed ? ' or a synced test account' : ''}`}; refused before dispatch.`),
|
|
250
251
|
{ code: 'PERPS_MAINNET_ORDER_REFUSED' },
|
|
251
252
|
);
|
|
252
253
|
}
|