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