@muhaven/mcp 0.2.1 → 0.2.2
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 +101 -0
- package/dist/broker.cjs +1092 -149
- package/dist/broker.js +1093 -150
- package/dist/index.cjs +2217 -231
- package/dist/index.d.cts +751 -16
- package/dist/index.d.ts +751 -16
- package/dist/index.js +2203 -220
- package/manifest.json +39 -3
- package/package.json +3 -2
package/dist/index.d.cts
CHANGED
|
@@ -9,14 +9,52 @@ import { z } from 'zod';
|
|
|
9
9
|
* (Windows). Each request is a single JSON object; each response is a
|
|
10
10
|
* single JSON object. No request pipelining, no streaming.
|
|
11
11
|
*
|
|
12
|
-
* **Protocol version 0.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
12
|
+
* **Protocol version 0.4.0** — additive bump from 0.3.0 for Wave 5
|
|
13
|
+
* Path D Slice 1. Adds the policy-snapshot subsystem so the broker can
|
|
14
|
+
* enforce scope + per-op spend cap BEFORE signing a UserOp:
|
|
15
|
+
* - `sign_userop` — like `sign_hash` but carries the structured inner
|
|
16
|
+
* call (target + callData) so the broker validates against the active
|
|
17
|
+
* policy snapshot before delegating to the signer. The MCP server
|
|
18
|
+
* computes the UserOp hash from the prepared UserOp; the broker
|
|
19
|
+
* signs it once policy passes.
|
|
20
|
+
* - `store_policy_snapshot` / `get_policy_snapshot` /
|
|
21
|
+
* `clear_policy_snapshot` — per-session policy CRUD. Snapshot
|
|
22
|
+
* persists across daemon restarts (file-backed under
|
|
23
|
+
* `~/.muhaven/policy-snapshots/<sessionId>.json`).
|
|
24
|
+
* - `get_active_session_id` — narrow "which session is live?" probe used
|
|
25
|
+
* by the MCP server to bootstrap Path D without needing the operator
|
|
26
|
+
* to thread the sessionId through env vars. Returns the sessionId of
|
|
27
|
+
* the SINGLE non-expired snapshot whose `signerAddress` matches the
|
|
28
|
+
* broker's loaded signer; returns `null` when zero match OR when
|
|
29
|
+
* multiple non-expired snapshots match (ambiguous case — caller falls
|
|
30
|
+
* back to Path C). Intentionally narrower than `list()` so RD-3 stays
|
|
31
|
+
* honoured (list() is daemon-internal only).
|
|
32
|
+
* Also adds error codes `rate_limited`, `max_spend_exceeded`,
|
|
33
|
+
* `policy_violation`, `scope_violation`, `no_active_snapshot`. Rate
|
|
34
|
+
* limiting is enforced in Slice 5; the enum value ships now so
|
|
35
|
+
* future-additive protocol bumps don't churn the codeset again.
|
|
36
|
+
*
|
|
37
|
+
* Trust model for `sign_userop`: per-selector, the broker decodes the
|
|
38
|
+
* uint256 at the snapshot-declared `capArgIndex` of `innerCall.callData`
|
|
39
|
+
* and enforces it against the matched `selectorCaps[i].maxAmount`. The
|
|
40
|
+
* broker does NOT re-compute the UserOp hash from packed fields (that
|
|
41
|
+
* would require entry-point + chain-id + packing knowledge inside the
|
|
42
|
+
* broker). The hash itself is trusted as supplied by the MCP server; the
|
|
43
|
+
* broker's job is to refuse signing when scope + per-op cap + signer
|
|
44
|
+
* binding don't match. The on-chain CallPolicy validator is the
|
|
45
|
+
* structural backstop for "what calldata can be executed at all" per
|
|
46
|
+
* RD-2 / RD-5 in `PATH_D_PLAN.md`. Slice 4 wildcard MUST graduate to
|
|
47
|
+
* canonical userOpHash reconstruction (RD-5).
|
|
48
|
+
*
|
|
49
|
+
* **Protocol version 0.3.0** (history) — additive bump from 0.2.0 in
|
|
50
|
+
* @muhaven/mcp@0.1.3 to add `hello.hasSessionKey` + `hello.effectiveConfig`
|
|
51
|
+
* (so a daemon booted without `MUHAVEN_BROKER_SESSION_KEY` can serve read
|
|
52
|
+
* paths AND surface its effective backend/dashboard URLs to
|
|
53
|
+
* `muhaven-broker login --from-daemon`). The 0.2.0 bump from 0.1.0 (Wave
|
|
54
|
+
* 4 P3 ADR-3) added the `store_jwt` / `get_jwt` / `clear_jwt` triple —
|
|
55
|
+
* the broker is the single keeper of the device-flow JWT (per ADR-3 D1
|
|
56
|
+
* "polling, not loopback callback") in addition to the session-key
|
|
57
|
+
* private half.
|
|
20
58
|
*
|
|
21
59
|
* @muhaven/mcp@0.1.5 added `hello.pid` (still protocol 0.3.0 — additive
|
|
22
60
|
* optional field) so `muhaven-broker stop` can reach into the daemon
|
|
@@ -28,14 +66,16 @@ import { z } from 'zod';
|
|
|
28
66
|
* - The broker NEVER reaches out to the network. It only:
|
|
29
67
|
* (a) signs hashes that the MCP server received from the backend,
|
|
30
68
|
* (b) stores / returns / clears a JWT that the MCP server
|
|
31
|
-
* received from the backend
|
|
69
|
+
* received from the backend,
|
|
70
|
+
* (c) stores / returns / clears per-session policy snapshots that
|
|
71
|
+
* the MCP server (or operator CLI) provides at mint time.
|
|
32
72
|
* Splitting network egress (MCP server) from signing + secret
|
|
33
73
|
* storage (broker) is the lethal-trifecta mitigation in
|
|
34
74
|
* `THREAT_MODEL_P0.md` §"Lethal-trifecta self-audit".
|
|
35
75
|
* - Requests are size-capped (`maxRequestBytes`) — a malformed peer
|
|
36
76
|
* cannot exhaust broker memory by sending an unbounded JSON blob.
|
|
37
77
|
*/
|
|
38
|
-
declare const BROKER_PROTOCOL_VERSION = "0.
|
|
78
|
+
declare const BROKER_PROTOCOL_VERSION = "0.4.0";
|
|
39
79
|
interface BrokerHelloRequest {
|
|
40
80
|
readonly type: 'hello';
|
|
41
81
|
}
|
|
@@ -63,6 +103,178 @@ interface BrokerGetJwtRequest {
|
|
|
63
103
|
interface BrokerClearJwtRequest {
|
|
64
104
|
readonly type: 'clear_jwt';
|
|
65
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Wave 5 Path D Slice 1 — request a policy-gated UserOp signature. The
|
|
108
|
+
* MCP server has computed `userOpHash` from a prepared UserOp; the broker
|
|
109
|
+
* looks up the snapshot for `sessionId`, validates `innerCall` against
|
|
110
|
+
* the snapshot's allowlist + per-op cap, signs `userOpHash`, and returns
|
|
111
|
+
* the signature. See protocol-version JSDoc above for the trust model.
|
|
112
|
+
*/
|
|
113
|
+
interface BrokerSignUserOpRequest {
|
|
114
|
+
readonly type: 'sign_userop';
|
|
115
|
+
/** Snapshot key — must match a snapshot previously stored via
|
|
116
|
+
* `store_policy_snapshot`. */
|
|
117
|
+
readonly sessionId: string;
|
|
118
|
+
/** EIP-4337 v0.7 userOpHash, 0x-prefixed 32-byte hex. Signed as-is. */
|
|
119
|
+
readonly userOpHash: `0x${string}`;
|
|
120
|
+
/** Structured representation of the kernel-inner call the prepared
|
|
121
|
+
* UserOp will execute. Broker uses this to validate scope + per-op
|
|
122
|
+
* cap. The MCP server is responsible for ensuring this matches what
|
|
123
|
+
* the on-chain executor will see. */
|
|
124
|
+
readonly innerCall: {
|
|
125
|
+
/** 0x-prefixed 20-byte hex (lowercased for compare-stability). */
|
|
126
|
+
readonly target: `0x${string}`;
|
|
127
|
+
/** Encoded selector + args. Selector = bytes 0..3; ABI words at
|
|
128
|
+
* bytes 4..35, 36..67, ... read by `decodeUint256ArgAt(callData,
|
|
129
|
+
* wordIndex)` per the matched `selectorCaps[i].capArgIndex`. */
|
|
130
|
+
readonly callData: `0x${string}`;
|
|
131
|
+
};
|
|
132
|
+
/** Free-form context for the audit log. NOT trusted as policy input. */
|
|
133
|
+
readonly intent?: {
|
|
134
|
+
readonly tool: string;
|
|
135
|
+
readonly summary?: string;
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Wave 5 Path D Slice 1 — write a per-session policy snapshot. The MCP
|
|
140
|
+
* server (or `muhaven-broker set-policy` CLI in Slice 3) calls this
|
|
141
|
+
* after the frontend mint ceremony to give the broker the rules it
|
|
142
|
+
* should enforce when `sign_userop` arrives.
|
|
143
|
+
*/
|
|
144
|
+
interface BrokerStorePolicySnapshotRequest {
|
|
145
|
+
readonly type: 'store_policy_snapshot';
|
|
146
|
+
readonly snapshot: PolicySnapshotWire;
|
|
147
|
+
}
|
|
148
|
+
interface BrokerGetPolicySnapshotRequest {
|
|
149
|
+
readonly type: 'get_policy_snapshot';
|
|
150
|
+
readonly sessionId: string;
|
|
151
|
+
}
|
|
152
|
+
interface BrokerClearPolicySnapshotRequest {
|
|
153
|
+
readonly type: 'clear_policy_snapshot';
|
|
154
|
+
readonly sessionId: string;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Wave 5 Path D Slice 1 (Commit 3) — narrow "active session" probe. No
|
|
158
|
+
* arguments; the broker resolves uniqueness against its loaded signer's
|
|
159
|
+
* address. The MCP server uses this to bootstrap Path D's broker-side
|
|
160
|
+
* signing path before Slice 2's backend-mirror `agent_scoped_sessions`
|
|
161
|
+
* table lands. Distinct from `get_policy_snapshot(sessionId)`: that
|
|
162
|
+
* fetches a known-id snapshot; this discovers the active id when there
|
|
163
|
+
* is exactly one.
|
|
164
|
+
*
|
|
165
|
+
* Returns `sessionId: null` (rather than an error) for both the "no
|
|
166
|
+
* active snapshot" AND "ambiguous, multiple match" cases — callers must
|
|
167
|
+
* treat them identically (fall back to Path C). Enumerating expired
|
|
168
|
+
* snapshots OR snapshots for other signers is daemon-internal; never
|
|
169
|
+
* surfaced over IPC.
|
|
170
|
+
*/
|
|
171
|
+
interface BrokerGetActiveSessionIdRequest {
|
|
172
|
+
readonly type: 'get_active_session_id';
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Per-selector enforcement rule. The broker matches `innerCall`'s
|
|
176
|
+
* selector against `selector`, then — if `capArgIndex` is not null —
|
|
177
|
+
* decodes the 32-byte word at that index after the 4-byte selector and
|
|
178
|
+
* compares to `maxAmount` (≤ semantics).
|
|
179
|
+
*
|
|
180
|
+
* `capArgIndex` is the 0-based word index INTO THE ABI-ENCODED ARG TAIL.
|
|
181
|
+
* For `subscription.purchase(address token, InEuint128 encShares,
|
|
182
|
+
* uint128 maxSharesHint, address ephemeralEOA)`, the layout is:
|
|
183
|
+
* word 0 (bytes 4..35): address token (left-zero-padded to 32 bytes)
|
|
184
|
+
* word 1 (bytes 36..67): InEuint128 dynamic-offset (FHE-encrypted tail)
|
|
185
|
+
* word 2 (bytes 68..99): uint128 maxSharesHint (left-zero-padded to 32 bytes per Solidity ABI v1; value in the low 16 bytes)
|
|
186
|
+
* word 3 (bytes 100..131): address ephemeralEOA (left-zero-padded)
|
|
187
|
+
* So `capArgIndex: 2` caps `maxSharesHint` — the plaintext upper bound
|
|
188
|
+
* on the encrypted share amount. The actual `encShares` is FHE-encrypted
|
|
189
|
+
* and the broker cannot decrypt it; `maxSharesHint` is the structural
|
|
190
|
+
* ceiling the broker enforces.
|
|
191
|
+
*
|
|
192
|
+
* `capArgIndex: null` means "selector is allowed, no arg-cap enforced"
|
|
193
|
+
* (intended for nullary-value selectors like `claim()` in future slices).
|
|
194
|
+
* `maxAmount` MUST be null when `capArgIndex` is null, and a uint256
|
|
195
|
+
* decimal string otherwise.
|
|
196
|
+
*
|
|
197
|
+
* **Static-arg assumption (Slice 1):** the broker assumes the calldata
|
|
198
|
+
* is ABI-encoded with no dynamic-type args at-or-before `capArgIndex`.
|
|
199
|
+
* For dynamic-arg targets (`bytes`, `string`, dynamic struct head), the
|
|
200
|
+
* 32-byte slot at the cap offset would be an OFFSET, not the value, and
|
|
201
|
+
* the cap would trivially pass on a tiny "offset" value while the real
|
|
202
|
+
* value sits in the dynamic tail. Slice 4 must NOT add such targets
|
|
203
|
+
* without a selector-aware ABI decoder. (`subscription.purchase` is safe
|
|
204
|
+
* — `InEuint128 calldata` is a dynamic struct, but `maxSharesHint` at
|
|
205
|
+
* word 2 is statically encoded after the dynamic-struct head-offset.)
|
|
206
|
+
*/
|
|
207
|
+
interface PolicySelectorCap {
|
|
208
|
+
/** 0x-prefixed 4-byte hex (lowercased). */
|
|
209
|
+
readonly selector: `0x${string}`;
|
|
210
|
+
/** 0-based word index after the selector. Null = no arg-cap enforced. */
|
|
211
|
+
readonly capArgIndex: number | null;
|
|
212
|
+
/** uint256 decimal string. Null iff `capArgIndex` is null. Unit is
|
|
213
|
+
* whatever the target arg's base unit is (e.g. for purchase, uint128
|
|
214
|
+
* maxSharesHint → shares, not mhUSDC base-6). MCP server is
|
|
215
|
+
* responsible for converting user-intent units to the on-chain unit
|
|
216
|
+
* at snapshot-mint time. */
|
|
217
|
+
readonly maxAmount: string | null;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Wire shape of a policy snapshot. Strings (not bigints) so JSON
|
|
221
|
+
* round-trips cleanly.
|
|
222
|
+
*
|
|
223
|
+
* Per-selector caps are carried in `selectorCaps`. The set of allowed
|
|
224
|
+
* selectors is exactly `selectorCaps.map(c => c.selector)` — no
|
|
225
|
+
* `allowedSelectors` field, single source of truth. See
|
|
226
|
+
* `PolicySelectorCap` JSDoc for the static-arg-encoding assumption +
|
|
227
|
+
* `capArgIndex` semantics.
|
|
228
|
+
*/
|
|
229
|
+
interface PolicySnapshotWire {
|
|
230
|
+
readonly sessionId: string;
|
|
231
|
+
readonly mode: 'scoped';
|
|
232
|
+
/** Address derived from the session-key private half. Broker compares
|
|
233
|
+
* this against its loaded `signer.address` at sign time; mismatch
|
|
234
|
+
* rejects with `policy_violation` to defend against snapshots minted
|
|
235
|
+
* for a rotated session-key being applied to the new key. */
|
|
236
|
+
readonly signerAddress: `0x${string}`;
|
|
237
|
+
/** Lowercased 0x-addresses that the broker will accept as
|
|
238
|
+
* `innerCall.target`. */
|
|
239
|
+
readonly targetContracts: readonly `0x${string}`[];
|
|
240
|
+
/** Per-selector rules. Selector must appear in this list AND
|
|
241
|
+
* `innerCall.target` must be in `targetContracts` for the selector
|
|
242
|
+
* match to authorize signing. */
|
|
243
|
+
readonly selectorCaps: readonly PolicySelectorCap[];
|
|
244
|
+
/** Epoch seconds. Snapshot is rejected on lookup after this time. */
|
|
245
|
+
readonly validUntilSec: number;
|
|
246
|
+
/** Epoch seconds at which the snapshot was created (audit). */
|
|
247
|
+
readonly mintedAtSec: number;
|
|
248
|
+
/** Optional Slice 1, REQUIRED Slice 4 wildcard. Hex-32 hash of the
|
|
249
|
+
* authorizing ConfirmToken's `actionHash` so the audit chain
|
|
250
|
+
* {userop → tier transition → consent token} is correlatable by
|
|
251
|
+
* stable key. Future-compat reserve per Trust Architect §4. */
|
|
252
|
+
readonly consentActionHash?: `0x${string}`;
|
|
253
|
+
/** Optional Slice 1, REQUIRED Slice 4 wildcard. Hex-32 hash of the
|
|
254
|
+
* consent text the user saw at mint time (Slice 4 gate #5). */
|
|
255
|
+
readonly consentTextSha256?: `0x${string}`;
|
|
256
|
+
/**
|
|
257
|
+
* Wave 5 Path D Slice 1 Commit 3.5 — `getPermissionId()` for the
|
|
258
|
+
* installed `@zerodev/permissions` PermissionValidator. 4-byte hex
|
|
259
|
+
* (`keccak256(policyAndSignerData).slice(0, 4)`). Used by the MCP
|
|
260
|
+
* server to compose the 24-byte nonce-key composite Kernel v3.1
|
|
261
|
+
* requires for UserOps routed through the permission validator
|
|
262
|
+
* (`pad(concat([VALIDATOR_MODE, VALIDATOR_TYPE.PERMISSION,
|
|
263
|
+
* identifier(20), customKey(2)]))`). Without this, the bundler reads
|
|
264
|
+
* the SUDO-validator nonce slot and the UserOp is routed through
|
|
265
|
+
* the wrong validator → `AA24 InvalidSigner`.
|
|
266
|
+
*
|
|
267
|
+
* **Optional in the wire shape** so the protocol stays back-compat
|
|
268
|
+
* with pre-Pickup-B mints (DB row + broker keystore snapshots
|
|
269
|
+
* predating the Pickup B commit `1a28618` may have NULL here).
|
|
270
|
+
* Pickup B's frontend mint flow ALWAYS populates this field; any
|
|
271
|
+
* non-Pickup-B client / legacy row that omits it surfaces as Path D
|
|
272
|
+
* fallback reason `no_permission_id_in_snapshot` and degrades
|
|
273
|
+
* cleanly to Path C. Broker daemon enforces no tighter today
|
|
274
|
+
* (additive optional field, no protocol version bump).
|
|
275
|
+
*/
|
|
276
|
+
readonly permissionId?: `0x${string}`;
|
|
277
|
+
}
|
|
66
278
|
interface BrokerHelloResponse {
|
|
67
279
|
readonly type: 'hello';
|
|
68
280
|
readonly version: string;
|
|
@@ -128,14 +340,45 @@ interface BrokerClearJwtResponse {
|
|
|
128
340
|
readonly type: 'clear_jwt';
|
|
129
341
|
readonly cleared: true;
|
|
130
342
|
}
|
|
343
|
+
interface BrokerSignUserOpResponse {
|
|
344
|
+
readonly type: 'sign_userop';
|
|
345
|
+
/** 0x-prefixed 65-byte ECDSA signature (r || s || v). */
|
|
346
|
+
readonly signature: `0x${string}`;
|
|
347
|
+
readonly signerAddress: `0x${string}`;
|
|
348
|
+
readonly sessionId: string;
|
|
349
|
+
}
|
|
350
|
+
interface BrokerStorePolicySnapshotResponse {
|
|
351
|
+
readonly type: 'store_policy_snapshot';
|
|
352
|
+
readonly stored: true;
|
|
353
|
+
readonly sessionId: string;
|
|
354
|
+
}
|
|
355
|
+
interface BrokerGetPolicySnapshotResponse {
|
|
356
|
+
readonly type: 'get_policy_snapshot';
|
|
357
|
+
/** Null when no snapshot for the requested sessionId, OR when the
|
|
358
|
+
* snapshot exists but is past `validUntilSec` (caller treats both as
|
|
359
|
+
* "no active snapshot"). */
|
|
360
|
+
readonly snapshot: PolicySnapshotWire | null;
|
|
361
|
+
}
|
|
362
|
+
interface BrokerClearPolicySnapshotResponse {
|
|
363
|
+
readonly type: 'clear_policy_snapshot';
|
|
364
|
+
readonly cleared: true;
|
|
365
|
+
readonly sessionId: string;
|
|
366
|
+
}
|
|
367
|
+
interface BrokerGetActiveSessionIdResponse {
|
|
368
|
+
readonly type: 'get_active_session_id';
|
|
369
|
+
/** Null when zero non-expired snapshots match the broker's loaded
|
|
370
|
+
* signer, OR when 2+ match (ambiguous). Callers fall back to Path C
|
|
371
|
+
* in either case. */
|
|
372
|
+
readonly sessionId: string | null;
|
|
373
|
+
}
|
|
131
374
|
interface BrokerErrorResponse {
|
|
132
375
|
readonly type: 'error';
|
|
133
376
|
readonly code: BrokerErrorCode;
|
|
134
377
|
readonly message: string;
|
|
135
378
|
}
|
|
136
|
-
type BrokerErrorCode = 'invalid_request' | 'payload_too_large' | 'unsupported_type' | 'internal' | 'forbidden' | 'keystore_unavailable' | 'session_key_unavailable';
|
|
137
|
-
type BrokerRequest = BrokerHelloRequest | BrokerSignHashRequest | BrokerStoreJwtRequest | BrokerGetJwtRequest | BrokerClearJwtRequest;
|
|
138
|
-
type BrokerResponse = BrokerHelloResponse | BrokerSignHashResponse | BrokerStoreJwtResponse | BrokerGetJwtResponse | BrokerClearJwtResponse | BrokerErrorResponse;
|
|
379
|
+
type BrokerErrorCode = 'invalid_request' | 'payload_too_large' | 'unsupported_type' | 'internal' | 'forbidden' | 'keystore_unavailable' | 'session_key_unavailable' | 'no_active_snapshot' | 'policy_violation' | 'scope_violation' | 'max_spend_exceeded' | 'rate_limited';
|
|
380
|
+
type BrokerRequest = BrokerHelloRequest | BrokerSignHashRequest | BrokerStoreJwtRequest | BrokerGetJwtRequest | BrokerClearJwtRequest | BrokerSignUserOpRequest | BrokerStorePolicySnapshotRequest | BrokerGetPolicySnapshotRequest | BrokerClearPolicySnapshotRequest | BrokerGetActiveSessionIdRequest;
|
|
381
|
+
type BrokerResponse = BrokerHelloResponse | BrokerSignHashResponse | BrokerStoreJwtResponse | BrokerGetJwtResponse | BrokerClearJwtResponse | BrokerSignUserOpResponse | BrokerStorePolicySnapshotResponse | BrokerGetPolicySnapshotResponse | BrokerClearPolicySnapshotResponse | BrokerGetActiveSessionIdResponse | BrokerErrorResponse;
|
|
139
382
|
/**
|
|
140
383
|
* Parse a single-line request payload. Returns either the validated
|
|
141
384
|
* request or a structured error — the daemon converts errors to a
|
|
@@ -160,12 +403,84 @@ type BrokerClientErrorCode = 'connect_failed' | 'timeout' | 'protocol_error' | '
|
|
|
160
403
|
declare class BrokerClientError extends Error {
|
|
161
404
|
readonly code: BrokerClientErrorCode;
|
|
162
405
|
readonly cause?: unknown | undefined;
|
|
163
|
-
|
|
406
|
+
/**
|
|
407
|
+
* When `code === 'broker_error'`, this carries the typed upstream
|
|
408
|
+
* error code from the daemon (e.g. `unsupported_type`,
|
|
409
|
+
* `policy_violation`, `no_active_snapshot`). Callers in the tool
|
|
410
|
+
* handler layer use it to map to fine-grained fallback reasons
|
|
411
|
+
* without substring-matching on the message. Undefined for non-
|
|
412
|
+
* broker_error variants (connect_failed / timeout / protocol_error)
|
|
413
|
+
* since they never carry a daemon-side code.
|
|
414
|
+
*
|
|
415
|
+
* Added in Wave 5 Path D Slice 1 Commit 3 to close the
|
|
416
|
+
* MCP-Builder H-1: a 0.3.x daemon returning `unsupported_type` for
|
|
417
|
+
* `get_active_session_id` was previously mapped to the generic
|
|
418
|
+
* `broker_internal` Path D fallback — operator-confusing. With
|
|
419
|
+
* `brokerCode`, callers can route `unsupported_type` to
|
|
420
|
+
* `version_too_old` explicitly.
|
|
421
|
+
*/
|
|
422
|
+
readonly brokerCode?: BrokerErrorCode | undefined;
|
|
423
|
+
constructor(code: BrokerClientErrorCode, message: string, cause?: unknown | undefined,
|
|
424
|
+
/**
|
|
425
|
+
* When `code === 'broker_error'`, this carries the typed upstream
|
|
426
|
+
* error code from the daemon (e.g. `unsupported_type`,
|
|
427
|
+
* `policy_violation`, `no_active_snapshot`). Callers in the tool
|
|
428
|
+
* handler layer use it to map to fine-grained fallback reasons
|
|
429
|
+
* without substring-matching on the message. Undefined for non-
|
|
430
|
+
* broker_error variants (connect_failed / timeout / protocol_error)
|
|
431
|
+
* since they never carry a daemon-side code.
|
|
432
|
+
*
|
|
433
|
+
* Added in Wave 5 Path D Slice 1 Commit 3 to close the
|
|
434
|
+
* MCP-Builder H-1: a 0.3.x daemon returning `unsupported_type` for
|
|
435
|
+
* `get_active_session_id` was previously mapped to the generic
|
|
436
|
+
* `broker_internal` Path D fallback — operator-confusing. With
|
|
437
|
+
* `brokerCode`, callers can route `unsupported_type` to
|
|
438
|
+
* `version_too_old` explicitly.
|
|
439
|
+
*/
|
|
440
|
+
brokerCode?: BrokerErrorCode | undefined);
|
|
164
441
|
}
|
|
165
442
|
interface BrokerClientOptions {
|
|
166
443
|
endpoint: string;
|
|
167
444
|
timeoutMs: number;
|
|
168
445
|
}
|
|
446
|
+
/**
|
|
447
|
+
* Result of `BrokerClient.preflight()`. Discriminated on `supported`.
|
|
448
|
+
* The unsupported variants carry enough structured info that the host
|
|
449
|
+
* LLM can render an actionable remediation message — semver-mismatch
|
|
450
|
+
* tells the operator to upgrade the broker; broker-unreachable tells
|
|
451
|
+
* them to start it; session-key-unavailable tells them to rotate env.
|
|
452
|
+
*/
|
|
453
|
+
type PreflightResult = {
|
|
454
|
+
readonly supported: true;
|
|
455
|
+
readonly daemonVersion: string;
|
|
456
|
+
readonly signerAddress: `0x${string}`;
|
|
457
|
+
} | {
|
|
458
|
+
readonly supported: false;
|
|
459
|
+
readonly reason: 'version_too_old';
|
|
460
|
+
readonly daemonVersion: string;
|
|
461
|
+
readonly requiredVersion: string;
|
|
462
|
+
} | {
|
|
463
|
+
readonly supported: false;
|
|
464
|
+
readonly reason: 'session_key_unavailable';
|
|
465
|
+
readonly daemonVersion: string;
|
|
466
|
+
readonly requiredVersion: string;
|
|
467
|
+
} | {
|
|
468
|
+
readonly supported: false;
|
|
469
|
+
readonly reason: 'broker_unreachable';
|
|
470
|
+
readonly message: string;
|
|
471
|
+
readonly requiredVersion: string;
|
|
472
|
+
};
|
|
473
|
+
/**
|
|
474
|
+
* Tiny semver-gte for "is X ≥ Y". Both inputs MUST be M.m.p (no
|
|
475
|
+
* pre-release / build-metadata suffixes) — the broker protocol version
|
|
476
|
+
* is locked to that shape. Non-conforming inputs throw rather than
|
|
477
|
+
* silently mis-compare.
|
|
478
|
+
*
|
|
479
|
+
* Kept inline to avoid adding a `semver` dep just for one three-segment
|
|
480
|
+
* comparison; the broker's version space is small enough that a regex
|
|
481
|
+
* + numeric compare is bulletproof.
|
|
482
|
+
*/
|
|
483
|
+
declare function semverGte(a: string, b: string): boolean;
|
|
169
484
|
declare class BrokerClient {
|
|
170
485
|
private readonly options;
|
|
171
486
|
constructor(options: BrokerClientOptions);
|
|
@@ -177,6 +492,35 @@ declare class BrokerClient {
|
|
|
177
492
|
storeJwt(jwt: string, expiresAtSec?: number): Promise<BrokerStoreJwtResponse>;
|
|
178
493
|
getJwt(): Promise<BrokerGetJwtResponse>;
|
|
179
494
|
clearJwt(): Promise<void>;
|
|
495
|
+
signUserOp(args: {
|
|
496
|
+
sessionId: string;
|
|
497
|
+
userOpHash: `0x${string}`;
|
|
498
|
+
innerCall: {
|
|
499
|
+
target: `0x${string}`;
|
|
500
|
+
callData: `0x${string}`;
|
|
501
|
+
};
|
|
502
|
+
intent?: {
|
|
503
|
+
tool: string;
|
|
504
|
+
summary?: string;
|
|
505
|
+
};
|
|
506
|
+
}): Promise<BrokerSignUserOpResponse>;
|
|
507
|
+
storePolicySnapshot(snapshot: PolicySnapshotWire): Promise<BrokerStorePolicySnapshotResponse>;
|
|
508
|
+
getPolicySnapshot(sessionId: string): Promise<BrokerGetPolicySnapshotResponse>;
|
|
509
|
+
clearPolicySnapshot(sessionId: string): Promise<BrokerClearPolicySnapshotResponse>;
|
|
510
|
+
getActiveSessionId(): Promise<BrokerGetActiveSessionIdResponse>;
|
|
511
|
+
/**
|
|
512
|
+
* Detect whether the running daemon speaks Path D (protocol 0.4.0+).
|
|
513
|
+
* Wraps `hello()` with a semver-gte comparison so the MCP tool layer
|
|
514
|
+
* can short-circuit to Path C with a clear `version_too_old` reason
|
|
515
|
+
* instead of surfacing the opaque `unsupported_type` error a stale
|
|
516
|
+
* 0.3.0 daemon would emit on `sign_userop` / `get_active_session_id`
|
|
517
|
+
* (Backend Architect H-2, round 2).
|
|
518
|
+
*
|
|
519
|
+
* Returns `{ supported: false }` on broker connect failure too — the
|
|
520
|
+
* caller treats "daemon down" identically to "version too old": Path D
|
|
521
|
+
* not available, fall through to Path C.
|
|
522
|
+
*/
|
|
523
|
+
preflight(): Promise<PreflightResult>;
|
|
180
524
|
private exchange;
|
|
181
525
|
}
|
|
182
526
|
|
|
@@ -276,6 +620,227 @@ declare class BackendClient {
|
|
|
276
620
|
private exchange;
|
|
277
621
|
}
|
|
278
622
|
|
|
623
|
+
/**
|
|
624
|
+
* ERC-4337 v0.7 bundler JSON-RPC client. Lives in the MCP SERVER (not
|
|
625
|
+
* the broker daemon) because submission is a network-egress action and
|
|
626
|
+
* the broker is bound by the R-1 zero-egress invariant (see
|
|
627
|
+
* `broker/protocol.ts` JSDoc + THREAT_MODEL_P0.md §"Lethal-trifecta
|
|
628
|
+
* self-audit"). The broker signs hashes; the MCP server (which already
|
|
629
|
+
* speaks HTTPS to the backend) speaks JSON-RPC to the bundler.
|
|
630
|
+
*
|
|
631
|
+
* Wave 5 Path D Slice 1 (Commit 3) scope: the BundlerClient surface +
|
|
632
|
+
* tests ship now, even though the actual UserOp build is deferred to
|
|
633
|
+
* Commit 3.5 (the FHE encrypt + kernel-execute encoding pieces have
|
|
634
|
+
* unresolved design points — see PATH_D_PLAN.md Commit 3 scope-cut).
|
|
635
|
+
*
|
|
636
|
+
* Three RPCs in scope:
|
|
637
|
+
* - `eth_sendUserOperation(userOp, entryPoint)` → returns
|
|
638
|
+
* `userOpHash: Hex32`.
|
|
639
|
+
* - `eth_getUserOperationReceipt(userOpHash)` → returns receipt or
|
|
640
|
+
* `null` when the userOp hasn't been bundled into a block yet.
|
|
641
|
+
* - `eth_chainId()` → returns the bundler's view of the chain id;
|
|
642
|
+
* used to detect operator misconfiguration (wrong network).
|
|
643
|
+
*
|
|
644
|
+
* Trust model: the bundler is a network peer; treat its responses as
|
|
645
|
+
* untrusted input. Every parse step throws a structured BundlerClientError
|
|
646
|
+
* on shape mismatch — never silently coerces. RPC errors (`{code, message,
|
|
647
|
+
* data}`) surface as `rpc_error` with the upstream `code` preserved so the
|
|
648
|
+
* host LLM can map known bundler error classes (`AA21`, `AA23`, etc.)
|
|
649
|
+
* without parsing message text.
|
|
650
|
+
*/
|
|
651
|
+
type BundlerClientErrorCode = 'config' | 'network' | 'timeout' | 'http_error' | 'invalid_response' | 'rpc_error' | 'receipt_timeout' | 'chain_mismatch';
|
|
652
|
+
declare class BundlerClientError extends Error {
|
|
653
|
+
readonly code: BundlerClientErrorCode;
|
|
654
|
+
/** Optional structured detail — for rpc_error, the upstream JSON-RPC
|
|
655
|
+
* error object (e.g. `{ code: -32600, message: "AA23 reverted", data: ... }`).
|
|
656
|
+
* Surface fields that help the LLM steer; never the raw upstream
|
|
657
|
+
* fields (they may carry implementation-specific data). */
|
|
658
|
+
readonly detail?: unknown | undefined;
|
|
659
|
+
constructor(code: BundlerClientErrorCode, message: string,
|
|
660
|
+
/** Optional structured detail — for rpc_error, the upstream JSON-RPC
|
|
661
|
+
* error object (e.g. `{ code: -32600, message: "AA23 reverted", data: ... }`).
|
|
662
|
+
* Surface fields that help the LLM steer; never the raw upstream
|
|
663
|
+
* fields (they may carry implementation-specific data). */
|
|
664
|
+
detail?: unknown | undefined);
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* EIP-4337 v0.7 packed UserOperation as accepted by `eth_sendUserOperation`.
|
|
668
|
+
* Bigint-valued fields are hex strings on the wire (`0x` + ≤64 hex).
|
|
669
|
+
*
|
|
670
|
+
* We accept a permissive shape (Record<string, string>) at the client
|
|
671
|
+
* boundary — the caller (Commit 3.5 UserOp builder) is responsible for
|
|
672
|
+
* stringifying its bigints. The client validates `0x`-prefix shape only;
|
|
673
|
+
* deeper shape correctness is the bundler's job to enforce.
|
|
674
|
+
*/
|
|
675
|
+
interface UserOperationV07Wire {
|
|
676
|
+
readonly sender: `0x${string}`;
|
|
677
|
+
readonly nonce: `0x${string}`;
|
|
678
|
+
readonly factory?: `0x${string}`;
|
|
679
|
+
readonly factoryData?: `0x${string}`;
|
|
680
|
+
readonly callData: `0x${string}`;
|
|
681
|
+
readonly callGasLimit: `0x${string}`;
|
|
682
|
+
readonly verificationGasLimit: `0x${string}`;
|
|
683
|
+
readonly preVerificationGas: `0x${string}`;
|
|
684
|
+
readonly maxFeePerGas: `0x${string}`;
|
|
685
|
+
readonly maxPriorityFeePerGas: `0x${string}`;
|
|
686
|
+
readonly paymaster?: `0x${string}`;
|
|
687
|
+
readonly paymasterVerificationGasLimit?: `0x${string}`;
|
|
688
|
+
readonly paymasterPostOpGasLimit?: `0x${string}`;
|
|
689
|
+
readonly paymasterData?: `0x${string}`;
|
|
690
|
+
readonly signature: `0x${string}`;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Wave 5 Path D Slice 1 Commit 3.5 — the unsigned UserOp shape passed
|
|
694
|
+
* to `pm_sponsorUserOperation`. ZeroDev's paymaster fills in the gas
|
|
695
|
+
* limits + paymaster fields and returns them; the caller then composes
|
|
696
|
+
* the full `UserOperationV07Wire` with the placeholder signature
|
|
697
|
+
* replaced by the broker's session-key signature.
|
|
698
|
+
*
|
|
699
|
+
* Optional fields the bundler tolerates being absent: gas + paymaster
|
|
700
|
+
* fields. `signature` is required (use a non-zero high-entropy
|
|
701
|
+
* placeholder so the bundler simulates a worst-case verifier cost).
|
|
702
|
+
*/
|
|
703
|
+
interface PartialUserOpForSponsorship {
|
|
704
|
+
readonly sender: `0x${string}`;
|
|
705
|
+
readonly nonce: `0x${string}`;
|
|
706
|
+
readonly callData: `0x${string}`;
|
|
707
|
+
readonly maxFeePerGas: `0x${string}`;
|
|
708
|
+
readonly maxPriorityFeePerGas: `0x${string}`;
|
|
709
|
+
/** Worst-case placeholder so paymaster simulates realistic gas. */
|
|
710
|
+
readonly signature: `0x${string}`;
|
|
711
|
+
}
|
|
712
|
+
interface SponsoredUserOpFields {
|
|
713
|
+
readonly paymaster: `0x${string}`;
|
|
714
|
+
readonly paymasterVerificationGasLimit: `0x${string}`;
|
|
715
|
+
readonly paymasterPostOpGasLimit: `0x${string}`;
|
|
716
|
+
readonly paymasterData: `0x${string}`;
|
|
717
|
+
readonly callGasLimit: `0x${string}`;
|
|
718
|
+
readonly verificationGasLimit: `0x${string}`;
|
|
719
|
+
readonly preVerificationGas: `0x${string}`;
|
|
720
|
+
}
|
|
721
|
+
interface EstimatedUserOpGas {
|
|
722
|
+
readonly callGasLimit: `0x${string}`;
|
|
723
|
+
readonly verificationGasLimit: `0x${string}`;
|
|
724
|
+
readonly preVerificationGas: `0x${string}`;
|
|
725
|
+
}
|
|
726
|
+
interface FeeData {
|
|
727
|
+
readonly maxFeePerGas: `0x${string}`;
|
|
728
|
+
readonly maxPriorityFeePerGas: `0x${string}`;
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Receipt shape as returned by `eth_getUserOperationReceipt`. Subset
|
|
732
|
+
* we depend on; bundlers commonly return additional fields we ignore.
|
|
733
|
+
*/
|
|
734
|
+
interface UserOperationReceipt {
|
|
735
|
+
readonly userOpHash: `0x${string}`;
|
|
736
|
+
readonly sender: `0x${string}`;
|
|
737
|
+
readonly success: boolean;
|
|
738
|
+
/** Bundler-reported revert reason (string or hex); present on failure. */
|
|
739
|
+
readonly reason?: string;
|
|
740
|
+
/** The underlying L1/L2 tx that bundled this UserOp. */
|
|
741
|
+
readonly receipt: {
|
|
742
|
+
readonly transactionHash: `0x${string}`;
|
|
743
|
+
readonly blockNumber: `0x${string}`;
|
|
744
|
+
readonly blockHash: `0x${string}`;
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
interface BundlerClientOptions {
|
|
748
|
+
/** Bundler RPC endpoint — MUHAVEN_BUNDLER_URL. https-or-loopback validated
|
|
749
|
+
* at config-load time (see `config.ts::validatePublicUrlEnv`). */
|
|
750
|
+
readonly endpoint: string;
|
|
751
|
+
/** Per-RPC HTTP timeout (ms). Default 15s — same as backend client. */
|
|
752
|
+
readonly requestTimeoutMs: number;
|
|
753
|
+
/** Expected chain id (Arb Sepolia = 421614). When set, `assertChainId()`
|
|
754
|
+
* refuses to proceed if the bundler reports a different chain. */
|
|
755
|
+
readonly expectedChainId?: number;
|
|
756
|
+
/** Inject for tests. */
|
|
757
|
+
readonly fetchImpl?: typeof fetch;
|
|
758
|
+
}
|
|
759
|
+
declare class BundlerClient {
|
|
760
|
+
private readonly options;
|
|
761
|
+
private readonly fetchImpl;
|
|
762
|
+
private nextRpcId;
|
|
763
|
+
constructor(options: BundlerClientOptions);
|
|
764
|
+
/**
|
|
765
|
+
* Submit a signed UserOperation. Returns the userOpHash the bundler
|
|
766
|
+
* computed (which must match the hash the broker signed — caller is
|
|
767
|
+
* responsible for the consistency check; broker policy snapshot
|
|
768
|
+
* captures the signer-binding piece).
|
|
769
|
+
*/
|
|
770
|
+
sendUserOp(userOp: UserOperationV07Wire, entryPoint: `0x${string}`): Promise<`0x${string}`>;
|
|
771
|
+
/** Return the receipt for a userOpHash, or null when the UserOp has
|
|
772
|
+
* not yet been bundled. */
|
|
773
|
+
getReceipt(userOpHash: `0x${string}`): Promise<UserOperationReceipt | null>;
|
|
774
|
+
/**
|
|
775
|
+
* Poll until the bundler returns a receipt, or `timeoutMs` elapses.
|
|
776
|
+
* Caller decides retry / fallback behaviour on `receipt_timeout`.
|
|
777
|
+
*
|
|
778
|
+
* Poll interval grows linearly from `initialIntervalMs` to
|
|
779
|
+
* `maxIntervalMs` to avoid burning the bundler quota when blocks are
|
|
780
|
+
* slow. Default tuning: 500ms → 2000ms over the first 6 polls; then
|
|
781
|
+
* pinned at 2000ms.
|
|
782
|
+
*/
|
|
783
|
+
waitForReceipt(userOpHash: `0x${string}`, opts: {
|
|
784
|
+
readonly timeoutMs: number;
|
|
785
|
+
readonly initialIntervalMs?: number;
|
|
786
|
+
readonly maxIntervalMs?: number;
|
|
787
|
+
/** Inject for tests that want deterministic timing. */
|
|
788
|
+
readonly clockMs?: () => number;
|
|
789
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
790
|
+
}): Promise<UserOperationReceipt>;
|
|
791
|
+
/**
|
|
792
|
+
* Wave 5 Path D Slice 1 Commit 3.5 — `pm_sponsorUserOperation`.
|
|
793
|
+
* ZeroDev's bundler URL serves both bundler RPCs AND paymaster RPCs
|
|
794
|
+
* at the same endpoint, so we don't need a separate paymaster URL.
|
|
795
|
+
* Returns the paymaster fields + the gas limits the paymaster's
|
|
796
|
+
* simulation computed (the caller doesn't need a separate
|
|
797
|
+
* `estimateUserOpGas` round-trip on the happy path).
|
|
798
|
+
*/
|
|
799
|
+
sponsorUserOp(userOp: PartialUserOpForSponsorship, entryPoint: `0x${string}`): Promise<SponsoredUserOpFields>;
|
|
800
|
+
/**
|
|
801
|
+
* Wave 5 Path D Slice 1 Commit 3.5 — `eth_estimateUserOperationGas`.
|
|
802
|
+
* Not used in the happy path (sponsorship returns gas), but lives as
|
|
803
|
+
* a fallback for unsponsored flows OR if the operator's paymaster
|
|
804
|
+
* goes down. Reading gas separately also makes the failure modes
|
|
805
|
+
* distinguishable for the LLM-facing fallback reasons.
|
|
806
|
+
*/
|
|
807
|
+
estimateUserOpGas(userOp: PartialUserOpForSponsorship, entryPoint: `0x${string}`): Promise<EstimatedUserOpGas>;
|
|
808
|
+
/**
|
|
809
|
+
* Wave 5 Path D Slice 1 Commit 3.5 — `eth_call` against the
|
|
810
|
+
* EntryPoint's `getNonce(sender, key)`. Uses the bundler URL as a
|
|
811
|
+
* full Arb Sepolia node (ZeroDev's bundler accepts read-side RPCs).
|
|
812
|
+
*
|
|
813
|
+
* Pass `key = 0n` for the default nonce key — Path D never uses a
|
|
814
|
+
* non-default key in Slice 1; reserved for batched UserOps in
|
|
815
|
+
* later slices.
|
|
816
|
+
*/
|
|
817
|
+
getNonce(sender: `0x${string}`, entryPoint: `0x${string}`, key?: bigint): Promise<bigint>;
|
|
818
|
+
/**
|
|
819
|
+
* Wave 5 Path D Slice 1 Commit 3.5 — fetch the fee market via
|
|
820
|
+
* `eth_gasPrice` (returns a single value the bundler will accept for
|
|
821
|
+
* both maxFee + maxPriorityFee on Arb Sepolia, which has effectively
|
|
822
|
+
* no priority-vs-base distinction).
|
|
823
|
+
*
|
|
824
|
+
* Simple-on-purpose: a full EIP-1559 fee market read would need two
|
|
825
|
+
* RPCs (`eth_maxPriorityFeePerGas` + `eth_getBlock`); Arb Sepolia's
|
|
826
|
+
* fee dynamics don't require that precision and the paymaster pays
|
|
827
|
+
* either way. A future caller wanting EIP-1559 precision can add a
|
|
828
|
+
* sibling method.
|
|
829
|
+
*/
|
|
830
|
+
getFeeData(): Promise<FeeData>;
|
|
831
|
+
/**
|
|
832
|
+
* Verify the bundler's reported chainId matches `expectedChainId`. Cheap
|
|
833
|
+
* to call once at MCP server boot (or lazily before the first send) so
|
|
834
|
+
* a misconfigured bundler URL surfaces as `chain_mismatch` before any
|
|
835
|
+
* user-facing send rather than after a guaranteed-failing submit.
|
|
836
|
+
*
|
|
837
|
+
* Throws `BundlerClientError(config)` if no `expectedChainId` is set —
|
|
838
|
+
* caller asked for an assert without configuring the expectation.
|
|
839
|
+
*/
|
|
840
|
+
assertChainId(): Promise<void>;
|
|
841
|
+
private rpc;
|
|
842
|
+
}
|
|
843
|
+
|
|
279
844
|
/**
|
|
280
845
|
* Tool descriptions are the **single source of truth** for what each MCP
|
|
281
846
|
* tool advertises to the host LLM. They are also hashed (SHA-256) at
|
|
@@ -396,6 +961,16 @@ interface SessionKeyRequiredPayload {
|
|
|
396
961
|
interface ToolDeps {
|
|
397
962
|
backend: BackendClient;
|
|
398
963
|
broker?: BrokerClient;
|
|
964
|
+
/**
|
|
965
|
+
* Wave 5 Path D Slice 1 (Commit 3) — ERC-4337 bundler JSON-RPC client.
|
|
966
|
+
* Undefined → Path D autonomous-buy disabled, position tools fall back
|
|
967
|
+
* to Path C deep-link (existing behaviour). Configured at MCP boot via
|
|
968
|
+
* `MUHAVEN_BUNDLER_URL`. Slice 1 ships the probe + cap-check chain;
|
|
969
|
+
* the actual UserOp build lands in Commit 3.5 (the FHE encrypt + kernel-
|
|
970
|
+
* execute encoding pieces have unresolved design points — see
|
|
971
|
+
* PATH_D_PLAN.md Commit 3 scope-cut).
|
|
972
|
+
*/
|
|
973
|
+
bundler?: BundlerClient;
|
|
399
974
|
/** Surface this MCP server is configured for. Always 'mcp' here, but
|
|
400
975
|
* carried as a dep so the audit tool can filter to the local surface. */
|
|
401
976
|
surface: 'mcp';
|
|
@@ -406,6 +981,26 @@ interface ToolDeps {
|
|
|
406
981
|
* production default when absent.
|
|
407
982
|
*/
|
|
408
983
|
dashboardBaseUrl?: string;
|
|
984
|
+
/**
|
|
985
|
+
* Wave 5 Path D Slice 1 Commit 3.5 — chain id Path D's UserOp hash
|
|
986
|
+
* computation needs. Sourced from `MUHAVEN_CHAIN_ID` env (default
|
|
987
|
+
* Arb Sepolia 421614). Always present; not optional even when Path
|
|
988
|
+
* D is disabled (cheap default + future read tools may want it).
|
|
989
|
+
*/
|
|
990
|
+
chainId?: number;
|
|
991
|
+
/**
|
|
992
|
+
* Wave 5 Path D Slice 1 Commit 3.5 — `MuHavenSubscription` contract
|
|
993
|
+
* address. Sourced from `MUHAVEN_SUBSCRIPTION_ADDRESS`. Undefined
|
|
994
|
+
* disables Path D's UserOp build path; position tools fall back to
|
|
995
|
+
* Path C with reason `subscription_address_unset`.
|
|
996
|
+
*/
|
|
997
|
+
subscriptionAddress?: `0x${string}`;
|
|
998
|
+
/**
|
|
999
|
+
* Wave 5 Path D Slice 1 Commit 3.5 — ERC-4337 EntryPoint v0.7
|
|
1000
|
+
* address. Sourced from `MUHAVEN_ENTRY_POINT` env (default canonical
|
|
1001
|
+
* deployment).
|
|
1002
|
+
*/
|
|
1003
|
+
entryPointAddress?: `0x${string}`;
|
|
409
1004
|
}
|
|
410
1005
|
type ToolResult<T> = {
|
|
411
1006
|
ok: true;
|
|
@@ -461,12 +1056,25 @@ interface BuildServerOptions {
|
|
|
461
1056
|
registry: readonly ToolEntry[];
|
|
462
1057
|
backend: BackendClient;
|
|
463
1058
|
broker: BrokerClient | undefined;
|
|
1059
|
+
/**
|
|
1060
|
+
* Wave 5 Path D Slice 1 (Commit 3) — bundler client wired through to
|
|
1061
|
+
* `ToolDeps.bundler`. Undefined → Path D autonomous mode off; position
|
|
1062
|
+
* tools stay on Path C deep-link (existing behaviour). Configured from
|
|
1063
|
+
* `MUHAVEN_BUNDLER_URL` env at MCP boot.
|
|
1064
|
+
*/
|
|
1065
|
+
bundler?: BundlerClient;
|
|
464
1066
|
/**
|
|
465
1067
|
* Threaded into `ToolDeps` so the `SESSION_KEY_REQUIRED` payload's
|
|
466
1068
|
* `mintUrl` points at the operator's actual dashboard, not a hardcoded
|
|
467
1069
|
* production URL.
|
|
468
1070
|
*/
|
|
469
1071
|
dashboardBaseUrl?: string;
|
|
1072
|
+
/** Wave 5 Path D Slice 1 (Commit 3.5) — Arb Sepolia chain id (421614). */
|
|
1073
|
+
chainId?: number;
|
|
1074
|
+
/** Wave 5 Path D Slice 1 (Commit 3.5) — MuHavenSubscription target. */
|
|
1075
|
+
subscriptionAddress?: `0x${string}`;
|
|
1076
|
+
/** Wave 5 Path D Slice 1 (Commit 3.5) — ERC-4337 EntryPoint v0.7 addr. */
|
|
1077
|
+
entryPointAddress?: `0x${string}`;
|
|
470
1078
|
}
|
|
471
1079
|
declare function buildMcpServer(opts: BuildServerOptions): Server;
|
|
472
1080
|
/**
|
|
@@ -522,6 +1130,41 @@ interface McpRuntimeConfig {
|
|
|
522
1130
|
allowedBackendHosts: readonly string[];
|
|
523
1131
|
/** In-process JWT cache TTL in seconds. Default 30. */
|
|
524
1132
|
jwtCacheTtlSec: number;
|
|
1133
|
+
/**
|
|
1134
|
+
* Wave 5 Path D Slice 1 (Commit 3) — ERC-4337 v0.7 bundler JSON-RPC URL.
|
|
1135
|
+
* Undefined → Path D autonomous-buy mode disabled (position tools fall
|
|
1136
|
+
* back to Path C deep-link). Validated via the same https-or-loopback
|
|
1137
|
+
* rule as backend/dashboard URLs.
|
|
1138
|
+
*/
|
|
1139
|
+
bundlerUrl: string | undefined;
|
|
1140
|
+
/** Path D request timeout for bundler RPC calls. Default 20s
|
|
1141
|
+
* (slightly higher than backend default to absorb head-of-line block
|
|
1142
|
+
* delays). */
|
|
1143
|
+
bundlerTimeoutMs: number;
|
|
1144
|
+
/** Wave 5 Path D — expected chain id (Arb Sepolia = 421614). Sourced
|
|
1145
|
+
* from env so a future mainnet rollout doesn't require a code change.
|
|
1146
|
+
* Default 421614. */
|
|
1147
|
+
chainId: number;
|
|
1148
|
+
/**
|
|
1149
|
+
* Wave 5 Path D Slice 1 (Commit 3.5) — the
|
|
1150
|
+
* `MuHavenSubscription.purchase` target the autonomous-buy UserOp
|
|
1151
|
+
* calls into. Undefined → Path D's UserOp build path is disabled
|
|
1152
|
+
* (handler falls back to Path C with reason
|
|
1153
|
+
* `subscription_address_unset`). Lives in env (NOT a contract
|
|
1154
|
+
* deployment lookup) so the MCP package stays free of the deployment
|
|
1155
|
+
* JSON files. Source of truth: `deployments/arb-sepolia-v2.json`
|
|
1156
|
+
* (prod) / `deployments/arb-sepolia-v2.staging.json` (stage) →
|
|
1157
|
+
* `subscription` field.
|
|
1158
|
+
*/
|
|
1159
|
+
subscriptionAddress: `0x${string}` | undefined;
|
|
1160
|
+
/**
|
|
1161
|
+
* Wave 5 Path D Slice 1 (Commit 3.5) — ERC-4337 EntryPoint v0.7
|
|
1162
|
+
* address. Defaults to the canonical deployment
|
|
1163
|
+
* `0x0000000071727De22E5E9d8BAf0edAc6f37da032` (same on every EVM
|
|
1164
|
+
* chain). Operators on a future EntryPoint rotation override via
|
|
1165
|
+
* `MUHAVEN_ENTRY_POINT`.
|
|
1166
|
+
*/
|
|
1167
|
+
entryPointAddress: `0x${string}`;
|
|
525
1168
|
}
|
|
526
1169
|
interface BrokerRuntimeConfig {
|
|
527
1170
|
/** Endpoint to bind: socket path on POSIX, named pipe name on Windows. */
|
|
@@ -670,6 +1313,22 @@ declare class DeviceFlowClient {
|
|
|
670
1313
|
interface ISigner {
|
|
671
1314
|
readonly address: `0x${string}`;
|
|
672
1315
|
signHash(hash: `0x${string}`): Promise<`0x${string}`>;
|
|
1316
|
+
/**
|
|
1317
|
+
* Wave 5 Path D Slice 1 Commit 3.5 — EIP-191 personal-sign over a
|
|
1318
|
+
* raw 32-byte hash. ZeroDev's permission validator on Kernel v3.1
|
|
1319
|
+
* (via `@zerodev/permissions::signUserOperation`) does
|
|
1320
|
+
* `signer.account.signMessage({ message: { raw: userOpHash } })` —
|
|
1321
|
+
* i.e., it ECDSA-signs `keccak256("\x19Ethereum Signed Message:\n32"
|
|
1322
|
+
* || userOpHash)`, NOT the raw userOpHash. For Path D autonomous
|
|
1323
|
+
* UserOps, the broker MUST sign via this path or the on-chain
|
|
1324
|
+
* `ecrecover` yields a different address → `AA24 InvalidSigner`.
|
|
1325
|
+
*
|
|
1326
|
+
* `signHash` (raw ECDSA over the supplied hash) stays for back-compat
|
|
1327
|
+
* with `sign_hash` IPC verb / Wave 4 placeholder envelope; the new
|
|
1328
|
+
* `signRawMessage` is the verb Path D's `sign_userop` daemon path
|
|
1329
|
+
* calls.
|
|
1330
|
+
*/
|
|
1331
|
+
signRawMessage(hash: `0x${string}`): Promise<`0x${string}`>;
|
|
673
1332
|
}
|
|
674
1333
|
/**
|
|
675
1334
|
* Sentinel signer for the read-only daemon posture (no
|
|
@@ -687,12 +1346,14 @@ declare const ZERO_ADDRESS: "0x0000000000000000000000000000000000000000";
|
|
|
687
1346
|
declare class NullSigner implements ISigner {
|
|
688
1347
|
readonly address: "0x0000000000000000000000000000000000000000";
|
|
689
1348
|
signHash(_hash: `0x${string}`): Promise<`0x${string}`>;
|
|
1349
|
+
signRawMessage(_hash: `0x${string}`): Promise<`0x${string}`>;
|
|
690
1350
|
}
|
|
691
1351
|
declare class ViemSigner implements ISigner {
|
|
692
1352
|
private readonly account;
|
|
693
1353
|
constructor(privateKey: `0x${string}`);
|
|
694
1354
|
get address(): `0x${string}`;
|
|
695
1355
|
signHash(hash: `0x${string}`): Promise<`0x${string}`>;
|
|
1356
|
+
signRawMessage(hash: `0x${string}`): Promise<`0x${string}`>;
|
|
696
1357
|
}
|
|
697
1358
|
|
|
698
1359
|
/**
|
|
@@ -748,6 +1409,71 @@ declare function openKeystore(options?: OpenKeystoreOptions): Promise<{
|
|
|
748
1409
|
fallbackReason: string | null;
|
|
749
1410
|
}>;
|
|
750
1411
|
|
|
1412
|
+
/**
|
|
1413
|
+
* Wave 5 Path D Slice 1 — per-session policy snapshot subsystem for the
|
|
1414
|
+
* broker daemon. Each snapshot describes the rules the broker enforces
|
|
1415
|
+
* BEFORE signing a UserOp: target-contract allowlist, selector allowlist,
|
|
1416
|
+
* per-op amount cap, expiry.
|
|
1417
|
+
*
|
|
1418
|
+
* Persistence model: one JSON file per snapshot under
|
|
1419
|
+
* `~/.muhaven/policy-snapshots/<sessionId>.json`. Atomic writes via
|
|
1420
|
+
* tmp-file + rename (POSIX & Windows). Mode 0600 on POSIX; Windows ACL
|
|
1421
|
+
* is whatever the user's profile dir provides.
|
|
1422
|
+
*
|
|
1423
|
+
* Why a directory not a single keystore record (cf. `keystore.ts` for
|
|
1424
|
+
* the JWT): users can have multiple concurrent scoped sessions
|
|
1425
|
+
* (different surfaces, different tiers), and the broker needs lookup-by-
|
|
1426
|
+
* sessionId during sign_userop. The JWT is a single-tenant record;
|
|
1427
|
+
* snapshots are a multi-record store keyed by sessionId.
|
|
1428
|
+
*
|
|
1429
|
+
* The OS keychain backend is intentionally NOT used here. Keychain APIs
|
|
1430
|
+
* generally hold one value per (service, account) pair and are awkward
|
|
1431
|
+
* for the "list all keys" lookup the doctor command needs. File-only
|
|
1432
|
+
* keeps it simple. Snapshots aren't long-term secrets — they describe
|
|
1433
|
+
* policy, not credentials — so the security posture is "operator
|
|
1434
|
+
* filesystem permissions" not "OS-trust-zone".
|
|
1435
|
+
*/
|
|
1436
|
+
|
|
1437
|
+
/** Public alias — callers can import either name. */
|
|
1438
|
+
type PolicySnapshot = PolicySnapshotWire;
|
|
1439
|
+
|
|
1440
|
+
interface IPolicyStore {
|
|
1441
|
+
/** Return the snapshot for `sessionId`, or null if absent OR past
|
|
1442
|
+
* `validUntilSec`. Callers treat both as "no active snapshot." */
|
|
1443
|
+
get(sessionId: string, nowSec: number): Promise<PolicySnapshot | null>;
|
|
1444
|
+
/** Overwrite any existing record for the snapshot's sessionId. */
|
|
1445
|
+
put(snapshot: PolicySnapshot): Promise<void>;
|
|
1446
|
+
/** Delete the snapshot. No-op when sessionId is absent. */
|
|
1447
|
+
delete(sessionId: string): Promise<void>;
|
|
1448
|
+
/** Return every snapshot in the store (including expired). Used by
|
|
1449
|
+
* doctor / hello surfaces for diagnostics. NEVER exposed over IPC —
|
|
1450
|
+
* see RD-3 commentary in PATH_D_PLAN.md and Backend Architect M-4. */
|
|
1451
|
+
list(): Promise<PolicySnapshot[]>;
|
|
1452
|
+
/**
|
|
1453
|
+
* Return the sessionId of the SINGLE non-expired snapshot whose
|
|
1454
|
+
* `signerAddress` (case-insensitive) matches `activeSignerAddress`, or
|
|
1455
|
+
* `null` when zero match OR when 2+ match (ambiguous case). Backs the
|
|
1456
|
+
* narrow `get_active_session_id` IPC verb (Wave 5 Path D Slice 1
|
|
1457
|
+
* Commit 3) — intentionally narrower than `list()` so RD-3 / M-4 stays
|
|
1458
|
+
* honoured (snapshot enumeration never crosses the IPC boundary; only
|
|
1459
|
+
* the unique-id answer does).
|
|
1460
|
+
*
|
|
1461
|
+
* Callers fall back to Path C on null. The "ambiguous" case is a
|
|
1462
|
+
* design choice — operators rotating through scoped sessions
|
|
1463
|
+
* intentionally must clear the prior session before the new one
|
|
1464
|
+
* becomes "active" — so we never auto-pick a winner the operator
|
|
1465
|
+
* didn't ask for.
|
|
1466
|
+
*/
|
|
1467
|
+
activeSessionId(activeSignerAddress: `0x${string}`, nowSec: number): Promise<string | null>;
|
|
1468
|
+
/** Optional one-shot async setup. The file-backed impl doesn't need
|
|
1469
|
+
* this — the dir is created lazily on first put. Slice 5's spend-
|
|
1470
|
+
* ledger impl will need to seed the SHA-256 chain from disk at boot;
|
|
1471
|
+
* daemon.start() awaits `policyStore.init?.()` so the seed runs
|
|
1472
|
+
* before any IPC request. Adding the optional method now avoids a
|
|
1473
|
+
* refactor when Slice 5 lands (Backend Architect M-1). */
|
|
1474
|
+
init?(): Promise<void>;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
751
1477
|
/**
|
|
752
1478
|
* `muhaven-broker` daemon — the single-purpose process that holds the
|
|
753
1479
|
* session-key private half AND the device-flow JWT, exposing only IPC
|
|
@@ -775,6 +1501,14 @@ interface BrokerDaemonOptions {
|
|
|
775
1501
|
signer?: ISigner;
|
|
776
1502
|
/** Inject a keystore for tests; default opens the configured backend. */
|
|
777
1503
|
keystore?: IKeystore;
|
|
1504
|
+
/**
|
|
1505
|
+
* Inject a policy store for tests; defaults to a `FilePolicyStore` rooted
|
|
1506
|
+
* at `~/.muhaven/policy-snapshots/`. The store is consulted by
|
|
1507
|
+
* `sign_userop` (validate before signing) and exposed via the
|
|
1508
|
+
* `store_policy_snapshot` / `get_policy_snapshot` / `clear_policy_snapshot`
|
|
1509
|
+
* verbs added in Wave 5 Path D Slice 1 (protocol 0.4.0).
|
|
1510
|
+
*/
|
|
1511
|
+
policyStore?: IPolicyStore;
|
|
778
1512
|
/** Override for the connection-handler logger; defaults to silent. */
|
|
779
1513
|
logger?: (event: BrokerLogEvent) => void;
|
|
780
1514
|
}
|
|
@@ -814,13 +1548,14 @@ interface HandleBrokerRequestOptions {
|
|
|
814
1548
|
*/
|
|
815
1549
|
pid?: number;
|
|
816
1550
|
}
|
|
817
|
-
declare function handleBrokerRequest(req: BrokerRequest, signer: ISigner, keystore: IKeystore, nowSec?: () => number, options?: HandleBrokerRequestOptions): Promise<BrokerResponse>;
|
|
1551
|
+
declare function handleBrokerRequest(req: BrokerRequest, signer: ISigner, keystore: IKeystore, nowSec?: () => number, options?: HandleBrokerRequestOptions, policyStore?: IPolicyStore): Promise<BrokerResponse>;
|
|
818
1552
|
declare class BrokerDaemon {
|
|
819
1553
|
private readonly server;
|
|
820
1554
|
private readonly signer;
|
|
821
1555
|
private readonly log;
|
|
822
1556
|
private readonly config;
|
|
823
1557
|
private keystore;
|
|
1558
|
+
private readonly policyStore;
|
|
824
1559
|
/**
|
|
825
1560
|
* Whether a session-key private half is actually loaded. `false` =
|
|
826
1561
|
* daemon booted in read-only posture (no `MUHAVEN_BROKER_SESSION_KEY`
|
|
@@ -834,4 +1569,4 @@ declare class BrokerDaemon {
|
|
|
834
1569
|
private runAndRespond;
|
|
835
1570
|
}
|
|
836
1571
|
|
|
837
|
-
export { BROKER_PROTOCOL_VERSION, BackendClient, BackendError, type BackendErrorCode, BrokerClient, BrokerClientError, type BrokerClientErrorCode, BrokerDaemon, type BrokerDaemonOptions, type BrokerRequest, type BrokerResponse, type BrokerRuntimeConfig, DeviceFlowAbortedError, DeviceFlowClient, type DeviceFlowEvent, type HandleBrokerRequestOptions, type IKeystore, type ISigner, JwtSource, type KeystoreBackend, KeystoreError, type McpRuntimeConfig, MissingSessionKeyError, NoJwtAvailableError, NullSigner, type RunMcpStdioCliOptions, SERVER_VERSION, TOOL_DESCRIPTORS, type ToolDescriptor, type ToolEntry, type ToolHashEntry, ViemSigner, ZERO_ADDRESS, buildMcpServer, buildToolHashTable, defaultBrokerEndpoint, fullToolRegistry, handleBrokerRequest, hashToolDescriptor, loadBrokerConfig, loadMcpConfig, openKeystore, parseBrokerRequest, registryForReadOnly, runMcpStdioCli, selectRegistry, serializeResponse, verifyDescriptorAgainstPin };
|
|
1572
|
+
export { BROKER_PROTOCOL_VERSION, BackendClient, BackendError, type BackendErrorCode, BrokerClient, BrokerClientError, type BrokerClientErrorCode, BrokerDaemon, type BrokerDaemonOptions, type BrokerRequest, type BrokerResponse, type BrokerRuntimeConfig, BundlerClient, BundlerClientError, type BundlerClientErrorCode, type BundlerClientOptions, DeviceFlowAbortedError, DeviceFlowClient, type DeviceFlowEvent, type HandleBrokerRequestOptions, type IKeystore, type ISigner, JwtSource, type KeystoreBackend, KeystoreError, type McpRuntimeConfig, MissingSessionKeyError, NoJwtAvailableError, NullSigner, type PreflightResult, type RunMcpStdioCliOptions, SERVER_VERSION, TOOL_DESCRIPTORS, type ToolDescriptor, type ToolEntry, type ToolHashEntry, type UserOperationReceipt, type UserOperationV07Wire, ViemSigner, ZERO_ADDRESS, buildMcpServer, buildToolHashTable, defaultBrokerEndpoint, fullToolRegistry, handleBrokerRequest, hashToolDescriptor, loadBrokerConfig, loadMcpConfig, openKeystore, parseBrokerRequest, registryForReadOnly, runMcpStdioCli, selectRegistry, semverGte, serializeResponse, verifyDescriptorAgainstPin };
|