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