@muhaven/mcp 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +101 -0
- package/dist/broker.cjs +1092 -149
- package/dist/broker.js +1093 -150
- package/dist/index.cjs +2364 -222
- package/dist/index.d.cts +763 -19
- package/dist/index.d.ts +763 -19
- package/dist/index.js +2350 -211
- package/manifest.json +41 -4
- package/package.json +3 -2
- package/tool-hashes.json +8 -4
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var promises = require('fs/promises');
|
|
3
|
+
var promises$1 = require('fs/promises');
|
|
4
4
|
require('fs');
|
|
5
5
|
var path = require('path');
|
|
6
6
|
var url = require('url');
|
|
@@ -8,9 +8,13 @@ var index_js = require('@modelcontextprotocol/sdk/server/index.js');
|
|
|
8
8
|
var stdio_js = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
9
9
|
var types_js = require('@modelcontextprotocol/sdk/types.js');
|
|
10
10
|
var zod = require('zod');
|
|
11
|
+
var zodToJsonSchema = require('zod-to-json-schema');
|
|
11
12
|
var os = require('os');
|
|
12
13
|
var net = require('net');
|
|
14
|
+
var promises = require('timers/promises');
|
|
15
|
+
var viem = require('viem');
|
|
13
16
|
var crypto = require('crypto');
|
|
17
|
+
var accountAbstraction = require('viem/account-abstraction');
|
|
14
18
|
var accounts = require('viem/accounts');
|
|
15
19
|
|
|
16
20
|
// ../../node_modules/.pnpm/tsup@8.5.1_postcss@8.5.14_tsx@4.21.0_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js
|
|
@@ -22,6 +26,10 @@ var DEFAULT_REQUEST_TIMEOUT_MS = 15e3;
|
|
|
22
26
|
var DEFAULT_BROKER_TIMEOUT_MS = 5e3;
|
|
23
27
|
var DEFAULT_BROKER_MAX_BYTES = 64 * 1024;
|
|
24
28
|
var DEFAULT_JWT_CACHE_TTL_SEC = 30;
|
|
29
|
+
var DEFAULT_BUNDLER_TIMEOUT_MS = 2e4;
|
|
30
|
+
var DEFAULT_CHAIN_ID = 421614;
|
|
31
|
+
var DEFAULT_ENTRY_POINT_ADDRESS = "0x0000000071727De22E5E9d8BAf0edAc6f37da032";
|
|
32
|
+
var ADDRESS_HEX_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
25
33
|
function defaultBrokerEndpoint() {
|
|
26
34
|
if (os.platform() === "win32") {
|
|
27
35
|
const user = process.env.USERNAME ?? "default";
|
|
@@ -97,6 +105,35 @@ function loadMcpConfig(env = process.env) {
|
|
|
97
105
|
const requestTimeoutMs = readEnvInt("MUHAVEN_REQUEST_TIMEOUT_MS", DEFAULT_REQUEST_TIMEOUT_MS, env);
|
|
98
106
|
const brokerTimeoutMs = readEnvInt("MUHAVEN_BROKER_TIMEOUT_MS", DEFAULT_BROKER_TIMEOUT_MS, env);
|
|
99
107
|
const jwtCacheTtlSec = readEnvInt("MUHAVEN_JWT_CACHE_TTL_SEC", DEFAULT_JWT_CACHE_TTL_SEC, env);
|
|
108
|
+
const bundlerUrlRaw = readEnv("MUHAVEN_BUNDLER_URL", env);
|
|
109
|
+
let bundlerUrl;
|
|
110
|
+
if (bundlerUrlRaw !== void 0) {
|
|
111
|
+
const validationErr = validatePublicUrlEnv("MUHAVEN_BUNDLER_URL", bundlerUrlRaw);
|
|
112
|
+
if (validationErr) throw new Error(validationErr);
|
|
113
|
+
bundlerUrl = trimTrailingSlash(bundlerUrlRaw);
|
|
114
|
+
}
|
|
115
|
+
const bundlerTimeoutMs = readEnvInt("MUHAVEN_BUNDLER_TIMEOUT_MS", DEFAULT_BUNDLER_TIMEOUT_MS, env);
|
|
116
|
+
const chainId = readEnvInt("MUHAVEN_CHAIN_ID", DEFAULT_CHAIN_ID, env);
|
|
117
|
+
const subscriptionAddressRaw = readEnv("MUHAVEN_SUBSCRIPTION_ADDRESS", env);
|
|
118
|
+
let subscriptionAddress;
|
|
119
|
+
if (subscriptionAddressRaw !== void 0) {
|
|
120
|
+
if (!ADDRESS_HEX_RE.test(subscriptionAddressRaw)) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`MUHAVEN_SUBSCRIPTION_ADDRESS must be a 0x-prefixed 20-byte hex string (got ${JSON.stringify(subscriptionAddressRaw)})`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
subscriptionAddress = subscriptionAddressRaw.toLowerCase();
|
|
126
|
+
}
|
|
127
|
+
const entryPointAddressRaw = readEnv("MUHAVEN_ENTRY_POINT", env);
|
|
128
|
+
let entryPointAddress = DEFAULT_ENTRY_POINT_ADDRESS;
|
|
129
|
+
if (entryPointAddressRaw !== void 0) {
|
|
130
|
+
if (!ADDRESS_HEX_RE.test(entryPointAddressRaw)) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`MUHAVEN_ENTRY_POINT must be a 0x-prefixed 20-byte hex string (got ${JSON.stringify(entryPointAddressRaw)})`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
entryPointAddress = entryPointAddressRaw.toLowerCase();
|
|
136
|
+
}
|
|
100
137
|
return {
|
|
101
138
|
backendBaseUrl,
|
|
102
139
|
dashboardBaseUrl,
|
|
@@ -105,7 +142,12 @@ function loadMcpConfig(env = process.env) {
|
|
|
105
142
|
requestTimeoutMs,
|
|
106
143
|
brokerTimeoutMs,
|
|
107
144
|
allowedBackendHosts: deriveAllowedHosts(backendBaseUrl),
|
|
108
|
-
jwtCacheTtlSec
|
|
145
|
+
jwtCacheTtlSec,
|
|
146
|
+
bundlerUrl,
|
|
147
|
+
bundlerTimeoutMs,
|
|
148
|
+
chainId,
|
|
149
|
+
subscriptionAddress,
|
|
150
|
+
entryPointAddress
|
|
109
151
|
};
|
|
110
152
|
}
|
|
111
153
|
var PRIVKEY_HEX_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
@@ -140,16 +182,366 @@ function loadBrokerConfig(env = process.env) {
|
|
|
140
182
|
dashboardBaseUrl
|
|
141
183
|
};
|
|
142
184
|
}
|
|
185
|
+
|
|
186
|
+
// src/broker/protocol.ts
|
|
187
|
+
var BROKER_PROTOCOL_VERSION = "0.4.0";
|
|
188
|
+
var HASH_HEX_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
189
|
+
var ADDRESS_HEX_RE2 = /^0x[0-9a-fA-F]{40}$/;
|
|
190
|
+
var SELECTOR_HEX_RE = /^0x[0-9a-fA-F]{8}$/;
|
|
191
|
+
var HEX_PREFIXED_RE = /^0x[0-9a-fA-F]*$/;
|
|
192
|
+
var JWT_RE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
193
|
+
var SESSION_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
194
|
+
var UINT256_DEC_RE = /^(0|[1-9][0-9]{0,77})$/;
|
|
195
|
+
var MAX_CALLDATA_HEX_LEN = 2e5;
|
|
196
|
+
function isHashHex(value) {
|
|
197
|
+
return typeof value === "string" && HASH_HEX_RE.test(value);
|
|
198
|
+
}
|
|
199
|
+
function isAddressHex(value) {
|
|
200
|
+
return typeof value === "string" && ADDRESS_HEX_RE2.test(value);
|
|
201
|
+
}
|
|
202
|
+
function isSelectorHex(value) {
|
|
203
|
+
return typeof value === "string" && SELECTOR_HEX_RE.test(value);
|
|
204
|
+
}
|
|
205
|
+
function isHexPrefixed(value) {
|
|
206
|
+
return typeof value === "string" && HEX_PREFIXED_RE.test(value);
|
|
207
|
+
}
|
|
208
|
+
function isJwtShape(value) {
|
|
209
|
+
return typeof value === "string" && value.length <= 8192 && JWT_RE.test(value);
|
|
210
|
+
}
|
|
211
|
+
function isSessionIdShape(value) {
|
|
212
|
+
return typeof value === "string" && SESSION_ID_RE.test(value);
|
|
213
|
+
}
|
|
214
|
+
function isUint256DecString(value) {
|
|
215
|
+
return typeof value === "string" && UINT256_DEC_RE.test(value);
|
|
216
|
+
}
|
|
217
|
+
var UINT256_MAX = (1n << 256n) - 1n;
|
|
218
|
+
function isUint256InRange(value) {
|
|
219
|
+
if (!isUint256DecString(value)) return false;
|
|
220
|
+
return BigInt(value) <= UINT256_MAX;
|
|
221
|
+
}
|
|
222
|
+
function parseSelectorCap(raw) {
|
|
223
|
+
if (typeof raw !== "object" || raw === null) {
|
|
224
|
+
return { error: "selectorCap must be an object" };
|
|
225
|
+
}
|
|
226
|
+
const obj = raw;
|
|
227
|
+
if (!isSelectorHex(obj.selector)) {
|
|
228
|
+
return { error: "selectorCap.selector must be a 4-byte 0x-hex" };
|
|
229
|
+
}
|
|
230
|
+
const capArgIndex = obj.capArgIndex;
|
|
231
|
+
const maxAmount = obj.maxAmount;
|
|
232
|
+
const indexIsNull = capArgIndex === null;
|
|
233
|
+
const amountIsNull = maxAmount === null;
|
|
234
|
+
if (indexIsNull !== amountIsNull) {
|
|
235
|
+
return {
|
|
236
|
+
error: "selectorCap.capArgIndex and selectorCap.maxAmount must both be null or both non-null"
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
if (!indexIsNull) {
|
|
240
|
+
if (typeof capArgIndex !== "number" || !Number.isInteger(capArgIndex) || capArgIndex < 0 || capArgIndex > 31) {
|
|
241
|
+
return {
|
|
242
|
+
error: "selectorCap.capArgIndex must be an integer in [0, 31] (max 32 ABI words)"
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
if (typeof maxAmount !== "string" || !isUint256InRange(maxAmount)) {
|
|
246
|
+
return { error: "selectorCap.maxAmount must be a uint256 decimal string \u2264 2^256-1" };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
selector: obj.selector.toLowerCase(),
|
|
251
|
+
capArgIndex: indexIsNull ? null : capArgIndex,
|
|
252
|
+
maxAmount: indexIsNull ? null : maxAmount
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function isOptionalHash32(value) {
|
|
256
|
+
return value === void 0 || isHashHex(value);
|
|
257
|
+
}
|
|
258
|
+
function isOptionalPermissionId(value) {
|
|
259
|
+
return value === void 0 || isSelectorHex(value);
|
|
260
|
+
}
|
|
261
|
+
function parsePolicySnapshot(raw) {
|
|
262
|
+
if (typeof raw !== "object" || raw === null) {
|
|
263
|
+
return { error: "snapshot must be a JSON object" };
|
|
264
|
+
}
|
|
265
|
+
const obj = raw;
|
|
266
|
+
if (!isSessionIdShape(obj.sessionId)) {
|
|
267
|
+
return { error: "snapshot.sessionId must be 1-128 chars [A-Za-z0-9_-]" };
|
|
268
|
+
}
|
|
269
|
+
if (obj.mode !== "scoped") {
|
|
270
|
+
return { error: "snapshot.mode must be 'scoped' (wildcard ships in Slice 4)" };
|
|
271
|
+
}
|
|
272
|
+
if (!isAddressHex(obj.signerAddress)) {
|
|
273
|
+
return { error: "snapshot.signerAddress must be a 0x-prefixed 20-byte hex" };
|
|
274
|
+
}
|
|
275
|
+
const targetContracts = obj.targetContracts;
|
|
276
|
+
if (!Array.isArray(targetContracts) || targetContracts.length === 0 || targetContracts.length > 32 || !targetContracts.every(isAddressHex)) {
|
|
277
|
+
return {
|
|
278
|
+
error: "snapshot.targetContracts must be a 1..32-element array of 0x-addresses"
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
const rawCaps = obj.selectorCaps;
|
|
282
|
+
if (!Array.isArray(rawCaps) || rawCaps.length === 0 || rawCaps.length > 32) {
|
|
283
|
+
return { error: "snapshot.selectorCaps must be a 1..32-element array" };
|
|
284
|
+
}
|
|
285
|
+
const parsedCaps = [];
|
|
286
|
+
const seenSelectors = /* @__PURE__ */ new Set();
|
|
287
|
+
for (const c of rawCaps) {
|
|
288
|
+
const parsed = parseSelectorCap(c);
|
|
289
|
+
if ("error" in parsed) return { error: `selectorCaps: ${parsed.error}` };
|
|
290
|
+
if (seenSelectors.has(parsed.selector)) {
|
|
291
|
+
return { error: `selectorCaps: duplicate selector ${parsed.selector}` };
|
|
292
|
+
}
|
|
293
|
+
seenSelectors.add(parsed.selector);
|
|
294
|
+
parsedCaps.push(parsed);
|
|
295
|
+
}
|
|
296
|
+
if (typeof obj.validUntilSec !== "number" || !Number.isFinite(obj.validUntilSec) || obj.validUntilSec <= 0) {
|
|
297
|
+
return { error: "snapshot.validUntilSec must be a positive number" };
|
|
298
|
+
}
|
|
299
|
+
if (typeof obj.mintedAtSec !== "number" || !Number.isFinite(obj.mintedAtSec) || obj.mintedAtSec <= 0) {
|
|
300
|
+
return { error: "snapshot.mintedAtSec must be a positive number" };
|
|
301
|
+
}
|
|
302
|
+
if (!isOptionalHash32(obj.consentActionHash)) {
|
|
303
|
+
return { error: "snapshot.consentActionHash must be a 0x-prefixed 32-byte hex when provided" };
|
|
304
|
+
}
|
|
305
|
+
if (!isOptionalHash32(obj.consentTextSha256)) {
|
|
306
|
+
return { error: "snapshot.consentTextSha256 must be a 0x-prefixed 32-byte hex when provided" };
|
|
307
|
+
}
|
|
308
|
+
if (!isOptionalPermissionId(obj.permissionId)) {
|
|
309
|
+
return { error: "snapshot.permissionId must be a 0x-prefixed 4-byte hex when provided" };
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
sessionId: obj.sessionId,
|
|
313
|
+
mode: "scoped",
|
|
314
|
+
signerAddress: obj.signerAddress.toLowerCase(),
|
|
315
|
+
targetContracts: targetContracts.map(
|
|
316
|
+
(a) => a.toLowerCase()
|
|
317
|
+
),
|
|
318
|
+
selectorCaps: parsedCaps,
|
|
319
|
+
validUntilSec: obj.validUntilSec,
|
|
320
|
+
mintedAtSec: obj.mintedAtSec,
|
|
321
|
+
...obj.consentActionHash === void 0 ? {} : { consentActionHash: obj.consentActionHash.toLowerCase() },
|
|
322
|
+
...obj.consentTextSha256 === void 0 ? {} : { consentTextSha256: obj.consentTextSha256.toLowerCase() },
|
|
323
|
+
...obj.permissionId === void 0 ? {} : { permissionId: obj.permissionId.toLowerCase() }
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function parseBrokerRequest(line) {
|
|
327
|
+
let parsed;
|
|
328
|
+
try {
|
|
329
|
+
parsed = JSON.parse(line);
|
|
330
|
+
} catch {
|
|
331
|
+
return { type: "error", code: "invalid_request", message: "request is not valid JSON" };
|
|
332
|
+
}
|
|
333
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
334
|
+
return { type: "error", code: "invalid_request", message: "request must be a JSON object" };
|
|
335
|
+
}
|
|
336
|
+
const obj = parsed;
|
|
337
|
+
switch (obj.type) {
|
|
338
|
+
case "hello":
|
|
339
|
+
return { type: "hello" };
|
|
340
|
+
case "sign_hash": {
|
|
341
|
+
const hash = obj.hash;
|
|
342
|
+
if (!isHashHex(hash)) {
|
|
343
|
+
return {
|
|
344
|
+
type: "error",
|
|
345
|
+
code: "invalid_request",
|
|
346
|
+
message: "sign_hash.hash must be a 0x-prefixed 32-byte hex string"
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
const intent = obj.intent;
|
|
350
|
+
const intentValid = intent === void 0 || typeof intent === "object" && intent !== null && typeof intent.tool === "string";
|
|
351
|
+
if (!intentValid) {
|
|
352
|
+
return {
|
|
353
|
+
type: "error",
|
|
354
|
+
code: "invalid_request",
|
|
355
|
+
message: "sign_hash.intent.tool must be a string when provided"
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
type: "sign_hash",
|
|
360
|
+
hash,
|
|
361
|
+
...intent === void 0 ? {} : { intent }
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
case "store_jwt": {
|
|
365
|
+
const jwt = obj.jwt;
|
|
366
|
+
if (!isJwtShape(jwt)) {
|
|
367
|
+
return {
|
|
368
|
+
type: "error",
|
|
369
|
+
code: "invalid_request",
|
|
370
|
+
message: "store_jwt.jwt must be a JWT-shaped string \u22648192 chars"
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const expiresAtSec = obj.expiresAtSec;
|
|
374
|
+
const expiresValid = expiresAtSec === void 0 || typeof expiresAtSec === "number" && Number.isFinite(expiresAtSec) && expiresAtSec > 0;
|
|
375
|
+
if (!expiresValid) {
|
|
376
|
+
return {
|
|
377
|
+
type: "error",
|
|
378
|
+
code: "invalid_request",
|
|
379
|
+
message: "store_jwt.expiresAtSec must be a positive number when provided"
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
type: "store_jwt",
|
|
384
|
+
jwt,
|
|
385
|
+
...expiresAtSec === void 0 ? {} : { expiresAtSec }
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
case "get_jwt":
|
|
389
|
+
return { type: "get_jwt" };
|
|
390
|
+
case "clear_jwt":
|
|
391
|
+
return { type: "clear_jwt" };
|
|
392
|
+
case "sign_userop": {
|
|
393
|
+
const sessionId = obj.sessionId;
|
|
394
|
+
if (!isSessionIdShape(sessionId)) {
|
|
395
|
+
return {
|
|
396
|
+
type: "error",
|
|
397
|
+
code: "invalid_request",
|
|
398
|
+
message: "sign_userop.sessionId must be 1-128 chars [A-Za-z0-9_-]"
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
const userOpHash = obj.userOpHash;
|
|
402
|
+
if (!isHashHex(userOpHash)) {
|
|
403
|
+
return {
|
|
404
|
+
type: "error",
|
|
405
|
+
code: "invalid_request",
|
|
406
|
+
message: "sign_userop.userOpHash must be a 0x-prefixed 32-byte hex string"
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
const innerCall = obj.innerCall;
|
|
410
|
+
if (typeof innerCall !== "object" || innerCall === null) {
|
|
411
|
+
return {
|
|
412
|
+
type: "error",
|
|
413
|
+
code: "invalid_request",
|
|
414
|
+
message: "sign_userop.innerCall must be an object with { target, callData }"
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
const ic = innerCall;
|
|
418
|
+
if (!isAddressHex(ic.target)) {
|
|
419
|
+
return {
|
|
420
|
+
type: "error",
|
|
421
|
+
code: "invalid_request",
|
|
422
|
+
message: "sign_userop.innerCall.target must be a 0x-prefixed 20-byte hex"
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
if (!isHexPrefixed(ic.callData) || ic.callData.length < 74 || ic.callData.length % 2 !== 0 || ic.callData.length > MAX_CALLDATA_HEX_LEN) {
|
|
426
|
+
return {
|
|
427
|
+
type: "error",
|
|
428
|
+
code: "invalid_request",
|
|
429
|
+
message: "sign_userop.innerCall.callData must be 0x-prefixed even-length hex \u226574 chars (selector + first uint256 arg) and \u2264200000 chars"
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
const intent = obj.intent;
|
|
433
|
+
let safeIntent;
|
|
434
|
+
if (intent !== void 0) {
|
|
435
|
+
if (typeof intent !== "object" || intent === null) {
|
|
436
|
+
return {
|
|
437
|
+
type: "error",
|
|
438
|
+
code: "invalid_request",
|
|
439
|
+
message: "sign_userop.intent must be an object when provided"
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
const intentObj = intent;
|
|
443
|
+
if (typeof intentObj.tool !== "string" || intentObj.tool.length > 64) {
|
|
444
|
+
return {
|
|
445
|
+
type: "error",
|
|
446
|
+
code: "invalid_request",
|
|
447
|
+
message: "sign_userop.intent.tool must be a string \u226464 chars"
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
if (intentObj.summary !== void 0 && (typeof intentObj.summary !== "string" || intentObj.summary.length > 256)) {
|
|
451
|
+
return {
|
|
452
|
+
type: "error",
|
|
453
|
+
code: "invalid_request",
|
|
454
|
+
message: "sign_userop.intent.summary must be a string \u2264256 chars when provided"
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
safeIntent = {
|
|
458
|
+
tool: intentObj.tool,
|
|
459
|
+
...typeof intentObj.summary === "string" ? { summary: intentObj.summary } : {}
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
type: "sign_userop",
|
|
464
|
+
sessionId,
|
|
465
|
+
userOpHash,
|
|
466
|
+
innerCall: {
|
|
467
|
+
target: ic.target.toLowerCase(),
|
|
468
|
+
callData: ic.callData.toLowerCase()
|
|
469
|
+
},
|
|
470
|
+
...safeIntent === void 0 ? {} : { intent: safeIntent }
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
case "store_policy_snapshot": {
|
|
474
|
+
const parsed2 = parsePolicySnapshot(obj.snapshot);
|
|
475
|
+
if ("error" in parsed2) {
|
|
476
|
+
return { type: "error", code: "invalid_request", message: parsed2.error };
|
|
477
|
+
}
|
|
478
|
+
return { type: "store_policy_snapshot", snapshot: parsed2 };
|
|
479
|
+
}
|
|
480
|
+
case "get_policy_snapshot": {
|
|
481
|
+
if (!isSessionIdShape(obj.sessionId)) {
|
|
482
|
+
return {
|
|
483
|
+
type: "error",
|
|
484
|
+
code: "invalid_request",
|
|
485
|
+
message: "get_policy_snapshot.sessionId must be 1-128 chars [A-Za-z0-9_-]"
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
return { type: "get_policy_snapshot", sessionId: obj.sessionId };
|
|
489
|
+
}
|
|
490
|
+
case "clear_policy_snapshot": {
|
|
491
|
+
if (!isSessionIdShape(obj.sessionId)) {
|
|
492
|
+
return {
|
|
493
|
+
type: "error",
|
|
494
|
+
code: "invalid_request",
|
|
495
|
+
message: "clear_policy_snapshot.sessionId must be 1-128 chars [A-Za-z0-9_-]"
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
return { type: "clear_policy_snapshot", sessionId: obj.sessionId };
|
|
499
|
+
}
|
|
500
|
+
case "get_active_session_id":
|
|
501
|
+
return { type: "get_active_session_id" };
|
|
502
|
+
default:
|
|
503
|
+
return {
|
|
504
|
+
type: "error",
|
|
505
|
+
code: "unsupported_type",
|
|
506
|
+
message: `unsupported request type: ${String(obj.type)}`
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
function serializeResponse(res) {
|
|
511
|
+
return JSON.stringify(res) + "\n";
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/clients/broker-client.ts
|
|
143
515
|
var BrokerClientError = class extends Error {
|
|
144
|
-
constructor(code, message, cause) {
|
|
516
|
+
constructor(code, message, cause, brokerCode) {
|
|
145
517
|
super(message);
|
|
146
518
|
this.code = code;
|
|
147
519
|
this.cause = cause;
|
|
520
|
+
this.brokerCode = brokerCode;
|
|
148
521
|
this.name = "BrokerClientError";
|
|
149
522
|
}
|
|
150
523
|
code;
|
|
151
524
|
cause;
|
|
525
|
+
brokerCode;
|
|
152
526
|
};
|
|
527
|
+
function semverGte(a, b) {
|
|
528
|
+
const re = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
529
|
+
const ma = re.exec(a);
|
|
530
|
+
const mb = re.exec(b);
|
|
531
|
+
if (!ma || !mb) {
|
|
532
|
+
throw new BrokerClientError(
|
|
533
|
+
"protocol_error",
|
|
534
|
+
`semverGte: malformed version (got ${JSON.stringify(a)} vs ${JSON.stringify(b)})`
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
for (let i = 1; i <= 3; i++) {
|
|
538
|
+
const ai = Number(ma[i]);
|
|
539
|
+
const bi = Number(mb[i]);
|
|
540
|
+
if (ai > bi) return true;
|
|
541
|
+
if (ai < bi) return false;
|
|
542
|
+
}
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
153
545
|
var BrokerClient = class {
|
|
154
546
|
constructor(options) {
|
|
155
547
|
this.options = options;
|
|
@@ -209,6 +601,109 @@ var BrokerClient = class {
|
|
|
209
601
|
);
|
|
210
602
|
}
|
|
211
603
|
}
|
|
604
|
+
// ── Wave 5 Path D Slice 1 (Commit 3) — policy snapshot CRUD + sign_userop ──
|
|
605
|
+
async signUserOp(args) {
|
|
606
|
+
const res = await this.exchange({
|
|
607
|
+
type: "sign_userop",
|
|
608
|
+
sessionId: args.sessionId,
|
|
609
|
+
userOpHash: args.userOpHash,
|
|
610
|
+
innerCall: args.innerCall,
|
|
611
|
+
...args.intent ? { intent: args.intent } : {}
|
|
612
|
+
});
|
|
613
|
+
if (res.type !== "sign_userop") {
|
|
614
|
+
throw new BrokerClientError(
|
|
615
|
+
"protocol_error",
|
|
616
|
+
`expected sign_userop response, got ${res.type}`
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
return res;
|
|
620
|
+
}
|
|
621
|
+
async storePolicySnapshot(snapshot) {
|
|
622
|
+
const res = await this.exchange({ type: "store_policy_snapshot", snapshot });
|
|
623
|
+
if (res.type !== "store_policy_snapshot") {
|
|
624
|
+
throw new BrokerClientError(
|
|
625
|
+
"protocol_error",
|
|
626
|
+
`expected store_policy_snapshot response, got ${res.type}`
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
return res;
|
|
630
|
+
}
|
|
631
|
+
async getPolicySnapshot(sessionId) {
|
|
632
|
+
const res = await this.exchange({ type: "get_policy_snapshot", sessionId });
|
|
633
|
+
if (res.type !== "get_policy_snapshot") {
|
|
634
|
+
throw new BrokerClientError(
|
|
635
|
+
"protocol_error",
|
|
636
|
+
`expected get_policy_snapshot response, got ${res.type}`
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
return res;
|
|
640
|
+
}
|
|
641
|
+
async clearPolicySnapshot(sessionId) {
|
|
642
|
+
const res = await this.exchange({ type: "clear_policy_snapshot", sessionId });
|
|
643
|
+
if (res.type !== "clear_policy_snapshot") {
|
|
644
|
+
throw new BrokerClientError(
|
|
645
|
+
"protocol_error",
|
|
646
|
+
`expected clear_policy_snapshot response, got ${res.type}`
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
return res;
|
|
650
|
+
}
|
|
651
|
+
async getActiveSessionId() {
|
|
652
|
+
const res = await this.exchange({ type: "get_active_session_id" });
|
|
653
|
+
if (res.type !== "get_active_session_id") {
|
|
654
|
+
throw new BrokerClientError(
|
|
655
|
+
"protocol_error",
|
|
656
|
+
`expected get_active_session_id response, got ${res.type}`
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
return res;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Detect whether the running daemon speaks Path D (protocol 0.4.0+).
|
|
663
|
+
* Wraps `hello()` with a semver-gte comparison so the MCP tool layer
|
|
664
|
+
* can short-circuit to Path C with a clear `version_too_old` reason
|
|
665
|
+
* instead of surfacing the opaque `unsupported_type` error a stale
|
|
666
|
+
* 0.3.0 daemon would emit on `sign_userop` / `get_active_session_id`
|
|
667
|
+
* (Backend Architect H-2, round 2).
|
|
668
|
+
*
|
|
669
|
+
* Returns `{ supported: false }` on broker connect failure too — the
|
|
670
|
+
* caller treats "daemon down" identically to "version too old": Path D
|
|
671
|
+
* not available, fall through to Path C.
|
|
672
|
+
*/
|
|
673
|
+
async preflight() {
|
|
674
|
+
let hello;
|
|
675
|
+
try {
|
|
676
|
+
hello = await this.hello();
|
|
677
|
+
} catch (err2) {
|
|
678
|
+
return {
|
|
679
|
+
supported: false,
|
|
680
|
+
reason: "broker_unreachable",
|
|
681
|
+
message: err2 instanceof BrokerClientError ? `broker.${err2.code}: ${err2.message}` : err2 instanceof Error ? err2.message : "broker unreachable",
|
|
682
|
+
requiredVersion: BROKER_PROTOCOL_VERSION
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
if (!semverGte(hello.version, BROKER_PROTOCOL_VERSION)) {
|
|
686
|
+
return {
|
|
687
|
+
supported: false,
|
|
688
|
+
reason: "version_too_old",
|
|
689
|
+
daemonVersion: hello.version,
|
|
690
|
+
requiredVersion: BROKER_PROTOCOL_VERSION
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
if (hello.hasSessionKey === false) {
|
|
694
|
+
return {
|
|
695
|
+
supported: false,
|
|
696
|
+
reason: "session_key_unavailable",
|
|
697
|
+
daemonVersion: hello.version,
|
|
698
|
+
requiredVersion: BROKER_PROTOCOL_VERSION
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
return {
|
|
702
|
+
supported: true,
|
|
703
|
+
daemonVersion: hello.version,
|
|
704
|
+
signerAddress: hello.sessionKeyAddress
|
|
705
|
+
};
|
|
706
|
+
}
|
|
212
707
|
exchange(request) {
|
|
213
708
|
return new Promise((resolve, reject) => {
|
|
214
709
|
let socket;
|
|
@@ -249,7 +744,12 @@ var BrokerClient = class {
|
|
|
249
744
|
const parsed = JSON.parse(line);
|
|
250
745
|
if (parsed.type === "error") {
|
|
251
746
|
settleErr(
|
|
252
|
-
new BrokerClientError(
|
|
747
|
+
new BrokerClientError(
|
|
748
|
+
"broker_error",
|
|
749
|
+
`${parsed.code}: ${parsed.message}`,
|
|
750
|
+
void 0,
|
|
751
|
+
parsed.code
|
|
752
|
+
)
|
|
253
753
|
);
|
|
254
754
|
return;
|
|
255
755
|
}
|
|
@@ -377,6 +877,23 @@ var BackendClient = class {
|
|
|
377
877
|
false
|
|
378
878
|
);
|
|
379
879
|
}
|
|
880
|
+
/**
|
|
881
|
+
* GET variant that sends no Authorization header. Use for backend
|
|
882
|
+
* endpoints that are intentionally public (e.g. `/api/v1/tokens`
|
|
883
|
+
* which the marketplace + the 0.2.1 `positionBuy` NAV-conversion
|
|
884
|
+
* both read). Avoids triggering the AUTH_REQUIRED branch for the
|
|
885
|
+
* "not yet logged in" case on read paths that don't need auth.
|
|
886
|
+
*/
|
|
887
|
+
async getUnauth(path, query) {
|
|
888
|
+
const url = this.buildUrl(path, query);
|
|
889
|
+
return this.exchange(
|
|
890
|
+
"GET",
|
|
891
|
+
url,
|
|
892
|
+
void 0,
|
|
893
|
+
/* withAuth */
|
|
894
|
+
false
|
|
895
|
+
);
|
|
896
|
+
}
|
|
380
897
|
buildUrl(path, query) {
|
|
381
898
|
if (!path.startsWith("/")) {
|
|
382
899
|
throw new BackendError("bad_request", `path must start with "/": ${path}`);
|
|
@@ -469,12 +986,413 @@ function mapStatus(status) {
|
|
|
469
986
|
if (status >= 500) return "server_error";
|
|
470
987
|
return "invalid_response";
|
|
471
988
|
}
|
|
472
|
-
var
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
989
|
+
var ENTRY_POINT_GET_NONCE_ABI = viem.parseAbi([
|
|
990
|
+
"function getNonce(address sender, uint192 key) view returns (uint256)"
|
|
991
|
+
]);
|
|
992
|
+
var BundlerClientError = class extends Error {
|
|
993
|
+
constructor(code, message, detail) {
|
|
994
|
+
super(message);
|
|
995
|
+
this.code = code;
|
|
996
|
+
this.detail = detail;
|
|
997
|
+
this.name = "BundlerClientError";
|
|
998
|
+
}
|
|
999
|
+
code;
|
|
1000
|
+
detail;
|
|
1001
|
+
};
|
|
1002
|
+
var BundlerClient = class {
|
|
1003
|
+
constructor(options) {
|
|
1004
|
+
this.options = options;
|
|
1005
|
+
this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
1006
|
+
}
|
|
1007
|
+
options;
|
|
1008
|
+
fetchImpl;
|
|
1009
|
+
nextRpcId = 1;
|
|
1010
|
+
/**
|
|
1011
|
+
* Submit a signed UserOperation. Returns the userOpHash the bundler
|
|
1012
|
+
* computed (which must match the hash the broker signed — caller is
|
|
1013
|
+
* responsible for the consistency check; broker policy snapshot
|
|
1014
|
+
* captures the signer-binding piece).
|
|
1015
|
+
*/
|
|
1016
|
+
async sendUserOp(userOp, entryPoint) {
|
|
1017
|
+
const result = await this.rpc("eth_sendUserOperation", [userOp, entryPoint]);
|
|
1018
|
+
if (typeof result !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(result)) {
|
|
1019
|
+
throw new BundlerClientError(
|
|
1020
|
+
"invalid_response",
|
|
1021
|
+
`eth_sendUserOperation returned non-hash result: ${JSON.stringify(result).slice(0, 80)}`
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
return result.toLowerCase();
|
|
1025
|
+
}
|
|
1026
|
+
/** Return the receipt for a userOpHash, or null when the UserOp has
|
|
1027
|
+
* not yet been bundled. */
|
|
1028
|
+
async getReceipt(userOpHash) {
|
|
1029
|
+
const result = await this.rpc("eth_getUserOperationReceipt", [userOpHash]);
|
|
1030
|
+
if (result === null || result === void 0) return null;
|
|
1031
|
+
return parseReceipt(result);
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Poll until the bundler returns a receipt, or `timeoutMs` elapses.
|
|
1035
|
+
* Caller decides retry / fallback behaviour on `receipt_timeout`.
|
|
1036
|
+
*
|
|
1037
|
+
* Poll interval grows linearly from `initialIntervalMs` to
|
|
1038
|
+
* `maxIntervalMs` to avoid burning the bundler quota when blocks are
|
|
1039
|
+
* slow. Default tuning: 500ms → 2000ms over the first 6 polls; then
|
|
1040
|
+
* pinned at 2000ms.
|
|
1041
|
+
*/
|
|
1042
|
+
async waitForReceipt(userOpHash, opts) {
|
|
1043
|
+
const clock = opts.clockMs ?? (() => Date.now());
|
|
1044
|
+
const sleep = opts.sleep ?? ((ms) => promises.setTimeout(ms));
|
|
1045
|
+
const initial = opts.initialIntervalMs ?? 500;
|
|
1046
|
+
const max = opts.maxIntervalMs ?? 2e3;
|
|
1047
|
+
const deadline = clock() + opts.timeoutMs;
|
|
1048
|
+
let attempt = 0;
|
|
1049
|
+
while (true) {
|
|
1050
|
+
const receipt = await this.getReceipt(userOpHash);
|
|
1051
|
+
if (receipt) return receipt;
|
|
1052
|
+
const now = clock();
|
|
1053
|
+
if (now >= deadline) {
|
|
1054
|
+
throw new BundlerClientError(
|
|
1055
|
+
"receipt_timeout",
|
|
1056
|
+
`no receipt for userOp ${userOpHash} within ${opts.timeoutMs}ms`
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
const interval = Math.min(max, initial + attempt * 250);
|
|
1060
|
+
const remaining = Math.max(0, deadline - now);
|
|
1061
|
+
await sleep(Math.min(interval, remaining));
|
|
1062
|
+
attempt++;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Wave 5 Path D Slice 1 Commit 3.5 — `pm_sponsorUserOperation`.
|
|
1067
|
+
* ZeroDev's bundler URL serves both bundler RPCs AND paymaster RPCs
|
|
1068
|
+
* at the same endpoint, so we don't need a separate paymaster URL.
|
|
1069
|
+
* Returns the paymaster fields + the gas limits the paymaster's
|
|
1070
|
+
* simulation computed (the caller doesn't need a separate
|
|
1071
|
+
* `estimateUserOpGas` round-trip on the happy path).
|
|
1072
|
+
*/
|
|
1073
|
+
async sponsorUserOp(userOp, entryPoint) {
|
|
1074
|
+
const result = await this.rpc("pm_sponsorUserOperation", [userOp, entryPoint]);
|
|
1075
|
+
return parseSponsoredFields(result);
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Wave 5 Path D Slice 1 Commit 3.5 — `eth_estimateUserOperationGas`.
|
|
1079
|
+
* Not used in the happy path (sponsorship returns gas), but lives as
|
|
1080
|
+
* a fallback for unsponsored flows OR if the operator's paymaster
|
|
1081
|
+
* goes down. Reading gas separately also makes the failure modes
|
|
1082
|
+
* distinguishable for the LLM-facing fallback reasons.
|
|
1083
|
+
*/
|
|
1084
|
+
async estimateUserOpGas(userOp, entryPoint) {
|
|
1085
|
+
const result = await this.rpc("eth_estimateUserOperationGas", [userOp, entryPoint]);
|
|
1086
|
+
if (typeof result !== "object" || result === null) {
|
|
1087
|
+
throw new BundlerClientError(
|
|
1088
|
+
"invalid_response",
|
|
1089
|
+
"eth_estimateUserOperationGas returned non-object"
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
const obj = result;
|
|
1093
|
+
return {
|
|
1094
|
+
callGasLimit: assertHex(obj.callGasLimit, "estimateUserOpGas.callGasLimit"),
|
|
1095
|
+
verificationGasLimit: assertHex(
|
|
1096
|
+
obj.verificationGasLimit,
|
|
1097
|
+
"estimateUserOpGas.verificationGasLimit"
|
|
1098
|
+
),
|
|
1099
|
+
preVerificationGas: assertHex(
|
|
1100
|
+
obj.preVerificationGas,
|
|
1101
|
+
"estimateUserOpGas.preVerificationGas"
|
|
1102
|
+
)
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Wave 5 Path D Slice 1 Commit 3.5 — `eth_call` against the
|
|
1107
|
+
* EntryPoint's `getNonce(sender, key)`. Uses the bundler URL as a
|
|
1108
|
+
* full Arb Sepolia node (ZeroDev's bundler accepts read-side RPCs).
|
|
1109
|
+
*
|
|
1110
|
+
* Pass `key = 0n` for the default nonce key — Path D never uses a
|
|
1111
|
+
* non-default key in Slice 1; reserved for batched UserOps in
|
|
1112
|
+
* later slices.
|
|
1113
|
+
*/
|
|
1114
|
+
async getNonce(sender, entryPoint, key = 0n) {
|
|
1115
|
+
const data = viem.encodeFunctionData({
|
|
1116
|
+
abi: ENTRY_POINT_GET_NONCE_ABI,
|
|
1117
|
+
functionName: "getNonce",
|
|
1118
|
+
args: [sender, key]
|
|
1119
|
+
});
|
|
1120
|
+
const result = await this.rpc("eth_call", [
|
|
1121
|
+
{ to: entryPoint, data },
|
|
1122
|
+
"latest"
|
|
1123
|
+
]);
|
|
1124
|
+
if (typeof result !== "string" || !/^0x[0-9a-fA-F]*$/.test(result)) {
|
|
1125
|
+
throw new BundlerClientError(
|
|
1126
|
+
"invalid_response",
|
|
1127
|
+
`eth_call returned non-hex: ${JSON.stringify(result).slice(0, 80)}`
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
const [nonce] = viem.decodeAbiParameters([{ type: "uint256" }], result);
|
|
1131
|
+
return nonce;
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Wave 5 Path D Slice 1 Commit 3.5 — fetch the fee market via
|
|
1135
|
+
* `eth_gasPrice` (returns a single value the bundler will accept for
|
|
1136
|
+
* both maxFee + maxPriorityFee on Arb Sepolia, which has effectively
|
|
1137
|
+
* no priority-vs-base distinction).
|
|
1138
|
+
*
|
|
1139
|
+
* Simple-on-purpose: a full EIP-1559 fee market read would need two
|
|
1140
|
+
* RPCs (`eth_maxPriorityFeePerGas` + `eth_getBlock`); Arb Sepolia's
|
|
1141
|
+
* fee dynamics don't require that precision and the paymaster pays
|
|
1142
|
+
* either way. A future caller wanting EIP-1559 precision can add a
|
|
1143
|
+
* sibling method.
|
|
1144
|
+
*/
|
|
1145
|
+
async getFeeData() {
|
|
1146
|
+
const result = await this.rpc("eth_gasPrice", []);
|
|
1147
|
+
if (typeof result !== "string" || !/^0x[0-9a-fA-F]+$/.test(result)) {
|
|
1148
|
+
throw new BundlerClientError(
|
|
1149
|
+
"invalid_response",
|
|
1150
|
+
`eth_gasPrice returned non-hex: ${JSON.stringify(result).slice(0, 80)}`
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
const base = BigInt(result);
|
|
1154
|
+
const margined = base * 2n;
|
|
1155
|
+
const hex = `0x${margined.toString(16)}`;
|
|
1156
|
+
return { maxFeePerGas: hex, maxPriorityFeePerGas: hex };
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Verify the bundler's reported chainId matches `expectedChainId`. Cheap
|
|
1160
|
+
* to call once at MCP server boot (or lazily before the first send) so
|
|
1161
|
+
* a misconfigured bundler URL surfaces as `chain_mismatch` before any
|
|
1162
|
+
* user-facing send rather than after a guaranteed-failing submit.
|
|
1163
|
+
*
|
|
1164
|
+
* Throws `BundlerClientError(config)` if no `expectedChainId` is set —
|
|
1165
|
+
* caller asked for an assert without configuring the expectation.
|
|
1166
|
+
*/
|
|
1167
|
+
async assertChainId() {
|
|
1168
|
+
if (this.options.expectedChainId === void 0) {
|
|
1169
|
+
throw new BundlerClientError(
|
|
1170
|
+
"config",
|
|
1171
|
+
"assertChainId called without expectedChainId configured"
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
const result = await this.rpc("eth_chainId", []);
|
|
1175
|
+
if (typeof result !== "string" || !/^0x[0-9a-fA-F]+$/.test(result)) {
|
|
1176
|
+
throw new BundlerClientError(
|
|
1177
|
+
"invalid_response",
|
|
1178
|
+
`eth_chainId returned non-hex result: ${JSON.stringify(result).slice(0, 80)}`
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
const reported = Number.parseInt(result, 16);
|
|
1182
|
+
if (reported !== this.options.expectedChainId) {
|
|
1183
|
+
throw new BundlerClientError(
|
|
1184
|
+
"chain_mismatch",
|
|
1185
|
+
`bundler reports chainId ${reported}, MCP expected ${this.options.expectedChainId}`,
|
|
1186
|
+
{ reportedChainId: reported, expectedChainId: this.options.expectedChainId }
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
async rpc(method, params) {
|
|
1191
|
+
const id = this.nextRpcId++;
|
|
1192
|
+
const body = JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
1193
|
+
const ctrl = new AbortController();
|
|
1194
|
+
const timer = setTimeout(() => ctrl.abort(), this.options.requestTimeoutMs);
|
|
1195
|
+
let res;
|
|
1196
|
+
try {
|
|
1197
|
+
res = await this.fetchImpl(this.options.endpoint, {
|
|
1198
|
+
method: "POST",
|
|
1199
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
1200
|
+
body,
|
|
1201
|
+
signal: ctrl.signal
|
|
1202
|
+
});
|
|
1203
|
+
} catch (err2) {
|
|
1204
|
+
clearTimeout(timer);
|
|
1205
|
+
if (err2.name === "AbortError") {
|
|
1206
|
+
throw new BundlerClientError("timeout", `bundler ${method} timed out`);
|
|
1207
|
+
}
|
|
1208
|
+
throw new BundlerClientError(
|
|
1209
|
+
"network",
|
|
1210
|
+
`bundler ${method} network error: ${err2 instanceof Error ? err2.message : String(err2)}`,
|
|
1211
|
+
err2
|
|
1212
|
+
);
|
|
1213
|
+
} finally {
|
|
1214
|
+
clearTimeout(timer);
|
|
1215
|
+
}
|
|
1216
|
+
if (!res.ok) {
|
|
1217
|
+
let text = "";
|
|
1218
|
+
try {
|
|
1219
|
+
text = (await res.text()).slice(0, 256);
|
|
1220
|
+
} catch {
|
|
1221
|
+
}
|
|
1222
|
+
throw new BundlerClientError(
|
|
1223
|
+
"http_error",
|
|
1224
|
+
`bundler ${method} \u2192 HTTP ${res.status}: ${text}`
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
let parsed;
|
|
1228
|
+
try {
|
|
1229
|
+
parsed = await res.json();
|
|
1230
|
+
} catch (err2) {
|
|
1231
|
+
throw new BundlerClientError(
|
|
1232
|
+
"invalid_response",
|
|
1233
|
+
`bundler ${method} returned non-JSON: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
1237
|
+
throw new BundlerClientError(
|
|
1238
|
+
"invalid_response",
|
|
1239
|
+
`bundler ${method} returned non-object`
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
const obj = parsed;
|
|
1243
|
+
if (obj.error !== void 0 && obj.error !== null) {
|
|
1244
|
+
const err2 = obj.error;
|
|
1245
|
+
throw new BundlerClientError(
|
|
1246
|
+
"rpc_error",
|
|
1247
|
+
`bundler ${method} rpc error: ${typeof err2.message === "string" ? err2.message : "<no message>"}`,
|
|
1248
|
+
{ code: err2.code, message: err2.message, data: err2.data }
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
return obj.result;
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
function parseReceipt(raw) {
|
|
1255
|
+
if (typeof raw !== "object" || raw === null) {
|
|
1256
|
+
throw new BundlerClientError("invalid_response", "receipt is not an object");
|
|
1257
|
+
}
|
|
1258
|
+
const obj = raw;
|
|
1259
|
+
const userOpHash = obj.userOpHash;
|
|
1260
|
+
if (typeof userOpHash !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(userOpHash)) {
|
|
1261
|
+
throw new BundlerClientError("invalid_response", "receipt.userOpHash malformed");
|
|
1262
|
+
}
|
|
1263
|
+
const sender = obj.sender;
|
|
1264
|
+
if (typeof sender !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(sender)) {
|
|
1265
|
+
throw new BundlerClientError("invalid_response", "receipt.sender malformed");
|
|
1266
|
+
}
|
|
1267
|
+
if (typeof obj.success !== "boolean") {
|
|
1268
|
+
throw new BundlerClientError("invalid_response", "receipt.success must be a boolean");
|
|
1269
|
+
}
|
|
1270
|
+
const inner = obj.receipt;
|
|
1271
|
+
if (typeof inner !== "object" || inner === null) {
|
|
1272
|
+
throw new BundlerClientError("invalid_response", "receipt.receipt missing");
|
|
1273
|
+
}
|
|
1274
|
+
const innerObj = inner;
|
|
1275
|
+
const txHash = innerObj.transactionHash;
|
|
1276
|
+
if (typeof txHash !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(txHash)) {
|
|
1277
|
+
throw new BundlerClientError(
|
|
1278
|
+
"invalid_response",
|
|
1279
|
+
"receipt.receipt.transactionHash malformed"
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
const blockNumber = innerObj.blockNumber;
|
|
1283
|
+
if (typeof blockNumber !== "string" || !/^0x[0-9a-fA-F]+$/.test(blockNumber)) {
|
|
1284
|
+
throw new BundlerClientError(
|
|
1285
|
+
"invalid_response",
|
|
1286
|
+
"receipt.receipt.blockNumber malformed"
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
const blockHash = innerObj.blockHash;
|
|
1290
|
+
if (typeof blockHash !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(blockHash)) {
|
|
1291
|
+
throw new BundlerClientError(
|
|
1292
|
+
"invalid_response",
|
|
1293
|
+
"receipt.receipt.blockHash malformed"
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
return {
|
|
1297
|
+
userOpHash: userOpHash.toLowerCase(),
|
|
1298
|
+
sender: sender.toLowerCase(),
|
|
1299
|
+
success: obj.success,
|
|
1300
|
+
...typeof obj.reason === "string" ? { reason: obj.reason } : {},
|
|
1301
|
+
receipt: {
|
|
1302
|
+
transactionHash: txHash.toLowerCase(),
|
|
1303
|
+
blockNumber: blockNumber.toLowerCase(),
|
|
1304
|
+
blockHash: blockHash.toLowerCase()
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
function parseSponsoredFields(raw) {
|
|
1309
|
+
if (typeof raw !== "object" || raw === null) {
|
|
1310
|
+
throw new BundlerClientError(
|
|
1311
|
+
"invalid_response",
|
|
1312
|
+
"pm_sponsorUserOperation returned non-object"
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
const obj = raw;
|
|
1316
|
+
return {
|
|
1317
|
+
paymaster: assertHexAddress(obj.paymaster, "sponsoredFields.paymaster"),
|
|
1318
|
+
paymasterVerificationGasLimit: assertHexNonZero(
|
|
1319
|
+
obj.paymasterVerificationGasLimit,
|
|
1320
|
+
"sponsoredFields.paymasterVerificationGasLimit",
|
|
1321
|
+
MAX_PAYMASTER_GAS_LIMIT
|
|
1322
|
+
),
|
|
1323
|
+
paymasterPostOpGasLimit: assertHexNonZero(
|
|
1324
|
+
obj.paymasterPostOpGasLimit,
|
|
1325
|
+
"sponsoredFields.paymasterPostOpGasLimit",
|
|
1326
|
+
MAX_PAYMASTER_GAS_LIMIT
|
|
1327
|
+
),
|
|
1328
|
+
// paymasterData is the only sponsored field that can legitimately
|
|
1329
|
+
// be empty (`0x`) — paymasters with no per-op data return that.
|
|
1330
|
+
paymasterData: assertHex(obj.paymasterData, "sponsoredFields.paymasterData"),
|
|
1331
|
+
callGasLimit: assertHexNonZero(
|
|
1332
|
+
obj.callGasLimit,
|
|
1333
|
+
"sponsoredFields.callGasLimit",
|
|
1334
|
+
MAX_CALL_GAS_LIMIT
|
|
1335
|
+
),
|
|
1336
|
+
verificationGasLimit: assertHexNonZero(
|
|
1337
|
+
obj.verificationGasLimit,
|
|
1338
|
+
"sponsoredFields.verificationGasLimit",
|
|
1339
|
+
MAX_VERIFICATION_GAS_LIMIT
|
|
1340
|
+
),
|
|
1341
|
+
preVerificationGas: assertHexNonZero(
|
|
1342
|
+
obj.preVerificationGas,
|
|
1343
|
+
"sponsoredFields.preVerificationGas",
|
|
1344
|
+
MAX_PRE_VERIFICATION_GAS
|
|
1345
|
+
)
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
function assertHex(value, label) {
|
|
1349
|
+
if (typeof value !== "string" || !/^0x([0-9a-fA-F]{2})*$/.test(value)) {
|
|
1350
|
+
const repr = value === void 0 ? "undefined" : JSON.stringify(value);
|
|
1351
|
+
const safe = typeof repr === "string" ? repr.slice(0, 80) : "unknown";
|
|
1352
|
+
throw new BundlerClientError(
|
|
1353
|
+
"invalid_response",
|
|
1354
|
+
`${label} must be a 0x-prefixed hex string (got ${safe})`
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
return value;
|
|
1358
|
+
}
|
|
1359
|
+
function assertHexNonZero(value, label, maxValue) {
|
|
1360
|
+
const hex = assertHex(value, label);
|
|
1361
|
+
if (hex.length === 2 || BigInt(hex) === 0n) {
|
|
1362
|
+
throw new BundlerClientError(
|
|
1363
|
+
"invalid_response",
|
|
1364
|
+
`${label} must be a non-zero hex value (got "${hex}")`
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
if (maxValue !== void 0 && BigInt(hex) > maxValue) {
|
|
1368
|
+
throw new BundlerClientError(
|
|
1369
|
+
"invalid_response",
|
|
1370
|
+
`${label} = ${BigInt(hex)} exceeds plausible ceiling ${maxValue} \u2014 refusing to sign + submit`
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
return hex;
|
|
1374
|
+
}
|
|
1375
|
+
var MAX_CALL_GAS_LIMIT = 20000000n;
|
|
1376
|
+
var MAX_VERIFICATION_GAS_LIMIT = 20000000n;
|
|
1377
|
+
var MAX_PRE_VERIFICATION_GAS = 5000000n;
|
|
1378
|
+
var MAX_PAYMASTER_GAS_LIMIT = 5000000n;
|
|
1379
|
+
function assertHexAddress(value, label) {
|
|
1380
|
+
if (typeof value !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(value)) {
|
|
1381
|
+
const repr = value === void 0 ? "undefined" : JSON.stringify(value);
|
|
1382
|
+
const safe = typeof repr === "string" ? repr.slice(0, 80) : "unknown";
|
|
1383
|
+
throw new BundlerClientError(
|
|
1384
|
+
"invalid_response",
|
|
1385
|
+
`${label} must be a 0x-prefixed 20-byte hex address (got ${safe})`
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
return value;
|
|
1389
|
+
}
|
|
1390
|
+
var TOOL_DESCRIPTORS = [
|
|
1391
|
+
{
|
|
1392
|
+
name: "muhaven.read.portfolio",
|
|
1393
|
+
group: "read",
|
|
1394
|
+
description: "Return the authenticated investor's encrypted-balance portfolio summary. Output exposes only public aggregates (token list, ebool isOverexposed / isUnderYield handles); decrypted balances are NEVER included in the response. The LLM should call muhaven.read.portfolio for fact-checks about the user's state, not estimate from chat history.",
|
|
1395
|
+
sensitive: false
|
|
478
1396
|
},
|
|
479
1397
|
{
|
|
480
1398
|
name: "muhaven.read.yields",
|
|
@@ -500,16 +1418,22 @@ var TOOL_DESCRIPTORS = [
|
|
|
500
1418
|
description: `Return the authenticated user's tiered-autonomy audit log entries. Cursor-paginated. Useful for forensic review ("why was I paused?") and grant-reviewer demos. Read-only \u2014 never exposes other users' data.`,
|
|
501
1419
|
sensitive: false
|
|
502
1420
|
},
|
|
1421
|
+
{
|
|
1422
|
+
name: "muhaven.read.activity",
|
|
1423
|
+
group: "read",
|
|
1424
|
+
description: "Return the authenticated investor's on-chain activity feed (buys / sells / wraps / unwraps / yield claims / transfers). Each row carries token address, tx hash, block timestamp, and event type \u2014 but NEVER cleartext amounts (encrypted handles only, decryptable client-side via permit). USE THIS to verify a Path C dashboard action settled: after position.buy / position.sell / cash.wrap, the user opens the deep-link, taps Authorize, the on-chain tx lands \u2192 a new row appears here. Far more reliable than re-calling read.portfolio (which only changes shape when a NEW token enters the catalog).",
|
|
1425
|
+
sensitive: false
|
|
1426
|
+
},
|
|
503
1427
|
{
|
|
504
1428
|
name: "muhaven.position.buy",
|
|
505
1429
|
group: "position",
|
|
506
|
-
description: 'Prepare a Subscription buy. Returns a dashboard deep-link URL (muhaven.app/trade?mode=buy&...) the user opens to review the pre-filled form, then taps Authorize. The user\'s passkey + ZeroDev kernel sign on the dashboard \u2014 this MCP tool never holds or submits a signing key. Use after the user names a clear amount + token (e.g. "Buy 5 mhUSDC of TBILL1" \u2192 `amountUsdc: "5"`). Token accepts either a symbol ("TBILL1") or 0x-address. The `amountUsdc` field is HUMAN-DECIMAL mhUSDC ("5" = 5 mhUSDC, "0.5" = half a mhUSDC) \u2014 NOT base-6 integer. Max 6 fractional digits. Settlement is NOT observable from MCP \u2014 verify by calling muhaven.read.
|
|
1430
|
+
description: 'Prepare a Subscription buy. Returns a dashboard deep-link URL (muhaven.app/trade?mode=buy&...) the user opens to review the pre-filled form, then taps Authorize. The user\'s passkey + ZeroDev kernel sign on the dashboard \u2014 this MCP tool never holds or submits a signing key. Use after the user names a clear amount + token (e.g. "Buy 5 mhUSDC of TBILL1" \u2192 `amountUsdc: "5"`). Token accepts either a symbol ("TBILL1") or 0x-address. The `amountUsdc` field is HUMAN-DECIMAL mhUSDC ("5" = 5 mhUSDC, "0.5" = half a mhUSDC) \u2014 NOT base-6 integer. Max 6 fractional digits. The tool fetches the current on-chain NAV for the token and converts the notional to integer shares (floor) before building the URL \u2014 so "Buy 3 mhUSDC of GOLD1" at NAV $0.01 becomes "Buy 300 GOLD1 shares (~3 mhUSDC)". Refuses with `amount_too_small_for_share` when the notional won\'t buy at least 1 share at current NAV; the error message tells the user the minimum mhUSDC needed. Settlement is NOT observable from MCP \u2014 verify by calling muhaven.read.activity after the user confirms done (a new "buy" row with the tx hash will appear).',
|
|
507
1431
|
sensitive: true
|
|
508
1432
|
},
|
|
509
1433
|
{
|
|
510
1434
|
name: "muhaven.position.sell",
|
|
511
1435
|
group: "position",
|
|
512
|
-
description:
|
|
1436
|
+
description: 'Prepare a Subscription sell. Returns a dashboard deep-link URL (muhaven.app/trade?mode=sell&...) with the form pre-filled. Same passkey + verify-after pattern as muhaven.position.buy. Input is amountShares (raw POSITIVE INTEGER share count, NOT mhUSDC notional) \u2014 fhERC-20 shares have no decimals so fractional inputs are rejected. Verify settlement by calling muhaven.read.activity (look for a "sell" or "sell-queued" row with the tx hash).',
|
|
513
1437
|
sensitive: true
|
|
514
1438
|
},
|
|
515
1439
|
{
|
|
@@ -528,7 +1452,7 @@ var TOOL_DESCRIPTORS = [
|
|
|
528
1452
|
{
|
|
529
1453
|
name: "muhaven.cash.wrap",
|
|
530
1454
|
group: "cash",
|
|
531
|
-
description: 'Prepare a USDC \u2192 mhUSDC wrap (the encrypted-balance conversion that funds buys). Returns a dashboard deep-link URL (muhaven.app/cash?action=wrap&...) with the amount pre-filled. Input amountUsdc is human-readable USDC ("100" = $100). Common LLM chain: read.portfolio \u2192 notice 0 mhUSDC \u2192 cash.wrap \u2192 then position.buy (each is its own user-confirmed deep-link).
|
|
1455
|
+
description: 'Prepare a USDC \u2192 mhUSDC wrap (the encrypted-balance conversion that funds buys). Returns a dashboard deep-link URL (muhaven.app/cash?action=wrap&...) with the amount pre-filled. Input amountUsdc is human-readable USDC ("100" = $100). Common LLM chain: read.portfolio \u2192 notice 0 mhUSDC \u2192 cash.wrap \u2192 then position.buy (each is its own user-confirmed deep-link). Verify settlement by calling muhaven.read.activity (a new "wrap" row will appear with the tx hash).',
|
|
532
1456
|
sensitive: true
|
|
533
1457
|
},
|
|
534
1458
|
{
|
|
@@ -696,6 +1620,10 @@ var ReadAuditInputSchema = zod.z.object({
|
|
|
696
1620
|
cursor: zod.z.string().min(1).max(512).optional(),
|
|
697
1621
|
limit: zod.z.number().int().min(1).max(200).optional()
|
|
698
1622
|
}).strict();
|
|
1623
|
+
var ReadActivityInputSchema = zod.z.object({
|
|
1624
|
+
limit: zod.z.number().int().min(1).max(50).optional(),
|
|
1625
|
+
offset: zod.z.number().int().min(0).max(1e3).optional()
|
|
1626
|
+
}).strict();
|
|
699
1627
|
var decimalUsdcAmountSchema = zod.z.string().regex(
|
|
700
1628
|
/^(0|[1-9]\d*)(\.\d{1,6})?$/,
|
|
701
1629
|
'must be a positive decimal mhUSDC amount with at most 6 fractional digits (e.g. "5", "0.5", "1234.567")'
|
|
@@ -806,6 +1734,119 @@ var GovernanceCastVoteInputSchema = zod.z.object({
|
|
|
806
1734
|
voteYes: zod.z.boolean()
|
|
807
1735
|
}).strict();
|
|
808
1736
|
|
|
1737
|
+
// src/auth/jwt-decode.ts
|
|
1738
|
+
var JwtDecodeError = class extends Error {
|
|
1739
|
+
code;
|
|
1740
|
+
constructor(code, message) {
|
|
1741
|
+
super(message);
|
|
1742
|
+
this.name = "JwtDecodeError";
|
|
1743
|
+
this.code = code;
|
|
1744
|
+
}
|
|
1745
|
+
};
|
|
1746
|
+
function decodeJwtPayload(jwt) {
|
|
1747
|
+
const segments = jwt.split(".");
|
|
1748
|
+
if (segments.length !== 3) {
|
|
1749
|
+
throw new JwtDecodeError(
|
|
1750
|
+
"malformed_segments",
|
|
1751
|
+
`expected 3 dot-separated segments, got ${segments.length}`
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
const payloadSegment = segments[1];
|
|
1755
|
+
if (payloadSegment === void 0) {
|
|
1756
|
+
throw new JwtDecodeError("malformed_segments", "payload segment missing");
|
|
1757
|
+
}
|
|
1758
|
+
let payloadJson;
|
|
1759
|
+
try {
|
|
1760
|
+
payloadJson = Buffer.from(payloadSegment, "base64url").toString("utf8");
|
|
1761
|
+
} catch (err2) {
|
|
1762
|
+
throw new JwtDecodeError(
|
|
1763
|
+
"malformed_base64",
|
|
1764
|
+
`payload segment is not valid base64url: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1767
|
+
let parsed;
|
|
1768
|
+
try {
|
|
1769
|
+
parsed = JSON.parse(payloadJson);
|
|
1770
|
+
} catch (err2) {
|
|
1771
|
+
throw new JwtDecodeError(
|
|
1772
|
+
"malformed_json",
|
|
1773
|
+
`payload is not valid JSON: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
1774
|
+
);
|
|
1775
|
+
}
|
|
1776
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
1777
|
+
throw new JwtDecodeError(
|
|
1778
|
+
"malformed_json",
|
|
1779
|
+
`payload is not a JSON object (got ${parsed === null ? "null" : typeof parsed})`
|
|
1780
|
+
);
|
|
1781
|
+
}
|
|
1782
|
+
const obj = parsed;
|
|
1783
|
+
return {
|
|
1784
|
+
sub: typeof obj.sub === "string" ? obj.sub : null,
|
|
1785
|
+
scope: Array.isArray(obj.scope) ? obj.scope.filter((s) => typeof s === "string") : [],
|
|
1786
|
+
expSec: typeof obj.exp === "number" ? obj.exp : null,
|
|
1787
|
+
iss: typeof obj.iss === "string" ? obj.iss : null
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
function truncateSubject(sub) {
|
|
1791
|
+
if (!sub) return "(missing)";
|
|
1792
|
+
const sanitized = sub.replace(/[^\x20-\x7e]/g, "?");
|
|
1793
|
+
if (sanitized.length <= 12) return sanitized;
|
|
1794
|
+
return `${sanitized.slice(0, 8)}\u2026${sanitized.slice(-4)}`;
|
|
1795
|
+
}
|
|
1796
|
+
var KERNEL_EXECUTE_ABI = viem.parseAbi([
|
|
1797
|
+
"function execute(bytes32 mode, bytes calldata executionCalldata)"
|
|
1798
|
+
]);
|
|
1799
|
+
var KERNEL_V3_SINGLE_CALL_MODE_DEFAULT = `0x${"00".repeat(32)}`;
|
|
1800
|
+
function encodeKernelExecuteSingleCall(input) {
|
|
1801
|
+
const executionCalldata = viem.encodePacked(
|
|
1802
|
+
["address", "uint256", "bytes"],
|
|
1803
|
+
[input.target, input.value, input.callData]
|
|
1804
|
+
);
|
|
1805
|
+
return viem.encodeFunctionData({
|
|
1806
|
+
abi: KERNEL_EXECUTE_ABI,
|
|
1807
|
+
functionName: "execute",
|
|
1808
|
+
args: [KERNEL_V3_SINGLE_CALL_MODE_DEFAULT, executionCalldata]
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
var ECDSA_SIG_HEX_RE = /^0x[0-9a-fA-F]{130}$/;
|
|
1812
|
+
var PERMISSION_USE_PREFIX = "0xff";
|
|
1813
|
+
function buildKernelSessionKeySignature(input) {
|
|
1814
|
+
if (!ECDSA_SIG_HEX_RE.test(input.ecdsaSignature)) {
|
|
1815
|
+
throw new Error(
|
|
1816
|
+
`buildKernelSessionKeySignature: ecdsaSignature must be a 0x-prefixed 65-byte hex (got ${input.ecdsaSignature.length} chars)`
|
|
1817
|
+
);
|
|
1818
|
+
}
|
|
1819
|
+
return viem.concatHex([PERMISSION_USE_PREFIX, input.ecdsaSignature]);
|
|
1820
|
+
}
|
|
1821
|
+
var VALIDATOR_MODE_DEFAULT = "0x00";
|
|
1822
|
+
var VALIDATOR_TYPE_PERMISSION = "0x02";
|
|
1823
|
+
var PERMISSION_ID_HEX_RE = /^0x[0-9a-fA-F]{8}$/;
|
|
1824
|
+
function composeKernelV3NonceKey(args) {
|
|
1825
|
+
if (!PERMISSION_ID_HEX_RE.test(args.permissionId)) {
|
|
1826
|
+
throw new Error(
|
|
1827
|
+
`composeKernelV3NonceKey: permissionId must be a 0x-prefixed 4-byte hex (got ${args.permissionId.length} chars)`
|
|
1828
|
+
);
|
|
1829
|
+
}
|
|
1830
|
+
const customKey = args.customKey ?? 0n;
|
|
1831
|
+
if (customKey < 0n || customKey > 0xffffn) {
|
|
1832
|
+
throw new Error(
|
|
1833
|
+
`composeKernelV3NonceKey: customKey must fit in 2 bytes (0..0xffff), got ${customKey}`
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
const paddedPermissionId = viem.pad(args.permissionId, { size: 20, dir: "right" });
|
|
1837
|
+
const customKeyHex = viem.pad(`0x${customKey.toString(16)}`, { size: 2 });
|
|
1838
|
+
const composite = viem.pad(
|
|
1839
|
+
viem.concatHex([
|
|
1840
|
+
VALIDATOR_MODE_DEFAULT,
|
|
1841
|
+
VALIDATOR_TYPE_PERMISSION,
|
|
1842
|
+
paddedPermissionId,
|
|
1843
|
+
customKeyHex
|
|
1844
|
+
]),
|
|
1845
|
+
{ size: 24 }
|
|
1846
|
+
);
|
|
1847
|
+
return BigInt(composite);
|
|
1848
|
+
}
|
|
1849
|
+
|
|
809
1850
|
// src/tools/auth-required.ts
|
|
810
1851
|
function authRequiredPayload() {
|
|
811
1852
|
return {
|
|
@@ -816,7 +1857,45 @@ function authRequiredPayload() {
|
|
|
816
1857
|
};
|
|
817
1858
|
}
|
|
818
1859
|
|
|
1860
|
+
// src/tools/decimal.ts
|
|
1861
|
+
function parseDecimalToUsd6(decimal) {
|
|
1862
|
+
const m = /^(\d+)(?:\.(\d+))?$/.exec(decimal);
|
|
1863
|
+
if (!m) {
|
|
1864
|
+
throw new Error(`Invalid decimal price: ${JSON.stringify(decimal)}`);
|
|
1865
|
+
}
|
|
1866
|
+
const intPart = m[1];
|
|
1867
|
+
const fracPart = m[2] ?? "";
|
|
1868
|
+
const fracPadded = (fracPart + "000000").slice(0, 6);
|
|
1869
|
+
return BigInt(intPart + fracPadded);
|
|
1870
|
+
}
|
|
1871
|
+
function computeSharesFromUsd6(notionalUsd6, navUsd6) {
|
|
1872
|
+
if (navUsd6 <= 0n) {
|
|
1873
|
+
throw new Error("navUsd6 must be positive");
|
|
1874
|
+
}
|
|
1875
|
+
if (notionalUsd6 < 0n) {
|
|
1876
|
+
throw new Error("notionalUsd6 must be non-negative");
|
|
1877
|
+
}
|
|
1878
|
+
return notionalUsd6 / navUsd6;
|
|
1879
|
+
}
|
|
1880
|
+
function formatUsd6AsDecimal(usd6) {
|
|
1881
|
+
if (usd6 < 0n) {
|
|
1882
|
+
throw new Error("usd6 must be non-negative");
|
|
1883
|
+
}
|
|
1884
|
+
const whole = usd6 / 1000000n;
|
|
1885
|
+
const frac = usd6 % 1000000n;
|
|
1886
|
+
if (frac === 0n) return whole.toString();
|
|
1887
|
+
const fracStr = frac.toString().padStart(6, "0").replace(/0+$/, "");
|
|
1888
|
+
return `${whole.toString()}.${fracStr}`;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
819
1891
|
// src/tools/handlers.ts
|
|
1892
|
+
var SUBSCRIPTION_PURCHASE_SELECTOR = viem.toFunctionSelector(
|
|
1893
|
+
"function purchase(address,(uint256,uint8,uint8,bytes),uint128,address)"
|
|
1894
|
+
).toLowerCase();
|
|
1895
|
+
var SUBSCRIPTION_PURCHASE_ABI = viem.parseAbi([
|
|
1896
|
+
"function purchase(address token, (uint256 ctHash, uint8 securityZone, uint8 utype, bytes signature) encShares, uint128 maxSharesHint, address ephemeralEOA)"
|
|
1897
|
+
]);
|
|
1898
|
+
var PLACEHOLDER_SIGNATURE = "0x" + "fe".repeat(86);
|
|
820
1899
|
function ok(data) {
|
|
821
1900
|
return { ok: true, data };
|
|
822
1901
|
}
|
|
@@ -828,85 +1907,775 @@ function mapBackendError(e) {
|
|
|
828
1907
|
if (e.code === "unauthorized") return authRequiredPayload();
|
|
829
1908
|
return err(`backend.${e.code}`, e.message);
|
|
830
1909
|
}
|
|
831
|
-
if (e instanceof Error) return err("backend.network", e.message);
|
|
832
|
-
return err("backend.network", "unknown backend error");
|
|
833
|
-
}
|
|
834
|
-
async function readPortfolio(_input, deps) {
|
|
1910
|
+
if (e instanceof Error) return err("backend.network", e.message);
|
|
1911
|
+
return err("backend.network", "unknown backend error");
|
|
1912
|
+
}
|
|
1913
|
+
async function readPortfolio(_input, deps) {
|
|
1914
|
+
try {
|
|
1915
|
+
const data = await deps.backend.get("/api/v1/portfolio");
|
|
1916
|
+
return ok(data);
|
|
1917
|
+
} catch (e) {
|
|
1918
|
+
return mapBackendError(e);
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
async function readYields(input, deps) {
|
|
1922
|
+
try {
|
|
1923
|
+
const data = await deps.backend.get("/api/v1/yields", {
|
|
1924
|
+
token: input.token,
|
|
1925
|
+
limit: input.limit
|
|
1926
|
+
});
|
|
1927
|
+
return ok(data);
|
|
1928
|
+
} catch (e) {
|
|
1929
|
+
return mapBackendError(e);
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
async function readDistribution(input, deps) {
|
|
1933
|
+
try {
|
|
1934
|
+
const data = await deps.backend.get("/api/v1/distributions", {
|
|
1935
|
+
token: input.token,
|
|
1936
|
+
epoch: input.epoch
|
|
1937
|
+
});
|
|
1938
|
+
return ok(data);
|
|
1939
|
+
} catch (e) {
|
|
1940
|
+
return mapBackendError(e);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
async function readTokens(_input, deps) {
|
|
1944
|
+
try {
|
|
1945
|
+
const data = await deps.backend.get("/api/v1/tokens");
|
|
1946
|
+
return ok(data);
|
|
1947
|
+
} catch (e) {
|
|
1948
|
+
return mapBackendError(e);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
async function readAudit(input, deps) {
|
|
1952
|
+
try {
|
|
1953
|
+
const data = await deps.backend.get("/api/v1/agent/policy/audit", {
|
|
1954
|
+
surface: input.surface,
|
|
1955
|
+
eventTypes: input.eventTypes?.join(","),
|
|
1956
|
+
since: input.since,
|
|
1957
|
+
until: input.until,
|
|
1958
|
+
cursor: input.cursor,
|
|
1959
|
+
limit: input.limit
|
|
1960
|
+
});
|
|
1961
|
+
return ok(data);
|
|
1962
|
+
} catch (e) {
|
|
1963
|
+
return mapBackendError(e);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
async function readActivity(input, deps) {
|
|
1967
|
+
try {
|
|
1968
|
+
const data = await deps.backend.get("/api/v1/activity", {
|
|
1969
|
+
limit: input.limit,
|
|
1970
|
+
offset: input.offset
|
|
1971
|
+
});
|
|
1972
|
+
return ok(data);
|
|
1973
|
+
} catch (e) {
|
|
1974
|
+
return mapBackendError(e);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
function buildPositionDeeplink(dashboardBaseUrl, action, params) {
|
|
1978
|
+
const base = dashboardBaseUrl.replace(/\/+$/, "");
|
|
1979
|
+
const path = action === "buy" || action === "sell" ? "/trade" : action === "claim" ? "/yields" : "/cash";
|
|
1980
|
+
const search = new URLSearchParams();
|
|
1981
|
+
if (action === "buy" || action === "sell") search.set("mode", action);
|
|
1982
|
+
for (const [k, v] of Object.entries(params)) search.set(k, v);
|
|
1983
|
+
search.set("from", "mcp");
|
|
1984
|
+
return `${base}${path}?${search.toString()}`;
|
|
1985
|
+
}
|
|
1986
|
+
function resolveDashboardBaseUrl(deps) {
|
|
1987
|
+
return deps.dashboardBaseUrl ?? "https://muhaven.app";
|
|
1988
|
+
}
|
|
1989
|
+
function resolveTokenInCatalog(identifier, catalog) {
|
|
1990
|
+
const needle = identifier.toLowerCase();
|
|
1991
|
+
return catalog.find(
|
|
1992
|
+
(t) => t.address.toLowerCase() === needle || t.symbol.toLowerCase() === needle
|
|
1993
|
+
) ?? null;
|
|
1994
|
+
}
|
|
1995
|
+
function sanitizeSymbolForLlmContext(raw) {
|
|
1996
|
+
const cleaned = raw.replace(/[^A-Za-z0-9_-]/g, "?");
|
|
1997
|
+
return cleaned.length > 16 ? cleaned.slice(0, 16) : cleaned;
|
|
1998
|
+
}
|
|
1999
|
+
function sanitizeRpcMessageForLlmContext(raw) {
|
|
2000
|
+
const cleaned = raw.replace(/[\x00-\x1f\x7f]/g, "").replace(/[^\x20-\x7e]/g, "?").replace(/\s+/g, " ").trim();
|
|
2001
|
+
return cleaned.length > 120 ? cleaned.slice(0, 120) + "\u2026" : cleaned;
|
|
2002
|
+
}
|
|
2003
|
+
function mapBrokerCallFailure(err2, verb, defaultReason = "broker_internal") {
|
|
2004
|
+
if (err2 instanceof BrokerClientError && err2.brokerCode === "unsupported_type") {
|
|
2005
|
+
return {
|
|
2006
|
+
kind: "fallback",
|
|
2007
|
+
reason: "version_too_old",
|
|
2008
|
+
message: `broker daemon rejected ${verb} as unsupported_type \u2014 daemon is likely older than protocol 0.4.0; upgrade @muhaven/mcp and restart the broker`
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
2011
|
+
return {
|
|
2012
|
+
kind: "fallback",
|
|
2013
|
+
reason: defaultReason,
|
|
2014
|
+
message: `broker rejected ${verb} (${typedErrorCode(err2)})`
|
|
2015
|
+
};
|
|
2016
|
+
}
|
|
2017
|
+
var MCP_HEX_20_BYTE_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
2018
|
+
var MCP_HEX_4_BYTE_RE = /^0x[0-9a-fA-F]{8}$/;
|
|
2019
|
+
var MCP_HEX_32_BYTE_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
2020
|
+
var MCP_SESSION_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
2021
|
+
var MirrorDtoMalformedError = class extends Error {
|
|
2022
|
+
constructor(message) {
|
|
2023
|
+
super(message);
|
|
2024
|
+
this.name = "MirrorDtoMalformedError";
|
|
2025
|
+
}
|
|
2026
|
+
};
|
|
2027
|
+
function mirrorDtoToPolicySnapshot(dto) {
|
|
2028
|
+
if (dto.mode !== "scoped") {
|
|
2029
|
+
throw new MirrorDtoMalformedError(
|
|
2030
|
+
`mode must be 'scoped' (got ${JSON.stringify(dto.mode)}); wildcard mirror auto-sync ships in Slice 4`
|
|
2031
|
+
);
|
|
2032
|
+
}
|
|
2033
|
+
if (dto.status !== "active") {
|
|
2034
|
+
throw new MirrorDtoMalformedError(
|
|
2035
|
+
`status must be 'active' for auto-sync (got ${JSON.stringify(dto.status)}); backend mirror should have filtered this row out`
|
|
2036
|
+
);
|
|
2037
|
+
}
|
|
2038
|
+
if (typeof dto.sessionId !== "string" || !MCP_SESSION_ID_RE.test(dto.sessionId)) {
|
|
2039
|
+
throw new MirrorDtoMalformedError(
|
|
2040
|
+
`sessionId must match /^[A-Za-z0-9_-]{1,128}$/`
|
|
2041
|
+
);
|
|
2042
|
+
}
|
|
2043
|
+
if (!MCP_HEX_20_BYTE_RE.test(dto.signerAddress)) {
|
|
2044
|
+
throw new MirrorDtoMalformedError(
|
|
2045
|
+
`signerAddress is not a 0x-prefixed 20-byte hex`
|
|
2046
|
+
);
|
|
2047
|
+
}
|
|
2048
|
+
if (!Array.isArray(dto.targetContracts) || dto.targetContracts.length === 0) {
|
|
2049
|
+
throw new MirrorDtoMalformedError(
|
|
2050
|
+
`targetContracts must be a non-empty array`
|
|
2051
|
+
);
|
|
2052
|
+
}
|
|
2053
|
+
for (const t of dto.targetContracts) {
|
|
2054
|
+
if (typeof t !== "string" || !MCP_HEX_20_BYTE_RE.test(t)) {
|
|
2055
|
+
throw new MirrorDtoMalformedError(
|
|
2056
|
+
`targetContracts entry is not a 0x-prefixed 20-byte hex`
|
|
2057
|
+
);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
if (!Array.isArray(dto.selectorCaps) || dto.selectorCaps.length === 0) {
|
|
2061
|
+
throw new MirrorDtoMalformedError(`selectorCaps must be a non-empty array`);
|
|
2062
|
+
}
|
|
2063
|
+
for (const c of dto.selectorCaps) {
|
|
2064
|
+
if (typeof c?.selector !== "string" || !MCP_HEX_4_BYTE_RE.test(c.selector)) {
|
|
2065
|
+
throw new MirrorDtoMalformedError(
|
|
2066
|
+
`selectorCaps entry has a malformed selector`
|
|
2067
|
+
);
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
if (dto.permissionId != null && !MCP_HEX_4_BYTE_RE.test(dto.permissionId)) {
|
|
2071
|
+
throw new MirrorDtoMalformedError(
|
|
2072
|
+
`permissionId is not a 0x-prefixed 4-byte hex`
|
|
2073
|
+
);
|
|
2074
|
+
}
|
|
2075
|
+
if (dto.consentActionHash != null && !MCP_HEX_32_BYTE_RE.test(dto.consentActionHash)) {
|
|
2076
|
+
throw new MirrorDtoMalformedError(
|
|
2077
|
+
`consentActionHash is not a 0x-prefixed 32-byte hex`
|
|
2078
|
+
);
|
|
2079
|
+
}
|
|
2080
|
+
if (dto.consentTextSha256 != null && !MCP_HEX_32_BYTE_RE.test(dto.consentTextSha256)) {
|
|
2081
|
+
throw new MirrorDtoMalformedError(
|
|
2082
|
+
`consentTextSha256 is not a 0x-prefixed 32-byte hex`
|
|
2083
|
+
);
|
|
2084
|
+
}
|
|
2085
|
+
return {
|
|
2086
|
+
sessionId: dto.sessionId,
|
|
2087
|
+
mode: "scoped",
|
|
2088
|
+
signerAddress: dto.signerAddress.toLowerCase(),
|
|
2089
|
+
targetContracts: dto.targetContracts.map(
|
|
2090
|
+
(a) => a.toLowerCase()
|
|
2091
|
+
),
|
|
2092
|
+
selectorCaps: dto.selectorCaps.map((c) => ({
|
|
2093
|
+
selector: c.selector.toLowerCase(),
|
|
2094
|
+
capArgIndex: c.capArgIndex,
|
|
2095
|
+
maxAmount: c.maxAmount
|
|
2096
|
+
})),
|
|
2097
|
+
validUntilSec: dto.validUntilSec,
|
|
2098
|
+
mintedAtSec: dto.mintedAtSec,
|
|
2099
|
+
...dto.consentActionHash ? { consentActionHash: dto.consentActionHash.toLowerCase() } : {},
|
|
2100
|
+
...dto.consentTextSha256 ? { consentTextSha256: dto.consentTextSha256.toLowerCase() } : {},
|
|
2101
|
+
...dto.permissionId ? { permissionId: dto.permissionId.toLowerCase() } : {}
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
function typedErrorCode(err2) {
|
|
2105
|
+
if (err2 instanceof BackendError) return `backend.${err2.code}`;
|
|
2106
|
+
if (err2 instanceof BrokerClientError) {
|
|
2107
|
+
return err2.brokerCode ? `broker.${err2.brokerCode}` : `broker.${err2.code}`;
|
|
2108
|
+
}
|
|
2109
|
+
if (err2 instanceof MirrorDtoMalformedError) return "malformed_mirror_row";
|
|
2110
|
+
return "unknown";
|
|
2111
|
+
}
|
|
2112
|
+
async function fetchJwtSubjectHint(deps) {
|
|
2113
|
+
if (!deps.broker) return null;
|
|
2114
|
+
try {
|
|
2115
|
+
const res = await deps.broker.getJwt();
|
|
2116
|
+
if (!res.jwt) return null;
|
|
2117
|
+
const decoded = decodeJwtPayload(res.jwt);
|
|
2118
|
+
return truncateSubject(decoded.sub);
|
|
2119
|
+
} catch {
|
|
2120
|
+
return null;
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
async function syncSnapshotFromMirror(deps, brokerSignerAddress) {
|
|
2124
|
+
if (!deps.broker) {
|
|
2125
|
+
return {
|
|
2126
|
+
kind: "fallback",
|
|
2127
|
+
reason: "mirror_sync_failed",
|
|
2128
|
+
message: "auto-sync invoked without a broker dep \u2014 pipeline bug"
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
let mirror;
|
|
2132
|
+
try {
|
|
2133
|
+
mirror = await deps.backend.get(
|
|
2134
|
+
"/api/v1/agent/policy/scoped-session",
|
|
2135
|
+
{ surface: "mcp" }
|
|
2136
|
+
);
|
|
2137
|
+
} catch (err2) {
|
|
2138
|
+
return {
|
|
2139
|
+
kind: "fallback",
|
|
2140
|
+
reason: "mirror_sync_failed",
|
|
2141
|
+
message: `backend mirror lookup failed (${typedErrorCode(err2)})`
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
if (!mirror || mirror.session == null) {
|
|
2145
|
+
const subjectHint = await fetchJwtSubjectHint(deps);
|
|
2146
|
+
const hintSuffix = subjectHint ? ` (broker JWT subject: ${subjectHint} \u2014 verify this matches the userId of the wallet you used to mint the scoped tier; if not, run \`muhaven-broker logout && muhaven-broker login\` and re-authorize with the correct passkey)` : "";
|
|
2147
|
+
return {
|
|
2148
|
+
kind: "fallback",
|
|
2149
|
+
reason: "no_active_session_key",
|
|
2150
|
+
message: "no active scoped session \u2014 visit /agent/policy/transition to mint one, then retry. (Mirror also empty; nothing to auto-sync.)" + hintSuffix
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
let snapshot;
|
|
2154
|
+
try {
|
|
2155
|
+
snapshot = mirrorDtoToPolicySnapshot(mirror.session);
|
|
2156
|
+
} catch (err2) {
|
|
2157
|
+
return {
|
|
2158
|
+
kind: "fallback",
|
|
2159
|
+
reason: "mirror_sync_failed",
|
|
2160
|
+
message: `mirror returned a malformed scoped-session row (${typedErrorCode(err2)})`
|
|
2161
|
+
};
|
|
2162
|
+
}
|
|
2163
|
+
if (snapshot.signerAddress.toLowerCase() !== brokerSignerAddress.toLowerCase()) {
|
|
2164
|
+
return {
|
|
2165
|
+
kind: "fallback",
|
|
2166
|
+
reason: "signer_mismatch",
|
|
2167
|
+
message: `mirror snapshot is bound to signer ${snapshot.signerAddress}, broker is currently signing as ${brokerSignerAddress} \u2014 re-mint the scoped tier from the dashboard against the broker's current session key, OR restart the broker with the session key matching the mirror snapshot`
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
try {
|
|
2171
|
+
await deps.broker.storePolicySnapshot(snapshot);
|
|
2172
|
+
} catch (err2) {
|
|
2173
|
+
return {
|
|
2174
|
+
kind: "fallback",
|
|
2175
|
+
reason: "mirror_sync_failed",
|
|
2176
|
+
message: `broker rejected store_policy_snapshot (${typedErrorCode(err2)})`
|
|
2177
|
+
};
|
|
2178
|
+
}
|
|
2179
|
+
let activeId;
|
|
2180
|
+
try {
|
|
2181
|
+
const res = await deps.broker.getActiveSessionId();
|
|
2182
|
+
activeId = res.sessionId;
|
|
2183
|
+
} catch (err2) {
|
|
2184
|
+
return {
|
|
2185
|
+
kind: "fallback",
|
|
2186
|
+
reason: "mirror_sync_failed",
|
|
2187
|
+
message: `broker re-probe after store_policy_snapshot failed (${typedErrorCode(err2)})`
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2190
|
+
if (!activeId) {
|
|
2191
|
+
return {
|
|
2192
|
+
kind: "fallback",
|
|
2193
|
+
reason: "mirror_sync_failed",
|
|
2194
|
+
message: "broker accepted store_policy_snapshot but get_active_session_id returned null \u2014 most likely cause: multiple non-expired snapshots for the same signer collapse to ambiguous. Clear stale snapshots via the muhaven-broker CLI before retrying. (Snapshot signer was pre-validated to match the broker; signer mismatch already filtered upstream.)"
|
|
2195
|
+
};
|
|
2196
|
+
}
|
|
2197
|
+
return { kind: "ok", sessionId: activeId };
|
|
2198
|
+
}
|
|
2199
|
+
async function attemptPathD(args, deps) {
|
|
2200
|
+
const { shares, tokenAddress, tokenSymbol } = args;
|
|
2201
|
+
if (!deps.broker || !deps.bundler) {
|
|
2202
|
+
return { kind: "unconfigured" };
|
|
2203
|
+
}
|
|
2204
|
+
if (!deps.subscriptionAddress) {
|
|
2205
|
+
return {
|
|
2206
|
+
kind: "fallback",
|
|
2207
|
+
reason: "subscription_address_unset",
|
|
2208
|
+
message: "MUHAVEN_SUBSCRIPTION_ADDRESS not configured \u2014 Path D autonomous-buy disabled until the operator sets it in the MCP env"
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2211
|
+
if (!deps.entryPointAddress) {
|
|
2212
|
+
return {
|
|
2213
|
+
kind: "fallback",
|
|
2214
|
+
reason: "entry_point_unset",
|
|
2215
|
+
message: "MUHAVEN_ENTRY_POINT resolved to undefined \u2014 Path D requires the EntryPoint v0.7 address"
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
if (typeof deps.chainId !== "number") {
|
|
2219
|
+
return {
|
|
2220
|
+
kind: "fallback",
|
|
2221
|
+
reason: "chain_id_unset",
|
|
2222
|
+
message: "MUHAVEN_CHAIN_ID not configured \u2014 Path D autonomous-buy requires a chain id for userOpHash"
|
|
2223
|
+
};
|
|
2224
|
+
}
|
|
2225
|
+
const subscriptionAddress = deps.subscriptionAddress;
|
|
2226
|
+
const entryPointAddress = deps.entryPointAddress;
|
|
2227
|
+
const chainId = deps.chainId;
|
|
2228
|
+
const preflight = await deps.broker.preflight();
|
|
2229
|
+
if (!preflight.supported) {
|
|
2230
|
+
if (preflight.reason === "broker_unreachable") {
|
|
2231
|
+
return {
|
|
2232
|
+
kind: "fallback",
|
|
2233
|
+
reason: "broker_unreachable",
|
|
2234
|
+
message: `broker daemon not reachable (${preflight.message}) \u2014 falling back to Path C dashboard deep-link`
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
if (preflight.reason === "version_too_old") {
|
|
2238
|
+
return {
|
|
2239
|
+
kind: "fallback",
|
|
2240
|
+
reason: "version_too_old",
|
|
2241
|
+
message: `broker speaks ${preflight.daemonVersion}, Path D requires \u2265${preflight.requiredVersion} \u2014 upgrade @muhaven/mcp and restart the broker`
|
|
2242
|
+
};
|
|
2243
|
+
}
|
|
2244
|
+
return {
|
|
2245
|
+
kind: "fallback",
|
|
2246
|
+
reason: "session_key_unavailable",
|
|
2247
|
+
message: "broker is running in read-only posture (no MUHAVEN_BROKER_SESSION_KEY set) \u2014 Path D requires a loaded session key"
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
let activeId;
|
|
2251
|
+
try {
|
|
2252
|
+
const res = await deps.broker.getActiveSessionId();
|
|
2253
|
+
activeId = res.sessionId;
|
|
2254
|
+
} catch (err2) {
|
|
2255
|
+
return mapBrokerCallFailure(err2, "get_active_session_id");
|
|
2256
|
+
}
|
|
2257
|
+
if (!activeId) {
|
|
2258
|
+
const synced = await syncSnapshotFromMirror(deps, preflight.signerAddress);
|
|
2259
|
+
if (synced.kind === "fallback") {
|
|
2260
|
+
return synced;
|
|
2261
|
+
}
|
|
2262
|
+
activeId = synced.sessionId;
|
|
2263
|
+
}
|
|
2264
|
+
let snapshot;
|
|
2265
|
+
try {
|
|
2266
|
+
const res = await deps.broker.getPolicySnapshot(activeId);
|
|
2267
|
+
snapshot = res.snapshot;
|
|
2268
|
+
} catch (err2) {
|
|
2269
|
+
return mapBrokerCallFailure(err2, "get_policy_snapshot", "snapshot_lookup_failed");
|
|
2270
|
+
}
|
|
2271
|
+
if (!snapshot) {
|
|
2272
|
+
return {
|
|
2273
|
+
kind: "fallback",
|
|
2274
|
+
reason: "no_active_snapshot",
|
|
2275
|
+
message: `broker reported session ${activeId} active but get_policy_snapshot returned null (race? \u2014 refresh tier from dashboard)`
|
|
2276
|
+
};
|
|
2277
|
+
}
|
|
2278
|
+
if (snapshot.signerAddress.toLowerCase() !== preflight.signerAddress.toLowerCase()) {
|
|
2279
|
+
return {
|
|
2280
|
+
kind: "fallback",
|
|
2281
|
+
reason: "signer_mismatch",
|
|
2282
|
+
message: `snapshot ${activeId} is bound to signer ${snapshot.signerAddress}, broker is currently signing as ${preflight.signerAddress} \u2014 broker session-key likely rotated mid-flight; re-mint the scoped tier from the dashboard`
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
const purchaseCap = snapshot.selectorCaps.find(
|
|
2286
|
+
(c) => c.selector.toLowerCase() === SUBSCRIPTION_PURCHASE_SELECTOR
|
|
2287
|
+
);
|
|
2288
|
+
if (!purchaseCap) {
|
|
2289
|
+
return {
|
|
2290
|
+
kind: "fallback",
|
|
2291
|
+
reason: "selector_not_in_snapshot",
|
|
2292
|
+
message: "active scoped session does not authorize subscription.purchase \u2014 re-mint the session with a purchase cap"
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
if (purchaseCap.maxAmount === null) {
|
|
2296
|
+
return {
|
|
2297
|
+
kind: "fallback",
|
|
2298
|
+
reason: "selector_uncapped",
|
|
2299
|
+
message: "active scoped session lists subscription.purchase but with no per-op cap (capArgIndex/maxAmount both null) \u2014 Slice 1 refuses to autonomy-buy without an explicit ceiling; re-mint with a maxAmount"
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
const maxShares = BigInt(purchaseCap.maxAmount);
|
|
2303
|
+
if (shares > maxShares) {
|
|
2304
|
+
return {
|
|
2305
|
+
kind: "fallback",
|
|
2306
|
+
reason: "out_of_scope",
|
|
2307
|
+
message: `requested ${shares} shares exceeds the active session's per-op cap of ${maxShares} shares \u2014 fall back to Path C dashboard deep-link for this larger buy`
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
if (!snapshot.targetContracts.some(
|
|
2311
|
+
(t) => t.toLowerCase() === subscriptionAddress.toLowerCase()
|
|
2312
|
+
)) {
|
|
2313
|
+
return {
|
|
2314
|
+
kind: "fallback",
|
|
2315
|
+
reason: "target_not_in_snapshot",
|
|
2316
|
+
message: `subscription target ${subscriptionAddress} not in active session's target allowlist \u2014 re-mint the session with subscription in scope`
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2319
|
+
let accountAddress;
|
|
835
2320
|
try {
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
2321
|
+
const stateDto = await deps.backend.get("/api/v1/agent/policy/state", {
|
|
2322
|
+
surface: "mcp"
|
|
2323
|
+
});
|
|
2324
|
+
if (!stateDto.accountAddress || !/^0x[0-9a-fA-F]{40}$/.test(stateDto.accountAddress)) {
|
|
2325
|
+
return {
|
|
2326
|
+
kind: "fallback",
|
|
2327
|
+
reason: "no_validator_registered",
|
|
2328
|
+
message: "backend /agent/policy/state returned no accountAddress \u2014 re-login the MCP"
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
accountAddress = stateDto.accountAddress.toLowerCase();
|
|
2332
|
+
} catch (err2) {
|
|
2333
|
+
return {
|
|
2334
|
+
kind: "fallback",
|
|
2335
|
+
reason: "no_validator_registered",
|
|
2336
|
+
message: `backend /agent/policy/state lookup failed: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
2337
|
+
};
|
|
840
2338
|
}
|
|
841
|
-
|
|
842
|
-
|
|
2339
|
+
if (!snapshot.permissionId) {
|
|
2340
|
+
return {
|
|
2341
|
+
kind: "fallback",
|
|
2342
|
+
reason: "no_permission_id_in_snapshot",
|
|
2343
|
+
message: "active scoped session snapshot lacks permissionId \u2014 frontend storePolicySnapshot wire-up is a Slice 2 prerequisite; falling back to Path C"
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
const permissionId = snapshot.permissionId;
|
|
2347
|
+
let encShares;
|
|
2348
|
+
let ephemeralEOA;
|
|
843
2349
|
try {
|
|
844
|
-
const
|
|
845
|
-
|
|
846
|
-
|
|
2350
|
+
const enc = await deps.backend.post("/api/v1/agent/path-d/encrypt-shares", {
|
|
2351
|
+
tokenAddress,
|
|
2352
|
+
sharesAmount: shares.toString()
|
|
847
2353
|
});
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
2354
|
+
if (!enc.encShares || typeof enc.encShares.ctHash !== "string" || typeof enc.encShares.securityZone !== "number" || typeof enc.encShares.utype !== "number" || typeof enc.encShares.signature !== "string" || typeof enc.ephemeralEOA !== "string") {
|
|
2355
|
+
return {
|
|
2356
|
+
kind: "fallback",
|
|
2357
|
+
reason: "encrypt_shares_server_error",
|
|
2358
|
+
message: "backend /agent/path-d/encrypt-shares returned malformed payload"
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
encShares = {
|
|
2362
|
+
ctHash: enc.encShares.ctHash,
|
|
2363
|
+
securityZone: enc.encShares.securityZone,
|
|
2364
|
+
utype: enc.encShares.utype,
|
|
2365
|
+
signature: enc.encShares.signature
|
|
2366
|
+
};
|
|
2367
|
+
ephemeralEOA = enc.ephemeralEOA;
|
|
2368
|
+
} catch (err2) {
|
|
2369
|
+
if (err2 instanceof BackendError) {
|
|
2370
|
+
const is4xx = typeof err2.status === "number" && err2.status < 500;
|
|
2371
|
+
return {
|
|
2372
|
+
kind: "fallback",
|
|
2373
|
+
reason: is4xx ? "encrypt_shares_rejected" : "encrypt_shares_server_error",
|
|
2374
|
+
message: `backend rejected encrypt-shares (backend.${err2.code})`
|
|
2375
|
+
};
|
|
2376
|
+
}
|
|
2377
|
+
return {
|
|
2378
|
+
kind: "fallback",
|
|
2379
|
+
reason: "encrypt_shares_server_error",
|
|
2380
|
+
message: `backend /agent/path-d/encrypt-shares failed: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
2381
|
+
};
|
|
851
2382
|
}
|
|
852
|
-
|
|
853
|
-
|
|
2383
|
+
const innerCallData = viem.encodeFunctionData({
|
|
2384
|
+
abi: SUBSCRIPTION_PURCHASE_ABI,
|
|
2385
|
+
functionName: "purchase",
|
|
2386
|
+
args: [
|
|
2387
|
+
tokenAddress,
|
|
2388
|
+
{
|
|
2389
|
+
ctHash: BigInt(encShares.ctHash),
|
|
2390
|
+
securityZone: encShares.securityZone,
|
|
2391
|
+
utype: encShares.utype,
|
|
2392
|
+
signature: encShares.signature
|
|
2393
|
+
},
|
|
2394
|
+
shares,
|
|
2395
|
+
// maxSharesHint — tight per spec
|
|
2396
|
+
ephemeralEOA
|
|
2397
|
+
]
|
|
2398
|
+
});
|
|
2399
|
+
const kernelCallData = encodeKernelExecuteSingleCall({
|
|
2400
|
+
target: subscriptionAddress,
|
|
2401
|
+
value: 0n,
|
|
2402
|
+
callData: innerCallData
|
|
2403
|
+
});
|
|
2404
|
+
let nonce;
|
|
2405
|
+
let feeData;
|
|
854
2406
|
try {
|
|
855
|
-
const
|
|
856
|
-
|
|
857
|
-
|
|
2407
|
+
const nonceKey = composeKernelV3NonceKey({ permissionId });
|
|
2408
|
+
nonce = await deps.bundler.getNonce(accountAddress, entryPointAddress, nonceKey);
|
|
2409
|
+
feeData = await deps.bundler.getFeeData();
|
|
2410
|
+
} catch (err2) {
|
|
2411
|
+
return {
|
|
2412
|
+
kind: "fallback",
|
|
2413
|
+
reason: "bundler_setup_failed",
|
|
2414
|
+
message: `bundler bootstrap failed: ${err2 instanceof BundlerClientError ? `${err2.code}: ${err2.message}` : String(err2)}`
|
|
2415
|
+
};
|
|
2416
|
+
}
|
|
2417
|
+
const partial = {
|
|
2418
|
+
sender: accountAddress,
|
|
2419
|
+
nonce: `0x${nonce.toString(16)}`,
|
|
2420
|
+
callData: kernelCallData,
|
|
2421
|
+
maxFeePerGas: feeData.maxFeePerGas,
|
|
2422
|
+
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
|
|
2423
|
+
signature: PLACEHOLDER_SIGNATURE
|
|
2424
|
+
};
|
|
2425
|
+
let sponsored;
|
|
2426
|
+
try {
|
|
2427
|
+
sponsored = await deps.bundler.sponsorUserOp(partial, entryPointAddress);
|
|
2428
|
+
} catch (err2) {
|
|
2429
|
+
const detail = err2 instanceof BundlerClientError && err2.detail && typeof err2.detail === "object" ? ` (rpc code=${err2.detail.code ?? "unknown"})` : "";
|
|
2430
|
+
const safeMsg = sanitizeRpcMessageForLlmContext(
|
|
2431
|
+
err2 instanceof Error ? err2.message : String(err2)
|
|
2432
|
+
);
|
|
2433
|
+
return {
|
|
2434
|
+
kind: "fallback",
|
|
2435
|
+
reason: "paymaster_rejected",
|
|
2436
|
+
message: `pm_sponsorUserOperation rejected${detail}: ${safeMsg}`
|
|
2437
|
+
};
|
|
2438
|
+
}
|
|
2439
|
+
const userOpForHash = {
|
|
2440
|
+
sender: accountAddress,
|
|
2441
|
+
nonce,
|
|
2442
|
+
factory: void 0,
|
|
2443
|
+
factoryData: void 0,
|
|
2444
|
+
callData: kernelCallData,
|
|
2445
|
+
callGasLimit: BigInt(sponsored.callGasLimit),
|
|
2446
|
+
verificationGasLimit: BigInt(sponsored.verificationGasLimit),
|
|
2447
|
+
preVerificationGas: BigInt(sponsored.preVerificationGas),
|
|
2448
|
+
maxFeePerGas: BigInt(feeData.maxFeePerGas),
|
|
2449
|
+
maxPriorityFeePerGas: BigInt(feeData.maxPriorityFeePerGas),
|
|
2450
|
+
paymaster: sponsored.paymaster,
|
|
2451
|
+
paymasterVerificationGasLimit: BigInt(sponsored.paymasterVerificationGasLimit),
|
|
2452
|
+
paymasterPostOpGasLimit: BigInt(sponsored.paymasterPostOpGasLimit),
|
|
2453
|
+
paymasterData: sponsored.paymasterData,
|
|
2454
|
+
signature: PLACEHOLDER_SIGNATURE
|
|
2455
|
+
};
|
|
2456
|
+
const userOpHash = accountAbstraction.getUserOperationHash({
|
|
2457
|
+
userOperation: userOpForHash,
|
|
2458
|
+
entryPointAddress,
|
|
2459
|
+
entryPointVersion: "0.7",
|
|
2460
|
+
chainId
|
|
2461
|
+
});
|
|
2462
|
+
let brokerSig;
|
|
2463
|
+
try {
|
|
2464
|
+
const signed = await deps.broker.signUserOp({
|
|
2465
|
+
sessionId: activeId,
|
|
2466
|
+
userOpHash,
|
|
2467
|
+
innerCall: { target: subscriptionAddress, callData: innerCallData },
|
|
2468
|
+
intent: {
|
|
2469
|
+
tool: "muhaven.position.buy",
|
|
2470
|
+
summary: `${shares.toString()} shares of ${sanitizeSymbolForLlmContext(tokenSymbol)}`
|
|
2471
|
+
}
|
|
858
2472
|
});
|
|
859
|
-
|
|
860
|
-
} catch (
|
|
861
|
-
|
|
2473
|
+
brokerSig = signed.signature;
|
|
2474
|
+
} catch (err2) {
|
|
2475
|
+
if (err2 instanceof BrokerClientError && err2.brokerCode) {
|
|
2476
|
+
const code = err2.brokerCode;
|
|
2477
|
+
if (code === "policy_violation") {
|
|
2478
|
+
return {
|
|
2479
|
+
kind: "fallback",
|
|
2480
|
+
reason: "broker_policy_violation",
|
|
2481
|
+
message: "broker rejected sign_userop: policy_violation (innerCall vs snapshot mismatch)"
|
|
2482
|
+
};
|
|
2483
|
+
}
|
|
2484
|
+
if (code === "scope_violation") {
|
|
2485
|
+
return {
|
|
2486
|
+
kind: "fallback",
|
|
2487
|
+
reason: "broker_scope_violation",
|
|
2488
|
+
message: "broker rejected sign_userop: scope_violation (snapshot expired between gate and sign)"
|
|
2489
|
+
};
|
|
2490
|
+
}
|
|
2491
|
+
if (code === "max_spend_exceeded") {
|
|
2492
|
+
return {
|
|
2493
|
+
kind: "fallback",
|
|
2494
|
+
reason: "broker_max_spend_exceeded",
|
|
2495
|
+
message: "broker rejected sign_userop: max_spend_exceeded (innerCall maxSharesHint over cap)"
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
if (code === "no_active_snapshot") {
|
|
2499
|
+
return {
|
|
2500
|
+
kind: "fallback",
|
|
2501
|
+
reason: "broker_no_active_snapshot_at_sign",
|
|
2502
|
+
message: "broker reported no_active_snapshot at sign time \u2014 likely GC race after our snapshot read"
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
return mapBrokerCallFailure(err2, "sign_userop", "broker_internal");
|
|
2507
|
+
}
|
|
2508
|
+
const signedUserOpWire = {
|
|
2509
|
+
sender: accountAddress,
|
|
2510
|
+
nonce: partial.nonce,
|
|
2511
|
+
callData: kernelCallData,
|
|
2512
|
+
callGasLimit: sponsored.callGasLimit,
|
|
2513
|
+
verificationGasLimit: sponsored.verificationGasLimit,
|
|
2514
|
+
preVerificationGas: sponsored.preVerificationGas,
|
|
2515
|
+
maxFeePerGas: feeData.maxFeePerGas,
|
|
2516
|
+
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
|
|
2517
|
+
paymaster: sponsored.paymaster,
|
|
2518
|
+
paymasterVerificationGasLimit: sponsored.paymasterVerificationGasLimit,
|
|
2519
|
+
paymasterPostOpGasLimit: sponsored.paymasterPostOpGasLimit,
|
|
2520
|
+
paymasterData: sponsored.paymasterData,
|
|
2521
|
+
signature: buildKernelSessionKeySignature({ ecdsaSignature: brokerSig })
|
|
2522
|
+
};
|
|
2523
|
+
let submittedHash;
|
|
2524
|
+
try {
|
|
2525
|
+
submittedHash = await deps.bundler.sendUserOp(signedUserOpWire, entryPointAddress);
|
|
2526
|
+
} catch (err2) {
|
|
2527
|
+
const detail = err2 instanceof BundlerClientError && err2.detail && typeof err2.detail === "object" ? ` (rpc code=${err2.detail.code ?? "unknown"})` : "";
|
|
2528
|
+
return {
|
|
2529
|
+
kind: "fallback",
|
|
2530
|
+
reason: "bundler_submit_rejected",
|
|
2531
|
+
message: `bundler eth_sendUserOperation rejected${detail}: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
2532
|
+
};
|
|
2533
|
+
}
|
|
2534
|
+
if (submittedHash.toLowerCase() !== userOpHash.toLowerCase()) {
|
|
2535
|
+
return {
|
|
2536
|
+
kind: "fallback",
|
|
2537
|
+
reason: "userop_hash_mismatch",
|
|
2538
|
+
message: `bundler reported userOpHash ${submittedHash} but we signed ${userOpHash} \u2014 refusing to wait for receipt`
|
|
2539
|
+
};
|
|
2540
|
+
}
|
|
2541
|
+
try {
|
|
2542
|
+
const receipt = await deps.bundler.waitForReceipt(userOpHash, { timeoutMs: 12e3 });
|
|
2543
|
+
return {
|
|
2544
|
+
kind: "ok",
|
|
2545
|
+
data: {
|
|
2546
|
+
action: "buy",
|
|
2547
|
+
status: "submitted",
|
|
2548
|
+
txHash: receipt.receipt.transactionHash,
|
|
2549
|
+
userOpHash,
|
|
2550
|
+
path: "D"
|
|
2551
|
+
}
|
|
2552
|
+
};
|
|
2553
|
+
} catch (err2) {
|
|
2554
|
+
try {
|
|
2555
|
+
const lateReceipt = await deps.bundler.getReceipt(userOpHash);
|
|
2556
|
+
if (lateReceipt) {
|
|
2557
|
+
return {
|
|
2558
|
+
kind: "ok",
|
|
2559
|
+
data: {
|
|
2560
|
+
action: "buy",
|
|
2561
|
+
status: "submitted",
|
|
2562
|
+
txHash: lateReceipt.receipt.transactionHash,
|
|
2563
|
+
userOpHash,
|
|
2564
|
+
path: "D"
|
|
2565
|
+
}
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
} catch {
|
|
2569
|
+
}
|
|
2570
|
+
return {
|
|
2571
|
+
kind: "fallback",
|
|
2572
|
+
reason: "bundler_receipt_timeout",
|
|
2573
|
+
message: `no receipt for userOp ${userOpHash} within 12s. The userOp may still mine. BEFORE proposing another position.buy for this intent, call muhaven.read.activity to verify whether the prior submit settled \u2014 re-issuing without that check risks double-filling.`,
|
|
2574
|
+
submittedUserOpHash: userOpHash
|
|
2575
|
+
};
|
|
862
2576
|
}
|
|
863
2577
|
}
|
|
864
|
-
async function
|
|
2578
|
+
async function positionBuy(input, deps) {
|
|
2579
|
+
let catalog;
|
|
865
2580
|
try {
|
|
866
|
-
|
|
867
|
-
return ok(data);
|
|
2581
|
+
catalog = await deps.backend.getUnauth("/api/v1/tokens");
|
|
868
2582
|
} catch (e) {
|
|
869
2583
|
return mapBackendError(e);
|
|
870
2584
|
}
|
|
871
|
-
|
|
872
|
-
|
|
2585
|
+
const token = resolveTokenInCatalog(input.token, catalog.tokens ?? []);
|
|
2586
|
+
if (!token) {
|
|
2587
|
+
return err(
|
|
2588
|
+
"token_not_found",
|
|
2589
|
+
`Token "${sanitizeSymbolForLlmContext(input.token)}" is not in the MuHaven catalog. Call muhaven.read.tokens for the canonical symbol list.`
|
|
2590
|
+
);
|
|
2591
|
+
}
|
|
2592
|
+
const safeSymbol = sanitizeSymbolForLlmContext(token.symbol);
|
|
2593
|
+
if (!token.latest_nav || !token.latest_nav.nav) {
|
|
2594
|
+
return err(
|
|
2595
|
+
"nav_unavailable",
|
|
2596
|
+
`No NAV snapshot available for ${safeSymbol} yet. The nav-worker may not have written one \u2014 retry shortly, or use the dashboard /trade page directly.`
|
|
2597
|
+
);
|
|
2598
|
+
}
|
|
2599
|
+
let navUsd6;
|
|
873
2600
|
try {
|
|
874
|
-
|
|
875
|
-
surface: input.surface,
|
|
876
|
-
eventTypes: input.eventTypes?.join(","),
|
|
877
|
-
since: input.since,
|
|
878
|
-
until: input.until,
|
|
879
|
-
cursor: input.cursor,
|
|
880
|
-
limit: input.limit
|
|
881
|
-
});
|
|
882
|
-
return ok(data);
|
|
2601
|
+
navUsd6 = parseDecimalToUsd6(token.latest_nav.nav);
|
|
883
2602
|
} catch (e) {
|
|
884
|
-
return
|
|
2603
|
+
return err(
|
|
2604
|
+
"nav_malformed",
|
|
2605
|
+
`NAV for ${safeSymbol} is not a valid decimal price. Open the dashboard /trade page directly.`
|
|
2606
|
+
);
|
|
2607
|
+
}
|
|
2608
|
+
if (navUsd6 <= 0n) {
|
|
2609
|
+
return err(
|
|
2610
|
+
"nav_non_positive",
|
|
2611
|
+
`NAV for ${safeSymbol} is non-positive. Cannot quote a buy.`
|
|
2612
|
+
);
|
|
2613
|
+
}
|
|
2614
|
+
let notionalUsd6;
|
|
2615
|
+
try {
|
|
2616
|
+
notionalUsd6 = parseDecimalToUsd6(input.amountUsdc);
|
|
2617
|
+
} catch (e) {
|
|
2618
|
+
return err(
|
|
2619
|
+
"invalid_amount",
|
|
2620
|
+
`amountUsdc "${input.amountUsdc}" is not a valid decimal mhUSDC amount.`
|
|
2621
|
+
);
|
|
2622
|
+
}
|
|
2623
|
+
if (notionalUsd6 <= 0n) {
|
|
2624
|
+
return err(
|
|
2625
|
+
"invalid_amount",
|
|
2626
|
+
"amountUsdc must be greater than zero."
|
|
2627
|
+
);
|
|
2628
|
+
}
|
|
2629
|
+
const shares = computeSharesFromUsd6(notionalUsd6, navUsd6);
|
|
2630
|
+
if (shares <= 0n) {
|
|
2631
|
+
const navDisplay2 = formatUsd6AsDecimal(navUsd6);
|
|
2632
|
+
return err(
|
|
2633
|
+
"amount_too_small_for_share",
|
|
2634
|
+
`${input.amountUsdc} mhUSDC isn't enough to buy 1 share of ${safeSymbol} at the current NAV of $${navDisplay2}/share. Need at least ${navDisplay2} mhUSDC to buy 1 share. Ask the user for a larger amount, or chain muhaven.cash.wrap first if they're short on mhUSDC.`
|
|
2635
|
+
);
|
|
2636
|
+
}
|
|
2637
|
+
const effectiveNotionalUsd6 = shares * navUsd6;
|
|
2638
|
+
const effectiveNotionalDisplay = formatUsd6AsDecimal(effectiveNotionalUsd6);
|
|
2639
|
+
const navDisplay = formatUsd6AsDecimal(navUsd6);
|
|
2640
|
+
const sharesStr = shares.toString();
|
|
2641
|
+
let pathDFallbackReason;
|
|
2642
|
+
let pathDSubmittedUserOpHash;
|
|
2643
|
+
const pathD = await attemptPathD(
|
|
2644
|
+
{ shares, tokenAddress: token.address, tokenSymbol: token.symbol },
|
|
2645
|
+
deps
|
|
2646
|
+
);
|
|
2647
|
+
if (pathD.kind === "ok") {
|
|
2648
|
+
return ok(pathD.data);
|
|
2649
|
+
}
|
|
2650
|
+
if (pathD.kind === "fallback") {
|
|
2651
|
+
pathDFallbackReason = pathD.reason;
|
|
2652
|
+
if (pathD.submittedUserOpHash) {
|
|
2653
|
+
pathDSubmittedUserOpHash = pathD.submittedUserOpHash;
|
|
2654
|
+
}
|
|
885
2655
|
}
|
|
886
|
-
}
|
|
887
|
-
function buildPositionDeeplink(dashboardBaseUrl, action, params) {
|
|
888
|
-
const base = dashboardBaseUrl.replace(/\/+$/, "");
|
|
889
|
-
const path = action === "buy" || action === "sell" ? "/trade" : action === "claim" ? "/yields" : "/cash";
|
|
890
|
-
const search = new URLSearchParams();
|
|
891
|
-
if (action === "buy" || action === "sell") search.set("mode", action);
|
|
892
|
-
for (const [k, v] of Object.entries(params)) search.set(k, v);
|
|
893
|
-
search.set("from", "mcp");
|
|
894
|
-
return `${base}${path}?${search.toString()}`;
|
|
895
|
-
}
|
|
896
|
-
function resolveDashboardBaseUrl(deps) {
|
|
897
|
-
return deps.dashboardBaseUrl ?? "https://muhaven.app";
|
|
898
|
-
}
|
|
899
|
-
async function positionBuy(input, deps) {
|
|
900
2656
|
const dashboardUrl = buildPositionDeeplink(resolveDashboardBaseUrl(deps), "buy", {
|
|
901
|
-
token:
|
|
902
|
-
amount:
|
|
2657
|
+
token: token.symbol,
|
|
2658
|
+
amount: sharesStr
|
|
903
2659
|
});
|
|
904
2660
|
return ok({
|
|
905
2661
|
dashboardUrl,
|
|
906
2662
|
action: "buy",
|
|
907
|
-
instructions: `Open this link to review and authorize the buy of ${
|
|
2663
|
+
instructions: `Open this link to review and authorize the buy of ${sharesStr} ${safeSymbol} shares (~${effectiveNotionalDisplay} mhUSDC at current NAV $${navDisplay}/share):
|
|
908
2664
|
${dashboardUrl}`,
|
|
909
|
-
echo: {
|
|
2665
|
+
echo: {
|
|
2666
|
+
action: "buy",
|
|
2667
|
+
token: token.symbol,
|
|
2668
|
+
amount: sharesStr,
|
|
2669
|
+
shares: sharesStr,
|
|
2670
|
+
// Carry the original request + the conversion math so the LLM
|
|
2671
|
+
// (and a human auditor reading the trace) can see why the URL
|
|
2672
|
+
// shows the share count instead of the user-stated notional.
|
|
2673
|
+
amountUsdc: input.amountUsdc,
|
|
2674
|
+
effectiveNotionalUsd6: effectiveNotionalUsd6.toString(),
|
|
2675
|
+
navUsd6: navUsd6.toString(),
|
|
2676
|
+
...pathDFallbackReason ? { pathDFallbackReason } : {},
|
|
2677
|
+
...pathDSubmittedUserOpHash ? { pathDSubmittedUserOpHash } : {}
|
|
2678
|
+
}
|
|
910
2679
|
});
|
|
911
2680
|
}
|
|
912
2681
|
async function positionSell(input, deps) {
|
|
@@ -1148,6 +2917,10 @@ var HANDLERS = {
|
|
|
1148
2917
|
schema: ReadAuditInputSchema,
|
|
1149
2918
|
handler: readAudit
|
|
1150
2919
|
},
|
|
2920
|
+
"muhaven.read.activity": {
|
|
2921
|
+
schema: ReadActivityInputSchema,
|
|
2922
|
+
handler: readActivity
|
|
2923
|
+
},
|
|
1151
2924
|
"muhaven.position.buy": {
|
|
1152
2925
|
schema: PositionBuyInputSchema,
|
|
1153
2926
|
handler: positionBuy
|
|
@@ -1252,14 +3025,20 @@ var SERVER_NAME = "@muhaven/mcp";
|
|
|
1252
3025
|
var SERVER_VERSION = resolveServerVersion();
|
|
1253
3026
|
function resolveServerVersion() {
|
|
1254
3027
|
{
|
|
1255
|
-
return "0.2.
|
|
3028
|
+
return "0.2.2";
|
|
1256
3029
|
}
|
|
1257
3030
|
}
|
|
1258
3031
|
function toJsonInputSchema(schema) {
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
3032
|
+
const json = zodToJsonSchema.zodToJsonSchema(schema, {
|
|
3033
|
+
target: "jsonSchema7",
|
|
3034
|
+
$refStrategy: "none",
|
|
3035
|
+
removeAdditionalStrategy: "strict"
|
|
3036
|
+
});
|
|
3037
|
+
if ("$schema" in json) {
|
|
3038
|
+
const { $schema: _drop, ...rest } = json;
|
|
3039
|
+
return rest;
|
|
3040
|
+
}
|
|
3041
|
+
return json;
|
|
1263
3042
|
}
|
|
1264
3043
|
async function loadPinnedToolHashes() {
|
|
1265
3044
|
const here = path.dirname(url.fileURLToPath(importMetaUrl));
|
|
@@ -1269,7 +3048,7 @@ async function loadPinnedToolHashes() {
|
|
|
1269
3048
|
];
|
|
1270
3049
|
for (const path of candidates) {
|
|
1271
3050
|
try {
|
|
1272
|
-
const raw = await promises.readFile(path, "utf8");
|
|
3051
|
+
const raw = await promises$1.readFile(path, "utf8");
|
|
1273
3052
|
const parsed = JSON.parse(raw);
|
|
1274
3053
|
if (Array.isArray(parsed?.tools)) return parsed.tools;
|
|
1275
3054
|
} catch {
|
|
@@ -1328,8 +3107,12 @@ function buildMcpServer(opts) {
|
|
|
1328
3107
|
const result = await entry.handler(parsed, {
|
|
1329
3108
|
backend: opts.backend,
|
|
1330
3109
|
broker: opts.broker,
|
|
3110
|
+
bundler: opts.bundler,
|
|
1331
3111
|
surface: "mcp",
|
|
1332
|
-
dashboardBaseUrl: opts.dashboardBaseUrl
|
|
3112
|
+
dashboardBaseUrl: opts.dashboardBaseUrl,
|
|
3113
|
+
chainId: opts.chainId,
|
|
3114
|
+
subscriptionAddress: opts.subscriptionAddress,
|
|
3115
|
+
entryPointAddress: opts.entryPointAddress
|
|
1333
3116
|
});
|
|
1334
3117
|
return toolJsonResponse(result);
|
|
1335
3118
|
} catch (err2) {
|
|
@@ -1380,6 +3163,11 @@ async function runMcpStdioCli(opts = {}) {
|
|
|
1380
3163
|
timeoutMs: config.requestTimeoutMs,
|
|
1381
3164
|
allowedHosts: config.allowedBackendHosts
|
|
1382
3165
|
});
|
|
3166
|
+
const bundler = config.bundlerUrl ? new BundlerClient({
|
|
3167
|
+
endpoint: config.bundlerUrl,
|
|
3168
|
+
requestTimeoutMs: config.bundlerTimeoutMs,
|
|
3169
|
+
expectedChainId: config.chainId
|
|
3170
|
+
}) : void 0;
|
|
1383
3171
|
const baseRegistry = selectRegistry(config.readOnly);
|
|
1384
3172
|
const registry = opts.filterRegistry ? opts.filterRegistry(baseRegistry) : baseRegistry;
|
|
1385
3173
|
if (registry.length === 0) {
|
|
@@ -1392,8 +3180,20 @@ async function runMcpStdioCli(opts = {}) {
|
|
|
1392
3180
|
registry,
|
|
1393
3181
|
backend,
|
|
1394
3182
|
broker: config.readOnly ? void 0 : broker,
|
|
1395
|
-
|
|
3183
|
+
bundler: config.readOnly ? void 0 : bundler,
|
|
3184
|
+
dashboardBaseUrl: config.dashboardBaseUrl,
|
|
3185
|
+
chainId: config.chainId,
|
|
3186
|
+
subscriptionAddress: config.subscriptionAddress,
|
|
3187
|
+
entryPointAddress: config.entryPointAddress
|
|
1396
3188
|
});
|
|
3189
|
+
if (bundler && !config.readOnly) {
|
|
3190
|
+
void bundler.assertChainId().catch((err2) => {
|
|
3191
|
+
process.stderr.write(
|
|
3192
|
+
`[muhaven-mcp] bundler chain-id assert failed: ${err2 instanceof Error ? err2.message : String(err2)} \u2014 Path D autonomous-buys will fail until MUHAVEN_BUNDLER_URL + MUHAVEN_CHAIN_ID agree
|
|
3193
|
+
`
|
|
3194
|
+
);
|
|
3195
|
+
});
|
|
3196
|
+
}
|
|
1397
3197
|
const transport = new stdio_js.StdioServerTransport();
|
|
1398
3198
|
await server.connect(transport);
|
|
1399
3199
|
await new Promise((resolve) => {
|
|
@@ -1498,140 +3298,52 @@ var DeviceFlowClient = class {
|
|
|
1498
3298
|
const body2 = await safeJson(res);
|
|
1499
3299
|
throw new DeviceFlowAbortedError({ code: "invalid_response", status: res.status, body: body2 });
|
|
1500
3300
|
}
|
|
1501
|
-
const body = await safeJson(res);
|
|
1502
|
-
if (!body || !body.deviceCode || !body.userCode) {
|
|
1503
|
-
throw new DeviceFlowAbortedError({ code: "invalid_response", status: res.status, body });
|
|
1504
|
-
}
|
|
1505
|
-
const verificationUri = `${trim(this.options.dashboardBaseUrl)}/link`;
|
|
1506
|
-
const verificationUriComplete = `${verificationUri}?code=${encodeURIComponent(body.userCode)}`;
|
|
1507
|
-
return {
|
|
1508
|
-
deviceCode: body.deviceCode,
|
|
1509
|
-
userCode: body.userCode,
|
|
1510
|
-
verificationUri,
|
|
1511
|
-
verificationUriComplete,
|
|
1512
|
-
expiresInSec: body.expiresInSec ?? 300,
|
|
1513
|
-
pollIntervalSec: body.pollIntervalSec ?? Math.floor(DEFAULT_POLL_INTERVAL_MS / 1e3)
|
|
1514
|
-
};
|
|
1515
|
-
}
|
|
1516
|
-
async pollOnce(deviceCode) {
|
|
1517
|
-
const url = new URL("/api/v1/auth/device/token", this.options.backendBaseUrl);
|
|
1518
|
-
let res;
|
|
1519
|
-
try {
|
|
1520
|
-
res = await this.fetchImpl(url, {
|
|
1521
|
-
method: "POST",
|
|
1522
|
-
headers: { "content-type": "application/json", accept: "application/json" },
|
|
1523
|
-
body: JSON.stringify({ deviceCode })
|
|
1524
|
-
});
|
|
1525
|
-
} catch (err2) {
|
|
1526
|
-
throw new DeviceFlowAbortedError({ code: "network", cause: err2 });
|
|
1527
|
-
}
|
|
1528
|
-
if (res.status === 429) {
|
|
1529
|
-
return { state: "pending" };
|
|
1530
|
-
}
|
|
1531
|
-
const body = await safeJson(res);
|
|
1532
|
-
if (!body || typeof body.state !== "string") {
|
|
1533
|
-
throw new DeviceFlowAbortedError({ code: "invalid_response", status: res.status, body });
|
|
1534
|
-
}
|
|
1535
|
-
return body;
|
|
1536
|
-
}
|
|
1537
|
-
};
|
|
1538
|
-
function trim(s) {
|
|
1539
|
-
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
1540
|
-
}
|
|
1541
|
-
async function safeJson(res) {
|
|
1542
|
-
try {
|
|
1543
|
-
return await res.json();
|
|
1544
|
-
} catch {
|
|
1545
|
-
return null;
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
// src/broker/protocol.ts
|
|
1550
|
-
var BROKER_PROTOCOL_VERSION = "0.3.0";
|
|
1551
|
-
var HASH_HEX_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
1552
|
-
var JWT_RE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
1553
|
-
function isHashHex(value) {
|
|
1554
|
-
return typeof value === "string" && HASH_HEX_RE.test(value);
|
|
1555
|
-
}
|
|
1556
|
-
function isJwtShape(value) {
|
|
1557
|
-
return typeof value === "string" && value.length <= 8192 && JWT_RE.test(value);
|
|
1558
|
-
}
|
|
1559
|
-
function parseBrokerRequest(line) {
|
|
1560
|
-
let parsed;
|
|
1561
|
-
try {
|
|
1562
|
-
parsed = JSON.parse(line);
|
|
1563
|
-
} catch {
|
|
1564
|
-
return { type: "error", code: "invalid_request", message: "request is not valid JSON" };
|
|
1565
|
-
}
|
|
1566
|
-
if (typeof parsed !== "object" || parsed === null) {
|
|
1567
|
-
return { type: "error", code: "invalid_request", message: "request must be a JSON object" };
|
|
1568
|
-
}
|
|
1569
|
-
const obj = parsed;
|
|
1570
|
-
switch (obj.type) {
|
|
1571
|
-
case "hello":
|
|
1572
|
-
return { type: "hello" };
|
|
1573
|
-
case "sign_hash": {
|
|
1574
|
-
const hash = obj.hash;
|
|
1575
|
-
if (!isHashHex(hash)) {
|
|
1576
|
-
return {
|
|
1577
|
-
type: "error",
|
|
1578
|
-
code: "invalid_request",
|
|
1579
|
-
message: "sign_hash.hash must be a 0x-prefixed 32-byte hex string"
|
|
1580
|
-
};
|
|
1581
|
-
}
|
|
1582
|
-
const intent = obj.intent;
|
|
1583
|
-
const intentValid = intent === void 0 || typeof intent === "object" && intent !== null && typeof intent.tool === "string";
|
|
1584
|
-
if (!intentValid) {
|
|
1585
|
-
return {
|
|
1586
|
-
type: "error",
|
|
1587
|
-
code: "invalid_request",
|
|
1588
|
-
message: "sign_hash.intent.tool must be a string when provided"
|
|
1589
|
-
};
|
|
1590
|
-
}
|
|
1591
|
-
return {
|
|
1592
|
-
type: "sign_hash",
|
|
1593
|
-
hash,
|
|
1594
|
-
...intent === void 0 ? {} : { intent }
|
|
1595
|
-
};
|
|
3301
|
+
const body = await safeJson(res);
|
|
3302
|
+
if (!body || !body.deviceCode || !body.userCode) {
|
|
3303
|
+
throw new DeviceFlowAbortedError({ code: "invalid_response", status: res.status, body });
|
|
1596
3304
|
}
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
};
|
|
3305
|
+
const verificationUri = `${trim(this.options.dashboardBaseUrl)}/link`;
|
|
3306
|
+
const verificationUriComplete = `${verificationUri}?code=${encodeURIComponent(body.userCode)}`;
|
|
3307
|
+
return {
|
|
3308
|
+
deviceCode: body.deviceCode,
|
|
3309
|
+
userCode: body.userCode,
|
|
3310
|
+
verificationUri,
|
|
3311
|
+
verificationUriComplete,
|
|
3312
|
+
expiresInSec: body.expiresInSec ?? 300,
|
|
3313
|
+
pollIntervalSec: body.pollIntervalSec ?? Math.floor(DEFAULT_POLL_INTERVAL_MS / 1e3)
|
|
3314
|
+
};
|
|
3315
|
+
}
|
|
3316
|
+
async pollOnce(deviceCode) {
|
|
3317
|
+
const url = new URL("/api/v1/auth/device/token", this.options.backendBaseUrl);
|
|
3318
|
+
let res;
|
|
3319
|
+
try {
|
|
3320
|
+
res = await this.fetchImpl(url, {
|
|
3321
|
+
method: "POST",
|
|
3322
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
3323
|
+
body: JSON.stringify({ deviceCode })
|
|
3324
|
+
});
|
|
3325
|
+
} catch (err2) {
|
|
3326
|
+
throw new DeviceFlowAbortedError({ code: "network", cause: err2 });
|
|
1620
3327
|
}
|
|
1621
|
-
|
|
1622
|
-
return {
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
message: `unsupported request type: ${String(obj.type)}`
|
|
1630
|
-
};
|
|
3328
|
+
if (res.status === 429) {
|
|
3329
|
+
return { state: "pending" };
|
|
3330
|
+
}
|
|
3331
|
+
const body = await safeJson(res);
|
|
3332
|
+
if (!body || typeof body.state !== "string") {
|
|
3333
|
+
throw new DeviceFlowAbortedError({ code: "invalid_response", status: res.status, body });
|
|
3334
|
+
}
|
|
3335
|
+
return body;
|
|
1631
3336
|
}
|
|
3337
|
+
};
|
|
3338
|
+
function trim(s) {
|
|
3339
|
+
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
1632
3340
|
}
|
|
1633
|
-
function
|
|
1634
|
-
|
|
3341
|
+
async function safeJson(res) {
|
|
3342
|
+
try {
|
|
3343
|
+
return await res.json();
|
|
3344
|
+
} catch {
|
|
3345
|
+
return null;
|
|
3346
|
+
}
|
|
1635
3347
|
}
|
|
1636
3348
|
var MissingSessionKeyError = class extends Error {
|
|
1637
3349
|
constructor() {
|
|
@@ -1647,6 +3359,9 @@ var NullSigner = class {
|
|
|
1647
3359
|
async signHash(_hash) {
|
|
1648
3360
|
throw new MissingSessionKeyError();
|
|
1649
3361
|
}
|
|
3362
|
+
async signRawMessage(_hash) {
|
|
3363
|
+
throw new MissingSessionKeyError();
|
|
3364
|
+
}
|
|
1650
3365
|
};
|
|
1651
3366
|
var ViemSigner = class {
|
|
1652
3367
|
account;
|
|
@@ -1659,6 +3374,9 @@ var ViemSigner = class {
|
|
|
1659
3374
|
async signHash(hash) {
|
|
1660
3375
|
return this.account.sign({ hash });
|
|
1661
3376
|
}
|
|
3377
|
+
async signRawMessage(hash) {
|
|
3378
|
+
return this.account.signMessage({ message: { raw: hash } });
|
|
3379
|
+
}
|
|
1662
3380
|
};
|
|
1663
3381
|
var KEYRING_SERVICE = "muhaven.mcp";
|
|
1664
3382
|
var KEYRING_ACCOUNT = "jwt";
|
|
@@ -1723,14 +3441,14 @@ var FileKeystore = class {
|
|
|
1723
3441
|
}
|
|
1724
3442
|
async set(record) {
|
|
1725
3443
|
const parent = path.dirname(this.path);
|
|
1726
|
-
await promises.mkdir(parent, { recursive: true, mode: 448 });
|
|
1727
|
-
await promises.chmod(parent, 448).catch(() => void 0);
|
|
1728
|
-
await promises.writeFile(this.path, JSON.stringify(record), { mode: 384 });
|
|
1729
|
-
await promises.chmod(this.path, 384).catch(() => void 0);
|
|
3444
|
+
await promises$1.mkdir(parent, { recursive: true, mode: 448 });
|
|
3445
|
+
await promises$1.chmod(parent, 448).catch(() => void 0);
|
|
3446
|
+
await promises$1.writeFile(this.path, JSON.stringify(record), { mode: 384 });
|
|
3447
|
+
await promises$1.chmod(this.path, 384).catch(() => void 0);
|
|
1730
3448
|
}
|
|
1731
3449
|
async get() {
|
|
1732
3450
|
try {
|
|
1733
|
-
const raw = await promises.readFile(this.path, "utf8");
|
|
3451
|
+
const raw = await promises$1.readFile(this.path, "utf8");
|
|
1734
3452
|
return parseRecord(raw);
|
|
1735
3453
|
} catch (err2) {
|
|
1736
3454
|
if (err2.code === "ENOENT") return null;
|
|
@@ -1739,7 +3457,7 @@ var FileKeystore = class {
|
|
|
1739
3457
|
}
|
|
1740
3458
|
async clear() {
|
|
1741
3459
|
try {
|
|
1742
|
-
await promises.unlink(this.path);
|
|
3460
|
+
await promises$1.unlink(this.path);
|
|
1743
3461
|
} catch (err2) {
|
|
1744
3462
|
if (err2.code === "ENOENT") return;
|
|
1745
3463
|
throw new KeystoreError("file_clear_failed", asMessage(err2), err2);
|
|
@@ -1815,11 +3533,293 @@ async function openKeystore(options = {}) {
|
|
|
1815
3533
|
}
|
|
1816
3534
|
return { keystore: new OsKeystore(entry), fallbackReason: null };
|
|
1817
3535
|
}
|
|
3536
|
+
var PolicyStoreError = class extends Error {
|
|
3537
|
+
constructor(code, message, cause) {
|
|
3538
|
+
super(message);
|
|
3539
|
+
this.code = code;
|
|
3540
|
+
this.cause = cause;
|
|
3541
|
+
this.name = "PolicyStoreError";
|
|
3542
|
+
}
|
|
3543
|
+
code;
|
|
3544
|
+
cause;
|
|
3545
|
+
};
|
|
3546
|
+
var SESSION_ID_RE2 = /^[A-Za-z0-9_-]{1,128}$/;
|
|
3547
|
+
var UINT256_MAX_LOCAL = (1n << 256n) - 1n;
|
|
3548
|
+
function validateSessionId(sessionId) {
|
|
3549
|
+
if (!SESSION_ID_RE2.test(sessionId)) {
|
|
3550
|
+
throw new PolicyStoreError(
|
|
3551
|
+
"invalid_session_id",
|
|
3552
|
+
`sessionId "${sessionId}" must be 1-128 chars [A-Za-z0-9_-] (path-traversal guard)`
|
|
3553
|
+
);
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
var FilePolicyStore = class {
|
|
3557
|
+
constructor(dir) {
|
|
3558
|
+
this.dir = dir;
|
|
3559
|
+
}
|
|
3560
|
+
dir;
|
|
3561
|
+
static defaultDir() {
|
|
3562
|
+
return path.join(os.homedir(), ".muhaven", "policy-snapshots");
|
|
3563
|
+
}
|
|
3564
|
+
snapshotPath(sessionId) {
|
|
3565
|
+
return path.join(this.dir, `${sessionId}.json`);
|
|
3566
|
+
}
|
|
3567
|
+
async get(sessionId, nowSec) {
|
|
3568
|
+
validateSessionId(sessionId);
|
|
3569
|
+
let raw;
|
|
3570
|
+
try {
|
|
3571
|
+
raw = await promises$1.readFile(this.snapshotPath(sessionId), "utf8");
|
|
3572
|
+
} catch (err2) {
|
|
3573
|
+
if (err2.code === "ENOENT") return null;
|
|
3574
|
+
throw new PolicyStoreError(
|
|
3575
|
+
"read_failed",
|
|
3576
|
+
`failed to read snapshot ${sessionId}: ${asMessage2(err2)}`,
|
|
3577
|
+
err2
|
|
3578
|
+
);
|
|
3579
|
+
}
|
|
3580
|
+
let parsed;
|
|
3581
|
+
try {
|
|
3582
|
+
const obj = JSON.parse(raw);
|
|
3583
|
+
parsed = coerceFromDisk(obj);
|
|
3584
|
+
} catch (err2) {
|
|
3585
|
+
throw new PolicyStoreError(
|
|
3586
|
+
"malformed_record",
|
|
3587
|
+
`snapshot ${sessionId} is not valid JSON: ${asMessage2(err2)}`,
|
|
3588
|
+
err2
|
|
3589
|
+
);
|
|
3590
|
+
}
|
|
3591
|
+
if (parsed.validUntilSec <= nowSec) return null;
|
|
3592
|
+
return parsed;
|
|
3593
|
+
}
|
|
3594
|
+
async put(snapshot) {
|
|
3595
|
+
validateSessionId(snapshot.sessionId);
|
|
3596
|
+
const dest = this.snapshotPath(snapshot.sessionId);
|
|
3597
|
+
const tmp = `${dest}.tmp-${crypto.randomBytes(6).toString("hex")}`;
|
|
3598
|
+
try {
|
|
3599
|
+
await promises$1.mkdir(this.dir, { recursive: true, mode: 448 });
|
|
3600
|
+
await promises$1.chmod(this.dir, 448).catch(() => void 0);
|
|
3601
|
+
await promises$1.writeFile(tmp, JSON.stringify(snapshot), { mode: 384 });
|
|
3602
|
+
await promises$1.chmod(tmp, 384).catch(() => void 0);
|
|
3603
|
+
await promises$1.rename(tmp, dest);
|
|
3604
|
+
} catch (err2) {
|
|
3605
|
+
await promises$1.unlink(tmp).catch(() => void 0);
|
|
3606
|
+
throw new PolicyStoreError(
|
|
3607
|
+
"write_failed",
|
|
3608
|
+
`failed to write snapshot ${snapshot.sessionId}: ${asMessage2(err2)}`,
|
|
3609
|
+
err2
|
|
3610
|
+
);
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
async delete(sessionId) {
|
|
3614
|
+
validateSessionId(sessionId);
|
|
3615
|
+
try {
|
|
3616
|
+
await promises$1.unlink(this.snapshotPath(sessionId));
|
|
3617
|
+
} catch (err2) {
|
|
3618
|
+
if (err2.code === "ENOENT") return;
|
|
3619
|
+
throw new PolicyStoreError(
|
|
3620
|
+
"delete_failed",
|
|
3621
|
+
`failed to delete snapshot ${sessionId}: ${asMessage2(err2)}`,
|
|
3622
|
+
err2
|
|
3623
|
+
);
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
async list() {
|
|
3627
|
+
let entries;
|
|
3628
|
+
try {
|
|
3629
|
+
entries = await promises$1.readdir(this.dir);
|
|
3630
|
+
} catch (err2) {
|
|
3631
|
+
if (err2.code === "ENOENT") return [];
|
|
3632
|
+
throw new PolicyStoreError(
|
|
3633
|
+
"read_failed",
|
|
3634
|
+
`failed to enumerate snapshot dir: ${asMessage2(err2)}`,
|
|
3635
|
+
err2
|
|
3636
|
+
);
|
|
3637
|
+
}
|
|
3638
|
+
const out = [];
|
|
3639
|
+
for (const entry of entries) {
|
|
3640
|
+
if (!entry.endsWith(".json")) continue;
|
|
3641
|
+
const sessionId = entry.slice(0, -".json".length);
|
|
3642
|
+
if (!SESSION_ID_RE2.test(sessionId)) continue;
|
|
3643
|
+
try {
|
|
3644
|
+
const raw = await promises$1.readFile(path.join(this.dir, entry), "utf8");
|
|
3645
|
+
out.push(coerceFromDisk(JSON.parse(raw)));
|
|
3646
|
+
} catch {
|
|
3647
|
+
continue;
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
return out;
|
|
3651
|
+
}
|
|
3652
|
+
async activeSessionId(activeSignerAddress, nowSec) {
|
|
3653
|
+
const all = await this.list();
|
|
3654
|
+
const needle = activeSignerAddress.toLowerCase();
|
|
3655
|
+
const matches = all.filter(
|
|
3656
|
+
(s) => s.validUntilSec > nowSec && s.signerAddress.toLowerCase() === needle
|
|
3657
|
+
);
|
|
3658
|
+
return matches.length === 1 ? matches[0].sessionId : null;
|
|
3659
|
+
}
|
|
3660
|
+
};
|
|
3661
|
+
function coerceFromDisk(obj) {
|
|
3662
|
+
if (typeof obj !== "object" || obj === null) throw new Error("snapshot not an object");
|
|
3663
|
+
const o = obj;
|
|
3664
|
+
if (typeof o.sessionId !== "string" || !SESSION_ID_RE2.test(o.sessionId)) {
|
|
3665
|
+
throw new Error("snapshot.sessionId malformed");
|
|
3666
|
+
}
|
|
3667
|
+
if (o.mode !== "scoped") throw new Error('snapshot.mode must be "scoped"');
|
|
3668
|
+
if (typeof o.signerAddress !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(o.signerAddress)) {
|
|
3669
|
+
throw new Error("snapshot.signerAddress malformed");
|
|
3670
|
+
}
|
|
3671
|
+
if (!Array.isArray(o.targetContracts) || !o.targetContracts.every((t) => typeof t === "string" && /^0x[0-9a-fA-F]{40}$/.test(t))) {
|
|
3672
|
+
throw new Error("snapshot.targetContracts malformed");
|
|
3673
|
+
}
|
|
3674
|
+
if (!Array.isArray(o.selectorCaps) || o.selectorCaps.length === 0) {
|
|
3675
|
+
throw new Error("snapshot.selectorCaps malformed");
|
|
3676
|
+
}
|
|
3677
|
+
const caps = [];
|
|
3678
|
+
for (const c of o.selectorCaps) {
|
|
3679
|
+
if (typeof c !== "object" || c === null) throw new Error("selectorCap not an object");
|
|
3680
|
+
const cap = c;
|
|
3681
|
+
if (typeof cap.selector !== "string" || !/^0x[0-9a-fA-F]{8}$/.test(cap.selector)) {
|
|
3682
|
+
throw new Error("selectorCap.selector malformed");
|
|
3683
|
+
}
|
|
3684
|
+
const indexNull = cap.capArgIndex === null;
|
|
3685
|
+
const amountNull = cap.maxAmount === null;
|
|
3686
|
+
if (indexNull !== amountNull) {
|
|
3687
|
+
throw new Error("selectorCap.capArgIndex/maxAmount must both be null or both non-null");
|
|
3688
|
+
}
|
|
3689
|
+
if (!indexNull) {
|
|
3690
|
+
if (typeof cap.capArgIndex !== "number" || !Number.isInteger(cap.capArgIndex) || cap.capArgIndex < 0 || cap.capArgIndex > 31) {
|
|
3691
|
+
throw new Error("selectorCap.capArgIndex must be an integer in [0, 31]");
|
|
3692
|
+
}
|
|
3693
|
+
if (typeof cap.maxAmount !== "string" || !/^(0|[1-9][0-9]{0,77})$/.test(cap.maxAmount)) {
|
|
3694
|
+
throw new Error("selectorCap.maxAmount malformed (length)");
|
|
3695
|
+
}
|
|
3696
|
+
if (BigInt(cap.maxAmount) > UINT256_MAX_LOCAL) {
|
|
3697
|
+
throw new Error("selectorCap.maxAmount exceeds uint256 max");
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
caps.push({
|
|
3701
|
+
selector: cap.selector.toLowerCase(),
|
|
3702
|
+
capArgIndex: indexNull ? null : cap.capArgIndex,
|
|
3703
|
+
maxAmount: indexNull ? null : cap.maxAmount
|
|
3704
|
+
});
|
|
3705
|
+
}
|
|
3706
|
+
if (typeof o.validUntilSec !== "number" || o.validUntilSec <= 0) {
|
|
3707
|
+
throw new Error("snapshot.validUntilSec malformed");
|
|
3708
|
+
}
|
|
3709
|
+
if (typeof o.mintedAtSec !== "number" || o.mintedAtSec <= 0) {
|
|
3710
|
+
throw new Error("snapshot.mintedAtSec malformed");
|
|
3711
|
+
}
|
|
3712
|
+
if (o.consentActionHash !== void 0) {
|
|
3713
|
+
if (typeof o.consentActionHash !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(o.consentActionHash)) {
|
|
3714
|
+
throw new Error("snapshot.consentActionHash malformed");
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
if (o.consentTextSha256 !== void 0) {
|
|
3718
|
+
if (typeof o.consentTextSha256 !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(o.consentTextSha256)) {
|
|
3719
|
+
throw new Error("snapshot.consentTextSha256 malformed");
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
if (o.permissionId !== void 0) {
|
|
3723
|
+
if (typeof o.permissionId !== "string" || !/^0x[0-9a-fA-F]{8}$/.test(o.permissionId)) {
|
|
3724
|
+
throw new Error("snapshot.permissionId malformed (must be 0x-prefixed 4-byte hex)");
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
return {
|
|
3728
|
+
sessionId: o.sessionId,
|
|
3729
|
+
mode: "scoped",
|
|
3730
|
+
signerAddress: o.signerAddress.toLowerCase(),
|
|
3731
|
+
targetContracts: o.targetContracts.map(
|
|
3732
|
+
(t) => t.toLowerCase()
|
|
3733
|
+
),
|
|
3734
|
+
selectorCaps: caps,
|
|
3735
|
+
validUntilSec: o.validUntilSec,
|
|
3736
|
+
mintedAtSec: o.mintedAtSec,
|
|
3737
|
+
...o.consentActionHash === void 0 ? {} : { consentActionHash: o.consentActionHash.toLowerCase() },
|
|
3738
|
+
...o.consentTextSha256 === void 0 ? {} : { consentTextSha256: o.consentTextSha256.toLowerCase() },
|
|
3739
|
+
...o.permissionId === void 0 ? {} : { permissionId: o.permissionId.toLowerCase() }
|
|
3740
|
+
};
|
|
3741
|
+
}
|
|
3742
|
+
function asMessage2(err2) {
|
|
3743
|
+
return err2 instanceof Error ? err2.message : String(err2);
|
|
3744
|
+
}
|
|
3745
|
+
function decodeUint256ArgAt(callDataHex, wordIndex) {
|
|
3746
|
+
if (!Number.isInteger(wordIndex) || wordIndex < 0) {
|
|
3747
|
+
throw new Error(`wordIndex must be a non-negative integer (got ${wordIndex})`);
|
|
3748
|
+
}
|
|
3749
|
+
const requiredLen = 2 + 8 + (wordIndex + 1) * 64;
|
|
3750
|
+
if (callDataHex.length < requiredLen) {
|
|
3751
|
+
throw new Error(
|
|
3752
|
+
`callData length ${callDataHex.length} too short to carry word ${wordIndex} (need \u2265${requiredLen} chars)`
|
|
3753
|
+
);
|
|
3754
|
+
}
|
|
3755
|
+
const wordStart = 2 + 8 + wordIndex * 64;
|
|
3756
|
+
const argHex = callDataHex.slice(wordStart, wordStart + 64);
|
|
3757
|
+
return BigInt(`0x${argHex}`);
|
|
3758
|
+
}
|
|
3759
|
+
function selectorOf(callDataHex) {
|
|
3760
|
+
return callDataHex.slice(0, 10).toLowerCase();
|
|
3761
|
+
}
|
|
3762
|
+
function checkPolicy(input) {
|
|
3763
|
+
const { snapshot, innerCall, activeSigner, nowSec } = input;
|
|
3764
|
+
if (snapshot.validUntilSec <= nowSec) {
|
|
3765
|
+
return {
|
|
3766
|
+
ok: false,
|
|
3767
|
+
code: "scope_violation",
|
|
3768
|
+
message: `snapshot ${snapshot.sessionId} expired at ${snapshot.validUntilSec} (now ${nowSec})`
|
|
3769
|
+
};
|
|
3770
|
+
}
|
|
3771
|
+
if (snapshot.signerAddress.toLowerCase() !== activeSigner.toLowerCase()) {
|
|
3772
|
+
return {
|
|
3773
|
+
ok: false,
|
|
3774
|
+
code: "policy_violation",
|
|
3775
|
+
message: `snapshot ${snapshot.sessionId} bound to signer ${snapshot.signerAddress}, broker active signer is ${activeSigner}`
|
|
3776
|
+
};
|
|
3777
|
+
}
|
|
3778
|
+
const targetLower = innerCall.target.toLowerCase();
|
|
3779
|
+
const targetMatch = snapshot.targetContracts.some((t) => t === targetLower);
|
|
3780
|
+
if (!targetMatch) {
|
|
3781
|
+
return {
|
|
3782
|
+
ok: false,
|
|
3783
|
+
code: "policy_violation",
|
|
3784
|
+
message: `target ${innerCall.target} not in allowlist for session ${snapshot.sessionId}`
|
|
3785
|
+
};
|
|
3786
|
+
}
|
|
3787
|
+
const selector = selectorOf(innerCall.callData);
|
|
3788
|
+
const rule = snapshot.selectorCaps.find((c) => c.selector === selector);
|
|
3789
|
+
if (!rule) {
|
|
3790
|
+
return {
|
|
3791
|
+
ok: false,
|
|
3792
|
+
code: "policy_violation",
|
|
3793
|
+
message: `selector ${selector} not in selectorCaps for session ${snapshot.sessionId}`
|
|
3794
|
+
};
|
|
3795
|
+
}
|
|
3796
|
+
if (rule.capArgIndex !== null && rule.maxAmount !== null) {
|
|
3797
|
+
let argValue;
|
|
3798
|
+
try {
|
|
3799
|
+
argValue = decodeUint256ArgAt(innerCall.callData, rule.capArgIndex);
|
|
3800
|
+
} catch (err2) {
|
|
3801
|
+
return {
|
|
3802
|
+
ok: false,
|
|
3803
|
+
code: "policy_violation",
|
|
3804
|
+
message: `callData decode failed at wordIndex ${rule.capArgIndex}: ${asMessage2(err2)}`
|
|
3805
|
+
};
|
|
3806
|
+
}
|
|
3807
|
+
const cap = BigInt(rule.maxAmount);
|
|
3808
|
+
if (argValue > cap) {
|
|
3809
|
+
return {
|
|
3810
|
+
ok: false,
|
|
3811
|
+
code: "max_spend_exceeded",
|
|
3812
|
+
message: `arg word[${rule.capArgIndex}] = ${argValue} exceeds maxAmount ${cap} for selector ${selector} (session ${snapshot.sessionId})`
|
|
3813
|
+
};
|
|
3814
|
+
}
|
|
3815
|
+
}
|
|
3816
|
+
return { ok: true };
|
|
3817
|
+
}
|
|
1818
3818
|
|
|
1819
3819
|
// src/broker/daemon.ts
|
|
1820
3820
|
var noopLogger = (_e) => {
|
|
1821
3821
|
};
|
|
1822
|
-
async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.floor(Date.now() / 1e3), options = {}) {
|
|
3822
|
+
async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.floor(Date.now() / 1e3), options = {}, policyStore) {
|
|
1823
3823
|
switch (req.type) {
|
|
1824
3824
|
case "hello": {
|
|
1825
3825
|
let hasJwt = false;
|
|
@@ -1892,6 +3892,141 @@ async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.fl
|
|
|
1892
3892
|
);
|
|
1893
3893
|
}
|
|
1894
3894
|
}
|
|
3895
|
+
case "sign_userop": {
|
|
3896
|
+
if (!policyStore) {
|
|
3897
|
+
return errorResponse(
|
|
3898
|
+
"internal",
|
|
3899
|
+
"broker daemon is not configured with a policy store"
|
|
3900
|
+
);
|
|
3901
|
+
}
|
|
3902
|
+
const now = nowSec();
|
|
3903
|
+
let snapshot;
|
|
3904
|
+
try {
|
|
3905
|
+
snapshot = await policyStore.get(req.sessionId, now);
|
|
3906
|
+
} catch (err2) {
|
|
3907
|
+
return errorResponse(
|
|
3908
|
+
"internal",
|
|
3909
|
+
err2 instanceof Error ? err2.message : "policy store read failed"
|
|
3910
|
+
);
|
|
3911
|
+
}
|
|
3912
|
+
if (!snapshot) {
|
|
3913
|
+
return errorResponse(
|
|
3914
|
+
"no_active_snapshot",
|
|
3915
|
+
`no active snapshot for session ${req.sessionId}`
|
|
3916
|
+
);
|
|
3917
|
+
}
|
|
3918
|
+
const check = checkPolicy({
|
|
3919
|
+
snapshot,
|
|
3920
|
+
innerCall: req.innerCall,
|
|
3921
|
+
activeSigner: signer.address,
|
|
3922
|
+
nowSec: now
|
|
3923
|
+
});
|
|
3924
|
+
if (!check.ok) {
|
|
3925
|
+
return errorResponse(check.code, check.message);
|
|
3926
|
+
}
|
|
3927
|
+
try {
|
|
3928
|
+
const signature = await signer.signRawMessage(req.userOpHash);
|
|
3929
|
+
return {
|
|
3930
|
+
type: "sign_userop",
|
|
3931
|
+
signature,
|
|
3932
|
+
signerAddress: signer.address,
|
|
3933
|
+
sessionId: req.sessionId
|
|
3934
|
+
};
|
|
3935
|
+
} catch (err2) {
|
|
3936
|
+
if (err2 instanceof MissingSessionKeyError) {
|
|
3937
|
+
return errorResponse("session_key_unavailable", err2.message);
|
|
3938
|
+
}
|
|
3939
|
+
throw err2;
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
case "store_policy_snapshot": {
|
|
3943
|
+
if (!policyStore) {
|
|
3944
|
+
return errorResponse(
|
|
3945
|
+
"internal",
|
|
3946
|
+
"broker daemon is not configured with a policy store"
|
|
3947
|
+
);
|
|
3948
|
+
}
|
|
3949
|
+
try {
|
|
3950
|
+
await policyStore.put(req.snapshot);
|
|
3951
|
+
return {
|
|
3952
|
+
type: "store_policy_snapshot",
|
|
3953
|
+
stored: true,
|
|
3954
|
+
sessionId: req.snapshot.sessionId
|
|
3955
|
+
};
|
|
3956
|
+
} catch (err2) {
|
|
3957
|
+
if (err2 instanceof PolicyStoreError) {
|
|
3958
|
+
return errorResponse("internal", err2.message);
|
|
3959
|
+
}
|
|
3960
|
+
return errorResponse(
|
|
3961
|
+
"internal",
|
|
3962
|
+
err2 instanceof Error ? err2.message : "policy store write failed"
|
|
3963
|
+
);
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
case "get_policy_snapshot": {
|
|
3967
|
+
if (!policyStore) {
|
|
3968
|
+
return errorResponse(
|
|
3969
|
+
"internal",
|
|
3970
|
+
"broker daemon is not configured with a policy store"
|
|
3971
|
+
);
|
|
3972
|
+
}
|
|
3973
|
+
try {
|
|
3974
|
+
const snapshot = await policyStore.get(req.sessionId, nowSec());
|
|
3975
|
+
return { type: "get_policy_snapshot", snapshot };
|
|
3976
|
+
} catch (err2) {
|
|
3977
|
+
if (err2 instanceof PolicyStoreError) {
|
|
3978
|
+
return errorResponse("internal", err2.message);
|
|
3979
|
+
}
|
|
3980
|
+
return errorResponse(
|
|
3981
|
+
"internal",
|
|
3982
|
+
err2 instanceof Error ? err2.message : "policy store read failed"
|
|
3983
|
+
);
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
case "clear_policy_snapshot": {
|
|
3987
|
+
if (!policyStore) {
|
|
3988
|
+
return errorResponse(
|
|
3989
|
+
"internal",
|
|
3990
|
+
"broker daemon is not configured with a policy store"
|
|
3991
|
+
);
|
|
3992
|
+
}
|
|
3993
|
+
try {
|
|
3994
|
+
await policyStore.delete(req.sessionId);
|
|
3995
|
+
return {
|
|
3996
|
+
type: "clear_policy_snapshot",
|
|
3997
|
+
cleared: true,
|
|
3998
|
+
sessionId: req.sessionId
|
|
3999
|
+
};
|
|
4000
|
+
} catch (err2) {
|
|
4001
|
+
if (err2 instanceof PolicyStoreError) {
|
|
4002
|
+
return errorResponse("internal", err2.message);
|
|
4003
|
+
}
|
|
4004
|
+
return errorResponse(
|
|
4005
|
+
"internal",
|
|
4006
|
+
err2 instanceof Error ? err2.message : "policy store clear failed"
|
|
4007
|
+
);
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
case "get_active_session_id": {
|
|
4011
|
+
if (!policyStore) {
|
|
4012
|
+
return errorResponse(
|
|
4013
|
+
"internal",
|
|
4014
|
+
"broker daemon is not configured with a policy store"
|
|
4015
|
+
);
|
|
4016
|
+
}
|
|
4017
|
+
try {
|
|
4018
|
+
const sessionId = await policyStore.activeSessionId(signer.address, nowSec());
|
|
4019
|
+
return { type: "get_active_session_id", sessionId };
|
|
4020
|
+
} catch (err2) {
|
|
4021
|
+
if (err2 instanceof PolicyStoreError) {
|
|
4022
|
+
return errorResponse("internal", err2.message);
|
|
4023
|
+
}
|
|
4024
|
+
return errorResponse(
|
|
4025
|
+
"internal",
|
|
4026
|
+
err2 instanceof Error ? err2.message : "policy store enumerate failed"
|
|
4027
|
+
);
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
1895
4030
|
}
|
|
1896
4031
|
}
|
|
1897
4032
|
function errorResponse(code, message) {
|
|
@@ -1900,12 +4035,12 @@ function errorResponse(code, message) {
|
|
|
1900
4035
|
async function prepareEndpoint(endpoint) {
|
|
1901
4036
|
if (os.platform() === "win32") return;
|
|
1902
4037
|
const parent = path.dirname(endpoint);
|
|
1903
|
-
await promises.mkdir(parent, { recursive: true, mode: 448 });
|
|
1904
|
-
await promises.chmod(parent, 448);
|
|
4038
|
+
await promises$1.mkdir(parent, { recursive: true, mode: 448 });
|
|
4039
|
+
await promises$1.chmod(parent, 448);
|
|
1905
4040
|
try {
|
|
1906
|
-
const s = await promises.stat(endpoint);
|
|
4041
|
+
const s = await promises$1.stat(endpoint);
|
|
1907
4042
|
if (s.isSocket() || s.isFIFO()) {
|
|
1908
|
-
await promises.unlink(endpoint);
|
|
4043
|
+
await promises$1.unlink(endpoint);
|
|
1909
4044
|
}
|
|
1910
4045
|
} catch (err2) {
|
|
1911
4046
|
if (err2.code !== "ENOENT") throw err2;
|
|
@@ -1913,7 +4048,7 @@ async function prepareEndpoint(endpoint) {
|
|
|
1913
4048
|
}
|
|
1914
4049
|
async function applySocketPermissions(endpoint) {
|
|
1915
4050
|
if (os.platform() === "win32") return;
|
|
1916
|
-
await promises.chmod(endpoint, 384);
|
|
4051
|
+
await promises$1.chmod(endpoint, 384);
|
|
1917
4052
|
}
|
|
1918
4053
|
var BrokerDaemon = class {
|
|
1919
4054
|
server;
|
|
@@ -1921,6 +4056,7 @@ var BrokerDaemon = class {
|
|
|
1921
4056
|
log;
|
|
1922
4057
|
config;
|
|
1923
4058
|
keystore;
|
|
4059
|
+
policyStore;
|
|
1924
4060
|
/**
|
|
1925
4061
|
* Whether a session-key private half is actually loaded. `false` =
|
|
1926
4062
|
* daemon booted in read-only posture (no `MUHAVEN_BROKER_SESSION_KEY`
|
|
@@ -1940,6 +4076,7 @@ var BrokerDaemon = class {
|
|
|
1940
4076
|
this.hasSessionKey = false;
|
|
1941
4077
|
}
|
|
1942
4078
|
this.keystore = options.keystore ?? null;
|
|
4079
|
+
this.policyStore = options.policyStore ?? new FilePolicyStore(FilePolicyStore.defaultDir());
|
|
1943
4080
|
this.log = options.logger ?? noopLogger;
|
|
1944
4081
|
this.server = net.createServer((socket) => this.onConnection(socket));
|
|
1945
4082
|
}
|
|
@@ -1955,6 +4092,7 @@ var BrokerDaemon = class {
|
|
|
1955
4092
|
});
|
|
1956
4093
|
}
|
|
1957
4094
|
}
|
|
4095
|
+
if (this.policyStore.init) await this.policyStore.init();
|
|
1958
4096
|
await prepareEndpoint(this.config.endpoint);
|
|
1959
4097
|
await new Promise((resolve, reject) => {
|
|
1960
4098
|
const onError = (err2) => {
|
|
@@ -1989,7 +4127,7 @@ var BrokerDaemon = class {
|
|
|
1989
4127
|
});
|
|
1990
4128
|
if (os.platform() !== "win32") {
|
|
1991
4129
|
try {
|
|
1992
|
-
await promises.unlink(this.config.endpoint);
|
|
4130
|
+
await promises$1.unlink(this.config.endpoint);
|
|
1993
4131
|
} catch (err2) {
|
|
1994
4132
|
if (err2.code !== "ENOENT") {
|
|
1995
4133
|
this.log({ level: "warn", msg: "failed to unlink socket on stop", meta: { err: err2 } });
|
|
@@ -2070,7 +4208,8 @@ var BrokerDaemon = class {
|
|
|
2070
4208
|
dashboardBaseUrl: this.config.dashboardBaseUrl
|
|
2071
4209
|
},
|
|
2072
4210
|
pid: process.pid
|
|
2073
|
-
}
|
|
4211
|
+
},
|
|
4212
|
+
this.policyStore
|
|
2074
4213
|
);
|
|
2075
4214
|
socket.end(serializeResponse(res));
|
|
2076
4215
|
} catch (err2) {
|
|
@@ -2092,6 +4231,8 @@ exports.BackendError = BackendError;
|
|
|
2092
4231
|
exports.BrokerClient = BrokerClient;
|
|
2093
4232
|
exports.BrokerClientError = BrokerClientError;
|
|
2094
4233
|
exports.BrokerDaemon = BrokerDaemon;
|
|
4234
|
+
exports.BundlerClient = BundlerClient;
|
|
4235
|
+
exports.BundlerClientError = BundlerClientError;
|
|
2095
4236
|
exports.DeviceFlowAbortedError = DeviceFlowAbortedError;
|
|
2096
4237
|
exports.DeviceFlowClient = DeviceFlowClient;
|
|
2097
4238
|
exports.JwtSource = JwtSource;
|
|
@@ -2116,5 +4257,6 @@ exports.parseBrokerRequest = parseBrokerRequest;
|
|
|
2116
4257
|
exports.registryForReadOnly = registryForReadOnly;
|
|
2117
4258
|
exports.runMcpStdioCli = runMcpStdioCli;
|
|
2118
4259
|
exports.selectRegistry = selectRegistry;
|
|
4260
|
+
exports.semverGte = semverGte;
|
|
2119
4261
|
exports.serializeResponse = serializeResponse;
|
|
2120
4262
|
exports.verifyDescriptorAgainstPin = verifyDescriptorAgainstPin;
|