@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/CHANGELOG.md +120 -0
- package/dist/broker.cjs +499 -6
- package/dist/broker.js +499 -6
- package/dist/index.cjs +832 -15
- package/dist/index.d.cts +301 -14
- package/dist/index.d.ts +301 -14
- package/dist/index.js +833 -16
- package/manifest.json +1 -1
- package/package.json +2 -1
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.
|
|
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:
|
|
1145
|
-
verificationGasLimit:
|
|
1303
|
+
callGasLimit: assertHexQuantity(obj.callGasLimit, "estimateUserOpGas.callGasLimit"),
|
|
1304
|
+
verificationGasLimit: assertHexQuantity(
|
|
1146
1305
|
obj.verificationGasLimit,
|
|
1147
1306
|
"estimateUserOpGas.verificationGasLimit"
|
|
1148
1307
|
),
|
|
1149
|
-
preVerificationGas:
|
|
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 =
|
|
1690
|
+
const hex = assertHexQuantity(value, label);
|
|
1521
1691
|
if (hex.length === 2 || BigInt(hex) === 0n) {
|
|
1522
1692
|
throw new BundlerClientError(
|
|
1523
1693
|
"invalid_response",
|
|
@@ -1968,6 +2138,26 @@ function encodeKernelExecuteSingleCall(input) {
|
|
|
1968
2138
|
args: [KERNEL_V3_SINGLE_CALL_MODE_DEFAULT, executionCalldata]
|
|
1969
2139
|
});
|
|
1970
2140
|
}
|
|
2141
|
+
function decodeKernelExecuteSingleCall(data) {
|
|
2142
|
+
let decoded;
|
|
2143
|
+
try {
|
|
2144
|
+
decoded = viem.decodeFunctionData({ abi: KERNEL_EXECUTE_ABI, data });
|
|
2145
|
+
} catch {
|
|
2146
|
+
return null;
|
|
2147
|
+
}
|
|
2148
|
+
const [mode, executionCalldata] = decoded.args;
|
|
2149
|
+
if (mode !== KERNEL_V3_SINGLE_CALL_MODE_DEFAULT) {
|
|
2150
|
+
return null;
|
|
2151
|
+
}
|
|
2152
|
+
const ec = executionCalldata.slice(2);
|
|
2153
|
+
if (ec.length < 20 * 2 + 32 * 2) {
|
|
2154
|
+
return null;
|
|
2155
|
+
}
|
|
2156
|
+
const target = `0x${ec.slice(0, 40)}`;
|
|
2157
|
+
const value = BigInt(`0x${ec.slice(40, 40 + 64)}`);
|
|
2158
|
+
const innerCallData = `0x${ec.slice(40 + 64)}`;
|
|
2159
|
+
return { mode, target, value, innerCallData };
|
|
2160
|
+
}
|
|
1971
2161
|
var ECDSA_SIG_HEX_RE = /^0x[0-9a-fA-F]{130}$/;
|
|
1972
2162
|
var PERMISSION_USE_PREFIX = "0xff";
|
|
1973
2163
|
function buildKernelSessionKeySignature(input) {
|
|
@@ -1979,6 +2169,7 @@ function buildKernelSessionKeySignature(input) {
|
|
|
1979
2169
|
return viem.concatHex([PERMISSION_USE_PREFIX, input.ecdsaSignature]);
|
|
1980
2170
|
}
|
|
1981
2171
|
var VALIDATOR_MODE_DEFAULT = "0x00";
|
|
2172
|
+
var VALIDATOR_MODE_ENABLE = "0x01";
|
|
1982
2173
|
var VALIDATOR_TYPE_PERMISSION = "0x02";
|
|
1983
2174
|
var PERMISSION_ID_HEX_RE = /^0x[0-9a-fA-F]{8}$/;
|
|
1984
2175
|
function composeKernelV3NonceKey(args) {
|
|
@@ -1995,9 +2186,10 @@ function composeKernelV3NonceKey(args) {
|
|
|
1995
2186
|
}
|
|
1996
2187
|
const paddedPermissionId = viem.pad(args.permissionId, { size: 20, dir: "right" });
|
|
1997
2188
|
const customKeyHex = viem.pad(`0x${customKey.toString(16)}`, { size: 2 });
|
|
2189
|
+
const modeByte = args.mode === "enable" ? VALIDATOR_MODE_ENABLE : VALIDATOR_MODE_DEFAULT;
|
|
1998
2190
|
const composite = viem.pad(
|
|
1999
2191
|
viem.concatHex([
|
|
2000
|
-
|
|
2192
|
+
modeByte,
|
|
2001
2193
|
VALIDATOR_TYPE_PERMISSION,
|
|
2002
2194
|
paddedPermissionId,
|
|
2003
2195
|
customKeyHex
|
|
@@ -2006,6 +2198,38 @@ function composeKernelV3NonceKey(args) {
|
|
|
2006
2198
|
);
|
|
2007
2199
|
return BigInt(composite);
|
|
2008
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
|
+
]);
|
|
2009
2233
|
|
|
2010
2234
|
// src/tools/auth-required.ts
|
|
2011
2235
|
function authRequiredPayload() {
|
|
@@ -2357,6 +2581,79 @@ async function syncSnapshotFromMirror(deps, brokerSignerAddress) {
|
|
|
2357
2581
|
}
|
|
2358
2582
|
return { kind: "ok", sessionId: activeId };
|
|
2359
2583
|
}
|
|
2584
|
+
var PURCHASE_SELECTOR_LOWER = SUBSCRIPTION_PURCHASE_SELECTOR.toLowerCase();
|
|
2585
|
+
function buildPathDDecodedCall(trace, deps) {
|
|
2586
|
+
const sponsorEvent = trace.find((e) => e.method === "zd_sponsorUserOperation");
|
|
2587
|
+
if (!sponsorEvent) return void 0;
|
|
2588
|
+
let req;
|
|
2589
|
+
try {
|
|
2590
|
+
req = JSON.parse(sponsorEvent.requestBody);
|
|
2591
|
+
} catch {
|
|
2592
|
+
return void 0;
|
|
2593
|
+
}
|
|
2594
|
+
const userOp = req.params?.[0]?.userOp;
|
|
2595
|
+
if (!userOp || typeof userOp.callData !== "string" || !userOp.callData.startsWith("0x")) {
|
|
2596
|
+
return void 0;
|
|
2597
|
+
}
|
|
2598
|
+
const sender = userOp.sender ?? "<missing>";
|
|
2599
|
+
const decoded = decodeKernelExecuteSingleCall(userOp.callData);
|
|
2600
|
+
if (!decoded) {
|
|
2601
|
+
return {
|
|
2602
|
+
sender,
|
|
2603
|
+
kernelExecuteMode: "<undecodable>",
|
|
2604
|
+
kernelExecuteTarget: "<undecodable>",
|
|
2605
|
+
kernelExecuteValue: "<undecodable>",
|
|
2606
|
+
innerSelector: "<undecodable>",
|
|
2607
|
+
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."
|
|
2608
|
+
};
|
|
2609
|
+
}
|
|
2610
|
+
const innerSelector = decoded.innerCallData.slice(0, 10).toLowerCase();
|
|
2611
|
+
let innerPurchaseTokenArg;
|
|
2612
|
+
let innerPurchaseMaxSharesHint;
|
|
2613
|
+
let innerPurchaseEphemeralEOA;
|
|
2614
|
+
if (innerSelector === PURCHASE_SELECTOR_LOWER) {
|
|
2615
|
+
try {
|
|
2616
|
+
const inner = viem.decodeFunctionData({
|
|
2617
|
+
abi: SUBSCRIPTION_PURCHASE_ABI,
|
|
2618
|
+
data: decoded.innerCallData
|
|
2619
|
+
});
|
|
2620
|
+
const [tokenArg, , maxSharesHint, ephemeralEOA] = inner.args;
|
|
2621
|
+
innerPurchaseTokenArg = tokenArg;
|
|
2622
|
+
innerPurchaseMaxSharesHint = maxSharesHint.toString();
|
|
2623
|
+
innerPurchaseEphemeralEOA = ephemeralEOA;
|
|
2624
|
+
} catch {
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
const expectedSubscriptionAddress = deps.subscriptionAddress?.toLowerCase();
|
|
2628
|
+
const kernelTargetLower = decoded.target.toLowerCase();
|
|
2629
|
+
let kernelExecuteTargetMatchesSubscription;
|
|
2630
|
+
let interpretation;
|
|
2631
|
+
if (expectedSubscriptionAddress === void 0) {
|
|
2632
|
+
interpretation = `kernel.execute target=${decoded.target}; inner purchase token=${innerPurchaseTokenArg ?? "<unknown>"}; MUHAVEN_SUBSCRIPTION_ADDRESS not wired on this MCP server \u2014 cannot cross-check.`;
|
|
2633
|
+
} else if (kernelTargetLower === expectedSubscriptionAddress) {
|
|
2634
|
+
kernelExecuteTargetMatchesSubscription = true;
|
|
2635
|
+
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.`;
|
|
2636
|
+
} else if (innerPurchaseTokenArg !== void 0 && kernelTargetLower === innerPurchaseTokenArg.toLowerCase()) {
|
|
2637
|
+
kernelExecuteTargetMatchesSubscription = false;
|
|
2638
|
+
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.`;
|
|
2639
|
+
} else {
|
|
2640
|
+
kernelExecuteTargetMatchesSubscription = false;
|
|
2641
|
+
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.`;
|
|
2642
|
+
}
|
|
2643
|
+
return {
|
|
2644
|
+
sender,
|
|
2645
|
+
kernelExecuteMode: decoded.mode,
|
|
2646
|
+
kernelExecuteTarget: decoded.target,
|
|
2647
|
+
kernelExecuteValue: decoded.value.toString(),
|
|
2648
|
+
innerSelector,
|
|
2649
|
+
innerPurchaseTokenArg,
|
|
2650
|
+
innerPurchaseMaxSharesHint,
|
|
2651
|
+
innerPurchaseEphemeralEOA,
|
|
2652
|
+
expectedSubscriptionAddress: deps.subscriptionAddress,
|
|
2653
|
+
kernelExecuteTargetMatchesSubscription,
|
|
2654
|
+
interpretation
|
|
2655
|
+
};
|
|
2656
|
+
}
|
|
2360
2657
|
async function attemptPathD(args, deps) {
|
|
2361
2658
|
const { shares, tokenAddress, tokenSymbol } = args;
|
|
2362
2659
|
if (!deps.broker || !deps.bundler) {
|
|
@@ -2479,6 +2776,9 @@ async function attemptPathD(args, deps) {
|
|
|
2479
2776
|
};
|
|
2480
2777
|
}
|
|
2481
2778
|
let accountAddress;
|
|
2779
|
+
let mirrorEnableStatus;
|
|
2780
|
+
let mirrorValidatorNonce;
|
|
2781
|
+
let mirrorSessionRow = null;
|
|
2482
2782
|
try {
|
|
2483
2783
|
const stateDto = await deps.backend.get("/api/v1/agent/policy/state", {
|
|
2484
2784
|
surface: "mcp"
|
|
@@ -2498,6 +2798,18 @@ async function attemptPathD(args, deps) {
|
|
|
2498
2798
|
message: `backend /agent/policy/state lookup failed: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
2499
2799
|
};
|
|
2500
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
|
+
}
|
|
2501
2813
|
if (!snapshot.permissionId) {
|
|
2502
2814
|
return {
|
|
2503
2815
|
kind: "fallback",
|
|
@@ -2506,6 +2818,115 @@ async function attemptPathD(args, deps) {
|
|
|
2506
2818
|
};
|
|
2507
2819
|
}
|
|
2508
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
|
+
}
|
|
2509
2930
|
let encShares;
|
|
2510
2931
|
let ephemeralEOA;
|
|
2511
2932
|
try {
|
|
@@ -2566,7 +2987,10 @@ async function attemptPathD(args, deps) {
|
|
|
2566
2987
|
let nonce;
|
|
2567
2988
|
let feeData;
|
|
2568
2989
|
try {
|
|
2569
|
-
const nonceKey = composeKernelV3NonceKey({
|
|
2990
|
+
const nonceKey = composeKernelV3NonceKey({
|
|
2991
|
+
permissionId,
|
|
2992
|
+
mode: needsEnable ? "enable" : "default"
|
|
2993
|
+
});
|
|
2570
2994
|
nonce = await deps.bundler.getNonce(accountAddress, entryPointAddress, nonceKey);
|
|
2571
2995
|
feeData = await deps.bundler.getFeeData();
|
|
2572
2996
|
} catch (err2) {
|
|
@@ -2576,13 +3000,32 @@ async function attemptPathD(args, deps) {
|
|
|
2576
3000
|
message: `bundler bootstrap failed: ${err2 instanceof BundlerClientError ? `${err2.code}: ${err2.message}` : String(err2)}`
|
|
2577
3001
|
};
|
|
2578
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;
|
|
2579
3022
|
const partial = {
|
|
2580
3023
|
sender: accountAddress,
|
|
2581
3024
|
nonce: `0x${nonce.toString(16)}`,
|
|
2582
3025
|
callData: kernelCallData,
|
|
2583
3026
|
maxFeePerGas: feeData.maxFeePerGas,
|
|
2584
3027
|
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
|
|
2585
|
-
signature:
|
|
3028
|
+
signature: sponsorshipSignature
|
|
2586
3029
|
};
|
|
2587
3030
|
let sponsored;
|
|
2588
3031
|
try {
|
|
@@ -2667,6 +3110,10 @@ async function attemptPathD(args, deps) {
|
|
|
2667
3110
|
}
|
|
2668
3111
|
return mapBrokerCallFailure(err2, "sign_userop", "broker_internal");
|
|
2669
3112
|
}
|
|
3113
|
+
const wrappedSessionKeySig = buildKernelSessionKeySignature({
|
|
3114
|
+
ecdsaSignature: brokerSig
|
|
3115
|
+
});
|
|
3116
|
+
const finalSignature = wrapForEnableMode ? wrapForEnableMode(wrappedSessionKeySig) : wrappedSessionKeySig;
|
|
2670
3117
|
const signedUserOpWire = {
|
|
2671
3118
|
sender: accountAddress,
|
|
2672
3119
|
nonce: partial.nonce,
|
|
@@ -2680,7 +3127,7 @@ async function attemptPathD(args, deps) {
|
|
|
2680
3127
|
paymasterVerificationGasLimit: sponsored.paymasterVerificationGasLimit,
|
|
2681
3128
|
paymasterPostOpGasLimit: sponsored.paymasterPostOpGasLimit,
|
|
2682
3129
|
paymasterData: sponsored.paymasterData,
|
|
2683
|
-
signature:
|
|
3130
|
+
signature: finalSignature
|
|
2684
3131
|
};
|
|
2685
3132
|
let submittedHash;
|
|
2686
3133
|
try {
|
|
@@ -2702,6 +3149,37 @@ async function attemptPathD(args, deps) {
|
|
|
2702
3149
|
}
|
|
2703
3150
|
try {
|
|
2704
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
|
+
}
|
|
2705
3183
|
return {
|
|
2706
3184
|
kind: "ok",
|
|
2707
3185
|
data: {
|
|
@@ -2804,6 +3282,7 @@ async function positionBuy(input, deps) {
|
|
|
2804
3282
|
let pathDFallbackDetail;
|
|
2805
3283
|
let pathDSubmittedUserOpHash;
|
|
2806
3284
|
let pathDBundlerTrace;
|
|
3285
|
+
let pathDDecodedCall;
|
|
2807
3286
|
const pathD = await attemptPathD(
|
|
2808
3287
|
{ shares, tokenAddress: token.address, tokenSymbol: token.symbol },
|
|
2809
3288
|
deps
|
|
@@ -2821,6 +3300,7 @@ async function positionBuy(input, deps) {
|
|
|
2821
3300
|
const trace = deps.bundler.drainTrace();
|
|
2822
3301
|
if (trace.length > 0) {
|
|
2823
3302
|
pathDBundlerTrace = trace;
|
|
3303
|
+
pathDDecodedCall = buildPathDDecodedCall(trace, deps);
|
|
2824
3304
|
}
|
|
2825
3305
|
}
|
|
2826
3306
|
}
|
|
@@ -2847,7 +3327,8 @@ ${dashboardUrl}`,
|
|
|
2847
3327
|
...pathDFallbackReason ? { pathDFallbackReason } : {},
|
|
2848
3328
|
...pathDFallbackDetail ? { pathDFallbackDetail } : {},
|
|
2849
3329
|
...pathDSubmittedUserOpHash ? { pathDSubmittedUserOpHash } : {},
|
|
2850
|
-
...pathDBundlerTrace ? { pathDBundlerTrace } : {}
|
|
3330
|
+
...pathDBundlerTrace ? { pathDBundlerTrace } : {},
|
|
3331
|
+
...pathDDecodedCall ? { pathDDecodedCall } : {}
|
|
2851
3332
|
}
|
|
2852
3333
|
});
|
|
2853
3334
|
}
|
|
@@ -3198,7 +3679,7 @@ var SERVER_NAME = "@muhaven/mcp";
|
|
|
3198
3679
|
var SERVER_VERSION = resolveServerVersion();
|
|
3199
3680
|
function resolveServerVersion() {
|
|
3200
3681
|
{
|
|
3201
|
-
return "0.
|
|
3682
|
+
return "0.3.0";
|
|
3202
3683
|
}
|
|
3203
3684
|
}
|
|
3204
3685
|
function toJsonInputSchema(schema) {
|
|
@@ -3998,11 +4479,285 @@ function checkPolicy(input) {
|
|
|
3998
4479
|
}
|
|
3999
4480
|
return { ok: true };
|
|
4000
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
|
+
};
|
|
4001
4756
|
|
|
4002
4757
|
// src/broker/daemon.ts
|
|
4003
4758
|
var noopLogger = (_e) => {
|
|
4004
4759
|
};
|
|
4005
|
-
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) {
|
|
4006
4761
|
switch (req.type) {
|
|
4007
4762
|
case "hello": {
|
|
4008
4763
|
let hasJwt = false;
|
|
@@ -4210,6 +4965,57 @@ async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.fl
|
|
|
4210
4965
|
);
|
|
4211
4966
|
}
|
|
4212
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
|
+
}
|
|
4213
5019
|
}
|
|
4214
5020
|
}
|
|
4215
5021
|
function errorResponse(code, message) {
|
|
@@ -4240,6 +5046,7 @@ var BrokerDaemon = class {
|
|
|
4240
5046
|
config;
|
|
4241
5047
|
keystore;
|
|
4242
5048
|
policyStore;
|
|
5049
|
+
outbound;
|
|
4243
5050
|
/**
|
|
4244
5051
|
* Whether a session-key private half is actually loaded. `false` =
|
|
4245
5052
|
* daemon booted in read-only posture (no `MUHAVEN_BROKER_SESSION_KEY`
|
|
@@ -4260,6 +5067,15 @@ var BrokerDaemon = class {
|
|
|
4260
5067
|
}
|
|
4261
5068
|
this.keystore = options.keystore ?? null;
|
|
4262
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
|
+
);
|
|
4263
5079
|
this.log = options.logger ?? noopLogger;
|
|
4264
5080
|
this.server = net.createServer((socket) => this.onConnection(socket));
|
|
4265
5081
|
}
|
|
@@ -4392,7 +5208,8 @@ var BrokerDaemon = class {
|
|
|
4392
5208
|
},
|
|
4393
5209
|
pid: process.pid
|
|
4394
5210
|
},
|
|
4395
|
-
this.policyStore
|
|
5211
|
+
this.policyStore,
|
|
5212
|
+
this.outbound
|
|
4396
5213
|
);
|
|
4397
5214
|
socket.end(serializeResponse(res));
|
|
4398
5215
|
} catch (err2) {
|