@muhaven/mcp 0.2.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/broker.cjs CHANGED
@@ -3,10 +3,12 @@
3
3
  var os = require('os');
4
4
  var child_process = require('child_process');
5
5
  var path = require('path');
6
+ var readline = require('readline');
6
7
  var net = require('net');
7
8
  var promises = require('fs/promises');
8
9
  var accounts = require('viem/accounts');
9
10
  var crypto = require('crypto');
11
+ var viem = require('viem');
10
12
 
11
13
  var DEFAULT_BACKEND_URL = "https://api.muhaven.app";
12
14
  var DEFAULT_DASHBOARD_URL = "https://muhaven.app";
@@ -161,22 +163,45 @@ function loadBrokerConfig(env = process.env) {
161
163
  env.MUHAVEN_DASHBOARD_URL,
162
164
  DEFAULT_DASHBOARD_URL
163
165
  );
166
+ const chainRpcUrlRaw = readEnv("MUHAVEN_BROKER_RPC_URL", env) ?? readEnv("MUHAVEN_BUNDLER_URL", env);
167
+ const chainRpcUrl = chainRpcUrlRaw === void 0 ? void 0 : resolvePublicUrlEnv(
168
+ "MUHAVEN_BROKER_RPC_URL",
169
+ chainRpcUrlRaw,
170
+ chainRpcUrlRaw
171
+ );
172
+ const callbackServiceSecretRaw = readEnv("BROKER_CALLBACK_SERVICE_SECRET", env);
173
+ let callbackServiceSecret;
174
+ if (callbackServiceSecretRaw !== void 0) {
175
+ if (callbackServiceSecretRaw.length < 16) {
176
+ throw new Error(
177
+ "BROKER_CALLBACK_SERVICE_SECRET must be at least 16 characters (matches backend with-service-secret middleware floor)"
178
+ );
179
+ }
180
+ callbackServiceSecret = callbackServiceSecretRaw;
181
+ }
182
+ const outboundOriginHeader = readEnv("MUHAVEN_BROKER_ORIGIN", env) ?? dashboardBaseUrl;
164
183
  return {
165
184
  endpoint,
166
185
  sessionKeyHex,
167
186
  maxRequestBytes,
168
187
  requestTimeoutMs,
169
188
  backendBaseUrl,
170
- dashboardBaseUrl
189
+ dashboardBaseUrl,
190
+ chainRpcUrl,
191
+ callbackServiceSecret,
192
+ outboundOriginHeader
171
193
  };
172
194
  }
173
195
 
174
196
  // src/broker/protocol.ts
175
- var BROKER_PROTOCOL_VERSION = "0.4.0";
197
+ var BROKER_PROTOCOL_VERSION = "0.5.0";
176
198
  var HASH_HEX_RE = /^0x[0-9a-fA-F]{64}$/;
177
199
  var ADDRESS_HEX_RE2 = /^0x[0-9a-fA-F]{40}$/;
178
200
  var SELECTOR_HEX_RE = /^0x[0-9a-fA-F]{8}$/;
179
201
  var HEX_PREFIXED_RE = /^0x[0-9a-fA-F]*$/;
202
+ var ENABLE_DATA_HEX_RE = /^0x[0-9a-fA-F]{2,65536}$/;
203
+ var ENABLE_SIG_HEX_RE = /^0x[0-9a-fA-F]{256,16384}$/;
204
+ var UINT32_MAX = 4294967295;
180
205
  var JWT_RE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
181
206
  var SESSION_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
182
207
  var UINT256_DEC_RE = /^(0|[1-9][0-9]{0,77})$/;
