@muhaven/mcp 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import { zodToJsonSchema } from 'zod-to-json-schema';
10
10
  import { platform, homedir } from 'os';
11
11
  import { connect, createServer } from 'net';
12
12
  import { setTimeout as setTimeout$1 } from 'timers/promises';
13
- import { parseAbi, toFunctionSelector, encodeFunctionData, encodePacked, pad, concatHex, decodeAbiParameters } from 'viem';
13
+ import { parseAbi, toFunctionSelector, encodeFunctionData, toEventSelector, decodeFunctionData, encodePacked, pad, concatHex, decodeAbiParameters, encodeAbiParameters, parseAbiParameters } from 'viem';
14
14
  import { createHash, randomBytes } from 'crypto';
15
15
  import { getUserOperationHash } from 'viem/account-abstraction';
16
16
  import { privateKeyToAccount } from 'viem/accounts';
@@ -169,22 +169,45 @@ function loadBrokerConfig(env = process.env) {
169
169
  env.MUHAVEN_DASHBOARD_URL,
170
170
  DEFAULT_DASHBOARD_URL
171
171
  );
172
+ const chainRpcUrlRaw = readEnv("MUHAVEN_BROKER_RPC_URL", env) ?? readEnv("MUHAVEN_BUNDLER_URL", env);
173
+ const chainRpcUrl = chainRpcUrlRaw === void 0 ? void 0 : resolvePublicUrlEnv(
174
+ "MUHAVEN_BROKER_RPC_URL",
175
+ chainRpcUrlRaw,
176
+ chainRpcUrlRaw
177
+ );
178
+ const callbackServiceSecretRaw = readEnv("BROKER_CALLBACK_SERVICE_SECRET", env);
179
+ let callbackServiceSecret;
180
+ if (callbackServiceSecretRaw !== void 0) {
181
+ if (callbackServiceSecretRaw.length < 16) {
182
+ throw new Error(
183
+ "BROKER_CALLBACK_SERVICE_SECRET must be at least 16 characters (matches backend with-service-secret middleware floor)"
184
+ );
185
+ }
186
+ callbackServiceSecret = callbackServiceSecretRaw;
187
+ }
188
+ const outboundOriginHeader = readEnv("MUHAVEN_BROKER_ORIGIN", env) ?? dashboardBaseUrl;
172
189
  return {
173
190
  endpoint,
174
191
  sessionKeyHex,
175
192
  maxRequestBytes,
176
193
  requestTimeoutMs,
177
194
  backendBaseUrl,
178
- dashboardBaseUrl
195
+ dashboardBaseUrl,
196
+ chainRpcUrl,
197
+ callbackServiceSecret,
198
+ outboundOriginHeader
179
199
  };
180
200
  }
181
201
 
182
202
  // src/broker/protocol.ts
183
- var BROKER_PROTOCOL_VERSION = "0.4.0";
203
+ var BROKER_PROTOCOL_VERSION = "0.5.0";
184
204
  var HASH_HEX_RE = /^0x[0-9a-fA-F]{64}$/;
185
205
  var ADDRESS_HEX_RE2 = /^0x[0-9a-fA-F]{40}$/;
186
206
  var SELECTOR_HEX_RE = /^0x[0-9a-fA-F]{8}$/;
187
207
  var HEX_PREFIXED_RE = /^0x[0-9a-fA-F]*$/;
208
+ var ENABLE_DATA_HEX_RE = /^0x[0-9a-fA-F]{2,65536}$/;
209
+ var ENABLE_SIG_HEX_RE = /^0x[0-9a-fA-F]{256,16384}$/;
210
+ var UINT32_MAX = 4294967295;
188
211
  var JWT_RE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
189
212
  var SESSION_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
190
213
  var UINT256_DEC_RE = /^(0|[1-9][0-9]{0,77})$/;
