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