@muhaven/mcp 0.2.9 → 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.cjs CHANGED
@@ -173,22 +173,45 @@ function loadBrokerConfig(env = process.env) {
173
173
  env.MUHAVEN_DASHBOARD_URL,
174
174
  DEFAULT_DASHBOARD_URL
175
175
  );
176
+ const chainRpcUrlRaw = readEnv("MUHAVEN_BROKER_RPC_URL", env) ?? readEnv("MUHAVEN_BUNDLER_URL", env);
177
+ const chainRpcUrl = chainRpcUrlRaw === void 0 ? void 0 : resolvePublicUrlEnv(
178
+ "MUHAVEN_BROKER_RPC_URL",
179
+ chainRpcUrlRaw,
180
+ chainRpcUrlRaw
181
+ );
182
+ const callbackServiceSecretRaw = readEnv("BROKER_CALLBACK_SERVICE_SECRET", env);
183
+ let callbackServiceSecret;
184
+ if (callbackServiceSecretRaw !== void 0) {
185
+ if (callbackServiceSecretRaw.length < 16) {
186
+ throw new Error(
187
+ "BROKER_CALLBACK_SERVICE_SECRET must be at least 16 characters (matches backend with-service-secret middleware floor)"
188
+ );
189
+ }
190
+ callbackServiceSecret = callbackServiceSecretRaw;
191
+ }
192
+ const outboundOriginHeader = readEnv("MUHAVEN_BROKER_ORIGIN", env) ?? dashboardBaseUrl;
176
193
  return {
177
194
  endpoint,
178
195
  sessionKeyHex,
179
196
  maxRequestBytes,
180
197
  requestTimeoutMs,
181
198
  backendBaseUrl,
182
- dashboardBaseUrl
199
+ dashboardBaseUrl,
200
+ chainRpcUrl,
201
+ callbackServiceSecret,
202
+ outboundOriginHeader
183
203
  };
184
204
  }
185
205
 
186
206
  // src/broker/protocol.ts
187
- var BROKER_PROTOCOL_VERSION = "0.4.0";
207
+ var BROKER_PROTOCOL_VERSION = "0.5.0";
188
208
  var HASH_HEX_RE = /^0x[0-9a-fA-F]{64}$/;
189
209
  var ADDRESS_HEX_RE2 = /^0x[0-9a-fA-F]{40}$/;
190
210
  var SELECTOR_HEX_RE = /^0x[0-9a-fA-F]{8}$/;
191
211
  var HEX_PREFIXED_RE = /^0x[0-9a-fA-F]*$/;
212
+ var ENABLE_DATA_HEX_RE = /^0x[0-9a-fA-F]{2,65536}$/;
213
+ var ENABLE_SIG_HEX_RE = /^0x[0-9a-fA-F]{256,16384}$/;
214
+ var UINT32_MAX = 4294967295;
192
215
  var JWT_RE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
193
216
  var SESSION_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
194
217
  var UINT256_DEC_RE = /^(0|[1-9][0-9]{0,77})$/;
