@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/CHANGELOG.md +72 -0
- package/dist/broker.cjs +499 -6
- package/dist/broker.js +499 -6
- package/dist/index.cjs +735 -14
- package/dist/index.d.cts +301 -14
- package/dist/index.d.ts +301 -14
- package/dist/index.js +736 -15
- package/manifest.json +1 -1
- package/package.json +2 -1
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, decodeFunctionData, 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.
|
|
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:
|
|
1141
|
-
verificationGasLimit:
|
|
1299
|
+
callGasLimit: assertHexQuantity(obj.callGasLimit, "estimateUserOpGas.callGasLimit"),
|
|
1300
|
+
verificationGasLimit: assertHexQuantity(
|
|
1142
1301
|
obj.verificationGasLimit,
|
|
1143
1302
|
"estimateUserOpGas.verificationGasLimit"
|
|
1144
1303
|
),
|
|
1145
|
-
preVerificationGas:
|
|
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 =
|
|
1686
|
+
const hex = assertHexQuantity(value, label);
|
|
1517
1687
|
if (hex.length === 2 || BigInt(hex) === 0n) {
|
|
1518
1688
|
throw new BundlerClientError(
|
|
1519
1689
|
"invalid_response",
|
|
@@ -1995,6 +2165,7 @@ function buildKernelSessionKeySignature(input) {
|
|
|
1995
2165
|
return concatHex([PERMISSION_USE_PREFIX, input.ecdsaSignature]);
|
|
1996
2166
|
}
|
|
1997
2167
|
var VALIDATOR_MODE_DEFAULT = "0x00";
|
|
2168
|
+
var VALIDATOR_MODE_ENABLE = "0x01";
|
|
1998
2169
|
var VALIDATOR_TYPE_PERMISSION = "0x02";
|
|
1999
2170
|
var PERMISSION_ID_HEX_RE = /^0x[0-9a-fA-F]{8}$/;
|
|
2000
2171
|
function composeKernelV3NonceKey(args) {
|
|
@@ -2011,9 +2182,10 @@ function composeKernelV3NonceKey(args) {
|
|
|
2011
2182
|
}
|
|
2012
2183
|
const paddedPermissionId = pad(args.permissionId, { size: 20, dir: "right" });
|
|
2013
2184
|
const customKeyHex = pad(`0x${customKey.toString(16)}`, { size: 2 });
|
|
2185
|
+
const modeByte = args.mode === "enable" ? VALIDATOR_MODE_ENABLE : VALIDATOR_MODE_DEFAULT;
|
|
2014
2186
|
const composite = pad(
|
|
2015
2187
|
concatHex([
|
|
2016
|
-
|
|
2188
|
+
modeByte,
|
|
2017
2189
|
VALIDATOR_TYPE_PERMISSION,
|
|
2018
2190
|
paddedPermissionId,
|
|
2019
2191
|
customKeyHex
|
|
@@ -2022,6 +2194,38 @@ function composeKernelV3NonceKey(args) {
|
|
|
2022
2194
|
);
|
|
2023
2195
|
return BigInt(composite);
|
|
2024
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
|
+
]);
|
|
2025
2229
|
|
|
2026
2230
|
// src/tools/auth-required.ts
|
|
2027
2231
|
function authRequiredPayload() {
|
|
@@ -2568,6 +2772,9 @@ async function attemptPathD(args, deps) {
|
|
|
2568
2772
|
};
|
|
2569
2773
|
}
|
|
2570
2774
|
let accountAddress;
|
|
2775
|
+
let mirrorEnableStatus;
|
|
2776
|
+
let mirrorValidatorNonce;
|
|
2777
|
+
let mirrorSessionRow = null;
|
|
2571
2778
|
try {
|
|
2572
2779
|
const stateDto = await deps.backend.get("/api/v1/agent/policy/state", {
|
|
2573
2780
|
surface: "mcp"
|
|
@@ -2587,6 +2794,18 @@ async function attemptPathD(args, deps) {
|
|
|
2587
2794
|
message: `backend /agent/policy/state lookup failed: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
2588
2795
|
};
|
|
2589
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
|
+
}
|
|
2590
2809
|
if (!snapshot.permissionId) {
|
|
2591
2810
|
return {
|
|
2592
2811
|
kind: "fallback",
|
|
@@ -2595,6 +2814,115 @@ async function attemptPathD(args, deps) {
|
|
|
2595
2814
|
};
|
|
2596
2815
|
}
|
|
2597
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
|
+
}
|
|
2598
2926
|
let encShares;
|
|
2599
2927
|
let ephemeralEOA;
|
|
2600
2928
|
try {
|
|
@@ -2655,7 +2983,10 @@ async function attemptPathD(args, deps) {
|
|
|
2655
2983
|
let nonce;
|
|
2656
2984
|
let feeData;
|
|
2657
2985
|
try {
|
|
2658
|
-
const nonceKey = composeKernelV3NonceKey({
|
|
2986
|
+
const nonceKey = composeKernelV3NonceKey({
|
|
2987
|
+
permissionId,
|
|
2988
|
+
mode: needsEnable ? "enable" : "default"
|
|
2989
|
+
});
|
|
2659
2990
|
nonce = await deps.bundler.getNonce(accountAddress, entryPointAddress, nonceKey);
|
|
2660
2991
|
feeData = await deps.bundler.getFeeData();
|
|
2661
2992
|
} catch (err2) {
|
|
@@ -2665,13 +2996,32 @@ async function attemptPathD(args, deps) {
|
|
|
2665
2996
|
message: `bundler bootstrap failed: ${err2 instanceof BundlerClientError ? `${err2.code}: ${err2.message}` : String(err2)}`
|
|
2666
2997
|
};
|
|
2667
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;
|
|
2668
3018
|
const partial = {
|
|
2669
3019
|
sender: accountAddress,
|
|
2670
3020
|
nonce: `0x${nonce.toString(16)}`,
|
|
2671
3021
|
callData: kernelCallData,
|
|
2672
3022
|
maxFeePerGas: feeData.maxFeePerGas,
|
|
2673
3023
|
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
|
|
2674
|
-
signature:
|
|
3024
|
+
signature: sponsorshipSignature
|
|
2675
3025
|
};
|
|
2676
3026
|
let sponsored;
|
|
2677
3027
|
try {
|
|
@@ -2756,6 +3106,10 @@ async function attemptPathD(args, deps) {
|
|
|
2756
3106
|
}
|
|
2757
3107
|
return mapBrokerCallFailure(err2, "sign_userop", "broker_internal");
|
|
2758
3108
|
}
|
|
3109
|
+
const wrappedSessionKeySig = buildKernelSessionKeySignature({
|
|
3110
|
+
ecdsaSignature: brokerSig
|
|
3111
|
+
});
|
|
3112
|
+
const finalSignature = wrapForEnableMode ? wrapForEnableMode(wrappedSessionKeySig) : wrappedSessionKeySig;
|
|
2759
3113
|
const signedUserOpWire = {
|
|
2760
3114
|
sender: accountAddress,
|
|
2761
3115
|
nonce: partial.nonce,
|
|
@@ -2769,7 +3123,7 @@ async function attemptPathD(args, deps) {
|
|
|
2769
3123
|
paymasterVerificationGasLimit: sponsored.paymasterVerificationGasLimit,
|
|
2770
3124
|
paymasterPostOpGasLimit: sponsored.paymasterPostOpGasLimit,
|
|
2771
3125
|
paymasterData: sponsored.paymasterData,
|
|
2772
|
-
signature:
|
|
3126
|
+
signature: finalSignature
|
|
2773
3127
|
};
|
|
2774
3128
|
let submittedHash;
|
|
2775
3129
|
try {
|
|
@@ -2791,6 +3145,37 @@ async function attemptPathD(args, deps) {
|
|
|
2791
3145
|
}
|
|
2792
3146
|
try {
|
|
2793
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
|
+
}
|
|
2794
3179
|
return {
|
|
2795
3180
|
kind: "ok",
|
|
2796
3181
|
data: {
|
|
@@ -3290,7 +3675,7 @@ var SERVER_NAME = "@muhaven/mcp";
|
|
|
3290
3675
|
var SERVER_VERSION = resolveServerVersion();
|
|
3291
3676
|
function resolveServerVersion() {
|
|
3292
3677
|
{
|
|
3293
|
-
return "0.
|
|
3678
|
+
return "0.3.0";
|
|
3294
3679
|
}
|
|
3295
3680
|
}
|
|
3296
3681
|
function toJsonInputSchema(schema) {
|
|
@@ -4090,11 +4475,285 @@ function checkPolicy(input) {
|
|
|
4090
4475
|
}
|
|
4091
4476
|
return { ok: true };
|
|
4092
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
|
+
};
|
|
4093
4752
|
|
|
4094
4753
|
// src/broker/daemon.ts
|
|
4095
4754
|
var noopLogger = (_e) => {
|
|
4096
4755
|
};
|
|
4097
|
-
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) {
|
|
4098
4757
|
switch (req.type) {
|
|
4099
4758
|
case "hello": {
|
|
4100
4759
|
let hasJwt = false;
|
|
@@ -4302,6 +4961,57 @@ async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.fl
|
|
|
4302
4961
|
);
|
|
4303
4962
|
}
|
|
4304
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
|
+
}
|
|
4305
5015
|
}
|
|
4306
5016
|
}
|
|
4307
5017
|
function errorResponse(code, message) {
|
|
@@ -4332,6 +5042,7 @@ var BrokerDaemon = class {
|
|
|
4332
5042
|
config;
|
|
4333
5043
|
keystore;
|
|
4334
5044
|
policyStore;
|
|
5045
|
+
outbound;
|
|
4335
5046
|
/**
|
|
4336
5047
|
* Whether a session-key private half is actually loaded. `false` =
|
|
4337
5048
|
* daemon booted in read-only posture (no `MUHAVEN_BROKER_SESSION_KEY`
|
|
@@ -4352,6 +5063,15 @@ var BrokerDaemon = class {
|
|
|
4352
5063
|
}
|
|
4353
5064
|
this.keystore = options.keystore ?? null;
|
|
4354
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
|
+
);
|
|
4355
5075
|
this.log = options.logger ?? noopLogger;
|
|
4356
5076
|
this.server = createServer((socket) => this.onConnection(socket));
|
|
4357
5077
|
}
|
|
@@ -4484,7 +5204,8 @@ var BrokerDaemon = class {
|
|
|
4484
5204
|
},
|
|
4485
5205
|
pid: process.pid
|
|
4486
5206
|
},
|
|
4487
|
-
this.policyStore
|
|
5207
|
+
this.policyStore,
|
|
5208
|
+
this.outbound
|
|
4488
5209
|
);
|
|
4489
5210
|
socket.end(serializeResponse(res));
|
|
4490
5211
|
} catch (err2) {
|