@@ -296,6 +321,43 @@ function parsePolicySnapshot(raw) {
296
321
  if (!isOptionalPermissionId(obj.permissionId)) {
297
322
  return { error: "snapshot.permissionId must be a 0x-prefixed 4-byte hex when provided" };
298
323
  }
324
+ const enableData = obj.enableData;
325
+ const enableSig = obj.enableSig;
326
+ const validatorNonce = obj.validatorNonce;
327
+ const installPresent = [enableData, enableSig, validatorNonce].filter(
328
+ (v) => v !== void 0
329
+ ).length;
330
+ if (installPresent !== 0 && installPresent !== 3) {
331
+ return {
332
+ error: "snapshot.{enableData,enableSig,validatorNonce} must be all-present or all-absent (Option D Commit 3 install-material trio)"
333
+ };
334
+ }
335
+ if (enableData !== void 0) {
336
+ if (typeof enableData !== "string" || !ENABLE_DATA_HEX_RE.test(enableData)) {
337
+ return {
338
+ error: "snapshot.enableData must be a 0x-prefixed hex string of 2..65536 chars when provided"
339
+ };
340
+ }
341
+ }
342
+ if (enableSig !== void 0) {
343
+ if (typeof enableSig !== "string" || !ENABLE_SIG_HEX_RE.test(enableSig)) {
344
+ return {
345
+ error: "snapshot.enableSig must be a 0x-prefixed hex string of 256..16384 chars (WebAuthn envelope) when provided"
346
+ };
347
+ }
348
+ }
349
+ if (validatorNonce !== void 0) {
350
+ if (typeof validatorNonce !== "number" || !Number.isInteger(validatorNonce) || validatorNonce < 0 || validatorNonce > UINT32_MAX) {
351
+ return {
352
+ error: "snapshot.validatorNonce must be an integer in [0, 2^32-1] when provided"
353
+ };
354
+ }
355
+ }
356
+ if (installPresent === 3 && obj.permissionId === void 0) {
357
+ return {
358
+ error: "snapshot install material requires permissionId in the same snapshot"
359
+ };
360
+ }
299
361
  return {
300
362
  sessionId: obj.sessionId,
301
363
  mode: "scoped",
@@ -308,7 +370,10 @@ function parsePolicySnapshot(raw) {
308
370
  mintedAtSec: obj.mintedAtSec,
309
371
  ...obj.consentActionHash === void 0 ? {} : { consentActionHash: obj.consentActionHash.toLowerCase() },
310
372
  ...obj.consentTextSha256 === void 0 ? {} : { consentTextSha256: obj.consentTextSha256.toLowerCase() },
311
- ...obj.permissionId === void 0 ? {} : { permissionId: obj.permissionId.toLowerCase() }
373
+ ...obj.permissionId === void 0 ? {} : { permissionId: obj.permissionId.toLowerCase() },
374
+ ...enableData === void 0 ? {} : { enableData: enableData.toLowerCase() },
375
+ ...enableSig === void 0 ? {} : { enableSig: enableSig.toLowerCase() },
376
+ ...validatorNonce === void 0 ? {} : { validatorNonce }
312
377
  };
313
378
  }
314
379
  function parseBrokerRequest(line) {
@@ -487,6 +552,72 @@ function parseBrokerRequest(line) {
487
552
  }
488
553
  case "get_active_session_id":
489
554
  return { type: "get_active_session_id" };
555
+ case "current_nonce": {
556
+ if (!isAddressHex(obj.accountAddress)) {
557
+ return {
558
+ type: "error",
559
+ code: "invalid_request",
560
+ message: "current_nonce.accountAddress must be a 0x-prefixed 20-byte hex"
561
+ };
562
+ }
563
+ return {
564
+ type: "current_nonce",
565
+ accountAddress: obj.accountAddress.toLowerCase()
566
+ };
567
+ }
568
+ case "notify_userop_landed": {
569
+ if (!isSessionIdShape(obj.sessionId)) {
570
+ return {
571
+ type: "error",
572
+ code: "invalid_request",
573
+ message: "notify_userop_landed.sessionId must be 1-128 chars [A-Za-z0-9_-]"
574
+ };
575
+ }
576
+ if (!isAddressHex(obj.accountAddress)) {
577
+ return {
578
+ type: "error",
579
+ code: "invalid_request",
580
+ message: "notify_userop_landed.accountAddress must be a 0x-prefixed 20-byte hex"
581
+ };
582
+ }
583
+ if (!isSelectorHex(obj.permissionId)) {
584
+ return {
585
+ type: "error",
586
+ code: "invalid_request",
587
+ message: "notify_userop_landed.permissionId must be a 0x-prefixed 4-byte hex"
588
+ };
589
+ }
590
+ if (!isHashHex(obj.txHash)) {
591
+ return {
592
+ type: "error",
593
+ code: "invalid_request",
594
+ message: "notify_userop_landed.txHash must be a 0x-prefixed 32-byte hex"
595
+ };
596
+ }
597
+ if (typeof obj.blockNumber !== "number" || !Number.isFinite(obj.blockNumber) || !Number.isInteger(obj.blockNumber) || obj.blockNumber < 0) {
598
+ return {
599
+ type: "error",
600
+ code: "invalid_request",
601
+ message: "notify_userop_landed.blockNumber must be a non-negative integer"
602
+ };
603
+ }
604
+ if (typeof obj.logIndex !== "number" || !Number.isFinite(obj.logIndex) || !Number.isInteger(obj.logIndex) || obj.logIndex < 0) {
605
+ return {
606
+ type: "error",
607
+ code: "invalid_request",
608
+ message: "notify_userop_landed.logIndex must be a non-negative integer"
609
+ };
610
+ }
611
+ return {
612
+ type: "notify_userop_landed",
613
+ sessionId: obj.sessionId,
614
+ accountAddress: obj.accountAddress.toLowerCase(),
615
+ permissionId: obj.permissionId.toLowerCase(),
616
+ txHash: obj.txHash.toLowerCase(),
617
+ blockNumber: obj.blockNumber,
618
+ logIndex: obj.logIndex
619
+ };
620
+ }
490
621
  default:
491
622
  return {
492
623
  type: "error",
@@ -646,6 +777,33 @@ var BrokerClient = class {
646
777
  }
647
778
  return res;
648
779
  }
780
+ // ── Wave 5 Option D Commit 3 — install-mode pre-check + callback notify ──
781
+ async currentNonce(accountAddress) {
782
+ const res = await this.exchange({ type: "current_nonce", accountAddress });
783
+ if (res.type !== "current_nonce") {
784
+ throw new BrokerClientError(
785
+ "protocol_error",
786
+ `expected current_nonce response, got ${res.type}`
787
+ );
788
+ }
789
+ if (res.accountAddress.toLowerCase() !== accountAddress.toLowerCase()) {
790
+ throw new BrokerClientError(
791
+ "protocol_error",
792
+ `current_nonce echo mismatch (requested ${accountAddress}, got ${res.accountAddress})`
793
+ );
794
+ }
795
+ return res;
796
+ }
797
+ async notifyUseropLanded(args) {
798
+ const res = await this.exchange({ type: "notify_userop_landed", ...args });
799
+ if (res.type !== "notify_userop_landed") {
800
+ throw new BrokerClientError(
801
+ "protocol_error",
802
+ `expected notify_userop_landed response, got ${res.type}`
803
+ );
804
+ }
805
+ return res;
806
+ }
649
807
  /**
650
808
  * Detect whether the running daemon speaks Path D (protocol 0.4.0+).
651
809
  * Wraps `hello()` with a semver-gte comparison so the MCP tool layer
@@ -1436,11 +1594,285 @@ function checkPolicy(input) {
1436
1594
  }
1437
1595
  return { ok: true };
1438
1596
  }
1597
+ var KERNEL_V3_CURRENT_NONCE_ABI = viem.parseAbi([
1598
+ "function currentNonce() view returns (uint32)"
1599
+ ]);
1600
+ var ChainRpcError = class extends Error {
1601
+ constructor(message, cause) {
1602
+ super(message);
1603
+ this.cause = cause;
1604
+ this.name = "ChainRpcError";
1605
+ }
1606
+ cause;
1607
+ };
1608
+ var CallbackError = class extends Error {
1609
+ constructor(message, cause) {
1610
+ super(message);
1611
+ this.cause = cause;
1612
+ this.name = "CallbackError";
1613
+ }
1614
+ cause;
1615
+ };
1616
+ var CALLBACK_RETRY_SCHEDULE_MS = [
1617
+ 5e3,
1618
+ 15e3,
1619
+ 6e4,
1620
+ 5 * 6e4
1621
+ ];
1622
+ var CALLBACK_MAX_ELAPSED_MS = 60 * 6e4;
1623
+ var DEFAULT_FETCH_TIMEOUT_MS = 15e3;
1624
+ var BrokerOutbound = class {
1625
+ constructor(config, log = () => {
1626
+ }) {
1627
+ this.config = config;
1628
+ this.log = log;
1629
+ this.fetchImpl = config.fetchImpl ?? fetch;
1630
+ this.fetchTimeoutMs = config.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
1631
+ this.setTimeoutImpl = config.setTimeout ?? setTimeout;
1632
+ this.clearTimeoutImpl = config.clearTimeout ?? clearTimeout;
1633
+ }
1634
+ config;
1635
+ log;
1636
+ fetchImpl;
1637
+ fetchTimeoutMs;
1638
+ setTimeoutImpl;
1639
+ clearTimeoutImpl;
1640
+ /**
1641
+ * Wave 5 Option D Commit 3 (multi-agent review SecEng-MED-3) —
1642
+ * in-process dedup of `notify_userop_landed` callbacks. Map of
1643
+ * `<sessionId>:<txHash>` → the in-flight retry loop's Promise.
1644
+ * Repeated IPC calls with the same key fold into the existing
1645
+ * loop instead of spawning a parallel POST. Defends against a
1646
+ * local-socket peer flooding the broker with replay attempts +
1647
+ * caps the retry-budget waste at one loop per real install.
1648
+ */
1649
+ inflightCallbacks = /* @__PURE__ */ new Map();
1650
+ /**
1651
+ * Read the kernel's `currentNonce()` view via `eth_call` against the
1652
+ * configured chain RPC. Returns a uint32. Throws `ChainRpcError` when
1653
+ * unconfigured / network failed / RPC returned non-decodable bytes.
1654
+ */
1655
+ async currentNonce(accountAddress) {
1656
+ if (!this.config.chainRpcUrl) {
1657
+ throw new ChainRpcError(
1658
+ "broker chain RPC unconfigured \u2014 set MUHAVEN_BROKER_RPC_URL or MUHAVEN_BUNDLER_URL"
1659
+ );
1660
+ }
1661
+ const data = viem.encodeFunctionData({
1662
+ abi: KERNEL_V3_CURRENT_NONCE_ABI,
1663
+ functionName: "currentNonce"
1664
+ });
1665
+ const body = JSON.stringify({
1666
+ jsonrpc: "2.0",
1667
+ id: 1,
1668
+ method: "eth_call",
1669
+ params: [{ to: accountAddress, data }, "latest"]
1670
+ });
1671
+ let res;
1672
+ const ac = new AbortController();
1673
+ const timer = this.setTimeoutImpl(() => ac.abort(), this.fetchTimeoutMs);
1674
+ try {
1675
+ res = await this.fetchImpl(this.config.chainRpcUrl, {
1676
+ method: "POST",
1677
+ headers: {
1678
+ "Content-Type": "application/json",
1679
+ Accept: "application/json",
1680
+ Origin: this.config.outboundOriginHeader
1681
+ },
1682
+ body,
1683
+ signal: ac.signal
1684
+ });
1685
+ } catch (err) {
1686
+ throw new ChainRpcError(
1687
+ `chain RPC fetch failed: ${err instanceof Error ? err.message : String(err)}`,
1688
+ err
1689
+ );
1690
+ } finally {
1691
+ this.clearTimeoutImpl(timer);
1692
+ }
1693
+ if (!res.ok) {
1694
+ throw new ChainRpcError(
1695
+ `chain RPC returned HTTP ${res.status}`
1696
+ );
1697
+ }
1698
+ let parsed;
1699
+ try {
1700
+ parsed = await res.json();
1701
+ } catch (err) {
1702
+ throw new ChainRpcError(
1703
+ `chain RPC returned non-JSON: ${err instanceof Error ? err.message : String(err)}`
1704
+ );
1705
+ }
1706
+ if (parsed.error) {
1707
+ throw new ChainRpcError(
1708
+ `chain RPC error: ${parsed.error.message ?? "unknown"}`
1709
+ );
1710
+ }
1711
+ if (typeof parsed.result !== "string" || !/^0x[0-9a-fA-F]*$/.test(parsed.result)) {
1712
+ throw new ChainRpcError(
1713
+ `chain RPC returned non-hex result: ${JSON.stringify(parsed.result).slice(0, 80)}`
1714
+ );
1715
+ }
1716
+ let nonce;
1717
+ try {
1718
+ const decoded = viem.decodeAbiParameters(
1719
+ [{ type: "uint32" }],
1720
+ parsed.result
1721
+ );
1722
+ nonce = Number(decoded[0]);
1723
+ } catch (err) {
1724
+ throw new ChainRpcError(
1725
+ `failed to decode currentNonce result: ${err instanceof Error ? err.message : String(err)}`
1726
+ );
1727
+ }
1728
+ if (!Number.isFinite(nonce) || nonce < 0 || nonce > 4294967295) {
1729
+ throw new ChainRpcError(`currentNonce out of uint32 range: ${nonce}`);
1730
+ }
1731
+ return nonce;
1732
+ }
1733
+ /**
1734
+ * Whether the callback path is wired (both secret + backend URL set).
1735
+ * The `notify_userop_landed` daemon handler checks this and returns
1736
+ * `callback_unconfigured` when false so the operator sees the gap.
1737
+ */
1738
+ isCallbackConfigured() {
1739
+ return Boolean(this.config.callbackServiceSecret) && Boolean(this.config.backendBaseUrl);
1740
+ }
1741
+ /**
1742
+ * Queue a `validator-enabled` callback POST to the backend. Returns
1743
+ * immediately; the retry loop runs in the background (5s / 15s / 60s
1744
+ * / 5m, max 1h elapsed). Failures are logged but do NOT propagate to
1745
+ * the IPC caller — the chain indexer is the authoritative safety
1746
+ * net.
1747
+ *
1748
+ * Idempotency: every POST carries an `Idempotency-Key` header
1749
+ * `<sessionId>:validator-enabled`. The backend route is no-op if the
1750
+ * mirror row's `enable_status` is already `'enabled'` (because the
1751
+ * chain indexer raced ahead).
1752
+ *
1753
+ * Returns a Promise resolved when the loop terminates (success or
1754
+ * max-elapsed). Callers don't need to await; tests use it for
1755
+ * deterministic assertions.
1756
+ */
1757
+ enqueueValidatorEnabledCallback(args) {
1758
+ if (!this.isCallbackConfigured()) {
1759
+ return Promise.resolve({
1760
+ ok: false,
1761
+ attempts: 0,
1762
+ lastError: "callback_unconfigured"
1763
+ });
1764
+ }
1765
+ const dedupKey = `${args.sessionId}:${args.txHash.toLowerCase()}:${args.accountAddress.toLowerCase()}`;
1766
+ const existing = this.inflightCallbacks.get(dedupKey);
1767
+ if (existing) {
1768
+ this.log("info", "validator-enabled callback already in flight \u2014 folded", {
1769
+ sessionId: args.sessionId
1770
+ });
1771
+ return existing;
1772
+ }
1773
+ const promise = this.runCallbackLoop(args).finally(() => {
1774
+ this.inflightCallbacks.delete(dedupKey);
1775
+ });
1776
+ this.inflightCallbacks.set(dedupKey, promise);
1777
+ return promise;
1778
+ }
1779
+ async runCallbackLoop(args) {
1780
+ const url = `${this.config.backendBaseUrl.replace(/\/+$/, "")}/api/v1/agent/policy/scoped-session/${encodeURIComponent(args.sessionId)}/validator-enabled`;
1781
+ const body = JSON.stringify({
1782
+ userId: args.userId,
1783
+ accountAddress: args.accountAddress,
1784
+ permissionId: args.permissionId,
1785
+ txHash: args.txHash,
1786
+ blockNumber: args.blockNumber,
1787
+ logIndex: args.logIndex
1788
+ });
1789
+ const startedAt = Date.now();
1790
+ let attempts = 0;
1791
+ let lastError;
1792
+ for (let i = 0; i <= CALLBACK_RETRY_SCHEDULE_MS.length; i++) {
1793
+ if (i > 0) {
1794
+ const delay = CALLBACK_RETRY_SCHEDULE_MS[i - 1] ?? 0;
1795
+ const elapsed = Date.now() - startedAt;
1796
+ if (elapsed + delay > CALLBACK_MAX_ELAPSED_MS) {
1797
+ lastError = `retry budget exhausted after ${attempts} attempts (last error: ${lastError ?? "unknown"})`;
1798
+ this.log("error", "validator-enabled callback abandoned", {
1799
+ sessionId: args.sessionId,
1800
+ attempts,
1801
+ lastError
1802
+ });
1803
+ return { ok: false, attempts, lastError };
1804
+ }
1805
+ await this.sleep(delay);
1806
+ }
1807
+ attempts++;
1808
+ try {
1809
+ const ok = await this.postCallback(url, body, args.sessionId);
1810
+ if (ok) {
1811
+ this.log("info", "validator-enabled callback succeeded", {
1812
+ sessionId: args.sessionId,
1813
+ attempts
1814
+ });
1815
+ return { ok: true, attempts };
1816
+ }
1817
+ lastError = "non-2xx response";
1818
+ } catch (err) {
1819
+ lastError = err instanceof Error ? err.message : String(err);
1820
+ this.log("warn", "validator-enabled callback attempt failed", {
1821
+ sessionId: args.sessionId,
1822
+ attempt: attempts,
1823
+ err: lastError
1824
+ });
1825
+ }
1826
+ }
1827
+ this.log("error", "validator-enabled callback retry budget exhausted", {
1828
+ sessionId: args.sessionId,
1829
+ attempts,
1830
+ lastError
1831
+ });
1832
+ return { ok: false, attempts, lastError };
1833
+ }
1834
+ async postCallback(url, body, sessionId) {
1835
+ const ac = new AbortController();
1836
+ const timer = this.setTimeoutImpl(() => ac.abort(), this.fetchTimeoutMs);
1837
+ let res;
1838
+ try {
1839
+ res = await this.fetchImpl(url, {
1840
+ method: "POST",
1841
+ headers: {
1842
+ "Content-Type": "application/json",
1843
+ Accept: "application/json",
1844
+ Authorization: `Bearer ${this.config.callbackServiceSecret}`,
1845
+ "Idempotency-Key": `${sessionId}:validator-enabled`,
1846
+ Origin: this.config.outboundOriginHeader
1847
+ },
1848
+ body,
1849
+ signal: ac.signal
1850
+ });
1851
+ } finally {
1852
+ this.clearTimeoutImpl(timer);
1853
+ }
1854
+ if (res.status === 409) {
1855
+ this.log("info", "callback returned 409 (row already enabled \u2014 idempotent)", {
1856
+ sessionId
1857
+ });
1858
+ return true;
1859
+ }
1860
+ if (res.ok) return true;
1861
+ throw new CallbackError(
1862
+ `backend callback returned HTTP ${res.status}`
1863
+ );
1864
+ }
1865
+ sleep(ms) {
1866
+ return new Promise((resolve) => {
1867
+ this.setTimeoutImpl(() => resolve(), ms);
1868
+ });
1869
+ }
1870
+ };
1439
1871
 
1440
1872
  // src/broker/daemon.ts
1441
1873
  var noopLogger = (_e) => {
1442
1874
  };
1443
- async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.floor(Date.now() / 1e3), options = {}, policyStore) {
1875
+ async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.floor(Date.now() / 1e3), options = {}, policyStore, outbound) {
1444
1876
  switch (req.type) {
1445
1877
  case "hello": {
1446
1878
  let hasJwt = false;
@@ -1648,6 +2080,57 @@ async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.fl
1648
2080
  );
1649
2081
  }
1650
2082
  }
