@muhaven/mcp 0.2.0 → 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/dist/index.d.ts 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.3.0** — additive bump from 0.2.0 in @muhaven/mcp@0.1.3
13
- * to add `hello.hasSessionKey` + `hello.effectiveConfig` (so a daemon
14
- * booted without `MUHAVEN_BROKER_SESSION_KEY` can serve read paths AND
15
- * surface its effective backend/dashboard URLs to `muhaven-broker login
16
- * --from-daemon`). The 0.2.0 bump from 0.1.0 (Wave 4 P3 ADR-3) added the
17
- * `store_jwt` / `get_jwt` / `clear_jwt` triple — the broker is the single
18
- * keeper of the device-flow JWT (per ADR-3 D1 "polling, not loopback
19
- * callback") in addition to the session-key private half.
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.3.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
- constructor(code: BrokerClientErrorCode, message: string, cause?: unknown | undefined);
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
 
@@ -263,11 +607,240 @@ declare class BackendClient {
263
607
  * Authorization header.
264
608
  */
265
609
  postUnauth<T>(path: string, body: unknown): Promise<T>;
610
+ /**
611
+ * GET variant that sends no Authorization header. Use for backend
612
+ * endpoints that are intentionally public (e.g. `/api/v1/tokens`
613
+ * which the marketplace + the 0.2.1 `positionBuy` NAV-conversion
614
+ * both read). Avoids triggering the AUTH_REQUIRED branch for the
615
+ * "not yet logged in" case on read paths that don't need auth.
616
+ */
617
+ getUnauth<T>(path: string, query?: Record<string, string | number | undefined>): Promise<T>;
266
618
  private buildUrl;
267
619
  private exchangeWithRetry;
268
620
  private exchange;
269
621
  }
270
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
+
271
844
  /**
272
845
  * Tool descriptions are the **single source of truth** for what each MCP
273
846
  * tool advertises to the host LLM. They are also hashed (SHA-256) at
@@ -305,14 +878,15 @@ interface ToolDescriptor {
305
878
  readonly sensitive: boolean;
306
879
  }
307
880
  /**
308
- * The 22 Wave 4 MCP tools across five groups:
309
- * muhaven.read.* (7 — incl. P11 protection_coverage + kyc_attestation)
881
+ * The 23 MCP tools across five groups:
882
+ * muhaven.read.* (8 — incl. P11 protection_coverage + kyc_attestation
883
+ * + 0.2.1 read.activity for Path C settle verify)
310
884
  * muhaven.position.* (4)
311
885
  * muhaven.policy.* (4)
312
886
  * muhaven.issuer.* (5 — P7)
313
887
  * muhaven.governance.* (2 — P11; cast_vote frontend runner deferred to Wave 5)
314
888
  *
315
- * `MUHAVEN_READ_ONLY=true` exposes only the 7 `muhaven.read.*` tools.
889
+ * `MUHAVEN_READ_ONLY=true` exposes only the 8 `muhaven.read.*` tools.
316
890
  * P5's `muhaven.checkout.*` namespace was retired before Wave 4 close — the
317
891
  * hosted checkout surface ships as a separate Vite SPA (apps/checkout-pay/),
318
892
  * not as an MCP tool group.
@@ -387,6 +961,16 @@ interface SessionKeyRequiredPayload {
387
961
  interface ToolDeps {
388
962
  backend: BackendClient;
389
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;
390
974
  /** Surface this MCP server is configured for. Always 'mcp' here, but
391
975
  * carried as a dep so the audit tool can filter to the local surface. */
392
976
  surface: 'mcp';
@@ -397,6 +981,26 @@ interface ToolDeps {
397
981
  * production default when absent.
398
982
  */
399
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}`;
400
1004
  }
401
1005
  type ToolResult<T> = {
402
1006
  ok: true;
@@ -452,12 +1056,25 @@ interface BuildServerOptions {
452
1056
  registry: readonly ToolEntry[];
453
1057
  backend: BackendClient;
454
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;
455
1066
  /**
456
1067
  * Threaded into `ToolDeps` so the `SESSION_KEY_REQUIRED` payload's
457
1068
  * `mintUrl` points at the operator's actual dashboard, not a hardcoded
458
1069
  * production URL.
459
1070
  */
460
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}`;
461
1078
  }
462
1079
  declare function buildMcpServer(opts: BuildServerOptions): Server;
463
1080
  /**
@@ -513,6 +1130,41 @@ interface McpRuntimeConfig {
513
1130
  allowedBackendHosts: readonly string[];
514
1131
  /** In-process JWT cache TTL in seconds. Default 30. */
515
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}`;
516
1168
  }
517
1169
  interface BrokerRuntimeConfig {
518
1170
  /** Endpoint to bind: socket path on POSIX, named pipe name on Windows. */
@@ -661,6 +1313,22 @@ declare class DeviceFlowClient {
661
1313
  interface ISigner {
662
1314
  readonly address: `0x${string}`;
663
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}`>;
664
1332
  }
665
1333
  /**
666
1334
  * Sentinel signer for the read-only daemon posture (no
@@ -678,12 +1346,14 @@ declare const ZERO_ADDRESS: "0x0000000000000000000000000000000000000000";
678
1346
  declare class NullSigner implements ISigner {
679
1347
  readonly address: "0x0000000000000000000000000000000000000000";
680
1348
  signHash(_hash: `0x${string}`): Promise<`0x${string}`>;
1349
+ signRawMessage(_hash: `0x${string}`): Promise<`0x${string}`>;
681
1350
  }
682
1351
  declare class ViemSigner implements ISigner {
683
1352
  private readonly account;
684
1353
  constructor(privateKey: `0x${string}`);
685
1354
  get address(): `0x${string}`;
686
1355
  signHash(hash: `0x${string}`): Promise<`0x${string}`>;
1356
+ signRawMessage(hash: `0x${string}`): Promise<`0x${string}`>;
687
1357
  }
688
1358
 
689
1359
  /**
@@ -739,6 +1409,71 @@ declare function openKeystore(options?: OpenKeystoreOptions): Promise<{
739
1409
  fallbackReason: string | null;
740
1410
  }>;
741
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
+
742
1477
  /**
743
1478
  * `muhaven-broker` daemon — the single-purpose process that holds the
744
1479
  * session-key private half AND the device-flow JWT, exposing only IPC
@@ -766,6 +1501,14 @@ interface BrokerDaemonOptions {
766
1501
  signer?: ISigner;
767
1502
  /** Inject a keystore for tests; default opens the configured backend. */
768
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;
769
1512
  /** Override for the connection-handler logger; defaults to silent. */
770
1513
  logger?: (event: BrokerLogEvent) => void;
771
1514
  }
@@ -805,13 +1548,14 @@ interface HandleBrokerRequestOptions {
805
1548
  */
806
1549
  pid?: number;
807
1550
  }
808
- 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>;
809
1552
  declare class BrokerDaemon {
810
1553
  private readonly server;
811
1554
  private readonly signer;
812
1555
  private readonly log;
813
1556
  private readonly config;
814
1557
  private keystore;
1558
+ private readonly policyStore;
815
1559
  /**
816
1560
  * Whether a session-key private half is actually loaded. `false` =
817
1561
  * daemon booted in read-only posture (no `MUHAVEN_BROKER_SESSION_KEY`
@@ -825,4 +1569,4 @@ declare class BrokerDaemon {
825
1569
  private runAndRespond;
826
1570
  }
827
1571
 
828
- 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 };