@@ -304,6 +327,43 @@ function parsePolicySnapshot(raw) {
304
327
  if (!isOptionalPermissionId(obj.permissionId)) {
305
328
  return { error: "snapshot.permissionId must be a 0x-prefixed 4-byte hex when provided" };
306
329
  }
330
+ const enableData = obj.enableData;
331
+ const enableSig = obj.enableSig;
332
+ const validatorNonce = obj.validatorNonce;
333
+ const installPresent = [enableData, enableSig, validatorNonce].filter(
334
+ (v) => v !== void 0
335
+ ).length;
336
+ if (installPresent !== 0 && installPresent !== 3) {
337
+ return {
338
+ error: "snapshot.{enableData,enableSig,validatorNonce} must be all-present or all-absent (Option D Commit 3 install-material trio)"
339
+ };
340
+ }
341
+ if (enableData !== void 0) {
342
+ if (typeof enableData !== "string" || !ENABLE_DATA_HEX_RE.test(enableData)) {
343
+ return {
344
+ error: "snapshot.enableData must be a 0x-prefixed hex string of 2..65536 chars when provided"
345
+ };
346
+ }
347
+ }
348
+ if (enableSig !== void 0) {
349
+ if (typeof enableSig !== "string" || !ENABLE_SIG_HEX_RE.test(enableSig)) {
350
+ return {
351
+ error: "snapshot.enableSig must be a 0x-prefixed hex string of 256..16384 chars (WebAuthn envelope) when provided"
352
+ };
353
+ }
354
+ }
355
+ if (validatorNonce !== void 0) {
356
+ if (typeof validatorNonce !== "number" || !Number.isInteger(validatorNonce) || validatorNonce < 0 || validatorNonce > UINT32_MAX) {
357
+ return {
358
+ error: "snapshot.validatorNonce must be an integer in [0, 2^32-1] when provided"
359
+ };
360
+ }
361
+ }
362
+ if (installPresent === 3 && obj.permissionId === void 0) {
363
+ return {
364
+ error: "snapshot install material requires permissionId in the same snapshot"
365
+ };
366
+ }
307
367
  return {
308
368
  sessionId: obj.sessionId,
309
369
  mode: "scoped",
@@ -316,7 +376,10 @@ function parsePolicySnapshot(raw) {
316
376
  mintedAtSec: obj.mintedAtSec,
317
377
  ...obj.consentActionHash === void 0 ? {} : { consentActionHash: obj.consentActionHash.toLowerCase() },
318
378
  ...obj.consentTextSha256 === void 0 ? {} : { consentTextSha256: obj.consentTextSha256.toLowerCase() },
319
- ...obj.permissionId === void 0 ? {} : { permissionId: obj.permissionId.toLowerCase() }
379
+ ...obj.permissionId === void 0 ? {} : { permissionId: obj.permissionId.toLowerCase() },
380
+ ...enableData === void 0 ? {} : { enableData: enableData.toLowerCase() },
381
+ ...enableSig === void 0 ? {} : { enableSig: enableSig.toLowerCase() },
382
+ ...validatorNonce === void 0 ? {} : { validatorNonce }
320
383
  };
321
384
  }
322
385
  function parseBrokerRequest(line) {
@@ -495,6 +558,72 @@ function parseBrokerRequest(line) {
495
558
  }
496
559
  case "get_active_session_id":
497
560
  return { type: "get_active_session_id" };
561
+ case "current_nonce": {
562
+ if (!isAddressHex(obj.accountAddress)) {
563
+ return {
564
+ type: "error",
565
+ code: "invalid_request",
566
+ message: "current_nonce.accountAddress must be a 0x-prefixed 20-byte hex"
567
+ };
568
+ }
569
+ return {
570
+ type: "current_nonce",
571
+ accountAddress: obj.accountAddress.toLowerCase()
572
+ };
573
+ }
574
+ case "notify_userop_landed": {
575
+ if (!isSessionIdShape(obj.sessionId)) {
576
+ return {
577
+ type: "error",
578
+ code: "invalid_request",
579
+ message: "notify_userop_landed.sessionId must be 1-128 chars [A-Za-z0-9_-]"
580
+ };
581
+ }
582
+ if (!isAddressHex(obj.accountAddress)) {
583
+ return {
584
+ type: "error",
585
+ code: "invalid_request",
586
+ message: "notify_userop_landed.accountAddress must be a 0x-prefixed 20-byte hex"
587
+ };
588
+ }
589
+ if (!isSelectorHex(obj.permissionId)) {
590
+ return {
591
+ type: "error",
592
+ code: "invalid_request",
593
+ message: "notify_userop_landed.permissionId must be a 0x-prefixed 4-byte hex"
594
+ };
595
+ }
596
+ if (!isHashHex(obj.txHash)) {
597
+ return {
598
+ type: "error",
599
+ code: "invalid_request",
600
+ message: "notify_userop_landed.txHash must be a 0x-prefixed 32-byte hex"
601
+ };
602
+ }
603
+ if (typeof obj.blockNumber !== "number" || !Number.isFinite(obj.blockNumber) || !Number.isInteger(obj.blockNumber) || obj.blockNumber < 0) {
604
+ return {
605
+ type: "error",
606
+ code: "invalid_request",
607
+ message: "notify_userop_landed.blockNumber must be a non-negative integer"
608
+ };
609
+ }
610
+ if (typeof obj.logIndex !== "number" || !Number.isFinite(obj.logIndex) || !Number.isInteger(obj.logIndex) || obj.logIndex < 0) {
611
+ return {
612
+ type: "error",
613
+ code: "invalid_request",
614
+ message: "notify_userop_landed.logIndex must be a non-negative integer"
615
+ };
616
+ }
617
+ return {
618
+ type: "notify_userop_landed",
619
+ sessionId: obj.sessionId,
620
+ accountAddress: obj.accountAddress.toLowerCase(),
621
+ permissionId: obj.permissionId.toLowerCase(),
622
+ txHash: obj.txHash.toLowerCase(),
623
+ blockNumber: obj.blockNumber,
624
+ logIndex: obj.logIndex
625
+ };
626
+ }
498
627
  default:
499
628
  return {
500
629
  type: "error",
@@ -654,6 +783,33 @@ var BrokerClient = class {
654
783
  }
655
784
  return res;
656
785
  }
786
+ // ── Wave 5 Option D Commit 3 — install-mode pre-check + callback notify ──
787
+ async currentNonce(accountAddress) {
788
+ const res = await this.exchange({ type: "current_nonce", accountAddress });
789
+ if (res.type !== "current_nonce") {
790
+ throw new BrokerClientError(
791
+ "protocol_error",
792
+ `expected current_nonce response, got ${res.type}`
793
+ );
794
+ }
795
+ if (res.accountAddress.toLowerCase() !== accountAddress.toLowerCase()) {
796
+ throw new BrokerClientError(
797
+ "protocol_error",
798
+ `current_nonce echo mismatch (requested ${accountAddress}, got ${res.accountAddress})`
799
+ );
800
+ }
801
+ return res;
802
+ }
803
+ async notifyUseropLanded(args) {
804
+ const res = await this.exchange({ type: "notify_userop_landed", ...args });
805
+ if (res.type !== "notify_userop_landed") {
806
+ throw new BrokerClientError(
807
+ "protocol_error",
808
+ `expected notify_userop_landed response, got ${res.type}`
809
+ );
810
+ }
811
+ return res;
812
+ }
657
813
  /**
658
814
  * Detect whether the running daemon speaks Path D (protocol 0.4.0+).
659
815
  * Wraps `hello()` with a semver-gte comparison so the MCP tool layer
@@ -928,6 +1084,9 @@ var BackendClient = class {
928
1084
  const jwt = await this.options.jwtSource.get();
929
1085
  headers.authorization = `Bearer ${jwt}`;
930
1086
  }
1087
+ return this.runFetch(method, url, body, headers);
1088
+ }
1089
+ async runFetch(method, url, body, headers) {
931
1090
  const ctrl = new AbortController();
932
1091
  const timer = setTimeout(() => ctrl.abort(), this.options.timeoutMs);
933
1092
  let res;
@@ -1137,12 +1296,12 @@ var BundlerClient = class _BundlerClient {
1137
1296
  }
1138
1297
  const obj = result;
1139
1298
  return {
1140
- callGasLimit: assertHex(obj.callGasLimit, "estimateUserOpGas.callGasLimit"),
1141
- verificationGasLimit: assertHex(
1299
+ callGasLimit: assertHexQuantity(obj.callGasLimit, "estimateUserOpGas.callGasLimit"),
1300
+ verificationGasLimit: assertHexQuantity(
1142
1301
  obj.verificationGasLimit,
1143
1302
  "estimateUserOpGas.verificationGasLimit"
1144
1303
  ),
1145
- preVerificationGas: assertHex(
1304
+ preVerificationGas: assertHexQuantity(
1146
1305
  obj.preVerificationGas,
1147
1306
  "estimateUserOpGas.preVerificationGas"
1148
1307
  )
@@ -1512,8 +1671,19 @@ function assertHex(value, label) {
1512
1671
  }
1513
1672
  return value;
1514
1673
  }
1674
+ function assertHexQuantity(value, label) {
1675
+ if (typeof value !== "string" || !/^0x[0-9a-fA-F]+$/.test(value)) {
1676
+ const repr = value === void 0 ? "undefined" : JSON.stringify(value);
1677
+ const safe = typeof repr === "string" ? repr.slice(0, 80) : "unknown";
1678
+ throw new BundlerClientError(
1679
+ "invalid_response",
1680
+ `${label} must be a 0x-prefixed hex quantity (got ${safe})`
1681
+ );
1682
+ }
1683
+ return value;
1684
+ }
1515
1685
  function assertHexNonZero(value, label, maxValue) {
1516
- const hex = assertHex(value, label);
1686
+ const hex = assertHexQuantity(value, label);
1517
1687
  if (hex.length === 2 || BigInt(hex) === 0n) {
1518
1688
  throw new BundlerClientError(
1519
1689
  "invalid_response",
@@ -1964,6 +2134,26 @@ function encodeKernelExecuteSingleCall(input) {
1964
2134
  args: [KERNEL_V3_SINGLE_CALL_MODE_DEFAULT, executionCalldata]
1965
2135
  });
1966
2136
  }
2137
+ function decodeKernelExecuteSingleCall(data) {
2138
+ let decoded;
2139
+ try {
2140
+ decoded = decodeFunctionData({ abi: KERNEL_EXECUTE_ABI, data });
2141
+ } catch {
2142
+ return null;
2143
+ }
2144
+ const [mode, executionCalldata] = decoded.args;
2145
+ if (mode !== KERNEL_V3_SINGLE_CALL_MODE_DEFAULT) {
2146
+ return null;
2147
+ }
2148
+ const ec = executionCalldata.slice(2);
2149
+ if (ec.length < 20 * 2 + 32 * 2) {
2150
+ return null;
2151
+ }
2152
+ const target = `0x${ec.slice(0, 40)}`;
2153
+ const value = BigInt(`0x${ec.slice(40, 40 + 64)}`);
2154
+ const innerCallData = `0x${ec.slice(40 + 64)}`;
2155
+ return { mode, target, value, innerCallData };
2156
+ }
1967
2157
  var ECDSA_SIG_HEX_RE = /^0x[0-9a-fA-F]{130}$/;
1968
2158
  var PERMISSION_USE_PREFIX = "0xff";
1969
2159
  function buildKernelSessionKeySignature(input) {
@@ -1975,6 +2165,7 @@ function buildKernelSessionKeySignature(input) {
1975
2165
  return concatHex([PERMISSION_USE_PREFIX, input.ecdsaSignature]);
1976
2166
  }
1977
2167
  var VALIDATOR_MODE_DEFAULT = "0x00";
2168
+ var VALIDATOR_MODE_ENABLE = "0x01";
1978
2169
  var VALIDATOR_TYPE_PERMISSION = "0x02";
1979
2170
  var PERMISSION_ID_HEX_RE = /^0x[0-9a-fA-F]{8}$/;
1980
2171
  function composeKernelV3NonceKey(args) {
@@ -1991,9 +2182,10 @@ function composeKernelV3NonceKey(args) {
1991
2182
  }
1992
2183
  const paddedPermissionId = pad(args.permissionId, { size: 20, dir: "right" });
1993
2184
  const customKeyHex = pad(`0x${customKey.toString(16)}`, { size: 2 });
2185
+ const modeByte = args.mode === "enable" ? VALIDATOR_MODE_ENABLE : VALIDATOR_MODE_DEFAULT;
1994
2186
  const composite = pad(
1995
2187
  concatHex([
1996
- VALIDATOR_MODE_DEFAULT,
2188
+ modeByte,
1997
2189
  VALIDATOR_TYPE_PERMISSION,
1998
2190
  paddedPermissionId,
1999
2191
  customKeyHex
@@ -2002,6 +2194,38 @@ function composeKernelV3NonceKey(args) {
2002
2194
  );
2003
2195
  return BigInt(composite);
2004
2196
  }
2197
+ var CALL_TYPE_DELEGATE_CALL = "0xFF";
2198
+ var ZERO_ADDRESS_HEX = "0x0000000000000000000000000000000000000000";
2199
+ function wrapEnableModeSignature(input) {
2200
+ const selectorData = concatHex([
2201
+ input.action.selector,
2202
+ input.action.address,
2203
+ input.action.hookAddress ?? ZERO_ADDRESS_HEX,
2204
+ encodeAbiParameters(
2205
+ parseAbiParameters("bytes selectorInitData, bytes hookInitData"),
2206
+ [CALL_TYPE_DELEGATE_CALL, "0x0000"]
2207
+ )
2208
+ ]);
2209
+ const abiEncoded = encodeAbiParameters(
2210
+ parseAbiParameters(
2211
+ "bytes validatorData, bytes hookData, bytes selectorData, bytes enableSig, bytes userOpSig"
2212
+ ),
2213
+ [
2214
+ input.enableData,
2215
+ input.hookData ?? "0x",
2216
+ selectorData,
2217
+ input.enableSig,
2218
+ input.userOpSignature
2219
+ ]
2220
+ );
2221
+ return concatHex([input.hookAddress ?? ZERO_ADDRESS_HEX, abiEncoded]);
2222
+ }
2223
+ parseAbi([
2224
+ "function currentNonce() view returns (uint32)"
2225
+ ]);
2226
+ parseAbi([
2227
+ "event SelectorSet(bytes4 selector, bytes21 vId, bool allowed)"
2228
+ ]);
2005
2229
 
2006
2230
  // src/tools/auth-required.ts
2007
2231
  function authRequiredPayload() {
@@ -2353,6 +2577,79 @@ async function syncSnapshotFromMirror(deps, brokerSignerAddress) {
2353
2577
  }
2354
2578
  return { kind: "ok", sessionId: activeId };
2355
2579
  }
2580
+ var PURCHASE_SELECTOR_LOWER = SUBSCRIPTION_PURCHASE_SELECTOR.toLowerCase();
2581
+ function buildPathDDecodedCall(trace, deps) {
2582
+ const sponsorEvent = trace.find((e) => e.method === "zd_sponsorUserOperation");
2583
+ if (!sponsorEvent) return void 0;
2584
+ let req;
2585
+ try {
2586
+ req = JSON.parse(sponsorEvent.requestBody);
2587
+ } catch {
2588
+ return void 0;
2589
+ }
2590
+ const userOp = req.params?.[0]?.userOp;
2591
+ if (!userOp || typeof userOp.callData !== "string" || !userOp.callData.startsWith("0x")) {
2592
+ return void 0;
2593
+ }
2594
+ const sender = userOp.sender ?? "<missing>";
2595
+ const decoded = decodeKernelExecuteSingleCall(userOp.callData);
2596
+ if (!decoded) {
2597
+ return {
2598
+ sender,
2599
+ kernelExecuteMode: "<undecodable>",
2600
+ kernelExecuteTarget: "<undecodable>",
2601
+ kernelExecuteValue: "<undecodable>",
2602
+ innerSelector: "<undecodable>",
2603
+ interpretation: "kernel.execute callData could not be decoded as single-call default mode. The mode word is non-zero or the executionCalldata layout is unexpected. Manual decode required."
2604
+ };
2605
+ }
2606
+ const innerSelector = decoded.innerCallData.slice(0, 10).toLowerCase();
2607
+ let innerPurchaseTokenArg;
2608
+ let innerPurchaseMaxSharesHint;
2609
+ let innerPurchaseEphemeralEOA;
2610
+ if (innerSelector === PURCHASE_SELECTOR_LOWER) {
2611
+ try {
2612
+ const inner = decodeFunctionData({
2613
+ abi: SUBSCRIPTION_PURCHASE_ABI,
2614
+ data: decoded.innerCallData
2615
+ });
2616
+ const [tokenArg, , maxSharesHint, ephemeralEOA] = inner.args;
2617
+ innerPurchaseTokenArg = tokenArg;
2618
+ innerPurchaseMaxSharesHint = maxSharesHint.toString();
2619
+ innerPurchaseEphemeralEOA = ephemeralEOA;
2620
+ } catch {
2621
+ }
2622
+ }
2623
+ const expectedSubscriptionAddress = deps.subscriptionAddress?.toLowerCase();
2624
+ const kernelTargetLower = decoded.target.toLowerCase();
2625
+ let kernelExecuteTargetMatchesSubscription;
2626
+ let interpretation;
2627
+ if (expectedSubscriptionAddress === void 0) {
2628
+ interpretation = `kernel.execute target=${decoded.target}; inner purchase token=${innerPurchaseTokenArg ?? "<unknown>"}; MUHAVEN_SUBSCRIPTION_ADDRESS not wired on this MCP server \u2014 cannot cross-check.`;
2629
+ } else if (kernelTargetLower === expectedSubscriptionAddress) {
2630
+ kernelExecuteTargetMatchesSubscription = true;
2631
+ interpretation = `kernel.execute target matches MuHavenSubscription (${decoded.target}). Inner purchase token = ${innerPurchaseTokenArg ?? "<unknown>"}. The shape is correct; the AA23 revert is downstream of the validator's signature decode \u2014 likely either an on-chain signer-vs-installed-permission mismatch, a target/selector not in the on-chain policy, or a cap-arg breach. Check muhaven.policy.session_key_status for the installed permission state.`;
2632
+ } else if (innerPurchaseTokenArg !== void 0 && kernelTargetLower === innerPurchaseTokenArg.toLowerCase()) {
2633
+ kernelExecuteTargetMatchesSubscription = false;
2634
+ interpretation = `kernel.execute target = ${decoded.target} = the RWA MuHavenToken (purchase.token arg0). Expected MuHavenSubscription (${deps.subscriptionAddress}). The kernel is dispatching purchase() to the token contract instead of the subscription \u2014 token doesn't have a purchase() selector, so fallback returns empty revert data (= AA23 reverted 0x). This is a code-side bug in the kernel.execute target wiring.`;
2635
+ } else {
2636
+ kernelExecuteTargetMatchesSubscription = false;
2637
+ interpretation = `kernel.execute target = ${decoded.target} \u2014 NEITHER the expected MuHavenSubscription (${deps.subscriptionAddress}) NOR the inner purchase.token arg (${innerPurchaseTokenArg ?? "<none>"}). This is an unexpected third-address dispatch; inspect deps.subscriptionAddress env wiring.`;
2638
+ }
2639
+ return {
2640
+ sender,
2641
+ kernelExecuteMode: decoded.mode,
2642
+ kernelExecuteTarget: decoded.target,
2643
+ kernelExecuteValue: decoded.value.toString(),
2644
+ innerSelector,
2645
+ innerPurchaseTokenArg,
2646
+ innerPurchaseMaxSharesHint,
2647
+ innerPurchaseEphemeralEOA,
2648
+ expectedSubscriptionAddress: deps.subscriptionAddress,
2649
+ kernelExecuteTargetMatchesSubscription,
2650
+ interpretation
2651
+ };
2652
+ }
2356
2653
  async function attemptPathD(args, deps) {
2357
2654
  const { shares, tokenAddress, tokenSymbol } = args;
2358
2655
  if (!deps.broker || !deps.bundler) {
@@ -2475,6 +2772,9 @@ async function attemptPathD(args, deps) {
2475
2772
  };
2476
2773
  }
2477
2774
  let accountAddress;
2775
+ let mirrorEnableStatus;
2776
+ let mirrorValidatorNonce;
2777
+ let mirrorSessionRow = null;
2478
2778
  try {
2479
2779
  const stateDto = await deps.backend.get("/api/v1/agent/policy/state", {
2480
2780
  surface: "mcp"
@@ -2494,6 +2794,18 @@ async function attemptPathD(args, deps) {
2494
2794
  message: `backend /agent/policy/state lookup failed: ${err2 instanceof Error ? err2.message : String(err2)}`
2495
2795
  };
2496
2796
  }
2797
+ try {
2798
+ const mirror = await deps.backend.get(
2799
+ "/api/v1/agent/policy/scoped-session",
2800
+ { surface: "mcp" }
2801
+ );
2802
+ if (mirror?.session) {
2803
+ mirrorSessionRow = mirror.session;
2804
+ mirrorEnableStatus = mirror.session.enableStatus ?? null;
2805
+ mirrorValidatorNonce = mirror.session.validatorNonce ?? null;
2806
+ }
2807
+ } catch (err2) {
2808
+ }
2497
2809
  if (!snapshot.permissionId) {
2498
2810
  return {
2499
2811
  kind: "fallback",
@@ -2502,6 +2814,115 @@ async function attemptPathD(args, deps) {
2502
2814
  };
2503
2815
  }
2504
2816
  const permissionId = snapshot.permissionId;
2817
+ if (mirrorEnableStatus === "failed") {
2818
+ return {
2819
+ kind: "fallback",
2820
+ reason: "validator_install_failed_re_walk_required",
2821
+ message: "previous Scoped session validator install was flagged failed by the watchdog \u2014 re-walk /agent/policy/transition to mint a fresh session"
2822
+ };
2823
+ }
2824
+ const needsEnable = mirrorEnableStatus === "pending";
2825
+ if (needsEnable && mirrorSessionRow && mirrorSessionRow.signerAddress.toLowerCase() !== snapshot.signerAddress.toLowerCase()) {
2826
+ return {
2827
+ kind: "fallback",
2828
+ reason: "signer_mismatch",
2829
+ message: `mirror row signer ${mirrorSessionRow.signerAddress} disagrees with broker snapshot signer ${snapshot.signerAddress} \u2014 refusing install`
2830
+ };
2831
+ }
2832
+ if (needsEnable && mirrorSessionRow?.permissionId && mirrorSessionRow.permissionId.toLowerCase() !== permissionId.toLowerCase()) {
2833
+ return {
2834
+ kind: "fallback",
2835
+ reason: "install_material_malformed",
2836
+ message: `mirror row permissionId ${mirrorSessionRow.permissionId} disagrees with broker snapshot permissionId ${permissionId} \u2014 refusing install`
2837
+ };
2838
+ }
2839
+ let installMaterial = null;
2840
+ if (needsEnable) {
2841
+ if (!mirrorSessionRow?.sessionId) {
2842
+ return {
2843
+ kind: "fallback",
2844
+ reason: "install_material_malformed",
2845
+ message: "mirror reports enable_status=pending but no sessionId was carried back \u2014 backend response shape regressed"
2846
+ };
2847
+ }
2848
+ let raw;
2849
+ try {
2850
+ raw = await deps.backend.get(
2851
+ `/api/v1/agent/policy/scoped-session/${encodeURIComponent(
2852
+ mirrorSessionRow.sessionId
2853
+ )}/install-material`
2854
+ );
2855
+ } catch (err2) {
2856
+ if (err2 instanceof BackendError && err2.status === 404) {
2857
+ return {
2858
+ kind: "fallback",
2859
+ reason: "install_material_malformed",
2860
+ message: "install-material subroute returned 404 \u2014 row vanished mid-flow OR not owned by the authenticated user; re-walk the ceremony"
2861
+ };
2862
+ }
2863
+ if (err2 instanceof BackendError && err2.status === 401) {
2864
+ return {
2865
+ kind: "fallback",
2866
+ reason: "install_material_unavailable",
2867
+ message: "install-material fetch returned 401 \u2014 the broker device-flow JWT is missing/expired/invalid. Run `muhaven-broker login` to refresh it, then retry."
2868
+ };
2869
+ }
2870
+ if (err2 instanceof BackendError && err2.status === 403) {
2871
+ return {
2872
+ kind: "fallback",
2873
+ reason: "install_material_unavailable",
2874
+ message: "install-material fetch returned 403 \u2014 the broker JWT lacks the `mcp.propose.*` scope. Re-authorize via `muhaven-broker login` (the device-flow must grant propose, not read-only). NOT a session problem \u2014 do not re-mint."
2875
+ };
2876
+ }
2877
+ if (err2 instanceof BackendError && (err2.status === void 0 || err2.status >= 500)) {
2878
+ return {
2879
+ kind: "fallback",
2880
+ reason: "install_material_unavailable",
2881
+ message: `install-material fetch hit a transient backend error (${typedErrorCode(err2)}) \u2014 retry shortly. If it persists, the operator should check backend logs + that OPTION_D_C2_ENCRYPTION_KEY is set. NOT a session problem \u2014 do not re-mint.`
2882
+ };
2883
+ }
2884
+ return {
2885
+ kind: "fallback",
2886
+ reason: "install_material_malformed",
2887
+ message: `install-material lookup failed (${typedErrorCode(err2)})`
2888
+ };
2889
+ }
2890
+ if (!raw?.installMaterial || !raw.installMaterial.enableData || !raw.installMaterial.enableSig || raw.installMaterial.validatorNonce == null || raw.installMaterial.permissionId == null) {
2891
+ return {
2892
+ kind: "fallback",
2893
+ reason: "install_material_malformed",
2894
+ message: "install-material subroute returned partial payload (enable_data / enable_sig / validator_nonce / permission_id) \u2014 re-walk the ceremony"
2895
+ };
2896
+ }
2897
+ if (raw.installMaterial.permissionId.toLowerCase() !== permissionId.toLowerCase()) {
2898
+ return {
2899
+ kind: "fallback",
2900
+ reason: "install_material_malformed",
2901
+ message: `install-material permissionId ${raw.installMaterial.permissionId} disagrees with broker snapshot permissionId ${permissionId} \u2014 re-walk the ceremony`
2902
+ };
2903
+ }
2904
+ installMaterial = raw.installMaterial;
2905
+ try {
2906
+ const liveNonce = await deps.broker.currentNonce(accountAddress);
2907
+ const minted = installMaterial.validatorNonce ?? mirrorValidatorNonce ?? null;
2908
+ if (minted !== null && liveNonce.nonce !== minted) {
2909
+ return {
2910
+ kind: "fallback",
2911
+ reason: "enable_sig_stale",
2912
+ message: `kernel.currentNonce() advanced ${minted} \u2192 ${liveNonce.nonce} since mint; the stored enableSig is over a stale typed-data digest. ` + (liveNonce.nonce === minted + 1 ? "This is most likely the benign post-install race (your own MODE.ENABLE just landed; the mirror flips to enabled within a few seconds) \u2014 WAIT a few seconds and retry; the repeat buy will use MODE.DEFAULT. Only re-mint if it persists." : "The validator nonce diverged by more than one \u2014 re-mint the Scoped session from the dashboard.")
2913
+ };
2914
+ }
2915
+ } catch (err2) {
2916
+ if (err2 instanceof BrokerClientError && err2.brokerCode === "chain_rpc_failed") {
2917
+ return {
2918
+ kind: "fallback",
2919
+ reason: "broker_chain_rpc_failed",
2920
+ message: "broker daemon currentNonce IPC returned chain_rpc_failed \u2014 set MUHAVEN_BROKER_RPC_URL on the broker (or MUHAVEN_BUNDLER_URL fallback) and restart"
2921
+ };
2922
+ }
2923
+ return mapBrokerCallFailure(err2, "current_nonce", "broker_internal");
2924
+ }
2925
+ }
2505
2926
  let encShares;
2506
2927
  let ephemeralEOA;
2507
2928
  try {
@@ -2562,7 +2983,10 @@ async function attemptPathD(args, deps) {
2562
2983
  let nonce;
2563
2984
  let feeData;
2564
2985
  try {
2565
- const nonceKey = composeKernelV3NonceKey({ permissionId });
2986
+ const nonceKey = composeKernelV3NonceKey({
2987
+ permissionId,
2988
+ mode: needsEnable ? "enable" : "default"
2989
+ });
2566
2990
  nonce = await deps.bundler.getNonce(accountAddress, entryPointAddress, nonceKey);
2567
2991
  feeData = await deps.bundler.getFeeData();
2568
2992
  } catch (err2) {
@@ -2572,13 +2996,32 @@ async function attemptPathD(args, deps) {
2572
2996
  message: `bundler bootstrap failed: ${err2 instanceof BundlerClientError ? `${err2.code}: ${err2.message}` : String(err2)}`
2573
2997
  };
2574
2998
  }
2999
+ let wrapForEnableMode = null;
3000
+ if (needsEnable && installMaterial) {
3001
+ const kernelExecuteSelector = toFunctionSelector(
3002
+ KERNEL_EXECUTE_ABI[0]
3003
+ ).toLowerCase();
3004
+ const enableActionAddress = "0x0000000000000000000000000000000000000000";
3005
+ const enableData = installMaterial.enableData;
3006
+ const enableSig = installMaterial.enableSig;
3007
+ wrapForEnableMode = (userOpSig) => wrapEnableModeSignature({
3008
+ enableData,
3009
+ enableSig,
3010
+ userOpSignature: userOpSig,
3011
+ action: {
3012
+ selector: kernelExecuteSelector,
3013
+ address: enableActionAddress
3014
+ }
3015
+ });
3016
+ }
3017
+ const sponsorshipSignature = wrapForEnableMode ? wrapForEnableMode(PLACEHOLDER_SIGNATURE) : PLACEHOLDER_SIGNATURE;
2575
3018
  const partial = {
2576
3019
  sender: accountAddress,
2577
3020
  nonce: `0x${nonce.toString(16)}`,
2578
3021
  callData: kernelCallData,
2579
3022
  maxFeePerGas: feeData.maxFeePerGas,
2580
3023
  maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
2581
- signature: PLACEHOLDER_SIGNATURE
3024
+ signature: sponsorshipSignature
2582
3025
  };
2583
3026
  let sponsored;
2584
3027
  try {
@@ -2663,6 +3106,10 @@ async function attemptPathD(args, deps) {
2663
3106
  }
2664
3107
  return mapBrokerCallFailure(err2, "sign_userop", "broker_internal");
2665
3108
  }
3109
+ const wrappedSessionKeySig = buildKernelSessionKeySignature({
3110
+ ecdsaSignature: brokerSig
3111
+ });
3112
+ const finalSignature = wrapForEnableMode ? wrapForEnableMode(wrappedSessionKeySig) : wrappedSessionKeySig;
2666
3113
  const signedUserOpWire = {
2667
3114
  sender: accountAddress,
2668
3115
  nonce: partial.nonce,
@@ -2676,7 +3123,7 @@ async function attemptPathD(args, deps) {
2676
3123
  paymasterVerificationGasLimit: sponsored.paymasterVerificationGasLimit,
2677
3124
  paymasterPostOpGasLimit: sponsored.paymasterPostOpGasLimit,
2678
3125
  paymasterData: sponsored.paymasterData,
2679
- signature: buildKernelSessionKeySignature({ ecdsaSignature: brokerSig })
3126
+ signature: finalSignature
2680
3127
  };
2681
3128
  let submittedHash;
2682
3129
  try {
@@ -2698,6 +3145,37 @@ async function attemptPathD(args, deps) {
2698
3145
  }
2699
3146
  try {
2700
3147
  const receipt = await deps.bundler.waitForReceipt(userOpHash, { timeoutMs: 12e3 });
3148
+ if (needsEnable && activeId) {
3149
+ try {
3150
+ let permissionLogIndex = 0;
3151
+ try {
3152
+ const r = receipt.receipt;
3153
+ if (Array.isArray(r.logs)) {
3154
+ const SELECTOR_SET_TOPIC0 = toEventSelector(
3155
+ "SelectorSet(bytes4,bytes21,bool)"
3156
+ ).toLowerCase();
3157
+ for (const l of r.logs) {
3158
+ if (l.topics?.[0]?.toLowerCase() === SELECTOR_SET_TOPIC0 && l.address?.toLowerCase() === accountAddress.toLowerCase()) {
3159
+ permissionLogIndex = l.logIndex ?? 0;
3160
+ break;
3161
+ }
3162
+ }
3163
+ }
3164
+ } catch {
3165
+ }
3166
+ await deps.broker.notifyUseropLanded({
3167
+ sessionId: activeId,
3168
+ accountAddress,
3169
+ permissionId,
3170
+ txHash: receipt.receipt.transactionHash,
3171
+ blockNumber: Number(
3172
+ receipt.receipt.blockNumber ?? 0
3173
+ ),
3174
+ logIndex: permissionLogIndex
3175
+ });
3176
+ } catch {
3177
+ }
3178
+ }
2701
3179
  return {
2702
3180
  kind: "ok",
2703
3181
  data: {
@@ -2800,6 +3278,7 @@ async function positionBuy(input, deps) {
2800
3278
  let pathDFallbackDetail;
2801
3279
  let pathDSubmittedUserOpHash;
2802
3280
  let pathDBundlerTrace;
3281
+ let pathDDecodedCall;
2803
3282
  const pathD = await attemptPathD(
2804
3283
  { shares, tokenAddress: token.address, tokenSymbol: token.symbol },
2805
3284
  deps
@@ -2817,6 +3296,7 @@ async function positionBuy(input, deps) {
2817
3296
  const trace = deps.bundler.drainTrace();
2818
3297
  if (trace.length > 0) {
2819
3298
  pathDBundlerTrace = trace;
3299
+ pathDDecodedCall = buildPathDDecodedCall(trace, deps);
2820
3300
  }
2821
3301
  }
2822
3302
  }
@@ -2843,7 +3323,8 @@ ${dashboardUrl}`,
2843
3323
  ...pathDFallbackReason ? { pathDFallbackReason } : {},
2844
3324
  ...pathDFallbackDetail ? { pathDFallbackDetail } : {},
2845
3325
  ...pathDSubmittedUserOpHash ? { pathDSubmittedUserOpHash } : {},
2846
- ...pathDBundlerTrace ? { pathDBundlerTrace } : {}
3326
+ ...pathDBundlerTrace ? { pathDBundlerTrace } : {},
3327
+ ...pathDDecodedCall ? { pathDDecodedCall } : {}
2847
3328
  }
2848
3329
  });
2849
3330
  }
@@ -3194,7 +3675,7 @@ var SERVER_NAME = "@muhaven/mcp";
3194
3675
  var SERVER_VERSION = resolveServerVersion();
3195
3676
  function resolveServerVersion() {
3196
3677
  {
3197
- return "0.2.8";
3678
+ return "0.3.0";
3198
3679
  }
3199
3680
  }
3200
3681
  function toJsonInputSchema(schema) {
@@ -3994,11 +4475,285 @@ function checkPolicy(input) {
3994
4475
  }
3995
4476
  return { ok: true };
3996
4477
  }
4478
+ var KERNEL_V3_CURRENT_NONCE_ABI2 = parseAbi([
4479
+ "function currentNonce() view returns (uint32)"
4480
+ ]);
4481
+ var ChainRpcError = class extends Error {
4482
+ constructor(message, cause) {
4483
+ super(message);
4484
+ this.cause = cause;
4485
+ this.name = "ChainRpcError";
4486
+ }
4487
+ cause;
4488
+ };
4489
+ var CallbackError = class extends Error {
4490
+ constructor(message, cause) {
4491
+ super(message);
4492
+ this.cause = cause;
4493
+ this.name = "CallbackError";
4494
+ }
4495
+ cause;
4496
+ };
4497
+ var CALLBACK_RETRY_SCHEDULE_MS = [
4498
+ 5e3,
4499
+ 15e3,
4500
+ 6e4,
4501
+ 5 * 6e4
4502
+ ];
4503
+ var CALLBACK_MAX_ELAPSED_MS = 60 * 6e4;
4504
+ var DEFAULT_FETCH_TIMEOUT_MS = 15e3;
4505
+ var BrokerOutbound = class {
4506
+ constructor(config, log = () => {
4507
+ }) {
4508
+ this.config = config;
4509
+ this.log = log;
4510
+ this.fetchImpl = config.fetchImpl ?? fetch;
4511
+ this.fetchTimeoutMs = config.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
4512
+ this.setTimeoutImpl = config.setTimeout ?? setTimeout;
4513
+ this.clearTimeoutImpl = config.clearTimeout ?? clearTimeout;
4514
+ }
4515
+ config;
4516
+ log;
4517
+ fetchImpl;
4518
+ fetchTimeoutMs;
4519
+ setTimeoutImpl;
4520
+ clearTimeoutImpl;
4521
+ /**
4522
+ * Wave 5 Option D Commit 3 (multi-agent review SecEng-MED-3) —
4523
+ * in-process dedup of `notify_userop_landed` callbacks. Map of
4524
+ * `<sessionId>:<txHash>` → the in-flight retry loop's Promise.
4525
+ * Repeated IPC calls with the same key fold into the existing
4526
+ * loop instead of spawning a parallel POST. Defends against a
4527
+ * local-socket peer flooding the broker with replay attempts +
4528
+ * caps the retry-budget waste at one loop per real install.
4529
+ */
4530
+ inflightCallbacks = /* @__PURE__ */ new Map();
4531
+ /**
4532
+ * Read the kernel's `currentNonce()` view via `eth_call` against the
4533
+ * configured chain RPC. Returns a uint32. Throws `ChainRpcError` when
4534
+ * unconfigured / network failed / RPC returned non-decodable bytes.
4535
+ */
4536
+ async currentNonce(accountAddress) {
4537
+ if (!this.config.chainRpcUrl) {
4538
+ throw new ChainRpcError(
4539
+ "broker chain RPC unconfigured \u2014 set MUHAVEN_BROKER_RPC_URL or MUHAVEN_BUNDLER_URL"
4540
+ );
4541
+ }
4542
+ const data = encodeFunctionData({
4543
+ abi: KERNEL_V3_CURRENT_NONCE_ABI2,
4544
+ functionName: "currentNonce"
4545
+ });
4546
+ const body = JSON.stringify({
4547
+ jsonrpc: "2.0",
4548
+ id: 1,
4549
+ method: "eth_call",
4550
+ params: [{ to: accountAddress, data }, "latest"]
4551
+ });
4552
+ let res;
4553
+ const ac = new AbortController();
4554
+ const timer = this.setTimeoutImpl(() => ac.abort(), this.fetchTimeoutMs);
4555
+ try {
4556
+ res = await this.fetchImpl(this.config.chainRpcUrl, {
4557
+ method: "POST",
4558
+ headers: {
4559
+ "Content-Type": "application/json",
4560
+ Accept: "application/json",
4561
+ Origin: this.config.outboundOriginHeader
4562
+ },
4563
+ body,
4564
+ signal: ac.signal
4565
+ });
4566
+ } catch (err2) {
4567
+ throw new ChainRpcError(
4568
+ `chain RPC fetch failed: ${err2 instanceof Error ? err2.message : String(err2)}`,
4569
+ err2
4570
+ );
4571
+ } finally {
4572
+ this.clearTimeoutImpl(timer);
4573
+ }
4574
+ if (!res.ok) {
4575
+ throw new ChainRpcError(
4576
+ `chain RPC returned HTTP ${res.status}`
4577
+ );
4578
+ }
4579
+ let parsed;
4580
+ try {
4581
+ parsed = await res.json();
4582
+ } catch (err2) {
4583
+ throw new ChainRpcError(
4584
+ `chain RPC returned non-JSON: ${err2 instanceof Error ? err2.message : String(err2)}`
4585
+ );
4586
+ }
4587
+ if (parsed.error) {
4588
+ throw new ChainRpcError(
4589
+ `chain RPC error: ${parsed.error.message ?? "unknown"}`
4590
+ );
4591
+ }
4592
+ if (typeof parsed.result !== "string" || !/^0x[0-9a-fA-F]*$/.test(parsed.result)) {
4593
+ throw new ChainRpcError(
4594
+ `chain RPC returned non-hex result: ${JSON.stringify(parsed.result).slice(0, 80)}`
4595
+ );
4596
+ }
4597
+ let nonce;
4598
+ try {
4599
+ const decoded = decodeAbiParameters(
4600
+ [{ type: "uint32" }],
4601
+ parsed.result
4602
+ );
4603
+ nonce = Number(decoded[0]);
4604
+ } catch (err2) {
4605
+ throw new ChainRpcError(
4606
+ `failed to decode currentNonce result: ${err2 instanceof Error ? err2.message : String(err2)}`
4607
+ );
4608
+ }
4609
+ if (!Number.isFinite(nonce) || nonce < 0 || nonce > 4294967295) {
4610
+ throw new ChainRpcError(`currentNonce out of uint32 range: ${nonce}`);
4611
+ }
4612
+ return nonce;
4613
+ }
4614
+ /**
4615
+ * Whether the callback path is wired (both secret + backend URL set).
4616
+ * The `notify_userop_landed` daemon handler checks this and returns
4617
+ * `callback_unconfigured` when false so the operator sees the gap.
4618
+ */
4619
+ isCallbackConfigured() {
4620
+ return Boolean(this.config.callbackServiceSecret) && Boolean(this.config.backendBaseUrl);
4621
+ }
4622
+ /**
4623
+ * Queue a `validator-enabled` callback POST to the backend. Returns
4624
+ * immediately; the retry loop runs in the background (5s / 15s / 60s
4625
+ * / 5m, max 1h elapsed). Failures are logged but do NOT propagate to
4626
+ * the IPC caller — the chain indexer is the authoritative safety
4627
+ * net.
4628
+ *
4629
+ * Idempotency: every POST carries an `Idempotency-Key` header
4630
+ * `<sessionId>:validator-enabled`. The backend route is no-op if the
4631
+ * mirror row's `enable_status` is already `'enabled'` (because the
4632
+ * chain indexer raced ahead).
4633
+ *
4634
+ * Returns a Promise resolved when the loop terminates (success or
4635
+ * max-elapsed). Callers don't need to await; tests use it for
4636
+ * deterministic assertions.
4637
+ */
4638
+ enqueueValidatorEnabledCallback(args) {
4639
+ if (!this.isCallbackConfigured()) {
4640
+ return Promise.resolve({
4641
+ ok: false,
4642
+ attempts: 0,
4643
+ lastError: "callback_unconfigured"
4644
+ });
4645
+ }
4646
+ const dedupKey = `${args.sessionId}:${args.txHash.toLowerCase()}:${args.accountAddress.toLowerCase()}`;
4647
+ const existing = this.inflightCallbacks.get(dedupKey);
4648
+ if (existing) {
4649
+ this.log("info", "validator-enabled callback already in flight \u2014 folded", {
4650
+ sessionId: args.sessionId
4651
+ });
4652
+ return existing;
4653
+ }
4654
+ const promise = this.runCallbackLoop(args).finally(() => {
4655
+ this.inflightCallbacks.delete(dedupKey);
4656
+ });
4657
+ this.inflightCallbacks.set(dedupKey, promise);
4658
+ return promise;
4659
+ }
4660
+ async runCallbackLoop(args) {
4661
+ const url = `${this.config.backendBaseUrl.replace(/\/+$/, "")}/api/v1/agent/policy/scoped-session/${encodeURIComponent(args.sessionId)}/validator-enabled`;
4662
+ const body = JSON.stringify({
4663
+ userId: args.userId,
4664
+ accountAddress: args.accountAddress,
4665
+ permissionId: args.permissionId,
4666
+ txHash: args.txHash,
4667
+ blockNumber: args.blockNumber,
4668
+ logIndex: args.logIndex
4669
+ });
4670
+ const startedAt = Date.now();
4671
+ let attempts = 0;
4672
+ let lastError;
4673
+ for (let i = 0; i <= CALLBACK_RETRY_SCHEDULE_MS.length; i++) {
4674
+ if (i > 0) {
4675
+ const delay2 = CALLBACK_RETRY_SCHEDULE_MS[i - 1] ?? 0;
4676
+ const elapsed = Date.now() - startedAt;
4677
+ if (elapsed + delay2 > CALLBACK_MAX_ELAPSED_MS) {
4678
+ lastError = `retry budget exhausted after ${attempts} attempts (last error: ${lastError ?? "unknown"})`;
4679
+ this.log("error", "validator-enabled callback abandoned", {
4680
+ sessionId: args.sessionId,
4681
+ attempts,
4682
+ lastError
4683
+ });
4684
+ return { ok: false, attempts, lastError };
4685
+ }
4686
+ await this.sleep(delay2);
4687
+ }
4688
+ attempts++;
4689
+ try {
4690
+ const ok2 = await this.postCallback(url, body, args.sessionId);
4691
+ if (ok2) {
4692
+ this.log("info", "validator-enabled callback succeeded", {
4693
+ sessionId: args.sessionId,
4694
+ attempts
4695
+ });
4696
+ return { ok: true, attempts };
4697
+ }
4698
+ lastError = "non-2xx response";
4699
+ } catch (err2) {
4700
+ lastError = err2 instanceof Error ? err2.message : String(err2);
4701
+ this.log("warn", "validator-enabled callback attempt failed", {
4702
+ sessionId: args.sessionId,
4703
+ attempt: attempts,
4704
+ err: lastError
4705
+ });
4706
+ }
4707
+ }
4708
+ this.log("error", "validator-enabled callback retry budget exhausted", {
4709
+ sessionId: args.sessionId,
4710
+ attempts,
4711
+ lastError
4712
+ });
4713
+ return { ok: false, attempts, lastError };
4714
+ }
4715
+ async postCallback(url, body, sessionId) {
4716
+ const ac = new AbortController();
4717
+ const timer = this.setTimeoutImpl(() => ac.abort(), this.fetchTimeoutMs);
4718
+ let res;
4719
+ try {
4720
+ res = await this.fetchImpl(url, {
4721
+ method: "POST",
4722
+ headers: {
4723
+ "Content-Type": "application/json",
4724
+ Accept: "application/json",
4725
+ Authorization: `Bearer ${this.config.callbackServiceSecret}`,
4726
+ "Idempotency-Key": `${sessionId}:validator-enabled`,
4727
+ Origin: this.config.outboundOriginHeader
4728
+ },
4729
+ body,
4730
+ signal: ac.signal
4731
+ });
4732
+ } finally {
4733
+ this.clearTimeoutImpl(timer);
4734
+ }
4735
+ if (res.status === 409) {
4736
+ this.log("info", "callback returned 409 (row already enabled \u2014 idempotent)", {
4737
+ sessionId
4738
+ });
4739
+ return true;
4740
+ }
4741
+ if (res.ok) return true;
4742
+ throw new CallbackError(
4743
+ `backend callback returned HTTP ${res.status}`
4744
+ );
4745
+ }
4746
+ sleep(ms) {
4747
+ return new Promise((resolve) => {
4748
+ this.setTimeoutImpl(() => resolve(), ms);
4749
+ });
4750
+ }
4751
+ };
3997
4752
 
3998
4753
  // src/broker/daemon.ts
3999
4754
  var noopLogger = (_e) => {
4000
4755
  };
4001
- async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.floor(Date.now() / 1e3), options = {}, policyStore) {
4756
+ async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.floor(Date.now() / 1e3), options = {}, policyStore, outbound) {
4002
4757
  switch (req.type) {
4003
4758
  case "hello": {
4004
4759
  let hasJwt = false;
@@ -4206,6 +4961,57 @@ async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.fl
4206
4961
  );
4207
4962
  }
4208
4963
  }
