@haven_ai/connect 0.1.1-alpha → 0.1.3-alpha
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/README.md +4 -4
- package/dist/cli.cjs +724 -106
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +726 -108
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +724 -106
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -2
- package/dist/index.d.ts +43 -2
- package/dist/index.js +726 -108
- package/dist/index.js.map +1 -1
- package/package.json +8 -6
package/dist/index.cjs
CHANGED
|
@@ -7,6 +7,7 @@ var os = require('os');
|
|
|
7
7
|
var path = require('path');
|
|
8
8
|
var child_process = require('child_process');
|
|
9
9
|
var util = require('util');
|
|
10
|
+
var mcp = require('@haven_ai/mcp');
|
|
10
11
|
var signer = require('@haven_ai/signer');
|
|
11
12
|
|
|
12
13
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -50,10 +51,13 @@ function createConnectApiClient(baseUrl, fetchImpl = fetch) {
|
|
|
50
51
|
body: JSON.stringify({
|
|
51
52
|
runtime: input.runtime,
|
|
52
53
|
connector_version: input.connectorVersion,
|
|
54
|
+
runtime_mcp_mode: input.runtimeMcpMode,
|
|
53
55
|
hosted_mcp_configured: input.hostedMcpConfigured,
|
|
54
56
|
local_signer_configured: input.localSignerConfigured,
|
|
57
|
+
local_mcp_configured: input.localMcpConfigured,
|
|
55
58
|
credential_files_written: input.credentialFilesWritten,
|
|
56
59
|
signer_acknowledged: input.signerAcknowledged,
|
|
60
|
+
local_mcp_acknowledged: input.localMcpAcknowledged,
|
|
57
61
|
activation_command_available: input.activationCommandAvailable,
|
|
58
62
|
probe_result: input.probeResult,
|
|
59
63
|
restart_required: input.restartRequired,
|
|
@@ -136,6 +140,7 @@ async function writeCredentialFiles(input) {
|
|
|
136
140
|
signerPath,
|
|
137
141
|
{
|
|
138
142
|
delegate_key: input.delegateKey,
|
|
143
|
+
delegate_address: input.delegateAddress,
|
|
139
144
|
agent_id: input.agentId,
|
|
140
145
|
safe_address: input.safeAddress,
|
|
141
146
|
chain_id: input.chainId,
|
|
@@ -155,6 +160,7 @@ async function writeCredentialFiles(input) {
|
|
|
155
160
|
network: input.network,
|
|
156
161
|
api_url: input.apiUrl,
|
|
157
162
|
hosted_mcp_url: input.hostedMcpUrl,
|
|
163
|
+
agent_budget: input.agentBudget,
|
|
158
164
|
note: "Haven API key identifies the agent only. It cannot spend without the local signer key and on-chain Haven wallet rules."
|
|
159
165
|
},
|
|
160
166
|
input.warn
|
|
@@ -201,9 +207,50 @@ async function restrictPermissions(path, mode, warn) {
|
|
|
201
207
|
);
|
|
202
208
|
}
|
|
203
209
|
}
|
|
210
|
+
var MCP_RUNTIME_MANIFEST = {
|
|
211
|
+
mcpPackage: "@haven_ai/mcp",
|
|
212
|
+
mcpVersion: mcp.MCP_VERSION,
|
|
213
|
+
sdkPackage: "@haven_ai/sdk",
|
|
214
|
+
sdkVersion: "0.1.6",
|
|
215
|
+
signerPackage: "@haven_ai/signer",
|
|
216
|
+
signerVersion: "0.1.0-alpha",
|
|
217
|
+
minimumNodeVersion: "20.0.0",
|
|
218
|
+
supportedClients: ["codex-cli", "codex-desktop", "claude-code"],
|
|
219
|
+
requiredTools: [
|
|
220
|
+
"haven_quote_x402",
|
|
221
|
+
"haven_pay_x402_quote",
|
|
222
|
+
"haven_resume_x402_payment",
|
|
223
|
+
"haven_quote_mpp",
|
|
224
|
+
"haven_pay_mpp_challenge",
|
|
225
|
+
"haven_resume_mpp_payment",
|
|
226
|
+
"haven_get_payment_status",
|
|
227
|
+
"haven_get_resume_state",
|
|
228
|
+
"haven_get_agent",
|
|
229
|
+
"haven_get_allowances",
|
|
230
|
+
"haven_list_receipts"
|
|
231
|
+
]
|
|
232
|
+
};
|
|
233
|
+
function mcpPackageSpec() {
|
|
234
|
+
return `${MCP_RUNTIME_MANIFEST.mcpPackage}@${MCP_RUNTIME_MANIFEST.mcpVersion}`;
|
|
235
|
+
}
|
|
236
|
+
function sdkPackageSpec() {
|
|
237
|
+
return `${MCP_RUNTIME_MANIFEST.sdkPackage}@${MCP_RUNTIME_MANIFEST.sdkVersion}`;
|
|
238
|
+
}
|
|
239
|
+
function signerPackageSpec() {
|
|
240
|
+
return `${MCP_RUNTIME_MANIFEST.signerPackage}@${MCP_RUNTIME_MANIFEST.signerVersion}`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/config-writers.ts
|
|
244
|
+
var InvalidCodexTomlError = class extends Error {
|
|
245
|
+
constructor(message) {
|
|
246
|
+
super(message);
|
|
247
|
+
this.name = "InvalidCodexTomlError";
|
|
248
|
+
}
|
|
249
|
+
};
|
|
204
250
|
async function writeRuntimeConfig(input) {
|
|
205
251
|
switch (input.runtime) {
|
|
206
252
|
case "codex-cli":
|
|
253
|
+
case "codex-desktop":
|
|
207
254
|
return writeCodexConfig(input);
|
|
208
255
|
case "cursor":
|
|
209
256
|
return writeJsonRuntimeConfig(input, cursorConfigPath(input.homeDir), "mcpServers");
|
|
@@ -215,6 +262,8 @@ async function writeRuntimeConfig(input) {
|
|
|
215
262
|
return {
|
|
216
263
|
hostedConfigured: false,
|
|
217
264
|
signerConfigured: false,
|
|
265
|
+
localMcpConfigured: false,
|
|
266
|
+
runtimeMcpMode: "manual",
|
|
218
267
|
target: "manual runtime setup",
|
|
219
268
|
changed: false,
|
|
220
269
|
restartRequired: true,
|
|
@@ -256,22 +305,21 @@ function mergeJsonMcpConfig(existingJson, serverRoot, hostedServer, signerServer
|
|
|
256
305
|
return `${JSON.stringify(config, null, 2)}
|
|
257
306
|
`;
|
|
258
307
|
}
|
|
259
|
-
function mergeCodexToml(existingToml,
|
|
260
|
-
let next =
|
|
308
|
+
function mergeCodexToml(existingToml, localMcpCommand) {
|
|
309
|
+
let next = removeTomlTableTree(removeTomlTableTree(existingToml, "mcp_servers.haven"), "mcp_servers.haven_signer");
|
|
261
310
|
next = next.trimEnd();
|
|
262
311
|
const block = [
|
|
263
312
|
"[mcp_servers.haven]",
|
|
264
|
-
`
|
|
265
|
-
|
|
266
|
-
""
|
|
267
|
-
"[mcp_servers.haven_signer]",
|
|
268
|
-
'command = "npx"',
|
|
269
|
-
`args = ["-y", ${tomlString(signerPackageName())}, "--credentials", ${tomlString(signerPath)}]`
|
|
313
|
+
`command = ${tomlString(localMcpCommand)}`,
|
|
314
|
+
"args = []",
|
|
315
|
+
"startup_timeout_sec = 120"
|
|
270
316
|
].join("\n");
|
|
271
|
-
|
|
317
|
+
validateCodexToml(block, "Generated Codex Haven config");
|
|
318
|
+
const merged = `${next ? `${next}
|
|
272
319
|
|
|
273
320
|
` : ""}${block}
|
|
274
321
|
`;
|
|
322
|
+
return merged;
|
|
275
323
|
}
|
|
276
324
|
async function writeJsonRuntimeConfig(input, target, serverRoot) {
|
|
277
325
|
try {
|
|
@@ -286,6 +334,8 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
|
|
|
286
334
|
return {
|
|
287
335
|
hostedConfigured: true,
|
|
288
336
|
signerConfigured: true,
|
|
337
|
+
localMcpConfigured: false,
|
|
338
|
+
runtimeMcpMode: "hosted_plus_signer",
|
|
289
339
|
target: configTargetLabel(input.runtime),
|
|
290
340
|
changed: existing !== merged,
|
|
291
341
|
restartRequired: input.runtime === "claude-desktop",
|
|
@@ -295,6 +345,8 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
|
|
|
295
345
|
return {
|
|
296
346
|
hostedConfigured: false,
|
|
297
347
|
signerConfigured: false,
|
|
348
|
+
localMcpConfigured: false,
|
|
349
|
+
runtimeMcpMode: "hosted_plus_signer",
|
|
298
350
|
target: configTargetLabel(input.runtime),
|
|
299
351
|
changed: false,
|
|
300
352
|
restartRequired: true,
|
|
@@ -305,46 +357,38 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
|
|
|
305
357
|
}
|
|
306
358
|
async function writeCodexConfig(input) {
|
|
307
359
|
const target = codexConfigPath(input.homeDir);
|
|
308
|
-
const envTarget = path.join(input.credentialDirectory, "identity.env");
|
|
309
|
-
const launchTarget = path.join(input.credentialDirectory, "start-codex.sh");
|
|
310
360
|
try {
|
|
311
361
|
const existing = await readOptional(target);
|
|
312
|
-
|
|
362
|
+
if (!input.localMcpCommand) {
|
|
363
|
+
throw new Error("local MCP wrapper command is required");
|
|
364
|
+
}
|
|
365
|
+
const merged = mergeCodexToml(existing ?? "", input.localMcpCommand);
|
|
313
366
|
await writeOwnerOnlyText(target, merged);
|
|
314
|
-
await writeOwnerOnlyText(envTarget, `export HAVEN_TOKEN=${shellToken(input.apiKey)}
|
|
315
|
-
`);
|
|
316
|
-
await writeOwnerExecutableText(
|
|
317
|
-
launchTarget,
|
|
318
|
-
[
|
|
319
|
-
"#!/bin/sh",
|
|
320
|
-
"set -eu",
|
|
321
|
-
`. ${shellToken(envTarget)}`,
|
|
322
|
-
'exec codex "$@"',
|
|
323
|
-
""
|
|
324
|
-
].join("\n")
|
|
325
|
-
);
|
|
326
367
|
return {
|
|
327
|
-
hostedConfigured:
|
|
368
|
+
hostedConfigured: false,
|
|
328
369
|
signerConfigured: true,
|
|
329
|
-
|
|
370
|
+
localMcpConfigured: true,
|
|
371
|
+
runtimeMcpMode: "local_stdio",
|
|
372
|
+
target: configTargetLabel(input.runtime),
|
|
330
373
|
changed: existing !== merged,
|
|
331
374
|
restartRequired: true,
|
|
332
|
-
activationCommand: shellToken(launchTarget),
|
|
333
375
|
messages: [
|
|
334
|
-
|
|
335
|
-
"
|
|
336
|
-
`Restart Codex with: ${shellToken(launchTarget)}`
|
|
376
|
+
`Updated local Haven MCP entry in ${configTargetLabel(input.runtime)}.`,
|
|
377
|
+
"After Haven approval, restart Codex normally so it can load Haven tools."
|
|
337
378
|
]
|
|
338
379
|
};
|
|
339
380
|
} catch (err) {
|
|
381
|
+
const invalidToml = err instanceof InvalidCodexTomlError;
|
|
340
382
|
return {
|
|
341
383
|
hostedConfigured: false,
|
|
342
384
|
signerConfigured: false,
|
|
343
|
-
|
|
385
|
+
localMcpConfigured: false,
|
|
386
|
+
runtimeMcpMode: "local_stdio",
|
|
387
|
+
target: configTargetLabel(input.runtime),
|
|
344
388
|
changed: false,
|
|
345
389
|
restartRequired: true,
|
|
346
|
-
messages: [`Could not update
|
|
347
|
-
errorCode: "runtime_config_write_failed"
|
|
390
|
+
messages: [`Could not update ${configTargetLabel(input.runtime)}: ${err instanceof Error ? err.message : String(err)}`],
|
|
391
|
+
errorCode: invalidToml ? "codex_config_invalid" : "runtime_config_write_failed"
|
|
348
392
|
};
|
|
349
393
|
}
|
|
350
394
|
}
|
|
@@ -361,11 +405,6 @@ async function writeOwnerOnlyText(path$1, value) {
|
|
|
361
405
|
await promises.writeFile(path$1, value, { mode: 384 });
|
|
362
406
|
await promises.chmod(path$1, 384).catch(() => void 0);
|
|
363
407
|
}
|
|
364
|
-
async function writeOwnerExecutableText(path$1, value) {
|
|
365
|
-
await promises.mkdir(path.dirname(path$1), { recursive: true, mode: 448 });
|
|
366
|
-
await promises.writeFile(path$1, value, { mode: 448 });
|
|
367
|
-
await promises.chmod(path$1, 448).catch(() => void 0);
|
|
368
|
-
}
|
|
369
408
|
function parseJsonObject(value) {
|
|
370
409
|
const parsed = JSON.parse(value);
|
|
371
410
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
@@ -373,30 +412,194 @@ function parseJsonObject(value) {
|
|
|
373
412
|
}
|
|
374
413
|
return parsed;
|
|
375
414
|
}
|
|
376
|
-
function
|
|
415
|
+
function removeTomlTableTree(toml, table) {
|
|
377
416
|
const lines = toml.split(/\r?\n/);
|
|
378
|
-
const start = `[${table}]`;
|
|
379
417
|
const kept = [];
|
|
380
418
|
let skipping = false;
|
|
381
419
|
for (const line of lines) {
|
|
382
420
|
const trimmed = line.trim();
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (skipping && trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
388
|
-
skipping = false;
|
|
421
|
+
const tableName = tomlTableName(trimmed);
|
|
422
|
+
if (tableName) {
|
|
423
|
+
skipping = tableName === table || tableName.startsWith(`${table}.`);
|
|
424
|
+
if (skipping) continue;
|
|
389
425
|
}
|
|
390
426
|
if (!skipping) kept.push(line);
|
|
391
427
|
}
|
|
392
428
|
return kept.join("\n");
|
|
393
429
|
}
|
|
430
|
+
function tomlTableName(line) {
|
|
431
|
+
if (line.startsWith("[[") && line.endsWith("]]")) return line.slice(2, -2).trim();
|
|
432
|
+
if (line.startsWith("[") && line.endsWith("]")) return line.slice(1, -1).trim();
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
function validateCodexToml(toml, label = "Codex config") {
|
|
436
|
+
const lines = toml.split(/\r?\n/);
|
|
437
|
+
let pendingValue = null;
|
|
438
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
439
|
+
const raw = lines[index];
|
|
440
|
+
const line = stripTomlComment(raw).trim();
|
|
441
|
+
if (!line) continue;
|
|
442
|
+
if (pendingValue) {
|
|
443
|
+
pendingValue.value = `${pendingValue.value}
|
|
444
|
+
${line}`;
|
|
445
|
+
if (hasBalancedTomlContainers(pendingValue.value)) {
|
|
446
|
+
if (!isTomlValue(pendingValue.value)) {
|
|
447
|
+
throw new InvalidCodexTomlError(`${label} has invalid TOML near line ${pendingValue.line}.`);
|
|
448
|
+
}
|
|
449
|
+
pendingValue = null;
|
|
450
|
+
}
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (isTomlTable(line)) continue;
|
|
454
|
+
const equalsIndex = line.indexOf("=");
|
|
455
|
+
if (equalsIndex <= 0) {
|
|
456
|
+
throw new InvalidCodexTomlError(`${label} has invalid TOML near line ${index + 1}.`);
|
|
457
|
+
}
|
|
458
|
+
const key = line.slice(0, equalsIndex).trim();
|
|
459
|
+
const value = line.slice(equalsIndex + 1).trim();
|
|
460
|
+
if (!isTomlKey(key)) {
|
|
461
|
+
throw new InvalidCodexTomlError(`${label} has invalid TOML near line ${index + 1}.`);
|
|
462
|
+
}
|
|
463
|
+
if (startsTomlContainer(value) && !hasBalancedTomlContainers(value)) {
|
|
464
|
+
pendingValue = { value, line: index + 1 };
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
if (!isTomlValue(value)) {
|
|
468
|
+
throw new InvalidCodexTomlError(`${label} has invalid TOML near line ${index + 1}.`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (pendingValue) {
|
|
472
|
+
throw new InvalidCodexTomlError(`${label} has invalid TOML near line ${pendingValue.line}.`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function isTomlTable(line) {
|
|
476
|
+
const table = tomlTableName(line);
|
|
477
|
+
return Boolean(table && splitTomlDottedKey(table).every(isTomlKeyPart));
|
|
478
|
+
}
|
|
479
|
+
function isTomlKey(value) {
|
|
480
|
+
return splitTomlDottedKey(value).every(isTomlKeyPart);
|
|
481
|
+
}
|
|
482
|
+
function splitTomlDottedKey(value) {
|
|
483
|
+
const parts = [];
|
|
484
|
+
let current = "";
|
|
485
|
+
let quote = null;
|
|
486
|
+
let escaped = false;
|
|
487
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
488
|
+
const char = value[i];
|
|
489
|
+
if (quote) {
|
|
490
|
+
current += char;
|
|
491
|
+
if (quote === '"' && char === "\\" && !escaped) {
|
|
492
|
+
escaped = true;
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (char === quote && !escaped) quote = null;
|
|
496
|
+
escaped = false;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (char === '"' || char === "'") {
|
|
500
|
+
quote = char;
|
|
501
|
+
current += char;
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (char === ".") {
|
|
505
|
+
parts.push(current.trim());
|
|
506
|
+
current = "";
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
current += char;
|
|
510
|
+
}
|
|
511
|
+
parts.push(current.trim());
|
|
512
|
+
return quote ? [] : parts;
|
|
513
|
+
}
|
|
514
|
+
function isTomlKeyPart(value) {
|
|
515
|
+
return isTomlBareKey(value) || isTomlQuotedString(value);
|
|
516
|
+
}
|
|
517
|
+
function isTomlBareKey(value) {
|
|
518
|
+
return /^[A-Za-z0-9_-]+$/.test(value);
|
|
519
|
+
}
|
|
520
|
+
function isTomlValue(value) {
|
|
521
|
+
if (!value) return false;
|
|
522
|
+
if (isTomlQuotedString(value)) return true;
|
|
523
|
+
if (/^(true|false)$/i.test(value)) return true;
|
|
524
|
+
if (/^[+-]?(?:inf|nan)$/i.test(value)) return true;
|
|
525
|
+
if (/^[+-]?(?:0|[1-9][0-9_]*)(?:\.[0-9_]+)?(?:[eE][+-]?[0-9_]+)?$/.test(value)) return true;
|
|
526
|
+
if (/^\d{4}-\d{2}-\d{2}(?:[Tt ][0-9:.+-Zz]+)?$/.test(value)) return true;
|
|
527
|
+
if (value.startsWith("[") && value.endsWith("]") || value.startsWith("{") && value.endsWith("}")) {
|
|
528
|
+
return hasBalancedTomlContainers(value);
|
|
529
|
+
}
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
function startsTomlContainer(value) {
|
|
533
|
+
return value.startsWith("[") || value.startsWith("{");
|
|
534
|
+
}
|
|
535
|
+
function isTomlQuotedString(value) {
|
|
536
|
+
if (value.startsWith('"""') || value.startsWith("'''")) {
|
|
537
|
+
const marker = value.slice(0, 3);
|
|
538
|
+
return value.length >= 6 && value.endsWith(marker);
|
|
539
|
+
}
|
|
540
|
+
if ((!value.startsWith('"') || !value.endsWith('"')) && (!value.startsWith("'") || !value.endsWith("'"))) {
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
return hasBalancedTomlContainers(value);
|
|
544
|
+
}
|
|
545
|
+
function stripTomlComment(value) {
|
|
546
|
+
let quote = null;
|
|
547
|
+
let escaped = false;
|
|
548
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
549
|
+
const char = value[i];
|
|
550
|
+
if (quote) {
|
|
551
|
+
if (quote === '"' && char === "\\" && !escaped) {
|
|
552
|
+
escaped = true;
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
if (char === quote && !escaped) quote = null;
|
|
556
|
+
escaped = false;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (char === '"' || char === "'") {
|
|
560
|
+
quote = char;
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
if (char === "#") return value.slice(0, i);
|
|
564
|
+
}
|
|
565
|
+
return value;
|
|
566
|
+
}
|
|
567
|
+
function hasBalancedTomlContainers(value) {
|
|
568
|
+
const stack = [];
|
|
569
|
+
let quote = null;
|
|
570
|
+
let escaped = false;
|
|
571
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
572
|
+
const char = value[i];
|
|
573
|
+
if (quote) {
|
|
574
|
+
if (quote === '"' && char === "\\" && !escaped) {
|
|
575
|
+
escaped = true;
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
if (char === quote && !escaped) quote = null;
|
|
579
|
+
escaped = false;
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
if (char === '"' || char === "'") {
|
|
583
|
+
quote = char;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
if (char === "[" || char === "{") {
|
|
587
|
+
stack.push(char);
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
if (char === "]") {
|
|
591
|
+
if (stack.pop() !== "[") return false;
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
if (char === "}") {
|
|
595
|
+
if (stack.pop() !== "{") return false;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return stack.length === 0 && quote === null;
|
|
599
|
+
}
|
|
394
600
|
function tomlString(value) {
|
|
395
601
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
396
602
|
}
|
|
397
|
-
function shellToken(value) {
|
|
398
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
399
|
-
}
|
|
400
603
|
function cursorConfigPath(homeDir = os.homedir()) {
|
|
401
604
|
return path.resolve(homeDir, ".cursor", "mcp.json");
|
|
402
605
|
}
|
|
@@ -421,6 +624,10 @@ function claudeDesktopConfigPath(homeDir = os.homedir()) {
|
|
|
421
624
|
}
|
|
422
625
|
function configTargetLabel(runtime) {
|
|
423
626
|
switch (runtime) {
|
|
627
|
+
case "codex-cli":
|
|
628
|
+
return "Codex CLI config";
|
|
629
|
+
case "codex-desktop":
|
|
630
|
+
return "Codex Desktop config";
|
|
424
631
|
case "cursor":
|
|
425
632
|
return "Cursor MCP config";
|
|
426
633
|
case "vscode":
|
|
@@ -432,7 +639,83 @@ function configTargetLabel(runtime) {
|
|
|
432
639
|
}
|
|
433
640
|
}
|
|
434
641
|
function signerPackageName() {
|
|
435
|
-
return
|
|
642
|
+
return signerPackageSpec();
|
|
643
|
+
}
|
|
644
|
+
async function acknowledgeLocalMcpConsent(identityPath, signerPath, log) {
|
|
645
|
+
try {
|
|
646
|
+
const input = await buildLocalMcpConsentInput(identityPath, signerPath);
|
|
647
|
+
const decision = await mcp.ensureConsent(input, {
|
|
648
|
+
credentialsPath: identityPath,
|
|
649
|
+
writeAck: true,
|
|
650
|
+
out: log ? { write: (chunk) => writeLogChunk(log, chunk) } : void 0
|
|
651
|
+
});
|
|
652
|
+
return {
|
|
653
|
+
acknowledged: decision.ok,
|
|
654
|
+
hash: decision.hash,
|
|
655
|
+
reason: decision.reason
|
|
656
|
+
};
|
|
657
|
+
} catch (err) {
|
|
658
|
+
return {
|
|
659
|
+
acknowledged: false,
|
|
660
|
+
error: err instanceof Error ? err.message : String(err)
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
async function getLocalMcpConsentStatus(identityPath, signerPath) {
|
|
665
|
+
try {
|
|
666
|
+
const input = await buildLocalMcpConsentInput(identityPath, signerPath);
|
|
667
|
+
const hash = mcp.computeConsentHash(input);
|
|
668
|
+
const stored = await readLocalMcpAckFile(localMcpAckPath(identityPath));
|
|
669
|
+
if (stored === hash) {
|
|
670
|
+
return { acknowledged: true, hash, reason: "ack_file_match" };
|
|
671
|
+
}
|
|
672
|
+
return {
|
|
673
|
+
acknowledged: false,
|
|
674
|
+
hash,
|
|
675
|
+
reason: stored ? "ack_file_mismatch" : "ack_file_missing"
|
|
676
|
+
};
|
|
677
|
+
} catch (err) {
|
|
678
|
+
return {
|
|
679
|
+
acknowledged: false,
|
|
680
|
+
error: err instanceof Error ? err.message : String(err)
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
function localMcpAckPath(identityPath) {
|
|
685
|
+
return path.resolve(`${identityPath}.ack.json`);
|
|
686
|
+
}
|
|
687
|
+
async function buildLocalMcpConsentInput(identityPath, signerPath) {
|
|
688
|
+
const credentials = await mcp.loadCredentials({ identityPath, signerPath });
|
|
689
|
+
const unavailableDuringSetup = {
|
|
690
|
+
getAllowances: async () => {
|
|
691
|
+
throw new Error("Haven approval is not complete yet.");
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
return mcp.consentInputFromClient(
|
|
695
|
+
unavailableDuringSetup,
|
|
696
|
+
{
|
|
697
|
+
apiKey: credentials.apiKey,
|
|
698
|
+
apiUrl: credentials.apiUrl,
|
|
699
|
+
agentId: credentials.agentId,
|
|
700
|
+
safeAddress: credentials.safeAddress,
|
|
701
|
+
delegateAddress: credentials.delegateAddress,
|
|
702
|
+
chainId: credentials.chainId,
|
|
703
|
+
allowanceSummary: credentials.allowanceSummary
|
|
704
|
+
},
|
|
705
|
+
mcp.registeredToolNames()
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
async function readLocalMcpAckFile(path) {
|
|
709
|
+
try {
|
|
710
|
+
const parsed = JSON.parse(await promises.readFile(path, "utf8"));
|
|
711
|
+
return typeof parsed.ack === "string" ? parsed.ack : null;
|
|
712
|
+
} catch {
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
function writeLogChunk(log, chunk) {
|
|
717
|
+
const message = String(chunk).trimEnd();
|
|
718
|
+
if (message) log(message);
|
|
436
719
|
}
|
|
437
720
|
async function probeHostedMcpTools(apiKey, hostedMcpUrl, fetchImpl = fetch) {
|
|
438
721
|
let response;
|
|
@@ -470,6 +753,72 @@ async function probeLocalSignerCredential(signerPath) {
|
|
|
470
753
|
return false;
|
|
471
754
|
}
|
|
472
755
|
}
|
|
756
|
+
async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4) {
|
|
757
|
+
return new Promise((resolve6) => {
|
|
758
|
+
const child = child_process.spawn(command, args, { stdio: ["pipe", "pipe", "ignore"] });
|
|
759
|
+
let stdout = "";
|
|
760
|
+
let settled = false;
|
|
761
|
+
let sawInitialize = false;
|
|
762
|
+
const finish = (result) => {
|
|
763
|
+
if (settled) return;
|
|
764
|
+
settled = true;
|
|
765
|
+
clearTimeout(timeout);
|
|
766
|
+
child.kill();
|
|
767
|
+
resolve6(result);
|
|
768
|
+
};
|
|
769
|
+
const timeout = setTimeout(() => finish({ status: "timeout" }), timeoutMs);
|
|
770
|
+
child.on("error", () => finish({ status: "process_error" }));
|
|
771
|
+
child.on("exit", (code) => {
|
|
772
|
+
if (!settled && code !== 0) finish({ status: "process_error" });
|
|
773
|
+
});
|
|
774
|
+
child.stdout.on("data", (chunk) => {
|
|
775
|
+
stdout += chunk.toString("utf8");
|
|
776
|
+
const lines = stdout.split(/\r?\n/);
|
|
777
|
+
stdout = lines.pop() ?? "";
|
|
778
|
+
for (const line of lines) {
|
|
779
|
+
const trimmed = line.trim();
|
|
780
|
+
if (!trimmed) continue;
|
|
781
|
+
let payload;
|
|
782
|
+
try {
|
|
783
|
+
payload = JSON.parse(trimmed);
|
|
784
|
+
} catch {
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
if (payload.error) {
|
|
788
|
+
finish({ status: "bad_response" });
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
if (payload.id === 1 && !sawInitialize) {
|
|
792
|
+
sawInitialize = true;
|
|
793
|
+
writeJsonRpc(child, { jsonrpc: "2.0", method: "notifications/initialized", params: {} });
|
|
794
|
+
writeJsonRpc(child, { jsonrpc: "2.0", id: 2, method: "tools/list", params: {} });
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
if (payload.id === 2) {
|
|
798
|
+
const tools = payload.result?.tools;
|
|
799
|
+
const toolNames = Array.isArray(tools) ? tools.map((tool) => tool && typeof tool === "object" && "name" in tool ? tool.name : void 0).filter((name) => typeof name === "string") : [];
|
|
800
|
+
const missing = requiredTools.filter((name) => !toolNames.includes(name));
|
|
801
|
+
finish({ status: missing.length === 0 ? "ok" : "missing_tools", toolNames });
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
writeJsonRpc(child, {
|
|
807
|
+
jsonrpc: "2.0",
|
|
808
|
+
id: 1,
|
|
809
|
+
method: "initialize",
|
|
810
|
+
params: {
|
|
811
|
+
protocolVersion: "2025-06-18",
|
|
812
|
+
capabilities: {},
|
|
813
|
+
clientInfo: { name: "haven-connect-probe", version: "0.0.0" }
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
function writeJsonRpc(child, payload) {
|
|
819
|
+
child.stdin?.write(`${JSON.stringify(payload)}
|
|
820
|
+
`);
|
|
821
|
+
}
|
|
473
822
|
function parseJsonRpcPayload(raw) {
|
|
474
823
|
const trimmed = raw.trim();
|
|
475
824
|
if (!trimmed) return null;
|
|
@@ -498,6 +847,161 @@ async function fetchWithTimeout(fetchImpl, url, init) {
|
|
|
498
847
|
clearTimeout(timeout);
|
|
499
848
|
}
|
|
500
849
|
}
|
|
850
|
+
var execFileAsync = util.promisify(child_process.execFile);
|
|
851
|
+
var UnsupportedNodeVersionError = class extends Error {
|
|
852
|
+
code = "local_mcp_unsupported_node_version";
|
|
853
|
+
constructor(nodeVersion, minimumNodeVersion) {
|
|
854
|
+
super(`Node.js ${nodeVersion} is not supported. Haven local MCP requires Node.js >=${minimumNodeVersion}.`);
|
|
855
|
+
this.name = "UnsupportedNodeVersionError";
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
async function prepareLocalMcpRuntime(input, deps = {}) {
|
|
859
|
+
assertSupportedNodeVersion(input.nodeVersion);
|
|
860
|
+
const homeDir = input.homeDir ?? os.homedir();
|
|
861
|
+
const runtimeDirectory = path.resolve(homeDir, ".haven", "mcp-runtime", MCP_RUNTIME_MANIFEST.mcpVersion);
|
|
862
|
+
const npmCacheDirectory = path.resolve(homeDir, ".haven", "npm-cache");
|
|
863
|
+
const cliPath = path.join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "dist", "cli.js");
|
|
864
|
+
const messages = [];
|
|
865
|
+
await promises.mkdir(runtimeDirectory, { recursive: true, mode: 448 });
|
|
866
|
+
await promises.chmod(runtimeDirectory, 448).catch(() => void 0);
|
|
867
|
+
await promises.mkdir(npmCacheDirectory, { recursive: true, mode: 448 });
|
|
868
|
+
await promises.chmod(npmCacheDirectory, 448).catch(() => void 0);
|
|
869
|
+
if (await installedRuntimeMatches(runtimeDirectory, cliPath)) {
|
|
870
|
+
messages.push(`Using existing local Haven MCP runtime ${mcpPackageSpec()}.`);
|
|
871
|
+
} else {
|
|
872
|
+
await installRuntimePackages(runtimeDirectory, npmCacheDirectory, deps.runCommand);
|
|
873
|
+
messages.push(`Installed local Haven MCP runtime ${mcpPackageSpec()}.`);
|
|
874
|
+
}
|
|
875
|
+
await assertFileExists(cliPath, "local Haven MCP CLI");
|
|
876
|
+
const wrapperPath = path.join(input.credentialDirectory, "bin", "haven-mcp");
|
|
877
|
+
await writeWrapper({
|
|
878
|
+
wrapperPath,
|
|
879
|
+
cliPath,
|
|
880
|
+
identityPath: input.identityPath,
|
|
881
|
+
signerPath: input.signerPath
|
|
882
|
+
});
|
|
883
|
+
await writeRuntimeSidecar({
|
|
884
|
+
path: path.join(input.credentialDirectory, "mcp-runtime.json"),
|
|
885
|
+
wrapperPath,
|
|
886
|
+
runtimeDirectory,
|
|
887
|
+
npmCacheDirectory,
|
|
888
|
+
cliPath
|
|
889
|
+
});
|
|
890
|
+
messages.push(`Prepared stable local Haven MCP wrapper: ${wrapperPath}`);
|
|
891
|
+
return {
|
|
892
|
+
command: wrapperPath,
|
|
893
|
+
args: [],
|
|
894
|
+
wrapperPath,
|
|
895
|
+
runtimeDirectory,
|
|
896
|
+
npmCacheDirectory,
|
|
897
|
+
cliPath,
|
|
898
|
+
messages
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
function assertSupportedNodeVersion(nodeVersion = process.versions.node, minimumNodeVersion = MCP_RUNTIME_MANIFEST.minimumNodeVersion) {
|
|
902
|
+
if (compareNodeVersions(nodeVersion, minimumNodeVersion) < 0) {
|
|
903
|
+
throw new UnsupportedNodeVersionError(nodeVersion, minimumNodeVersion);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
function compareNodeVersions(left, right) {
|
|
907
|
+
const leftParts = parseNodeVersion(left);
|
|
908
|
+
const rightParts = parseNodeVersion(right);
|
|
909
|
+
for (let i = 0; i < 3; i += 1) {
|
|
910
|
+
if (leftParts[i] !== rightParts[i]) return leftParts[i] > rightParts[i] ? 1 : -1;
|
|
911
|
+
}
|
|
912
|
+
return 0;
|
|
913
|
+
}
|
|
914
|
+
function parseNodeVersion(value) {
|
|
915
|
+
const match = value.trim().match(/^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
|
916
|
+
if (!match) return [0, 0, 0];
|
|
917
|
+
return [
|
|
918
|
+
Number(match[1] ?? 0),
|
|
919
|
+
Number(match[2] ?? 0),
|
|
920
|
+
Number(match[3] ?? 0)
|
|
921
|
+
];
|
|
922
|
+
}
|
|
923
|
+
async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, runCommand) {
|
|
924
|
+
const args = [
|
|
925
|
+
"install",
|
|
926
|
+
"--prefix",
|
|
927
|
+
runtimeDirectory,
|
|
928
|
+
"--cache",
|
|
929
|
+
npmCacheDirectory,
|
|
930
|
+
"--no-audit",
|
|
931
|
+
"--no-fund",
|
|
932
|
+
"--omit=dev",
|
|
933
|
+
mcpPackageSpec(),
|
|
934
|
+
sdkPackageSpec()
|
|
935
|
+
];
|
|
936
|
+
try {
|
|
937
|
+
if (runCommand) await runCommand("npm", args);
|
|
938
|
+
else await execFileAsync("npm", args, { timeout: 12e4, maxBuffer: 1024 * 1024 });
|
|
939
|
+
} catch (err) {
|
|
940
|
+
throw new Error(`Could not install local Haven MCP runtime ${mcpPackageSpec()}: ${err instanceof Error ? err.message : String(err)}`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
async function installedRuntimeMatches(runtimeDirectory, cliPath) {
|
|
944
|
+
try {
|
|
945
|
+
await assertFileExists(cliPath, "local Haven MCP CLI");
|
|
946
|
+
const [mcpPackage, sdkPackage] = await Promise.all([
|
|
947
|
+
readPackageJson(path.join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "package.json")),
|
|
948
|
+
readPackageJson(path.join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
|
|
949
|
+
]);
|
|
950
|
+
return mcpPackage.version === MCP_RUNTIME_MANIFEST.mcpVersion && sdkPackage.version === MCP_RUNTIME_MANIFEST.sdkVersion;
|
|
951
|
+
} catch {
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
async function readPackageJson(path) {
|
|
956
|
+
return JSON.parse(await promises.readFile(path, "utf8"));
|
|
957
|
+
}
|
|
958
|
+
async function writeWrapper(input) {
|
|
959
|
+
await promises.mkdir(path.dirname(input.wrapperPath), { recursive: true, mode: 448 });
|
|
960
|
+
await promises.chmod(path.dirname(input.wrapperPath), 448).catch(() => void 0);
|
|
961
|
+
const source = [
|
|
962
|
+
"#!/usr/bin/env node",
|
|
963
|
+
"import { spawn } from 'node:child_process'",
|
|
964
|
+
"",
|
|
965
|
+
`const cliPath = ${JSON.stringify(input.cliPath)}`,
|
|
966
|
+
`const identityPath = ${JSON.stringify(input.identityPath)}`,
|
|
967
|
+
`const signerPath = ${JSON.stringify(input.signerPath)}`,
|
|
968
|
+
"",
|
|
969
|
+
"const child = spawn(process.execPath, [cliPath, '--identity', identityPath, '--signer', signerPath, ...process.argv.slice(2)], {",
|
|
970
|
+
" stdio: 'inherit',",
|
|
971
|
+
"})",
|
|
972
|
+
"",
|
|
973
|
+
"child.on('exit', (code, signal) => {",
|
|
974
|
+
" if (signal) process.kill(process.pid, signal)",
|
|
975
|
+
" else process.exit(code ?? 1)",
|
|
976
|
+
"})",
|
|
977
|
+
""
|
|
978
|
+
].join("\n");
|
|
979
|
+
await promises.writeFile(input.wrapperPath, source, { mode: 448 });
|
|
980
|
+
await promises.chmod(input.wrapperPath, 448).catch(() => void 0);
|
|
981
|
+
}
|
|
982
|
+
async function writeRuntimeSidecar(input) {
|
|
983
|
+
const value = {
|
|
984
|
+
mcp_package: MCP_RUNTIME_MANIFEST.mcpPackage,
|
|
985
|
+
mcp_version: MCP_RUNTIME_MANIFEST.mcpVersion,
|
|
986
|
+
sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
|
|
987
|
+
sdk_version: MCP_RUNTIME_MANIFEST.sdkVersion,
|
|
988
|
+
minimum_node_version: MCP_RUNTIME_MANIFEST.minimumNodeVersion,
|
|
989
|
+
wrapper_path: input.wrapperPath,
|
|
990
|
+
runtime_directory: input.runtimeDirectory,
|
|
991
|
+
npm_cache_directory: input.npmCacheDirectory,
|
|
992
|
+
cli_path: input.cliPath
|
|
993
|
+
};
|
|
994
|
+
await promises.writeFile(input.path, `${JSON.stringify(value, null, 2)}
|
|
995
|
+
`, { mode: 384 });
|
|
996
|
+
await promises.chmod(input.path, 384).catch(() => void 0);
|
|
997
|
+
}
|
|
998
|
+
async function assertFileExists(path, label) {
|
|
999
|
+
try {
|
|
1000
|
+
await promises.access(path);
|
|
1001
|
+
} catch {
|
|
1002
|
+
throw new Error(`Missing ${label}: ${path}`);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
501
1005
|
|
|
502
1006
|
// src/runtime-registry.ts
|
|
503
1007
|
var RUNTIME_PROFILES = {
|
|
@@ -513,6 +1017,12 @@ var RUNTIME_PROFILES = {
|
|
|
513
1017
|
restartMode: "restart-session",
|
|
514
1018
|
canWriteRuntimeConfig: true
|
|
515
1019
|
},
|
|
1020
|
+
"codex-desktop": {
|
|
1021
|
+
id: "codex-desktop",
|
|
1022
|
+
label: "Codex Desktop",
|
|
1023
|
+
restartMode: "restart-session",
|
|
1024
|
+
canWriteRuntimeConfig: true
|
|
1025
|
+
},
|
|
516
1026
|
cursor: {
|
|
517
1027
|
id: "cursor",
|
|
518
1028
|
label: "Cursor",
|
|
@@ -547,6 +1057,12 @@ var RUNTIME_ALIASES = {
|
|
|
547
1057
|
"codex-cli": "codex-cli",
|
|
548
1058
|
codexcli: "codex-cli",
|
|
549
1059
|
"codex_cli": "codex-cli",
|
|
1060
|
+
"codex-desktop": "codex-desktop",
|
|
1061
|
+
"codex_desktop": "codex-desktop",
|
|
1062
|
+
codexdesktop: "codex-desktop",
|
|
1063
|
+
"codex-app": "codex-desktop",
|
|
1064
|
+
"codex_app": "codex-desktop",
|
|
1065
|
+
codexapp: "codex-desktop",
|
|
550
1066
|
cursor: "cursor",
|
|
551
1067
|
vscode: "vscode",
|
|
552
1068
|
"vs-code": "vscode",
|
|
@@ -588,7 +1104,7 @@ async function acknowledgeLocalSignerConsent(signerPath, log) {
|
|
|
588
1104
|
const decision = await signer.ensureSignerConsent(input, {
|
|
589
1105
|
credentialsPath: signerPath,
|
|
590
1106
|
writeAck: true,
|
|
591
|
-
out: log ? { write: (chunk) =>
|
|
1107
|
+
out: log ? { write: (chunk) => writeLogChunk2(log, chunk) } : void 0
|
|
592
1108
|
});
|
|
593
1109
|
return {
|
|
594
1110
|
acknowledged: decision.ok,
|
|
@@ -647,65 +1163,110 @@ async function readSignerAckFile(path) {
|
|
|
647
1163
|
return null;
|
|
648
1164
|
}
|
|
649
1165
|
}
|
|
650
|
-
function
|
|
1166
|
+
function writeLogChunk2(log, chunk) {
|
|
651
1167
|
const message = String(chunk).trimEnd();
|
|
652
1168
|
if (message) log(message);
|
|
653
1169
|
}
|
|
654
1170
|
|
|
655
1171
|
// src/runtime-install.ts
|
|
656
|
-
var
|
|
1172
|
+
var execFileAsync2 = util.promisify(child_process.execFile);
|
|
657
1173
|
async function installRuntime(input, deps = {}) {
|
|
658
1174
|
const runtime = normalizeRuntime(input.runtime, deps.env);
|
|
659
1175
|
const profile = runtimeProfile(runtime, deps.env);
|
|
660
|
-
const
|
|
661
|
-
const
|
|
1176
|
+
const localRuntime = usesLocalMcp(runtime);
|
|
1177
|
+
const consentMessages = [];
|
|
1178
|
+
const localMcpConsent = localRuntime ? await resolveLocalMcpConsent(input, consentMessages) : void 0;
|
|
1179
|
+
const signerConsent = localRuntime ? void 0 : await resolveSignerConsent(input, consentMessages);
|
|
662
1180
|
if (runtime === "other") {
|
|
663
1181
|
const signerCredentialReady2 = await probeLocalSignerCredential(input.signerPath);
|
|
664
|
-
const signerReady = signerCredentialReady2 && signerConsent
|
|
1182
|
+
const signerReady = signerCredentialReady2 && signerConsent?.acknowledged;
|
|
665
1183
|
return {
|
|
666
1184
|
runtime,
|
|
1185
|
+
runtimeMcpMode: "manual",
|
|
667
1186
|
hostedMcpConfigured: false,
|
|
668
1187
|
localSignerConfigured: false,
|
|
1188
|
+
localMcpConfigured: false,
|
|
669
1189
|
probeResult: signerReady ? "manual_runtime_setup_required_local_signer_ready" : "manual_runtime_setup_required_local_signer_unavailable",
|
|
670
1190
|
restartRequired: true,
|
|
671
1191
|
nextUserAction: "return_to_haven_for_wallet_approval_then_configure_runtime",
|
|
672
1192
|
errorCode: "manual_runtime_setup_required",
|
|
673
1193
|
configTarget: "manual runtime setup",
|
|
674
|
-
signerAcknowledged: signerConsent
|
|
1194
|
+
signerAcknowledged: signerConsent?.acknowledged,
|
|
1195
|
+
localMcpAcknowledged: false,
|
|
675
1196
|
messages: [
|
|
676
|
-
...
|
|
1197
|
+
...consentMessages,
|
|
677
1198
|
"Runtime was not recognized. Keep the local credentials and add Haven MCP entries manually after wallet approval."
|
|
678
1199
|
]
|
|
679
1200
|
};
|
|
680
1201
|
}
|
|
681
|
-
|
|
1202
|
+
let localRuntimeInstall;
|
|
1203
|
+
let localRuntimeError;
|
|
1204
|
+
if (localRuntime) {
|
|
1205
|
+
try {
|
|
1206
|
+
localRuntimeInstall = await prepareRuntimeForLocalMcp(input, deps);
|
|
1207
|
+
} catch (err) {
|
|
1208
|
+
localRuntimeError = err;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
if (localRuntimeError) {
|
|
1212
|
+
const errorCode2 = localRuntimePrepareErrorCode(localRuntimeError);
|
|
1213
|
+
return {
|
|
1214
|
+
runtime,
|
|
1215
|
+
runtimeMcpMode: "local_stdio",
|
|
1216
|
+
hostedMcpConfigured: false,
|
|
1217
|
+
localSignerConfigured: false,
|
|
1218
|
+
localMcpConfigured: false,
|
|
1219
|
+
probeResult: errorCode2 === "local_mcp_unsupported_node_version" ? "local_stdio_mcp_unsupported_node_version" : "local_stdio_mcp_runtime_install_failed",
|
|
1220
|
+
restartRequired: true,
|
|
1221
|
+
nextUserAction: nextAction(runtime, profile.restartMode, errorCode2),
|
|
1222
|
+
errorCode: errorCode2,
|
|
1223
|
+
configTarget: profile.label,
|
|
1224
|
+
signerAcknowledged: signerConsent?.acknowledged,
|
|
1225
|
+
localMcpAcknowledged: localMcpConsent?.acknowledged,
|
|
1226
|
+
activationCommand: void 0,
|
|
1227
|
+
messages: [
|
|
1228
|
+
...consentMessages,
|
|
1229
|
+
`Could not prepare local Haven MCP runtime: ${localRuntimeError instanceof Error ? localRuntimeError.message : String(localRuntimeError)}`
|
|
1230
|
+
]
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
const configResult = runtime === "claude-code" ? await configureClaudeCode(deps, localRuntimeInstall?.command ?? "") : await writeRuntimeConfig({
|
|
682
1234
|
runtime,
|
|
683
1235
|
hostedMcpUrl: input.hostedMcpUrl,
|
|
684
1236
|
apiKey: input.apiKey,
|
|
1237
|
+
identityPath: input.identityPath,
|
|
685
1238
|
signerPath: input.signerPath,
|
|
686
1239
|
credentialDirectory: input.credentialDirectory,
|
|
1240
|
+
localMcpCommand: localRuntimeInstall?.command,
|
|
687
1241
|
homeDir: deps.homeDir
|
|
688
1242
|
});
|
|
689
|
-
const
|
|
1243
|
+
const localProbePromise = configResult.runtimeMcpMode === "local_stdio" && localRuntimeInstall ? runLocalMcpProbe(localRuntimeInstall, deps) : Promise.resolve(void 0);
|
|
1244
|
+
const [hostedProbe, signerCredentialReady, localMcpProbe] = await Promise.all([
|
|
690
1245
|
configResult.hostedConfigured ? probeHostedMcpTools(input.apiKey, input.hostedMcpUrl, deps.fetch) : Promise.resolve({ status: "bad_response" }),
|
|
691
|
-
probeLocalSignerCredential(input.signerPath)
|
|
1246
|
+
probeLocalSignerCredential(input.signerPath),
|
|
1247
|
+
localProbePromise
|
|
692
1248
|
]);
|
|
693
1249
|
const hostedOk = configResult.hostedConfigured && hostedProbe.status !== "unauthorized";
|
|
694
|
-
const
|
|
1250
|
+
const localMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialReady && Boolean(localMcpConsent?.acknowledged) && localMcpProbe?.status === "ok";
|
|
1251
|
+
const signerOk = configResult.runtimeMcpMode === "local_stdio" ? localMcpOk : configResult.signerConfigured && signerCredentialReady && Boolean(signerConsent?.acknowledged);
|
|
695
1252
|
const restartRequired = configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env);
|
|
696
|
-
const errorCode = configResult.errorCode ?? signerConsentErrorCode(signerCredentialReady, signerConsent);
|
|
1253
|
+
const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : signerConsentErrorCode(signerCredentialReady, signerConsent));
|
|
1254
|
+
const localProbeMessages = localMcpProbe && localMcpProbe.status !== "ok" ? [`Local Haven MCP handshake failed: ${localMcpProbe.status}.`] : localMcpProbe?.status === "ok" ? ["Verified local Haven MCP tools with a stdio handshake."] : [];
|
|
697
1255
|
return {
|
|
698
1256
|
runtime,
|
|
1257
|
+
runtimeMcpMode: configResult.runtimeMcpMode,
|
|
699
1258
|
hostedMcpConfigured: hostedOk,
|
|
700
1259
|
localSignerConfigured: signerOk,
|
|
701
|
-
|
|
1260
|
+
localMcpConfigured: localMcpOk,
|
|
1261
|
+
probeResult: buildProbeResult(configResult.runtimeMcpMode, configResult.hostedConfigured, hostedProbe.status, signerOk, localMcpOk, localMcpProbe?.status),
|
|
702
1262
|
restartRequired,
|
|
703
1263
|
nextUserAction: nextAction(runtime, profile.restartMode, errorCode),
|
|
704
1264
|
errorCode,
|
|
705
1265
|
configTarget: configResult.target,
|
|
706
|
-
signerAcknowledged: signerConsent
|
|
1266
|
+
signerAcknowledged: signerConsent?.acknowledged,
|
|
1267
|
+
localMcpAcknowledged: localMcpConsent?.acknowledged,
|
|
707
1268
|
activationCommand: configResult.activationCommand,
|
|
708
|
-
messages: [...
|
|
1269
|
+
messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...localProbeMessages]
|
|
709
1270
|
};
|
|
710
1271
|
}
|
|
711
1272
|
function runtimeInstallCapabilities(runtime, env = process.env) {
|
|
@@ -715,45 +1276,41 @@ function runtimeInstallCapabilities(runtime, env = process.env) {
|
|
|
715
1276
|
restartRequired: restartRequiredForRuntime(runtime, env)
|
|
716
1277
|
};
|
|
717
1278
|
}
|
|
718
|
-
async function configureClaudeCode(
|
|
1279
|
+
async function configureClaudeCode(deps, localMcpCommand) {
|
|
719
1280
|
const runCommand = deps.runCommand ?? defaultRunCommand;
|
|
1281
|
+
const serverJson = JSON.stringify({
|
|
1282
|
+
type: "stdio",
|
|
1283
|
+
command: localMcpCommand,
|
|
1284
|
+
args: [],
|
|
1285
|
+
env: {}
|
|
1286
|
+
});
|
|
720
1287
|
try {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
"add",
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
input.hostedMcpUrl,
|
|
728
|
-
"--header",
|
|
729
|
-
`Authorization: Bearer ${input.apiKey}`
|
|
730
|
-
]);
|
|
731
|
-
await runCommand("claude", [
|
|
732
|
-
"mcp",
|
|
733
|
-
"add",
|
|
734
|
-
"haven-signer",
|
|
735
|
-
"--",
|
|
736
|
-
"npx",
|
|
737
|
-
"-y",
|
|
738
|
-
signerPackageName2(),
|
|
739
|
-
"--credentials",
|
|
740
|
-
input.signerPath
|
|
741
|
-
]);
|
|
1288
|
+
if (!localMcpCommand) throw new Error("local MCP wrapper command is required");
|
|
1289
|
+
await runCommand("claude", ["mcp", "add-json", "haven", serverJson, "--scope", "user"]).catch(async () => {
|
|
1290
|
+
await runCommand("claude", ["mcp", "add", "haven", "--scope", "user", "--", localMcpCommand]);
|
|
1291
|
+
});
|
|
1292
|
+
await runCommand("claude", ["mcp", "remove", "haven-signer"]).catch(() => void 0);
|
|
1293
|
+
const verified = await runCommand("claude", ["mcp", "get", "haven"]).then(() => true).catch(() => false);
|
|
742
1294
|
return {
|
|
743
|
-
hostedConfigured:
|
|
1295
|
+
hostedConfigured: false,
|
|
744
1296
|
signerConfigured: true,
|
|
1297
|
+
localMcpConfigured: true,
|
|
1298
|
+
runtimeMcpMode: "local_stdio",
|
|
745
1299
|
target: "Claude Code MCP config",
|
|
746
1300
|
changed: true,
|
|
747
1301
|
restartRequired: true,
|
|
748
1302
|
messages: [
|
|
749
|
-
"Updated Haven MCP
|
|
750
|
-
"
|
|
1303
|
+
"Updated local Haven MCP entry with Claude Code.",
|
|
1304
|
+
...verified ? ["Verified Claude Code MCP entry."] : [],
|
|
1305
|
+
"After Haven approval, restart Claude Code normally so it can load Haven tools."
|
|
751
1306
|
]
|
|
752
1307
|
};
|
|
753
1308
|
} catch (err) {
|
|
754
1309
|
return {
|
|
755
1310
|
hostedConfigured: false,
|
|
756
1311
|
signerConfigured: false,
|
|
1312
|
+
localMcpConfigured: false,
|
|
1313
|
+
runtimeMcpMode: "local_stdio",
|
|
757
1314
|
target: "Claude Code MCP config",
|
|
758
1315
|
changed: false,
|
|
759
1316
|
restartRequired: true,
|
|
@@ -766,15 +1323,31 @@ async function configureClaudeCode(input, deps) {
|
|
|
766
1323
|
}
|
|
767
1324
|
}
|
|
768
1325
|
async function defaultRunCommand(command, args) {
|
|
769
|
-
await
|
|
1326
|
+
await execFileAsync2(command, args, { timeout: 1e4 });
|
|
770
1327
|
}
|
|
771
|
-
function buildProbeResult(hostedConfigured, hostedStatus, signerReady) {
|
|
1328
|
+
function buildProbeResult(mode, hostedConfigured, hostedStatus, signerReady, localMcpReady, localMcpProbeStatus) {
|
|
1329
|
+
if (mode === "local_stdio") {
|
|
1330
|
+
if (localMcpReady) return "local_stdio_mcp_ready";
|
|
1331
|
+
return localMcpProbeStatus ? `local_stdio_mcp_${localMcpProbeStatus}` : "local_stdio_mcp_unavailable";
|
|
1332
|
+
}
|
|
772
1333
|
const hostedPart = hostedConfigured ? `hosted_${hostedStatus}` : "hosted_not_configured";
|
|
773
1334
|
const signerPart = signerReady ? "local_signer_ready" : "local_signer_unavailable";
|
|
774
1335
|
return `${hostedPart}_${signerPart}`.slice(0, 120);
|
|
775
1336
|
}
|
|
1337
|
+
async function resolveLocalMcpConsent(input, messages) {
|
|
1338
|
+
if (input.ackLocalTools || input.ackSigner) {
|
|
1339
|
+
const status = await acknowledgeLocalMcpConsent(input.identityPath, input.signerPath, (message) => messages.push(message));
|
|
1340
|
+
if (status.acknowledged) {
|
|
1341
|
+
messages.push("Prepared the local Haven tools acknowledgement.");
|
|
1342
|
+
} else {
|
|
1343
|
+
messages.push("Local Haven tools acknowledgement still needs attention.");
|
|
1344
|
+
}
|
|
1345
|
+
return status;
|
|
1346
|
+
}
|
|
1347
|
+
return getLocalMcpConsentStatus(input.identityPath, input.signerPath);
|
|
1348
|
+
}
|
|
776
1349
|
async function resolveSignerConsent(input, messages) {
|
|
777
|
-
if (input.ackSigner) {
|
|
1350
|
+
if (input.ackSigner || input.ackLocalTools) {
|
|
778
1351
|
const status = await acknowledgeLocalSignerConsent(input.signerPath, (message) => messages.push(message));
|
|
779
1352
|
if (status.acknowledged) {
|
|
780
1353
|
messages.push("Prepared the local Haven signer acknowledgement.");
|
|
@@ -787,24 +1360,53 @@ async function resolveSignerConsent(input, messages) {
|
|
|
787
1360
|
}
|
|
788
1361
|
function signerConsentErrorCode(signerCredentialReady, signerConsent) {
|
|
789
1362
|
if (!signerCredentialReady) return "local_signer_credential_unavailable";
|
|
790
|
-
if (!signerConsent
|
|
1363
|
+
if (!signerConsent?.acknowledged) return "local_signer_ack_required";
|
|
1364
|
+
return void 0;
|
|
1365
|
+
}
|
|
1366
|
+
function localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbeStatus) {
|
|
1367
|
+
if (!signerCredentialReady) return "local_signer_credential_unavailable";
|
|
1368
|
+
if (!localMcpConsent?.acknowledged) return "local_mcp_ack_required";
|
|
1369
|
+
if (localMcpProbeStatus && localMcpProbeStatus !== "ok") return `local_mcp_probe_${localMcpProbeStatus}`;
|
|
791
1370
|
return void 0;
|
|
792
1371
|
}
|
|
793
1372
|
function nextAction(runtime, restartMode, errorCode) {
|
|
794
1373
|
if (errorCode) return "return_to_haven_for_wallet_approval_then_finish_runtime_setup";
|
|
795
1374
|
if (restartMode === "hot-reload") return "return_to_haven_for_wallet_approval";
|
|
796
|
-
if (runtime === "codex-cli") return "
|
|
1375
|
+
if (runtime === "codex-cli" || runtime === "codex-desktop") return "return_to_haven_for_wallet_approval_then_restart_codex";
|
|
797
1376
|
if (runtime === "claude-code") return "return_to_haven_for_wallet_approval_then_restart_claude_code";
|
|
798
1377
|
if (restartMode === "restart-app") return "return_to_haven_for_wallet_approval_then_restart_app";
|
|
799
1378
|
if (restartMode === "restart-session") return "return_to_haven_for_wallet_approval_then_restart_agent_session";
|
|
800
1379
|
return "return_to_haven_for_wallet_approval_then_configure_runtime";
|
|
801
1380
|
}
|
|
802
|
-
function
|
|
803
|
-
return
|
|
1381
|
+
function usesLocalMcp(runtime) {
|
|
1382
|
+
return runtime === "codex-cli" || runtime === "codex-desktop" || runtime === "claude-code";
|
|
1383
|
+
}
|
|
1384
|
+
async function prepareRuntimeForLocalMcp(input, deps) {
|
|
1385
|
+
const prepare = deps.prepareLocalMcpRuntime ?? ((runtimeInput) => prepareLocalMcpRuntime(runtimeInput, { runCommand: deps.runCommand }));
|
|
1386
|
+
return prepare({
|
|
1387
|
+
credentialDirectory: input.credentialDirectory,
|
|
1388
|
+
identityPath: input.identityPath,
|
|
1389
|
+
signerPath: input.signerPath,
|
|
1390
|
+
homeDir: deps.homeDir
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
async function runLocalMcpProbe(runtimeInstall, deps) {
|
|
1394
|
+
const probe = deps.probeLocalMcpTools ?? probeLocalMcpTools;
|
|
1395
|
+
try {
|
|
1396
|
+
return await probe(runtimeInstall.command, runtimeInstall.args, MCP_RUNTIME_MANIFEST.requiredTools);
|
|
1397
|
+
} catch {
|
|
1398
|
+
return { status: "process_error" };
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
function localRuntimePrepareErrorCode(err) {
|
|
1402
|
+
if (err && typeof err === "object" && "code" in err && err.code === "local_mcp_unsupported_node_version") {
|
|
1403
|
+
return "local_mcp_unsupported_node_version";
|
|
1404
|
+
}
|
|
1405
|
+
return "local_mcp_runtime_install_failed";
|
|
804
1406
|
}
|
|
805
1407
|
|
|
806
1408
|
// src/runtime.ts
|
|
807
|
-
var CONNECTOR_VERSION = "0.1.
|
|
1409
|
+
var CONNECTOR_VERSION = "0.1.2";
|
|
808
1410
|
async function runConnect(options, deps = {}) {
|
|
809
1411
|
const connectorVersion = options.connectorVersion ?? CONNECTOR_VERSION;
|
|
810
1412
|
const api = deps.api ?? createConnectApiClient(options.apiBaseUrl);
|
|
@@ -850,9 +1452,15 @@ async function runConnect(options, deps = {}) {
|
|
|
850
1452
|
agentId: registration.agent_id,
|
|
851
1453
|
apiKey: localApiKey,
|
|
852
1454
|
delegateKey: localKey.privateKey,
|
|
1455
|
+
delegateAddress: localKey.address,
|
|
853
1456
|
safeAddress: setup.haven_wallet.address,
|
|
854
1457
|
chainId: setup.haven_wallet.chain_id,
|
|
855
1458
|
network: setup.haven_wallet.network,
|
|
1459
|
+
agentBudget: setup.agent_budget.map((budget) => ({
|
|
1460
|
+
token_symbol: budget.token_symbol,
|
|
1461
|
+
allowance_amount: budget.allowance_amount,
|
|
1462
|
+
reset_period_min: budget.reset_period_min
|
|
1463
|
+
})),
|
|
856
1464
|
apiUrl: options.apiBaseUrl,
|
|
857
1465
|
hostedMcpUrl: registration.hosted_mcp_url,
|
|
858
1466
|
warn: log
|
|
@@ -867,17 +1475,21 @@ async function runConnect(options, deps = {}) {
|
|
|
867
1475
|
identityPath: credentialPaths.identityPath,
|
|
868
1476
|
credentialDirectory: credentialPaths.directory,
|
|
869
1477
|
environmentLabel: options.environmentLabel ?? "Local workspace",
|
|
870
|
-
ackSigner: options.ackSigner
|
|
1478
|
+
ackSigner: options.ackSigner,
|
|
1479
|
+
ackLocalTools: options.ackLocalTools
|
|
871
1480
|
});
|
|
872
1481
|
printRuntimeInstall(runtimeInstall, log);
|
|
873
1482
|
try {
|
|
874
1483
|
await api.updateInstallStatus(registration.setup_id, localApiKey, {
|
|
875
1484
|
runtime: runtimeInstall.runtime,
|
|
876
1485
|
connectorVersion,
|
|
1486
|
+
runtimeMcpMode: runtimeInstall.runtimeMcpMode,
|
|
877
1487
|
hostedMcpConfigured: runtimeInstall.hostedMcpConfigured,
|
|
878
1488
|
localSignerConfigured: runtimeInstall.localSignerConfigured,
|
|
1489
|
+
localMcpConfigured: runtimeInstall.localMcpConfigured,
|
|
879
1490
|
credentialFilesWritten: true,
|
|
880
1491
|
signerAcknowledged: runtimeInstall.signerAcknowledged,
|
|
1492
|
+
localMcpAcknowledged: runtimeInstall.localMcpAcknowledged,
|
|
881
1493
|
activationCommandAvailable: Boolean(runtimeInstall.activationCommand),
|
|
882
1494
|
probeResult: runtimeInstall.probeResult,
|
|
883
1495
|
restartRequired: runtimeInstall.restartRequired,
|
|
@@ -890,7 +1502,7 @@ async function runConnect(options, deps = {}) {
|
|
|
890
1502
|
}
|
|
891
1503
|
log("Return to Haven to approve the agent rules.");
|
|
892
1504
|
if (runtimeInstall.restartRequired) {
|
|
893
|
-
log("
|
|
1505
|
+
log("After approval, restart this agent normally so it can load Haven tools.");
|
|
894
1506
|
}
|
|
895
1507
|
return {
|
|
896
1508
|
setupId: registration.setup_id,
|
|
@@ -916,10 +1528,12 @@ function secureLogger(log) {
|
|
|
916
1528
|
}
|
|
917
1529
|
function printRuntimeInstall(result, log) {
|
|
918
1530
|
for (const message of result.messages) log(message);
|
|
919
|
-
if (result.
|
|
1531
|
+
if (result.localMcpConfigured) {
|
|
1532
|
+
log("Configured local Haven MCP tools.");
|
|
1533
|
+
} else if (result.hostedMcpConfigured) {
|
|
920
1534
|
log("Configured hosted Haven MCP identity.");
|
|
921
1535
|
} else {
|
|
922
|
-
log("
|
|
1536
|
+
log("Haven MCP tools still need runtime setup.");
|
|
923
1537
|
}
|
|
924
1538
|
if (result.localSignerConfigured) {
|
|
925
1539
|
log("Configured local Haven signer.");
|
|
@@ -949,8 +1563,11 @@ function parseArgs(argv, env = process.env) {
|
|
|
949
1563
|
options.credentialsDir = requireValue(argv, ++i, arg);
|
|
950
1564
|
} else if (arg === "--environment-label") {
|
|
951
1565
|
options.environmentLabel = requireValue(argv, ++i, arg);
|
|
1566
|
+
} else if (arg === "--ack-local-tools") {
|
|
1567
|
+
options.ackLocalTools = true;
|
|
952
1568
|
} else if (arg === "--ack-signer") {
|
|
953
1569
|
options.ackSigner = true;
|
|
1570
|
+
options.ackLocalTools = true;
|
|
954
1571
|
} else if (arg === "--version") {
|
|
955
1572
|
process.stdout.write(`${CONNECTOR_VERSION}
|
|
956
1573
|
`);
|
|
@@ -979,15 +1596,16 @@ function helpText() {
|
|
|
979
1596
|
"sends Haven only the public signing address plus a proof signature.",
|
|
980
1597
|
"",
|
|
981
1598
|
"Usage:",
|
|
982
|
-
" npx -y @haven_ai/connect --setup hv_setup_... --api https://api.haven.example --ack-
|
|
1599
|
+
" npx -y @haven_ai/connect --setup hv_setup_... --api https://api.haven.example --ack-local-tools --runtime claude-code",
|
|
983
1600
|
"",
|
|
984
1601
|
"Options:",
|
|
985
1602
|
" --setup <token> Short-lived setup token from Haven.",
|
|
986
1603
|
" --api <url> Haven backend API URL. Defaults to HAVEN_API_URL or http://localhost:3001.",
|
|
987
|
-
" --runtime <name> Agent runtime hint, such as claude-code, codex-cli, cursor, vscode, or claude-desktop.",
|
|
1604
|
+
" --runtime <name> Agent runtime hint, such as claude-code, codex-cli, codex-desktop, cursor, vscode, or claude-desktop.",
|
|
988
1605
|
" --credentials-dir <path> Credential directory fallback. Defaults to ~/.haven/agents.",
|
|
989
1606
|
" --environment-label <text> Non-sensitive label shown in Haven setup review.",
|
|
990
|
-
" --ack-
|
|
1607
|
+
" --ack-local-tools Write the one-time local Haven tools acknowledgement during setup.",
|
|
1608
|
+
" --ack-signer Backward-compatible alias for --ack-local-tools.",
|
|
991
1609
|
" --help Show this help.",
|
|
992
1610
|
"",
|
|
993
1611
|
"The connector never prints the private key and never sends it to Haven."
|