2083
+ case "current_nonce": {
2084
+ if (!outbound) {
2085
+ return errorResponse(
2086
+ "chain_rpc_failed",
2087
+ "broker daemon was not configured with a chain RPC URL"
2088
+ );
2089
+ }
2090
+ try {
2091
+ const nonce = await outbound.currentNonce(req.accountAddress);
2092
+ return {
2093
+ type: "current_nonce",
2094
+ nonce,
2095
+ accountAddress: req.accountAddress
2096
+ };
2097
+ } catch (err) {
2098
+ if (err instanceof ChainRpcError) {
2099
+ return errorResponse("chain_rpc_failed", err.message);
2100
+ }
2101
+ return errorResponse(
2102
+ "chain_rpc_failed",
2103
+ err instanceof Error ? err.message : "chain RPC eth_call failed"
2104
+ );
2105
+ }
2106
+ }
2107
+ case "notify_userop_landed": {
2108
+ if (!outbound) {
2109
+ return errorResponse(
2110
+ "callback_unconfigured",
2111
+ "broker daemon was not configured with the callback service secret"
2112
+ );
2113
+ }
2114
+ if (!outbound.isCallbackConfigured()) {
2115
+ return errorResponse(
2116
+ "callback_unconfigured",
2117
+ "BROKER_CALLBACK_SERVICE_SECRET or backend URL is unset \u2014 validator-enabled callback skipped (chain indexer is the safety net)"
2118
+ );
2119
+ }
2120
+ void outbound.enqueueValidatorEnabledCallback({
2121
+ sessionId: req.sessionId,
2122
+ accountAddress: req.accountAddress,
2123
+ permissionId: req.permissionId,
2124
+ txHash: req.txHash,
2125
+ blockNumber: req.blockNumber,
2126
+ logIndex: req.logIndex
2127
+ });
2128
+ return {
2129
+ type: "notify_userop_landed",
2130
+ queued: true,
2131
+ sessionId: req.sessionId
2132
+ };
2133
+ }
1651
2134
  }