@@ -308,6 +331,43 @@ function parsePolicySnapshot(raw) {
308
331
  if (!isOptionalPermissionId(obj.permissionId)) {
309
332
  return { error: "snapshot.permissionId must be a 0x-prefixed 4-byte hex when provided" };
310
333
  }
334
+ const enableData = obj.enableData;
335
+ const enableSig = obj.enableSig;
336
+ const validatorNonce = obj.validatorNonce;
337
+ const installPresent = [enableData, enableSig, validatorNonce].filter(
338
+ (v) => v !== void 0
339
+ ).length;
340
+ if (installPresent !== 0 && installPresent !== 3) {
341
+ return {
342
+ error: "snapshot.{enableData,enableSig,validatorNonce} must be all-present or all-absent (Option D Commit 3 install-material trio)"
343
+ };
344
+ }
345
+ if (enableData !== void 0) {
346
+ if (typeof enableData !== "string" || !ENABLE_DATA_HEX_RE.test(enableData)) {
347
+ return {
348
+ error: "snapshot.enableData must be a 0x-prefixed hex string of 2..65536 chars when provided"
349
+ };
350
+ }
351
+ }
352
+ if (enableSig !== void 0) {
353
+ if (typeof enableSig !== "string" || !ENABLE_SIG_HEX_RE.test(enableSig)) {
354
+ return {
355
+ error: "snapshot.enableSig must be a 0x-prefixed hex string of 256..16384 chars (WebAuthn envelope) when provided"
356
+ };
357
+ }
358
+ }
359
+ if (validatorNonce !== void 0) {
360
+ if (typeof validatorNonce !== "number" || !Number.isInteger(validatorNonce) || validatorNonce < 0 || validatorNonce > UINT32_MAX) {
361
+ return {
362
+ error: "snapshot.validatorNonce must be an integer in [0, 2^32-1] when provided"
363
+ };
364
+ }
365
+ }
366
+ if (installPresent === 3 && obj.permissionId === void 0) {
367
+ return {
368
+ error: "snapshot install material requires permissionId in the same snapshot"
369
+ };
370
+ }
311
371
  return {
312
372
  sessionId: obj.sessionId,
313
373
  mode: "scoped",
@@ -320,7 +380,10 @@ function parsePolicySnapshot(raw) {
320
380
  mintedAtSec: obj.mintedAtSec,
321
381
  ...obj.consentActionHash === void 0 ? {} : { consentActionHash: obj.consentActionHash.toLowerCase() },
322
382
  ...obj.consentTextSha256 === void 0 ? {} : { consentTextSha256: obj.consentTextSha256.toLowerCase() },
323
- ...obj.permissionId === void 0 ? {} : { permissionId: obj.permissionId.toLowerCase() }
383
+ ...obj.permissionId === void 0 ? {} : { permissionId: obj.permissionId.toLowerCase() },
384
+ ...enableData === void 0 ? {} : { enableData: enableData.toLowerCase() },
385
+ ...enableSig === void 0 ? {} : { enableSig: enableSig.toLowerCase() },
386
+ ...validatorNonce === void 0 ? {} : { validatorNonce }
324
387
  };
325
388
  }
326
389
  function parseBrokerRequest(line) {
@@ -499,6 +562,72 @@ function parseBrokerRequest(line) {
499
562
  }
500
563
  case "get_active_session_id":
501
564
  return { type: "get_active_session_id" };
565
+ case "current_nonce": {
566
+ if (!isAddressHex(obj.accountAddress)) {
567
+ return {
568
+ type: "error",
569
+ code: "invalid_request",
570
+ message: "current_nonce.accountAddress must be a 0x-prefixed 20-byte hex"
571
+ };
572
+ }
573
+ return {
574
+ type: "current_nonce",
575
+ accountAddress: obj.accountAddress.toLowerCase()
576
+ };
577
+ }
578
+ case "notify_userop_landed": {
579
+ if (!isSessionIdShape(obj.sessionId)) {
580
+ return {
581
+ type: "error",
582
+ code: "invalid_request",
583
+ message: "notify_userop_landed.sessionId must be 1-128 chars [A-Za-z0-9_-]"
584
+ };
585
+ }
586
+ if (!isAddressHex(obj.accountAddress)) {
587
+ return {
588
+ type: "error",
589
+ code: "invalid_request",
590
+ message: "notify_userop_landed.accountAddress must be a 0x-prefixed 20-byte hex"
591
+ };
592
+ }
593
+ if (!isSelectorHex(obj.permissionId)) {
594
+ return {
595
+ type: "error",
596
+ code: "invalid_request",
597
+ message: "notify_userop_landed.permissionId must be a 0x-prefixed 4-byte hex"
598
+ };
599
+ }
600
+ if (!isHashHex(obj.txHash)) {
601
+ return {
602
+ type: "error",
603
+ code: "invalid_request",
604
+ message: "notify_userop_landed.txHash must be a 0x-prefixed 32-byte hex"
605
+ };
606
+ }
607
+ if (typeof obj.blockNumber !== "number" || !Number.isFinite(obj.blockNumber) || !Number.isInteger(obj.blockNumber) || obj.blockNumber < 0) {
608
+ return {
609
+ type: "error",
610
+ code: "invalid_request",
611
+ message: "notify_userop_landed.blockNumber must be a non-negative integer"
612
+ };
613
+ }
614
+ if (typeof obj.logIndex !== "number" || !Number.isFinite(obj.logIndex) || !Number.isInteger(obj.logIndex) || obj.logIndex < 0) {
615
+ return {
616
+ type: "error",
617
+ code: "invalid_request",
618
+ message: "notify_userop_landed.logIndex must be a non-negative integer"
619
+ };
620
+ }
621
+ return {
622
+ type: "notify_userop_landed",
623
+ sessionId: obj.sessionId,
624
+ accountAddress: obj.accountAddress.toLowerCase(),
625
+ permissionId: obj.permissionId.toLowerCase(),
626
+ txHash: obj.txHash.toLowerCase(),
627
+ blockNumber: obj.blockNumber,
628
+ logIndex: obj.logIndex
629
+ };
630
+ }
502
631
  default:
503
632
  return {
504
633
  type: "error",
@@ -658,6 +787,33 @@ var BrokerClient = class {
658
787
  }
659
788
  return res;
660
789
  }
790
+ // ── Wave 5 Option D Commit 3 — install-mode pre-check + callback notify ──
791
+ async currentNonce(accountAddress) {
792
+ const res = await this.exchange({ type: "current_nonce", accountAddress });
793
+ if (res.type !== "current_nonce") {
794
+ throw new BrokerClientError(
795
+ "protocol_error",
796
+ `expected current_nonce response, got ${res.type}`
797
+ );
798
+ }
799
+ if (res.accountAddress.toLowerCase() !== accountAddress.toLowerCase()) {
800
+ throw new BrokerClientError(
801
+ "protocol_error",
802
+ `current_nonce echo mismatch (requested ${accountAddress}, got ${res.accountAddress})`
803
+ );
804
+ }
805
+ return res;
806
+ }
807
+ async notifyUseropLanded(args) {
808
+ const res = await this.exchange({ type: "notify_userop_landed", ...args });
809
+ if (res.type !== "notify_userop_landed") {
810
+ throw new BrokerClientError(
811
+ "protocol_error",
812
+ `expected notify_userop_landed response, got ${res.type}`
813
+ );
814
+ }
815
+ return res;
816
+ }
661
817
  /**
662
818
  * Detect whether the running daemon speaks Path D (protocol 0.4.0+).
663
819
  * Wraps `hello()` with a semver-gte comparison so the MCP tool layer
@@ -932,6 +1088,9 @@ var BackendClient = class {
932
1088
  const jwt = await this.options.jwtSource.get();
933
1089
  headers.authorization = `Bearer ${jwt}`;
934
1090
  }
1091
+ return this.runFetch(method, url, body, headers);
1092
+ }
1093
+ async runFetch(method, url, body, headers) {
935
1094
  const ctrl = new AbortController();
936
1095
  const timer = setTimeout(() => ctrl.abort(), this.options.timeoutMs);
937
1096
  let res;
@@ -1141,12 +1300,12 @@ var BundlerClient = class _BundlerClient {
1141
1300
  }
1142
1301
  const obj = result;
1143
1302
  return {
1144
- callGasLimit: assertHex(obj.callGasLimit, "estimateUserOpGas.callGasLimit"),
1145
- verificationGasLimit: assertHex(
1303
+ callGasLimit: assertHexQuantity(obj.callGasLimit, "estimateUserOpGas.callGasLimit"),
1304
+ verificationGasLimit: assertHexQuantity(
1146
1305
  obj.verificationGasLimit,
1147
1306
  "estimateUserOpGas.verificationGasLimit"
1148
1307
  ),
1149
- preVerificationGas: assertHex(
1308
+ preVerificationGas: assertHexQuantity(
1150
1309
  obj.preVerificationGas,
1151
1310
  "estimateUserOpGas.preVerificationGas"
1152
1311
  )
@@ -1516,8 +1675,19 @@ function assertHex(value, label) {
1516
1675
  }
1517
1676
  return value;
1518
1677
  }
1678
+ function assertHexQuantity(value, label) {
1679
+ if (typeof value !== "string" || !/^0x[0-9a-fA-F]+$/.test(value)) {
1680
+ const repr = value === void 0 ? "undefined" : JSON.stringify(value);
1681
+ const safe = typeof repr === "string" ? repr.slice(0, 80) : "unknown";
1682
+ throw new BundlerClientError(
1683
+ "invalid_response",
1684
+ `${label} must be a 0x-prefixed hex quantity (got ${safe})`
1685
+ );
1686
+ }
1687
+ return value;
1688
+ }
1519
1689
  function assertHexNonZero(value, label, maxValue) {
1520
- const hex = assertHex(value, label);
1690
+ const hex = assertHexQuantity(value, label);
1521
1691
  if (hex.length === 2 || BigInt(hex) === 0n) {
1522
1692
  throw new BundlerClientError(
1523
1693
  "invalid_response",
@@ -1999,6 +2169,7 @@ function buildKernelSessionKeySignature(input) {
1999
2169
  return viem.concatHex([PERMISSION_USE_PREFIX, input.ecdsaSignature]);
2000
2170
  }
2001
2171
  var VALIDATOR_MODE_DEFAULT = "0x00";
2172
+ var VALIDATOR_MODE_ENABLE = "0x01";
2002
2173
  var VALIDATOR_TYPE_PERMISSION = "0x02";
2003
2174
  var PERMISSION_ID_HEX_RE = /^0x[0-9a-fA-F]{8}$/;
2004
2175
  function composeKernelV3NonceKey(args) {
@@ -2015,9 +2186,10 @@ function composeKernelV3NonceKey(args) {
2015
2186
  }
2016
2187
  const paddedPermissionId = viem.pad(args.permissionId, { size: 20, dir: "right" });
2017
2188
  const customKeyHex = viem.pad(`0x${customKey.toString(16)}`, { size: 2 });
2189
+ const modeByte = args.mode === "enable" ? VALIDATOR_MODE_ENABLE : VALIDATOR_MODE_DEFAULT;
2018
2190
  const composite = viem.pad(
2019
2191
  viem.concatHex([
2020
- VALIDATOR_MODE_DEFAULT,
2192
+ modeByte,
2021
2193
  VALIDATOR_TYPE_PERMISSION,
2022
2194
  paddedPermissionId,
2023
2195
  customKeyHex
@@ -2026,6 +2198,38 @@ function composeKernelV3NonceKey(args) {
2026
2198
  );
2027
2199
  return BigInt(composite);
2028
2200
  }
2201
+ var CALL_TYPE_DELEGATE_CALL = "0xFF";
2202
+ var ZERO_ADDRESS_HEX = "0x0000000000000000000000000000000000000000";
2203
+ function wrapEnableModeSignature(input) {
2204
+ const selectorData = viem.concatHex([
2205
+ input.action.selector,
2206
+ input.action.address,
2207
+ input.action.hookAddress ?? ZERO_ADDRESS_HEX,
2208
+ viem.encodeAbiParameters(
2209
+ viem.parseAbiParameters("bytes selectorInitData, bytes hookInitData"),
2210
+ [CALL_TYPE_DELEGATE_CALL, "0x0000"]
2211
+ )
2212
+ ]);
2213
+ const abiEncoded = viem.encodeAbiParameters(
2214
+ viem.parseAbiParameters(
2215
+ "bytes validatorData, bytes hookData, bytes selectorData, bytes enableSig, bytes userOpSig"
2216
+ ),
2217
+ [
2218
+ input.enableData,
2219
+ input.hookData ?? "0x",
2220
+ selectorData,
2221
+ input.enableSig,
2222
+ input.userOpSignature
2223
+ ]
2224
+ );
2225
+ return viem.concatHex([input.hookAddress ?? ZERO_ADDRESS_HEX, abiEncoded]);
2226
+ }
2227
+ viem.parseAbi([
2228
+ "function currentNonce() view returns (uint32)"
2229
+ ]);
2230
+ viem.parseAbi([
2231
+ "event SelectorSet(bytes4 selector, bytes21 vId, bool allowed)"
2232
+ ]);
2029
2233
 
2030
2234
  // src/tools/auth-required.ts
2031
2235
  function authRequiredPayload() {
@@ -2572,6 +2776,9 @@ async function attemptPathD(args, deps) {
2572
2776
  };
2573
2777
  }
2574
2778
  let accountAddress;
2779
+ let mirrorEnableStatus;
2780
+ let mirrorValidatorNonce;
2781
+ let mirrorSessionRow = null;
2575
2782
  try {
2576
2783
  const stateDto = await deps.backend.get("/api/v1/agent/policy/state", {
2577
2784
  surface: "mcp"
@@ -2591,6 +2798,18 @@ async function attemptPathD(args, deps) {
2591
2798
  message: `backend /agent/policy/state lookup failed: ${err2 instanceof Error ? err2.message : String(err2)}`
2592
2799
  };
2593
2800
  }
2801
+ try {
2802
+ const mirror = await deps.backend.get(
2803
+ "/api/v1/agent/policy/scoped-session",
2804
+ { surface: "mcp" }
2805
+ );
2806
+ if (mirror?.session) {
2807
+ mirrorSessionRow = mirror.session;
2808
+ mirrorEnableStatus = mirror.session.enableStatus ?? null;
2809
+ mirrorValidatorNonce = mirror.session.validatorNonce ?? null;
2810
+ }
2811
+ } catch (err2) {
2812
+ }
2594
2813
  if (!snapshot.permissionId) {
2595
2814
  return {
2596
2815
  kind: "fallback",
@@ -2599,6 +2818,115 @@ async function attemptPathD(args, deps) {
2599
2818
  };
2600
2819
  }
2601
2820
  const permissionId = snapshot.permissionId;
2821
+ if (mirrorEnableStatus === "failed") {
2822
+ return {
2823
+ kind: "fallback",
2824
+ reason: "validator_install_failed_re_walk_required",
2825
+ message: "previous Scoped session validator install was flagged failed by the watchdog \u2014 re-walk /agent/policy/transition to mint a fresh session"
2826
+ };
2827
+ }
2828
+ const needsEnable = mirrorEnableStatus === "pending";
2829
+ if (needsEnable && mirrorSessionRow && mirrorSessionRow.signerAddress.toLowerCase() !== snapshot.signerAddress.toLowerCase()) {
2830
+ return {
2831
+ kind: "fallback",
2832
+ reason: "signer_mismatch",
2833
+ message: `mirror row signer ${mirrorSessionRow.signerAddress} disagrees with broker snapshot signer ${snapshot.signerAddress} \u2014 refusing install`
2834
+ };
2835
+ }
2836
+ if (needsEnable && mirrorSessionRow?.permissionId && mirrorSessionRow.permissionId.toLowerCase() !== permissionId.toLowerCase()) {
2837
+ return {
2838
+ kind: "fallback",
2839
+ reason: "install_material_malformed",
2840
+ message: `mirror row permissionId ${mirrorSessionRow.permissionId} disagrees with broker snapshot permissionId ${permissionId} \u2014 refusing install`
2841
+ };
2842
+ }
2843
+ let installMaterial = null;
2844
+ if (needsEnable) {
2845
+ if (!mirrorSessionRow?.sessionId) {
2846
+ return {
2847
+ kind: "fallback",
2848
+ reason: "install_material_malformed",
2849
+ message: "mirror reports enable_status=pending but no sessionId was carried back \u2014 backend response shape regressed"
2850
+ };
2851
+ }
2852
+ let raw;
2853
+ try {
2854
+ raw = await deps.backend.get(
2855
+ `/api/v1/agent/policy/scoped-session/${encodeURIComponent(
2856
+ mirrorSessionRow.sessionId
2857
+ )}/install-material`
2858
+ );
2859
+ } catch (err2) {
2860
+ if (err2 instanceof BackendError && err2.status === 404) {
2861
+ return {
2862
+ kind: "fallback",
2863
+ reason: "install_material_malformed",
2864
+ message: "install-material subroute returned 404 \u2014 row vanished mid-flow OR not owned by the authenticated user; re-walk the ceremony"
2865
+ };
2866
+ }
2867
+ if (err2 instanceof BackendError && err2.status === 401) {
2868
+ return {
2869
+ kind: "fallback",
2870
+ reason: "install_material_unavailable",
2871
+ 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."
2872
+ };
2873
+ }
2874
+ if (err2 instanceof BackendError && err2.status === 403) {
2875
+ return {
2876
+ kind: "fallback",
2877
+ reason: "install_material_unavailable",
2878
+ 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."
2879
+ };
2880
+ }
2881
+ if (err2 instanceof BackendError && (err2.status === void 0 || err2.status >= 500)) {
2882
+ return {
2883
+ kind: "fallback",
2884
+ reason: "install_material_unavailable",
2885
+ 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.`
2886
+ };
2887
+ }
2888
+ return {
2889
+ kind: "fallback",
2890
+ reason: "install_material_malformed",
2891
+ message: `install-material lookup failed (${typedErrorCode(err2)})`
2892
+ };
2893
+ }
2894
+ if (!raw?.installMaterial || !raw.installMaterial.enableData || !raw.installMaterial.enableSig || raw.installMaterial.validatorNonce == null || raw.installMaterial.permissionId == null) {
2895
+ return {
2896
+ kind: "fallback",
2897
+ reason: "install_material_malformed",
2898
+ message: "install-material subroute returned partial payload (enable_data / enable_sig / validator_nonce / permission_id) \u2014 re-walk the ceremony"
2899
+ };
2900
+ }
2901
+ if (raw.installMaterial.permissionId.toLowerCase() !== permissionId.toLowerCase()) {
2902
+ return {
2903
+ kind: "fallback",
2904
+ reason: "install_material_malformed",
2905
+ message: `install-material permissionId ${raw.installMaterial.permissionId} disagrees with broker snapshot permissionId ${permissionId} \u2014 re-walk the ceremony`
2906
+ };
2907
+ }
2908
+ installMaterial = raw.installMaterial;
2909
+ try {
2910
+ const liveNonce = await deps.broker.currentNonce(accountAddress);
2911
+ const minted = installMaterial.validatorNonce ?? mirrorValidatorNonce ?? null;
2912
+ if (minted !== null && liveNonce.nonce !== minted) {
2913
+ return {
2914
+ kind: "fallback",
2915
+ reason: "enable_sig_stale",
2916
+ 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.")
2917
+ };
2918
+ }
2919
+ } catch (err2) {
2920
+ if (err2 instanceof BrokerClientError && err2.brokerCode === "chain_rpc_failed") {
2921
+ return {
2922
+ kind: "fallback",
2923
+ reason: "broker_chain_rpc_failed",
2924
+ 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"
2925
+ };
2926
+ }
2927
+ return mapBrokerCallFailure(err2, "current_nonce", "broker_internal");
2928
+ }
2929
+ }
2602
2930
  let encShares;
2603
2931
  let ephemeralEOA;
2604
2932
  try {
@@ -2659,7 +2987,10 @@ async function attemptPathD(args, deps) {
2659
2987
  let nonce;
2660
2988
  let feeData;
2661
2989
  try {
2662
- const nonceKey = composeKernelV3NonceKey({ permissionId });
2990
+ const nonceKey = composeKernelV3NonceKey({
2991
+ permissionId,
2992
+ mode: needsEnable ? "enable" : "default"
2993
+ });
2663
2994
  nonce = await deps.bundler.getNonce(accountAddress, entryPointAddress, nonceKey);
2664
2995
  feeData = await deps.bundler.getFeeData();
2665
2996
  } catch (err2) {
@@ -2669,13 +3000,32 @@ async function attemptPathD(args, deps) {
2669
3000
  message: `bundler bootstrap failed: ${err2 instanceof BundlerClientError ? `${err2.code}: ${err2.message}` : String(err2)}`
2670
3001
  };
2671
3002
  }
3003
+ let wrapForEnableMode = null;
3004
+ if (needsEnable && installMaterial) {
3005
+ const kernelExecuteSelector = viem.toFunctionSelector(
3006
+ KERNEL_EXECUTE_ABI[0]
3007
+ ).toLowerCase();
3008
+ const enableActionAddress = "0x0000000000000000000000000000000000000000";
3009
+ const enableData = installMaterial.enableData;
3010
+ const enableSig = installMaterial.enableSig;
3011
+ wrapForEnableMode = (userOpSig) => wrapEnableModeSignature({
3012
+ enableData,
3013
+ enableSig,
3014
+ userOpSignature: userOpSig,
3015
+ action: {
3016
+ selector: kernelExecuteSelector,
3017
+ address: enableActionAddress
3018
+ }
3019
+ });
3020
+ }
3021
+ const sponsorshipSignature = wrapForEnableMode ? wrapForEnableMode(PLACEHOLDER_SIGNATURE) : PLACEHOLDER_SIGNATURE;
2672
3022
  const partial = {
2673
3023
  sender: accountAddress,
2674
3024
  nonce: `0x${nonce.toString(16)}`,
2675
3025
  callData: kernelCallData,
2676
3026
  maxFeePerGas: feeData.maxFeePerGas,
2677
3027
  maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
2678
- signature: PLACEHOLDER_SIGNATURE
3028
+ signature: sponsorshipSignature
2679
3029
  };
2680
3030
  let sponsored;
2681
3031
  try {
@@ -2760,6 +3110,10 @@ async function attemptPathD(args, deps) {
2760
3110
  }
2761
3111
  return mapBrokerCallFailure(err2, "sign_userop", "broker_internal");
2762
3112
  }
3113
+ const wrappedSessionKeySig = buildKernelSessionKeySignature({
3114
+ ecdsaSignature: brokerSig
3115
+ });
3116
+ const finalSignature = wrapForEnableMode ? wrapForEnableMode(wrappedSessionKeySig) : wrappedSessionKeySig;
2763
3117
  const signedUserOpWire = {
2764
3118
  sender: accountAddress,
2765
3119
  nonce: partial.nonce,
@@ -2773,7 +3127,7 @@ async function attemptPathD(args, deps) {
2773
3127
  paymasterVerificationGasLimit: sponsored.paymasterVerificationGasLimit,
2774
3128
  paymasterPostOpGasLimit: sponsored.paymasterPostOpGasLimit,
2775
3129
  paymasterData: sponsored.paymasterData,
2776
- signature: buildKernelSessionKeySignature({ ecdsaSignature: brokerSig })
3130
+ signature: finalSignature
2777
3131
  };
2778
3132
  let submittedHash;
2779
3133
  try {
@@ -2795,6 +3149,37 @@ async function attemptPathD(args, deps) {
2795
3149
  }
2796
3150
  try {
2797
3151
  const receipt = await deps.bundler.waitForReceipt(userOpHash, { timeoutMs: 12e3 });
3152
+ if (needsEnable && activeId) {
3153
+ try {
3154
+ let permissionLogIndex = 0;
3155
+ try {
3156
+ const r = receipt.receipt;
3157
+ if (Array.isArray(r.logs)) {
3158
+ const SELECTOR_SET_TOPIC0 = viem.toEventSelector(
3159
+ "SelectorSet(bytes4,bytes21,bool)"
3160
+ ).toLowerCase();
3161
+ for (const l of r.logs) {
3162
+ if (l.topics?.[0]?.toLowerCase() === SELECTOR_SET_TOPIC0 && l.address?.toLowerCase() === accountAddress.toLowerCase()) {
3163
+ permissionLogIndex = l.logIndex ?? 0;
3164
+ break;
3165
+ }
3166
+ }
3167
+ }
3168
+ } catch {
3169
+ }
3170
+ await deps.broker.notifyUseropLanded({
3171
+ sessionId: activeId,
3172
+ accountAddress,
3173
+ permissionId,
3174
+ txHash: receipt.receipt.transactionHash,
3175
+ blockNumber: Number(
3176
+ receipt.receipt.blockNumber ?? 0
3177
+ ),
3178
+ logIndex: permissionLogIndex
3179
+ });
3180
+ } catch {
3181
+ }
3182
+ }
2798
3183
  return {
2799
3184
  kind: "ok",
2800
3185
  data: {
@@ -3294,7 +3679,7 @@ var SERVER_NAME = "@muhaven/mcp";
3294
3679
  var SERVER_VERSION = resolveServerVersion();
3295
3680
  function resolveServerVersion() {
3296
3681
  {
3297
- return "0.2.9";
3682
+ return "0.3.0";
3298
3683
  }
3299
3684
  }
3300
3685
  function toJsonInputSchema(schema) {
@@ -4094,11 +4479,285 @@ function checkPolicy(input) {
4094
4479
  }
4095
4480
  return { ok: true };
4096
4481
  }
4482
+ var KERNEL_V3_CURRENT_NONCE_ABI2 = viem.parseAbi([
4483
+ "function currentNonce() view returns (uint32)"
4484
+ ]);
4485
+ var ChainRpcError = class extends Error {
4486
+ constructor(message, cause) {
4487
+ super(message);
4488
+ this.cause = cause;
4489
+ this.name = "ChainRpcError";
4490
+ }
4491
+ cause;
4492
+ };
4493
+ var CallbackError = class extends Error {
4494
+ constructor(message, cause) {
4495
+ super(message);
4496
+ this.cause = cause;
4497
+ this.name = "CallbackError";
4498
+ }
4499
+ cause;
4500
+ };
4501
+ var CALLBACK_RETRY_SCHEDULE_MS = [
4502
+ 5e3,
4503
+ 15e3,
4504
+ 6e4,
4505
+ 5 * 6e4
4506
+ ];
4507
+ var CALLBACK_MAX_ELAPSED_MS = 60 * 6e4;
4508
+ var DEFAULT_FETCH_TIMEOUT_MS = 15e3;
4509
+ var BrokerOutbound = class {
4510
+ constructor(config, log = () => {
4511
+ }) {
4512
+ this.config = config;
4513
+ this.log = log;
4514
+ this.fetchImpl = config.fetchImpl ?? fetch;
4515
+ this.fetchTimeoutMs = config.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
4516
+ this.setTimeoutImpl = config.setTimeout ?? setTimeout;
4517
+ this.clearTimeoutImpl = config.clearTimeout ?? clearTimeout;
4518
+ }
4519
+ config;
4520
+ log;
4521
+ fetchImpl;
4522
+ fetchTimeoutMs;
4523
+ setTimeoutImpl;
4524
+ clearTimeoutImpl;
4525
+ /**
4526
+ * Wave 5 Option D Commit 3 (multi-agent review SecEng-MED-3) —
4527
+ * in-process dedup of `notify_userop_landed` callbacks. Map of
4528
+ * `<sessionId>:<txHash>` → the in-flight retry loop's Promise.
4529
+ * Repeated IPC calls with the same key fold into the existing
4530
+ * loop instead of spawning a parallel POST. Defends against a
4531
+ * local-socket peer flooding the broker with replay attempts +
4532
+ * caps the retry-budget waste at one loop per real install.
4533
+ */
4534
+ inflightCallbacks = /* @__PURE__ */ new Map();
4535
+ /**
4536
+ * Read the kernel's `currentNonce()` view via `eth_call` against the
4537
+ * configured chain RPC. Returns a uint32. Throws `ChainRpcError` when
4538
+ * unconfigured / network failed / RPC returned non-decodable bytes.
4539
+ */
4540
+ async currentNonce(accountAddress) {
4541
+ if (!this.config.chainRpcUrl) {
4542
+ throw new ChainRpcError(
4543
+ "broker chain RPC unconfigured \u2014 set MUHAVEN_BROKER_RPC_URL or MUHAVEN_BUNDLER_URL"
4544
+ );
4545
+ }
4546
+ const data = viem.encodeFunctionData({
4547
+ abi: KERNEL_V3_CURRENT_NONCE_ABI2,
4548
+ functionName: "currentNonce"
4549
+ });
4550
+ const body = JSON.stringify({
4551
+ jsonrpc: "2.0",
4552
+ id: 1,
4553
+ method: "eth_call",
4554
+ params: [{ to: accountAddress, data }, "latest"]
4555
+ });
4556
+ let res;
4557
+ const ac = new AbortController();
4558
+ const timer = this.setTimeoutImpl(() => ac.abort(), this.fetchTimeoutMs);
4559
+ try {
4560
+ res = await this.fetchImpl(this.config.chainRpcUrl, {
4561
+ method: "POST",
4562
+ headers: {
4563
+ "Content-Type": "application/json",
4564
+ Accept: "application/json",
4565
+ Origin: this.config.outboundOriginHeader
4566
+ },
4567
+ body,
4568
+ signal: ac.signal
4569
+ });
4570
+ } catch (err2) {
4571
+ throw new ChainRpcError(
4572
+ `chain RPC fetch failed: ${err2 instanceof Error ? err2.message : String(err2)}`,
4573
+ err2
4574
+ );
4575
+ } finally {
4576
+ this.clearTimeoutImpl(timer);
4577
+ }
4578
+ if (!res.ok) {
4579
+ throw new ChainRpcError(
4580
+ `chain RPC returned HTTP ${res.status}`
4581
+ );
4582
+ }
4583
+ let parsed;
4584
+ try {
4585
+ parsed = await res.json();
4586
+ } catch (err2) {
4587
+ throw new ChainRpcError(
4588
+ `chain RPC returned non-JSON: ${err2 instanceof Error ? err2.message : String(err2)}`
4589
+ );
4590
+ }
4591
+ if (parsed.error) {
4592
+ throw new ChainRpcError(
4593
+ `chain RPC error: ${parsed.error.message ?? "unknown"}`
4594
+ );
4595
+ }
4596
+ if (typeof parsed.result !== "string" || !/^0x[0-9a-fA-F]*$/.test(parsed.result)) {
4597
+ throw new ChainRpcError(
4598
+ `chain RPC returned non-hex result: ${JSON.stringify(parsed.result).slice(0, 80)}`
4599
+ );
4600
+ }
4601
+ let nonce;
4602
+ try {
4603
+ const decoded = viem.decodeAbiParameters(
4604
+ [{ type: "uint32" }],
4605
+ parsed.result
4606
+ );
4607
+ nonce = Number(decoded[0]);
4608
+ } catch (err2) {
4609
+ throw new ChainRpcError(
4610
+ `failed to decode currentNonce result: ${err2 instanceof Error ? err2.message : String(err2)}`
4611
+ );
4612
+ }
4613
+ if (!Number.isFinite(nonce) || nonce < 0 || nonce > 4294967295) {
4614
+ throw new ChainRpcError(`currentNonce out of uint32 range: ${nonce}`);
4615
+ }
4616
+ return nonce;
4617
+ }
4618
+ /**
4619
+ * Whether the callback path is wired (both secret + backend URL set).
4620
+ * The `notify_userop_landed` daemon handler checks this and returns
4621
+ * `callback_unconfigured` when false so the operator sees the gap.
4622
+ */
4623
+ isCallbackConfigured() {
4624
+ return Boolean(this.config.callbackServiceSecret) && Boolean(this.config.backendBaseUrl);
4625
+ }
4626
+ /**
4627
+ * Queue a `validator-enabled` callback POST to the backend. Returns
4628
+ * immediately; the retry loop runs in the background (5s / 15s / 60s
4629
+ * / 5m, max 1h elapsed). Failures are logged but do NOT propagate to
4630
+ * the IPC caller — the chain indexer is the authoritative safety
4631
+ * net.
4632
+ *
4633
+ * Idempotency: every POST carries an `Idempotency-Key` header
4634
+ * `<sessionId>:validator-enabled`. The backend route is no-op if the
4635
+ * mirror row's `enable_status` is already `'enabled'` (because the
4636
+ * chain indexer raced ahead).
4637
+ *
4638
+ * Returns a Promise resolved when the loop terminates (success or
4639
+ * max-elapsed). Callers don't need to await; tests use it for
4640
+ * deterministic assertions.
4641
+ */
4642
+ enqueueValidatorEnabledCallback(args) {
4643
+ if (!this.isCallbackConfigured()) {
4644
+ return Promise.resolve({
4645
+ ok: false,
4646
+ attempts: 0,
4647
+ lastError: "callback_unconfigured"
4648
+ });
4649
+ }
4650
+ const dedupKey = `${args.sessionId}:${args.txHash.toLowerCase()}:${args.accountAddress.toLowerCase()}`;
4651
+ const existing = this.inflightCallbacks.get(dedupKey);
4652
+ if (existing) {
4653
+ this.log("info", "validator-enabled callback already in flight \u2014 folded", {
4654
+ sessionId: args.sessionId
4655
+ });
4656
+ return existing;
4657
+ }
4658
+ const promise = this.runCallbackLoop(args).finally(() => {
4659
+ this.inflightCallbacks.delete(dedupKey);
4660
+ });
4661
+ this.inflightCallbacks.set(dedupKey, promise);
4662
+ return promise;
4663
+ }
4664
+ async runCallbackLoop(args) {
4665
+ const url = `${this.config.backendBaseUrl.replace(/\/+$/, "")}/api/v1/agent/policy/scoped-session/${encodeURIComponent(args.sessionId)}/validator-enabled`;
4666
+ const body = JSON.stringify({
4667
+ userId: args.userId,
4668
+ accountAddress: args.accountAddress,
4669
+ permissionId: args.permissionId,
4670
+ txHash: args.txHash,
4671
+ blockNumber: args.blockNumber,
4672
+ logIndex: args.logIndex
4673
+ });
4674
+ const startedAt = Date.now();
4675
+ let attempts = 0;
4676
+ let lastError;
4677
+ for (let i = 0; i <= CALLBACK_RETRY_SCHEDULE_MS.length; i++) {
4678
+ if (i > 0) {
4679
+ const delay2 = CALLBACK_RETRY_SCHEDULE_MS[i - 1] ?? 0;
4680
+ const elapsed = Date.now() - startedAt;
4681
+ if (elapsed + delay2 > CALLBACK_MAX_ELAPSED_MS) {
4682
+ lastError = `retry budget exhausted after ${attempts} attempts (last error: ${lastError ?? "unknown"})`;
4683
+ this.log("error", "validator-enabled callback abandoned", {
4684
+ sessionId: args.sessionId,
4685
+ attempts,
4686
+ lastError
4687
+ });
4688
+ return { ok: false, attempts, lastError };
4689
+ }
4690
+ await this.sleep(delay2);
4691
+ }
4692
+ attempts++;
4693
+ try {
4694
+ const ok2 = await this.postCallback(url, body, args.sessionId);
4695
+ if (ok2) {
4696
+ this.log("info", "validator-enabled callback succeeded", {
4697
+ sessionId: args.sessionId,
4698
+ attempts
4699
+ });
4700
+ return { ok: true, attempts };
4701
+ }
4702
+ lastError = "non-2xx response";
4703
+ } catch (err2) {
4704
+ lastError = err2 instanceof Error ? err2.message : String(err2);
4705
+ this.log("warn", "validator-enabled callback attempt failed", {
4706
+ sessionId: args.sessionId,
4707
+ attempt: attempts,
4708
+ err: lastError
4709
+ });
4710
+ }
4711
+ }
4712
+ this.log("error", "validator-enabled callback retry budget exhausted", {
4713
+ sessionId: args.sessionId,
4714
+ attempts,
4715
+ lastError
4716
+ });
4717
+ return { ok: false, attempts, lastError };
4718
+ }
4719
+ async postCallback(url, body, sessionId) {
4720
+ const ac = new AbortController();
4721
+ const timer = this.setTimeoutImpl(() => ac.abort(), this.fetchTimeoutMs);
4722
+ let res;
4723
+ try {
4724
+ res = await this.fetchImpl(url, {
4725
+ method: "POST",
4726
+ headers: {
4727
+ "Content-Type": "application/json",
4728
+ Accept: "application/json",
4729
+ Authorization: `Bearer ${this.config.callbackServiceSecret}`,
4730
+ "Idempotency-Key": `${sessionId}:validator-enabled`,
4731
+ Origin: this.config.outboundOriginHeader
4732
+ },
4733
+ body,
4734
+ signal: ac.signal
4735
+ });
4736
+ } finally {
4737
+ this.clearTimeoutImpl(timer);
4738
+ }
4739
+ if (res.status === 409) {
4740
+ this.log("info", "callback returned 409 (row already enabled \u2014 idempotent)", {
4741
+ sessionId
4742
+ });
4743
+ return true;
4744
+ }
4745
+ if (res.ok) return true;
4746
+ throw new CallbackError(
4747
+ `backend callback returned HTTP ${res.status}`
4748
+ );
4749
+ }
4750
+ sleep(ms) {
4751
+ return new Promise((resolve) => {
4752
+ this.setTimeoutImpl(() => resolve(), ms);
4753
+ });
4754
+ }
4755
+ };
4097
4756
 
4098
4757
  // src/broker/daemon.ts
4099
4758
  var noopLogger = (_e) => {
4100
4759
  };
4101
- async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.floor(Date.now() / 1e3), options = {}, policyStore) {
4760
+ async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.floor(Date.now() / 1e3), options = {}, policyStore, outbound) {
4102
4761
  switch (req.type) {
4103
4762
  case "hello": {
4104
4763
  let hasJwt = false;
@@ -4306,6 +4965,57 @@ async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.fl
4306
4965
  );
4307
4966
  }
4308
4967
  }
