@haven_ai/connect 0.1.1-alpha → 0.1.3-alpha

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