@muhaven/mcp 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -0
- package/dist/broker.cjs +433 -40
- package/dist/broker.d.cts +31 -1
- package/dist/broker.d.ts +31 -1
- package/dist/broker.js +433 -41
- package/dist/index.cjs +4 -3
- package/dist/index.js +4 -3
- package/manifest.json +1 -1
- package/package.json +1 -1
package/dist/broker.d.ts
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `muhaven-broker start` / `muhaven-broker update` — install a
|
|
3
|
+
* DASHBOARD-minted Scoped session key onto the broker daemon in one shot.
|
|
4
|
+
*
|
|
5
|
+
* Wave 5 Option D OPEN-D (2026-05-24). Replaces the manual last mile after
|
|
6
|
+
* a dashboard mint / revoke:
|
|
7
|
+
*
|
|
8
|
+
* start — bring the daemon UP on a provided key (daemon NOT running).
|
|
9
|
+
* update — ROTATE the key on a (possibly) running daemon: stop → swap →
|
|
10
|
+
* restart → REUSE the existing JWT (a key rotation must not
|
|
11
|
+
* force a device-code re-login).
|
|
12
|
+
*
|
|
13
|
+
* Both resolve the key via the shared precedence (`--session` flag >
|
|
14
|
+
* interactive masked prompt > error — see `session-input.ts`). Both
|
|
15
|
+
* REQUIRE a key (unlike `setup`, which self-mints on the fresh-install
|
|
16
|
+
* path). The key is injected ONLY into the spawned daemon's child env
|
|
17
|
+
* (**Option B** — operator decision 2026-05-24); it never touches disk,
|
|
18
|
+
* so the daemon (`loadBrokerConfig`) and the keystore stay unchanged.
|
|
19
|
+
*
|
|
20
|
+
* The orchestrator is pure-ish: all IO (broker IPC, daemon spawn, stop,
|
|
21
|
+
* login, prompts) is injected via `BringUpDeps` so the decision tree is
|
|
22
|
+
* unit-testable without spawning real processes — mirrors the
|
|
23
|
+
* `runSetup` / `runStop` style.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
type BringUpMode = 'start' | 'update';
|
|
27
|
+
|
|
1
28
|
/**
|
|
2
29
|
* `muhaven-broker` CLI subcommand router.
|
|
3
30
|
*
|
|
@@ -8,6 +35,7 @@
|
|
|
8
35
|
* doctor → environment + keystore capability report
|
|
9
36
|
* --help, -h → usage
|
|
10
37
|
*/
|
|
38
|
+
|
|
11
39
|
interface LoginFlags {
|
|
12
40
|
noLaunchBrowser: boolean;
|
|
13
41
|
brokerEndpoint?: string;
|
|
@@ -43,6 +71,8 @@ declare function runSetup(argv: readonly string[]): Promise<number>;
|
|
|
43
71
|
* Wire `runStop` against the real BrokerClient + Node's process.kill.
|
|
44
72
|
*/
|
|
45
73
|
declare function runStop(): Promise<number>;
|
|
74
|
+
/** Wire `muhaven-broker start` / `update` against the real cli helpers. */
|
|
75
|
+
declare function runStartOrUpdate(mode: BringUpMode, argv: readonly string[]): Promise<number>;
|
|
46
76
|
declare function runCli(argv: readonly string[]): Promise<number>;
|
|
47
77
|
|
|
48
|
-
export { getBrokerPackageVersion, parseLoginFlags, runCli, runDoctor, runLogin, runLogout, runSetup, runStop };
|
|
78
|
+
export { getBrokerPackageVersion, parseLoginFlags, runCli, runDoctor, runLogin, runLogout, runSetup, runStartOrUpdate, runStop };
|
package/dist/broker.js
CHANGED
|
@@ -2,6 +2,7 @@ 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';
|
|
@@ -19,6 +20,7 @@ var DEFAULT_BROKER_MAX_BYTES = 64 * 1024;
|
|
|
19
20
|
var DEFAULT_JWT_CACHE_TTL_SEC = 30;
|
|
20
21
|
var DEFAULT_BUNDLER_TIMEOUT_MS = 2e4;
|
|
21
22
|
var DEFAULT_CHAIN_ID = 421614;
|
|
23
|
+
var DEFAULT_BROKER_RPC_URL = "https://sepolia-rollup.arbitrum.io/rpc";
|
|
22
24
|
var DEFAULT_ENTRY_POINT_ADDRESS = "0x0000000071727De22E5E9d8BAf0edAc6f37da032";
|
|
23
25
|
var ADDRESS_HEX_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
24
26
|
function defaultBrokerEndpoint() {
|
|
@@ -164,8 +166,8 @@ function loadBrokerConfig(env = process.env) {
|
|
|
164
166
|
env.MUHAVEN_DASHBOARD_URL,
|
|
165
167
|
DEFAULT_DASHBOARD_URL
|
|
166
168
|
);
|
|
167
|
-
const chainRpcUrlRaw = readEnv("MUHAVEN_BROKER_RPC_URL", env) ?? readEnv("MUHAVEN_BUNDLER_URL", env);
|
|
168
|
-
const chainRpcUrl =
|
|
169
|
+
const chainRpcUrlRaw = readEnv("MUHAVEN_BROKER_RPC_URL", env) ?? readEnv("MUHAVEN_BUNDLER_URL", env) ?? DEFAULT_BROKER_RPC_URL;
|
|
170
|
+
const chainRpcUrl = resolvePublicUrlEnv(
|
|
169
171
|
"MUHAVEN_BROKER_RPC_URL",
|
|
170
172
|
chainRpcUrlRaw,
|
|
171
173
|
chainRpcUrlRaw
|
|
@@ -2368,6 +2370,63 @@ async function runBrokerDaemonCli() {
|
|
|
2368
2370
|
await new Promise(() => {
|
|
2369
2371
|
});
|
|
2370
2372
|
}
|
|
2373
|
+
|
|
2374
|
+
// src/broker/session-input.ts
|
|
2375
|
+
var SESSION_KEY_HEX_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
2376
|
+
function validateSessionKeyShape(key) {
|
|
2377
|
+
if (key.length === 0) return "session key is empty";
|
|
2378
|
+
if (!key.startsWith("0x")) return "session key must be 0x-prefixed";
|
|
2379
|
+
if (!SESSION_KEY_HEX_RE.test(key)) {
|
|
2380
|
+
return `session key must be a 0x-prefixed 32-byte hex string (got ${key.length} chars; expected 66)`;
|
|
2381
|
+
}
|
|
2382
|
+
return null;
|
|
2383
|
+
}
|
|
2384
|
+
async function resolveSessionKey(opts) {
|
|
2385
|
+
const { sessionFlag, policy, deps } = opts;
|
|
2386
|
+
if (sessionFlag !== void 0) {
|
|
2387
|
+
let raw;
|
|
2388
|
+
if (sessionFlag === "-") {
|
|
2389
|
+
const stdin = await deps.readStdinAll();
|
|
2390
|
+
raw = stdin.trim();
|
|
2391
|
+
if (raw.length === 0) {
|
|
2392
|
+
return { kind: "error", message: "--session - was given but stdin was empty" };
|
|
2393
|
+
}
|
|
2394
|
+
} else {
|
|
2395
|
+
raw = sessionFlag.trim();
|
|
2396
|
+
}
|
|
2397
|
+
const shapeErr2 = validateSessionKeyShape(raw);
|
|
2398
|
+
if (shapeErr2) return { kind: "error", message: shapeErr2 };
|
|
2399
|
+
return { kind: "key", key: raw };
|
|
2400
|
+
}
|
|
2401
|
+
if (!deps.isTty) {
|
|
2402
|
+
if (policy === "mint-fallback") return { kind: "mint" };
|
|
2403
|
+
return {
|
|
2404
|
+
kind: "error",
|
|
2405
|
+
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"
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
const hasKey = await deps.promptYesNo(
|
|
2409
|
+
"Do you have a session key from the dashboard? [Y/n] "
|
|
2410
|
+
);
|
|
2411
|
+
if (!hasKey) {
|
|
2412
|
+
if (policy === "mint-fallback") return { kind: "mint" };
|
|
2413
|
+
return {
|
|
2414
|
+
kind: "error",
|
|
2415
|
+
message: "a session key is required for this command \u2014 paste the dashboard-minted key, or run `muhaven-broker setup` to mint one"
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
const pasted = (await deps.promptSecret("Paste the session key: ")).trim();
|
|
2419
|
+
const shapeErr = validateSessionKeyShape(pasted);
|
|
2420
|
+
if (shapeErr) {
|
|
2421
|
+
return {
|
|
2422
|
+
kind: "error",
|
|
2423
|
+
message: `${shapeErr} \u2014 re-run and paste the key from the dashboard's session-reveal modal`
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
return { kind: "key", key: pasted };
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
// src/broker/setup.ts
|
|
2371
2430
|
var DANGEROUS_NODE_ENV_VARS = [
|
|
2372
2431
|
"NODE_OPTIONS",
|
|
2373
2432
|
"NODE_TLS_REJECT_UNAUTHORIZED",
|
|
@@ -2455,6 +2514,26 @@ function validateBrokerEndpointFlag(value, platformId) {
|
|
|
2455
2514
|
}
|
|
2456
2515
|
return null;
|
|
2457
2516
|
}
|
|
2517
|
+
var LOGIN_SEED_ENV_KEYS = [
|
|
2518
|
+
"MUHAVEN_BACKEND_URL",
|
|
2519
|
+
"MUHAVEN_DASHBOARD_URL",
|
|
2520
|
+
"MUHAVEN_BROKER_ENDPOINT"
|
|
2521
|
+
];
|
|
2522
|
+
async function withSeededLoginEnv(effectiveEnv, fn) {
|
|
2523
|
+
const originalValues = {};
|
|
2524
|
+
for (const k of LOGIN_SEED_ENV_KEYS) {
|
|
2525
|
+
originalValues[k] = process.env[k];
|
|
2526
|
+
if (effectiveEnv[k]) process.env[k] = effectiveEnv[k];
|
|
2527
|
+
}
|
|
2528
|
+
try {
|
|
2529
|
+
return await fn();
|
|
2530
|
+
} finally {
|
|
2531
|
+
for (const k of LOGIN_SEED_ENV_KEYS) {
|
|
2532
|
+
if (originalValues[k] === void 0) delete process.env[k];
|
|
2533
|
+
else process.env[k] = originalValues[k];
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2458
2537
|
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2459
2538
|
async function waitForBroker(options) {
|
|
2460
2539
|
const timeoutMs = options.timeoutMs ?? 8e3;
|
|
@@ -2492,6 +2571,7 @@ function parseSetupFlags(argv) {
|
|
|
2492
2571
|
let brokerEndpoint;
|
|
2493
2572
|
let backendBaseUrl;
|
|
2494
2573
|
let dashboardBaseUrl;
|
|
2574
|
+
let brokerRpcUrl;
|
|
2495
2575
|
let skipLogin = false;
|
|
2496
2576
|
const register = [];
|
|
2497
2577
|
let registerScope = "user";
|
|
@@ -2503,6 +2583,7 @@ function parseSetupFlags(argv) {
|
|
|
2503
2583
|
else if (a === "--broker-endpoint" && i + 1 < argv.length) brokerEndpoint = argv[++i];
|
|
2504
2584
|
else if (a === "--backend-base-url" && i + 1 < argv.length) backendBaseUrl = argv[++i];
|
|
2505
2585
|
else if (a === "--dashboard-base-url" && i + 1 < argv.length) dashboardBaseUrl = argv[++i];
|
|
2586
|
+
else if (a === "--broker-rpc-url" && i + 1 < argv.length) brokerRpcUrl = argv[++i];
|
|
2506
2587
|
else if (a === "--register" && i + 1 < argv.length) {
|
|
2507
2588
|
const value = argv[++i];
|
|
2508
2589
|
for (const raw of value.split(",")) {
|
|
@@ -2533,6 +2614,7 @@ function parseSetupFlags(argv) {
|
|
|
2533
2614
|
brokerEndpoint,
|
|
2534
2615
|
backendBaseUrl,
|
|
2535
2616
|
dashboardBaseUrl,
|
|
2617
|
+
brokerRpcUrl,
|
|
2536
2618
|
skipLogin,
|
|
2537
2619
|
register,
|
|
2538
2620
|
registerScope
|
|
@@ -2656,7 +2738,7 @@ async function runSetup(argv, deps) {
|
|
|
2656
2738
|
} catch (err) {
|
|
2657
2739
|
deps.printErr(`error: ${err.message}`);
|
|
2658
2740
|
deps.printErr(
|
|
2659
|
-
"usage: muhaven-broker setup [--foreground|-f] [--no-launch-browser] [--skip-login]\n [--broker-endpoint PATH] [--backend-base-url URL]\n [--dashboard-base-url URL]\n [--register HOST[,HOST...]] [--register-scope user|project|local]"
|
|
2741
|
+
"usage: muhaven-broker setup [--foreground|-f] [--no-launch-browser] [--skip-login]\n [--broker-endpoint PATH] [--backend-base-url URL]\n [--dashboard-base-url URL] [--broker-rpc-url URL]\n [--register HOST[,HOST...]] [--register-scope user|project|local]"
|
|
2660
2742
|
);
|
|
2661
2743
|
return 2;
|
|
2662
2744
|
}
|
|
@@ -2681,6 +2763,13 @@ async function runSetup(argv, deps) {
|
|
|
2681
2763
|
return 2;
|
|
2682
2764
|
}
|
|
2683
2765
|
}
|
|
2766
|
+
if (flags.brokerRpcUrl) {
|
|
2767
|
+
const err = validateHttpUrlFlag("--broker-rpc-url", flags.brokerRpcUrl);
|
|
2768
|
+
if (err) {
|
|
2769
|
+
deps.printErr(`error: ${err}`);
|
|
2770
|
+
return 2;
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2684
2773
|
const overrides = applyEnvDefaults({
|
|
2685
2774
|
env: deps.env,
|
|
2686
2775
|
platformId: deps.platformId,
|
|
@@ -2696,6 +2785,7 @@ async function runSetup(argv, deps) {
|
|
|
2696
2785
|
if (flags.brokerEndpoint) effectiveEnv.MUHAVEN_BROKER_ENDPOINT = flags.brokerEndpoint;
|
|
2697
2786
|
if (flags.backendBaseUrl) effectiveEnv.MUHAVEN_BACKEND_URL = flags.backendBaseUrl;
|
|
2698
2787
|
if (flags.dashboardBaseUrl) effectiveEnv.MUHAVEN_DASHBOARD_URL = flags.dashboardBaseUrl;
|
|
2788
|
+
if (flags.brokerRpcUrl) effectiveEnv.MUHAVEN_BROKER_RPC_URL = flags.brokerRpcUrl;
|
|
2699
2789
|
for (const name of overrides.preserved) {
|
|
2700
2790
|
deps.print(`Env preserved: ${name} (set in your shell)`);
|
|
2701
2791
|
}
|
|
@@ -2704,12 +2794,32 @@ async function runSetup(argv, deps) {
|
|
|
2704
2794
|
}
|
|
2705
2795
|
let sessionKey = effectiveEnv.MUHAVEN_BROKER_SESSION_KEY;
|
|
2706
2796
|
let mintedKey = false;
|
|
2707
|
-
if (
|
|
2708
|
-
sessionKey = deps.mintSessionKey();
|
|
2709
|
-
mintedKey = true;
|
|
2710
|
-
deps.print("Session key: minted fresh (secp256k1, ephemeral to this daemon).");
|
|
2711
|
-
} else {
|
|
2797
|
+
if (sessionKey && sessionKey.length > 0) {
|
|
2712
2798
|
deps.print("Session key: using MUHAVEN_BROKER_SESSION_KEY from env.");
|
|
2799
|
+
} else {
|
|
2800
|
+
const sessionInput = deps.sessionInput ?? {
|
|
2801
|
+
isTty: false,
|
|
2802
|
+
readStdinAll: async () => "",
|
|
2803
|
+
promptYesNo: async () => false,
|
|
2804
|
+
promptSecret: async () => ""
|
|
2805
|
+
};
|
|
2806
|
+
const resolution = await resolveSessionKey({
|
|
2807
|
+
sessionFlag: void 0,
|
|
2808
|
+
policy: "mint-fallback",
|
|
2809
|
+
deps: sessionInput
|
|
2810
|
+
});
|
|
2811
|
+
if (resolution.kind === "error") {
|
|
2812
|
+
deps.printErr(`error: ${resolution.message}`);
|
|
2813
|
+
return 2;
|
|
2814
|
+
}
|
|
2815
|
+
if (resolution.kind === "key") {
|
|
2816
|
+
sessionKey = resolution.key;
|
|
2817
|
+
deps.print("Session key: using the pasted dashboard key.");
|
|
2818
|
+
} else {
|
|
2819
|
+
sessionKey = deps.mintSessionKey();
|
|
2820
|
+
mintedKey = true;
|
|
2821
|
+
deps.print("Session key: minted fresh (secp256k1, ephemeral to this daemon).");
|
|
2822
|
+
}
|
|
2713
2823
|
}
|
|
2714
2824
|
effectiveEnv.MUHAVEN_BROKER_SESSION_KEY = sessionKey;
|
|
2715
2825
|
if (flags.foreground) {
|
|
@@ -2758,7 +2868,11 @@ async function runSetup(argv, deps) {
|
|
|
2758
2868
|
MUHAVEN_BROKER_ENDPOINT: config.brokerEndpoint,
|
|
2759
2869
|
MUHAVEN_BACKEND_URL: effectiveEnv.MUHAVEN_BACKEND_URL,
|
|
2760
2870
|
MUHAVEN_DASHBOARD_URL: effectiveEnv.MUHAVEN_DASHBOARD_URL,
|
|
2761
|
-
MUHAVEN_BROKER_SESSION_KEY: sessionKey
|
|
2871
|
+
MUHAVEN_BROKER_SESSION_KEY: sessionKey,
|
|
2872
|
+
// Forward the chain RPC URL only when resolved (flag or shell env);
|
|
2873
|
+
// absent → the daemon's loadBrokerConfig applies the public Arb
|
|
2874
|
+
// Sepolia default.
|
|
2875
|
+
...effectiveEnv.MUHAVEN_BROKER_RPC_URL ? { MUHAVEN_BROKER_RPC_URL: effectiveEnv.MUHAVEN_BROKER_RPC_URL } : {}
|
|
2762
2876
|
}
|
|
2763
2877
|
});
|
|
2764
2878
|
try {
|
|
@@ -2793,21 +2907,7 @@ async function runSetup(argv, deps) {
|
|
|
2793
2907
|
if (flags.dashboardBaseUrl) {
|
|
2794
2908
|
loginArgv.push("--dashboard-base-url", flags.dashboardBaseUrl);
|
|
2795
2909
|
}
|
|
2796
|
-
const
|
|
2797
|
-
const originalValues = {};
|
|
2798
|
-
for (const k of restorationKeys) {
|
|
2799
|
-
originalValues[k] = process.env[k];
|
|
2800
|
-
if (effectiveEnv[k]) process.env[k] = effectiveEnv[k];
|
|
2801
|
-
}
|
|
2802
|
-
let code;
|
|
2803
|
-
try {
|
|
2804
|
-
code = await deps.runLogin(loginArgv);
|
|
2805
|
-
} finally {
|
|
2806
|
-
for (const k of restorationKeys) {
|
|
2807
|
-
if (originalValues[k] === void 0) delete process.env[k];
|
|
2808
|
-
else process.env[k] = originalValues[k];
|
|
2809
|
-
}
|
|
2810
|
-
}
|
|
2910
|
+
const code = await withSeededLoginEnv(effectiveEnv, () => deps.runLogin(loginArgv));
|
|
2811
2911
|
if (code !== 0) {
|
|
2812
2912
|
deps.printErr(
|
|
2813
2913
|
"Setup: login step failed \u2014 daemon is still running, re-run `muhaven-broker login` to retry."
|
|
@@ -2891,13 +2991,15 @@ async function runStop(deps) {
|
|
|
2891
2991
|
deps.print("Broker daemon: not running, nothing to stop.");
|
|
2892
2992
|
return 0;
|
|
2893
2993
|
}
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2994
|
+
if (deps.clearJwtOnStop ?? true) {
|
|
2995
|
+
try {
|
|
2996
|
+
await broker.clearJwt();
|
|
2997
|
+
deps.print("JWT cleared from keystore.");
|
|
2998
|
+
} catch (err) {
|
|
2999
|
+
deps.print(
|
|
3000
|
+
`Warning: clearJwt failed (${err instanceof Error ? err.message : String(err)}); continuing with daemon shutdown.`
|
|
3001
|
+
);
|
|
3002
|
+
}
|
|
2901
3003
|
}
|
|
2902
3004
|
const pid = hello.pid;
|
|
2903
3005
|
if (pid === void 0) {
|
|
@@ -2955,6 +3057,212 @@ function defaultKillProcess(pid, signal) {
|
|
|
2955
3057
|
}
|
|
2956
3058
|
}
|
|
2957
3059
|
|
|
3060
|
+
// src/broker/bring-up.ts
|
|
3061
|
+
function parseBringUpFlags(argv) {
|
|
3062
|
+
let session;
|
|
3063
|
+
let noLaunchBrowser = false;
|
|
3064
|
+
let skipLogin = false;
|
|
3065
|
+
let brokerEndpoint;
|
|
3066
|
+
let backendBaseUrl;
|
|
3067
|
+
let dashboardBaseUrl;
|
|
3068
|
+
let brokerRpcUrl;
|
|
3069
|
+
for (let i = 0; i < argv.length; i++) {
|
|
3070
|
+
const a = argv[i];
|
|
3071
|
+
if (a === "--no-launch-browser") noLaunchBrowser = true;
|
|
3072
|
+
else if (a === "--skip-login") skipLogin = true;
|
|
3073
|
+
else if (a === "--session") {
|
|
3074
|
+
const next = argv[i + 1];
|
|
3075
|
+
if (next === void 0) {
|
|
3076
|
+
throw new Error("--session requires a value (a 0x\u2026 key, or `-` to read from stdin)");
|
|
3077
|
+
}
|
|
3078
|
+
if (next !== "-" && next.startsWith("-")) {
|
|
3079
|
+
throw new Error(`--session requires a key value (or \`-\` for stdin), got flag: ${next}`);
|
|
3080
|
+
}
|
|
3081
|
+
session = argv[++i];
|
|
3082
|
+
} else if (a === "--broker-endpoint" && i + 1 < argv.length) brokerEndpoint = argv[++i];
|
|
3083
|
+
else if (a === "--backend-base-url" && i + 1 < argv.length) backendBaseUrl = argv[++i];
|
|
3084
|
+
else if (a === "--dashboard-base-url" && i + 1 < argv.length) dashboardBaseUrl = argv[++i];
|
|
3085
|
+
else if (a === "--broker-rpc-url" && i + 1 < argv.length) brokerRpcUrl = argv[++i];
|
|
3086
|
+
else throw new Error(`unknown flag: ${a}`);
|
|
3087
|
+
}
|
|
3088
|
+
return {
|
|
3089
|
+
session,
|
|
3090
|
+
noLaunchBrowser,
|
|
3091
|
+
skipLogin,
|
|
3092
|
+
brokerEndpoint,
|
|
3093
|
+
backendBaseUrl,
|
|
3094
|
+
dashboardBaseUrl,
|
|
3095
|
+
brokerRpcUrl
|
|
3096
|
+
};
|
|
3097
|
+
}
|
|
3098
|
+
function usageLine(mode) {
|
|
3099
|
+
return `usage: muhaven-broker ${mode} --session <key|-> [--no-launch-browser] [--skip-login]
|
|
3100
|
+
[--broker-endpoint PATH] [--backend-base-url URL]
|
|
3101
|
+
[--dashboard-base-url URL] [--broker-rpc-url URL]
|
|
3102
|
+
(omit --session to be asked interactively; pipe the key with \`--session -\`)`;
|
|
3103
|
+
}
|
|
3104
|
+
async function runBringUp(mode, argv, deps) {
|
|
3105
|
+
let flags;
|
|
3106
|
+
try {
|
|
3107
|
+
flags = parseBringUpFlags(argv);
|
|
3108
|
+
} catch (err) {
|
|
3109
|
+
deps.printErr(`error: ${err.message}`);
|
|
3110
|
+
deps.printErr(usageLine(mode));
|
|
3111
|
+
return 2;
|
|
3112
|
+
}
|
|
3113
|
+
if (flags.backendBaseUrl) {
|
|
3114
|
+
const e = validateHttpUrlFlag("--backend-base-url", flags.backendBaseUrl);
|
|
3115
|
+
if (e) {
|
|
3116
|
+
deps.printErr(`error: ${e}`);
|
|
3117
|
+
return 2;
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
if (flags.dashboardBaseUrl) {
|
|
3121
|
+
const e = validateHttpUrlFlag("--dashboard-base-url", flags.dashboardBaseUrl);
|
|
3122
|
+
if (e) {
|
|
3123
|
+
deps.printErr(`error: ${e}`);
|
|
3124
|
+
return 2;
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
if (flags.brokerEndpoint) {
|
|
3128
|
+
const e = validateBrokerEndpointFlag(flags.brokerEndpoint, deps.platformId);
|
|
3129
|
+
if (e) {
|
|
3130
|
+
deps.printErr(`error: ${e}`);
|
|
3131
|
+
return 2;
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
if (flags.brokerRpcUrl) {
|
|
3135
|
+
const e = validateHttpUrlFlag("--broker-rpc-url", flags.brokerRpcUrl);
|
|
3136
|
+
if (e) {
|
|
3137
|
+
deps.printErr(`error: ${e}`);
|
|
3138
|
+
return 2;
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
const resolution = await resolveSessionKey({
|
|
3142
|
+
sessionFlag: flags.session,
|
|
3143
|
+
policy: "require",
|
|
3144
|
+
deps: deps.sessionPrompt
|
|
3145
|
+
});
|
|
3146
|
+
if (resolution.kind !== "key") {
|
|
3147
|
+
const message = resolution.kind === "error" ? resolution.message : "no session key resolved";
|
|
3148
|
+
deps.printErr(`error: ${message}`);
|
|
3149
|
+
return 2;
|
|
3150
|
+
}
|
|
3151
|
+
const sessionKey = resolution.key;
|
|
3152
|
+
const overrides = applyEnvDefaults({
|
|
3153
|
+
env: deps.env,
|
|
3154
|
+
platformId: deps.platformId,
|
|
3155
|
+
osRelease: deps.osRelease
|
|
3156
|
+
});
|
|
3157
|
+
const effectiveEnv = {};
|
|
3158
|
+
for (const [k, v] of Object.entries(deps.env)) {
|
|
3159
|
+
if (typeof v === "string") effectiveEnv[k] = v;
|
|
3160
|
+
}
|
|
3161
|
+
for (const [k, v] of Object.entries(overrides.toSet)) effectiveEnv[k] = v;
|
|
3162
|
+
if (flags.brokerEndpoint) effectiveEnv.MUHAVEN_BROKER_ENDPOINT = flags.brokerEndpoint;
|
|
3163
|
+
if (flags.backendBaseUrl) effectiveEnv.MUHAVEN_BACKEND_URL = flags.backendBaseUrl;
|
|
3164
|
+
if (flags.dashboardBaseUrl) effectiveEnv.MUHAVEN_DASHBOARD_URL = flags.dashboardBaseUrl;
|
|
3165
|
+
if (flags.brokerRpcUrl) effectiveEnv.MUHAVEN_BROKER_RPC_URL = flags.brokerRpcUrl;
|
|
3166
|
+
for (const name of overrides.preserved) deps.print(`Env preserved: ${name} (set in your shell)`);
|
|
3167
|
+
for (const [k, v] of Object.entries(overrides.toSet)) deps.print(`Env defaulted: ${k}=${v}`);
|
|
3168
|
+
const config = loadMcpConfig(effectiveEnv);
|
|
3169
|
+
const broker = deps.newBrokerClient(config.brokerEndpoint, config.brokerTimeoutMs);
|
|
3170
|
+
let running = false;
|
|
3171
|
+
try {
|
|
3172
|
+
await broker.hello();
|
|
3173
|
+
running = true;
|
|
3174
|
+
} catch {
|
|
3175
|
+
running = false;
|
|
3176
|
+
}
|
|
3177
|
+
if (mode === "start") {
|
|
3178
|
+
if (running) {
|
|
3179
|
+
deps.printErr(
|
|
3180
|
+
`Broker daemon is already running at ${config.brokerEndpoint}. To rotate its key use: muhaven-broker update --session <key>`
|
|
3181
|
+
);
|
|
3182
|
+
return 1;
|
|
3183
|
+
}
|
|
3184
|
+
deps.print("Broker daemon: not running \u2014 starting one (detached) on the provided key ...");
|
|
3185
|
+
} else {
|
|
3186
|
+
if (running) {
|
|
3187
|
+
deps.print("Broker daemon: running \u2014 stopping it before installing the new key ...");
|
|
3188
|
+
const stopCode = await deps.stopDaemon(config.brokerEndpoint, config.brokerTimeoutMs);
|
|
3189
|
+
if (stopCode !== 0) {
|
|
3190
|
+
deps.printErr(
|
|
3191
|
+
`Broker daemon stop returned ${stopCode}; refusing to start a second daemon on the same endpoint. Resolve the running daemon (muhaven-broker doctor) and retry.`
|
|
3192
|
+
);
|
|
3193
|
+
return stopCode;
|
|
3194
|
+
}
|
|
3195
|
+
} else {
|
|
3196
|
+
deps.print("Broker daemon: not running \u2014 `update` will start a fresh one on the provided key.");
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
const daemonPid = deps.spawnDaemon({
|
|
3200
|
+
binPath: deps.resolveBinPath(),
|
|
3201
|
+
env: {
|
|
3202
|
+
...overrides.toSet,
|
|
3203
|
+
MUHAVEN_BROKER_ENDPOINT: config.brokerEndpoint,
|
|
3204
|
+
MUHAVEN_BACKEND_URL: effectiveEnv.MUHAVEN_BACKEND_URL,
|
|
3205
|
+
MUHAVEN_DASHBOARD_URL: effectiveEnv.MUHAVEN_DASHBOARD_URL,
|
|
3206
|
+
MUHAVEN_BROKER_SESSION_KEY: sessionKey,
|
|
3207
|
+
// Forward the chain RPC URL only when resolved (flag or shell env).
|
|
3208
|
+
// When absent, the daemon's loadBrokerConfig applies the public
|
|
3209
|
+
// Arb Sepolia default — no need to inject it here.
|
|
3210
|
+
...effectiveEnv.MUHAVEN_BROKER_RPC_URL ? { MUHAVEN_BROKER_RPC_URL: effectiveEnv.MUHAVEN_BROKER_RPC_URL } : {}
|
|
3211
|
+
}
|
|
3212
|
+
});
|
|
3213
|
+
let ready;
|
|
3214
|
+
try {
|
|
3215
|
+
ready = await deps.waitForBroker({ broker });
|
|
3216
|
+
} catch (err) {
|
|
3217
|
+
deps.printErr(err.message);
|
|
3218
|
+
deps.printErr(
|
|
3219
|
+
" hint: check that no other broker is bound to the same endpoint (muhaven-broker doctor)."
|
|
3220
|
+
);
|
|
3221
|
+
return 1;
|
|
3222
|
+
}
|
|
3223
|
+
deps.print(`Broker daemon: ready (PID ${daemonPid}, endpoint ${config.brokerEndpoint}).`);
|
|
3224
|
+
try {
|
|
3225
|
+
const h = await broker.hello();
|
|
3226
|
+
const hasKey = h.hasSessionKey ?? true;
|
|
3227
|
+
if (!hasKey) {
|
|
3228
|
+
deps.printErr(
|
|
3229
|
+
"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."
|
|
3230
|
+
);
|
|
3231
|
+
return 1;
|
|
3232
|
+
}
|
|
3233
|
+
if (h.sessionKeyAddress) deps.print(`Broker signer: ${h.sessionKeyAddress}`);
|
|
3234
|
+
} catch {
|
|
3235
|
+
}
|
|
3236
|
+
if (flags.skipLogin) {
|
|
3237
|
+
deps.print("Login: skipped per --skip-login.");
|
|
3238
|
+
} else if (ready.hasJwt) {
|
|
3239
|
+
deps.print("Login: skipped \u2014 JWT already in keystore (reused).");
|
|
3240
|
+
} else {
|
|
3241
|
+
const loginArgv = [];
|
|
3242
|
+
if (flags.noLaunchBrowser) loginArgv.push("--no-launch-browser");
|
|
3243
|
+
if (flags.brokerEndpoint) loginArgv.push("--broker-endpoint", flags.brokerEndpoint);
|
|
3244
|
+
if (flags.backendBaseUrl) loginArgv.push("--backend-base-url", flags.backendBaseUrl);
|
|
3245
|
+
if (flags.dashboardBaseUrl) loginArgv.push("--dashboard-base-url", flags.dashboardBaseUrl);
|
|
3246
|
+
const code = await withSeededLoginEnv(effectiveEnv, () => deps.runLogin(loginArgv));
|
|
3247
|
+
if (code !== 0) {
|
|
3248
|
+
deps.printErr(
|
|
3249
|
+
"Login step failed \u2014 the daemon is running on the new key; re-run `muhaven-broker login` to retry."
|
|
3250
|
+
);
|
|
3251
|
+
return code;
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
deps.print("");
|
|
3255
|
+
deps.print("================================");
|
|
3256
|
+
deps.print(mode === "start" ? "Broker started." : "Session key rotated.");
|
|
3257
|
+
deps.print(` Daemon PID : ${daemonPid}`);
|
|
3258
|
+
const killCmd = deps.platformId === "win32" ? `Stop-Process -Id ${daemonPid}` : `kill ${daemonPid}`;
|
|
3259
|
+
deps.print(` Stop daemon: ${killCmd} (or: muhaven-broker stop)`);
|
|
3260
|
+
deps.print(` Endpoint : ${config.brokerEndpoint}`);
|
|
3261
|
+
deps.print(" Rotate key : muhaven-broker update --session <new-key>");
|
|
3262
|
+
deps.print("================================");
|
|
3263
|
+
return 0;
|
|
3264
|
+
}
|
|
3265
|
+
|
|
2958
3266
|
// src/broker/cli.ts
|
|
2959
3267
|
function print(line) {
|
|
2960
3268
|
process.stdout.write(line + "\n");
|
|
@@ -3267,6 +3575,13 @@ function printUsage() {
|
|
|
3267
3575
|
print(" (claude-code today; claude-desktop / cursor reserved for Wave 5)");
|
|
3268
3576
|
print(" [--register-scope user|project|local] scope for the host-config write");
|
|
3269
3577
|
print(" (default: user \u2014 every project sees the server)");
|
|
3578
|
+
print(" start Bring the daemon up on a DASHBOARD-minted session key (daemon NOT running)");
|
|
3579
|
+
print(" --session <key|-> the key (or `-` to read it from stdin); omit to be");
|
|
3580
|
+
print(" asked interactively. [--skip-login] [--no-launch-browser]");
|
|
3581
|
+
print(" [--broker-rpc-url URL] chain RPC for Path D (default: public Arb Sepolia)");
|
|
3582
|
+
print(" update Rotate the session key on a running daemon (stop \u2192 swap \u2192 restart,");
|
|
3583
|
+
print(" reusing the existing JWT). --session <key|-> (or interactive).");
|
|
3584
|
+
print(" [--broker-rpc-url URL] override the daemon chain RPC for Path D");
|
|
3270
3585
|
print(" stop Cleanly stop a running daemon (SIGTERM with SIGKILL fallback");
|
|
3271
3586
|
print(" after 5s). Also clears the keystore JWT as a best effort.");
|
|
3272
3587
|
print(" login Acquire a JWT via the device-code flow + store in keystore");
|
|
@@ -3278,7 +3593,7 @@ function printUsage() {
|
|
|
3278
3593
|
}
|
|
3279
3594
|
function getBrokerPackageVersion() {
|
|
3280
3595
|
{
|
|
3281
|
-
return "0.
|
|
3596
|
+
return "0.4.1";
|
|
3282
3597
|
}
|
|
3283
3598
|
}
|
|
3284
3599
|
function printVersion() {
|
|
@@ -3314,6 +3629,66 @@ function defaultShellOut(cmd, argv) {
|
|
|
3314
3629
|
});
|
|
3315
3630
|
});
|
|
3316
3631
|
}
|
|
3632
|
+
function stdinIsTty() {
|
|
3633
|
+
return process.stdin.isTTY === true;
|
|
3634
|
+
}
|
|
3635
|
+
function promptYesNo(question) {
|
|
3636
|
+
return new Promise((resolve) => {
|
|
3637
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
3638
|
+
rl.question(question, (answer) => {
|
|
3639
|
+
rl.close();
|
|
3640
|
+
const a = answer.trim().toLowerCase();
|
|
3641
|
+
resolve(a === "" || a === "y" || a === "yes");
|
|
3642
|
+
});
|
|
3643
|
+
});
|
|
3644
|
+
}
|
|
3645
|
+
function promptSecret(question) {
|
|
3646
|
+
return new Promise((resolve) => {
|
|
3647
|
+
process.stdout.write(`${question.trimEnd()} (input hidden)
|
|
3648
|
+
`);
|
|
3649
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
3650
|
+
const rlAny = rl;
|
|
3651
|
+
rlAny._writeToOutput = () => {
|
|
3652
|
+
};
|
|
3653
|
+
rl.question("", (answer) => {
|
|
3654
|
+
rl.close();
|
|
3655
|
+
process.stdout.write("\n");
|
|
3656
|
+
resolve(answer);
|
|
3657
|
+
});
|
|
3658
|
+
});
|
|
3659
|
+
}
|
|
3660
|
+
async function readStdinAll() {
|
|
3661
|
+
if (process.stdin.isTTY) return "";
|
|
3662
|
+
const chunks = [];
|
|
3663
|
+
for await (const chunk of process.stdin) {
|
|
3664
|
+
chunks.push(Buffer.from(chunk));
|
|
3665
|
+
}
|
|
3666
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
3667
|
+
}
|
|
3668
|
+
function makeSessionPromptDeps() {
|
|
3669
|
+
return {
|
|
3670
|
+
isTty: stdinIsTty(),
|
|
3671
|
+
readStdinAll,
|
|
3672
|
+
promptYesNo,
|
|
3673
|
+
promptSecret
|
|
3674
|
+
};
|
|
3675
|
+
}
|
|
3676
|
+
function makeStopDeps(clearJwtOnStop, override) {
|
|
3677
|
+
const resolved = override ?? (() => {
|
|
3678
|
+
const config = loadMcpConfig();
|
|
3679
|
+
return { endpoint: config.brokerEndpoint, brokerTimeoutMs: config.brokerTimeoutMs };
|
|
3680
|
+
})();
|
|
3681
|
+
return {
|
|
3682
|
+
print,
|
|
3683
|
+
printErr,
|
|
3684
|
+
newBrokerClient: (endpoint, timeoutMs) => new BrokerClient({ endpoint, timeoutMs }),
|
|
3685
|
+
killProcess: defaultKillProcess,
|
|
3686
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
3687
|
+
endpoint: resolved.endpoint,
|
|
3688
|
+
brokerTimeoutMs: resolved.brokerTimeoutMs,
|
|
3689
|
+
clearJwtOnStop
|
|
3690
|
+
};
|
|
3691
|
+
}
|
|
3317
3692
|
async function runSetup2(argv) {
|
|
3318
3693
|
const deps = {
|
|
3319
3694
|
print,
|
|
@@ -3328,22 +3703,35 @@ async function runSetup2(argv) {
|
|
|
3328
3703
|
env: process.env,
|
|
3329
3704
|
platformId: process.platform,
|
|
3330
3705
|
osRelease: release(),
|
|
3331
|
-
shellOut: defaultShellOut
|
|
3706
|
+
shellOut: defaultShellOut,
|
|
3707
|
+
sessionInput: makeSessionPromptDeps()
|
|
3332
3708
|
};
|
|
3333
3709
|
return runSetup(argv, deps);
|
|
3334
3710
|
}
|
|
3335
3711
|
async function runStop2() {
|
|
3336
|
-
|
|
3337
|
-
|
|
3712
|
+
return runStop(makeStopDeps(true));
|
|
3713
|
+
}
|
|
3714
|
+
function makeBringUpDeps() {
|
|
3715
|
+
return {
|
|
3338
3716
|
print,
|
|
3339
3717
|
printErr,
|
|
3340
3718
|
newBrokerClient: (endpoint, timeoutMs) => new BrokerClient({ endpoint, timeoutMs }),
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3719
|
+
spawnDaemon,
|
|
3720
|
+
waitForBroker,
|
|
3721
|
+
// `update` stops the old daemon but PRESERVES the JWT (key rotation
|
|
3722
|
+
// must not force a device-code re-login). Targets the resolved
|
|
3723
|
+
// endpoint so a `--broker-endpoint` override stops the right daemon.
|
|
3724
|
+
stopDaemon: (endpoint, brokerTimeoutMs) => runStop(makeStopDeps(false, { endpoint, brokerTimeoutMs })),
|
|
3725
|
+
runLogin,
|
|
3726
|
+
resolveBinPath: resolveBrokerBinPath,
|
|
3727
|
+
env: process.env,
|
|
3728
|
+
platformId: process.platform,
|
|
3729
|
+
osRelease: release(),
|
|
3730
|
+
sessionPrompt: makeSessionPromptDeps()
|
|
3345
3731
|
};
|
|
3346
|
-
|
|
3732
|
+
}
|
|
3733
|
+
async function runStartOrUpdate(mode, argv) {
|
|
3734
|
+
return runBringUp(mode, argv, makeBringUpDeps());
|
|
3347
3735
|
}
|
|
3348
3736
|
async function runCli(argv) {
|
|
3349
3737
|
const [sub, ...rest] = argv;
|
|
@@ -3353,6 +3741,10 @@ async function runCli(argv) {
|
|
|
3353
3741
|
return 0;
|
|
3354
3742
|
case "setup":
|
|
3355
3743
|
return runSetup2(rest);
|
|
3744
|
+
case "start":
|
|
3745
|
+
return runStartOrUpdate("start", rest);
|
|
3746
|
+
case "update":
|
|
3747
|
+
return runStartOrUpdate("update", rest);
|
|
3356
3748
|
case "stop":
|
|
3357
3749
|
return runStop2();
|
|
3358
3750
|
case "login":
|
|
@@ -3376,4 +3768,4 @@ async function runCli(argv) {
|
|
|
3376
3768
|
}
|
|
3377
3769
|
}
|
|
3378
3770
|
|
|
3379
|
-
export { getBrokerPackageVersion, parseLoginFlags, runCli, runDoctor, runLogin, runLogout, runSetup2 as runSetup, runStop2 as runStop };
|
|
3771
|
+
export { getBrokerPackageVersion, parseLoginFlags, runCli, runDoctor, runLogin, runLogout, runSetup2 as runSetup, runStartOrUpdate, runStop2 as runStop };
|