@haven_ai/connect 0.1.26-alpha.0 → 0.1.27-alpha.0

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
@@ -1,263 +1,26 @@
1
1
  #!/usr/bin/env node
2
- import { realpathSync } from 'fs';
3
- import { fileURLToPath, pathToFileURL } from 'url';
4
- import { HAVEN_MINIMUM_NODE_VERSION, isSupportedNodeVersion, unsupportedNodeVersionMessage, SKILL_FOLDER_NAME, resolveTokenFromAddress, HAVEN_SKILL_MD, HAVEN_SKILL_BODY_MD } from '@haven_ai/sdk';
5
- import crypto from 'crypto';
6
- import { Wallet } from 'ethers';
7
- import { mkdir, rm, chmod, access, writeFile, readFile, unlink } from 'fs/promises';
2
+ import { ensureConsent, computeConsentHash, loadCredentials, consentInputFromClient, registeredToolNames, MCP_VERSION } from '@haven_ai/mcp';
3
+ import { ensureSignerConsent, computeSignerConsentHash, loadSignerCredentials, createEdgeSigner, toolSchemas } from '@haven_ai/signer';
4
+ import { isSupportedNodeVersion, unsupportedNodeVersionMessage, SKILL_FOLDER_NAME, resolveTokenFromAddress, HAVEN_SKILL_MD, HAVEN_SKILL_BODY_MD, HAVEN_MINIMUM_NODE_VERSION } from '@haven_ai/sdk';
5
+ import { mkdir, rm, chmod, access, writeFile, readFile, unlink, readdir, stat } from 'fs/promises';
8
6
  import { homedir, platform } from 'os';
9
7
  import { join, resolve, dirname } from 'path';
8
+ import { parseDocument, isMap, stringify } from 'yaml';
10
9
  import { execFile, spawn } from 'child_process';
11
10
  import { promisify } from 'util';
12
- import { parseDocument, isMap, stringify } from 'yaml';
13
- import { registeredToolNames, MCP_VERSION, ensureConsent, computeConsentHash, loadCredentials, consentInputFromClient } from '@haven_ai/mcp';
14
- import { ensureSignerConsent, computeSignerConsentHash, loadSignerCredentials, createEdgeSigner, toolSchemas } from '@haven_ai/signer';
11
+ import { realpathSync } from 'fs';
12
+ import { fileURLToPath, pathToFileURL } from 'url';
13
+ import crypto from 'crypto';
14
+ import { Wallet } from 'ethers';
15
15
 
