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