4964
+ case "current_nonce": {
4965
+ if (!outbound) {
4966
+ return errorResponse(
4967
+ "chain_rpc_failed",
4968
+ "broker daemon was not configured with a chain RPC URL"
4969
+ );
4970
+ }
4971
+ try {
4972
+ const nonce = await outbound.currentNonce(req.accountAddress);
4973
+ return {
4974
+ type: "current_nonce",
4975
+ nonce,
4976
+ accountAddress: req.accountAddress
4977
+ };
4978
+ } catch (err2) {
4979
+ if (err2 instanceof ChainRpcError) {
4980
+ return errorResponse("chain_rpc_failed", err2.message);
4981
+ }
4982
+ return errorResponse(
4983
+ "chain_rpc_failed",
4984
+ err2 instanceof Error ? err2.message : "chain RPC eth_call failed"
4985
+ );
4986
+ }
4987
+ }
4988
+ case "notify_userop_landed": {
4989
+ if (!outbound) {
4990
+ return errorResponse(
4991
+ "callback_unconfigured",
4992
+ "broker daemon was not configured with the callback service secret"
4993
+ );
4994
+ }
4995
+ if (!outbound.isCallbackConfigured()) {
4996
+ return errorResponse(
4997
+ "callback_unconfigured",
4998
+ "BROKER_CALLBACK_SERVICE_SECRET or backend URL is unset \u2014 validator-enabled callback skipped (chain indexer is the safety net)"
4999
+ );
5000
+ }
5001
+ void outbound.enqueueValidatorEnabledCallback({
5002
+ sessionId: req.sessionId,
5003
+ accountAddress: req.accountAddress,
5004
+ permissionId: req.permissionId,
5005
+ txHash: req.txHash,
5006
+ blockNumber: req.blockNumber,
5007
+ logIndex: req.logIndex
5008
+ });
5009
+ return {
5010
+ type: "notify_userop_landed",
5011
+ queued: true,
5012
+ sessionId: req.sessionId
5013
+ };
5014
+ }
4209
5015
  }