1652
2135
  }
1653
2136
  function errorResponse(code, message) {
@@ -1678,6 +2161,7 @@ var BrokerDaemon = class {
1678
2161
  config;
1679
2162
  keystore;
1680
2163
  policyStore;
2164
+ outbound;
1681
2165
  /**
1682
2166
  * Whether a session-key private half is actually loaded. `false` =
1683
2167
  * daemon booted in read-only posture (no `MUHAVEN_BROKER_SESSION_KEY`
@@ -1698,6 +2182,15 @@ var BrokerDaemon = class {
1698
2182
  }
1699
2183
  this.keystore = options.keystore ?? null;
1700
2184
  this.policyStore = options.policyStore ?? new FilePolicyStore(FilePolicyStore.defaultDir());
2185
+ this.outbound = options.outbound ?? new BrokerOutbound(
2186
+ {
2187
+ chainRpcUrl: options.config.chainRpcUrl,
2188
+ backendBaseUrl: options.config.backendBaseUrl,
2189
+ callbackServiceSecret: options.config.callbackServiceSecret,
2190
+ outboundOriginHeader: options.config.outboundOriginHeader ?? options.config.dashboardBaseUrl
2191
+ },
2192
+ (level, msg, meta) => (options.logger ?? noopLogger)({ level, msg, meta })
2193
+ );
1701
2194
  this.log = options.logger ?? noopLogger;
1702
2195
  this.server = net.createServer((socket) => this.onConnection(socket));
1703
2196
  }
@@ -1830,7 +2323,8 @@ var BrokerDaemon = class {
1830
2323
  },
1831
2324
  pid: process.pid
1832
2325
  },
1833
- this.policyStore
2326
+ this.policyStore,
2327
+ this.outbound
1834
2328
  );
1835
2329
  socket.end(serializeResponse(res));
1836
2330
  } catch (err) {
@@ -1873,6 +2367,63 @@ async function runBrokerDaemonCli() {
1873
2367
  await new Promise(() => {
1874
2368
  });
1875
2369
  }
2370
+
2371
+ // src/broker/session-input.ts
2372
+ var SESSION_KEY_HEX_RE = /^0x[0-9a-fA-F]{64}$/;
2373
+ function validateSessionKeyShape(key) {
2374
+ if (key.length === 0) return "session key is empty";
2375
+ if (!key.startsWith("0x")) return "session key must be 0x-prefixed";
2376
+ if (!SESSION_KEY_HEX_RE.test(key)) {
2377
+ return `session key must be a 0x-prefixed 32-byte hex string (got ${key.length} chars; expected 66)`;
2378
+ }
2379
+ return null;
2380
+ }
2381
+ async function resolveSessionKey(opts) {
2382
+ const { sessionFlag, policy, deps } = opts;
2383
+ if (sessionFlag !== void 0) {
2384
+ let raw;
2385
+ if (sessionFlag === "-") {
2386
+ const stdin = await deps.readStdinAll();
2387
+ raw = stdin.trim();
2388
+ if (raw.length === 0) {
2389
+ return { kind: "error", message: "--session - was given but stdin was empty" };
2390
+ }
2391
+ } else {
2392
+ raw = sessionFlag.trim();
2393
+ }
2394
+ const shapeErr2 = validateSessionKeyShape(raw);
2395
+ if (shapeErr2) return { kind: "error", message: shapeErr2 };
2396
+ return { kind: "key", key: raw };
2397
+ }
2398
+ if (!deps.isTty) {
2399
+ if (policy === "mint-fallback") return { kind: "mint" };
2400
+ return {
2401
+ kind: "error",
2402
+ message: "no session key provided and stdin is not a TTY \u2014 pass --session <key> (or `--session -` to pipe it), or run `muhaven-broker setup` to mint a fresh key"
2403
+ };
2404
+ }
2405
+ const hasKey = await deps.promptYesNo(
2406
+ "Do you have a session key from the dashboard? [Y/n] "
2407
+ );
2408
+ if (!hasKey) {
2409
+ if (policy === "mint-fallback") return { kind: "mint" };
2410
+ return {
2411
+ kind: "error",
2412
+ message: "a session key is required for this command \u2014 paste the dashboard-minted key, or run `muhaven-broker setup` to mint one"
2413
+ };
2414
+ }
2415
+ const pasted = (await deps.promptSecret("Paste the session key: ")).trim();
2416
+ const shapeErr = validateSessionKeyShape(pasted);
2417
+ if (shapeErr) {
2418
+ return {
2419
+ kind: "error",
2420
+ message: `${shapeErr} \u2014 re-run and paste the key from the dashboard's session-reveal modal`
2421
+ };
2422
+ }
2423
+ return { kind: "key", key: pasted };
2424
+ }
2425
+
2426
+ // src/broker/setup.ts
1876
2427
  var DANGEROUS_NODE_ENV_VARS = [
1877
2428
  "NODE_OPTIONS",
1878
2429
  "NODE_TLS_REJECT_UNAUTHORIZED",
@@ -1960,6 +2511,26 @@ function validateBrokerEndpointFlag(value, platformId) {
1960
2511
  }
1961
2512
  return null;
1962
2513
  }
2514
+ var LOGIN_SEED_ENV_KEYS = [
2515
+ "MUHAVEN_BACKEND_URL",
2516
+ "MUHAVEN_DASHBOARD_URL",
2517
+ "MUHAVEN_BROKER_ENDPOINT"
2518
+ ];
2519
+ async function withSeededLoginEnv(effectiveEnv, fn) {
2520
+ const originalValues = {};
2521
+ for (const k of LOGIN_SEED_ENV_KEYS) {
2522
+ originalValues[k] = process.env[k];
2523
+ if (effectiveEnv[k]) process.env[k] = effectiveEnv[k];
2524
+ }
2525
+ try {
2526
+ return await fn();
2527
+ } finally {
2528
+ for (const k of LOGIN_SEED_ENV_KEYS) {
2529
+ if (originalValues[k] === void 0) delete process.env[k];
2530
+ else process.env[k] = originalValues[k];
2531
+ }
2532
+ }
2533
+ }
1963
2534
  var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1964
2535
  async function waitForBroker(options) {
1965
2536
  const timeoutMs = options.timeoutMs ?? 8e3;
@@ -2209,12 +2780,32 @@ async function runSetup(argv, deps) {
2209
2780
  }
2210
2781
  let sessionKey = effectiveEnv.MUHAVEN_BROKER_SESSION_KEY;
2211
2782
  let mintedKey = false;
2212
- if (!sessionKey || sessionKey === "") {
2213
- sessionKey = deps.mintSessionKey();
2214
- mintedKey = true;
2215
- deps.print("Session key: minted fresh (secp256k1, ephemeral to this daemon).");
2216
- } else {
2783
+ if (sessionKey && sessionKey.length > 0) {
2217
2784
  deps.print("Session key: using MUHAVEN_BROKER_SESSION_KEY from env.");
2785
+ } else {
2786
+ const sessionInput = deps.sessionInput ?? {
2787
+ isTty: false,
2788
+ readStdinAll: async () => "",
2789
+ promptYesNo: async () => false,
2790
+ promptSecret: async () => ""
2791
+ };
2792
+ const resolution = await resolveSessionKey({
2793
+ sessionFlag: void 0,
2794
+ policy: "mint-fallback",
2795
+ deps: sessionInput
2796
+ });
2797
+ if (resolution.kind === "error") {
2798
+ deps.printErr(`error: ${resolution.message}`);
2799
+ return 2;
2800
+ }
2801
+ if (resolution.kind === "key") {
2802
+ sessionKey = resolution.key;
2803
+ deps.print("Session key: using the pasted dashboard key.");
2804
+ } else {
2805
+ sessionKey = deps.mintSessionKey();
2806
+ mintedKey = true;
2807
+ deps.print("Session key: minted fresh (secp256k1, ephemeral to this daemon).");
2808
+ }
2218
2809
  }
2219
2810
  effectiveEnv.MUHAVEN_BROKER_SESSION_KEY = sessionKey;
2220
2811
  if (flags.foreground) {
@@ -2298,21 +2889,7 @@ async function runSetup(argv, deps) {
2298
2889
  if (flags.dashboardBaseUrl) {
2299
2890
  loginArgv.push("--dashboard-base-url", flags.dashboardBaseUrl);
2300
2891
  }
2301
- const restorationKeys = ["MUHAVEN_BACKEND_URL", "MUHAVEN_DASHBOARD_URL", "MUHAVEN_BROKER_ENDPOINT"];
2302
- const originalValues = {};
2303
- for (const k of restorationKeys) {
2304
- originalValues[k] = process.env[k];
2305
- if (effectiveEnv[k]) process.env[k] = effectiveEnv[k];
2306
- }
2307
- let code;
2308
- try {
2309
- code = await deps.runLogin(loginArgv);
2310
- } finally {
2311
- for (const k of restorationKeys) {
2312
- if (originalValues[k] === void 0) delete process.env[k];
2313
- else process.env[k] = originalValues[k];
2314
- }
2315
- }
2892
+ const code = await withSeededLoginEnv(effectiveEnv, () => deps.runLogin(loginArgv));
2316
2893
  if (code !== 0) {
2317
2894
  deps.printErr(
2318
2895
  "Setup: login step failed \u2014 daemon is still running, re-run `muhaven-broker login` to retry."
@@ -2396,13 +2973,15 @@ async function runStop(deps) {
2396
2973
  deps.print("Broker daemon: not running, nothing to stop.");
2397
2974
  return 0;
2398
2975
  }
2399
- try {
2400
- await broker.clearJwt();
2401
- deps.print("JWT cleared from keystore.");
2402
- } catch (err) {
2403
- deps.print(
2404
- `Warning: clearJwt failed (${err instanceof Error ? err.message : String(err)}); continuing with daemon shutdown.`
2405
- );
2976
+ if (deps.clearJwtOnStop ?? true) {
2977
+ try {
2978
+ await broker.clearJwt();
2979
+ deps.print("JWT cleared from keystore.");
2980
+ } catch (err) {
2981
+ deps.print(
2982
+ `Warning: clearJwt failed (${err instanceof Error ? err.message : String(err)}); continuing with daemon shutdown.`
2983
+ );
2984
+ }
2406
2985
  }
2407
2986
  const pid = hello.pid;
2408
2987
  if (pid === void 0) {
@@ -2460,6 +3039,190 @@ function defaultKillProcess(pid, signal) {
2460
3039
  }
2461
3040
  }
2462
3041
 
3042
+ // src/broker/bring-up.ts
3043
+ function parseBringUpFlags(argv) {
3044
+ let session;
3045
+ let noLaunchBrowser = false;
3046
+ let skipLogin = false;
3047
+ let brokerEndpoint;
3048
+ let backendBaseUrl;
3049
+ let dashboardBaseUrl;
3050
+ for (let i = 0; i < argv.length; i++) {
3051
+ const a = argv[i];
3052
+ if (a === "--no-launch-browser") noLaunchBrowser = true;
3053
+ else if (a === "--skip-login") skipLogin = true;
3054
+ else if (a === "--session") {
3055
+ const next = argv[i + 1];
3056
+ if (next === void 0) {
3057
+ throw new Error("--session requires a value (a 0x\u2026 key, or `-` to read from stdin)");
3058
+ }
3059
+ if (next !== "-" && next.startsWith("-")) {
3060
+ throw new Error(`--session requires a key value (or \`-\` for stdin), got flag: ${next}`);
3061
+ }
3062
+ session = argv[++i];
3063
+ } else if (a === "--broker-endpoint" && i + 1 < argv.length) brokerEndpoint = argv[++i];
3064
+ else if (a === "--backend-base-url" && i + 1 < argv.length) backendBaseUrl = argv[++i];
3065
+ else if (a === "--dashboard-base-url" && i + 1 < argv.length) dashboardBaseUrl = argv[++i];
3066
+ else throw new Error(`unknown flag: ${a}`);
3067
+ }
3068
+ return { session, noLaunchBrowser, skipLogin, brokerEndpoint, backendBaseUrl, dashboardBaseUrl };
3069
+ }
3070
+ function usageLine(mode) {
3071
+ return `usage: muhaven-broker ${mode} --session <key|-> [--no-launch-browser] [--skip-login]
3072
+ [--broker-endpoint PATH] [--backend-base-url URL]
3073
+ [--dashboard-base-url URL]
3074
+ (omit --session to be asked interactively; pipe the key with \`--session -\`)`;
3075
+ }
3076
+ async function runBringUp(mode, argv, deps) {
3077
+ let flags;
3078
+ try {
3079
+ flags = parseBringUpFlags(argv);
3080
+ } catch (err) {
3081
+ deps.printErr(`error: ${err.message}`);
3082
+ deps.printErr(usageLine(mode));
3083
+ return 2;
3084
+ }
3085
+ if (flags.backendBaseUrl) {
3086
+ const e = validateHttpUrlFlag("--backend-base-url", flags.backendBaseUrl);
3087
+ if (e) {
3088
+ deps.printErr(`error: ${e}`);
3089
+ return 2;
3090
+ }
3091
+ }
3092
+ if (flags.dashboardBaseUrl) {
3093
+ const e = validateHttpUrlFlag("--dashboard-base-url", flags.dashboardBaseUrl);
3094
+ if (e) {
3095
+ deps.printErr(`error: ${e}`);
3096
+ return 2;
3097
+ }
3098
+ }
3099
+ if (flags.brokerEndpoint) {
3100
+ const e = validateBrokerEndpointFlag(flags.brokerEndpoint, deps.platformId);
3101
+ if (e) {
3102
+ deps.printErr(`error: ${e}`);
3103
+ return 2;
3104
+ }
3105
+ }
3106
+ const resolution = await resolveSessionKey({
3107
+ sessionFlag: flags.session,
3108
+ policy: "require",
3109
+ deps: deps.sessionPrompt
3110
+ });
3111
+ if (resolution.kind !== "key") {
3112
+ const message = resolution.kind === "error" ? resolution.message : "no session key resolved";
3113
+ deps.printErr(`error: ${message}`);
3114
+ return 2;
3115
+ }
3116
+ const sessionKey = resolution.key;
3117
+ const overrides = applyEnvDefaults({
3118
+ env: deps.env,
3119
+ platformId: deps.platformId,
3120
+ osRelease: deps.osRelease
3121
+ });
3122
+ const effectiveEnv = {};
3123
+ for (const [k, v] of Object.entries(deps.env)) {
3124
+ if (typeof v === "string") effectiveEnv[k] = v;
3125
+ }
3126
+ for (const [k, v] of Object.entries(overrides.toSet)) effectiveEnv[k] = v;
3127
+ if (flags.brokerEndpoint) effectiveEnv.MUHAVEN_BROKER_ENDPOINT = flags.brokerEndpoint;
3128
+ if (flags.backendBaseUrl) effectiveEnv.MUHAVEN_BACKEND_URL = flags.backendBaseUrl;
3129
+ if (flags.dashboardBaseUrl) effectiveEnv.MUHAVEN_DASHBOARD_URL = flags.dashboardBaseUrl;
3130
+ for (const name of overrides.preserved) deps.print(`Env preserved: ${name} (set in your shell)`);
3131
+ for (const [k, v] of Object.entries(overrides.toSet)) deps.print(`Env defaulted: ${k}=${v}`);
3132
+ const config = loadMcpConfig(effectiveEnv);
3133
+ const broker = deps.newBrokerClient(config.brokerEndpoint, config.brokerTimeoutMs);
3134
+ let running = false;
3135
+ try {
3136
+ await broker.hello();
3137
+ running = true;
3138
+ } catch {
3139
+ running = false;
3140
+ }
3141
+ if (mode === "start") {
3142
+ if (running) {
3143
+ deps.printErr(
3144
+ `Broker daemon is already running at ${config.brokerEndpoint}. To rotate its key use: muhaven-broker update --session <key>`
3145
+ );
3146
+ return 1;
3147
+ }
3148
+ deps.print("Broker daemon: not running \u2014 starting one (detached) on the provided key ...");
3149
+ } else {
3150
+ if (running) {
3151
+ deps.print("Broker daemon: running \u2014 stopping it before installing the new key ...");
3152
+ const stopCode = await deps.stopDaemon(config.brokerEndpoint, config.brokerTimeoutMs);
3153
+ if (stopCode !== 0) {
3154
+ deps.printErr(
3155
+ `Broker daemon stop returned ${stopCode}; refusing to start a second daemon on the same endpoint. Resolve the running daemon (muhaven-broker doctor) and retry.`
3156
+ );
3157
+ return stopCode;
3158
+ }
3159
+ } else {
3160
+ deps.print("Broker daemon: not running \u2014 `update` will start a fresh one on the provided key.");
3161
+ }
3162
+ }
3163
+ const daemonPid = deps.spawnDaemon({
3164
+ binPath: deps.resolveBinPath(),
3165
+ env: {
3166
+ ...overrides.toSet,
3167
+ MUHAVEN_BROKER_ENDPOINT: config.brokerEndpoint,
3168
+ MUHAVEN_BACKEND_URL: effectiveEnv.MUHAVEN_BACKEND_URL,
3169
+ MUHAVEN_DASHBOARD_URL: effectiveEnv.MUHAVEN_DASHBOARD_URL,
3170
+ MUHAVEN_BROKER_SESSION_KEY: sessionKey
3171
+ }
3172
+ });
3173
+ let ready;
3174
+ try {
3175
+ ready = await deps.waitForBroker({ broker });
3176
+ } catch (err) {
3177
+ deps.printErr(err.message);
3178
+ deps.printErr(
3179
+ " hint: check that no other broker is bound to the same endpoint (muhaven-broker doctor)."
3180
+ );
3181
+ return 1;
3182
+ }
3183
+ deps.print(`Broker daemon: ready (PID ${daemonPid}, endpoint ${config.brokerEndpoint}).`);
3184
+ try {
3185
+ const h = await broker.hello();
3186
+ const hasKey = h.hasSessionKey ?? true;
3187
+ if (!hasKey) {
3188
+ deps.printErr(
3189
+ "Broker came up in READ-ONLY posture \u2014 the session key did not reach the daemon, so it cannot sign. Stop it (muhaven-broker stop) and retry."
3190
+ );
3191
+ return 1;
3192
+ }
3193
+ if (h.sessionKeyAddress) deps.print(`Broker signer: ${h.sessionKeyAddress}`);
3194
+ } catch {
3195
+ }
3196
+ if (flags.skipLogin) {
3197
+ deps.print("Login: skipped per --skip-login.");
3198
+ } else if (ready.hasJwt) {
3199
+ deps.print("Login: skipped \u2014 JWT already in keystore (reused).");
3200
+ } else {
3201
+ const loginArgv = [];
3202
+ if (flags.noLaunchBrowser) loginArgv.push("--no-launch-browser");
3203
+ if (flags.brokerEndpoint) loginArgv.push("--broker-endpoint", flags.brokerEndpoint);
3204
+ if (flags.backendBaseUrl) loginArgv.push("--backend-base-url", flags.backendBaseUrl);
3205
+ if (flags.dashboardBaseUrl) loginArgv.push("--dashboard-base-url", flags.dashboardBaseUrl);
3206
+ const code = await withSeededLoginEnv(effectiveEnv, () => deps.runLogin(loginArgv));
3207
+ if (code !== 0) {
3208
+ deps.printErr(
3209
+ "Login step failed \u2014 the daemon is running on the new key; re-run `muhaven-broker login` to retry."
3210
+ );
3211
+ return code;
3212
+ }
3213
+ }
3214
+ deps.print("");
3215
+ deps.print("================================");
3216
+ deps.print(mode === "start" ? "Broker started." : "Session key rotated.");
3217
+ deps.print(` Daemon PID : ${daemonPid}`);
3218
+ const killCmd = deps.platformId === "win32" ? `Stop-Process -Id ${daemonPid}` : `kill ${daemonPid}`;
3219
+ deps.print(` Stop daemon: ${killCmd} (or: muhaven-broker stop)`);
3220
+ deps.print(` Endpoint : ${config.brokerEndpoint}`);
3221
+ deps.print(" Rotate key : muhaven-broker update --session <new-key>");
3222
+ deps.print("================================");
3223
+ return 0;
3224
+ }
3225
+
2463
3226
  // src/broker/cli.ts
2464
3227
  function print(line) {
2465
3228
  process.stdout.write(line + "\n");
@@ -2772,6 +3535,11 @@ function printUsage() {
2772
3535
  print(" (claude-code today; claude-desktop / cursor reserved for Wave 5)");
2773
3536
  print(" [--register-scope user|project|local] scope for the host-config write");
2774
3537
  print(" (default: user \u2014 every project sees the server)");
3538
+ print(" start Bring the daemon up on a DASHBOARD-minted session key (daemon NOT running)");
3539
+ print(" --session <key|-> the key (or `-` to read it from stdin); omit to be");
3540
+ print(" asked interactively. [--skip-login] [--no-launch-browser]");
3541
+ print(" update Rotate the session key on a running daemon (stop \u2192 swap \u2192 restart,");
3542
+ print(" reusing the existing JWT). --session <key|-> (or interactive).");
2775
3543
  print(" stop Cleanly stop a running daemon (SIGTERM with SIGKILL fallback");
2776
3544
  print(" after 5s). Also clears the keystore JWT as a best effort.");
2777
3545
  print(" login Acquire a JWT via the device-code flow + store in keystore");
@@ -2783,7 +3551,7 @@ function printUsage() {
2783
3551
  }
2784
3552
  function getBrokerPackageVersion() {
2785
3553
  {
2786
- return "0.2.9";
3554
+ return "0.4.0";
2787
3555
  }
2788
3556
  }
2789
3557
  function printVersion() {
@@ -2819,6 +3587,66 @@ function defaultShellOut(cmd, argv) {
2819
3587
  });
2820
3588
  });
2821
3589
  }
3590
+ function stdinIsTty() {
3591
+ return process.stdin.isTTY === true;
3592
+ }
3593
+ function promptYesNo(question) {
3594
+ return new Promise((resolve) => {
3595
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
3596
+ rl.question(question, (answer) => {
3597
+ rl.close();
3598
+ const a = answer.trim().toLowerCase();
3599
+ resolve(a === "" || a === "y" || a === "yes");
3600
+ });
3601
+ });
3602
+ }
3603
+ function promptSecret(question) {
3604
+ return new Promise((resolve) => {
3605
+ process.stdout.write(`${question.trimEnd()} (input hidden)
3606
+ `);
3607
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
3608
+ const rlAny = rl;
3609
+ rlAny._writeToOutput = () => {
3610
+ };
3611
+ rl.question("", (answer) => {
3612
+ rl.close();
3613
+ process.stdout.write("\n");
3614
+ resolve(answer);
3615
+ });
3616
+ });
3617
+ }
3618
+ async function readStdinAll() {
3619
+ if (process.stdin.isTTY) return "";
3620
+ const chunks = [];
3621
+ for await (const chunk of process.stdin) {
3622
+ chunks.push(Buffer.from(chunk));
3623
+ }
3624
+ return Buffer.concat(chunks).toString("utf8");
3625
+ }
3626
+ function makeSessionPromptDeps() {
3627
+ return {
3628
+ isTty: stdinIsTty(),
3629
+ readStdinAll,
3630
+ promptYesNo,
3631
+ promptSecret
3632
+ };
3633
+ }
3634
+ function makeStopDeps(clearJwtOnStop, override) {
3635
+ const resolved = override ?? (() => {
3636
+ const config = loadMcpConfig();
3637
+ return { endpoint: config.brokerEndpoint, brokerTimeoutMs: config.brokerTimeoutMs };
3638
+ })();
3639
+ return {
3640
+ print,
3641
+ printErr,
3642
+ newBrokerClient: (endpoint, timeoutMs) => new BrokerClient({ endpoint, timeoutMs }),
3643
+ killProcess: defaultKillProcess,
3644
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
3645
+ endpoint: resolved.endpoint,
3646
+ brokerTimeoutMs: resolved.brokerTimeoutMs,
3647
+ clearJwtOnStop
3648
+ };
3649
+ }
2822
3650
  async function runSetup2(argv) {
2823
3651
  const deps = {
2824
3652
  print,
@@ -2833,22 +3661,35 @@ async function runSetup2(argv) {
2833
3661
  env: process.env,
2834
3662
  platformId: process.platform,
2835
3663
  osRelease: os.release(),
2836
- shellOut: defaultShellOut
3664
+ shellOut: defaultShellOut,
3665
+ sessionInput: makeSessionPromptDeps()
2837
3666
  };
2838
3667
  return runSetup(argv, deps);
2839
3668
  }
2840
3669
  async function runStop2() {
2841
- const config = loadMcpConfig();
2842
- const deps = {
3670
+ return runStop(makeStopDeps(true));
3671
+ }
3672
+ function makeBringUpDeps() {
3673
+ return {
2843
3674
  print,
2844
3675
  printErr,
2845
3676
  newBrokerClient: (endpoint, timeoutMs) => new BrokerClient({ endpoint, timeoutMs }),
2846
- killProcess: defaultKillProcess,
2847
- sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
2848
- endpoint: config.brokerEndpoint,
2849
- brokerTimeoutMs: config.brokerTimeoutMs
3677
+ spawnDaemon,
3678
+ waitForBroker,
3679
+ // `update` stops the old daemon but PRESERVES the JWT (key rotation
3680
+ // must not force a device-code re-login). Targets the resolved
3681
+ // endpoint so a `--broker-endpoint` override stops the right daemon.
3682
+ stopDaemon: (endpoint, brokerTimeoutMs) => runStop(makeStopDeps(false, { endpoint, brokerTimeoutMs })),
3683
+ runLogin,
3684
+ resolveBinPath: resolveBrokerBinPath,
3685
+ env: process.env,
3686
+ platformId: process.platform,
3687
+ osRelease: os.release(),
3688
+ sessionPrompt: makeSessionPromptDeps()
2850
3689
  };
2851
- return runStop(deps);
3690
+ }
3691
+ async function runStartOrUpdate(mode, argv) {
3692
+ return runBringUp(mode, argv, makeBringUpDeps());
2852
3693
  }
2853
3694
  async function runCli(argv) {
2854
3695
  const [sub, ...rest] = argv;
@@ -2858,6 +3699,10 @@ async function runCli(argv) {
2858
3699
  return 0;
2859
3700
  case "setup":
2860
3701
  return runSetup2(rest);
3702
+ case "start":
3703
+ return runStartOrUpdate("start", rest);
3704
+ case "update":
3705
+ return runStartOrUpdate("update", rest);
2861
3706
  case "stop":
2862
3707
  return runStop2();
2863
3708
  case "login":
@@ -2888,4 +3733,5 @@ exports.runDoctor = runDoctor;
2888
3733
  exports.runLogin = runLogin;
2889
3734
  exports.runLogout = runLogout;
2890
3735
  exports.runSetup = runSetup2;
3736
+ exports.runStartOrUpdate = runStartOrUpdate;
2891
3737
  exports.runStop = runStop2;