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