4210
5016
  }
4211
5017
  function errorResponse(code, message) {
@@ -4236,6 +5042,7 @@ var BrokerDaemon = class {
4236
5042
  config;
4237
5043
  keystore;
4238
5044
  policyStore;
5045
+ outbound;
4239
5046
  /**
4240
5047
  * Whether a session-key private half is actually loaded. `false` =
4241
5048
  * daemon booted in read-only posture (no `MUHAVEN_BROKER_SESSION_KEY`
@@ -4256,6 +5063,15 @@ var BrokerDaemon = class {
4256
5063
  }
4257
5064
  this.keystore = options.keystore ?? null;
4258
5065
  this.policyStore = options.policyStore ?? new FilePolicyStore(FilePolicyStore.defaultDir());
5066
+ this.outbound = options.outbound ?? new BrokerOutbound(
5067
+ {
5068
+ chainRpcUrl: options.config.chainRpcUrl,
5069
+ backendBaseUrl: options.config.backendBaseUrl,
5070
+ callbackServiceSecret: options.config.callbackServiceSecret,
5071
+ outboundOriginHeader: options.config.outboundOriginHeader ?? options.config.dashboardBaseUrl
5072
+ },
5073
+ (level, msg, meta) => (options.logger ?? noopLogger)({ level, msg, meta })
5074
+ );
4259
5075
  this.log = options.logger ?? noopLogger;
4260
5076
  this.server = createServer((socket) => this.onConnection(socket));
4261
5077
  }
@@ -4388,7 +5204,8 @@ var BrokerDaemon = class {
4388
5204
  },
4389
5205
  pid: process.pid
4390
5206
  },
4391
- this.policyStore
5207
+ this.policyStore,
5208
+ this.outbound
4392
5209
  );
4393
5210
  socket.end(serializeResponse(res));
4394
5211
  } catch (err2) {