@muhaven/mcp 0.2.1 → 0.2.3

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/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("broker_error", `${parsed.code}: ${parsed.message}`)
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
  }
@@ -486,6 +986,414 @@ function mapStatus(status) {
486
986
  if (status >= 500) return "server_error";
487
987
  return "invalid_response";
488
988
  }
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
+ const headers = {
1198
+ "content-type": "application/json",
1199
+ accept: "application/json"
1200
+ };
1201
+ if (this.options.originHeader) {
1202
+ headers["origin"] = this.options.originHeader;
1203
+ }
1204
+ res = await this.fetchImpl(this.options.endpoint, {
1205
+ method: "POST",
1206
+ headers,
1207
+ body,
1208
+ signal: ctrl.signal
1209
+ });
1210
+ } catch (err2) {
1211
+ clearTimeout(timer);
1212
+ if (err2.name === "AbortError") {
1213
+ throw new BundlerClientError("timeout", `bundler ${method} timed out`);
1214
+ }
1215
+ throw new BundlerClientError(
1216
+ "network",
1217
+ `bundler ${method} network error: ${err2 instanceof Error ? err2.message : String(err2)}`,
1218
+ err2
1219
+ );
1220
+ } finally {
1221
+ clearTimeout(timer);
1222
+ }
1223
+ if (!res.ok) {
1224
+ let text = "";
1225
+ try {
1226
+ text = (await res.text()).slice(0, 256);
1227
+ } catch {
1228
+ }
1229
+ throw new BundlerClientError(
1230
+ "http_error",
1231
+ `bundler ${method} \u2192 HTTP ${res.status}: ${text}`
1232
+ );
1233
+ }
1234
+ let parsed;
1235
+ try {
1236
+ parsed = await res.json();
1237
+ } catch (err2) {
1238
+ throw new BundlerClientError(
1239
+ "invalid_response",
1240
+ `bundler ${method} returned non-JSON: ${err2 instanceof Error ? err2.message : String(err2)}`
1241
+ );
1242
+ }
1243
+ if (typeof parsed !== "object" || parsed === null) {
1244
+ throw new BundlerClientError(
1245
+ "invalid_response",
1246
+ `bundler ${method} returned non-object`
1247
+ );
1248
+ }
1249
+ const obj = parsed;
1250
+ if (obj.error !== void 0 && obj.error !== null) {
1251
+ const err2 = obj.error;
1252
+ throw new BundlerClientError(
1253
+ "rpc_error",
1254
+ `bundler ${method} rpc error: ${typeof err2.message === "string" ? err2.message : "<no message>"}`,
1255
+ { code: err2.code, message: err2.message, data: err2.data }
1256
+ );
1257
+ }
1258
+ return obj.result;
1259
+ }
1260
+ };
1261
+ function parseReceipt(raw) {
1262
+ if (typeof raw !== "object" || raw === null) {
1263
+ throw new BundlerClientError("invalid_response", "receipt is not an object");
1264
+ }
1265
+ const obj = raw;
1266
+ const userOpHash = obj.userOpHash;
1267
+ if (typeof userOpHash !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(userOpHash)) {
1268
+ throw new BundlerClientError("invalid_response", "receipt.userOpHash malformed");
1269
+ }
1270
+ const sender = obj.sender;
1271
+ if (typeof sender !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(sender)) {
1272
+ throw new BundlerClientError("invalid_response", "receipt.sender malformed");
1273
+ }
1274
+ if (typeof obj.success !== "boolean") {
1275
+ throw new BundlerClientError("invalid_response", "receipt.success must be a boolean");
1276
+ }
1277
+ const inner = obj.receipt;
1278
+ if (typeof inner !== "object" || inner === null) {
1279
+ throw new BundlerClientError("invalid_response", "receipt.receipt missing");
1280
+ }
1281
+ const innerObj = inner;
1282
+ const txHash = innerObj.transactionHash;
1283
+ if (typeof txHash !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(txHash)) {
1284
+ throw new BundlerClientError(
1285
+ "invalid_response",
1286
+ "receipt.receipt.transactionHash malformed"
1287
+ );
1288
+ }
1289
+ const blockNumber = innerObj.blockNumber;
1290
+ if (typeof blockNumber !== "string" || !/^0x[0-9a-fA-F]+$/.test(blockNumber)) {
1291
+ throw new BundlerClientError(
1292
+ "invalid_response",
1293
+ "receipt.receipt.blockNumber malformed"
1294
+ );
1295
+ }
1296
+ const blockHash = innerObj.blockHash;
1297
+ if (typeof blockHash !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(blockHash)) {
1298
+ throw new BundlerClientError(
1299
+ "invalid_response",
1300
+ "receipt.receipt.blockHash malformed"
1301
+ );
1302
+ }
1303
+ return {
1304
+ userOpHash: userOpHash.toLowerCase(),
1305
+ sender: sender.toLowerCase(),
1306
+ success: obj.success,
1307
+ ...typeof obj.reason === "string" ? { reason: obj.reason } : {},
1308
+ receipt: {
1309
+ transactionHash: txHash.toLowerCase(),
1310
+ blockNumber: blockNumber.toLowerCase(),
1311
+ blockHash: blockHash.toLowerCase()
1312
+ }
1313
+ };
1314
+ }
1315
+ function parseSponsoredFields(raw) {
1316
+ if (typeof raw !== "object" || raw === null) {
1317
+ throw new BundlerClientError(
1318
+ "invalid_response",
1319
+ "pm_sponsorUserOperation returned non-object"
1320
+ );
1321
+ }
1322
+ const obj = raw;
1323
+ return {
1324
+ paymaster: assertHexAddress(obj.paymaster, "sponsoredFields.paymaster"),
1325
+ paymasterVerificationGasLimit: assertHexNonZero(
1326
+ obj.paymasterVerificationGasLimit,
1327
+ "sponsoredFields.paymasterVerificationGasLimit",
1328
+ MAX_PAYMASTER_GAS_LIMIT
1329
+ ),
1330
+ paymasterPostOpGasLimit: assertHexNonZero(
1331
+ obj.paymasterPostOpGasLimit,
1332
+ "sponsoredFields.paymasterPostOpGasLimit",
1333
+ MAX_PAYMASTER_GAS_LIMIT
1334
+ ),
1335
+ // paymasterData is the only sponsored field that can legitimately
1336
+ // be empty (`0x`) — paymasters with no per-op data return that.
1337
+ paymasterData: assertHex(obj.paymasterData, "sponsoredFields.paymasterData"),
1338
+ callGasLimit: assertHexNonZero(
1339
+ obj.callGasLimit,
1340
+ "sponsoredFields.callGasLimit",
1341
+ MAX_CALL_GAS_LIMIT
1342
+ ),
1343
+ verificationGasLimit: assertHexNonZero(
1344
+ obj.verificationGasLimit,
1345
+ "sponsoredFields.verificationGasLimit",
1346
+ MAX_VERIFICATION_GAS_LIMIT
1347
+ ),
1348
+ preVerificationGas: assertHexNonZero(
1349
+ obj.preVerificationGas,
1350
+ "sponsoredFields.preVerificationGas",
1351
+ MAX_PRE_VERIFICATION_GAS
1352
+ )
1353
+ };
1354
+ }
1355
+ function assertHex(value, label) {
1356
+ if (typeof value !== "string" || !/^0x([0-9a-fA-F]{2})*$/.test(value)) {
1357
+ const repr = value === void 0 ? "undefined" : JSON.stringify(value);
1358
+ const safe = typeof repr === "string" ? repr.slice(0, 80) : "unknown";
1359
+ throw new BundlerClientError(
1360
+ "invalid_response",
1361
+ `${label} must be a 0x-prefixed hex string (got ${safe})`
1362
+ );
1363
+ }
1364
+ return value;
1365
+ }
1366
+ function assertHexNonZero(value, label, maxValue) {
1367
+ const hex = assertHex(value, label);
1368
+ if (hex.length === 2 || BigInt(hex) === 0n) {
1369
+ throw new BundlerClientError(
1370
+ "invalid_response",
1371
+ `${label} must be a non-zero hex value (got "${hex}")`
1372
+ );
1373
+ }
1374
+ if (maxValue !== void 0 && BigInt(hex) > maxValue) {
1375
+ throw new BundlerClientError(
1376
+ "invalid_response",
1377
+ `${label} = ${BigInt(hex)} exceeds plausible ceiling ${maxValue} \u2014 refusing to sign + submit`
1378
+ );
1379
+ }
1380
+ return hex;
1381
+ }
1382
+ var MAX_CALL_GAS_LIMIT = 20000000n;
1383
+ var MAX_VERIFICATION_GAS_LIMIT = 20000000n;
1384
+ var MAX_PRE_VERIFICATION_GAS = 5000000n;
1385
+ var MAX_PAYMASTER_GAS_LIMIT = 5000000n;
1386
+ function assertHexAddress(value, label) {
1387
+ if (typeof value !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(value)) {
1388
+ const repr = value === void 0 ? "undefined" : JSON.stringify(value);
1389
+ const safe = typeof repr === "string" ? repr.slice(0, 80) : "unknown";
1390
+ throw new BundlerClientError(
1391
+ "invalid_response",
1392
+ `${label} must be a 0x-prefixed 20-byte hex address (got ${safe})`
1393
+ );
1394
+ }
1395
+ return value;
1396
+ }
489
1397
  var TOOL_DESCRIPTORS = [
490
1398
  {
491
1399
  name: "muhaven.read.portfolio",
@@ -833,6 +1741,119 @@ var GovernanceCastVoteInputSchema = zod.z.object({
833
1741
  voteYes: zod.z.boolean()
834
1742
  }).strict();
835
1743
 
1744
+ // src/auth/jwt-decode.ts
1745
+ var JwtDecodeError = class extends Error {
1746
+ code;
1747
+ constructor(code, message) {
1748
+ super(message);
1749
+ this.name = "JwtDecodeError";
1750
+ this.code = code;
1751
+ }
1752
+ };
1753
+ function decodeJwtPayload(jwt) {
1754
+ const segments = jwt.split(".");
1755
+ if (segments.length !== 3) {
1756
+ throw new JwtDecodeError(
1757
+ "malformed_segments",
1758
+ `expected 3 dot-separated segments, got ${segments.length}`
1759
+ );
1760
+ }
1761
+ const payloadSegment = segments[1];
1762
+ if (payloadSegment === void 0) {
1763
+ throw new JwtDecodeError("malformed_segments", "payload segment missing");
1764
+ }
1765
+ let payloadJson;
1766
+ try {
1767
+ payloadJson = Buffer.from(payloadSegment, "base64url").toString("utf8");
1768
+ } catch (err2) {
1769
+ throw new JwtDecodeError(
1770
+ "malformed_base64",
1771
+ `payload segment is not valid base64url: ${err2 instanceof Error ? err2.message : String(err2)}`
1772
+ );
1773
+ }
1774
+ let parsed;
1775
+ try {
1776
+ parsed = JSON.parse(payloadJson);
1777
+ } catch (err2) {
1778
+ throw new JwtDecodeError(
1779
+ "malformed_json",
1780
+ `payload is not valid JSON: ${err2 instanceof Error ? err2.message : String(err2)}`
1781
+ );
1782
+ }
1783
+ if (parsed === null || typeof parsed !== "object") {
1784
+ throw new JwtDecodeError(
1785
+ "malformed_json",
1786
+ `payload is not a JSON object (got ${parsed === null ? "null" : typeof parsed})`
1787
+ );
1788
+ }
1789
+ const obj = parsed;
1790
+ return {
1791
+ sub: typeof obj.sub === "string" ? obj.sub : null,
1792
+ scope: Array.isArray(obj.scope) ? obj.scope.filter((s) => typeof s === "string") : [],
1793
+ expSec: typeof obj.exp === "number" ? obj.exp : null,
1794
+ iss: typeof obj.iss === "string" ? obj.iss : null
1795
+ };
1796
+ }
1797
+ function truncateSubject(sub) {
1798
+ if (!sub) return "(missing)";
1799
+ const sanitized = sub.replace(/[^\x20-\x7e]/g, "?");
1800
+ if (sanitized.length <= 12) return sanitized;
1801
+ return `${sanitized.slice(0, 8)}\u2026${sanitized.slice(-4)}`;
1802
+ }
1803
+ var KERNEL_EXECUTE_ABI = viem.parseAbi([
1804
+ "function execute(bytes32 mode, bytes calldata executionCalldata)"
1805
+ ]);
1806
+ var KERNEL_V3_SINGLE_CALL_MODE_DEFAULT = `0x${"00".repeat(32)}`;
1807
+ function encodeKernelExecuteSingleCall(input) {
1808
+ const executionCalldata = viem.encodePacked(
1809
+ ["address", "uint256", "bytes"],
1810
+ [input.target, input.value, input.callData]
1811
+ );
1812
+ return viem.encodeFunctionData({
1813
+ abi: KERNEL_EXECUTE_ABI,
1814
+ functionName: "execute",
1815
+ args: [KERNEL_V3_SINGLE_CALL_MODE_DEFAULT, executionCalldata]
1816
+ });
1817
+ }
1818
+ var ECDSA_SIG_HEX_RE = /^0x[0-9a-fA-F]{130}$/;
1819
+ var PERMISSION_USE_PREFIX = "0xff";
1820
+ function buildKernelSessionKeySignature(input) {
1821
+ if (!ECDSA_SIG_HEX_RE.test(input.ecdsaSignature)) {
1822
+ throw new Error(
1823
+ `buildKernelSessionKeySignature: ecdsaSignature must be a 0x-prefixed 65-byte hex (got ${input.ecdsaSignature.length} chars)`
1824
+ );
1825
+ }
1826
+ return viem.concatHex([PERMISSION_USE_PREFIX, input.ecdsaSignature]);
1827
+ }
1828
+ var VALIDATOR_MODE_DEFAULT = "0x00";
1829
+ var VALIDATOR_TYPE_PERMISSION = "0x02";
1830
+ var PERMISSION_ID_HEX_RE = /^0x[0-9a-fA-F]{8}$/;
1831
+ function composeKernelV3NonceKey(args) {
1832
+ if (!PERMISSION_ID_HEX_RE.test(args.permissionId)) {
1833
+ throw new Error(
1834
+ `composeKernelV3NonceKey: permissionId must be a 0x-prefixed 4-byte hex (got ${args.permissionId.length} chars)`
1835
+ );
1836
+ }
1837
+ const customKey = args.customKey ?? 0n;
1838
+ if (customKey < 0n || customKey > 0xffffn) {
1839
+ throw new Error(
1840
+ `composeKernelV3NonceKey: customKey must fit in 2 bytes (0..0xffff), got ${customKey}`
1841
+ );
1842
+ }
1843
+ const paddedPermissionId = viem.pad(args.permissionId, { size: 20, dir: "right" });
1844
+ const customKeyHex = viem.pad(`0x${customKey.toString(16)}`, { size: 2 });
1845
+ const composite = viem.pad(
1846
+ viem.concatHex([
1847
+ VALIDATOR_MODE_DEFAULT,
1848
+ VALIDATOR_TYPE_PERMISSION,
1849
+ paddedPermissionId,
1850
+ customKeyHex
1851
+ ]),
1852
+ { size: 24 }
1853
+ );
1854
+ return BigInt(composite);
1855
+ }
1856
+
836
1857
  // src/tools/auth-required.ts
837
1858
  function authRequiredPayload() {
838
1859
  return {
@@ -875,6 +1896,13 @@ function formatUsd6AsDecimal(usd6) {
875
1896
  }
876
1897
 
877
1898
  // src/tools/handlers.ts
1899
+ var SUBSCRIPTION_PURCHASE_SELECTOR = viem.toFunctionSelector(
1900
+ "function purchase(address,(uint256,uint8,uint8,bytes),uint128,address)"
1901
+ ).toLowerCase();
1902
+ var SUBSCRIPTION_PURCHASE_ABI = viem.parseAbi([
1903
+ "function purchase(address token, (uint256 ctHash, uint8 securityZone, uint8 utype, bytes signature) encShares, uint128 maxSharesHint, address ephemeralEOA)"
1904
+ ]);
1905
+ var PLACEHOLDER_SIGNATURE = "0x" + "fe".repeat(86);
878
1906
  function ok(data) {
879
1907
  return { ok: true, data };
880
1908
  }
@@ -886,94 +1914,673 @@ function mapBackendError(e) {
886
1914
  if (e.code === "unauthorized") return authRequiredPayload();
887
1915
  return err(`backend.${e.code}`, e.message);
888
1916
  }
889
- if (e instanceof Error) return err("backend.network", e.message);
890
- return err("backend.network", "unknown backend error");
891
- }
892
- async function readPortfolio(_input, deps) {
893
- try {
894
- const data = await deps.backend.get("/api/v1/portfolio");
895
- return ok(data);
896
- } catch (e) {
897
- return mapBackendError(e);
1917
+ if (e instanceof Error) return err("backend.network", e.message);
1918
+ return err("backend.network", "unknown backend error");
1919
+ }
1920
+ async function readPortfolio(_input, deps) {
1921
+ try {
1922
+ const data = await deps.backend.get("/api/v1/portfolio");
1923
+ return ok(data);
1924
+ } catch (e) {
1925
+ return mapBackendError(e);
1926
+ }
1927
+ }
1928
+ async function readYields(input, deps) {
1929
+ try {
1930
+ const data = await deps.backend.get("/api/v1/yields", {
1931
+ token: input.token,
1932
+ limit: input.limit
1933
+ });
1934
+ return ok(data);
1935
+ } catch (e) {
1936
+ return mapBackendError(e);
1937
+ }
1938
+ }
1939
+ async function readDistribution(input, deps) {
1940
+ try {
1941
+ const data = await deps.backend.get("/api/v1/distributions", {
1942
+ token: input.token,
1943
+ epoch: input.epoch
1944
+ });
1945
+ return ok(data);
1946
+ } catch (e) {
1947
+ return mapBackendError(e);
1948
+ }
1949
+ }
1950
+ async function readTokens(_input, deps) {
1951
+ try {
1952
+ const data = await deps.backend.get("/api/v1/tokens");
1953
+ return ok(data);
1954
+ } catch (e) {
1955
+ return mapBackendError(e);
1956
+ }
1957
+ }
1958
+ async function readAudit(input, deps) {
1959
+ try {
1960
+ const data = await deps.backend.get("/api/v1/agent/policy/audit", {
1961
+ surface: input.surface,
1962
+ eventTypes: input.eventTypes?.join(","),
1963
+ since: input.since,
1964
+ until: input.until,
1965
+ cursor: input.cursor,
1966
+ limit: input.limit
1967
+ });
1968
+ return ok(data);
1969
+ } catch (e) {
1970
+ return mapBackendError(e);
1971
+ }
1972
+ }
1973
+ async function readActivity(input, deps) {
1974
+ try {
1975
+ const data = await deps.backend.get("/api/v1/activity", {
1976
+ limit: input.limit,
1977
+ offset: input.offset
1978
+ });
1979
+ return ok(data);
1980
+ } catch (e) {
1981
+ return mapBackendError(e);
1982
+ }
1983
+ }
1984
+ function buildPositionDeeplink(dashboardBaseUrl, action, params) {
1985
+ const base = dashboardBaseUrl.replace(/\/+$/, "");
1986
+ const path = action === "buy" || action === "sell" ? "/trade" : action === "claim" ? "/yields" : "/cash";
1987
+ const search = new URLSearchParams();
1988
+ if (action === "buy" || action === "sell") search.set("mode", action);
1989
+ for (const [k, v] of Object.entries(params)) search.set(k, v);
1990
+ search.set("from", "mcp");
1991
+ return `${base}${path}?${search.toString()}`;
1992
+ }
1993
+ function resolveDashboardBaseUrl(deps) {
1994
+ return deps.dashboardBaseUrl ?? "https://muhaven.app";
1995
+ }
1996
+ function resolveTokenInCatalog(identifier, catalog) {
1997
+ const needle = identifier.toLowerCase();
1998
+ return catalog.find(
1999
+ (t) => t.address.toLowerCase() === needle || t.symbol.toLowerCase() === needle
2000
+ ) ?? null;
2001
+ }
2002
+ function sanitizeSymbolForLlmContext(raw) {
2003
+ const cleaned = raw.replace(/[^A-Za-z0-9_-]/g, "?");
2004
+ return cleaned.length > 16 ? cleaned.slice(0, 16) : cleaned;
2005
+ }
2006
+ function sanitizeRpcMessageForLlmContext(raw) {
2007
+ const cleaned = raw.replace(/[\x00-\x1f\x7f]/g, "").replace(/[^\x20-\x7e]/g, "?").replace(/\s+/g, " ").trim();
2008
+ return cleaned.length > 120 ? cleaned.slice(0, 120) + "\u2026" : cleaned;
2009
+ }
2010
+ function mapBrokerCallFailure(err2, verb, defaultReason = "broker_internal") {
2011
+ if (err2 instanceof BrokerClientError && err2.brokerCode === "unsupported_type") {
2012
+ return {
2013
+ kind: "fallback",
2014
+ reason: "version_too_old",
2015
+ 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`
2016
+ };
2017
+ }
2018
+ return {
2019
+ kind: "fallback",
2020
+ reason: defaultReason,
2021
+ message: `broker rejected ${verb} (${typedErrorCode(err2)})`
2022
+ };
2023
+ }
2024
+ var MCP_HEX_20_BYTE_RE = /^0x[0-9a-fA-F]{40}$/;
2025
+ var MCP_HEX_4_BYTE_RE = /^0x[0-9a-fA-F]{8}$/;
2026
+ var MCP_HEX_32_BYTE_RE = /^0x[0-9a-fA-F]{64}$/;
2027
+ var MCP_SESSION_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
2028
+ var MirrorDtoMalformedError = class extends Error {
2029
+ constructor(message) {
2030
+ super(message);
2031
+ this.name = "MirrorDtoMalformedError";
2032
+ }
2033
+ };
2034
+ function mirrorDtoToPolicySnapshot(dto) {
2035
+ if (dto.mode !== "scoped") {
2036
+ throw new MirrorDtoMalformedError(
2037
+ `mode must be 'scoped' (got ${JSON.stringify(dto.mode)}); wildcard mirror auto-sync ships in Slice 4`
2038
+ );
2039
+ }
2040
+ if (dto.status !== "active") {
2041
+ throw new MirrorDtoMalformedError(
2042
+ `status must be 'active' for auto-sync (got ${JSON.stringify(dto.status)}); backend mirror should have filtered this row out`
2043
+ );
2044
+ }
2045
+ if (typeof dto.sessionId !== "string" || !MCP_SESSION_ID_RE.test(dto.sessionId)) {
2046
+ throw new MirrorDtoMalformedError(
2047
+ `sessionId must match /^[A-Za-z0-9_-]{1,128}$/`
2048
+ );
2049
+ }
2050
+ if (!MCP_HEX_20_BYTE_RE.test(dto.signerAddress)) {
2051
+ throw new MirrorDtoMalformedError(
2052
+ `signerAddress is not a 0x-prefixed 20-byte hex`
2053
+ );
2054
+ }
2055
+ if (!Array.isArray(dto.targetContracts) || dto.targetContracts.length === 0) {
2056
+ throw new MirrorDtoMalformedError(
2057
+ `targetContracts must be a non-empty array`
2058
+ );
2059
+ }
2060
+ for (const t of dto.targetContracts) {
2061
+ if (typeof t !== "string" || !MCP_HEX_20_BYTE_RE.test(t)) {
2062
+ throw new MirrorDtoMalformedError(
2063
+ `targetContracts entry is not a 0x-prefixed 20-byte hex`
2064
+ );
2065
+ }
2066
+ }
2067
+ if (!Array.isArray(dto.selectorCaps) || dto.selectorCaps.length === 0) {
2068
+ throw new MirrorDtoMalformedError(`selectorCaps must be a non-empty array`);
2069
+ }
2070
+ for (const c of dto.selectorCaps) {
2071
+ if (typeof c?.selector !== "string" || !MCP_HEX_4_BYTE_RE.test(c.selector)) {
2072
+ throw new MirrorDtoMalformedError(
2073
+ `selectorCaps entry has a malformed selector`
2074
+ );
2075
+ }
2076
+ }
2077
+ if (dto.permissionId != null && !MCP_HEX_4_BYTE_RE.test(dto.permissionId)) {
2078
+ throw new MirrorDtoMalformedError(
2079
+ `permissionId is not a 0x-prefixed 4-byte hex`
2080
+ );
2081
+ }
2082
+ if (dto.consentActionHash != null && !MCP_HEX_32_BYTE_RE.test(dto.consentActionHash)) {
2083
+ throw new MirrorDtoMalformedError(
2084
+ `consentActionHash is not a 0x-prefixed 32-byte hex`
2085
+ );
2086
+ }
2087
+ if (dto.consentTextSha256 != null && !MCP_HEX_32_BYTE_RE.test(dto.consentTextSha256)) {
2088
+ throw new MirrorDtoMalformedError(
2089
+ `consentTextSha256 is not a 0x-prefixed 32-byte hex`
2090
+ );
2091
+ }
2092
+ return {
2093
+ sessionId: dto.sessionId,
2094
+ mode: "scoped",
2095
+ signerAddress: dto.signerAddress.toLowerCase(),
2096
+ targetContracts: dto.targetContracts.map(
2097
+ (a) => a.toLowerCase()
2098
+ ),
2099
+ selectorCaps: dto.selectorCaps.map((c) => ({
2100
+ selector: c.selector.toLowerCase(),
2101
+ capArgIndex: c.capArgIndex,
2102
+ maxAmount: c.maxAmount
2103
+ })),
2104
+ validUntilSec: dto.validUntilSec,
2105
+ mintedAtSec: dto.mintedAtSec,
2106
+ ...dto.consentActionHash ? { consentActionHash: dto.consentActionHash.toLowerCase() } : {},
2107
+ ...dto.consentTextSha256 ? { consentTextSha256: dto.consentTextSha256.toLowerCase() } : {},
2108
+ ...dto.permissionId ? { permissionId: dto.permissionId.toLowerCase() } : {}
2109
+ };
2110
+ }
2111
+ function typedErrorCode(err2) {
2112
+ if (err2 instanceof BackendError) return `backend.${err2.code}`;
2113
+ if (err2 instanceof BrokerClientError) {
2114
+ return err2.brokerCode ? `broker.${err2.brokerCode}` : `broker.${err2.code}`;
2115
+ }
2116
+ if (err2 instanceof MirrorDtoMalformedError) return "malformed_mirror_row";
2117
+ return "unknown";
2118
+ }
2119
+ async function fetchJwtSubjectHint(deps) {
2120
+ if (!deps.broker) return null;
2121
+ try {
2122
+ const res = await deps.broker.getJwt();
2123
+ if (!res.jwt) return null;
2124
+ const decoded = decodeJwtPayload(res.jwt);
2125
+ return truncateSubject(decoded.sub);
2126
+ } catch {
2127
+ return null;
2128
+ }
2129
+ }
2130
+ async function syncSnapshotFromMirror(deps, brokerSignerAddress) {
2131
+ if (!deps.broker) {
2132
+ return {
2133
+ kind: "fallback",
2134
+ reason: "mirror_sync_failed",
2135
+ message: "auto-sync invoked without a broker dep \u2014 pipeline bug"
2136
+ };
2137
+ }
2138
+ let mirror;
2139
+ try {
2140
+ mirror = await deps.backend.get(
2141
+ "/api/v1/agent/policy/scoped-session",
2142
+ { surface: "mcp" }
2143
+ );
2144
+ } catch (err2) {
2145
+ return {
2146
+ kind: "fallback",
2147
+ reason: "mirror_sync_failed",
2148
+ message: `backend mirror lookup failed (${typedErrorCode(err2)})`
2149
+ };
2150
+ }
2151
+ if (!mirror || mirror.session == null) {
2152
+ const subjectHint = await fetchJwtSubjectHint(deps);
2153
+ 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)` : "";
2154
+ return {
2155
+ kind: "fallback",
2156
+ reason: "no_active_session_key",
2157
+ message: "no active scoped session \u2014 visit /agent/policy/transition to mint one, then retry. (Mirror also empty; nothing to auto-sync.)" + hintSuffix
2158
+ };
2159
+ }
2160
+ let snapshot;
2161
+ try {
2162
+ snapshot = mirrorDtoToPolicySnapshot(mirror.session);
2163
+ } catch (err2) {
2164
+ return {
2165
+ kind: "fallback",
2166
+ reason: "mirror_sync_failed",
2167
+ message: `mirror returned a malformed scoped-session row (${typedErrorCode(err2)})`
2168
+ };
2169
+ }
2170
+ if (snapshot.signerAddress.toLowerCase() !== brokerSignerAddress.toLowerCase()) {
2171
+ return {
2172
+ kind: "fallback",
2173
+ reason: "signer_mismatch",
2174
+ 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`
2175
+ };
2176
+ }
2177
+ try {
2178
+ await deps.broker.storePolicySnapshot(snapshot);
2179
+ } catch (err2) {
2180
+ return {
2181
+ kind: "fallback",
2182
+ reason: "mirror_sync_failed",
2183
+ message: `broker rejected store_policy_snapshot (${typedErrorCode(err2)})`
2184
+ };
2185
+ }
2186
+ let activeId;
2187
+ try {
2188
+ const res = await deps.broker.getActiveSessionId();
2189
+ activeId = res.sessionId;
2190
+ } catch (err2) {
2191
+ return {
2192
+ kind: "fallback",
2193
+ reason: "mirror_sync_failed",
2194
+ message: `broker re-probe after store_policy_snapshot failed (${typedErrorCode(err2)})`
2195
+ };
2196
+ }
2197
+ if (!activeId) {
2198
+ return {
2199
+ kind: "fallback",
2200
+ reason: "mirror_sync_failed",
2201
+ 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.)"
2202
+ };
2203
+ }
2204
+ return { kind: "ok", sessionId: activeId };
2205
+ }
2206
+ async function attemptPathD(args, deps) {
2207
+ const { shares, tokenAddress, tokenSymbol } = args;
2208
+ if (!deps.broker || !deps.bundler) {
2209
+ return { kind: "unconfigured" };
2210
+ }
2211
+ if (!deps.subscriptionAddress) {
2212
+ return {
2213
+ kind: "fallback",
2214
+ reason: "subscription_address_unset",
2215
+ message: "MUHAVEN_SUBSCRIPTION_ADDRESS not configured \u2014 Path D autonomous-buy disabled until the operator sets it in the MCP env"
2216
+ };
2217
+ }
2218
+ if (!deps.entryPointAddress) {
2219
+ return {
2220
+ kind: "fallback",
2221
+ reason: "entry_point_unset",
2222
+ message: "MUHAVEN_ENTRY_POINT resolved to undefined \u2014 Path D requires the EntryPoint v0.7 address"
2223
+ };
2224
+ }
2225
+ if (typeof deps.chainId !== "number") {
2226
+ return {
2227
+ kind: "fallback",
2228
+ reason: "chain_id_unset",
2229
+ message: "MUHAVEN_CHAIN_ID not configured \u2014 Path D autonomous-buy requires a chain id for userOpHash"
2230
+ };
2231
+ }
2232
+ const subscriptionAddress = deps.subscriptionAddress;
2233
+ const entryPointAddress = deps.entryPointAddress;
2234
+ const chainId = deps.chainId;
2235
+ const preflight = await deps.broker.preflight();
2236
+ if (!preflight.supported) {
2237
+ if (preflight.reason === "broker_unreachable") {
2238
+ return {
2239
+ kind: "fallback",
2240
+ reason: "broker_unreachable",
2241
+ message: `broker daemon not reachable (${preflight.message}) \u2014 falling back to Path C dashboard deep-link`
2242
+ };
2243
+ }
2244
+ if (preflight.reason === "version_too_old") {
2245
+ return {
2246
+ kind: "fallback",
2247
+ reason: "version_too_old",
2248
+ message: `broker speaks ${preflight.daemonVersion}, Path D requires \u2265${preflight.requiredVersion} \u2014 upgrade @muhaven/mcp and restart the broker`
2249
+ };
2250
+ }
2251
+ return {
2252
+ kind: "fallback",
2253
+ reason: "session_key_unavailable",
2254
+ message: "broker is running in read-only posture (no MUHAVEN_BROKER_SESSION_KEY set) \u2014 Path D requires a loaded session key"
2255
+ };
2256
+ }
2257
+ let activeId;
2258
+ try {
2259
+ const res = await deps.broker.getActiveSessionId();
2260
+ activeId = res.sessionId;
2261
+ } catch (err2) {
2262
+ return mapBrokerCallFailure(err2, "get_active_session_id");
2263
+ }
2264
+ if (!activeId) {
2265
+ const synced = await syncSnapshotFromMirror(deps, preflight.signerAddress);
2266
+ if (synced.kind === "fallback") {
2267
+ return synced;
2268
+ }
2269
+ activeId = synced.sessionId;
2270
+ }
2271
+ let snapshot;
2272
+ try {
2273
+ const res = await deps.broker.getPolicySnapshot(activeId);
2274
+ snapshot = res.snapshot;
2275
+ } catch (err2) {
2276
+ return mapBrokerCallFailure(err2, "get_policy_snapshot", "snapshot_lookup_failed");
2277
+ }
2278
+ if (!snapshot) {
2279
+ return {
2280
+ kind: "fallback",
2281
+ reason: "no_active_snapshot",
2282
+ message: `broker reported session ${activeId} active but get_policy_snapshot returned null (race? \u2014 refresh tier from dashboard)`
2283
+ };
2284
+ }
2285
+ if (snapshot.signerAddress.toLowerCase() !== preflight.signerAddress.toLowerCase()) {
2286
+ return {
2287
+ kind: "fallback",
2288
+ reason: "signer_mismatch",
2289
+ 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`
2290
+ };
2291
+ }
2292
+ const purchaseCap = snapshot.selectorCaps.find(
2293
+ (c) => c.selector.toLowerCase() === SUBSCRIPTION_PURCHASE_SELECTOR
2294
+ );
2295
+ if (!purchaseCap) {
2296
+ return {
2297
+ kind: "fallback",
2298
+ reason: "selector_not_in_snapshot",
2299
+ message: "active scoped session does not authorize subscription.purchase \u2014 re-mint the session with a purchase cap"
2300
+ };
2301
+ }
2302
+ if (purchaseCap.maxAmount === null) {
2303
+ return {
2304
+ kind: "fallback",
2305
+ reason: "selector_uncapped",
2306
+ 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"
2307
+ };
2308
+ }
2309
+ const maxShares = BigInt(purchaseCap.maxAmount);
2310
+ if (shares > maxShares) {
2311
+ return {
2312
+ kind: "fallback",
2313
+ reason: "out_of_scope",
2314
+ 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`
2315
+ };
898
2316
  }
899
- }
900
- async function readYields(input, deps) {
2317
+ if (!snapshot.targetContracts.some(
2318
+ (t) => t.toLowerCase() === subscriptionAddress.toLowerCase()
2319
+ )) {
2320
+ return {
2321
+ kind: "fallback",
2322
+ reason: "target_not_in_snapshot",
2323
+ message: `subscription target ${subscriptionAddress} not in active session's target allowlist \u2014 re-mint the session with subscription in scope`
2324
+ };
2325
+ }
2326
+ let accountAddress;
901
2327
  try {
902
- const data = await deps.backend.get("/api/v1/yields", {
903
- token: input.token,
904
- limit: input.limit
2328
+ const stateDto = await deps.backend.get("/api/v1/agent/policy/state", {
2329
+ surface: "mcp"
905
2330
  });
906
- return ok(data);
907
- } catch (e) {
908
- return mapBackendError(e);
2331
+ if (!stateDto.accountAddress || !/^0x[0-9a-fA-F]{40}$/.test(stateDto.accountAddress)) {
2332
+ return {
2333
+ kind: "fallback",
2334
+ reason: "no_validator_registered",
2335
+ message: "backend /agent/policy/state returned no accountAddress \u2014 re-login the MCP"
2336
+ };
2337
+ }
2338
+ accountAddress = stateDto.accountAddress.toLowerCase();
2339
+ } catch (err2) {
2340
+ return {
2341
+ kind: "fallback",
2342
+ reason: "no_validator_registered",
2343
+ message: `backend /agent/policy/state lookup failed: ${err2 instanceof Error ? err2.message : String(err2)}`
2344
+ };
909
2345
  }
910
- }
911
- async function readDistribution(input, deps) {
2346
+ if (!snapshot.permissionId) {
2347
+ return {
2348
+ kind: "fallback",
2349
+ reason: "no_permission_id_in_snapshot",
2350
+ message: "active scoped session snapshot lacks permissionId \u2014 frontend storePolicySnapshot wire-up is a Slice 2 prerequisite; falling back to Path C"
2351
+ };
2352
+ }
2353
+ const permissionId = snapshot.permissionId;
2354
+ let encShares;
2355
+ let ephemeralEOA;
912
2356
  try {
913
- const data = await deps.backend.get("/api/v1/distributions", {
914
- token: input.token,
915
- epoch: input.epoch
2357
+ const enc = await deps.backend.post("/api/v1/agent/path-d/encrypt-shares", {
2358
+ tokenAddress,
2359
+ sharesAmount: shares.toString()
916
2360
  });
917
- return ok(data);
918
- } catch (e) {
919
- return mapBackendError(e);
2361
+ 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") {
2362
+ return {
2363
+ kind: "fallback",
2364
+ reason: "encrypt_shares_server_error",
2365
+ message: "backend /agent/path-d/encrypt-shares returned malformed payload"
2366
+ };
2367
+ }
2368
+ encShares = {
2369
+ ctHash: enc.encShares.ctHash,
2370
+ securityZone: enc.encShares.securityZone,
2371
+ utype: enc.encShares.utype,
2372
+ signature: enc.encShares.signature
2373
+ };
2374
+ ephemeralEOA = enc.ephemeralEOA;
2375
+ } catch (err2) {
2376
+ if (err2 instanceof BackendError) {
2377
+ const is4xx = typeof err2.status === "number" && err2.status < 500;
2378
+ return {
2379
+ kind: "fallback",
2380
+ reason: is4xx ? "encrypt_shares_rejected" : "encrypt_shares_server_error",
2381
+ message: `backend rejected encrypt-shares (backend.${err2.code})`
2382
+ };
2383
+ }
2384
+ return {
2385
+ kind: "fallback",
2386
+ reason: "encrypt_shares_server_error",
2387
+ message: `backend /agent/path-d/encrypt-shares failed: ${err2 instanceof Error ? err2.message : String(err2)}`
2388
+ };
920
2389
  }
921
- }
922
- async function readTokens(_input, deps) {
2390
+ const innerCallData = viem.encodeFunctionData({
2391
+ abi: SUBSCRIPTION_PURCHASE_ABI,
2392
+ functionName: "purchase",
2393
+ args: [
2394
+ tokenAddress,
2395
+ {
2396
+ ctHash: BigInt(encShares.ctHash),
2397
+ securityZone: encShares.securityZone,
2398
+ utype: encShares.utype,
2399
+ signature: encShares.signature
2400
+ },
2401
+ shares,
2402
+ // maxSharesHint — tight per spec
2403
+ ephemeralEOA
2404
+ ]
2405
+ });
2406
+ const kernelCallData = encodeKernelExecuteSingleCall({
2407
+ target: subscriptionAddress,
2408
+ value: 0n,
2409
+ callData: innerCallData
2410
+ });
2411
+ let nonce;
2412
+ let feeData;
923
2413
  try {
924
- const data = await deps.backend.get("/api/v1/tokens");
925
- return ok(data);
926
- } catch (e) {
927
- return mapBackendError(e);
2414
+ const nonceKey = composeKernelV3NonceKey({ permissionId });
2415
+ nonce = await deps.bundler.getNonce(accountAddress, entryPointAddress, nonceKey);
2416
+ feeData = await deps.bundler.getFeeData();
2417
+ } catch (err2) {
2418
+ return {
2419
+ kind: "fallback",
2420
+ reason: "bundler_setup_failed",
2421
+ message: `bundler bootstrap failed: ${err2 instanceof BundlerClientError ? `${err2.code}: ${err2.message}` : String(err2)}`
2422
+ };
928
2423
  }
929
- }
930
- async function readAudit(input, deps) {
2424
+ const partial = {
2425
+ sender: accountAddress,
2426
+ nonce: `0x${nonce.toString(16)}`,
2427
+ callData: kernelCallData,
2428
+ maxFeePerGas: feeData.maxFeePerGas,
2429
+ maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
2430
+ signature: PLACEHOLDER_SIGNATURE
2431
+ };
2432
+ let sponsored;
931
2433
  try {
932
- const data = await deps.backend.get("/api/v1/agent/policy/audit", {
933
- surface: input.surface,
934
- eventTypes: input.eventTypes?.join(","),
935
- since: input.since,
936
- until: input.until,
937
- cursor: input.cursor,
938
- limit: input.limit
939
- });
940
- return ok(data);
941
- } catch (e) {
942
- return mapBackendError(e);
2434
+ sponsored = await deps.bundler.sponsorUserOp(partial, entryPointAddress);
2435
+ } catch (err2) {
2436
+ const detail = err2 instanceof BundlerClientError && err2.detail && typeof err2.detail === "object" ? ` (rpc code=${err2.detail.code ?? "unknown"})` : "";
2437
+ const safeMsg = sanitizeRpcMessageForLlmContext(
2438
+ err2 instanceof Error ? err2.message : String(err2)
2439
+ );
2440
+ return {
2441
+ kind: "fallback",
2442
+ reason: "paymaster_rejected",
2443
+ message: `pm_sponsorUserOperation rejected${detail}: ${safeMsg}`
2444
+ };
943
2445
  }
944
- }
945
- async function readActivity(input, deps) {
2446
+ const userOpForHash = {
2447
+ sender: accountAddress,
2448
+ nonce,
2449
+ factory: void 0,
2450
+ factoryData: void 0,
2451
+ callData: kernelCallData,
2452
+ callGasLimit: BigInt(sponsored.callGasLimit),
2453
+ verificationGasLimit: BigInt(sponsored.verificationGasLimit),
2454
+ preVerificationGas: BigInt(sponsored.preVerificationGas),
2455
+ maxFeePerGas: BigInt(feeData.maxFeePerGas),
2456
+ maxPriorityFeePerGas: BigInt(feeData.maxPriorityFeePerGas),
2457
+ paymaster: sponsored.paymaster,
2458
+ paymasterVerificationGasLimit: BigInt(sponsored.paymasterVerificationGasLimit),
2459
+ paymasterPostOpGasLimit: BigInt(sponsored.paymasterPostOpGasLimit),
2460
+ paymasterData: sponsored.paymasterData,
2461
+ signature: PLACEHOLDER_SIGNATURE
2462
+ };
2463
+ const userOpHash = accountAbstraction.getUserOperationHash({
2464
+ userOperation: userOpForHash,
2465
+ entryPointAddress,
2466
+ entryPointVersion: "0.7",
2467
+ chainId
2468
+ });
2469
+ let brokerSig;
946
2470
  try {
947
- const data = await deps.backend.get("/api/v1/activity", {
948
- limit: input.limit,
949
- offset: input.offset
2471
+ const signed = await deps.broker.signUserOp({
2472
+ sessionId: activeId,
2473
+ userOpHash,
2474
+ innerCall: { target: subscriptionAddress, callData: innerCallData },
2475
+ intent: {
2476
+ tool: "muhaven.position.buy",
2477
+ summary: `${shares.toString()} shares of ${sanitizeSymbolForLlmContext(tokenSymbol)}`
2478
+ }
950
2479
  });
951
- return ok(data);
952
- } catch (e) {
953
- return mapBackendError(e);
2480
+ brokerSig = signed.signature;
2481
+ } catch (err2) {
2482
+ if (err2 instanceof BrokerClientError && err2.brokerCode) {
2483
+ const code = err2.brokerCode;
2484
+ if (code === "policy_violation") {
2485
+ return {
2486
+ kind: "fallback",
2487
+ reason: "broker_policy_violation",
2488
+ message: "broker rejected sign_userop: policy_violation (innerCall vs snapshot mismatch)"
2489
+ };
2490
+ }
2491
+ if (code === "scope_violation") {
2492
+ return {
2493
+ kind: "fallback",
2494
+ reason: "broker_scope_violation",
2495
+ message: "broker rejected sign_userop: scope_violation (snapshot expired between gate and sign)"
2496
+ };
2497
+ }
2498
+ if (code === "max_spend_exceeded") {
2499
+ return {
2500
+ kind: "fallback",
2501
+ reason: "broker_max_spend_exceeded",
2502
+ message: "broker rejected sign_userop: max_spend_exceeded (innerCall maxSharesHint over cap)"
2503
+ };
2504
+ }
2505
+ if (code === "no_active_snapshot") {
2506
+ return {
2507
+ kind: "fallback",
2508
+ reason: "broker_no_active_snapshot_at_sign",
2509
+ message: "broker reported no_active_snapshot at sign time \u2014 likely GC race after our snapshot read"
2510
+ };
2511
+ }
2512
+ }
2513
+ return mapBrokerCallFailure(err2, "sign_userop", "broker_internal");
2514
+ }
2515
+ const signedUserOpWire = {
2516
+ sender: accountAddress,
2517
+ nonce: partial.nonce,
2518
+ callData: kernelCallData,
2519
+ callGasLimit: sponsored.callGasLimit,
2520
+ verificationGasLimit: sponsored.verificationGasLimit,
2521
+ preVerificationGas: sponsored.preVerificationGas,
2522
+ maxFeePerGas: feeData.maxFeePerGas,
2523
+ maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
2524
+ paymaster: sponsored.paymaster,
2525
+ paymasterVerificationGasLimit: sponsored.paymasterVerificationGasLimit,
2526
+ paymasterPostOpGasLimit: sponsored.paymasterPostOpGasLimit,
2527
+ paymasterData: sponsored.paymasterData,
2528
+ signature: buildKernelSessionKeySignature({ ecdsaSignature: brokerSig })
2529
+ };
2530
+ let submittedHash;
2531
+ try {
2532
+ submittedHash = await deps.bundler.sendUserOp(signedUserOpWire, entryPointAddress);
2533
+ } catch (err2) {
2534
+ const detail = err2 instanceof BundlerClientError && err2.detail && typeof err2.detail === "object" ? ` (rpc code=${err2.detail.code ?? "unknown"})` : "";
2535
+ return {
2536
+ kind: "fallback",
2537
+ reason: "bundler_submit_rejected",
2538
+ message: `bundler eth_sendUserOperation rejected${detail}: ${err2 instanceof Error ? err2.message : String(err2)}`
2539
+ };
2540
+ }
2541
+ if (submittedHash.toLowerCase() !== userOpHash.toLowerCase()) {
2542
+ return {
2543
+ kind: "fallback",
2544
+ reason: "userop_hash_mismatch",
2545
+ message: `bundler reported userOpHash ${submittedHash} but we signed ${userOpHash} \u2014 refusing to wait for receipt`
2546
+ };
2547
+ }
2548
+ try {
2549
+ const receipt = await deps.bundler.waitForReceipt(userOpHash, { timeoutMs: 12e3 });
2550
+ return {
2551
+ kind: "ok",
2552
+ data: {
2553
+ action: "buy",
2554
+ status: "submitted",
2555
+ txHash: receipt.receipt.transactionHash,
2556
+ userOpHash,
2557
+ path: "D"
2558
+ }
2559
+ };
2560
+ } catch (err2) {
2561
+ try {
2562
+ const lateReceipt = await deps.bundler.getReceipt(userOpHash);
2563
+ if (lateReceipt) {
2564
+ return {
2565
+ kind: "ok",
2566
+ data: {
2567
+ action: "buy",
2568
+ status: "submitted",
2569
+ txHash: lateReceipt.receipt.transactionHash,
2570
+ userOpHash,
2571
+ path: "D"
2572
+ }
2573
+ };
2574
+ }
2575
+ } catch {
2576
+ }
2577
+ return {
2578
+ kind: "fallback",
2579
+ reason: "bundler_receipt_timeout",
2580
+ 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.`,
2581
+ submittedUserOpHash: userOpHash
2582
+ };
954
2583
  }
955
- }
956
- function buildPositionDeeplink(dashboardBaseUrl, action, params) {
957
- const base = dashboardBaseUrl.replace(/\/+$/, "");
958
- const path = action === "buy" || action === "sell" ? "/trade" : action === "claim" ? "/yields" : "/cash";
959
- const search = new URLSearchParams();
960
- if (action === "buy" || action === "sell") search.set("mode", action);
961
- for (const [k, v] of Object.entries(params)) search.set(k, v);
962
- search.set("from", "mcp");
963
- return `${base}${path}?${search.toString()}`;
964
- }
965
- function resolveDashboardBaseUrl(deps) {
966
- return deps.dashboardBaseUrl ?? "https://muhaven.app";
967
- }
968
- function resolveTokenInCatalog(identifier, catalog) {
969
- const needle = identifier.toLowerCase();
970
- return catalog.find(
971
- (t) => t.address.toLowerCase() === needle || t.symbol.toLowerCase() === needle
972
- ) ?? null;
973
- }
974
- function sanitizeSymbolForLlmContext(raw) {
975
- const cleaned = raw.replace(/[^A-Za-z0-9_-]/g, "?");
976
- return cleaned.length > 16 ? cleaned.slice(0, 16) : cleaned;
977
2584
  }
978
2585
  async function positionBuy(input, deps) {
979
2586
  let catalog;
@@ -1038,6 +2645,21 @@ async function positionBuy(input, deps) {
1038
2645
  const effectiveNotionalDisplay = formatUsd6AsDecimal(effectiveNotionalUsd6);
1039
2646
  const navDisplay = formatUsd6AsDecimal(navUsd6);
1040
2647
  const sharesStr = shares.toString();
2648
+ let pathDFallbackReason;
2649
+ let pathDSubmittedUserOpHash;
2650
+ const pathD = await attemptPathD(
2651
+ { shares, tokenAddress: token.address, tokenSymbol: token.symbol },
2652
+ deps
2653
+ );
2654
+ if (pathD.kind === "ok") {
2655
+ return ok(pathD.data);
2656
+ }
2657
+ if (pathD.kind === "fallback") {
2658
+ pathDFallbackReason = pathD.reason;
2659
+ if (pathD.submittedUserOpHash) {
2660
+ pathDSubmittedUserOpHash = pathD.submittedUserOpHash;
2661
+ }
2662
+ }
1041
2663
  const dashboardUrl = buildPositionDeeplink(resolveDashboardBaseUrl(deps), "buy", {
1042
2664
  token: token.symbol,
1043
2665
  amount: sharesStr
@@ -1057,7 +2679,9 @@ ${dashboardUrl}`,
1057
2679
  // shows the share count instead of the user-stated notional.
1058
2680
  amountUsdc: input.amountUsdc,
1059
2681
  effectiveNotionalUsd6: effectiveNotionalUsd6.toString(),
1060
- navUsd6: navUsd6.toString()
2682
+ navUsd6: navUsd6.toString(),
2683
+ ...pathDFallbackReason ? { pathDFallbackReason } : {},
2684
+ ...pathDSubmittedUserOpHash ? { pathDSubmittedUserOpHash } : {}
1061
2685
  }
1062
2686
  });
1063
2687
  }
@@ -1408,14 +3032,20 @@ var SERVER_NAME = "@muhaven/mcp";
1408
3032
  var SERVER_VERSION = resolveServerVersion();
1409
3033
  function resolveServerVersion() {
1410
3034
  {
1411
- return "0.2.1";
3035
+ return "0.2.3";
1412
3036
  }
1413
3037
  }
1414
3038
  function toJsonInputSchema(schema) {
1415
- return {
1416
- type: "object",
1417
- additionalProperties: false
1418
- };
3039
+ const json = zodToJsonSchema.zodToJsonSchema(schema, {
3040
+ target: "jsonSchema7",
3041
+ $refStrategy: "none",
3042
+ removeAdditionalStrategy: "strict"
3043
+ });
3044
+ if ("$schema" in json) {
3045
+ const { $schema: _drop, ...rest } = json;
3046
+ return rest;
3047
+ }
3048
+ return json;
1419
3049
  }
1420
3050
  async function loadPinnedToolHashes() {
1421
3051
  const here = path.dirname(url.fileURLToPath(importMetaUrl));
@@ -1425,7 +3055,7 @@ async function loadPinnedToolHashes() {
1425
3055
  ];
1426
3056
  for (const path of candidates) {
1427
3057
  try {
1428
- const raw = await promises.readFile(path, "utf8");
3058
+ const raw = await promises$1.readFile(path, "utf8");
1429
3059
  const parsed = JSON.parse(raw);
1430
3060
  if (Array.isArray(parsed?.tools)) return parsed.tools;
1431
3061
  } catch {
@@ -1484,8 +3114,12 @@ function buildMcpServer(opts) {
1484
3114
  const result = await entry.handler(parsed, {
1485
3115
  backend: opts.backend,
1486
3116
  broker: opts.broker,
3117
+ bundler: opts.bundler,
1487
3118
  surface: "mcp",
1488
- dashboardBaseUrl: opts.dashboardBaseUrl
3119
+ dashboardBaseUrl: opts.dashboardBaseUrl,
3120
+ chainId: opts.chainId,
3121
+ subscriptionAddress: opts.subscriptionAddress,
3122
+ entryPointAddress: opts.entryPointAddress
1489
3123
  });
1490
3124
  return toolJsonResponse(result);
1491
3125
  } catch (err2) {
@@ -1536,6 +3170,17 @@ async function runMcpStdioCli(opts = {}) {
1536
3170
  timeoutMs: config.requestTimeoutMs,
1537
3171
  allowedHosts: config.allowedBackendHosts
1538
3172
  });
3173
+ const bundler = config.bundlerUrl ? new BundlerClient({
3174
+ endpoint: config.bundlerUrl,
3175
+ requestTimeoutMs: config.bundlerTimeoutMs,
3176
+ expectedChainId: config.chainId,
3177
+ // Wave 5 Path D 0.2.3 — Origin defaults to the dashboard URL
3178
+ // so ZeroDev's domain-allowlist accepts the MCP server's RPC
3179
+ // traffic. The dashboard URL is the natural match because it's
3180
+ // also the SIWE / passkey origin the project already trusts
3181
+ // for browser-side traffic.
3182
+ originHeader: config.dashboardBaseUrl
3183
+ }) : void 0;
1539
3184
  const baseRegistry = selectRegistry(config.readOnly);
1540
3185
  const registry = opts.filterRegistry ? opts.filterRegistry(baseRegistry) : baseRegistry;
1541
3186
  if (registry.length === 0) {
@@ -1548,8 +3193,20 @@ async function runMcpStdioCli(opts = {}) {
1548
3193
  registry,
1549
3194
  backend,
1550
3195
  broker: config.readOnly ? void 0 : broker,
1551
- dashboardBaseUrl: config.dashboardBaseUrl
3196
+ bundler: config.readOnly ? void 0 : bundler,
3197
+ dashboardBaseUrl: config.dashboardBaseUrl,
3198
+ chainId: config.chainId,
3199
+ subscriptionAddress: config.subscriptionAddress,
3200
+ entryPointAddress: config.entryPointAddress
1552
3201
  });
3202
+ if (bundler && !config.readOnly) {
3203
+ void bundler.assertChainId().catch((err2) => {
3204
+ process.stderr.write(
3205
+ `[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
3206
+ `
3207
+ );
3208
+ });
3209
+ }
1553
3210
  const transport = new stdio_js.StdioServerTransport();
1554
3211
  await server.connect(transport);
1555
3212
  await new Promise((resolve) => {
@@ -1654,140 +3311,52 @@ var DeviceFlowClient = class {
1654
3311
  const body2 = await safeJson(res);
1655
3312
  throw new DeviceFlowAbortedError({ code: "invalid_response", status: res.status, body: body2 });
1656
3313
  }
1657
- const body = await safeJson(res);
1658
- if (!body || !body.deviceCode || !body.userCode) {
1659
- throw new DeviceFlowAbortedError({ code: "invalid_response", status: res.status, body });
1660
- }
1661
- const verificationUri = `${trim(this.options.dashboardBaseUrl)}/link`;
1662
- const verificationUriComplete = `${verificationUri}?code=${encodeURIComponent(body.userCode)}`;
1663
- return {
1664
- deviceCode: body.deviceCode,
1665
- userCode: body.userCode,
1666
- verificationUri,
1667
- verificationUriComplete,
1668
- expiresInSec: body.expiresInSec ?? 300,
1669
- pollIntervalSec: body.pollIntervalSec ?? Math.floor(DEFAULT_POLL_INTERVAL_MS / 1e3)
1670
- };
1671
- }
1672
- async pollOnce(deviceCode) {
1673
- const url = new URL("/api/v1/auth/device/token", this.options.backendBaseUrl);
1674
- let res;
1675
- try {
1676
- res = await this.fetchImpl(url, {
1677
- method: "POST",
1678
- headers: { "content-type": "application/json", accept: "application/json" },
1679
- body: JSON.stringify({ deviceCode })
1680
- });
1681
- } catch (err2) {
1682
- throw new DeviceFlowAbortedError({ code: "network", cause: err2 });
1683
- }
1684
- if (res.status === 429) {
1685
- return { state: "pending" };
1686
- }
1687
- const body = await safeJson(res);
1688
- if (!body || typeof body.state !== "string") {
1689
- throw new DeviceFlowAbortedError({ code: "invalid_response", status: res.status, body });
1690
- }
1691
- return body;
1692
- }
1693
- };
1694
- function trim(s) {
1695
- return s.endsWith("/") ? s.slice(0, -1) : s;
1696
- }
1697
- async function safeJson(res) {
1698
- try {
1699
- return await res.json();
1700
- } catch {
1701
- return null;
1702
- }
1703
- }
1704
-
1705
- // src/broker/protocol.ts
1706
- var BROKER_PROTOCOL_VERSION = "0.3.0";
1707
- var HASH_HEX_RE = /^0x[0-9a-fA-F]{64}$/;
1708
- var JWT_RE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
1709
- function isHashHex(value) {
1710
- return typeof value === "string" && HASH_HEX_RE.test(value);
1711
- }
1712
- function isJwtShape(value) {
1713
- return typeof value === "string" && value.length <= 8192 && JWT_RE.test(value);
1714
- }
1715
- function parseBrokerRequest(line) {
1716
- let parsed;
1717
- try {
1718
- parsed = JSON.parse(line);
1719
- } catch {
1720
- return { type: "error", code: "invalid_request", message: "request is not valid JSON" };
1721
- }
1722
- if (typeof parsed !== "object" || parsed === null) {
1723
- return { type: "error", code: "invalid_request", message: "request must be a JSON object" };
1724
- }
1725
- const obj = parsed;
1726
- switch (obj.type) {
1727
- case "hello":
1728
- return { type: "hello" };
1729
- case "sign_hash": {
1730
- const hash = obj.hash;
1731
- if (!isHashHex(hash)) {
1732
- return {
1733
- type: "error",
1734
- code: "invalid_request",
1735
- message: "sign_hash.hash must be a 0x-prefixed 32-byte hex string"
1736
- };
1737
- }
1738
- const intent = obj.intent;
1739
- const intentValid = intent === void 0 || typeof intent === "object" && intent !== null && typeof intent.tool === "string";
1740
- if (!intentValid) {
1741
- return {
1742
- type: "error",
1743
- code: "invalid_request",
1744
- message: "sign_hash.intent.tool must be a string when provided"
1745
- };
1746
- }
1747
- return {
1748
- type: "sign_hash",
1749
- hash,
1750
- ...intent === void 0 ? {} : { intent }
1751
- };
3314
+ const body = await safeJson(res);
3315
+ if (!body || !body.deviceCode || !body.userCode) {
3316
+ throw new DeviceFlowAbortedError({ code: "invalid_response", status: res.status, body });
1752
3317
  }
1753
- case "store_jwt": {
1754
- const jwt = obj.jwt;
1755
- if (!isJwtShape(jwt)) {
1756
- return {
1757
- type: "error",
1758
- code: "invalid_request",
1759
- message: "store_jwt.jwt must be a JWT-shaped string \u22648192 chars"
1760
- };
1761
- }
1762
- const expiresAtSec = obj.expiresAtSec;
1763
- const expiresValid = expiresAtSec === void 0 || typeof expiresAtSec === "number" && Number.isFinite(expiresAtSec) && expiresAtSec > 0;
1764
- if (!expiresValid) {
1765
- return {
1766
- type: "error",
1767
- code: "invalid_request",
1768
- message: "store_jwt.expiresAtSec must be a positive number when provided"
1769
- };
1770
- }
1771
- return {
1772
- type: "store_jwt",
1773
- jwt,
1774
- ...expiresAtSec === void 0 ? {} : { expiresAtSec }
1775
- };
3318
+ const verificationUri = `${trim(this.options.dashboardBaseUrl)}/link`;
3319
+ const verificationUriComplete = `${verificationUri}?code=${encodeURIComponent(body.userCode)}`;
3320
+ return {
3321
+ deviceCode: body.deviceCode,
3322
+ userCode: body.userCode,
3323
+ verificationUri,
3324
+ verificationUriComplete,
3325
+ expiresInSec: body.expiresInSec ?? 300,
3326
+ pollIntervalSec: body.pollIntervalSec ?? Math.floor(DEFAULT_POLL_INTERVAL_MS / 1e3)
3327
+ };
3328
+ }
3329
+ async pollOnce(deviceCode) {
3330
+ const url = new URL("/api/v1/auth/device/token", this.options.backendBaseUrl);
3331
+ let res;
3332
+ try {
3333
+ res = await this.fetchImpl(url, {
3334
+ method: "POST",
3335
+ headers: { "content-type": "application/json", accept: "application/json" },
3336
+ body: JSON.stringify({ deviceCode })
3337
+ });
3338
+ } catch (err2) {
3339
+ throw new DeviceFlowAbortedError({ code: "network", cause: err2 });
1776
3340
  }
1777
- case "get_jwt":
1778
- return { type: "get_jwt" };
1779
- case "clear_jwt":
1780
- return { type: "clear_jwt" };
1781
- default:
1782
- return {
1783
- type: "error",
1784
- code: "unsupported_type",
1785
- message: `unsupported request type: ${String(obj.type)}`
1786
- };
3341
+ if (res.status === 429) {
3342
+ return { state: "pending" };
3343
+ }
3344
+ const body = await safeJson(res);
3345
+ if (!body || typeof body.state !== "string") {
3346
+ throw new DeviceFlowAbortedError({ code: "invalid_response", status: res.status, body });
3347
+ }
3348
+ return body;
1787
3349
  }
3350
+ };
3351
+ function trim(s) {
3352
+ return s.endsWith("/") ? s.slice(0, -1) : s;
1788
3353
  }
1789
- function serializeResponse(res) {
1790
- return JSON.stringify(res) + "\n";
3354
+ async function safeJson(res) {
3355
+ try {
3356
+ return await res.json();
3357
+ } catch {
3358
+ return null;
3359
+ }
1791
3360
  }
1792
3361
  var MissingSessionKeyError = class extends Error {
1793
3362
  constructor() {
@@ -1803,6 +3372,9 @@ var NullSigner = class {
1803
3372
  async signHash(_hash) {
1804
3373
  throw new MissingSessionKeyError();
1805
3374
  }
3375
+ async signRawMessage(_hash) {
3376
+ throw new MissingSessionKeyError();
3377
+ }
1806
3378
  };
1807
3379
  var ViemSigner = class {
1808
3380
  account;
@@ -1815,6 +3387,9 @@ var ViemSigner = class {
1815
3387
  async signHash(hash) {
1816
3388
  return this.account.sign({ hash });
1817
3389
  }
3390
+ async signRawMessage(hash) {
3391
+ return this.account.signMessage({ message: { raw: hash } });
3392
+ }
1818
3393
  };
1819
3394
  var KEYRING_SERVICE = "muhaven.mcp";
1820
3395
  var KEYRING_ACCOUNT = "jwt";
@@ -1879,14 +3454,14 @@ var FileKeystore = class {
1879
3454
  }
1880
3455
  async set(record) {
1881
3456
  const parent = path.dirname(this.path);
1882
- await promises.mkdir(parent, { recursive: true, mode: 448 });
1883
- await promises.chmod(parent, 448).catch(() => void 0);
1884
- await promises.writeFile(this.path, JSON.stringify(record), { mode: 384 });
1885
- await promises.chmod(this.path, 384).catch(() => void 0);
3457
+ await promises$1.mkdir(parent, { recursive: true, mode: 448 });
3458
+ await promises$1.chmod(parent, 448).catch(() => void 0);
3459
+ await promises$1.writeFile(this.path, JSON.stringify(record), { mode: 384 });
3460
+ await promises$1.chmod(this.path, 384).catch(() => void 0);
1886
3461
  }
1887
3462
  async get() {
1888
3463
  try {
1889
- const raw = await promises.readFile(this.path, "utf8");
3464
+ const raw = await promises$1.readFile(this.path, "utf8");
1890
3465
  return parseRecord(raw);
1891
3466
  } catch (err2) {
1892
3467
  if (err2.code === "ENOENT") return null;
@@ -1895,7 +3470,7 @@ var FileKeystore = class {
1895
3470
  }
1896
3471
  async clear() {
1897
3472
  try {
1898
- await promises.unlink(this.path);
3473
+ await promises$1.unlink(this.path);
1899
3474
  } catch (err2) {
1900
3475
  if (err2.code === "ENOENT") return;
1901
3476
  throw new KeystoreError("file_clear_failed", asMessage(err2), err2);
@@ -1971,11 +3546,293 @@ async function openKeystore(options = {}) {
1971
3546
  }
1972
3547
  return { keystore: new OsKeystore(entry), fallbackReason: null };
1973
3548
  }
3549
+ var PolicyStoreError = class extends Error {
3550
+ constructor(code, message, cause) {
3551
+ super(message);
3552
+ this.code = code;
3553
+ this.cause = cause;
3554
+ this.name = "PolicyStoreError";
3555
+ }
3556
+ code;
3557
+ cause;
3558
+ };
3559
+ var SESSION_ID_RE2 = /^[A-Za-z0-9_-]{1,128}$/;
3560
+ var UINT256_MAX_LOCAL = (1n << 256n) - 1n;
3561
+ function validateSessionId(sessionId) {
3562
+ if (!SESSION_ID_RE2.test(sessionId)) {
3563
+ throw new PolicyStoreError(
3564
+ "invalid_session_id",
3565
+ `sessionId "${sessionId}" must be 1-128 chars [A-Za-z0-9_-] (path-traversal guard)`
3566
+ );
3567
+ }
3568
+ }
3569
+ var FilePolicyStore = class {
3570
+ constructor(dir) {
3571
+ this.dir = dir;
3572
+ }
3573
+ dir;
3574
+ static defaultDir() {
3575
+ return path.join(os.homedir(), ".muhaven", "policy-snapshots");
3576
+ }
3577
+ snapshotPath(sessionId) {
3578
+ return path.join(this.dir, `${sessionId}.json`);
3579
+ }
3580
+ async get(sessionId, nowSec) {
3581
+ validateSessionId(sessionId);
3582
+ let raw;
3583
+ try {
3584
+ raw = await promises$1.readFile(this.snapshotPath(sessionId), "utf8");
3585
+ } catch (err2) {
3586
+ if (err2.code === "ENOENT") return null;
3587
+ throw new PolicyStoreError(
3588
+ "read_failed",
3589
+ `failed to read snapshot ${sessionId}: ${asMessage2(err2)}`,
3590
+ err2
3591
+ );
3592
+ }
3593
+ let parsed;
3594
+ try {
3595
+ const obj = JSON.parse(raw);
3596
+ parsed = coerceFromDisk(obj);
3597
+ } catch (err2) {
3598
+ throw new PolicyStoreError(
3599
+ "malformed_record",
3600
+ `snapshot ${sessionId} is not valid JSON: ${asMessage2(err2)}`,
3601
+ err2
3602
+ );
3603
+ }
3604
+ if (parsed.validUntilSec <= nowSec) return null;
3605
+ return parsed;
3606
+ }
3607
+ async put(snapshot) {
3608
+ validateSessionId(snapshot.sessionId);
3609
+ const dest = this.snapshotPath(snapshot.sessionId);
3610
+ const tmp = `${dest}.tmp-${crypto.randomBytes(6).toString("hex")}`;
3611
+ try {
3612
+ await promises$1.mkdir(this.dir, { recursive: true, mode: 448 });
3613
+ await promises$1.chmod(this.dir, 448).catch(() => void 0);
3614
+ await promises$1.writeFile(tmp, JSON.stringify(snapshot), { mode: 384 });
3615
+ await promises$1.chmod(tmp, 384).catch(() => void 0);
3616
+ await promises$1.rename(tmp, dest);
3617
+ } catch (err2) {
3618
+ await promises$1.unlink(tmp).catch(() => void 0);
3619
+ throw new PolicyStoreError(
3620
+ "write_failed",
3621
+ `failed to write snapshot ${snapshot.sessionId}: ${asMessage2(err2)}`,
3622
+ err2
3623
+ );
3624
+ }
3625
+ }
3626
+ async delete(sessionId) {
3627
+ validateSessionId(sessionId);
3628
+ try {
3629
+ await promises$1.unlink(this.snapshotPath(sessionId));
3630
+ } catch (err2) {
3631
+ if (err2.code === "ENOENT") return;
3632
+ throw new PolicyStoreError(
3633
+ "delete_failed",
3634
+ `failed to delete snapshot ${sessionId}: ${asMessage2(err2)}`,
3635
+ err2
3636
+ );
3637
+ }
3638
+ }
3639
+ async list() {
3640
+ let entries;
3641
+ try {
3642
+ entries = await promises$1.readdir(this.dir);
3643
+ } catch (err2) {
3644
+ if (err2.code === "ENOENT") return [];
3645
+ throw new PolicyStoreError(
3646
+ "read_failed",
3647
+ `failed to enumerate snapshot dir: ${asMessage2(err2)}`,
3648
+ err2
3649
+ );
3650
+ }
3651
+ const out = [];
3652
+ for (const entry of entries) {
3653
+ if (!entry.endsWith(".json")) continue;
3654
+ const sessionId = entry.slice(0, -".json".length);
3655
+ if (!SESSION_ID_RE2.test(sessionId)) continue;
3656
+ try {
3657
+ const raw = await promises$1.readFile(path.join(this.dir, entry), "utf8");
3658
+ out.push(coerceFromDisk(JSON.parse(raw)));
3659
+ } catch {
3660
+ continue;
3661
+ }
3662
+ }
3663
+ return out;
3664
+ }
3665
+ async activeSessionId(activeSignerAddress, nowSec) {
3666
+ const all = await this.list();
3667
+ const needle = activeSignerAddress.toLowerCase();
3668
+ const matches = all.filter(
3669
+ (s) => s.validUntilSec > nowSec && s.signerAddress.toLowerCase() === needle
3670
+ );
3671
+ return matches.length === 1 ? matches[0].sessionId : null;
3672
+ }
3673
+ };
3674
+ function coerceFromDisk(obj) {
3675
+ if (typeof obj !== "object" || obj === null) throw new Error("snapshot not an object");
3676
+ const o = obj;
3677
+ if (typeof o.sessionId !== "string" || !SESSION_ID_RE2.test(o.sessionId)) {
3678
+ throw new Error("snapshot.sessionId malformed");
3679
+ }
3680
+ if (o.mode !== "scoped") throw new Error('snapshot.mode must be "scoped"');
3681
+ if (typeof o.signerAddress !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(o.signerAddress)) {
3682
+ throw new Error("snapshot.signerAddress malformed");
3683
+ }
3684
+ if (!Array.isArray(o.targetContracts) || !o.targetContracts.every((t) => typeof t === "string" && /^0x[0-9a-fA-F]{40}$/.test(t))) {
3685
+ throw new Error("snapshot.targetContracts malformed");
3686
+ }
3687
+ if (!Array.isArray(o.selectorCaps) || o.selectorCaps.length === 0) {
3688
+ throw new Error("snapshot.selectorCaps malformed");
3689
+ }
3690
+ const caps = [];
3691
+ for (const c of o.selectorCaps) {
3692
+ if (typeof c !== "object" || c === null) throw new Error("selectorCap not an object");
3693
+ const cap = c;
3694
+ if (typeof cap.selector !== "string" || !/^0x[0-9a-fA-F]{8}$/.test(cap.selector)) {
3695
+ throw new Error("selectorCap.selector malformed");
3696
+ }
3697
+ const indexNull = cap.capArgIndex === null;
3698
+ const amountNull = cap.maxAmount === null;
3699
+ if (indexNull !== amountNull) {
3700
+ throw new Error("selectorCap.capArgIndex/maxAmount must both be null or both non-null");
3701
+ }
3702
+ if (!indexNull) {
3703
+ if (typeof cap.capArgIndex !== "number" || !Number.isInteger(cap.capArgIndex) || cap.capArgIndex < 0 || cap.capArgIndex > 31) {
3704
+ throw new Error("selectorCap.capArgIndex must be an integer in [0, 31]");
3705
+ }
3706
+ if (typeof cap.maxAmount !== "string" || !/^(0|[1-9][0-9]{0,77})$/.test(cap.maxAmount)) {
3707
+ throw new Error("selectorCap.maxAmount malformed (length)");
3708
+ }
3709
+ if (BigInt(cap.maxAmount) > UINT256_MAX_LOCAL) {
3710
+ throw new Error("selectorCap.maxAmount exceeds uint256 max");
3711
+ }
3712
+ }
3713
+ caps.push({
3714
+ selector: cap.selector.toLowerCase(),
3715
+ capArgIndex: indexNull ? null : cap.capArgIndex,
3716
+ maxAmount: indexNull ? null : cap.maxAmount
3717
+ });
3718
+ }
3719
+ if (typeof o.validUntilSec !== "number" || o.validUntilSec <= 0) {
3720
+ throw new Error("snapshot.validUntilSec malformed");
3721
+ }
3722
+ if (typeof o.mintedAtSec !== "number" || o.mintedAtSec <= 0) {
3723
+ throw new Error("snapshot.mintedAtSec malformed");
3724
+ }
3725
+ if (o.consentActionHash !== void 0) {
3726
+ if (typeof o.consentActionHash !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(o.consentActionHash)) {
3727
+ throw new Error("snapshot.consentActionHash malformed");
3728
+ }
3729
+ }
3730
+ if (o.consentTextSha256 !== void 0) {
3731
+ if (typeof o.consentTextSha256 !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(o.consentTextSha256)) {
3732
+ throw new Error("snapshot.consentTextSha256 malformed");
3733
+ }
3734
+ }
3735
+ if (o.permissionId !== void 0) {
3736
+ if (typeof o.permissionId !== "string" || !/^0x[0-9a-fA-F]{8}$/.test(o.permissionId)) {
3737
+ throw new Error("snapshot.permissionId malformed (must be 0x-prefixed 4-byte hex)");
3738
+ }
3739
+ }
3740
+ return {
3741
+ sessionId: o.sessionId,
3742
+ mode: "scoped",
3743
+ signerAddress: o.signerAddress.toLowerCase(),
3744
+ targetContracts: o.targetContracts.map(
3745
+ (t) => t.toLowerCase()
3746
+ ),
3747
+ selectorCaps: caps,
3748
+ validUntilSec: o.validUntilSec,
3749
+ mintedAtSec: o.mintedAtSec,
3750
+ ...o.consentActionHash === void 0 ? {} : { consentActionHash: o.consentActionHash.toLowerCase() },
3751
+ ...o.consentTextSha256 === void 0 ? {} : { consentTextSha256: o.consentTextSha256.toLowerCase() },
3752
+ ...o.permissionId === void 0 ? {} : { permissionId: o.permissionId.toLowerCase() }
3753
+ };
3754
+ }
3755
+ function asMessage2(err2) {
3756
+ return err2 instanceof Error ? err2.message : String(err2);
3757
+ }
3758
+ function decodeUint256ArgAt(callDataHex, wordIndex) {
3759
+ if (!Number.isInteger(wordIndex) || wordIndex < 0) {
3760
+ throw new Error(`wordIndex must be a non-negative integer (got ${wordIndex})`);
3761
+ }
3762
+ const requiredLen = 2 + 8 + (wordIndex + 1) * 64;
3763
+ if (callDataHex.length < requiredLen) {
3764
+ throw new Error(
3765
+ `callData length ${callDataHex.length} too short to carry word ${wordIndex} (need \u2265${requiredLen} chars)`
3766
+ );
3767
+ }
3768
+ const wordStart = 2 + 8 + wordIndex * 64;
3769
+ const argHex = callDataHex.slice(wordStart, wordStart + 64);
3770
+ return BigInt(`0x${argHex}`);
3771
+ }
3772
+ function selectorOf(callDataHex) {
3773
+ return callDataHex.slice(0, 10).toLowerCase();
3774
+ }
3775
+ function checkPolicy(input) {
3776
+ const { snapshot, innerCall, activeSigner, nowSec } = input;
3777
+ if (snapshot.validUntilSec <= nowSec) {
3778
+ return {
3779
+ ok: false,
3780
+ code: "scope_violation",
3781
+ message: `snapshot ${snapshot.sessionId} expired at ${snapshot.validUntilSec} (now ${nowSec})`
3782
+ };
3783
+ }
3784
+ if (snapshot.signerAddress.toLowerCase() !== activeSigner.toLowerCase()) {
3785
+ return {
3786
+ ok: false,
3787
+ code: "policy_violation",
3788
+ message: `snapshot ${snapshot.sessionId} bound to signer ${snapshot.signerAddress}, broker active signer is ${activeSigner}`
3789
+ };
3790
+ }
3791
+ const targetLower = innerCall.target.toLowerCase();
3792
+ const targetMatch = snapshot.targetContracts.some((t) => t === targetLower);
3793
+ if (!targetMatch) {
3794
+ return {
3795
+ ok: false,
3796
+ code: "policy_violation",
3797
+ message: `target ${innerCall.target} not in allowlist for session ${snapshot.sessionId}`
3798
+ };
3799
+ }
3800
+ const selector = selectorOf(innerCall.callData);
3801
+ const rule = snapshot.selectorCaps.find((c) => c.selector === selector);
3802
+ if (!rule) {
3803
+ return {
3804
+ ok: false,
3805
+ code: "policy_violation",
3806
+ message: `selector ${selector} not in selectorCaps for session ${snapshot.sessionId}`
3807
+ };
3808
+ }
3809
+ if (rule.capArgIndex !== null && rule.maxAmount !== null) {
3810
+ let argValue;
3811
+ try {
3812
+ argValue = decodeUint256ArgAt(innerCall.callData, rule.capArgIndex);
3813
+ } catch (err2) {
3814
+ return {
3815
+ ok: false,
3816
+ code: "policy_violation",
3817
+ message: `callData decode failed at wordIndex ${rule.capArgIndex}: ${asMessage2(err2)}`
3818
+ };
3819
+ }
3820
+ const cap = BigInt(rule.maxAmount);
3821
+ if (argValue > cap) {
3822
+ return {
3823
+ ok: false,
3824
+ code: "max_spend_exceeded",
3825
+ message: `arg word[${rule.capArgIndex}] = ${argValue} exceeds maxAmount ${cap} for selector ${selector} (session ${snapshot.sessionId})`
3826
+ };
3827
+ }
3828
+ }
3829
+ return { ok: true };
3830
+ }
1974
3831
 
1975
3832
  // src/broker/daemon.ts
1976
3833
  var noopLogger = (_e) => {
1977
3834
  };
1978
- async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.floor(Date.now() / 1e3), options = {}) {
3835
+ async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.floor(Date.now() / 1e3), options = {}, policyStore) {
1979
3836
  switch (req.type) {
1980
3837
  case "hello": {
1981
3838
  let hasJwt = false;
@@ -2048,6 +3905,141 @@ async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.fl
2048
3905
  );
2049
3906
  }
2050
3907
  }
3908
+ case "sign_userop": {
3909
+ if (!policyStore) {
3910
+ return errorResponse(
3911
+ "internal",
3912
+ "broker daemon is not configured with a policy store"
3913
+ );
3914
+ }
3915
+ const now = nowSec();
3916
+ let snapshot;
3917
+ try {
3918
+ snapshot = await policyStore.get(req.sessionId, now);
3919
+ } catch (err2) {
3920
+ return errorResponse(
3921
+ "internal",
3922
+ err2 instanceof Error ? err2.message : "policy store read failed"
3923
+ );
3924
+ }
3925
+ if (!snapshot) {
3926
+ return errorResponse(
3927
+ "no_active_snapshot",
3928
+ `no active snapshot for session ${req.sessionId}`
3929
+ );
3930
+ }
3931
+ const check = checkPolicy({
3932
+ snapshot,
3933
+ innerCall: req.innerCall,
3934
+ activeSigner: signer.address,
3935
+ nowSec: now
3936
+ });
3937
+ if (!check.ok) {
3938
+ return errorResponse(check.code, check.message);
3939
+ }
3940
+ try {
3941
+ const signature = await signer.signRawMessage(req.userOpHash);
3942
+ return {
3943
+ type: "sign_userop",
3944
+ signature,
3945
+ signerAddress: signer.address,
3946
+ sessionId: req.sessionId
3947
+ };
3948
+ } catch (err2) {
3949
+ if (err2 instanceof MissingSessionKeyError) {
3950
+ return errorResponse("session_key_unavailable", err2.message);
3951
+ }
3952
+ throw err2;
3953
+ }
3954
+ }
3955
+ case "store_policy_snapshot": {
3956
+ if (!policyStore) {
3957
+ return errorResponse(
3958
+ "internal",
3959
+ "broker daemon is not configured with a policy store"
3960
+ );
3961
+ }
3962
+ try {
3963
+ await policyStore.put(req.snapshot);
3964
+ return {
3965
+ type: "store_policy_snapshot",
3966
+ stored: true,
3967
+ sessionId: req.snapshot.sessionId
3968
+ };
3969
+ } catch (err2) {
3970
+ if (err2 instanceof PolicyStoreError) {
3971
+ return errorResponse("internal", err2.message);
3972
+ }
3973
+ return errorResponse(
3974
+ "internal",
3975
+ err2 instanceof Error ? err2.message : "policy store write failed"
3976
+ );
3977
+ }
3978
+ }
3979
+ case "get_policy_snapshot": {
3980
+ if (!policyStore) {
3981
+ return errorResponse(
3982
+ "internal",
3983
+ "broker daemon is not configured with a policy store"
3984
+ );
3985
+ }
3986
+ try {
3987
+ const snapshot = await policyStore.get(req.sessionId, nowSec());
3988
+ return { type: "get_policy_snapshot", snapshot };
3989
+ } catch (err2) {
3990
+ if (err2 instanceof PolicyStoreError) {
3991
+ return errorResponse("internal", err2.message);
3992
+ }
3993
+ return errorResponse(
3994
+ "internal",
3995
+ err2 instanceof Error ? err2.message : "policy store read failed"
3996
+ );
3997
+ }
3998
+ }
3999
+ case "clear_policy_snapshot": {
4000
+ if (!policyStore) {
4001
+ return errorResponse(
4002
+ "internal",
4003
+ "broker daemon is not configured with a policy store"
4004
+ );
4005
+ }
4006
+ try {
4007
+ await policyStore.delete(req.sessionId);
4008
+ return {
4009
+ type: "clear_policy_snapshot",
4010
+ cleared: true,
4011
+ sessionId: req.sessionId
4012
+ };
4013
+ } catch (err2) {
4014
+ if (err2 instanceof PolicyStoreError) {
4015
+ return errorResponse("internal", err2.message);
4016
+ }
4017
+ return errorResponse(
4018
+ "internal",
4019
+ err2 instanceof Error ? err2.message : "policy store clear failed"
4020
+ );
4021
+ }
4022
+ }
4023
+ case "get_active_session_id": {
4024
+ if (!policyStore) {
4025
+ return errorResponse(
4026
+ "internal",
4027
+ "broker daemon is not configured with a policy store"
4028
+ );
4029
+ }
4030
+ try {
4031
+ const sessionId = await policyStore.activeSessionId(signer.address, nowSec());
4032
+ return { type: "get_active_session_id", sessionId };
4033
+ } catch (err2) {
4034
+ if (err2 instanceof PolicyStoreError) {
4035
+ return errorResponse("internal", err2.message);
4036
+ }
4037
+ return errorResponse(
4038
+ "internal",
4039
+ err2 instanceof Error ? err2.message : "policy store enumerate failed"
4040
+ );
4041
+ }
4042
+ }
2051
4043
  }
2052
4044
  }
2053
4045
  function errorResponse(code, message) {
@@ -2056,12 +4048,12 @@ function errorResponse(code, message) {
2056
4048
  async function prepareEndpoint(endpoint) {
2057
4049
  if (os.platform() === "win32") return;
2058
4050
  const parent = path.dirname(endpoint);
2059
- await promises.mkdir(parent, { recursive: true, mode: 448 });
2060
- await promises.chmod(parent, 448);
4051
+ await promises$1.mkdir(parent, { recursive: true, mode: 448 });
4052
+ await promises$1.chmod(parent, 448);
2061
4053
  try {
2062
- const s = await promises.stat(endpoint);
4054
+ const s = await promises$1.stat(endpoint);
2063
4055
  if (s.isSocket() || s.isFIFO()) {
2064
- await promises.unlink(endpoint);
4056
+ await promises$1.unlink(endpoint);
2065
4057
  }
2066
4058
  } catch (err2) {
2067
4059
  if (err2.code !== "ENOENT") throw err2;
@@ -2069,7 +4061,7 @@ async function prepareEndpoint(endpoint) {
2069
4061
  }
2070
4062
  async function applySocketPermissions(endpoint) {
2071
4063
  if (os.platform() === "win32") return;
2072
- await promises.chmod(endpoint, 384);
4064
+ await promises$1.chmod(endpoint, 384);
2073
4065
  }
2074
4066
  var BrokerDaemon = class {
2075
4067
  server;
@@ -2077,6 +4069,7 @@ var BrokerDaemon = class {
2077
4069
  log;
2078
4070
  config;
2079
4071
  keystore;
4072
+ policyStore;
2080
4073
  /**
2081
4074
  * Whether a session-key private half is actually loaded. `false` =
2082
4075
  * daemon booted in read-only posture (no `MUHAVEN_BROKER_SESSION_KEY`
@@ -2096,6 +4089,7 @@ var BrokerDaemon = class {
2096
4089
  this.hasSessionKey = false;
2097
4090
  }
2098
4091
  this.keystore = options.keystore ?? null;
4092
+ this.policyStore = options.policyStore ?? new FilePolicyStore(FilePolicyStore.defaultDir());
2099
4093
  this.log = options.logger ?? noopLogger;
2100
4094
  this.server = net.createServer((socket) => this.onConnection(socket));
2101
4095
  }
@@ -2111,6 +4105,7 @@ var BrokerDaemon = class {
2111
4105
  });
2112
4106
  }
2113
4107
  }
4108
+ if (this.policyStore.init) await this.policyStore.init();
2114
4109
  await prepareEndpoint(this.config.endpoint);
2115
4110
  await new Promise((resolve, reject) => {
2116
4111
  const onError = (err2) => {
@@ -2145,7 +4140,7 @@ var BrokerDaemon = class {
2145
4140
  });
2146
4141
  if (os.platform() !== "win32") {
2147
4142
  try {
2148
- await promises.unlink(this.config.endpoint);
4143
+ await promises$1.unlink(this.config.endpoint);
2149
4144
  } catch (err2) {
2150
4145
  if (err2.code !== "ENOENT") {
2151
4146
  this.log({ level: "warn", msg: "failed to unlink socket on stop", meta: { err: err2 } });
@@ -2226,7 +4221,8 @@ var BrokerDaemon = class {
2226
4221
  dashboardBaseUrl: this.config.dashboardBaseUrl
2227
4222
  },
2228
4223
  pid: process.pid
2229
- }
4224
+ },
4225
+ this.policyStore
2230
4226
  );
2231
4227
  socket.end(serializeResponse(res));
2232
4228
  } catch (err2) {
@@ -2248,6 +4244,8 @@ exports.BackendError = BackendError;
2248
4244
  exports.BrokerClient = BrokerClient;
2249
4245
  exports.BrokerClientError = BrokerClientError;
2250
4246
  exports.BrokerDaemon = BrokerDaemon;
4247
+ exports.BundlerClient = BundlerClient;
4248
+ exports.BundlerClientError = BundlerClientError;
2251
4249
  exports.DeviceFlowAbortedError = DeviceFlowAbortedError;
2252
4250
  exports.DeviceFlowClient = DeviceFlowClient;
2253
4251
  exports.JwtSource = JwtSource;
@@ -2272,5 +4270,6 @@ exports.parseBrokerRequest = parseBrokerRequest;
2272
4270
  exports.registryForReadOnly = registryForReadOnly;
2273
4271
  exports.runMcpStdioCli = runMcpStdioCli;
2274
4272
  exports.selectRegistry = selectRegistry;
4273
+ exports.semverGte = semverGte;
2275
4274
  exports.serializeResponse = serializeResponse;
2276
4275
  exports.verifyDescriptorAgainstPin = verifyDescriptorAgainstPin;