@muhaven/mcp 0.3.0 → 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/CHANGELOG.md +39 -0
- package/dist/broker.cjs +389 -36
- package/dist/broker.d.cts +31 -1
- package/dist/broker.d.ts +31 -1
- package/dist/broker.js +389 -37
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/manifest.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] — 2026-05-24
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Wave 5 Option D OPEN-D — `muhaven-broker start` / `update` session-key
|
|
15
|
+
CLI.** Automates the manual last mile after a dashboard mint / revoke so
|
|
16
|
+
the operator no longer hand-edits `MUHAVEN_BROKER_SESSION_KEY` + restarts
|
|
17
|
+
the daemon:
|
|
18
|
+
- `muhaven-broker start --session <key|->` — bring the daemon UP on a
|
|
19
|
+
provided key (when it is NOT running). Refuses if a daemon is already
|
|
20
|
+
bound to the endpoint (points the operator at `update`).
|
|
21
|
+
- `muhaven-broker update --session <key|->` — ROTATE the key on a
|
|
22
|
+
(possibly) running daemon: stop → swap → restart, **reusing the
|
|
23
|
+
existing device-flow JWT** (a key rotation does not force a fresh
|
|
24
|
+
device-code login). Fully stops the old daemon before the new one binds
|
|
25
|
+
the endpoint.
|
|
26
|
+
- Both accept `--session -` to read the key from stdin (keeps it out of
|
|
27
|
+
`ps` / shell history), and when run WITHOUT `--session` ask
|
|
28
|
+
interactively ("Do you have a session key from the dashboard? [Y/n]" →
|
|
29
|
+
masked paste). Non-TTY (CI / piped) never hangs — it requires
|
|
30
|
+
`--session` instead.
|
|
31
|
+
- `setup` gained the same interactive prompt: with no
|
|
32
|
+
`MUHAVEN_BROKER_SESSION_KEY` set, it asks whether you have a
|
|
33
|
+
dashboard-minted key (paste it) or mints a fresh one (the
|
|
34
|
+
fresh-install default). Scripted runs (env var set, or non-TTY) keep
|
|
35
|
+
the prior self-mint behavior.
|
|
36
|
+
- **Key-persistence model: Option B (operator decision 2026-05-24).** The
|
|
37
|
+
resolved key is injected ONLY into the spawned daemon's child env — it
|
|
38
|
+
never touches disk. The daemon (`loadBrokerConfig`) and the keystore are
|
|
39
|
+
unchanged; the broker wire protocol is unchanged (no protocol bump). The
|
|
40
|
+
session key is validated (`0x` + 64 hex) and NEVER logged / echoed /
|
|
41
|
+
embedded in an error message.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- `runStop` (`broker/stop.ts`) gained an optional `clearJwtOnStop` flag
|
|
46
|
+
(default `true`, preserving the `stop` subcommand's behavior). `update`
|
|
47
|
+
passes `false` so the JWT survives the key rotation.
|
|
48
|
+
|
|
10
49
|
## [0.3.0] — 2026-05-23
|
|
11
50
|
|
|
12
51
|
### Added
|
package/dist/broker.cjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
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');
|
|
@@ -2366,6 +2367,63 @@ async function runBrokerDaemonCli() {
|
|
|
2366
2367
|
await new Promise(() => {
|
|
2367
2368
|
});
|
|
2368
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
|
|
2369
2427
|
var DANGEROUS_NODE_ENV_VARS = [
|
|
2370
2428
|
"NODE_OPTIONS",
|
|
2371
2429
|
"NODE_TLS_REJECT_UNAUTHORIZED",
|
|
@@ -2453,6 +2511,26 @@ function validateBrokerEndpointFlag(value, platformId) {
|
|
|
2453
2511
|
}
|
|
2454
2512
|
return null;
|
|
2455
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
|
+
}
|
|
2456
2534
|
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2457
2535
|
async function waitForBroker(options) {
|
|
2458
2536
|
const timeoutMs = options.timeoutMs ?? 8e3;
|
|
@@ -2702,12 +2780,32 @@ async function runSetup(argv, deps) {
|
|
|
2702
2780
|
}
|
|
2703
2781
|
let sessionKey = effectiveEnv.MUHAVEN_BROKER_SESSION_KEY;
|
|
2704
2782
|
let mintedKey = false;
|
|
2705
|
-
if (
|
|
2706
|
-
sessionKey = deps.mintSessionKey();
|
|
2707
|
-
mintedKey = true;
|
|
2708
|
-
deps.print("Session key: minted fresh (secp256k1, ephemeral to this daemon).");
|
|
2709
|
-
} else {
|
|
2783
|
+
if (sessionKey && sessionKey.length > 0) {
|
|
2710
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
|
+
}
|
|
2711
2809
|
}
|
|
2712
2810
|
effectiveEnv.MUHAVEN_BROKER_SESSION_KEY = sessionKey;
|
|
2713
2811
|
if (flags.foreground) {
|
|
@@ -2791,21 +2889,7 @@ async function runSetup(argv, deps) {
|
|
|
2791
2889
|
if (flags.dashboardBaseUrl) {
|
|
2792
2890
|
loginArgv.push("--dashboard-base-url", flags.dashboardBaseUrl);
|
|
2793
2891
|
}
|
|
2794
|
-
const
|
|
2795
|
-
const originalValues = {};
|
|
2796
|
-
for (const k of restorationKeys) {
|
|
2797
|
-
originalValues[k] = process.env[k];
|
|
2798
|
-
if (effectiveEnv[k]) process.env[k] = effectiveEnv[k];
|
|
2799
|
-
}
|
|
2800
|
-
let code;
|
|
2801
|
-
try {
|
|
2802
|
-
code = await deps.runLogin(loginArgv);
|
|
2803
|
-
} finally {
|
|
2804
|
-
for (const k of restorationKeys) {
|
|
2805
|
-
if (originalValues[k] === void 0) delete process.env[k];
|
|
2806
|
-
else process.env[k] = originalValues[k];
|
|
2807
|
-
}
|
|
2808
|
-
}
|
|
2892
|
+
const code = await withSeededLoginEnv(effectiveEnv, () => deps.runLogin(loginArgv));
|
|
2809
2893
|
if (code !== 0) {
|
|
2810
2894
|
deps.printErr(
|
|
2811
2895
|
"Setup: login step failed \u2014 daemon is still running, re-run `muhaven-broker login` to retry."
|
|
@@ -2889,13 +2973,15 @@ async function runStop(deps) {
|
|
|
2889
2973
|
deps.print("Broker daemon: not running, nothing to stop.");
|
|
2890
2974
|
return 0;
|
|
2891
2975
|
}
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
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
|
+
}
|
|
2899
2985
|
}
|
|
2900
2986
|
const pid = hello.pid;
|
|
2901
2987
|
if (pid === void 0) {
|
|
@@ -2953,6 +3039,190 @@ function defaultKillProcess(pid, signal) {
|
|
|
2953
3039
|
}
|
|
2954
3040
|
}
|
|
2955
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
|
+
|
|
2956
3226
|
// src/broker/cli.ts
|
|
2957
3227
|
function print(line) {
|
|
2958
3228
|
process.stdout.write(line + "\n");
|
|
@@ -3265,6 +3535,11 @@ function printUsage() {
|
|
|
3265
3535
|
print(" (claude-code today; claude-desktop / cursor reserved for Wave 5)");
|
|
3266
3536
|
print(" [--register-scope user|project|local] scope for the host-config write");
|
|
3267
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).");
|
|
3268
3543
|
print(" stop Cleanly stop a running daemon (SIGTERM with SIGKILL fallback");
|
|
3269
3544
|
print(" after 5s). Also clears the keystore JWT as a best effort.");
|
|
3270
3545
|
print(" login Acquire a JWT via the device-code flow + store in keystore");
|
|
@@ -3276,7 +3551,7 @@ function printUsage() {
|
|
|
3276
3551
|
}
|
|
3277
3552
|
function getBrokerPackageVersion() {
|
|
3278
3553
|
{
|
|
3279
|
-
return "0.
|
|
3554
|
+
return "0.4.0";
|
|
3280
3555
|
}
|
|
3281
3556
|
}
|
|
3282
3557
|
function printVersion() {
|
|
@@ -3312,6 +3587,66 @@ function defaultShellOut(cmd, argv) {
|
|
|
3312
3587
|
});
|
|
3313
3588
|
});
|
|
3314
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
|
+
}
|
|
3315
3650
|
async function runSetup2(argv) {
|
|
3316
3651
|
const deps = {
|
|
3317
3652
|
print,
|
|
@@ -3326,22 +3661,35 @@ async function runSetup2(argv) {
|
|
|
3326
3661
|
env: process.env,
|
|
3327
3662
|
platformId: process.platform,
|
|
3328
3663
|
osRelease: os.release(),
|
|
3329
|
-
shellOut: defaultShellOut
|
|
3664
|
+
shellOut: defaultShellOut,
|
|
3665
|
+
sessionInput: makeSessionPromptDeps()
|
|
3330
3666
|
};
|
|
3331
3667
|
return runSetup(argv, deps);
|
|
3332
3668
|
}
|
|
3333
3669
|
async function runStop2() {
|
|
3334
|
-
|
|
3335
|
-
|
|
3670
|
+
return runStop(makeStopDeps(true));
|
|
3671
|
+
}
|
|
3672
|
+
function makeBringUpDeps() {
|
|
3673
|
+
return {
|
|
3336
3674
|
print,
|
|
3337
3675
|
printErr,
|
|
3338
3676
|
newBrokerClient: (endpoint, timeoutMs) => new BrokerClient({ endpoint, timeoutMs }),
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
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()
|
|
3343
3689
|
};
|
|
3344
|
-
|
|
3690
|
+
}
|
|
3691
|
+
async function runStartOrUpdate(mode, argv) {
|
|
3692
|
+
return runBringUp(mode, argv, makeBringUpDeps());
|
|
3345
3693
|
}
|
|
3346
3694
|
async function runCli(argv) {
|
|
3347
3695
|
const [sub, ...rest] = argv;
|
|
@@ -3351,6 +3699,10 @@ async function runCli(argv) {
|
|
|
3351
3699
|
return 0;
|
|
3352
3700
|
case "setup":
|
|
3353
3701
|
return runSetup2(rest);
|
|
3702
|
+
case "start":
|
|
3703
|
+
return runStartOrUpdate("start", rest);
|
|
3704
|
+
case "update":
|
|
3705
|
+
return runStartOrUpdate("update", rest);
|
|
3354
3706
|
case "stop":
|
|
3355
3707
|
return runStop2();
|
|
3356
3708
|
case "login":
|
|
@@ -3381,4 +3733,5 @@ exports.runDoctor = runDoctor;
|
|
|
3381
3733
|
exports.runLogin = runLogin;
|
|
3382
3734
|
exports.runLogout = runLogout;
|
|
3383
3735
|
exports.runSetup = runSetup2;
|
|
3736
|
+
exports.runStartOrUpdate = runStartOrUpdate;
|
|
3384
3737
|
exports.runStop = runStop2;
|
package/dist/broker.d.cts
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.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';
|
|
@@ -2368,6 +2369,63 @@ async function runBrokerDaemonCli() {
|
|
|
2368
2369
|
await new Promise(() => {
|
|
2369
2370
|
});
|
|
2370
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
|
|
2371
2429
|
var DANGEROUS_NODE_ENV_VARS = [
|
|
2372
2430
|
"NODE_OPTIONS",
|
|
2373
2431
|
"NODE_TLS_REJECT_UNAUTHORIZED",
|
|
@@ -2455,6 +2513,26 @@ function validateBrokerEndpointFlag(value, platformId) {
|
|
|
2455
2513
|
}
|
|
2456
2514
|
return null;
|
|
2457
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
|
+
}
|
|
2458
2536
|
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2459
2537
|
async function waitForBroker(options) {
|
|
2460
2538
|
const timeoutMs = options.timeoutMs ?? 8e3;
|
|
@@ -2704,12 +2782,32 @@ async function runSetup(argv, deps) {
|
|
|
2704
2782
|
}
|
|
2705
2783
|
let sessionKey = effectiveEnv.MUHAVEN_BROKER_SESSION_KEY;
|
|
2706
2784
|
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 {
|
|
2785
|
+
if (sessionKey && sessionKey.length > 0) {
|
|
2712
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
|
+
}
|
|
2713
2811
|
}
|
|
2714
2812
|
effectiveEnv.MUHAVEN_BROKER_SESSION_KEY = sessionKey;
|
|
2715
2813
|
if (flags.foreground) {
|
|
@@ -2793,21 +2891,7 @@ async function runSetup(argv, deps) {
|
|
|
2793
2891
|
if (flags.dashboardBaseUrl) {
|
|
2794
2892
|
loginArgv.push("--dashboard-base-url", flags.dashboardBaseUrl);
|
|
2795
2893
|
}
|
|
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
|
-
}
|
|
2894
|
+
const code = await withSeededLoginEnv(effectiveEnv, () => deps.runLogin(loginArgv));
|
|
2811
2895
|
if (code !== 0) {
|
|
2812
2896
|
deps.printErr(
|
|
2813
2897
|
"Setup: login step failed \u2014 daemon is still running, re-run `muhaven-broker login` to retry."
|
|
@@ -2891,13 +2975,15 @@ async function runStop(deps) {
|
|
|
2891
2975
|
deps.print("Broker daemon: not running, nothing to stop.");
|
|
2892
2976
|
return 0;
|
|
2893
2977
|
}
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
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
|
+
}
|
|
2901
2987
|
}
|
|
2902
2988
|
const pid = hello.pid;
|
|
2903
2989
|
if (pid === void 0) {
|
|
@@ -2955,6 +3041,190 @@ function defaultKillProcess(pid, signal) {
|
|
|
2955
3041
|
}
|
|
2956
3042
|
}
|
|
2957
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
|
+
|
|
2958
3228
|
// src/broker/cli.ts
|
|
2959
3229
|
function print(line) {
|
|
2960
3230
|
process.stdout.write(line + "\n");
|
|
@@ -3267,6 +3537,11 @@ function printUsage() {
|
|
|
3267
3537
|
print(" (claude-code today; claude-desktop / cursor reserved for Wave 5)");
|
|
3268
3538
|
print(" [--register-scope user|project|local] scope for the host-config write");
|
|
3269
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).");
|
|
3270
3545
|
print(" stop Cleanly stop a running daemon (SIGTERM with SIGKILL fallback");
|
|
3271
3546
|
print(" after 5s). Also clears the keystore JWT as a best effort.");
|
|
3272
3547
|
print(" login Acquire a JWT via the device-code flow + store in keystore");
|
|
@@ -3278,7 +3553,7 @@ function printUsage() {
|
|
|
3278
3553
|
}
|
|
3279
3554
|
function getBrokerPackageVersion() {
|
|
3280
3555
|
{
|
|
3281
|
-
return "0.
|
|
3556
|
+
return "0.4.0";
|
|
3282
3557
|
}
|
|
3283
3558
|
}
|
|
3284
3559
|
function printVersion() {
|
|
@@ -3314,6 +3589,66 @@ function defaultShellOut(cmd, argv) {
|
|
|
3314
3589
|
});
|
|
3315
3590
|
});
|
|
3316
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
|
+
}
|
|
3317
3652
|
async function runSetup2(argv) {
|
|
3318
3653
|
const deps = {
|
|
3319
3654
|
print,
|
|
@@ -3328,22 +3663,35 @@ async function runSetup2(argv) {
|
|
|
3328
3663
|
env: process.env,
|
|
3329
3664
|
platformId: process.platform,
|
|
3330
3665
|
osRelease: release(),
|
|
3331
|
-
shellOut: defaultShellOut
|
|
3666
|
+
shellOut: defaultShellOut,
|
|
3667
|
+
sessionInput: makeSessionPromptDeps()
|
|
3332
3668
|
};
|
|
3333
3669
|
return runSetup(argv, deps);
|
|
3334
3670
|
}
|
|
3335
3671
|
async function runStop2() {
|
|
3336
|
-
|
|
3337
|
-
|
|
3672
|
+
return runStop(makeStopDeps(true));
|
|
3673
|
+
}
|
|
3674
|
+
function makeBringUpDeps() {
|
|
3675
|
+
return {
|
|
3338
3676
|
print,
|
|
3339
3677
|
printErr,
|
|
3340
3678
|
newBrokerClient: (endpoint, timeoutMs) => new BrokerClient({ endpoint, timeoutMs }),
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
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()
|
|
3345
3691
|
};
|
|
3346
|
-
|
|
3692
|
+
}
|
|
3693
|
+
async function runStartOrUpdate(mode, argv) {
|
|
3694
|
+
return runBringUp(mode, argv, makeBringUpDeps());
|
|
3347
3695
|
}
|
|
3348
3696
|
async function runCli(argv) {
|
|
3349
3697
|
const [sub, ...rest] = argv;
|
|
@@ -3353,6 +3701,10 @@ async function runCli(argv) {
|
|
|
3353
3701
|
return 0;
|
|
3354
3702
|
case "setup":
|
|
3355
3703
|
return runSetup2(rest);
|
|
3704
|
+
case "start":
|
|
3705
|
+
return runStartOrUpdate("start", rest);
|
|
3706
|
+
case "update":
|
|
3707
|
+
return runStartOrUpdate("update", rest);
|
|
3356
3708
|
case "stop":
|
|
3357
3709
|
return runStop2();
|
|
3358
3710
|
case "login":
|
|
@@ -3376,4 +3728,4 @@ async function runCli(argv) {
|
|
|
3376
3728
|
}
|
|
3377
3729
|
}
|
|
3378
3730
|
|
|
3379
|
-
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 };
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
package/manifest.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"manifest_version": "0.2",
|
|
4
4
|
"name": "muhaven-mcp",
|
|
5
5
|
"display_name": "MuHaven (RWA portfolio)",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.4.0",
|
|
7
7
|
"description": "Confidential RWA portfolio management on Fhenix CoFHE. Read your encrypted balances, propose yield claims and policy changes — all signing happens in a sibling broker daemon, the LLM never sees your private key.",
|
|
8
8
|
"long_description": "MuHaven MCP exposes 24 tools across read.* / position.* / policy.* / issuer.* / governance.* groups for managing real-world asset (RWA) tokens with FHE-encrypted balances. Authentication uses a one-time device-code ceremony (run `muhaven-broker login`); subsequent tool calls fetch the JWT from the broker over a Unix socket. Position / governance tools deep-link to the dashboard for passkey signing — they NEVER auto-submit to a bundler. The companion `muhaven-broker` daemon must be running before tools can be invoked. See README for setup.",
|
|
9
9
|
"author": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muhaven/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "MuHaven MCP server — read/position/policy toolsets bridging Claude Desktop / Cursor / Claude Code to the MuHaven backend, with a sibling muhaven-broker daemon holding the session-key private half over a local IPC socket",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|