4968
+ case "current_nonce": {
4969
+ if (!outbound) {
4970
+ return errorResponse(
4971
+ "chain_rpc_failed",
4972
+ "broker daemon was not configured with a chain RPC URL"
4973
+ );
4974
+ }
4975
+ try {
4976
+ const nonce = await outbound.currentNonce(req.accountAddress);
4977
+ return {
4978
+ type: "current_nonce",
4979
+ nonce,
4980
+ accountAddress: req.accountAddress
4981
+ };
4982
+ } catch (err2) {
4983
+ if (err2 instanceof ChainRpcError) {
4984
+ return errorResponse("chain_rpc_failed", err2.message);
4985
+ }
4986
+ return errorResponse(
4987
+ "chain_rpc_failed",
4988
+ err2 instanceof Error ? err2.message : "chain RPC eth_call failed"
4989
+ );
4990
+ }
4991
+ }
4992
+ case "notify_userop_landed": {
4993
+ if (!outbound) {
4994
+ return errorResponse(
4995
+ "callback_unconfigured",
4996
+ "broker daemon was not configured with the callback service secret"
4997
+ );
4998
+ }
4999
+ if (!outbound.isCallbackConfigured()) {
5000
+ return errorResponse(
5001
+ "callback_unconfigured",
5002
+ "BROKER_CALLBACK_SERVICE_SECRET or backend URL is unset \u2014 validator-enabled callback skipped (chain indexer is the safety net)"
5003
+ );
5004
+ }
5005
+ void outbound.enqueueValidatorEnabledCallback({
5006
+ sessionId: req.sessionId,
5007
+ accountAddress: req.accountAddress,
5008
+ permissionId: req.permissionId,
5009
+ txHash: req.txHash,
5010
+ blockNumber: req.blockNumber,
5011
+ logIndex: req.logIndex
5012
+ });
5013
+ return {
5014
+ type: "notify_userop_landed",
5015
+ queued: true,
5016
+ sessionId: req.sessionId
5017
+ };
5018
+ }
4309
5019
  }
