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