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