16
- // src/api.ts
17
- function createConnectApiClient(baseUrl, fetchImpl = fetch) {
18
- const root = baseUrl.replace(/\/+$/, "");
19
- return {
20
- resolveSetup: (input) => request(fetchImpl, `${root}/agent-connection-setups/resolve`, {
21
- method: "POST",
22
- body: JSON.stringify({
23
- setup_token: input.setupToken,
24
- connector_version: input.connectorVersion,
25
- runtime: input.runtime
26
- })
27
- }),
28
- registerSetup: (input) => request(fetchImpl, `${root}/agent-connection-setups/register`, {
29
- method: "POST",
30
- body: JSON.stringify({
31
- setup_token: input.setupToken,
32
- challenge_id: input.challengeId,
33
- delegate_address: input.delegateAddress,
34
- proof_signature: input.proofSignature,
35
- api_key_hash: input.apiKeyHash,
36
- api_key_prefix: input.apiKeyPrefix,
37
- runtime: input.runtime,
38
- connector_version: input.connectorVersion,
39
- connector_context: input.connectorContext,
40
- install_capabilities: input.installCapabilities && {
41
- can_write_runtime_config: input.installCapabilities.canWriteRuntimeConfig,
42
- restart_required: input.installCapabilities.restartRequired
43
- }
44
- })
45
- }),
46
- getConnectorStatus: (setupId, apiKey) => request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/connector-status`, {
47
- method: "GET",
48
- headers: { Authorization: `Bearer ${apiKey}` }
49
- }),
50
- updateInstallStatus: async (setupId, apiKey, input) => {
51
- await request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/install-status`, {
52
- method: "POST",
53
- headers: { Authorization: `Bearer ${apiKey}` },
54
- body: JSON.stringify({
55
- runtime: input.runtime,
56
- connector_version: input.connectorVersion,
57
- runtime_mcp_mode: input.runtimeMcpMode,
58
- hosted_mcp_configured: input.hostedMcpConfigured,
59
- local_signer_configured: input.localSignerConfigured,
60
- local_mcp_configured: input.localMcpConfigured,
61
- credential_files_written: input.credentialFilesWritten,
62
- signer_acknowledged: input.signerAcknowledged,
63
- local_mcp_acknowledged: input.localMcpAcknowledged,
64
- activation_command_available: input.activationCommandAvailable,
65
- skill_installed: input.skillInstalled,
66
- probe_result: input.probeResult,
67
- restart_required: input.restartRequired,
68
- next_user_action: input.nextUserAction,
69
- error_code: input.errorCode ?? null,
70
- environment_label: input.environmentLabel
71
- })
72
- });
73
- }
74
- };
75
- }
76
- var ConnectRequestError = class extends Error {
77
- constructor(message, status) {
78
- super(message);
79
- this.status = status;
80
- this.name = "ConnectRequestError";
81
- }
82
- status;
16
+ var __defProp = Object.defineProperty;
17
+ var __getOwnPropNames = Object.getOwnPropertyNames;
18
+ var __esm = (fn, res) => function __init() {
19
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
83
20
  };
84
- async function request(fetchImpl, url, init) {
85
- const response = await fetchImpl(url, {
86
- ...init,
87
- headers: {
88
- "Content-Type": "application/json",
89
- ...init.headers ?? {}
90
- }
91
- });
92
- const text = await response.text();
93
- const body = text ? JSON.parse(text) : null;
94
- if (!response.ok) {
95
- const message = body?.error ?? body?.message ?? `${response.status} ${response.statusText}`;
96
- throw new ConnectRequestError(`Haven setup request failed: ${message}`, response.status);
97
- }
98
- return body;
99
- }
100
- function generateDelegateKey() {
101
- return delegateKeyFromPrivateKey(Wallet.createRandom().privateKey);
102
- }
103
- function delegateKeyFromPrivateKey(privateKey) {
104
- const wallet = new Wallet(privateKey);
105
- return {
106
- privateKey: wallet.privateKey,
107
- address: wallet.address,
108
- signChallenge: (message) => wallet.signMessage(message)
109
- };
110
- }
111
- function generateAgentApiKey() {
112
- return `sk_agent_${crypto.randomBytes(24).toString("hex")}`;
113
- }
114
- function hashAgentApiKey(apiKey) {
115
- return crypto.createHash("sha256").update(apiKey).digest("hex");
116
- }
117
- function agentApiKeyPrefix(apiKey) {
118
- return apiKey.slice(0, 12);
119
- }
120
-
121
- // src/redact.ts
122
- var API_KEY_RE = /sk_agent_[A-Za-z0-9]+/g;
123
- var PRIVATE_KEY_RE = /0x[0-9a-fA-F]{64}/g;
124
- function redactSecrets(value) {
125
- return value.replace(API_KEY_RE, "sk_agent_[redacted]").replace(PRIVATE_KEY_RE, "0x[redacted-private-key]");
126
- }
127
- function shortAddress(address) {
128
- if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return address;
129
- return `${address.slice(0, 6)}...${address.slice(-4)}`;
130
- }
131
- async function preflightCredentialStorage(input = {}) {
132
- const directory = defaultCredentialRoot(input.baseDir);
133
- await mkdir(directory, { recursive: true, mode: 448 });
134
- await restrictPermissions(directory, 448, input.warn);
135
- const probePath = join(directory, `.haven-connect-preflight-${crypto.randomBytes(8).toString("hex")}`);
136
- try {
137
- await writeOwnerOnlyJson(probePath, { ok: true }, input.warn);
138
- } finally {
139
- await rm(probePath, { force: true }).catch(() => void 0);
140
- }
141
- return directory;
142
- }
143
- async function writeCredentialFiles(input) {
144
- const directory = defaultAgentDirectory(input.agentId, input.baseDir);
145
- await mkdir(directory, { recursive: true, mode: 448 });
146
- await restrictPermissions(directory, 448, input.warn);
147
- const identityPath = join(directory, "identity.json");
148
- const signerPath = join(directory, "signer.json");
149
- const agentPath = join(directory, "agent.json");
150
- await assertDoesNotExist(identityPath);
151
- await assertDoesNotExist(signerPath);
152
- await assertDoesNotExist(agentPath);
153
- await writeOwnerOnlyJson(
154
- signerPath,
155
- {
156
- delegate_key: input.delegateKey,
157
- delegate_address: input.delegateAddress,
158
- agent_id: input.agentId,
159
- safe_address: input.safeAddress,
160
- chain_id: input.chainId,
161
- network: input.network,
162
- x402_binding_signer: input.x402BindingSigner,
163
- note: "Local signer credential. Haven backend never receives this private key."
164
- },
165
- input.warn
166
- );
167
- try {
168
- await writeOwnerOnlyJson(
169
- identityPath,
170
- {
171
- api_key: input.apiKey,
172
- agent_id: input.agentId,
173
- safe_address: input.safeAddress,
174
- chain_id: input.chainId,
175
- network: input.network,
176
- api_url: input.apiUrl,
177
- hosted_mcp_url: input.hostedMcpUrl,
178
- agent_budget: input.agentBudget,
179
- note: "Haven API key identifies the agent only. It cannot spend without the local signer key and on-chain Haven wallet rules."
180
- },
181
- input.warn
182
- );
183
- } catch (err) {
184
- await rm(signerPath, { force: true }).catch(() => void 0);
185
- throw err;
186
- }
187
- try {
188
- await writeOwnerOnlyJson(
189
- agentPath,
190
- {
191
- agent_id: input.agentId,
192
- delegate_address: input.delegateAddress,
193
- safe_address: input.safeAddress,
194
- chain_id: input.chainId,
195
- network: input.network,
196
- agent_budget: input.agentBudget,
197
- note: "Non-secret orientation for the agent: public delegate/Haven wallet identity + configured budget. Contains no API key or signing key. For the live remaining budget, call haven_get_allowances."
198
- },
199
- input.warn
200
- );
201
- } catch (err) {
202
- await rm(signerPath, { force: true }).catch(() => void 0);
203
- await rm(identityPath, { force: true }).catch(() => void 0);
204
- await rm(agentPath, { force: true }).catch(() => void 0);
205
- throw err;
206
- }
207
- return { directory, identityPath, signerPath, agentPath };
208
- }
209
- function defaultAgentDirectory(agentId, baseDir = join(homedir(), ".haven", "agents")) {
210
- return resolve(defaultCredentialRoot(baseDir), safePathPart(agentId));
211
- }
212
- function defaultCredentialRoot(baseDir = join(homedir(), ".haven", "agents")) {
213
- return resolve(baseDir);
214
- }
215
- async function writeOwnerOnlyJson(path, value, warn) {
216
- const json = JSON.stringify(dropUndefined(value), null, 2);
217
- await writeFile(path, `${json}
218
- `, { mode: 384, flag: "wx" });
219
- await restrictPermissions(path, 384, warn);
220
- }
221
- function safePathPart(value) {
222
- return value.replace(/[^A-Za-z0-9_.-]/g, "_");
223
- }
224
- function dropUndefined(value) {
225
- return Object.fromEntries(Object.entries(value).filter(([, field]) => field !== void 0));
226
- }
227
- async function assertDoesNotExist(path) {
228
- try {
229
- await access(path);
230
- } catch (err) {
231
- if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") return;
232
- throw err;
233
- }
234
- throw new Error(`Refusing to overwrite existing Haven credential file: ${path}`);
235
- }
236
- async function restrictPermissions(path, mode, warn) {
237
- try {
238
- await chmod(path, mode);
239
- } catch (err) {
240
- warn?.(
241
- `Warning: could not restrict permissions on ${path} to ${mode.toString(8)}. Move this credential to a private location or run chmod ${mode.toString(8)} ${path}. ${err instanceof Error ? err.message : String(err)}`
242
- );
243
- }
244
- }
245
- var MCP_RUNTIME_MANIFEST = {
246
- mcpPackage: "@haven_ai/mcp",
247
- mcpVersion: MCP_VERSION,
248
- sdkPackage: "@haven_ai/sdk",
249
- sdkVersion: "0.1.26-alpha.0",
250
- signerPackage: "@haven_ai/signer",
251
- signerVersion: "0.1.26-alpha.0",
252
- // Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
253
- // while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
254
- // so the guard that was supposed to enforce the floor waved Node v23 through
255
- // — including on the `--local` path where it does run. A hand-maintained
256
- // second copy of a number is a drift waiting to happen; a guard test pins
257
- // this against `package.json`'s `engines.node`.
258
- minimumNodeVersion: HAVEN_MINIMUM_NODE_VERSION,
259
- supportedClients: ["codex-cli", "codex-desktop", "claude-code"],
260
- requiredTools: registeredToolNames()
21
+ var __export = (target, all) => {
22
+ for (var name in all)
23
+ __defProp(target, name, { get: all[name], enumerable: true });
261
24
  };
262
25
  function mcpPackageSpec() {
263
26
  return `${MCP_RUNTIME_MANIFEST.mcpPackage}@${MCP_RUNTIME_MANIFEST.mcpVersion}`;
@@ -268,21 +31,35 @@ function sdkPackageSpec() {
268
31
  function signerPackageSpec() {
269
32
  return `${MCP_RUNTIME_MANIFEST.signerPackage}@${MCP_RUNTIME_MANIFEST.signerVersion}`;
270
33
  }
271
-
272
- // src/config-writers.ts
273
- var HERMES_API_KEY_ENV = "MCP_HAVEN_API_KEY";
274
- var HermesConfigRecoveryError = class extends Error {
275
- constructor() {
276
- super("Hermes configuration recovery did not complete");
277
- this.name = "HermesConfigRecoveryError";
278
- }
279
- };
280
- var InvalidCodexTomlError = class extends Error {
281
- constructor(message) {
282
- super(message);
283
- this.name = "InvalidCodexTomlError";
34
+ var MCP_RUNTIME_MANIFEST;
35
+ var init_runtime_manifest = __esm({
36
+ "src/runtime-manifest.ts"() {
37
+ MCP_RUNTIME_MANIFEST = {
38
+ mcpPackage: "@haven_ai/mcp",
39
+ mcpVersion: MCP_VERSION,
40
+ sdkPackage: "@haven_ai/sdk",
41
+ sdkVersion: "0.1.27-alpha.0",
42
+ signerPackage: "@haven_ai/signer",
43
+ signerVersion: "0.1.27-alpha.0",
44
+ // Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
45
+ // while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
46
+ // so the guard that was supposed to enforce the floor waved Node v23 through
47
+ // — including on the `--local` path where it does run. A hand-maintained
48
+ // second copy of a number is a drift waiting to happen; a guard test pins
49
+ // this against `package.json`'s `engines.node`.
50
+ minimumNodeVersion: HAVEN_MINIMUM_NODE_VERSION,
51
+ supportedClients: ["codex-cli", "codex-desktop", "claude-code"],
52
+ requiredTools: registeredToolNames(),
53
+ /**
54
+ * The signer MCP's tool surface, DERIVED from the pinned @haven_ai/signer
55
+ * package (#1587) — same anti-drift rule as `requiredTools` above: a
56
+ * literal list here would rot the first time the signer gains a tool.
57
+ * The handshake probe requires all of them.
58
+ */
59
+ requiredSignerTools: Object.keys(toolSchemas)
60
+ };
284
61
  }
285
- };
62
+ });
286
63
  async function writeRuntimeConfig(input, deps = {}) {
287
64
  switch (input.runtime) {
288
65
  case "codex-cli":
@@ -867,16 +644,35 @@ function hasBalancedTomlContainers(value) {
867
644
  function tomlString(value) {
868
645
  return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
869
646
  }
870
- function cursorConfigPath(homeDir = homedir()) {
871
- return resolve(homeDir, ".cursor", "mcp.json");
872
- }
873
- function codexConfigPath(homeDir = homedir()) {
874
- return resolve(homeDir, ".codex", "config.toml");
875
- }
876
- function vscodeConfigPath(homeDir = homedir()) {
877
- if (platform() === "darwin") return resolve(homeDir, "Library", "Application Support", "Code", "User", "mcp.json");
878
- if (platform() === "win32") {
879
- return resolve(process.env.APPDATA ?? join(homeDir, "AppData", "Roaming"), "Code", "User", "mcp.json");
647
+ function runtimeConfigPathFor(runtime, homeDir = homedir()) {
648
+ switch (runtime) {
649
+ case "cursor":
650
+ return cursorConfigPath(homeDir);
651
+ case "codex-cli":
652
+ case "codex-desktop":
653
+ return codexConfigPath(homeDir);
654
+ case "vscode":
655
+ return vscodeConfigPath(homeDir);
656
+ case "vscode-insiders":
657
+ return vscodeInsidersConfigPath(homeDir);
658
+ case "claude-desktop":
659
+ return claudeDesktopConfigPath(homeDir);
660
+ case "hermes":
661
+ return hermesConfigPath(homeDir);
662
+ default:
663
+ return null;
664
+ }
665
+ }
666
+ function cursorConfigPath(homeDir = homedir()) {
667
+ return resolve(homeDir, ".cursor", "mcp.json");
668
+ }
669
+ function codexConfigPath(homeDir = homedir()) {
670
+ return resolve(homeDir, ".codex", "config.toml");
671
+ }
672
+ function vscodeConfigPath(homeDir = homedir()) {
673
+ if (platform() === "darwin") return resolve(homeDir, "Library", "Application Support", "Code", "User", "mcp.json");
674
+ if (platform() === "win32") {
675
+ return resolve(process.env.APPDATA ?? join(homeDir, "AppData", "Roaming"), "Code", "User", "mcp.json");
880
676
  }
881
677
  return resolve(homeDir, ".config", "Code", "User", "mcp.json");
882
678
  }
@@ -923,82 +719,25 @@ function configTargetLabel(runtime) {
923
719
  return "runtime MCP config";
924
720
  }
925
721
  }
926
- async function acknowledgeLocalMcpConsent(identityPath, signerPath, log) {
927
- try {
928
- const input = await buildLocalMcpConsentInput(identityPath, signerPath);
929
- const decision = await ensureConsent(input, {
930
- credentialsPath: identityPath,
931
- writeAck: true,
932
- out: log ? { write: (chunk) => writeLogChunk(log, chunk) } : void 0
933
- });
934
- return {
935
- acknowledged: decision.ok,
936
- hash: decision.hash,
937
- reason: decision.reason
938
- };
939
- } catch (err) {
940
- return {
941
- acknowledged: false,
942
- error: err instanceof Error ? err.message : String(err)
943
- };
944
- }
945
- }
946
- async function getLocalMcpConsentStatus(identityPath, signerPath) {
947
- try {
948
- const input = await buildLocalMcpConsentInput(identityPath, signerPath);
949
- const hash = computeConsentHash(input);
950
- const stored = await readLocalMcpAckFile(localMcpAckPath(identityPath));
951
- if (stored === hash) {
952
- return { acknowledged: true, hash, reason: "ack_file_match" };
953
- }
954
- return {
955
- acknowledged: false,
956
- hash,
957
- reason: stored ? "ack_file_mismatch" : "ack_file_missing"
722
+ var HERMES_API_KEY_ENV, HermesConfigRecoveryError, InvalidCodexTomlError;
723
+ var init_config_writers = __esm({
724
+ "src/config-writers.ts"() {
725
+ init_runtime_manifest();
726
+ HERMES_API_KEY_ENV = "MCP_HAVEN_API_KEY";
727
+ HermesConfigRecoveryError = class extends Error {
728
+ constructor() {
729
+ super("Hermes configuration recovery did not complete");
730
+ this.name = "HermesConfigRecoveryError";
731
+ }
958
732
  };
959
- } catch (err) {
960
- return {
961
- acknowledged: false,
962
- error: err instanceof Error ? err.message : String(err)
733
+ InvalidCodexTomlError = class extends Error {
734
+ constructor(message) {
735
+ super(message);
736
+ this.name = "InvalidCodexTomlError";
737
+ }
963
738
  };
964
739
  }
965
- }
966
- function localMcpAckPath(identityPath) {
967
- return resolve(`${identityPath}.ack.json`);
968
- }
969
- async function buildLocalMcpConsentInput(identityPath, signerPath) {
970
- const credentials = await loadCredentials({ identityPath, signerPath });
971
- const unavailableDuringSetup = {
972
- getAllowances: async () => {
973
- throw new Error("Haven approval is not complete yet.");
974
- }
975
- };
976
- return consentInputFromClient(
977
- unavailableDuringSetup,
978
- {
979
- apiKey: credentials.apiKey,
980
- apiUrl: credentials.apiUrl,
981
- agentId: credentials.agentId,
982
- safeAddress: credentials.safeAddress,
983
- delegateAddress: credentials.delegateAddress,
984
- chainId: credentials.chainId,
985
- allowanceSummary: credentials.allowanceSummary
986
- },
987
- registeredToolNames()
988
- );
989
- }
990
- async function readLocalMcpAckFile(path) {
991
- try {
992
- const parsed = JSON.parse(await readFile(path, "utf8"));
993
- return typeof parsed.ack === "string" ? parsed.ack : null;
994
- } catch {
995
- return null;
996
- }
997
- }
998
- function writeLogChunk(log, chunk) {
999
- const message = String(chunk).trimEnd();
1000
- if (message) log(message);
1001
- }
740
+ });
1002
741
  async function probeHostedMcpTools(apiKey, hostedMcpUrl, fetchImpl = fetch) {
1003
742
  let response;
1004
743
  try {
@@ -1041,6 +780,8 @@ async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4)
1041
780
  let stdout = "";
1042
781
  let settled = false;
1043
782
  let sawInitialize = false;
783
+ let serverInfo;
784
+ let capabilities;
1044
785
  const finish = (result) => {
1045
786
  if (settled) return;
1046
787
  settled = true;
@@ -1072,6 +813,9 @@ async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4)
1072
813
  }
1073
814
  if (payload.id === 1 && !sawInitialize) {
1074
815
  sawInitialize = true;
816
+ const init = payload.result;
817
+ serverInfo = init?.serverInfo;
818
+ capabilities = init?.capabilities;
1075
819
  writeJsonRpc(child, { jsonrpc: "2.0", method: "notifications/initialized", params: {} });
1076
820
  writeJsonRpc(child, { jsonrpc: "2.0", id: 2, method: "tools/list", params: {} });
1077
821
  continue;
@@ -1080,7 +824,7 @@ async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4)
1080
824
  const tools = payload.result?.tools;
1081
825
  const toolNames = Array.isArray(tools) ? tools.map((tool) => tool && typeof tool === "object" && "name" in tool ? tool.name : void 0).filter((name) => typeof name === "string") : [];
1082
826
  const missing = requiredTools.filter((name) => !toolNames.includes(name));
1083
- finish({ status: missing.length === 0 ? "ok" : "missing_tools", toolNames });
827
+ finish({ status: missing.length === 0 ? "ok" : "missing_tools", toolNames, serverInfo, capabilities });
1084
828
  return;
1085
829
  }
1086
830
  }
@@ -1129,51 +873,37 @@ async function fetchWithTimeout(fetchImpl, url, init) {
1129
873
  clearTimeout(timeout);
1130
874
  }
1131
875
  }
1132
- var execFileAsync = promisify(execFile);
1133
- var UnsupportedNodeVersionError = class extends Error {
1134
- code = "local_mcp_unsupported_node_version";
1135
- nodeVersion;
1136
- minimumNodeVersion;
1137
- constructor(nodeVersion, minimumNodeVersion, subject = "Haven setup") {
1138
- super(unsupportedNodeVersionMessage({ subject, nodeVersion, minimumNodeVersion }));
1139
- this.name = "UnsupportedNodeVersionError";
1140
- this.nodeVersion = nodeVersion;
1141
- this.minimumNodeVersion = minimumNodeVersion;
876
+ var init_probes = __esm({
877
+ "src/probes.ts"() {
1142
878
  }
1143
- };
1144
- async function prepareLocalMcpRuntime(input, deps = {}) {
1145
- assertSupportedNodeVersion(input.nodeVersion);
879
+ });
880
+ async function prepareSignerRuntime(input, deps = {}) {
1146
881
  const homeDir = input.homeDir ?? homedir();
1147
- const runtimeDirectory = resolve(homeDir, ".haven", "mcp-runtime", MCP_RUNTIME_MANIFEST.mcpVersion);
882
+ const runtimeDirectory = resolve(homeDir, ".haven", "signer-runtime", MCP_RUNTIME_MANIFEST.signerVersion);
1148
883
  const npmCacheDirectory = resolve(homeDir, ".haven", "npm-cache");
1149
- const cliPath = join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "dist", "cli.js");
884
+ const cliPath = join(runtimeDirectory, "node_modules", "@haven_ai", "signer", "dist", "cli.js");
1150
885
  const messages = [];
1151
886
  await mkdir(runtimeDirectory, { recursive: true, mode: 448 });
1152
887
  await chmod(runtimeDirectory, 448).catch(() => void 0);
1153
888
  await mkdir(npmCacheDirectory, { recursive: true, mode: 448 });
1154
889
  await chmod(npmCacheDirectory, 448).catch(() => void 0);
1155
890
  if (await installedRuntimeMatches(runtimeDirectory, cliPath)) {
1156
- messages.push(`Using existing local Haven MCP runtime ${mcpPackageSpec()}.`);
891
+ messages.push(`Using existing local Haven signer runtime ${signerPackageSpec()}.`);
1157
892
  } else {
1158
- await installRuntimePackages(runtimeDirectory, npmCacheDirectory, deps.runCommand);
1159
- messages.push(`Installed local Haven MCP runtime ${mcpPackageSpec()}.`);
893
+ await installRuntimePackages(runtimeDirectory, npmCacheDirectory, deps);
894
+ messages.push(`Installed local Haven signer runtime ${signerPackageSpec()}.`);
1160
895
  }
1161
- await assertFileExists(cliPath, "local Haven MCP CLI");
1162
- const wrapperPath = join(input.credentialDirectory, "bin", "haven-mcp");
1163
- await writeWrapper({
1164
- wrapperPath,
1165
- cliPath,
1166
- identityPath: input.identityPath,
1167
- signerPath: input.signerPath
1168
- });
896
+ await assertFileExists(cliPath, "local Haven signer CLI");
897
+ const wrapperPath = join(input.credentialDirectory, "bin", "haven-signer.mjs");
898
+ await writeWrapper({ wrapperPath, cliPath, signerPath: input.signerPath });
1169
899
  await writeRuntimeSidecar({
1170
- path: join(input.credentialDirectory, "mcp-runtime.json"),
900
+ path: join(input.credentialDirectory, "signer-runtime.json"),
1171
901
  wrapperPath,
1172
902
  runtimeDirectory,
1173
903
  npmCacheDirectory,
1174
904
  cliPath
1175
905
  });
1176
- messages.push(`Prepared stable local Haven MCP wrapper: ${wrapperPath}`);
906
+ messages.push(`Prepared stable local Haven signer wrapper: ${wrapperPath}`);
1177
907
  return {
1178
908
  command: wrapperPath,
1179
909
  args: [],
@@ -1184,12 +914,8 @@ async function prepareLocalMcpRuntime(input, deps = {}) {
1184
914
  messages
1185
915
  };
1186
916
  }
1187
- function assertSupportedNodeVersion(nodeVersion = process.versions.node, minimumNodeVersion = MCP_RUNTIME_MANIFEST.minimumNodeVersion, subject = "Haven setup") {
1188
- if (!isSupportedNodeVersion(nodeVersion, minimumNodeVersion)) {
1189
- throw new UnsupportedNodeVersionError(nodeVersion, minimumNodeVersion, subject);
1190
- }
1191
- }
1192
- async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, runCommand) {
917
+ async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, deps) {
918
+ const { runCommand, onProgress } = deps;
1193
919
  const baseArgs = [
1194
920
  "install",
1195
921
  "--prefix",
@@ -1198,12 +924,22 @@ async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, runCo
1198
924
  "--no-fund",
1199
925
  "--omit=dev",
1200
926
  "--prefer-offline",
1201
- mcpPackageSpec(),
927
+ signerPackageSpec(),
1202
928
  sdkPackageSpec()
1203
929
  ];
1204
930
  const run = async (args) => {
1205
- if (runCommand) await runCommand("npm", args);
1206
- else await execFileAsync("npm", args, { timeout: 12e4, maxBuffer: 1024 * 1024 });
931
+ const startedAt = Date.now();
932
+ const heartbeat = setInterval(() => {
933
+ const seconds = Math.round((Date.now() - startedAt) / 1e3);
934
+ onProgress?.(`Still installing the local Haven signer runtime\u2026 (${seconds}s \u2014 a cold cache can take several minutes)`);
935
+ }, SIGNER_INSTALL_HEARTBEAT_MS);
936
+ heartbeat.unref?.();
937
+ try {
938
+ if (runCommand) await runCommand("npm", args);
939
+ else await execFileAsync("npm", args, { timeout: SIGNER_INSTALL_TIMEOUT_MS, maxBuffer: 1024 * 1024 });
940
+ } finally {
941
+ clearInterval(heartbeat);
942
+ }
1207
943
  };
1208
944
  try {
1209
945
  await run(baseArgs);
@@ -1211,18 +947,20 @@ async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, runCo
1211
947
  try {
1212
948
  await run([...baseArgs, "--cache", npmCacheDirectory]);
1213
949
  } catch (err) {
1214
- throw new Error(`Could not install local Haven MCP runtime ${mcpPackageSpec()}: ${err instanceof Error ? err.message : String(err)}`);
950
+ throw new Error(
951
+ `Could not install local Haven signer runtime ${signerPackageSpec()}: ${err instanceof Error ? err.message : String(err)}`
952
+ );
1215
953
  }
1216
954
  }
1217
955
  }
1218
956
  async function installedRuntimeMatches(runtimeDirectory, cliPath) {
1219
957
  try {
1220
- await assertFileExists(cliPath, "local Haven MCP CLI");
1221
- const [mcpPackage, sdkPackage] = await Promise.all([
1222
- readPackageJson(join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "package.json")),
958
+ await assertFileExists(cliPath, "local Haven signer CLI");
959
+ const [signerPackage, sdkPackage] = await Promise.all([
960
+ readPackageJson(join(runtimeDirectory, "node_modules", "@haven_ai", "signer", "package.json")),
1223
961
  readPackageJson(join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
1224
962
  ]);
1225
- return mcpPackage.version === MCP_RUNTIME_MANIFEST.mcpVersion && sdkPackage.version === MCP_RUNTIME_MANIFEST.sdkVersion;
963
+ return signerPackage.version === MCP_RUNTIME_MANIFEST.signerVersion && sdkPackage.version === MCP_RUNTIME_MANIFEST.sdkVersion;
1226
964
  } catch {
1227
965
  return false;
1228
966
  }
@@ -1238,10 +976,9 @@ async function writeWrapper(input) {
1238
976
  "import { spawn } from 'node:child_process'",
1239
977
  "",
1240
978
  `const cliPath = ${JSON.stringify(input.cliPath)}`,
1241
- `const identityPath = ${JSON.stringify(input.identityPath)}`,
1242
979
  `const signerPath = ${JSON.stringify(input.signerPath)}`,
1243
980
  "",
1244
- "const child = spawn(process.execPath, [cliPath, '--identity', identityPath, '--signer', signerPath, ...process.argv.slice(2)], {",
981
+ "const child = spawn(process.execPath, [cliPath, '--credentials', signerPath, ...process.argv.slice(2)], {",
1245
982
  " stdio: 'inherit',",
1246
983
  "})",
1247
984
  "",
@@ -1254,57 +991,889 @@ async function writeWrapper(input) {
1254
991
  await writeFile(input.wrapperPath, source, { mode: 448 });
1255
992
  await chmod(input.wrapperPath, 448).catch(() => void 0);
1256
993
  }
1257
- async function writeRuntimeSidecar(input) {
1258
- const value = {
1259
- mcp_package: MCP_RUNTIME_MANIFEST.mcpPackage,
1260
- mcp_version: MCP_RUNTIME_MANIFEST.mcpVersion,
1261
- sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
1262
- sdk_version: MCP_RUNTIME_MANIFEST.sdkVersion,
1263
- minimum_node_version: MCP_RUNTIME_MANIFEST.minimumNodeVersion,
1264
- wrapper_path: input.wrapperPath,
1265
- runtime_directory: input.runtimeDirectory,
1266
- npm_cache_directory: input.npmCacheDirectory,
1267
- cli_path: input.cliPath
994
+ async function readRuntimeSidecar(credentialDirectory) {
995
+ try {
996
+ return JSON.parse(
997
+ await readFile(join(credentialDirectory, "signer-runtime.json"), "utf8")
998
+ );
999
+ } catch {
1000
+ return null;
1001
+ }
1002
+ }
1003
+ async function writeRuntimeSidecar(input) {
1004
+ const value = {
1005
+ signer_package: MCP_RUNTIME_MANIFEST.signerPackage,
1006
+ signer_version: MCP_RUNTIME_MANIFEST.signerVersion,
1007
+ sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
1008
+ sdk_version: MCP_RUNTIME_MANIFEST.sdkVersion,
1009
+ wrapper_path: input.wrapperPath,
1010
+ runtime_directory: input.runtimeDirectory,
1011
+ npm_cache_directory: input.npmCacheDirectory,
1012
+ cli_path: input.cliPath
1013
+ };
1014
+ await writeFile(input.path, `${JSON.stringify(value, null, 2)}
1015
+ `, { mode: 384 });
1016
+ await chmod(input.path, 384).catch(() => void 0);
1017
+ }
1018
+ async function assertFileExists(path, label) {
1019
+ try {
1020
+ await access(path);
1021
+ } catch {
1022
+ throw new Error(`Missing ${label}: ${path}`);
1023
+ }
1024
+ }
1025
+ var execFileAsync, SIGNER_INSTALL_TIMEOUT_MS, SIGNER_INSTALL_HEARTBEAT_MS;
1026
+ var init_signer_runtime = __esm({
1027
+ "src/signer-runtime.ts"() {
1028
+ init_runtime_manifest();
1029
+ execFileAsync = promisify(execFile);
1030
+ SIGNER_INSTALL_TIMEOUT_MS = 6e5;
1031
+ SIGNER_INSTALL_HEARTBEAT_MS = 15e3;
1032
+ }
1033
+ });
1034
+
1035
+ // src/runtime-registry.ts
1036
+ function runtimeProfile(runtime, env = process.env) {
1037
+ return RUNTIME_PROFILES[normalizeRuntime(runtime, env)];
1038
+ }
1039
+ function normalizeRuntime(runtime, env = process.env) {
1040
+ const explicit = normalizeRuntimeName(runtime);
1041
+ if (explicit) return explicit;
1042
+ return detectRuntime(env) ?? "other";
1043
+ }
1044
+ function restartRequiredForRuntime(runtime, env = process.env) {
1045
+ const mode = runtimeProfile(runtime, env).restartMode;
1046
+ return mode === "restart-session" || mode === "restart-app";
1047
+ }
1048
+ function runtimeVerificationInstruction(runtime) {
1049
+ const label = RUNTIME_PROFILES[runtime].label;
1050
+ return `In ${label}, run the read-only \`haven_get_agent\` and \`haven_get_allowances\` tools to confirm the Haven wallet and live budget. Do not sign, fund, or create a payment to verify setup.`;
1051
+ }
1052
+ function normalizeRuntimeName(runtime) {
1053
+ const key = runtime?.trim().toLowerCase();
1054
+ if (!key) return null;
1055
+ return RUNTIME_ALIASES[key.replace(/\s+/g, "-")] ?? null;
1056
+ }
1057
+ function detectRuntime(env) {
1058
+ if (env.CLAUDECODE || env.CLAUDE_CODE || env.CLAUDECODE_CWD) return "claude-code";
1059
+ if (env.CODEX_SANDBOX || env.CODEX_HOME || env.CODEX_CWD) return "codex-cli";
1060
+ if (env.VSCODE_CWD || env.VSCODE_IPC_HOOK_CLI || env.TERM_PROGRAM === "vscode") return "vscode";
1061
+ if (env.HERMES_HOME || env.HERMES_AGENT) return "hermes";
1062
+ return null;
1063
+ }
1064
+ var RUNTIME_PROFILES, RUNTIME_ALIASES;
1065
+ var init_runtime_registry = __esm({
1066
+ "src/runtime-registry.ts"() {
1067
+ RUNTIME_PROFILES = {
1068
+ "claude-code": {
1069
+ id: "claude-code",
1070
+ label: "Claude Code",
1071
+ restartMode: "restart-session",
1072
+ canWriteRuntimeConfig: true,
1073
+ activationInstruction: "Start a new Claude Code session so it loads the Haven MCP entries."
1074
+ },
1075
+ "codex-cli": {
1076
+ id: "codex-cli",
1077
+ label: "Codex CLI",
1078
+ restartMode: "restart-session",
1079
+ canWriteRuntimeConfig: true,
1080
+ activationInstruction: "Start a fresh Codex CLI session (for example, run `codex resume --last`)."
1081
+ },
1082
+ "codex-desktop": {
1083
+ id: "codex-desktop",
1084
+ label: "Codex Desktop",
1085
+ restartMode: "restart-session",
1086
+ canWriteRuntimeConfig: true,
1087
+ activationInstruction: "Quit and reopen Codex Desktop so it loads the Haven MCP entries."
1088
+ },
1089
+ cursor: {
1090
+ id: "cursor",
1091
+ label: "Cursor",
1092
+ restartMode: "hot-reload",
1093
+ canWriteRuntimeConfig: true,
1094
+ activationInstruction: "Wait for Cursor to hot-reload the Haven MCP entries; no app restart is required."
1095
+ },
1096
+ vscode: {
1097
+ id: "vscode",
1098
+ label: "VS Code",
1099
+ restartMode: "hot-reload",
1100
+ canWriteRuntimeConfig: true,
1101
+ activationInstruction: "Wait for VS Code to hot-reload the Haven MCP entries; no app restart is required."
1102
+ },
1103
+ "vscode-insiders": {
1104
+ id: "vscode-insiders",
1105
+ label: "VS Code Insiders",
1106
+ restartMode: "hot-reload",
1107
+ canWriteRuntimeConfig: true,
1108
+ activationInstruction: "Wait for VS Code Insiders to hot-reload the Haven MCP entries; no app restart is required."
1109
+ },
1110
+ "claude-desktop": {
1111
+ id: "claude-desktop",
1112
+ label: "Claude Desktop",
1113
+ restartMode: "restart-app",
1114
+ canWriteRuntimeConfig: true,
1115
+ activationInstruction: "Quit and reopen Claude Desktop so it loads the Haven MCP entries."
1116
+ },
1117
+ hermes: {
1118
+ id: "hermes",
1119
+ label: "Hermes Agent",
1120
+ restartMode: "restart-session",
1121
+ canWriteRuntimeConfig: true,
1122
+ activationInstruction: "Start a new Hermes session; in Hermes Gateway, run `/restart` instead."
1123
+ },
1124
+ other: {
1125
+ id: "other",
1126
+ label: "Other agent runtime",
1127
+ restartMode: "manual",
1128
+ canWriteRuntimeConfig: false,
1129
+ activationInstruction: "Finish the manual MCP setup shown above, then start a fresh session in that runtime."
1130
+ }
1131
+ };
1132
+ RUNTIME_ALIASES = {
1133
+ claude: "claude-code",
1134
+ "claude-code": "claude-code",
1135
+ claudecode: "claude-code",
1136
+ "claude_code": "claude-code",
1137
+ codex: "codex-cli",
1138
+ "codex-cli": "codex-cli",
1139
+ codexcli: "codex-cli",
1140
+ "codex_cli": "codex-cli",
1141
+ "codex-desktop": "codex-desktop",
1142
+ "codex_desktop": "codex-desktop",
1143
+ codexdesktop: "codex-desktop",
1144
+ "codex-app": "codex-desktop",
1145
+ "codex_app": "codex-desktop",
1146
+ codexapp: "codex-desktop",
1147
+ cursor: "cursor",
1148
+ vscode: "vscode",
1149
+ "vs-code": "vscode",
1150
+ "vs_code": "vscode",
1151
+ code: "vscode",
1152
+ "vscode-insiders": "vscode-insiders",
1153
+ "vscode_insiders": "vscode-insiders",
1154
+ vscodeinsiders: "vscode-insiders",
1155
+ "vs-code-insiders": "vscode-insiders",
1156
+ "code-insiders": "vscode-insiders",
1157
+ insiders: "vscode-insiders",
1158
+ "claude-desktop": "claude-desktop",
1159
+ "claude_desktop": "claude-desktop",
1160
+ claudesktop: "claude-desktop",
1161
+ desktop: "claude-desktop",
1162
+ hermes: "hermes",
1163
+ "hermes-agent": "hermes",
1164
+ hermes_agent: "hermes",
1165
+ hermesagent: "hermes",
1166
+ other: "other",
1167
+ manual: "other"
1168
+ };
1169
+ }
1170
+ });
1171
+ async function acknowledgeLocalSignerConsent(signerPath, log) {
1172
+ try {
1173
+ const input = await buildSignerConsentInput(signerPath);
1174
+ const decision = await ensureSignerConsent(input, {
1175
+ credentialsPath: signerPath,
1176
+ writeAck: true,
1177
+ out: log ? { write: (chunk) => writeLogChunk2(log, chunk) } : void 0
1178
+ });
1179
+ return {
1180
+ acknowledged: decision.ok,
1181
+ hash: decision.hash,
1182
+ reason: decision.reason
1183
+ };
1184
+ } catch (err) {
1185
+ return {
1186
+ acknowledged: false,
1187
+ error: err instanceof Error ? err.message : String(err)
1188
+ };
1189
+ }
1190
+ }
1191
+ async function getLocalSignerConsentStatus(signerPath) {
1192
+ try {
1193
+ const input = await buildSignerConsentInput(signerPath);
1194
+ const hash = computeSignerConsentHash(input);
1195
+ const stored = await readSignerAckFile(signerAckPath(signerPath));
1196
+ if (stored === hash) {
1197
+ return { acknowledged: true, hash, reason: "ack_file_match" };
1198
+ }
1199
+ return {
1200
+ acknowledged: false,
1201
+ hash,
1202
+ reason: stored ? "ack_file_mismatch" : "ack_file_missing"
1203
+ };
1204
+ } catch (err) {
1205
+ return {
1206
+ acknowledged: false,
1207
+ error: err instanceof Error ? err.message : String(err)
1208
+ };
1209
+ }
1210
+ }
1211
+ function signerAckPath(signerPath) {
1212
+ return resolve(`${signerPath}.signer-ack.json`);
1213
+ }
1214
+ async function buildSignerConsentInput(signerPath) {
1215
+ const credentials = await loadSignerCredentials(signerPath);
1216
+ const signer = createEdgeSigner(credentials.delegateKey, {
1217
+ x402BindingSigner: credentials.x402BindingSigner
1218
+ });
1219
+ return {
1220
+ delegateAddress: signer.delegateAddress,
1221
+ safeAddress: credentials.safeAddress,
1222
+ agentId: credentials.agentId,
1223
+ chainId: credentials.chainId,
1224
+ network: credentials.network,
1225
+ toolNames: Object.keys(toolSchemas)
1226
+ };
1227
+ }
1228
+ async function readSignerAckFile(path) {
1229
+ try {
1230
+ const parsed = JSON.parse(await readFile(path, "utf8"));
1231
+ return typeof parsed.ack === "string" ? parsed.ack : null;
1232
+ } catch {
1233
+ return null;
1234
+ }
1235
+ }
1236
+ function writeLogChunk2(log, chunk) {
1237
+ const message = String(chunk).trimEnd();
1238
+ if (message) log(message);
1239
+ }
1240
+ var init_signer_consent = __esm({
1241
+ "src/signer-consent.ts"() {
1242
+ }
1243
+ });
1244
+
1245
+ // src/doctor.ts
1246
+ var doctor_exports = {};
1247
+ __export(doctor_exports, {
1248
+ runDoctor: () => runDoctor,
1249
+ runRepair: () => runRepair
1250
+ });
1251
+ async function discoverCredentialDirectory(homeDir, explicit) {
1252
+ if (explicit) return { directory: explicit };
1253
+ const root = join(homeDir, ".haven", "agents");
1254
+ let entries = [];
1255
+ try {
1256
+ entries = await readdir(root);
1257
+ } catch {
1258
+ return {};
1259
+ }
1260
+ const candidates = [];
1261
+ for (const entry of entries) {
1262
+ const directory = join(root, entry);
1263
+ try {
1264
+ const s = await stat(join(directory, "identity.json"));
1265
+ candidates.push({ directory, mtimeMs: s.mtimeMs });
1266
+ } catch {
1267
+ }
1268
+ }
1269
+ if (candidates.length === 0) return {};
1270
+ candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
1271
+ return {
1272
+ directory: candidates[0].directory,
1273
+ note: candidates.length > 1 ? `${candidates.length} agent credential dirs found; examining the newest.` : void 0
1274
+ };
1275
+ }
1276
+ async function runDoctor(input, deps = {}) {
1277
+ const homeDir = deps.homeDir ?? homedir();
1278
+ const checks = [];
1279
+ let signerCapabilities;
1280
+ const { directory, note } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
1281
+ let identity;
1282
+ let signerParses = false;
1283
+ if (!directory) {
1284
+ checks.push({
1285
+ id: "credentials",
1286
+ label: "Agent credentials",
1287
+ ok: false,
1288
+ detail: "No agent credential directory with an identity.json under ~/.haven/agents.",
1289
+ repair: `Run the full setup once: ${RERUN} --setup <token from the Haven dashboard>.`
1290
+ });
1291
+ } else {
1292
+ try {
1293
+ identity = JSON.parse(await readFile(join(directory, "identity.json"), "utf8"));
1294
+ } catch {
1295
+ identity = void 0;
1296
+ }
1297
+ try {
1298
+ const signer = JSON.parse(await readFile(join(directory, "signer.json"), "utf8"));
1299
+ signerParses = typeof signer === "object" && signer !== null;
1300
+ } catch {
1301
+ signerParses = false;
1302
+ }
1303
+ const ok = Boolean(identity?.api_key) && signerParses;
1304
+ checks.push({
1305
+ id: "credentials",
1306
+ label: "Agent credentials",
1307
+ ok,
1308
+ detail: ok ? `identity.json and signer.json parse (agent ${identity?.agent_id ?? "unknown"})${note ? ` \u2014 ${note}` : ""}` : "identity.json or signer.json is missing or unparseable.",
1309
+ ...ok ? {} : { repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` }
1310
+ });
1311
+ }
1312
+ let sidecar = null;
1313
+ if (directory) {
1314
+ sidecar = await readRuntimeSidecar(directory);
1315
+ if (!sidecar) {
1316
+ checks.push({
1317
+ id: "signer_runtime",
1318
+ label: "Signer runtime (preinstalled wrapper)",
1319
+ ok: false,
1320
+ detail: "No signer-runtime.json sidecar \u2014 the pinned signer runtime was never prepared (or a pre-#1586 npx config).",
1321
+ repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
1322
+ });
1323
+ } else {
1324
+ const matches = await installedRuntimeMatches(sidecar.runtime_directory, sidecar.cli_path);
1325
+ const versionOk = sidecar.signer_version === MCP_RUNTIME_MANIFEST.signerVersion;
1326
+ const ok = matches && versionOk;
1327
+ checks.push({
1328
+ id: "signer_runtime",
1329
+ label: "Signer runtime (preinstalled wrapper)",
1330
+ ok,
1331
+ detail: ok ? `Installed ${sidecar.signer_package}@${sidecar.signer_version} at ${sidecar.runtime_directory}` : matches ? `Installed version ${sidecar.signer_version} does not match the connector's pinned ${MCP_RUNTIME_MANIFEST.signerVersion}.` : `Runtime directory is stale or empty (${sidecar.runtime_directory}) \u2014 the CLI or package versions are missing.`,
1332
+ ...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
1333
+ });
1334
+ }
1335
+ }
1336
+ const configPath = runtimeConfigPathFor(input.runtime, homeDir);
1337
+ if (configPath === null) {
1338
+ checks.push({
1339
+ id: "runtime_config",
1340
+ label: "Runtime MCP config",
1341
+ ok: true,
1342
+ detail: `Runtime '${input.runtime}' has no file-based config the connector owns (CLI-managed) \u2014 skipping the file check.`
1343
+ });
1344
+ } else {
1345
+ let configText = null;
1346
+ try {
1347
+ configText = await readFile(configPath, "utf8");
1348
+ } catch {
1349
+ configText = null;
1350
+ }
1351
+ if (configText === null) {
1352
+ checks.push({
1353
+ id: "runtime_config",
1354
+ label: "Runtime MCP config",
1355
+ ok: false,
1356
+ detail: `No runtime config at ${configPath}.`,
1357
+ repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
1358
+ });
1359
+ } else {
1360
+ const hasHaven = identity?.hosted_mcp_url ? configText.includes(identity.hosted_mcp_url) : configText.includes("haven");
1361
+ const signerViaNpx = configText.includes("@haven_ai/signer");
1362
+ const wrapperReferenced = sidecar ? configText.includes(sidecar.wrapper_path) : false;
1363
+ const ok = hasHaven && !signerViaNpx && (sidecar ? wrapperReferenced : true);
1364
+ checks.push({
1365
+ id: "runtime_config",
1366
+ label: "Runtime MCP config",
1367
+ ok,
1368
+ detail: ok ? `Config at ${configPath} references the hosted server and the prepared signer wrapper.` : signerViaNpx ? `Config at ${configPath} still launches the signer via npx \u2014 the pre-#1586 shape that cannot start under a 120s startup timeout.` : `Config at ${configPath} is missing the Haven entries${sidecar && !wrapperReferenced ? " (or references a different signer wrapper)" : ""}.`,
1369
+ ...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
1370
+ });
1371
+ }
1372
+ }
1373
+ if (identity?.api_key && (identity.hosted_mcp_url || identity.api_url)) {
1374
+ const hostedUrl = identity.hosted_mcp_url ?? `${identity.api_url}/mcp`;
1375
+ const probe = await (deps.probeHosted ?? probeHostedMcpTools)(identity.api_key, hostedUrl, deps.fetch);
1376
+ checks.push({
1377
+ id: "hosted_mcp",
1378
+ label: "Hosted Haven MCP",
1379
+ ok: probe.status === "ok",
1380
+ detail: probe.status === "ok" ? `Reachable and authorized (${hostedUrl}).` : `Probe failed: ${probe.status} (${hostedUrl}).`,
1381
+ ...probe.status === "ok" ? {} : {
1382
+ repair: probe.status === "unauthorized" ? `The stored API key was rejected \u2014 re-run the full setup with a fresh token: ${RERUN} --setup <token>.` : "Check network access to the hosted MCP URL, then re-run --doctor."
1383
+ }
1384
+ });
1385
+ } else {
1386
+ checks.push({
1387
+ id: "hosted_mcp",
1388
+ label: "Hosted Haven MCP",
1389
+ ok: false,
1390
+ detail: "No stored API key / hosted MCP URL to probe with.",
1391
+ repair: `Re-run the full setup: ${RERUN} --setup <token>.`
1392
+ });
1393
+ }
1394
+ if (sidecar && directory) {
1395
+ const consent = await getLocalSignerConsentStatus(join(directory, "signer.json"));
1396
+ if (!consent.acknowledged) {
1397
+ checks.push({
1398
+ id: "signer_process",
1399
+ label: "Signer stdio handshake",
1400
+ ok: false,
1401
+ detail: "The local-tools consent is not acknowledged, so the signer refuses to start (by design).",
1402
+ repair: `Run: ${RERUN} --ack-local-tools --setup <token> (or re-run your original setup command with --ack-local-tools).`
1403
+ });
1404
+ } else {
1405
+ const probe = await (deps.probeSignerTools ?? probeLocalMcpTools)(
1406
+ sidecar.wrapper_path,
1407
+ [],
1408
+ MCP_RUNTIME_MANIFEST.requiredSignerTools
1409
+ );
1410
+ const experimental = probe.capabilities?.experimental ?? probe.capabilities;
1411
+ const compat = experimental?.["haven/signer-compatibility"];
1412
+ signerCapabilities = compat ? { "haven/signer-compatibility": compat } : void 0;
1413
+ const compatDetail = compat ? ` Compat: x402 expected-context v${JSON.stringify(compat.x402_expected_context_versions ?? "?")}.` : "";
1414
+ checks.push({
1415
+ id: "signer_process",
1416
+ label: "Signer stdio handshake",
1417
+ ok: probe.status === "ok",
1418
+ detail: probe.status === "ok" ? `Signer started, listed ${probe.toolNames?.length ?? 0} tools${probe.serverInfo?.version ? ` (v${probe.serverInfo.version})` : ""}.${compatDetail}` : `Handshake failed: ${probe.status}.`,
1419
+ ...probe.status === "ok" ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
1420
+ });
1421
+ }
1422
+ } else if (directory) {
1423
+ checks.push({
1424
+ id: "signer_process",
1425
+ label: "Signer stdio handshake",
1426
+ ok: false,
1427
+ detail: "Skipped \u2014 no prepared signer runtime to probe.",
1428
+ repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
1429
+ });
1430
+ }
1431
+ const restart = restartRequiredForRuntime(input.runtime, deps.env);
1432
+ checks.push({
1433
+ id: "restart",
1434
+ label: "Runtime restart",
1435
+ ok: true,
1436
+ detail: restart ? "This runtime loads MCP config at startup \u2014 restart it after any repair before expecting the tools to appear." : "No restart requirement known for this runtime."
1437
+ });
1438
+ return {
1439
+ version: 1,
1440
+ ok: checks.every((check) => check.ok),
1441
+ runtime: input.runtime,
1442
+ credentialDirectory: directory,
1443
+ checks,
1444
+ ...signerCapabilities ? { signerCapabilities } : {}
1445
+ };
1446
+ }
1447
+ async function runRepair(input, deps = {}) {
1448
+ const homeDir = deps.homeDir ?? homedir();
1449
+ const messages = [];
1450
+ const { directory, note } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
1451
+ if (note) messages.push(`Note: ${note}`);
1452
+ if (!directory) {
1453
+ return {
1454
+ ok: false,
1455
+ messages: [`No agent credentials found to repair \u2014 run the full setup: ${RERUN} --setup <token>.`]
1456
+ };
1457
+ }
1458
+ let identity;
1459
+ try {
1460
+ identity = JSON.parse(await readFile(join(directory, "identity.json"), "utf8"));
1461
+ } catch {
1462
+ return { ok: false, messages: ["identity.json is unreadable \u2014 re-run the full setup with a fresh token."] };
1463
+ }
1464
+ if (!identity.api_key || !(identity.hosted_mcp_url || identity.api_url)) {
1465
+ return { ok: false, messages: ["identity.json lacks the stored API key / hosted URL \u2014 re-run the full setup."] };
1466
+ }
1467
+ const configPath = runtimeConfigPathFor(input.runtime, homeDir);
1468
+ if (configPath) {
1469
+ try {
1470
+ const existing = await readFile(configPath, "utf8");
1471
+ if (existing.includes("bin/haven-mcp") || existing.includes(".haven/mcp-runtime")) {
1472
+ return {
1473
+ ok: false,
1474
+ messages: [
1475
+ `The config at ${configPath} is the LOCAL-stdio topology (--local). Repair currently rewrites only the hosted+signer shape and will not touch it.`,
1476
+ "Re-run your original setup command (with --local) to repair a local-stdio install."
1477
+ ]
1478
+ };
1479
+ }
1480
+ } catch {
1481
+ }
1482
+ }
1483
+ const signerPath = join(directory, "signer.json");
1484
+ const prepared = await prepareSignerRuntime(
1485
+ { credentialDirectory: directory, signerPath, homeDir },
1486
+ { runCommand: deps.runCommand }
1487
+ );
1488
+ messages.push(...prepared.messages);
1489
+ const configResult = await writeRuntimeConfig({
1490
+ runtime: input.runtime,
1491
+ hostedMcpUrl: identity.hosted_mcp_url ?? `${identity.api_url}/mcp`,
1492
+ apiKey: identity.api_key,
1493
+ identityPath: join(directory, "identity.json"),
1494
+ signerPath,
1495
+ credentialDirectory: directory,
1496
+ signerCommand: { command: prepared.command, args: prepared.args },
1497
+ homeDir,
1498
+ mode: "hosted"
1499
+ });
1500
+ messages.push(...configResult.messages);
1501
+ messages.push("Repair complete \u2014 restart the runtime, then verify with --doctor.");
1502
+ return { ok: true, messages };
1503
+ }
1504
+ var RERUN;
1505
+ var init_doctor = __esm({
1506
+ "src/doctor.ts"() {
1507
+ init_runtime_manifest();
1508
+ init_probes();
1509
+ init_signer_runtime();
1510
+ init_config_writers();
1511
+ init_runtime_registry();
1512
+ init_signer_consent();
1513
+ RERUN = "npx @haven_ai/connect@alpha";
1514
+ }
1515
+ });
1516
+
1517
+ // src/api.ts
1518
+ function createConnectApiClient(baseUrl, fetchImpl = fetch) {
1519
+ const root = baseUrl.replace(/\/+$/, "");
1520
+ return {
1521
+ resolveSetup: (input) => request(fetchImpl, `${root}/agent-connection-setups/resolve`, {
1522
+ method: "POST",
1523
+ body: JSON.stringify({
1524
+ setup_token: input.setupToken,
1525
+ connector_version: input.connectorVersion,
1526
+ runtime: input.runtime
1527
+ })
1528
+ }),
1529
+ registerSetup: (input) => request(fetchImpl, `${root}/agent-connection-setups/register`, {
1530
+ method: "POST",
1531
+ body: JSON.stringify({
1532
+ setup_token: input.setupToken,
1533
+ challenge_id: input.challengeId,
1534
+ delegate_address: input.delegateAddress,
1535
+ proof_signature: input.proofSignature,
1536
+ api_key_hash: input.apiKeyHash,
1537
+ api_key_prefix: input.apiKeyPrefix,
1538
+ runtime: input.runtime,
1539
+ connector_version: input.connectorVersion,
1540
+ connector_context: input.connectorContext,
1541
+ install_capabilities: input.installCapabilities && {
1542
+ can_write_runtime_config: input.installCapabilities.canWriteRuntimeConfig,
1543
+ restart_required: input.installCapabilities.restartRequired
1544
+ }
1545
+ })
1546
+ }),
1547
+ getConnectorStatus: (setupId, apiKey) => request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/connector-status`, {
1548
+ method: "GET",
1549
+ headers: { Authorization: `Bearer ${apiKey}` }
1550
+ }),
1551
+ updateInstallStatus: async (setupId, apiKey, input) => {
1552
+ await request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/install-status`, {
1553
+ method: "POST",
1554
+ headers: { Authorization: `Bearer ${apiKey}` },
1555
+ body: JSON.stringify({
1556
+ runtime: input.runtime,
1557
+ connector_version: input.connectorVersion,
1558
+ runtime_mcp_mode: input.runtimeMcpMode,
1559
+ hosted_mcp_configured: input.hostedMcpConfigured,
1560
+ local_signer_configured: input.localSignerConfigured,
1561
+ local_mcp_configured: input.localMcpConfigured,
1562
+ credential_files_written: input.credentialFilesWritten,
1563
+ signer_acknowledged: input.signerAcknowledged,
1564
+ local_mcp_acknowledged: input.localMcpAcknowledged,
1565
+ activation_command_available: input.activationCommandAvailable,
1566
+ skill_installed: input.skillInstalled,
1567
+ probe_result: input.probeResult,
1568
+ restart_required: input.restartRequired,
1569
+ next_user_action: input.nextUserAction,
1570
+ error_code: input.errorCode ?? null,
1571
+ environment_label: input.environmentLabel
1572
+ })
1573
+ });
1574
+ }
1575
+ };
1576
+ }
1577
+ var ConnectRequestError = class extends Error {
1578
+ constructor(message, status) {
1579
+ super(message);
1580
+ this.status = status;
1581
+ this.name = "ConnectRequestError";
1582
+ }
1583
+ status;
1584
+ };
1585
+ async function request(fetchImpl, url, init) {
1586
+ const response = await fetchImpl(url, {
1587
+ ...init,
1588
+ headers: {
1589
+ "Content-Type": "application/json",
1590
+ ...init.headers ?? {}
1591
+ }
1592
+ });
1593
+ const text = await response.text();
1594
+ const body = text ? JSON.parse(text) : null;
1595
+ if (!response.ok) {
1596
+ const message = body?.error ?? body?.message ?? `${response.status} ${response.statusText}`;
1597
+ throw new ConnectRequestError(`Haven setup request failed: ${message}`, response.status);
1598
+ }
1599
+ return body;
1600
+ }
1601
+ function generateDelegateKey() {
1602
+ return delegateKeyFromPrivateKey(Wallet.createRandom().privateKey);
1603
+ }
1604
+ function delegateKeyFromPrivateKey(privateKey) {
1605
+ const wallet = new Wallet(privateKey);
1606
+ return {
1607
+ privateKey: wallet.privateKey,
1608
+ address: wallet.address,
1609
+ signChallenge: (message) => wallet.signMessage(message)
1610
+ };
1611
+ }
1612
+ function generateAgentApiKey() {
1613
+ return `sk_agent_${crypto.randomBytes(24).toString("hex")}`;
1614
+ }
1615
+ function hashAgentApiKey(apiKey) {
1616
+ return crypto.createHash("sha256").update(apiKey).digest("hex");
1617
+ }
1618
+ function agentApiKeyPrefix(apiKey) {
1619
+ return apiKey.slice(0, 12);
1620
+ }
1621
+
1622
+ // src/redact.ts
1623
+ var API_KEY_RE = /sk_agent_[A-Za-z0-9]+/g;
1624
+ var PRIVATE_KEY_RE = /0x[0-9a-fA-F]{64}/g;
1625
+ function redactSecrets(value) {
1626
+ return value.replace(API_KEY_RE, "sk_agent_[redacted]").replace(PRIVATE_KEY_RE, "0x[redacted-private-key]");
1627
+ }
1628
+ function shortAddress(address) {
1629
+ if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return address;
1630
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
1631
+ }
1632
+ async function preflightCredentialStorage(input = {}) {
1633
+ const directory = defaultCredentialRoot(input.baseDir);
1634
+ await mkdir(directory, { recursive: true, mode: 448 });
1635
+ await restrictPermissions(directory, 448, input.warn);
1636
+ const probePath = join(directory, `.haven-connect-preflight-${crypto.randomBytes(8).toString("hex")}`);
1637
+ try {
1638
+ await writeOwnerOnlyJson(probePath, { ok: true }, input.warn);
1639
+ } finally {
1640
+ await rm(probePath, { force: true }).catch(() => void 0);
1641
+ }
1642
+ return directory;
1643
+ }
1644
+ async function writeCredentialFiles(input) {
1645
+ const directory = defaultAgentDirectory(input.agentId, input.baseDir);
1646
+ await mkdir(directory, { recursive: true, mode: 448 });
1647
+ await restrictPermissions(directory, 448, input.warn);
1648
+ const identityPath = join(directory, "identity.json");
1649
+ const signerPath = join(directory, "signer.json");
1650
+ const agentPath = join(directory, "agent.json");
1651
+ await assertDoesNotExist(identityPath);
1652
+ await assertDoesNotExist(signerPath);
1653
+ await assertDoesNotExist(agentPath);
1654
+ await writeOwnerOnlyJson(
1655
+ signerPath,
1656
+ {
1657
+ delegate_key: input.delegateKey,
1658
+ delegate_address: input.delegateAddress,
1659
+ agent_id: input.agentId,
1660
+ safe_address: input.safeAddress,
1661
+ chain_id: input.chainId,
1662
+ network: input.network,
1663
+ x402_binding_signer: input.x402BindingSigner,
1664
+ note: "Local signer credential. Haven backend never receives this private key."
1665
+ },
1666
+ input.warn
1667
+ );
1668
+ try {
1669
+ await writeOwnerOnlyJson(
1670
+ identityPath,
1671
+ {
1672
+ api_key: input.apiKey,
1673
+ agent_id: input.agentId,
1674
+ safe_address: input.safeAddress,
1675
+ chain_id: input.chainId,
1676
+ network: input.network,
1677
+ api_url: input.apiUrl,
1678
+ hosted_mcp_url: input.hostedMcpUrl,
1679
+ agent_budget: input.agentBudget,
1680
+ note: "Haven API key identifies the agent only. It cannot spend without the local signer key and on-chain Haven wallet rules."
1681
+ },
1682
+ input.warn
1683
+ );
1684
+ } catch (err) {
1685
+ await rm(signerPath, { force: true }).catch(() => void 0);
1686
+ throw err;
1687
+ }
1688
+ try {
1689
+ await writeOwnerOnlyJson(
1690
+ agentPath,
1691
+ {
1692
+ agent_id: input.agentId,
1693
+ delegate_address: input.delegateAddress,
1694
+ safe_address: input.safeAddress,
1695
+ chain_id: input.chainId,
1696
+ network: input.network,
1697
+ agent_budget: input.agentBudget,
1698
+ note: "Non-secret orientation for the agent: public delegate/Haven wallet identity + configured budget. Contains no API key or signing key. For the live remaining budget, call haven_get_allowances."
1699
+ },
1700
+ input.warn
1701
+ );
1702
+ } catch (err) {
1703
+ await rm(signerPath, { force: true }).catch(() => void 0);
1704
+ await rm(identityPath, { force: true }).catch(() => void 0);
1705
+ await rm(agentPath, { force: true }).catch(() => void 0);
1706
+ throw err;
1707
+ }
1708
+ return { directory, identityPath, signerPath, agentPath };
1709
+ }
1710
+ function defaultAgentDirectory(agentId, baseDir = join(homedir(), ".haven", "agents")) {
1711
+ return resolve(defaultCredentialRoot(baseDir), safePathPart(agentId));
1712
+ }
1713
+ function defaultCredentialRoot(baseDir = join(homedir(), ".haven", "agents")) {
1714
+ return resolve(baseDir);
1715
+ }
1716
+ async function writeOwnerOnlyJson(path, value, warn) {
1717
+ const json = JSON.stringify(dropUndefined(value), null, 2);
1718
+ await writeFile(path, `${json}
1719
+ `, { mode: 384, flag: "wx" });
1720
+ await restrictPermissions(path, 384, warn);
1721
+ }
1722
+ function safePathPart(value) {
1723
+ return value.replace(/[^A-Za-z0-9_.-]/g, "_");
1724
+ }
1725
+ function dropUndefined(value) {
1726
+ return Object.fromEntries(Object.entries(value).filter(([, field]) => field !== void 0));
1727
+ }
1728
+ async function assertDoesNotExist(path) {
1729
+ try {
1730
+ await access(path);
1731
+ } catch (err) {
1732
+ if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") return;
1733
+ throw err;
1734
+ }
1735
+ throw new Error(`Refusing to overwrite existing Haven credential file: ${path}`);
1736
+ }
1737
+ async function restrictPermissions(path, mode, warn) {
1738
+ try {
1739
+ await chmod(path, mode);
1740
+ } catch (err) {
1741
+ warn?.(
1742
+ `Warning: could not restrict permissions on ${path} to ${mode.toString(8)}. Move this credential to a private location or run chmod ${mode.toString(8)} ${path}. ${err instanceof Error ? err.message : String(err)}`
1743
+ );
1744
+ }
1745
+ }
1746
+
1747
+ // src/runtime-install.ts
1748
+ init_config_writers();
1749
+ async function acknowledgeLocalMcpConsent(identityPath, signerPath, log) {
1750
+ try {
1751
+ const input = await buildLocalMcpConsentInput(identityPath, signerPath);
1752
+ const decision = await ensureConsent(input, {
1753
+ credentialsPath: identityPath,
1754
+ writeAck: true,
1755
+ out: log ? { write: (chunk) => writeLogChunk(log, chunk) } : void 0
1756
+ });
1757
+ return {
1758
+ acknowledged: decision.ok,
1759
+ hash: decision.hash,
1760
+ reason: decision.reason
1761
+ };
1762
+ } catch (err) {
1763
+ return {
1764
+ acknowledged: false,
1765
+ error: err instanceof Error ? err.message : String(err)
1766
+ };
1767
+ }
1768
+ }
1769
+ async function getLocalMcpConsentStatus(identityPath, signerPath) {
1770
+ try {
1771
+ const input = await buildLocalMcpConsentInput(identityPath, signerPath);
1772
+ const hash = computeConsentHash(input);
1773
+ const stored = await readLocalMcpAckFile(localMcpAckPath(identityPath));
1774
+ if (stored === hash) {
1775
+ return { acknowledged: true, hash, reason: "ack_file_match" };
1776
+ }
1777
+ return {
1778
+ acknowledged: false,
1779
+ hash,
1780
+ reason: stored ? "ack_file_mismatch" : "ack_file_missing"
1781
+ };
1782
+ } catch (err) {
1783
+ return {
1784
+ acknowledged: false,
1785
+ error: err instanceof Error ? err.message : String(err)
1786
+ };
1787
+ }
1788
+ }
1789
+ function localMcpAckPath(identityPath) {
1790
+ return resolve(`${identityPath}.ack.json`);
1791
+ }
1792
+ async function buildLocalMcpConsentInput(identityPath, signerPath) {
1793
+ const credentials = await loadCredentials({ identityPath, signerPath });
1794
+ const unavailableDuringSetup = {
1795
+ getAllowances: async () => {
1796
+ throw new Error("Haven approval is not complete yet.");
1797
+ }
1268
1798
  };
1269
- await writeFile(input.path, `${JSON.stringify(value, null, 2)}
1270
- `, { mode: 384 });
1271
- await chmod(input.path, 384).catch(() => void 0);
1799
+ return consentInputFromClient(
1800
+ unavailableDuringSetup,
1801
+ {
1802
+ apiKey: credentials.apiKey,
1803
+ apiUrl: credentials.apiUrl,
1804
+ agentId: credentials.agentId,
1805
+ safeAddress: credentials.safeAddress,
1806
+ delegateAddress: credentials.delegateAddress,
1807
+ chainId: credentials.chainId,
1808
+ allowanceSummary: credentials.allowanceSummary
1809
+ },
1810
+ registeredToolNames()
1811
+ );
1272
1812
  }
1273
- async function assertFileExists(path, label) {
1813
+ async function readLocalMcpAckFile(path) {
1274
1814
  try {
1275
- await access(path);
1815
+ const parsed = JSON.parse(await readFile(path, "utf8"));
1816
+ return typeof parsed.ack === "string" ? parsed.ack : null;
1276
1817
  } catch {
1277
- throw new Error(`Missing ${label}: ${path}`);
1818
+ return null;
1278
1819
  }
1279
1820
  }
1821
+ function writeLogChunk(log, chunk) {
1822
+ const message = String(chunk).trimEnd();
1823
+ if (message) log(message);
1824
+ }
1825
+
1826
+ // src/runtime-install.ts
1827
+ init_probes();
1828
+
1829
+ // src/local-mcp-runtime.ts
1830
+ init_signer_runtime();
1831
+ init_runtime_manifest();
1280
1832
  var execFileAsync2 = promisify(execFile);
1281
- async function prepareSignerRuntime(input, deps = {}) {
1833
+ var UnsupportedNodeVersionError = class extends Error {
1834
+ code = "local_mcp_unsupported_node_version";
1835
+ nodeVersion;
1836
+ minimumNodeVersion;
1837
+ constructor(nodeVersion, minimumNodeVersion, subject = "Haven setup") {
1838
+ super(unsupportedNodeVersionMessage({ subject, nodeVersion, minimumNodeVersion }));
1839
+ this.name = "UnsupportedNodeVersionError";
1840
+ this.nodeVersion = nodeVersion;
1841
+ this.minimumNodeVersion = minimumNodeVersion;
1842
+ }
1843
+ };
1844
+ async function prepareLocalMcpRuntime(input, deps = {}) {
1845
+ assertSupportedNodeVersion(input.nodeVersion);
1282
1846
  const homeDir = input.homeDir ?? homedir();
1283
- const runtimeDirectory = resolve(homeDir, ".haven", "signer-runtime", MCP_RUNTIME_MANIFEST.signerVersion);
1847
+ const runtimeDirectory = resolve(homeDir, ".haven", "mcp-runtime", MCP_RUNTIME_MANIFEST.mcpVersion);
1284
1848
  const npmCacheDirectory = resolve(homeDir, ".haven", "npm-cache");
1285
- const cliPath = join(runtimeDirectory, "node_modules", "@haven_ai", "signer", "dist", "cli.js");
1849
+ const cliPath = join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "dist", "cli.js");
1286
1850
  const messages = [];
1287
1851
  await mkdir(runtimeDirectory, { recursive: true, mode: 448 });
1288
1852
  await chmod(runtimeDirectory, 448).catch(() => void 0);
1289
1853
  await mkdir(npmCacheDirectory, { recursive: true, mode: 448 });
1290
1854
  await chmod(npmCacheDirectory, 448).catch(() => void 0);
1291
1855
  if (await installedRuntimeMatches2(runtimeDirectory, cliPath)) {
1292
- messages.push(`Using existing local Haven signer runtime ${signerPackageSpec()}.`);
1856
+ messages.push(`Using existing local Haven MCP runtime ${mcpPackageSpec()}.`);
1293
1857
  } else {
1294
- await installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps.runCommand);
1295
- messages.push(`Installed local Haven signer runtime ${signerPackageSpec()}.`);
1858
+ await installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps);
1859
+ messages.push(`Installed local Haven MCP runtime ${mcpPackageSpec()}.`);
1296
1860
  }
1297
- await assertFileExists2(cliPath, "local Haven signer CLI");
1298
- const wrapperPath = join(input.credentialDirectory, "bin", "haven-signer.mjs");
1299
- await writeWrapper2({ wrapperPath, cliPath, signerPath: input.signerPath });
1861
+ await assertFileExists2(cliPath, "local Haven MCP CLI");
1862
+ const wrapperPath = join(input.credentialDirectory, "bin", "haven-mcp");
1863
+ await writeWrapper2({
1864
+ wrapperPath,
1865
+ cliPath,
1866
+ identityPath: input.identityPath,
1867
+ signerPath: input.signerPath
1868
+ });
1300
1869
  await writeRuntimeSidecar2({
1301
- path: join(input.credentialDirectory, "signer-runtime.json"),
1870
+ path: join(input.credentialDirectory, "mcp-runtime.json"),
1302
1871
  wrapperPath,
1303
1872
  runtimeDirectory,
1304
1873
  npmCacheDirectory,
1305
1874
  cliPath
1306
1875
  });
1307
- messages.push(`Prepared stable local Haven signer wrapper: ${wrapperPath}`);
1876
+ messages.push(`Prepared stable local Haven MCP wrapper: ${wrapperPath}`);
1308
1877
  return {
1309
1878
  command: wrapperPath,
1310
1879
  args: [],
@@ -1315,7 +1884,13 @@ async function prepareSignerRuntime(input, deps = {}) {
1315
1884
  messages
1316
1885
  };
1317
1886
  }
1318
- async function installRuntimePackages2(runtimeDirectory, npmCacheDirectory, runCommand) {
1887
+ function assertSupportedNodeVersion(nodeVersion = process.versions.node, minimumNodeVersion = MCP_RUNTIME_MANIFEST.minimumNodeVersion, subject = "Haven setup") {
1888
+ if (!isSupportedNodeVersion(nodeVersion, minimumNodeVersion)) {
1889
+ throw new UnsupportedNodeVersionError(nodeVersion, minimumNodeVersion, subject);
1890
+ }
1891
+ }
1892
+ async function installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps) {
1893
+ const { runCommand, onProgress } = deps;
1319
1894
  const baseArgs = [
1320
1895
  "install",
1321
1896
  "--prefix",
@@ -1324,12 +1899,22 @@ async function installRuntimePackages2(runtimeDirectory, npmCacheDirectory, runC
1324
1899
  "--no-fund",
1325
1900
  "--omit=dev",
1326
1901
  "--prefer-offline",
1327
- signerPackageSpec(),
1902
+ mcpPackageSpec(),
1328
1903
  sdkPackageSpec()
1329
1904
  ];
1330
1905
  const run = async (args) => {
1331
- if (runCommand) await runCommand("npm", args);
1332
- else await execFileAsync2("npm", args, { timeout: 12e4, maxBuffer: 1024 * 1024 });
1906
+ const startedAt = Date.now();
1907
+ const heartbeat = setInterval(() => {
1908
+ const seconds = Math.round((Date.now() - startedAt) / 1e3);
1909
+ onProgress?.(`Still installing the local Haven MCP runtime\u2026 (${seconds}s \u2014 a cold cache can take several minutes)`);
1910
+ }, SIGNER_INSTALL_HEARTBEAT_MS);
1911
+ heartbeat.unref?.();
1912
+ try {
1913
+ if (runCommand) await runCommand("npm", args);
1914
+ else await execFileAsync2("npm", args, { timeout: SIGNER_INSTALL_TIMEOUT_MS, maxBuffer: 1024 * 1024 });
1915
+ } finally {
1916
+ clearInterval(heartbeat);
1917
+ }
1333
1918
  };
1334
1919
  try {
1335
1920
  await run(baseArgs);
@@ -1337,20 +1922,18 @@ async function installRuntimePackages2(runtimeDirectory, npmCacheDirectory, runC
1337
1922
  try {
1338
1923
  await run([...baseArgs, "--cache", npmCacheDirectory]);
1339
1924
  } catch (err) {
1340
- throw new Error(
1341
- `Could not install local Haven signer runtime ${signerPackageSpec()}: ${err instanceof Error ? err.message : String(err)}`
1342
- );
1925
+ throw new Error(`Could not install local Haven MCP runtime ${mcpPackageSpec()}: ${err instanceof Error ? err.message : String(err)}`);
1343
1926
  }
1344
1927
  }
1345
1928
  }
1346
1929
  async function installedRuntimeMatches2(runtimeDirectory, cliPath) {
1347
1930
  try {
1348
- await assertFileExists2(cliPath, "local Haven signer CLI");
1349
- const [signerPackage, sdkPackage] = await Promise.all([
1350
- readPackageJson2(join(runtimeDirectory, "node_modules", "@haven_ai", "signer", "package.json")),
1931
+ await assertFileExists2(cliPath, "local Haven MCP CLI");
1932
+ const [mcpPackage, sdkPackage] = await Promise.all([
1933
+ readPackageJson2(join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "package.json")),
1351
1934
  readPackageJson2(join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
1352
1935
  ]);
1353
- return signerPackage.version === MCP_RUNTIME_MANIFEST.signerVersion && sdkPackage.version === MCP_RUNTIME_MANIFEST.sdkVersion;
1936
+ return mcpPackage.version === MCP_RUNTIME_MANIFEST.mcpVersion && sdkPackage.version === MCP_RUNTIME_MANIFEST.sdkVersion;
1354
1937
  } catch {
1355
1938
  return false;
1356
1939
  }
@@ -1366,9 +1949,10 @@ async function writeWrapper2(input) {
1366
1949
  "import { spawn } from 'node:child_process'",
1367
1950
  "",
1368
1951
  `const cliPath = ${JSON.stringify(input.cliPath)}`,
1952
+ `const identityPath = ${JSON.stringify(input.identityPath)}`,
1369
1953
  `const signerPath = ${JSON.stringify(input.signerPath)}`,
1370
1954
  "",
1371
- "const child = spawn(process.execPath, [cliPath, '--credentials', signerPath, ...process.argv.slice(2)], {",
1955
+ "const child = spawn(process.execPath, [cliPath, '--identity', identityPath, '--signer', signerPath, ...process.argv.slice(2)], {",
1372
1956
  " stdio: 'inherit',",
1373
1957
  "})",
1374
1958
  "",
@@ -1383,10 +1967,11 @@ async function writeWrapper2(input) {
1383
1967
  }
1384
1968
  async function writeRuntimeSidecar2(input) {
1385
1969
  const value = {
1386
- signer_package: MCP_RUNTIME_MANIFEST.signerPackage,
1387
- signer_version: MCP_RUNTIME_MANIFEST.signerVersion,
1970
+ mcp_package: MCP_RUNTIME_MANIFEST.mcpPackage,
1971
+ mcp_version: MCP_RUNTIME_MANIFEST.mcpVersion,
1388
1972
  sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
1389
1973
  sdk_version: MCP_RUNTIME_MANIFEST.sdkVersion,
1974
+ minimum_node_version: MCP_RUNTIME_MANIFEST.minimumNodeVersion,
1390
1975
  wrapper_path: input.wrapperPath,
1391
1976
  runtime_directory: input.runtimeDirectory,
1392
1977
  npm_cache_directory: input.npmCacheDirectory,
@@ -1403,6 +1988,10 @@ async function assertFileExists2(path, label) {
1403
1988
  throw new Error(`Missing ${label}: ${path}`);
1404
1989
  }
1405
1990
  }
1991
+
1992
+ // src/runtime-install.ts
1993
+ init_signer_runtime();
1994
+ init_runtime_manifest();
1406
1995
  var CODEX_AGENTS_BEGIN_MARKER = "<!-- BEGIN haven-pay (managed by @haven_ai/connect; edits inside this section are overwritten on re-setup) -->";
1407
1996
  var CODEX_AGENTS_END_MARKER = "<!-- END haven-pay -->";
1408
1997
  async function installSkillForRuntime(runtime, deps = {}) {
@@ -1507,208 +2096,9 @@ function hermesHome(deps) {
1507
2096
  return env.HERMES_HOME ?? join(deps.homeDir ?? homedir(), ".hermes");
1508
2097
  }
1509
2098
 
1510
- // src/runtime-registry.ts
1511
- var RUNTIME_PROFILES = {
1512
- "claude-code": {
1513
- id: "claude-code",
1514
- label: "Claude Code",
1515
- restartMode: "restart-session",
1516
- canWriteRuntimeConfig: true,
1517
- activationInstruction: "Start a new Claude Code session so it loads the Haven MCP entries."
1518
- },
1519
- "codex-cli": {
1520
- id: "codex-cli",
1521
- label: "Codex CLI",
1522
- restartMode: "restart-session",
1523
- canWriteRuntimeConfig: true,
1524
- activationInstruction: "Start a fresh Codex CLI session (for example, run `codex resume --last`)."
1525
- },
1526
- "codex-desktop": {
1527
- id: "codex-desktop",
1528
- label: "Codex Desktop",
1529
- restartMode: "restart-session",
1530
- canWriteRuntimeConfig: true,
1531
- activationInstruction: "Quit and reopen Codex Desktop so it loads the Haven MCP entries."
1532
- },
1533
- cursor: {
1534
- id: "cursor",
1535
- label: "Cursor",
1536
- restartMode: "hot-reload",
1537
- canWriteRuntimeConfig: true,
1538
- activationInstruction: "Wait for Cursor to hot-reload the Haven MCP entries; no app restart is required."
1539
- },
1540
- vscode: {
1541
- id: "vscode",
1542
- label: "VS Code",
1543
- restartMode: "hot-reload",
1544
- canWriteRuntimeConfig: true,
1545
- activationInstruction: "Wait for VS Code to hot-reload the Haven MCP entries; no app restart is required."
1546
- },
1547
- "vscode-insiders": {
1548
- id: "vscode-insiders",
1549
- label: "VS Code Insiders",
1550
- restartMode: "hot-reload",
1551
- canWriteRuntimeConfig: true,
1552
- activationInstruction: "Wait for VS Code Insiders to hot-reload the Haven MCP entries; no app restart is required."
1553
- },
1554
- "claude-desktop": {
1555
- id: "claude-desktop",
1556
- label: "Claude Desktop",
1557
- restartMode: "restart-app",
1558
- canWriteRuntimeConfig: true,
1559
- activationInstruction: "Quit and reopen Claude Desktop so it loads the Haven MCP entries."
1560
- },
1561
- hermes: {
1562
- id: "hermes",
1563
- label: "Hermes Agent",
1564
- restartMode: "restart-session",
1565
- canWriteRuntimeConfig: true,
1566
- activationInstruction: "Start a new Hermes session; in Hermes Gateway, run `/restart` instead."
1567
- },
1568
- other: {
1569
- id: "other",
1570
- label: "Other agent runtime",
1571
- restartMode: "manual",
1572
- canWriteRuntimeConfig: false,
1573
- activationInstruction: "Finish the manual MCP setup shown above, then start a fresh session in that runtime."
1574
- }
1575
- };
1576
- var RUNTIME_ALIASES = {
1577
- claude: "claude-code",
1578
- "claude-code": "claude-code",
1579
- claudecode: "claude-code",
1580
- "claude_code": "claude-code",
1581
- codex: "codex-cli",
1582
- "codex-cli": "codex-cli",
1583
- codexcli: "codex-cli",
1584
- "codex_cli": "codex-cli",
1585
- "codex-desktop": "codex-desktop",
1586
- "codex_desktop": "codex-desktop",
1587
- codexdesktop: "codex-desktop",
1588
- "codex-app": "codex-desktop",
1589
- "codex_app": "codex-desktop",
1590
- codexapp: "codex-desktop",
1591
- cursor: "cursor",
1592
- vscode: "vscode",
1593
- "vs-code": "vscode",
1594
- "vs_code": "vscode",
1595
- code: "vscode",
1596
- "vscode-insiders": "vscode-insiders",
1597
- "vscode_insiders": "vscode-insiders",
1598
- vscodeinsiders: "vscode-insiders",
1599
- "vs-code-insiders": "vscode-insiders",
1600
- "code-insiders": "vscode-insiders",
1601
- insiders: "vscode-insiders",
1602
- "claude-desktop": "claude-desktop",
1603
- "claude_desktop": "claude-desktop",
1604
- claudesktop: "claude-desktop",
1605
- desktop: "claude-desktop",
1606
- hermes: "hermes",
1607
- "hermes-agent": "hermes",
1608
- hermes_agent: "hermes",
1609
- hermesagent: "hermes",
1610
- other: "other",
1611
- manual: "other"
1612
- };
1613
- function runtimeProfile(runtime, env = process.env) {
1614
- return RUNTIME_PROFILES[normalizeRuntime(runtime, env)];
1615
- }
1616
- function normalizeRuntime(runtime, env = process.env) {
1617
- const explicit = normalizeRuntimeName(runtime);
1618
- if (explicit) return explicit;
1619
- return detectRuntime(env) ?? "other";
1620
- }
1621
- function restartRequiredForRuntime(runtime, env = process.env) {
1622
- const mode = runtimeProfile(runtime, env).restartMode;
1623
- return mode === "restart-session" || mode === "restart-app";
1624
- }
1625
- function runtimeVerificationInstruction(runtime) {
1626
- const label = RUNTIME_PROFILES[runtime].label;
1627
- return `In ${label}, run the read-only \`haven_get_agent\` and \`haven_get_allowances\` tools to confirm the Haven wallet and live budget. Do not sign, fund, or create a payment to verify setup.`;
1628
- }
1629
- function normalizeRuntimeName(runtime) {
1630
- const key = runtime?.trim().toLowerCase();
1631
- if (!key) return null;
1632
- return RUNTIME_ALIASES[key.replace(/\s+/g, "-")] ?? null;
1633
- }
1634
- function detectRuntime(env) {
1635
- if (env.CLAUDECODE || env.CLAUDE_CODE || env.CLAUDECODE_CWD) return "claude-code";
1636
- if (env.CODEX_SANDBOX || env.CODEX_HOME || env.CODEX_CWD) return "codex-cli";
1637
- if (env.VSCODE_CWD || env.VSCODE_IPC_HOOK_CLI || env.TERM_PROGRAM === "vscode") return "vscode";
1638
- if (env.HERMES_HOME || env.HERMES_AGENT) return "hermes";
1639
- return null;
1640
- }
1641
- async function acknowledgeLocalSignerConsent(signerPath, log) {
1642
- try {
1643
- const input = await buildSignerConsentInput(signerPath);
1644
- const decision = await ensureSignerConsent(input, {
1645
- credentialsPath: signerPath,
1646
- writeAck: true,
1647
- out: log ? { write: (chunk) => writeLogChunk2(log, chunk) } : void 0
1648
- });
1649
- return {
1650
- acknowledged: decision.ok,
1651
- hash: decision.hash,
1652
- reason: decision.reason
1653
- };
1654
- } catch (err) {
1655
- return {
1656
- acknowledged: false,
1657
- error: err instanceof Error ? err.message : String(err)
1658
- };
1659
- }
1660
- }
1661
- async function getLocalSignerConsentStatus(signerPath) {
1662
- try {
1663
- const input = await buildSignerConsentInput(signerPath);
1664
- const hash = computeSignerConsentHash(input);
1665
- const stored = await readSignerAckFile(signerAckPath(signerPath));
1666
- if (stored === hash) {
1667
- return { acknowledged: true, hash, reason: "ack_file_match" };
1668
- }
1669
- return {
1670
- acknowledged: false,
1671
- hash,
1672
- reason: stored ? "ack_file_mismatch" : "ack_file_missing"
1673
- };
1674
- } catch (err) {
1675
- return {
1676
- acknowledged: false,
1677
- error: err instanceof Error ? err.message : String(err)
1678
- };
1679
- }
1680
- }
1681
- function signerAckPath(signerPath) {
1682
- return resolve(`${signerPath}.signer-ack.json`);
1683
- }
1684
- async function buildSignerConsentInput(signerPath) {
1685
- const credentials = await loadSignerCredentials(signerPath);
1686
- const signer = createEdgeSigner(credentials.delegateKey, {
1687
- x402BindingSigner: credentials.x402BindingSigner
1688
- });
1689
- return {
1690
- delegateAddress: signer.delegateAddress,
1691
- safeAddress: credentials.safeAddress,
1692
- agentId: credentials.agentId,
1693
- chainId: credentials.chainId,
1694
- network: credentials.network,
1695
- toolNames: Object.keys(toolSchemas)
1696
- };
1697
- }
1698
- async function readSignerAckFile(path) {
1699
- try {
1700
- const parsed = JSON.parse(await readFile(path, "utf8"));
1701
- return typeof parsed.ack === "string" ? parsed.ack : null;
1702
- } catch {
1703
- return null;
1704
- }
1705
- }
1706
- function writeLogChunk2(log, chunk) {
1707
- const message = String(chunk).trimEnd();
1708
- if (message) log(message);
1709
- }
1710
-
1711
2099
  // src/runtime-install.ts
2100
+ init_runtime_registry();
2101
+ init_signer_consent();
1712
2102
  var execFileAsync3 = promisify(execFile);
1713
2103
  async function installRuntime(input, deps = {}) {
1714
2104
  const runtime = normalizeRuntime(input.runtime, deps.env);
@@ -1784,9 +2174,28 @@ async function installRuntime(input, deps = {}) {
1784
2174
  signerCommand = { command: signerRuntime.command, args: signerRuntime.args };
1785
2175
  consentMessages.push(...signerRuntime.messages);
1786
2176
  } catch (err) {
1787
- consentMessages.push(
1788
- `Could not pre-install the local Haven signer; falling back to npx launch: ${err instanceof Error ? err.message : String(err)}`
1789
- );
2177
+ return {
2178
+ runtime,
2179
+ runtimeMcpMode: "hosted_plus_signer",
2180
+ hostedMcpConfigured: false,
2181
+ localSignerConfigured: false,
2182
+ localMcpConfigured: false,
2183
+ probeResult: "signer_runtime_install_failed",
2184
+ restartRequired: false,
2185
+ nextUserAction: "The local Haven signer runtime could not be installed, so no configuration was written. Check your network (a cold install downloads the signer package set) and re-run: npx @haven_ai/connect@alpha",
2186
+ errorCode: "signer_runtime_install_failed",
2187
+ configTarget: profile.label,
2188
+ signerAcknowledged: signerConsent?.acknowledged,
2189
+ localMcpAcknowledged: localMcpConsent?.acknowledged,
2190
+ activationCommand: void 0,
2191
+ signerRuntimePrepared: false,
2192
+ messages: [
2193
+ ...consentMessages,
2194
+ `Could not pre-install the local Haven signer: ${err instanceof Error ? err.message : String(err)}`,
2195
+ "No runtime configuration was written (fail-closed): a config pointing at an uninstalled signer looks wired but cannot start.",
2196
+ "Re-run `npx @haven_ai/connect@alpha` to retry the setup."
2197
+ ]
2198
+ };
1790
2199
  }
1791
2200
  }
1792
2201
  const signerRuntimePrepared = localRuntime ? void 0 : signerCommand !== void 0;
@@ -1803,19 +2212,51 @@ async function installRuntime(input, deps = {}) {
1803
2212
  homeDir: deps.homeDir,
1804
2213
  mode: localRuntime ? "local" : "hosted"
1805
2214
  });
2215
+ if (deps.onRuntimeConfigured) {
2216
+ const signerCredentialOnDisk = await probeLocalSignerCredential(input.signerPath);
2217
+ const earlyLocalMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialOnDisk && Boolean(localMcpConsent?.acknowledged);
2218
+ const earlySignerOk = configResult.runtimeMcpMode === "local_stdio" ? earlyLocalMcpOk : configResult.signerConfigured && signerCredentialOnDisk && Boolean(signerConsent?.acknowledged);
2219
+ try {
2220
+ await deps.onRuntimeConfigured({
2221
+ runtime,
2222
+ runtimeMcpMode: configResult.runtimeMcpMode,
2223
+ hostedMcpConfigured: configResult.hostedConfigured,
2224
+ localSignerConfigured: earlySignerOk,
2225
+ localMcpConfigured: earlyLocalMcpOk,
2226
+ signerAcknowledged: signerConsent?.acknowledged,
2227
+ localMcpAcknowledged: localMcpConsent?.acknowledged,
2228
+ restartRequired: configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env),
2229
+ nextUserAction: nextAction(runtime, profile.restartMode, configResult.errorCode),
2230
+ errorCode: configResult.errorCode
2231
+ });
2232
+ } catch {
2233
+ }
2234
+ }
1806
2235
  progress("Almost there \u2014 just confirming everything connects\u2026");
1807
2236
  const localProbePromise = configResult.runtimeMcpMode === "local_stdio" && localRuntimeInstall ? runLocalMcpProbe(localRuntimeInstall, deps) : Promise.resolve(void 0);
1808
- const [hostedProbe, signerCredentialReady, localMcpProbe] = await Promise.all([
2237
+ const signerProbePromise = configResult.runtimeMcpMode !== "local_stdio" && signerCommand ? (deps.probeSignerTools ?? probeLocalMcpTools)(
2238
+ signerCommand.command,
2239
+ signerCommand.args,
2240
+ MCP_RUNTIME_MANIFEST.requiredSignerTools
2241
+ ) : Promise.resolve(void 0);
2242
+ const [hostedProbe, signerCredentialReady, localMcpProbe, signerProbe] = await Promise.all([
1809
2243
  configResult.hostedConfigured ? probeHostedMcpTools(input.apiKey, input.hostedMcpUrl, deps.fetch) : Promise.resolve({ status: "bad_response" }),
1810
2244
  probeLocalSignerCredential(input.signerPath),
1811
- localProbePromise
2245
+ localProbePromise,
2246
+ signerProbePromise
1812
2247
  ]);
1813
2248
  const hostedOk = configResult.hostedConfigured && hostedProbe.status === "ok";
1814
2249
  const localMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialReady && Boolean(localMcpConsent?.acknowledged) && localMcpProbe?.status === "ok";
1815
- const signerOk = configResult.runtimeMcpMode === "local_stdio" ? localMcpOk : configResult.signerConfigured && signerCredentialReady && Boolean(signerConsent?.acknowledged);
2250
+ const signerOk = configResult.runtimeMcpMode === "local_stdio" ? localMcpOk : configResult.signerConfigured && signerCredentialReady && Boolean(signerConsent?.acknowledged) && // #1587: no handshake, no green. A signer command that was registered
2251
+ // but not probed (manual topology) keeps the old semantics.
2252
+ (signerProbe === void 0 || signerProbe.status === "ok");
1816
2253
  const restartRequired = configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env);
1817
- const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : hostedMcpErrorCode(configResult.hostedConfigured, hostedProbe.status) ?? signerConsentErrorCode(signerCredentialReady, signerConsent));
2254
+ const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : hostedMcpErrorCode(configResult.hostedConfigured, hostedProbe.status) ?? signerConsentErrorCode(signerCredentialReady, signerConsent) ?? signerProbeErrorCode(signerProbe));
1818
2255
  const hostedProbeMessages = configResult.hostedConfigured && hostedProbe.status !== "ok" ? [`Hosted Haven MCP probe failed: ${hostedProbe.status}.`] : configResult.hostedConfigured ? ["Verified hosted Haven MCP tools with a read-only handshake."] : [];
2256
+ const signerProbeMessages = signerProbe ? signerProbe.status === "ok" ? ["Verified local Haven signer with a stdio handshake."] : [
2257
+ `Local Haven signer handshake failed: ${signerProbe.status}.`,
2258
+ "Re-run `npx @haven_ai/connect@alpha` to repair the signer setup."
2259
+ ] : [];
1819
2260
  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."] : [];
1820
2261
  const skillInstall = !configResult.errorCode ? await installSkillForRuntime(runtime, { homeDir: deps.homeDir, env: deps.env }) : void 0;
1821
2262
  return {
@@ -1834,7 +2275,7 @@ async function installRuntime(input, deps = {}) {
1834
2275
  activationCommand: configResult.activationCommand,
1835
2276
  skillInstalled: skillInstall?.installed,
1836
2277
  signerRuntimePrepared,
1837
- messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...hostedProbeMessages, ...localProbeMessages, ...skillInstall?.messages ?? []]
2278
+ messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...hostedProbeMessages, ...signerProbeMessages, ...localProbeMessages, ...skillInstall?.messages ?? []]
1838
2279
  };
1839
2280
  }
1840
2281
  function runtimeInstallCapabilities(runtime, env = process.env) {
@@ -1854,10 +2295,11 @@ async function configureClaudeCode(deps, localMcpCommand) {
1854
2295
  });
1855
2296
  try {
1856
2297
  if (!localMcpCommand) throw new Error("local MCP wrapper command is required");
2298
+ await runCommand("claude", ["mcp", "remove", "haven"]).catch(() => void 0);
2299
+ await runCommand("claude", ["mcp", "remove", "haven-signer"]).catch(() => void 0);
1857
2300
  await runCommand("claude", ["mcp", "add-json", "haven", serverJson, "--scope", "user"]).catch(async () => {
1858
2301
  await runCommand("claude", ["mcp", "add", "haven", "--scope", "user", "--", localMcpCommand]);
1859
2302
  });
1860
- await runCommand("claude", ["mcp", "remove", "haven-signer"]).catch(() => void 0);
1861
2303
  const verified = await runCommand("claude", ["mcp", "get", "haven"]).then(() => true).catch(() => false);
1862
2304
  return {
1863
2305
  hostedConfigured: false,
@@ -1979,6 +2421,10 @@ function signerConsentErrorCode(signerCredentialReady, signerConsent) {
1979
2421
  if (!signerConsent?.acknowledged) return "local_signer_ack_required";
1980
2422
  return void 0;
1981
2423
  }
2424
+ function signerProbeErrorCode(probe) {
2425
+ if (!probe || probe.status === "ok") return void 0;
2426
+ return `local_signer_probe_${probe.status}`;
2427
+ }
1982
2428
  function hostedMcpErrorCode(hostedConfigured, hostedProbeStatus) {
1983
2429
  if (!hostedConfigured || hostedProbeStatus === "ok") return void 0;
1984
2430
  return `hosted_mcp_probe_${hostedProbeStatus}`;
@@ -2002,7 +2448,7 @@ function supportsLocalMcp(runtime) {
2002
2448
  return runtime === "codex-cli" || runtime === "codex-desktop" || runtime === "claude-code";
2003
2449
  }
2004
2450
  async function prepareRuntimeForLocalMcp(input, deps) {
2005
- const prepare = deps.prepareLocalMcpRuntime ?? ((runtimeInput) => prepareLocalMcpRuntime(runtimeInput, { runCommand: deps.runCommand }));
2451
+ const prepare = deps.prepareLocalMcpRuntime ?? ((runtimeInput) => prepareLocalMcpRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress }));
2006
2452
  return prepare({
2007
2453
  credentialDirectory: input.credentialDirectory,
2008
2454
  identityPath: input.identityPath,
@@ -2011,7 +2457,13 @@ async function prepareRuntimeForLocalMcp(input, deps) {
2011
2457
  });
2012
2458
  }
2013
2459
  async function prepareSignerForRuntime(input, deps) {
2014
- const prepare = deps.prepareSignerRuntime ?? ((runtimeInput) => prepareSignerRuntime(runtimeInput, { runCommand: deps.runCommand }));
2460
+ const prepare = deps.prepareSignerRuntime ?? ((runtimeInput) => (
2461
+ // onProgress threaded through on purpose (#1586 review): without it the
2462
+ // install heartbeat was dead code in production and the console still
2463
+ // went silent for the whole cold install — the exact symptom the issue
2464
+ // set out to remove, at a longer timeout.
2465
+ prepareSignerRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress })
2466
+ ));
2015
2467
  return prepare({
2016
2468
  credentialDirectory: input.credentialDirectory,
2017
2469
  signerPath: input.signerPath,
@@ -2034,7 +2486,9 @@ function localRuntimePrepareErrorCode(err) {
2034
2486
  }
2035
2487
 
2036
2488
  // src/runtime.ts
2037
- var CONNECTOR_VERSION = "0.1.26-alpha.0";
2489
+ init_runtime_registry();
2490
+ init_runtime_manifest();
2491
+ var CONNECTOR_VERSION = "0.1.27-alpha.0";
2038
2492
  var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
2039
2493
  async function runConnect(options, deps = {}) {
2040
2494
  assertSupportedNodeVersion(deps.nodeVersion, MCP_RUNTIME_MANIFEST.minimumNodeVersion);
@@ -2141,7 +2595,35 @@ async function runConnect(options, deps = {}) {
2141
2595
  ackSigner: options.ackSigner,
2142
2596
  ackLocalTools: options.ackLocalTools,
2143
2597
  localMcp: options.localMcp
2144
- }, { onProgress: log });
2598
+ }, {
2599
+ onProgress: log,
2600
+ // #1543: report "runtime configured" the moment the config write settles,
2601
+ // so the dashboard can expose its budget-approval controls without
2602
+ // waiting on the probes and skill install — a tail that approval does not
2603
+ // depend on. Silent and best-effort: the complete report below remains
2604
+ // authoritative (it overwrites these keys, adding the probe verdicts and
2605
+ // skill state), so an early failure costs nothing but the head start.
2606
+ onRuntimeConfigured: async (early) => {
2607
+ try {
2608
+ await api.updateInstallStatus(registration.setup_id, localApiKey, {
2609
+ runtime: early.runtime,
2610
+ connectorVersion,
2611
+ runtimeMcpMode: early.runtimeMcpMode,
2612
+ hostedMcpConfigured: early.hostedMcpConfigured,
2613
+ localSignerConfigured: early.localSignerConfigured,
2614
+ localMcpConfigured: early.localMcpConfigured,
2615
+ credentialFilesWritten: true,
2616
+ signerAcknowledged: early.signerAcknowledged,
2617
+ localMcpAcknowledged: early.localMcpAcknowledged,
2618
+ restartRequired: early.restartRequired,
2619
+ nextUserAction: early.nextUserAction,
2620
+ errorCode: early.errorCode ?? null,
2621
+ environmentLabel: options.environmentLabel ?? "Local workspace"
2622
+ });
2623
+ } catch {
2624
+ }
2625
+ }
2626
+ });
2145
2627
  printRuntimeInstall(runtimeInstall, log);
2146
2628
  if (runtimeInstall.errorCode) {
2147
2629
  log("Haven setup needs a couple more steps on this machine \u2014 see the notes above.");
@@ -2170,10 +2652,11 @@ async function runConnect(options, deps = {}) {
2170
2652
  } catch (err) {
2171
2653
  log(`Could not report install status to Haven: ${err instanceof Error ? err.message : String(err)}`);
2172
2654
  }
2655
+ let approval;
2173
2656
  if (options.waitForApproval !== false && !runtimeInstall.errorCode) {
2174
- await waitForBudgetApproval(api, registration.setup_id, localApiKey, log, options.approvalWait);
2657
+ approval = await waitForBudgetApproval(api, registration.setup_id, localApiKey, log, options.approvalWait);
2175
2658
  }
2176
- printNextSteps(runtimeInstall, log);
2659
+ printNextSteps(runtimeInstall, log, approval);
2177
2660
  return {
2178
2661
  setupId: registration.setup_id,
2179
2662
  agentId: registration.agent_id,
@@ -2314,9 +2797,14 @@ async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
2314
2797
  const sleep = options.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
2315
2798
  const maxPolls = Math.max(1, Math.floor(timeoutMs / intervalMs));
2316
2799
  const remindEvery = Math.max(1, Math.floor(3e4 / intervalMs));
2317
- log("Registered with Haven \u2014 waiting for you to approve the budget in the dashboard\u2026");
2800
+ let waitingAnnounced = false;
2801
+ const announceWaiting = () => {
2802
+ if (waitingAnnounced) return;
2803
+ waitingAnnounced = true;
2804
+ log("Registered with Haven \u2014 waiting for you to approve the budget in the dashboard\u2026");
2805
+ };
2318
2806
  for (let i = 0; i < maxPolls; i++) {
2319
- await sleep(intervalMs);
2807
+ if (i > 0) await sleep(intervalMs);
2320
2808
  let status;
2321
2809
  try {
2322
2810
  status = await api.getConnectorStatus(setupId, apiKey);
@@ -2325,6 +2813,7 @@ async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
2325
2813
  log("This setup ended in Haven \u2014 start a fresh connection from the dashboard when ready.");
2326
2814
  return "ended";
2327
2815
  }
2816
+ announceWaiting();
2328
2817
  continue;
2329
2818
  }
2330
2819
  if (status.status === "active") {
@@ -2337,7 +2826,8 @@ async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
2337
2826
  log(`This setup ended in Haven (${status.status}) \u2014 start a fresh connection from the dashboard when ready.`);
2338
2827
  return "ended";
2339
2828
  }
2340
- if ((i + 1) % remindEvery === 0) {
2829
+ announceWaiting();
2830
+ if (i > 0 && i % remindEvery === 0) {
2341
2831
  log("Still waiting for budget approval in Haven\u2026");
2342
2832
  }
2343
2833
  }
@@ -2346,11 +2836,11 @@ async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
2346
2836
  );
2347
2837
  return "pending";
2348
2838
  }
2349
- function completionHandoffLines(result) {
2839
+ function completionHandoffLines(result, approval) {
2350
2840
  if (result.errorCode === "manual_runtime_setup_required") {
2351
2841
  return [
2352
2842
  "Next steps:",
2353
- "1. Return to Haven and approve the agent rules. Approval \u2014 not restarting \u2014 unlocks Haven tools.",
2843
+ "1. Return to Haven and approve the budget. Approval \u2014 not restarting \u2014 unlocks Haven tools.",
2354
2844
  "2. Finish the manual MCP setup using the secret-free file references printed above, then start a fresh session in your runtime.",
2355
2845
  `3. ${runtimeVerificationInstruction(result.runtime)}`
2356
2846
  ];
@@ -2360,16 +2850,38 @@ function completionHandoffLines(result) {
2360
2850
  "Recovery: runtime setup is not complete. Resolve the reported problem, then return to Haven for a fresh connection and run its new Connect command. Do not manually edit runtime config or paste credentials into prompts, logs, or config."
2361
2851
  ];
2362
2852
  }
2853
+ if (approval === "ended") {
2854
+ return [
2855
+ "Next step: this setup ended in Haven before the budget was approved, so there is nothing left to approve on it \u2014 start a fresh connection from the dashboard when ready."
2856
+ ];
2857
+ }
2363
2858
  const profile = runtimeProfile(result.runtime);
2859
+ const activation = activationInstructionWithWhy(profile);
2860
+ if (approval === "approved") {
2861
+ return [
2862
+ "Next steps \u2014 the budget is already approved, so there is nothing more to do in Haven:",
2863
+ `1. ${activation}`,
2864
+ `2. ${runtimeVerificationInstruction(result.runtime)}`
2865
+ ];
2866
+ }
2364
2867
  return [
2365
2868
  "Next steps:",
2366
- "1. Return to Haven and approve the agent rules. Approval \u2014 not restarting \u2014 unlocks Haven tools.",
2367
- `2. ${profile.activationInstruction}`,
2869
+ "1. Return to Haven and approve the budget. Approval \u2014 not restarting \u2014 unlocks Haven tools.",
2870
+ `2. ${activation}`,
2368
2871
  `3. ${runtimeVerificationInstruction(result.runtime)}`
2369
2872
  ];
2370
2873
  }
2371
- function printNextSteps(result, log) {
2372
- for (const line of completionHandoffLines(result)) log(line);
2874
+ function activationInstructionWithWhy(profile) {
2875
+ if (profile.restartMode === "restart-session") {
2876
+ return `${profile.activationInstruction} (The entries are already written and verified \u2014 ${profile.label} only reads MCP config when a session starts, which is why this step is still needed.)`;
2877
+ }
2878
+ if (profile.restartMode === "restart-app") {
2879
+ return `${profile.activationInstruction} (The entries are already written and verified \u2014 ${profile.label} only reads MCP config at app launch, which is why this step is still needed.)`;
2880
+ }
2881
+ return profile.activationInstruction;
2882
+ }
2883
+ function printNextSteps(result, log, approval) {
2884
+ for (const line of completionHandoffLines(result, approval)) log(line);
2373
2885
  }
2374
2886
 
2375
2887
  // src/args.ts
@@ -2380,12 +2892,18 @@ function parseArgs(argv, env = process.env) {
2380
2892
  };
2381
2893
  let help = false;
2382
2894
  let json = false;
2895
+ let doctor = false;
2896
+ let repair = false;
2383
2897
  for (let i = 0; i < argv.length; i += 1) {
2384
2898
  const arg = argv[i];
2385
2899
  if (arg === "--help" || arg === "-h") {
2386
2900
  help = true;
2387
2901
  } else if (arg === "--json") {
2388
2902
  json = true;
2903
+ } else if (arg === "--doctor") {
2904
+ doctor = true;
2905
+ } else if (arg === "--repair") {
2906
+ repair = true;
2389
2907
  } else if (arg === "--setup" || arg === "--setup-token") {
2390
2908
  options.setupToken = requireValue(argv, ++i, arg);
2391
2909
  } else if (arg === "--api" || arg === "--api-url") {
@@ -2412,7 +2930,13 @@ function parseArgs(argv, env = process.env) {
2412
2930
  }
2413
2931
  }
2414
2932
  if (help) {
2415
- return { options, help, json };
2933
+ return { options, help, json, doctor, repair };
2934
+ }
2935
+ if (doctor || repair) {
2936
+ if (!options.runtime) {
2937
+ throw new Error("--doctor/--repair need --runtime <runtime> (which config to examine).");
2938
+ }
2939
+ return { options, help, json, doctor, repair };
2416
2940
  }
2417
2941
  if (!options.setupToken) {
2418
2942
  throw new Error("Missing --setup <hv_setup_...> setup token.");
@@ -2421,7 +2945,7 @@ function parseArgs(argv, env = process.env) {
2421
2945
  throw new Error("Missing --api <Haven API URL>.");
2422
2946
  }
2423
2947
  options.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, "");
2424
- return { options, help, json };
2948
+ return { options, help, json, doctor, repair };
2425
2949
  }
2426
2950
  function helpText() {
2427
2951
  return [
@@ -2444,6 +2968,11 @@ function helpText() {
2444
2968
  " --local Advanced: install the fully-local Haven MCP (no hosted dependency).",
2445
2969
  " Only available for Claude Code and Codex. Default is hosted MCP + local signer.",
2446
2970
  " --json Emit one versioned, secret-free result object on stdout; progress stays on stderr.",
2971
+ " --doctor Diagnose an existing setup (read-only, no token): config, credentials,",
2972
+ " signer runtime, hosted MCP, and a live signer handshake. Exits non-zero on any failure.",
2973
+ " --repair Repair, then re-diagnose (implies --doctor): reinstall the pinned signer",
2974
+ " runtime, rewrite the wrapper and runtime config from stored credentials.",
2975
+ " Hosted topology only (refuses to touch a --local config). No keys, no token.",
2447
2976
  " --help Show this help.",
2448
2977
  "",
2449
2978
  "The connector never prints the private key and never sends it to Haven. JSON output never includes credential contents or full credential paths."
@@ -2481,6 +3010,37 @@ async function runCli(argv, io = {
2481
3010
  `);
2482
3011
  return 0;
2483
3012
  }
3013
+ if (parsed.doctor || parsed.repair) {
3014
+ const { runDoctor: runDoctor2, runRepair: runRepair2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
3015
+ const runtime = parsed.options.runtime ?? "";
3016
+ const credentialsDir = parsed.options.credentialsDir;
3017
+ try {
3018
+ if (parsed.repair) {
3019
+ const repair = await runRepair2({ runtime, credentialsDir });
3020
+ for (const message of repair.messages) io.stderr(`${redactSecrets(message)}
3021
+ `);
3022
+ if (!repair.ok) return 1;
3023
+ }
3024
+ const report = await runDoctor2({ runtime, credentialsDir });
3025
+ if (parsed.json) {
3026
+ io.stdout(`${redactSecrets(JSON.stringify(report))}
3027
+ `);
3028
+ } else {
3029
+ for (const check of report.checks) {
3030
+ io.stdout(redactSecrets(`${check.ok ? "\u2713" : "\u2717"} ${check.label}: ${check.detail}
3031
+ `));
3032
+ if (check.repair) io.stdout(redactSecrets(` \u21B3 repair: ${check.repair}
3033
+ `));
3034
+ }
3035
+ io.stdout(report.ok ? "All checks passed.\n" : "One or more checks FAILED \u2014 see repairs above.\n");
3036
+ }
3037
+ return report.ok ? 0 : 1;
3038
+ } catch (err) {
3039
+ io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
3040
+ `);
3041
+ return 1;
3042
+ }
3043
+ }
2484
3044
  try {
2485
3045
  const result = await runConnect(
2486
3046
  { ...parsed.options, waitForApproval: !parsed.json },