4310
5020
  }
4311
5021
  function errorResponse(code, message) {
@@ -4336,6 +5046,7 @@ var BrokerDaemon = class {
4336
5046
  config;
4337
5047
  keystore;
4338
5048
  policyStore;
5049
+ outbound;
4339
5050
  /**
4340
5051
  * Whether a session-key private half is actually loaded. `false` =
4341
5052
  * daemon booted in read-only posture (no `MUHAVEN_BROKER_SESSION_KEY`
@@ -4356,6 +5067,15 @@ var BrokerDaemon = class {
4356
5067
  }
4357
5068
  this.keystore = options.keystore ?? null;
4358
5069
  this.policyStore = options.policyStore ?? new FilePolicyStore(FilePolicyStore.defaultDir());
5070
+ this.outbound = options.outbound ?? new BrokerOutbound(
5071
+ {
5072
+ chainRpcUrl: options.config.chainRpcUrl,
5073
+ backendBaseUrl: options.config.backendBaseUrl,
5074
+ callbackServiceSecret: options.config.callbackServiceSecret,
5075
+ outboundOriginHeader: options.config.outboundOriginHeader ?? options.config.dashboardBaseUrl
5076
+ },
5077
+ (level, msg, meta) => (options.logger ?? noopLogger)({ level, msg, meta })
5078
+ );
4359
5079
  this.log = options.logger ?? noopLogger;
4360
5080
  this.server = net.createServer((socket) => this.onConnection(socket));
4361
5081
  }
@@ -4488,7 +5208,8 @@ var BrokerDaemon = class {
4488
5208
  },
4489
5209
  pid: process.pid
4490
5210
  },
4491
- this.policyStore
5211
+ this.policyStore,
5212
+ this.outbound
4492
5213
  );
4493
5214
  socket.end(serializeResponse(res));
4494
5215
  } catch (err2) {