@muhaven/mcp 0.1.0 → 0.1.3
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 +136 -2
- package/README.md +125 -125
- package/bin/muhaven-broker.cjs +11 -11
- package/bin/muhaven-mcp.cjs +11 -11
- package/dist/broker.cjs +138 -22
- package/dist/broker.d.cts +21 -1
- package/dist/broker.d.ts +21 -1
- package/dist/broker.js +138 -23
- package/dist/index.cjs +124 -20
- package/dist/index.d.cts +143 -13
- package/dist/index.d.ts +143 -13
- package/dist/index.js +120 -21
- package/manifest.json +98 -98
- package/package.json +104 -104
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var promises = require('fs/promises');
|
|
4
|
+
require('fs');
|
|
4
5
|
var path = require('path');
|
|
5
6
|
var url = require('url');
|
|
6
7
|
var index_js = require('@modelcontextprotocol/sdk/server/index.js');
|
|
@@ -81,23 +82,26 @@ function loadMcpConfig(env = process.env) {
|
|
|
81
82
|
}
|
|
82
83
|
var PRIVKEY_HEX_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
83
84
|
function loadBrokerConfig(env = process.env) {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
throw new Error("MUHAVEN_BROKER_SESSION_KEY must be a 0x-prefixed 32-byte hex string");
|
|
85
|
+
const sessionKeyHexRaw = env.MUHAVEN_BROKER_SESSION_KEY;
|
|
86
|
+
let sessionKeyHex;
|
|
87
|
+
if (sessionKeyHexRaw && sessionKeyHexRaw.length > 0) {
|
|
88
|
+
if (!PRIVKEY_HEX_RE.test(sessionKeyHexRaw)) {
|
|
89
|
+
throw new Error("MUHAVEN_BROKER_SESSION_KEY must be a 0x-prefixed 32-byte hex string");
|
|
90
|
+
}
|
|
91
|
+
sessionKeyHex = sessionKeyHexRaw;
|
|
92
92
|
}
|
|
93
93
|
const endpoint = env.MUHAVEN_BROKER_ENDPOINT ?? defaultBrokerEndpoint();
|
|
94
94
|
const maxRequestBytes = readEnvInt("MUHAVEN_BROKER_MAX_BYTES", DEFAULT_BROKER_MAX_BYTES, env);
|
|
95
95
|
const requestTimeoutMs = readEnvInt("MUHAVEN_BROKER_TIMEOUT_MS", DEFAULT_BROKER_TIMEOUT_MS, env);
|
|
96
|
+
const backendBaseUrl = trimTrailingSlash(env.MUHAVEN_BACKEND_URL ?? DEFAULT_BACKEND_URL);
|
|
97
|
+
const dashboardBaseUrl = trimTrailingSlash(env.MUHAVEN_DASHBOARD_URL ?? DEFAULT_DASHBOARD_URL);
|
|
96
98
|
return {
|
|
97
99
|
endpoint,
|
|
98
100
|
sessionKeyHex,
|
|
99
101
|
maxRequestBytes,
|
|
100
|
-
requestTimeoutMs
|
|
102
|
+
requestTimeoutMs,
|
|
103
|
+
backendBaseUrl,
|
|
104
|
+
dashboardBaseUrl
|
|
101
105
|
};
|
|
102
106
|
}
|
|
103
107
|
var BrokerClientError = class extends Error {
|
|
@@ -736,6 +740,15 @@ function authRequiredPayload() {
|
|
|
736
740
|
loginCommand: "muhaven-broker login"
|
|
737
741
|
};
|
|
738
742
|
}
|
|
743
|
+
function sessionKeyRequiredPayload(dashboardBaseUrl = "https://muhaven.app") {
|
|
744
|
+
const mintUrl = `${trimTrailingSlash(dashboardBaseUrl)}/agent/policy/transition`;
|
|
745
|
+
return {
|
|
746
|
+
ok: false,
|
|
747
|
+
code: "SESSION_KEY_REQUIRED",
|
|
748
|
+
message: `No session key loaded in broker (read-only posture). Mint one via the dashboard at ${mintUrl}, copy the 0x-prefixed hex into MUHAVEN_BROKER_SESSION_KEY, and restart the daemon. Do NOT run \`muhaven-broker login\` for this \u2014 that mints a JWT, not a session key.`,
|
|
749
|
+
mintUrl
|
|
750
|
+
};
|
|
751
|
+
}
|
|
739
752
|
|
|
740
753
|
// src/tools/handlers.ts
|
|
741
754
|
function ok(data) {
|
|
@@ -825,6 +838,7 @@ function sortKeys(value) {
|
|
|
825
838
|
}
|
|
826
839
|
return value;
|
|
827
840
|
}
|
|
841
|
+
var cachedHasSessionKeyProbe = null;
|
|
828
842
|
async function signEnvelope(intent, toolName, summary, deps) {
|
|
829
843
|
const intentHash = computeIntentHash(intent);
|
|
830
844
|
if (!deps.broker) {
|
|
@@ -833,8 +847,29 @@ async function signEnvelope(intent, toolName, summary, deps) {
|
|
|
833
847
|
"position tools require a running muhaven-broker daemon \u2014 see README \xA7Broker setup"
|
|
834
848
|
);
|
|
835
849
|
}
|
|
850
|
+
const broker = deps.broker;
|
|
851
|
+
if (cachedHasSessionKeyProbe === null) {
|
|
852
|
+
cachedHasSessionKeyProbe = (async () => {
|
|
853
|
+
try {
|
|
854
|
+
const hello = await broker.hello();
|
|
855
|
+
return hello.hasSessionKey ?? true;
|
|
856
|
+
} catch (err2) {
|
|
857
|
+
cachedHasSessionKeyProbe = null;
|
|
858
|
+
throw err2;
|
|
859
|
+
}
|
|
860
|
+
})();
|
|
861
|
+
}
|
|
862
|
+
let hasSessionKey;
|
|
836
863
|
try {
|
|
837
|
-
|
|
864
|
+
hasSessionKey = await cachedHasSessionKeyProbe;
|
|
865
|
+
} catch (e) {
|
|
866
|
+
return mapBrokerError(e);
|
|
867
|
+
}
|
|
868
|
+
if (hasSessionKey === false) {
|
|
869
|
+
return sessionKeyRequiredPayload(deps.dashboardBaseUrl);
|
|
870
|
+
}
|
|
871
|
+
try {
|
|
872
|
+
const sig = await broker.signHash(intentHash, { tool: toolName, summary });
|
|
838
873
|
return ok({
|
|
839
874
|
intentHash,
|
|
840
875
|
unsignedUserOp: {
|
|
@@ -846,6 +881,10 @@ async function signEnvelope(intent, toolName, summary, deps) {
|
|
|
846
881
|
signerAddress: sig.signerAddress
|
|
847
882
|
});
|
|
848
883
|
} catch (e) {
|
|
884
|
+
if (e instanceof BrokerClientError && e.code === "broker_error" && /session_key_unavailable/.test(e.message)) {
|
|
885
|
+
cachedHasSessionKeyProbe = Promise.resolve(false);
|
|
886
|
+
return sessionKeyRequiredPayload(deps.dashboardBaseUrl);
|
|
887
|
+
}
|
|
849
888
|
return mapBrokerError(e);
|
|
850
889
|
}
|
|
851
890
|
}
|
|
@@ -1168,7 +1207,12 @@ function selectRegistry(readOnly) {
|
|
|
1168
1207
|
|
|
1169
1208
|
// src/server.ts
|
|
1170
1209
|
var SERVER_NAME = "@muhaven/mcp";
|
|
1171
|
-
var SERVER_VERSION =
|
|
1210
|
+
var SERVER_VERSION = resolveServerVersion();
|
|
1211
|
+
function resolveServerVersion() {
|
|
1212
|
+
{
|
|
1213
|
+
return "0.1.3";
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1172
1216
|
function toJsonInputSchema(schema) {
|
|
1173
1217
|
return {
|
|
1174
1218
|
type: "object",
|
|
@@ -1242,7 +1286,8 @@ function buildMcpServer(opts) {
|
|
|
1242
1286
|
const result = await entry.handler(parsed, {
|
|
1243
1287
|
backend: opts.backend,
|
|
1244
1288
|
broker: opts.broker,
|
|
1245
|
-
surface: "mcp"
|
|
1289
|
+
surface: "mcp",
|
|
1290
|
+
dashboardBaseUrl: opts.dashboardBaseUrl
|
|
1246
1291
|
});
|
|
1247
1292
|
return toolJsonResponse(result);
|
|
1248
1293
|
} catch (err2) {
|
|
@@ -1304,7 +1349,8 @@ async function runMcpStdioCli(opts = {}) {
|
|
|
1304
1349
|
const server = buildMcpServer({
|
|
1305
1350
|
registry,
|
|
1306
1351
|
backend,
|
|
1307
|
-
broker: config.readOnly ? void 0 : broker
|
|
1352
|
+
broker: config.readOnly ? void 0 : broker,
|
|
1353
|
+
dashboardBaseUrl: config.dashboardBaseUrl
|
|
1308
1354
|
});
|
|
1309
1355
|
const transport = new stdio_js.StdioServerTransport();
|
|
1310
1356
|
await server.connect(transport);
|
|
@@ -1459,7 +1505,7 @@ async function safeJson(res) {
|
|
|
1459
1505
|
}
|
|
1460
1506
|
|
|
1461
1507
|
// src/broker/protocol.ts
|
|
1462
|
-
var BROKER_PROTOCOL_VERSION = "0.
|
|
1508
|
+
var BROKER_PROTOCOL_VERSION = "0.3.0";
|
|
1463
1509
|
var HASH_HEX_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
1464
1510
|
var JWT_RE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
1465
1511
|
function isHashHex(value) {
|
|
@@ -1545,6 +1591,21 @@ function parseBrokerRequest(line) {
|
|
|
1545
1591
|
function serializeResponse(res) {
|
|
1546
1592
|
return JSON.stringify(res) + "\n";
|
|
1547
1593
|
}
|
|
1594
|
+
var MissingSessionKeyError = class extends Error {
|
|
1595
|
+
constructor() {
|
|
1596
|
+
super(
|
|
1597
|
+
"session_key_unavailable: daemon booted in read-only posture (no MUHAVEN_BROKER_SESSION_KEY at env-load time). Mint a session key via the dashboard /agent/policy/transition flow, set MUHAVEN_BROKER_SESSION_KEY, and restart the daemon. (Note: `muhaven-broker login` mints a JWT, NOT a session key \u2014 do not loop on that command for this error.)"
|
|
1598
|
+
);
|
|
1599
|
+
this.name = "MissingSessionKeyError";
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1602
|
+
var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
1603
|
+
var NullSigner = class {
|
|
1604
|
+
address = ZERO_ADDRESS;
|
|
1605
|
+
async signHash(_hash) {
|
|
1606
|
+
throw new MissingSessionKeyError();
|
|
1607
|
+
}
|
|
1608
|
+
};
|
|
1548
1609
|
var ViemSigner = class {
|
|
1549
1610
|
account;
|
|
1550
1611
|
constructor(privateKey) {
|
|
@@ -1716,7 +1777,7 @@ async function openKeystore(options = {}) {
|
|
|
1716
1777
|
// src/broker/daemon.ts
|
|
1717
1778
|
var noopLogger = (_e) => {
|
|
1718
1779
|
};
|
|
1719
|
-
async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.floor(Date.now() / 1e3)) {
|
|
1780
|
+
async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.floor(Date.now() / 1e3), options = {}) {
|
|
1720
1781
|
switch (req.type) {
|
|
1721
1782
|
case "hello": {
|
|
1722
1783
|
let hasJwt = false;
|
|
@@ -1726,16 +1787,26 @@ async function handleBrokerRequest(req, signer, keystore, nowSec = () => Math.fl
|
|
|
1726
1787
|
} catch {
|
|
1727
1788
|
hasJwt = false;
|
|
1728
1789
|
}
|
|
1790
|
+
const hasSessionKey = options.hasSessionKey ?? true;
|
|
1729
1791
|
return {
|
|
1730
1792
|
type: "hello",
|
|
1731
1793
|
version: BROKER_PROTOCOL_VERSION,
|
|
1732
1794
|
sessionKeyAddress: signer.address,
|
|
1733
|
-
hasJwt
|
|
1795
|
+
hasJwt,
|
|
1796
|
+
hasSessionKey,
|
|
1797
|
+
...options.effectiveConfig ? { effectiveConfig: options.effectiveConfig } : {}
|
|
1734
1798
|
};
|
|
1735
1799
|
}
|
|
1736
1800
|
case "sign_hash": {
|
|
1737
|
-
|
|
1738
|
-
|
|
1801
|
+
try {
|
|
1802
|
+
const signature = await signer.signHash(req.hash);
|
|
1803
|
+
return { type: "sign_hash", signature, signerAddress: signer.address };
|
|
1804
|
+
} catch (err2) {
|
|
1805
|
+
if (err2 instanceof MissingSessionKeyError) {
|
|
1806
|
+
return errorResponse("session_key_unavailable", err2.message);
|
|
1807
|
+
}
|
|
1808
|
+
throw err2;
|
|
1809
|
+
}
|
|
1739
1810
|
}
|
|
1740
1811
|
case "store_jwt": {
|
|
1741
1812
|
try {
|
|
@@ -1807,9 +1878,24 @@ var BrokerDaemon = class {
|
|
|
1807
1878
|
log;
|
|
1808
1879
|
config;
|
|
1809
1880
|
keystore;
|
|
1881
|
+
/**
|
|
1882
|
+
* Whether a session-key private half is actually loaded. `false` =
|
|
1883
|
+
* daemon booted in read-only posture (no `MUHAVEN_BROKER_SESSION_KEY`
|
|
1884
|
+
* at env-load time) and uses a `NullSigner` whose `signHash` throws.
|
|
1885
|
+
*/
|
|
1886
|
+
hasSessionKey;
|
|
1810
1887
|
constructor(options) {
|
|
1811
1888
|
this.config = options.config;
|
|
1812
|
-
|
|
1889
|
+
if (options.signer) {
|
|
1890
|
+
this.signer = options.signer;
|
|
1891
|
+
this.hasSessionKey = true;
|
|
1892
|
+
} else if (options.config.sessionKeyHex) {
|
|
1893
|
+
this.signer = new ViemSigner(options.config.sessionKeyHex);
|
|
1894
|
+
this.hasSessionKey = true;
|
|
1895
|
+
} else {
|
|
1896
|
+
this.signer = new NullSigner();
|
|
1897
|
+
this.hasSessionKey = false;
|
|
1898
|
+
}
|
|
1813
1899
|
this.keystore = options.keystore ?? null;
|
|
1814
1900
|
this.log = options.logger ?? noopLogger;
|
|
1815
1901
|
this.server = net.createServer((socket) => this.onConnection(socket));
|
|
@@ -1847,6 +1933,7 @@ var BrokerDaemon = class {
|
|
|
1847
1933
|
meta: {
|
|
1848
1934
|
endpoint: this.config.endpoint,
|
|
1849
1935
|
signer: this.signer.address,
|
|
1936
|
+
hasSessionKey: this.hasSessionKey,
|
|
1850
1937
|
keystore: this.keystore.backend,
|
|
1851
1938
|
version: BROKER_PROTOCOL_VERSION
|
|
1852
1939
|
}
|
|
@@ -1928,7 +2015,19 @@ var BrokerDaemon = class {
|
|
|
1928
2015
|
return;
|
|
1929
2016
|
}
|
|
1930
2017
|
try {
|
|
1931
|
-
const res = await handleBrokerRequest(
|
|
2018
|
+
const res = await handleBrokerRequest(
|
|
2019
|
+
parsed,
|
|
2020
|
+
this.signer,
|
|
2021
|
+
this.keystore,
|
|
2022
|
+
void 0,
|
|
2023
|
+
{
|
|
2024
|
+
hasSessionKey: this.hasSessionKey,
|
|
2025
|
+
effectiveConfig: {
|
|
2026
|
+
backendBaseUrl: this.config.backendBaseUrl,
|
|
2027
|
+
dashboardBaseUrl: this.config.dashboardBaseUrl
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
);
|
|
1932
2031
|
socket.end(serializeResponse(res));
|
|
1933
2032
|
} catch (err2) {
|
|
1934
2033
|
this.log({
|
|
@@ -1953,8 +2052,13 @@ exports.DeviceFlowAbortedError = DeviceFlowAbortedError;
|
|
|
1953
2052
|
exports.DeviceFlowClient = DeviceFlowClient;
|
|
1954
2053
|
exports.JwtSource = JwtSource;
|
|
1955
2054
|
exports.KeystoreError = KeystoreError;
|
|
2055
|
+
exports.MissingSessionKeyError = MissingSessionKeyError;
|
|
1956
2056
|
exports.NoJwtAvailableError = NoJwtAvailableError;
|
|
2057
|
+
exports.NullSigner = NullSigner;
|
|
2058
|
+
exports.SERVER_VERSION = SERVER_VERSION;
|
|
1957
2059
|
exports.TOOL_DESCRIPTORS = TOOL_DESCRIPTORS;
|
|
2060
|
+
exports.ViemSigner = ViemSigner;
|
|
2061
|
+
exports.ZERO_ADDRESS = ZERO_ADDRESS;
|
|
1958
2062
|
exports.buildMcpServer = buildMcpServer;
|
|
1959
2063
|
exports.buildToolHashTable = buildToolHashTable;
|
|
1960
2064
|
exports.defaultBrokerEndpoint = defaultBrokerEndpoint;
|
package/dist/index.d.cts
CHANGED
|
@@ -9,11 +9,14 @@ import { z } from 'zod';
|
|
|
9
9
|
* (Windows). Each request is a single JSON object; each response is a
|
|
10
10
|
* single JSON object. No request pipelining, no streaming.
|
|
11
11
|
*
|
|
12
|
-
* **Protocol version 0.
|
|
13
|
-
* to add
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* **Protocol version 0.3.0** — additive bump from 0.2.0 in @muhaven/mcp@0.1.3
|
|
13
|
+
* to add `hello.hasSessionKey` + `hello.effectiveConfig` (so a daemon
|
|
14
|
+
* booted without `MUHAVEN_BROKER_SESSION_KEY` can serve read paths AND
|
|
15
|
+
* surface its effective backend/dashboard URLs to `muhaven-broker login
|
|
16
|
+
* --from-daemon`). The 0.2.0 bump from 0.1.0 (Wave 4 P3 ADR-3) added the
|
|
17
|
+
* `store_jwt` / `get_jwt` / `clear_jwt` triple — the broker is the single
|
|
18
|
+
* keeper of the device-flow JWT (per ADR-3 D1 "polling, not loopback
|
|
19
|
+
* callback") in addition to the session-key private half.
|
|
17
20
|
*
|
|
18
21
|
* Threat-model invariants:
|
|
19
22
|
* - The broker NEVER reaches out to the network. It only:
|
|
@@ -26,7 +29,7 @@ import { z } from 'zod';
|
|
|
26
29
|
* - Requests are size-capped (`maxRequestBytes`) — a malformed peer
|
|
27
30
|
* cannot exhaust broker memory by sending an unbounded JSON blob.
|
|
28
31
|
*/
|
|
29
|
-
declare const BROKER_PROTOCOL_VERSION = "0.
|
|
32
|
+
declare const BROKER_PROTOCOL_VERSION = "0.3.0";
|
|
30
33
|
interface BrokerHelloRequest {
|
|
31
34
|
readonly type: 'hello';
|
|
32
35
|
}
|
|
@@ -57,10 +60,40 @@ interface BrokerClearJwtRequest {
|
|
|
57
60
|
interface BrokerHelloResponse {
|
|
58
61
|
readonly type: 'hello';
|
|
59
62
|
readonly version: string;
|
|
60
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* 0x-prefixed checksummed address derived from the session key. When
|
|
65
|
+
* the daemon was booted without `MUHAVEN_BROKER_SESSION_KEY` (read-only
|
|
66
|
+
* posture; see `hasSessionKey`), this is the zero address.
|
|
67
|
+
*
|
|
68
|
+
* **DO NOT** use address-equality (`sessionKeyAddress !== ZERO_ADDRESS`)
|
|
69
|
+
* as a proxy for "session key is loaded" — that's a soft signal that
|
|
70
|
+
* future changes (e.g. allowing custom EOA-bound dev posture) could
|
|
71
|
+
* break silently. The authoritative aliveness check is `hasSessionKey`.
|
|
72
|
+
*/
|
|
61
73
|
readonly sessionKeyAddress: `0x${string}`;
|
|
62
74
|
/** Whether a JWT is currently in the keystore. Useful for `doctor`. */
|
|
63
75
|
readonly hasJwt: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Whether a session-key private half is loaded into the daemon. False
|
|
78
|
+
* when the daemon was booted without `MUHAVEN_BROKER_SESSION_KEY` — in
|
|
79
|
+
* that posture read tools still work (the broker serves JWT verbs), but
|
|
80
|
+
* any `sign_hash` request returns `session_key_unavailable`. Field added
|
|
81
|
+
* in protocol 0.3.0; absence implies `true` for back-compat with
|
|
82
|
+
* 0.2.0 daemons.
|
|
83
|
+
*/
|
|
84
|
+
readonly hasSessionKey?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Effective backend + dashboard URLs the daemon resolved from its own
|
|
87
|
+
* process env at boot. Surfaced so `muhaven-broker login --from-daemon`
|
|
88
|
+
* can stay in lockstep with the daemon's view rather than re-reading
|
|
89
|
+
* the CLI's env (which may diverge — e.g. login invoked over ssh inherits
|
|
90
|
+
* a different shell env than the systemd-launched daemon). Field added
|
|
91
|
+
* in protocol 0.3.0; absent on older daemons.
|
|
92
|
+
*/
|
|
93
|
+
readonly effectiveConfig?: {
|
|
94
|
+
readonly backendBaseUrl: string;
|
|
95
|
+
readonly dashboardBaseUrl: string;
|
|
96
|
+
};
|
|
64
97
|
}
|
|
65
98
|
interface BrokerSignHashResponse {
|
|
66
99
|
readonly type: 'sign_hash';
|
|
@@ -87,7 +120,7 @@ interface BrokerErrorResponse {
|
|
|
87
120
|
readonly code: BrokerErrorCode;
|
|
88
121
|
readonly message: string;
|
|
89
122
|
}
|
|
90
|
-
type BrokerErrorCode = 'invalid_request' | 'payload_too_large' | 'unsupported_type' | 'internal' | 'forbidden' | 'keystore_unavailable';
|
|
123
|
+
type BrokerErrorCode = 'invalid_request' | 'payload_too_large' | 'unsupported_type' | 'internal' | 'forbidden' | 'keystore_unavailable' | 'session_key_unavailable';
|
|
91
124
|
type BrokerRequest = BrokerHelloRequest | BrokerSignHashRequest | BrokerStoreJwtRequest | BrokerGetJwtRequest | BrokerClearJwtRequest;
|
|
92
125
|
type BrokerResponse = BrokerHelloResponse | BrokerSignHashResponse | BrokerStoreJwtResponse | BrokerGetJwtResponse | BrokerClearJwtResponse | BrokerErrorResponse;
|
|
93
126
|
/**
|
|
@@ -316,6 +349,27 @@ interface AuthRequiredPayload {
|
|
|
316
349
|
readonly message: string;
|
|
317
350
|
readonly loginCommand: string;
|
|
318
351
|
}
|
|
352
|
+
/**
|
|
353
|
+
* Sibling to `AUTH_REQUIRED` — used when the broker daemon is reachable
|
|
354
|
+
* but boots in read-only posture (no `MUHAVEN_BROKER_SESSION_KEY` at
|
|
355
|
+
* startup). The remediation is NOT `muhaven-broker login` — that mints
|
|
356
|
+
* a JWT, which is different from minting a session key. The session key
|
|
357
|
+
* comes from the dashboard's `/agent/policy/transition` ceremony (see
|
|
358
|
+
* Q1 in the post-§4 queue).
|
|
359
|
+
*
|
|
360
|
+
* Surfacing this as a distinct code lets the host LLM disambiguate:
|
|
361
|
+
* - `AUTH_REQUIRED` → run a CLI command (deterministic, one-step).
|
|
362
|
+
* - `SESSION_KEY_REQUIRED` → open a URL and complete a passkey ceremony
|
|
363
|
+
* (operator-mediated, multi-step). The LLM should NOT auto-suggest
|
|
364
|
+
* `muhaven-broker login` for this case — that would loop the user
|
|
365
|
+
* indefinitely without minting a key.
|
|
366
|
+
*/
|
|
367
|
+
interface SessionKeyRequiredPayload {
|
|
368
|
+
readonly ok: false;
|
|
369
|
+
readonly code: 'SESSION_KEY_REQUIRED';
|
|
370
|
+
readonly message: string;
|
|
371
|
+
readonly mintUrl: string;
|
|
372
|
+
}
|
|
319
373
|
|
|
320
374
|
/**
|
|
321
375
|
* Tool handlers — pure functions of `(input, deps)` returning a structured
|
|
@@ -343,6 +397,13 @@ interface ToolDeps {
|
|
|
343
397
|
/** Surface this MCP server is configured for. Always 'mcp' here, but
|
|
344
398
|
* carried as a dep so the audit tool can filter to the local surface. */
|
|
345
399
|
surface: 'mcp';
|
|
400
|
+
/**
|
|
401
|
+
* Dashboard base URL — used to build the mint-URL surfaced in the
|
|
402
|
+
* `SESSION_KEY_REQUIRED` payload when the broker is in read-only
|
|
403
|
+
* posture. Optional for back-compat; the payload falls back to the
|
|
404
|
+
* production default when absent.
|
|
405
|
+
*/
|
|
406
|
+
dashboardBaseUrl?: string;
|
|
346
407
|
}
|
|
347
408
|
type ToolResult<T> = {
|
|
348
409
|
ok: true;
|
|
@@ -351,7 +412,7 @@ type ToolResult<T> = {
|
|
|
351
412
|
ok: false;
|
|
352
413
|
code: string;
|
|
353
414
|
message: string;
|
|
354
|
-
} | AuthRequiredPayload;
|
|
415
|
+
} | AuthRequiredPayload | SessionKeyRequiredPayload;
|
|
355
416
|
|
|
356
417
|
/**
|
|
357
418
|
* Static registry binding each tool descriptor to its zod schema and
|
|
@@ -393,10 +454,17 @@ declare function selectRegistry(readOnly: boolean): readonly ToolEntry[];
|
|
|
393
454
|
* instructs the user to run `muhaven-broker login`.
|
|
394
455
|
*/
|
|
395
456
|
|
|
457
|
+
declare const SERVER_VERSION: string;
|
|
396
458
|
interface BuildServerOptions {
|
|
397
459
|
registry: readonly ToolEntry[];
|
|
398
460
|
backend: BackendClient;
|
|
399
461
|
broker: BrokerClient | undefined;
|
|
462
|
+
/**
|
|
463
|
+
* Threaded into `ToolDeps` so the `SESSION_KEY_REQUIRED` payload's
|
|
464
|
+
* `mintUrl` points at the operator's actual dashboard, not a hardcoded
|
|
465
|
+
* production URL.
|
|
466
|
+
*/
|
|
467
|
+
dashboardBaseUrl?: string;
|
|
400
468
|
}
|
|
401
469
|
declare function buildMcpServer(opts: BuildServerOptions): Server;
|
|
402
470
|
/**
|
|
@@ -456,12 +524,26 @@ interface McpRuntimeConfig {
|
|
|
456
524
|
interface BrokerRuntimeConfig {
|
|
457
525
|
/** Endpoint to bind: socket path on POSIX, named pipe name on Windows. */
|
|
458
526
|
endpoint: string;
|
|
459
|
-
/**
|
|
460
|
-
|
|
527
|
+
/**
|
|
528
|
+
* 0x-prefixed 32-byte private key, OR undefined for read-only posture.
|
|
529
|
+
* When undefined, the daemon still serves `hello` + the JWT verbs
|
|
530
|
+
* (so MCP read tools work), but any `sign_hash` request returns
|
|
531
|
+
* `session_key_unavailable`. Sensitive when present — keychain-backed.
|
|
532
|
+
*/
|
|
533
|
+
sessionKeyHex: `0x${string}` | undefined;
|
|
461
534
|
/** Maximum payload bytes accepted from the IPC peer. */
|
|
462
535
|
maxRequestBytes: number;
|
|
463
536
|
/** Per-request hard timeout (ms). */
|
|
464
537
|
requestTimeoutMs: number;
|
|
538
|
+
/**
|
|
539
|
+
* Effective backend URL the daemon read from its own env at boot. Used
|
|
540
|
+
* to populate `hello.effectiveConfig` so a `muhaven-broker login
|
|
541
|
+
* --from-daemon` call uses the same URL as the daemon, even when the
|
|
542
|
+
* login CLI was launched from a different shell env.
|
|
543
|
+
*/
|
|
544
|
+
backendBaseUrl: string;
|
|
545
|
+
/** Effective dashboard URL paired with backendBaseUrl. */
|
|
546
|
+
dashboardBaseUrl: string;
|
|
465
547
|
}
|
|
466
548
|
/**
|
|
467
549
|
* Compute the default IPC endpoint for the broker. POSIX: a socket file
|
|
@@ -587,6 +669,29 @@ interface ISigner {
|
|
|
587
669
|
readonly address: `0x${string}`;
|
|
588
670
|
signHash(hash: `0x${string}`): Promise<`0x${string}`>;
|
|
589
671
|
}
|
|
672
|
+
/**
|
|
673
|
+
* Sentinel signer for the read-only daemon posture (no
|
|
674
|
+
* `MUHAVEN_BROKER_SESSION_KEY` at boot). `address` returns the zero
|
|
675
|
+
* address; `signHash` throws `MissingSessionKeyError`, which the daemon
|
|
676
|
+
* maps to a structured `session_key_unavailable` broker error response.
|
|
677
|
+
*
|
|
678
|
+
* Closes §3e⁶ F-broker-session-key-required-for-reads — the daemon can
|
|
679
|
+
* serve `hello` + JWT verbs for read paths without an on-chain signer.
|
|
680
|
+
*/
|
|
681
|
+
declare class MissingSessionKeyError extends Error {
|
|
682
|
+
constructor();
|
|
683
|
+
}
|
|
684
|
+
declare const ZERO_ADDRESS: "0x0000000000000000000000000000000000000000";
|
|
685
|
+
declare class NullSigner implements ISigner {
|
|
686
|
+
readonly address: "0x0000000000000000000000000000000000000000";
|
|
687
|
+
signHash(_hash: `0x${string}`): Promise<`0x${string}`>;
|
|
688
|
+
}
|
|
689
|
+
declare class ViemSigner implements ISigner {
|
|
690
|
+
private readonly account;
|
|
691
|
+
constructor(privateKey: `0x${string}`);
|
|
692
|
+
get address(): `0x${string}`;
|
|
693
|
+
signHash(hash: `0x${string}`): Promise<`0x${string}`>;
|
|
694
|
+
}
|
|
590
695
|
|
|
591
696
|
/**
|
|
592
697
|
* Cross-platform JWT keystore for the broker daemon.
|
|
@@ -681,13 +786,38 @@ interface BrokerLogEvent {
|
|
|
681
786
|
* keystore, returns the response object. Easy to unit-test without
|
|
682
787
|
* spawning a socket.
|
|
683
788
|
*/
|
|
684
|
-
|
|
789
|
+
interface HandleBrokerRequestOptions {
|
|
790
|
+
/**
|
|
791
|
+
* Whether the daemon was booted with a session-key private half. Drives
|
|
792
|
+
* the `hello.hasSessionKey` field + the `session_key_unavailable` error
|
|
793
|
+
* mapping. Defaults to `true` when omitted (current-runtime back-compat
|
|
794
|
+
* — older callers that don't know about the read-only posture still
|
|
795
|
+
* get the pre-0.3.0 behaviour).
|
|
796
|
+
*/
|
|
797
|
+
hasSessionKey?: boolean;
|
|
798
|
+
/**
|
|
799
|
+
* Effective backend + dashboard URLs the daemon resolved from its env
|
|
800
|
+
* at boot. Surfaced via `hello.effectiveConfig` so the `--from-daemon`
|
|
801
|
+
* login path can use the daemon's view of these URLs (not the CLI's).
|
|
802
|
+
*/
|
|
803
|
+
effectiveConfig?: {
|
|
804
|
+
readonly backendBaseUrl: string;
|
|
805
|
+
readonly dashboardBaseUrl: string;
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
declare function handleBrokerRequest(req: BrokerRequest, signer: ISigner, keystore: IKeystore, nowSec?: () => number, options?: HandleBrokerRequestOptions): Promise<BrokerResponse>;
|
|
685
809
|
declare class BrokerDaemon {
|
|
686
810
|
private readonly server;
|
|
687
811
|
private readonly signer;
|
|
688
812
|
private readonly log;
|
|
689
813
|
private readonly config;
|
|
690
814
|
private keystore;
|
|
815
|
+
/**
|
|
816
|
+
* Whether a session-key private half is actually loaded. `false` =
|
|
817
|
+
* daemon booted in read-only posture (no `MUHAVEN_BROKER_SESSION_KEY`
|
|
818
|
+
* at env-load time) and uses a `NullSigner` whose `signHash` throws.
|
|
819
|
+
*/
|
|
820
|
+
private readonly hasSessionKey;
|
|
691
821
|
constructor(options: BrokerDaemonOptions);
|
|
692
822
|
start(): Promise<string>;
|
|
693
823
|
stop(): Promise<void>;
|
|
@@ -695,4 +825,4 @@ declare class BrokerDaemon {
|
|
|
695
825
|
private runAndRespond;
|
|
696
826
|
}
|
|
697
827
|
|
|
698
|
-
export { BROKER_PROTOCOL_VERSION, BackendClient, BackendError, type BackendErrorCode, BrokerClient, BrokerClientError, type BrokerClientErrorCode, BrokerDaemon, type BrokerDaemonOptions, type BrokerRequest, type BrokerResponse, type BrokerRuntimeConfig, DeviceFlowAbortedError, DeviceFlowClient, type DeviceFlowEvent, type IKeystore, JwtSource, type KeystoreBackend, KeystoreError, type McpRuntimeConfig, NoJwtAvailableError, type RunMcpStdioCliOptions, TOOL_DESCRIPTORS, type ToolDescriptor, type ToolEntry, type ToolHashEntry, buildMcpServer, buildToolHashTable, defaultBrokerEndpoint, fullToolRegistry, handleBrokerRequest, hashToolDescriptor, loadBrokerConfig, loadMcpConfig, openKeystore, parseBrokerRequest, registryForReadOnly, runMcpStdioCli, selectRegistry, serializeResponse, verifyDescriptorAgainstPin };
|
|
828
|
+
export { BROKER_PROTOCOL_VERSION, BackendClient, BackendError, type BackendErrorCode, BrokerClient, BrokerClientError, type BrokerClientErrorCode, BrokerDaemon, type BrokerDaemonOptions, type BrokerRequest, type BrokerResponse, type BrokerRuntimeConfig, DeviceFlowAbortedError, DeviceFlowClient, type DeviceFlowEvent, type HandleBrokerRequestOptions, type IKeystore, type ISigner, JwtSource, type KeystoreBackend, KeystoreError, type McpRuntimeConfig, MissingSessionKeyError, NoJwtAvailableError, NullSigner, type RunMcpStdioCliOptions, SERVER_VERSION, TOOL_DESCRIPTORS, type ToolDescriptor, type ToolEntry, type ToolHashEntry, ViemSigner, ZERO_ADDRESS, buildMcpServer, buildToolHashTable, defaultBrokerEndpoint, fullToolRegistry, handleBrokerRequest, hashToolDescriptor, loadBrokerConfig, loadMcpConfig, openKeystore, parseBrokerRequest, registryForReadOnly, runMcpStdioCli, selectRegistry, serializeResponse, verifyDescriptorAgainstPin };
|