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