@haven_ai/connect 0.1.28-alpha.0 → 0.1.30-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,17 @@
1
1
  'use strict';
2
2
 
3
- var mcp = require('@haven_ai/mcp');
4
- var signer = require('@haven_ai/signer');
5
- var sdk = require('@haven_ai/sdk');
3
+ var crypto = require('crypto');
4
+ var ethers = require('ethers');
6
5
  var promises = require('fs/promises');
7
6
  var os = require('os');
8
7
  var path = require('path');
8
+ var mcp = require('@haven_ai/mcp');
9
+ var signer = require('@haven_ai/signer');
10
+ var sdk = require('@haven_ai/sdk');
9
11
  var yaml = require('yaml');
10
12
  var child_process = require('child_process');
11
13
  var util = require('util');
12
- var crypto = require('crypto');
13
- var ethers = require('ethers');
14
+ var readline = require('readline');
14
15
  var fs = require('fs');
15
16
  var url = require('url');
16
17
 
@@ -28,125 +29,605 @@ var __export = (target, all) => {
28
29
  for (var name in all)
29
30
  __defProp(target, name, { get: all[name], enumerable: true });
30
31
  };
31
- function mcpPackageSpec() {
32
- return `${exports.MCP_RUNTIME_MANIFEST.mcpPackage}@${exports.MCP_RUNTIME_MANIFEST.mcpVersion}`;
33
- }
34
- function sdkPackageSpec() {
35
- return `${exports.MCP_RUNTIME_MANIFEST.sdkPackage}@${exports.MCP_RUNTIME_MANIFEST.sdkVersion}`;
32
+
33
+ // src/api.ts
34
+ function createConnectApiClient(baseUrl, fetchImpl = fetch) {
35
+ const root = baseUrl.replace(/\/+$/, "");
36
+ return {
37
+ resolveSetup: (input) => request(fetchImpl, `${root}/agent-connection-setups/resolve`, {
38
+ method: "POST",
39
+ body: JSON.stringify({
40
+ setup_token: input.setupToken,
41
+ connector_version: input.connectorVersion,
42
+ runtime: input.runtime
43
+ })
44
+ }),
45
+ registerSetup: (input) => request(fetchImpl, `${root}/agent-connection-setups/register`, {
46
+ method: "POST",
47
+ body: JSON.stringify({
48
+ setup_token: input.setupToken,
49
+ challenge_id: input.challengeId,
50
+ delegate_address: input.delegateAddress,
51
+ proof_signature: input.proofSignature,
52
+ api_key_hash: input.apiKeyHash,
53
+ api_key_prefix: input.apiKeyPrefix,
54
+ runtime: input.runtime,
55
+ connector_version: input.connectorVersion,
56
+ mcp_server_name: input.mcpServerName,
57
+ connector_context: input.connectorContext,
58
+ install_capabilities: input.installCapabilities && {
59
+ can_write_runtime_config: input.installCapabilities.canWriteRuntimeConfig,
60
+ restart_required: input.installCapabilities.restartRequired
61
+ }
62
+ })
63
+ }),
64
+ getAgentIdentity: (apiKey) => request(fetchImpl, `${root}/machine-payments/agent`, {
65
+ method: "GET",
66
+ headers: { Authorization: `Bearer ${apiKey}` }
67
+ }),
68
+ getConnectorStatus: (setupId, apiKey) => request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/connector-status`, {
69
+ method: "GET",
70
+ headers: { Authorization: `Bearer ${apiKey}` }
71
+ }),
72
+ updateInstallStatus: async (setupId, apiKey, input) => {
73
+ await request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/install-status`, {
74
+ method: "POST",
75
+ headers: { Authorization: `Bearer ${apiKey}` },
76
+ body: JSON.stringify({
77
+ runtime: input.runtime,
78
+ connector_version: input.connectorVersion,
79
+ runtime_mcp_mode: input.runtimeMcpMode,
80
+ hosted_mcp_configured: input.hostedMcpConfigured,
81
+ local_signer_configured: input.localSignerConfigured,
82
+ local_mcp_configured: input.localMcpConfigured,
83
+ credential_files_written: input.credentialFilesWritten,
84
+ signer_acknowledged: input.signerAcknowledged,
85
+ local_mcp_acknowledged: input.localMcpAcknowledged,
86
+ activation_command_available: input.activationCommandAvailable,
87
+ skill_installed: input.skillInstalled,
88
+ probe_result: input.probeResult,
89
+ restart_required: input.restartRequired,
90
+ next_user_action: input.nextUserAction,
91
+ error_code: input.errorCode ?? null,
92
+ environment_label: input.environmentLabel
93
+ })
94
+ });
95
+ }
96
+ };
36
97
  }
37
- function signerPackageSpec() {
38
- return `${exports.MCP_RUNTIME_MANIFEST.signerPackage}@${exports.MCP_RUNTIME_MANIFEST.signerVersion}`;
98
+ async function request(fetchImpl, url, init) {
99
+ const response = await fetchImpl(url, {
100
+ ...init,
101
+ headers: {
102
+ "Content-Type": "application/json",
103
+ ...init.headers ?? {}
104
+ }
105
+ });
106
+ const text = await response.text();
107
+ const body = text ? JSON.parse(text) : null;
108
+ if (!response.ok) {
109
+ const message = body?.error ?? body?.message ?? `${response.status} ${response.statusText}`;
110
+ throw new ConnectRequestError(`Haven setup request failed: ${message}`, response.status);
111
+ }
112
+ return body;
39
113
  }
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.28-alpha.0",
48
- signerPackage: "@haven_ai/signer",
49
- signerVersion: "0.1.28-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)
114
+ var ConnectRequestError;
115
+ var init_api = __esm({
116
+ "src/api.ts"() {
117
+ ConnectRequestError = class extends Error {
118
+ constructor(message, status) {
119
+ super(message);
120
+ this.status = status;
121
+ this.name = "ConnectRequestError";
122
+ }
123
+ status;
66
124
  };
67
125
  }
68
126
  });
69
- async function writeRuntimeConfig(input, deps = {}) {
70
- switch (input.runtime) {
71
- case "codex-cli":
72
- case "codex-desktop":
73
- return writeCodexConfig(input);
74
- case "cursor":
75
- return writeJsonRuntimeConfig(input, cursorConfigPath(input.homeDir), "mcpServers");
76
- case "vscode":
77
- return writeJsonRuntimeConfig(input, vscodeConfigPath(input.homeDir), "servers");
78
- case "vscode-insiders":
79
- return writeJsonRuntimeConfig(input, vscodeInsidersConfigPath(input.homeDir), "servers");
80
- case "claude-desktop":
81
- return writeJsonRuntimeConfig(input, claudeDesktopConfigPath(input.homeDir), "mcpServers");
82
- case "hermes":
83
- return writeHermesConfig(input, deps);
84
- default:
85
- return {
86
- hostedConfigured: false,
87
- signerConfigured: false,
88
- localMcpConfigured: false,
89
- runtimeMcpMode: "manual",
90
- target: "manual runtime setup",
91
- changed: false,
92
- restartRequired: true,
93
- messages: ["Runtime config needs to be added manually for this agent environment."],
94
- errorCode: "manual_runtime_setup_required"
95
- };
127
+ function generateDelegateKey() {
128
+ return delegateKeyFromPrivateKey(ethers.Wallet.createRandom().privateKey);
129
+ }
130
+ function delegateKeyFromPrivateKey(privateKey) {
131
+ const wallet = new ethers.Wallet(privateKey);
132
+ return {
133
+ privateKey: wallet.privateKey,
134
+ address: wallet.address,
135
+ signChallenge: (message) => wallet.signMessage(message)
136
+ };
137
+ }
138
+ function generateAgentApiKey() {
139
+ return `sk_agent_${crypto__default.default.randomBytes(24).toString("hex")}`;
140
+ }
141
+ function hashAgentApiKey(apiKey) {
142
+ return crypto__default.default.createHash("sha256").update(apiKey).digest("hex");
143
+ }
144
+ function agentApiKeyPrefix(apiKey) {
145
+ return apiKey.slice(0, 12);
146
+ }
147
+ var init_key = __esm({
148
+ "src/key.ts"() {
96
149
  }
150
+ });
151
+
152
+ // src/redact.ts
153
+ function redactSecrets(value) {
154
+ return value.replace(API_KEY_RE, "sk_agent_[redacted]").replace(PRIVATE_KEY_RE, "0x[redacted-private-key]");
97
155
  }
98
- function buildHostedServer(hostedMcpUrl, apiKey, runtime) {
99
- if (runtime === "vscode" || runtime === "vscode-insiders") {
156
+ function shortAddress(address) {
157
+ if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return address;
158
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
159
+ }
160
+ var API_KEY_RE, PRIVATE_KEY_RE;
161
+ var init_redact = __esm({
162
+ "src/redact.ts"() {
163
+ API_KEY_RE = /sk_agent_[A-Za-z0-9]+/g;
164
+ PRIVATE_KEY_RE = /0x[0-9a-fA-F]{64}/g;
165
+ }
166
+ });
167
+
168
+ // src/server-names.ts
169
+ function assertValidServerSlug(slug) {
170
+ if (slug.length === 0 || slug.length > 32 || !SLUG_RE.test(slug)) {
171
+ throw new Error(
172
+ `Invalid server name ${JSON.stringify(slug)}: use 1-32 lowercase letters, digits, and single hyphens (e.g. "research").`
173
+ );
174
+ }
175
+ if (slug === "haven" || slug === "haven-signer") {
176
+ throw new Error(
177
+ `Invalid server name ${JSON.stringify(slug)}: "haven" and "haven-signer" are the unnamed pair's own names \u2014 omit --name for the bare pair.`
178
+ );
179
+ }
180
+ if (slug === "signer" || slug.startsWith("signer-")) {
181
+ throw new Error(
182
+ `Invalid server name ${JSON.stringify(slug)}: "signer" and "signer-*" are reserved \u2014 they would collide with another pair's haven-signer-* entry.`
183
+ );
184
+ }
185
+ }
186
+ function serverNamesFor(slug) {
187
+ if (slug === void 0) {
100
188
  return {
101
- type: "http",
102
- url: hostedMcpUrl,
103
- headers: { Authorization: `Bearer ${apiKey}` }
189
+ hosted: "haven",
190
+ signer: "haven-signer",
191
+ // Historical Codex table names — every wired Codex host has these.
192
+ codexHosted: "haven",
193
+ codexSigner: "haven_signer",
194
+ hermesEnvKey: "MCP_HAVEN_API_KEY"
104
195
  };
105
196
  }
197
+ assertValidServerSlug(slug);
106
198
  return {
107
- url: hostedMcpUrl,
108
- headers: { Authorization: `Bearer ${apiKey}` }
199
+ hosted: `haven-${slug}`,
200
+ signer: `haven-signer-${slug}`,
201
+ // Hyphens are valid TOML bare keys, so named Codex tables match the
202
+ // JSON/YAML names instead of inheriting the legacy underscore.
203
+ codexHosted: `haven-${slug}`,
204
+ codexSigner: `haven-signer-${slug}`,
205
+ hermesEnvKey: `MCP_HAVEN_${slug.toUpperCase().replace(/-/g, "_")}_API_KEY`
109
206
  };
110
207
  }
111
- function resolveSignerLaunchSpec(input) {
112
- return input.signerCommand ?? {
113
- command: "npx",
114
- args: ["-y", signerPackageSpec(), "--credentials", input.signerPath]
208
+ var SLUG_RE;
209
+ var init_server_names = __esm({
210
+ "src/server-names.ts"() {
211
+ SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
212
+ }
213
+ });
214
+ async function preflightCredentialStorage(input = {}) {
215
+ const directory = defaultCredentialRoot(input.baseDir);
216
+ await promises.mkdir(directory, { recursive: true, mode: 448 });
217
+ await restrictPermissions(directory, 448, input.warn);
218
+ const probePath = path.join(directory, `.haven-connect-preflight-${crypto__default.default.randomBytes(8).toString("hex")}`);
219
+ try {
220
+ await writeOwnerOnlyJson(probePath, { ok: true }, input.warn);
221
+ } finally {
222
+ await promises.rm(probePath, { force: true }).catch(() => void 0);
223
+ }
224
+ return directory;
225
+ }
226
+ async function writeCredentialFiles(input) {
227
+ const directory = defaultAgentDirectory(input.serverName ?? input.agentId, input.baseDir);
228
+ await promises.mkdir(directory, { recursive: true, mode: 448 });
229
+ await restrictPermissions(directory, 448, input.warn);
230
+ const identityPath = path.join(directory, "identity.json");
231
+ const signerPath = path.join(directory, "signer.json");
232
+ const agentPath = path.join(directory, "agent.json");
233
+ await assertDoesNotExist(identityPath);
234
+ await assertDoesNotExist(signerPath);
235
+ await assertDoesNotExist(agentPath);
236
+ await writeOwnerOnlyJson(signerPath, signerPayload(input), input.warn);
237
+ try {
238
+ await writeOwnerOnlyJson(identityPath, identityPayload(input), input.warn);
239
+ } catch (err) {
240
+ await promises.rm(signerPath, { force: true }).catch(() => void 0);
241
+ throw err;
242
+ }
243
+ try {
244
+ await writeOwnerOnlyJson(agentPath, agentPayload(input), input.warn);
245
+ } catch (err) {
246
+ await promises.rm(signerPath, { force: true }).catch(() => void 0);
247
+ await promises.rm(identityPath, { force: true }).catch(() => void 0);
248
+ await promises.rm(agentPath, { force: true }).catch(() => void 0);
249
+ throw err;
250
+ }
251
+ return { directory, identityPath, signerPath, agentPath };
252
+ }
253
+ function signerPayload(input) {
254
+ return {
255
+ delegate_key: input.delegateKey,
256
+ delegate_address: input.delegateAddress,
257
+ agent_id: input.agentId,
258
+ safe_address: input.safeAddress,
259
+ chain_id: input.chainId,
260
+ network: input.network,
261
+ x402_binding_signer: input.x402BindingSigner,
262
+ note: "Local signer credential. Haven backend never receives this private key."
115
263
  };
116
264
  }
117
- function buildSignerServer(spec, runtime) {
118
- const server = {
119
- command: spec.command,
120
- args: spec.args
265
+ function identityPayload(input) {
266
+ return {
267
+ api_key: input.apiKey,
268
+ agent_id: input.agentId,
269
+ safe_address: input.safeAddress,
270
+ chain_id: input.chainId,
271
+ network: input.network,
272
+ api_url: input.apiUrl,
273
+ hosted_mcp_url: input.hostedMcpUrl,
274
+ agent_budget: input.agentBudget,
275
+ note: "Haven API key identifies the agent only. It cannot spend without the local signer key and on-chain Haven wallet rules."
121
276
  };
122
- if (runtime === "vscode" || runtime === "vscode-insiders") return { type: "stdio", ...server };
123
- return server;
124
277
  }
125
- function mergeJsonMcpConfig(existingJson, serverRoot, hostedServer, signerServer) {
126
- const config = existingJson?.trim() ? parseJsonObject(existingJson) : {};
127
- const existingRoot = config[serverRoot];
128
- const servers = existingRoot && typeof existingRoot === "object" && !Array.isArray(existingRoot) ? existingRoot : {};
129
- config[serverRoot] = {
130
- ...servers,
131
- haven: hostedServer,
132
- "haven-signer": signerServer
278
+ function agentPayload(input) {
279
+ return {
280
+ agent_id: input.agentId,
281
+ delegate_address: input.delegateAddress,
282
+ safe_address: input.safeAddress,
283
+ chain_id: input.chainId,
284
+ network: input.network,
285
+ agent_budget: input.agentBudget,
286
+ 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."
133
287
  };
134
- return `${JSON.stringify(config, null, 2)}
135
- `;
136
288
  }
137
- function mergeHermesYaml(existingYaml, hostedServer, signerServer) {
138
- if (!existingYaml?.trim()) return renderHermesYaml({ haven: hostedServer, "haven-signer": signerServer });
139
- const doc = yaml.parseDocument(existingYaml, { keepSourceTokens: true });
140
- if (doc.errors.length > 0 || !yaml.isMap(doc.contents)) {
141
- throw new Error("Hermes config must be a YAML object");
142
- }
289
+ async function readStoredCredentials(serverName, agentIdOrSlug, baseDir) {
290
+ const key = serverName ?? agentIdOrSlug;
291
+ const directory = key ? defaultAgentDirectory(key, baseDir) : await discoverSoleAgentDirectory(baseDir);
292
+ const identity = await readJsonFile(path.join(directory, "identity.json"));
293
+ if (!identity) {
294
+ throw new Error(
295
+ `No Haven credentials at ${directory}. Nothing to re-key \u2014 connect this agent first, or pass the --name you wired it under.`
296
+ );
297
+ }
298
+ const agent = await readJsonFile(path.join(directory, "agent.json")) ?? {};
299
+ const signer = await readJsonFile(path.join(directory, "signer.json")) ?? {};
300
+ const agentId = asString(identity.agent_id);
301
+ const apiKey = asString(identity.api_key);
302
+ const apiUrl = asString(identity.api_url);
303
+ const hostedMcpUrl = asString(identity.hosted_mcp_url);
304
+ if (!agentId || !apiKey || !apiUrl || !hostedMcpUrl) {
305
+ throw new Error(
306
+ `The credential set at ${directory} is incomplete (identity.json is missing agent_id, api_key, api_url or hosted_mcp_url). Re-key cannot rebuild it \u2014 reconnect the agent instead.`
307
+ );
308
+ }
309
+ return {
310
+ directory,
311
+ agentId,
312
+ apiKey,
313
+ apiUrl,
314
+ hostedMcpUrl,
315
+ delegateAddress: asString(agent.delegate_address) ?? asString(signer.delegate_address),
316
+ safeAddress: asString(identity.safe_address) ?? asString(agent.safe_address),
317
+ chainId: typeof identity.chain_id === "number" ? identity.chain_id : void 0,
318
+ network: asString(identity.network),
319
+ x402BindingSigner: asString(signer.x402_binding_signer),
320
+ agentBudget: Array.isArray(identity.agent_budget) ? identity.agent_budget : void 0
321
+ };
322
+ }
323
+ async function discoverSoleAgentDirectory(baseDir) {
324
+ const root = defaultCredentialRoot(baseDir);
325
+ let entries = [];
326
+ try {
327
+ entries = await promises.readdir(root);
328
+ } catch {
329
+ throw new Error(`No Haven credentials found under ${root}. Connect an agent on this machine first.`);
330
+ }
331
+ const candidates = [];
332
+ for (const entry of entries) {
333
+ const directory = path.join(root, entry);
334
+ if (!await readJsonFile(path.join(directory, "identity.json"))) continue;
335
+ if (await readJsonFile(path.join(directory, "TOMBSTONE.json"))) continue;
336
+ candidates.push(directory);
337
+ }
338
+ if (candidates.length === 1) return candidates[0];
339
+ if (candidates.length === 0) {
340
+ throw new Error(`No Haven credentials found under ${root}. Connect an agent on this machine first.`);
341
+ }
342
+ throw new Error(
343
+ `Several agents are wired on this machine, so --rekey cannot tell which one you mean:
344
+ ` + candidates.map((d) => ` ${d}`).join("\n") + "\nRe-run with --name <slug> to pick one."
345
+ );
346
+ }
347
+ async function rewriteCredentialFiles(input) {
348
+ const directory = defaultAgentDirectory(input.serverName ?? input.agentId, input.baseDir);
349
+ const identityPath = path.join(directory, "identity.json");
350
+ const signerPath = path.join(directory, "signer.json");
351
+ const agentPath = path.join(directory, "agent.json");
352
+ const targets = [
353
+ { path: signerPath, payload: signerPayload(input) },
354
+ { path: identityPath, payload: identityPayload(input) },
355
+ { path: agentPath, payload: agentPayload(input) }
356
+ ];
357
+ const originals = /* @__PURE__ */ new Map();
358
+ for (const { path } of targets) {
359
+ originals.set(path, await readRawFile(path));
360
+ }
361
+ const temps = [];
362
+ try {
363
+ for (const { path, payload } of targets) {
364
+ const temp = `${path}.rekey-${crypto__default.default.randomBytes(6).toString("hex")}.tmp`;
365
+ await writeOwnerOnlyJson(temp, payload, input.warn);
366
+ temps.push({ from: temp, to: path });
367
+ }
368
+ for (const { from, to } of temps) {
369
+ await promises.rename(from, to);
370
+ }
371
+ } catch (err) {
372
+ for (const { from } of temps) await promises.rm(from, { force: true }).catch(() => void 0);
373
+ for (const [path, contents] of originals) {
374
+ if (contents === null) {
375
+ await promises.rm(path, { force: true }).catch(() => void 0);
376
+ } else {
377
+ await promises.writeFile(path, contents, { mode: 384 }).catch(() => void 0);
378
+ }
379
+ }
380
+ throw err;
381
+ }
382
+ return { directory, identityPath, signerPath, agentPath };
383
+ }
384
+ async function writeRekeyPending(directory, pending, warn) {
385
+ const path$1 = path.join(directory, REKEY_PENDING_FILENAME);
386
+ await promises.rm(path$1, { force: true }).catch(() => void 0);
387
+ await writeOwnerOnlyJson(path$1, { ...pending }, warn);
388
+ return path$1;
389
+ }
390
+ async function readRekeyPending(directory, now = Date.now()) {
391
+ const path$1 = path.join(directory, REKEY_PENDING_FILENAME);
392
+ const raw = await readJsonFile(path$1);
393
+ if (!raw) {
394
+ throw new Error(
395
+ `No re-key in progress at ${directory}. Run the connector with --rekey first \u2014 it prints the new signing address to paste into the Haven agent page.`
396
+ );
397
+ }
398
+ const pending = raw;
399
+ if (!pending.new_delegate_key || !pending.new_delegate_address || !pending.agent_id) {
400
+ throw new Error(`The pending re-key at ${path$1} is unreadable. Delete it and start again with --rekey.`);
401
+ }
402
+ if (pending.expires_at && Date.parse(pending.expires_at) < now) {
403
+ throw new Error(
404
+ `The re-key started at ${pending.started_at} has expired. Start again with --rekey \u2014 the address currently shown in your dashboard is no longer the one this machine holds.`
405
+ );
406
+ }
407
+ return pending;
408
+ }
409
+ async function inspectRekeyPending(directory, now = Date.now()) {
410
+ const path$1 = path.join(directory, REKEY_PENDING_FILENAME);
411
+ const raw = await readJsonFile(path$1);
412
+ if (!raw) {
413
+ const present = await readRawFile(path$1) !== null;
414
+ return present ? { state: "unreadable", path: path$1 } : null;
415
+ }
416
+ const agentId = asString(raw.agent_id);
417
+ const newDelegateAddress = asString(raw.new_delegate_address);
418
+ const startedAt = asString(raw.started_at);
419
+ const expiresAt = asString(raw.expires_at);
420
+ if (!agentId || !newDelegateAddress) {
421
+ return { state: "unreadable", path: path$1, ...startedAt ? { startedAt } : {} };
422
+ }
423
+ const expired = expiresAt !== void 0 && Date.parse(expiresAt) < now;
424
+ return {
425
+ state: expired ? "expired" : "pending",
426
+ path: path$1,
427
+ agentId,
428
+ newDelegateAddress,
429
+ ...startedAt ? { startedAt } : {},
430
+ ...expiresAt ? { expiresAt } : {}
431
+ };
432
+ }
433
+ async function clearRekeyPending(directory) {
434
+ await promises.rm(path.join(directory, REKEY_PENDING_FILENAME), { force: true }).catch(() => void 0);
435
+ }
436
+ async function readJsonFile(path) {
437
+ const raw = await readRawFile(path);
438
+ if (raw === null) return null;
439
+ try {
440
+ const parsed = JSON.parse(raw);
441
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
442
+ } catch {
443
+ return null;
444
+ }
445
+ }
446
+ async function readRawFile(path) {
447
+ try {
448
+ return await promises.readFile(path, "utf8");
449
+ } catch {
450
+ return null;
451
+ }
452
+ }
453
+ function asString(value) {
454
+ return typeof value === "string" && value.length > 0 ? value : void 0;
455
+ }
456
+ async function assertServerSlugAvailable(serverName, baseDir) {
457
+ const directory = defaultAgentDirectory(serverName, baseDir);
458
+ try {
459
+ await promises.stat(path.join(directory, "identity.json"));
460
+ } catch {
461
+ return;
462
+ }
463
+ throw new Error(
464
+ `The name "${serverName}" is already wired on this machine (${directory} holds credentials). Pick a different --name, or revoke and remove that agent first \u2014 connect never overwrites credentials.`
465
+ );
466
+ }
467
+ function defaultAgentDirectory(agentId, baseDir = path.join(os.homedir(), ".haven", "agents")) {
468
+ return path.resolve(defaultCredentialRoot(baseDir), safePathPart(agentId));
469
+ }
470
+ function defaultCredentialRoot(baseDir = path.join(os.homedir(), ".haven", "agents")) {
471
+ return path.resolve(baseDir);
472
+ }
473
+ async function writeOwnerOnlyJson(path, value, warn) {
474
+ const json = JSON.stringify(dropUndefined(value), null, 2);
475
+ await promises.writeFile(path, `${json}
476
+ `, { mode: 384, flag: "wx" });
477
+ await restrictPermissions(path, 384, warn);
478
+ }
479
+ function safePathPart(value) {
480
+ return value.replace(/[^A-Za-z0-9_.-]/g, "_");
481
+ }
482
+ function dropUndefined(value) {
483
+ return Object.fromEntries(Object.entries(value).filter(([, field]) => field !== void 0));
484
+ }
485
+ async function assertDoesNotExist(path) {
486
+ try {
487
+ await promises.access(path);
488
+ } catch (err) {
489
+ if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") return;
490
+ throw err;
491
+ }
492
+ throw new Error(`Refusing to overwrite existing Haven credential file: ${path}`);
493
+ }
494
+ async function restrictPermissions(path, mode, warn) {
495
+ try {
496
+ await promises.chmod(path, mode);
497
+ } catch (err) {
498
+ warn?.(
499
+ `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)}`
500
+ );
501
+ }
502
+ }
503
+ var REKEY_PENDING_FILENAME, REKEY_PENDING_TTL_MS;
504
+ var init_storage = __esm({
505
+ "src/storage.ts"() {
506
+ REKEY_PENDING_FILENAME = "rekey-pending.json";
507
+ REKEY_PENDING_TTL_MS = 24 * 60 * 60 * 1e3;
508
+ }
509
+ });
510
+ function mcpPackageSpec() {
511
+ return `${exports.MCP_RUNTIME_MANIFEST.mcpPackage}@${exports.MCP_RUNTIME_MANIFEST.mcpVersion}`;
512
+ }
513
+ function sdkPackageSpec() {
514
+ return `${exports.MCP_RUNTIME_MANIFEST.sdkPackage}@${exports.MCP_RUNTIME_MANIFEST.sdkVersion}`;
515
+ }
516
+ function signerPackageSpec() {
517
+ return `${exports.MCP_RUNTIME_MANIFEST.signerPackage}@${exports.MCP_RUNTIME_MANIFEST.signerVersion}`;
518
+ }
519
+ exports.MCP_RUNTIME_MANIFEST = void 0;
520
+ var init_runtime_manifest = __esm({
521
+ "src/runtime-manifest.ts"() {
522
+ exports.MCP_RUNTIME_MANIFEST = {
523
+ mcpPackage: "@haven_ai/mcp",
524
+ mcpVersion: mcp.MCP_VERSION,
525
+ sdkPackage: "@haven_ai/sdk",
526
+ sdkVersion: "0.1.30-alpha.0",
527
+ signerPackage: "@haven_ai/signer",
528
+ signerVersion: "0.1.30-alpha.0",
529
+ // Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
530
+ // while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
531
+ // so the guard that was supposed to enforce the floor waved Node v23 through
532
+ // — including on the `--local` path where it does run. A hand-maintained
533
+ // second copy of a number is a drift waiting to happen; a guard test pins
534
+ // this against `package.json`'s `engines.node`.
535
+ minimumNodeVersion: sdk.HAVEN_MINIMUM_NODE_VERSION,
536
+ supportedClients: ["codex-cli", "codex-desktop", "claude-code"],
537
+ requiredTools: mcp.registeredToolNames(),
538
+ /**
539
+ * The signer MCP's tool surface, DERIVED from the pinned @haven_ai/signer
540
+ * package (#1587) — same anti-drift rule as `requiredTools` above: a
541
+ * literal list here would rot the first time the signer gains a tool.
542
+ * The handshake probe requires all of them.
543
+ */
544
+ requiredSignerTools: Object.keys(signer.toolSchemas)
545
+ };
546
+ }
547
+ });
548
+ async function writeRuntimeConfig(input, deps = {}) {
549
+ switch (input.runtime) {
550
+ case "codex-cli":
551
+ case "codex-desktop":
552
+ return writeCodexConfig(input);
553
+ case "cursor":
554
+ return writeJsonRuntimeConfig(input, cursorConfigPath(input.homeDir), "mcpServers");
555
+ case "vscode":
556
+ return writeJsonRuntimeConfig(input, vscodeConfigPath(input.homeDir), "servers");
557
+ case "vscode-insiders":
558
+ return writeJsonRuntimeConfig(input, vscodeInsidersConfigPath(input.homeDir), "servers");
559
+ case "claude-desktop":
560
+ return writeJsonRuntimeConfig(input, claudeDesktopConfigPath(input.homeDir), "mcpServers");
561
+ case "hermes":
562
+ return writeHermesConfig(input, deps);
563
+ default:
564
+ return {
565
+ hostedConfigured: false,
566
+ signerConfigured: false,
567
+ localMcpConfigured: false,
568
+ runtimeMcpMode: "manual",
569
+ target: "manual runtime setup",
570
+ changed: false,
571
+ restartRequired: true,
572
+ messages: ["Runtime config needs to be added manually for this agent environment."],
573
+ errorCode: "manual_runtime_setup_required"
574
+ };
575
+ }
576
+ }
577
+ function buildHostedServer(hostedMcpUrl, apiKey, runtime) {
578
+ if (runtime === "vscode" || runtime === "vscode-insiders") {
579
+ return {
580
+ type: "http",
581
+ url: hostedMcpUrl,
582
+ headers: { Authorization: `Bearer ${apiKey}` }
583
+ };
584
+ }
585
+ return {
586
+ url: hostedMcpUrl,
587
+ headers: { Authorization: `Bearer ${apiKey}` }
588
+ };
589
+ }
590
+ function resolveSignerLaunchSpec(input) {
591
+ return input.signerCommand ?? {
592
+ command: "npx",
593
+ args: ["-y", signerPackageSpec(), "--credentials", input.signerPath]
594
+ };
595
+ }
596
+ function buildSignerServer(spec, runtime) {
597
+ const server = {
598
+ command: spec.command,
599
+ args: spec.args
600
+ };
601
+ if (runtime === "vscode" || runtime === "vscode-insiders") return { type: "stdio", ...server };
602
+ return server;
603
+ }
604
+ function mergeJsonMcpConfig(existingJson, serverRoot, hostedServer, signerServer, names = serverNamesFor(), configPath) {
605
+ const config = existingJson?.trim() ? parseJsonObject(existingJson, configPath) : {};
606
+ const existingRoot = config[serverRoot];
607
+ const servers = existingRoot && typeof existingRoot === "object" && !Array.isArray(existingRoot) ? existingRoot : {};
608
+ config[serverRoot] = {
609
+ ...servers,
610
+ [names.hosted]: hostedServer,
611
+ [names.signer]: signerServer
612
+ };
613
+ return `${JSON.stringify(config, null, 2)}
614
+ `;
615
+ }
616
+ function mergeHermesYaml(existingYaml, hostedServer, signerServer, names = serverNamesFor(), configPath) {
617
+ if (!existingYaml?.trim()) {
618
+ return renderHermesYaml({ [names.hosted]: hostedServer, [names.signer]: signerServer });
619
+ }
620
+ const doc = yaml.parseDocument(existingYaml, { keepSourceTokens: true });
621
+ if (doc.errors.length > 0 || !yaml.isMap(doc.contents)) {
622
+ throw new UnreadableRuntimeConfigError(configPath ?? "the Hermes config", "it is not a YAML object");
623
+ }
143
624
  const mcpPair = doc.contents.items.find((item) => item.key?.toString() === "mcp_servers");
144
625
  const existingServers = mcpPair && yaml.isMap(mcpPair.value) ? mcpPair.value.toJSON() : {};
145
626
  const servers = isRecord(existingServers) ? existingServers : {};
146
627
  const mergedServers = {
147
628
  ...servers,
148
- haven: hostedServer,
149
- "haven-signer": signerServer
629
+ [names.hosted]: hostedServer,
630
+ [names.signer]: signerServer
150
631
  };
151
632
  if (!mcpPair) {
152
633
  return appendHermesMcpServers(existingYaml, mergedServers);
@@ -154,9 +635,9 @@ function mergeHermesYaml(existingYaml, hostedServer, signerServer) {
154
635
  if (!mcpPair.value?.range) return replaceEmptyHermesMcpServers(existingYaml, mcpPair.key?.range, mergedServers);
155
636
  return replaceHermesMcpServers(existingYaml, mcpPair.key?.range, mcpPair.value.range, mergedServers);
156
637
  }
157
- function mergeHermesEnv(existingEnv, apiKey) {
638
+ function mergeHermesEnv(existingEnv, apiKey, envKey = HERMES_API_KEY_ENV) {
158
639
  if (/[\r\n]/.test(apiKey)) throw new Error("Hermes API key must be a single line");
159
- const assignment = `${HERMES_API_KEY_ENV}=${apiKey}`;
640
+ const assignment = `${envKey}=${apiKey}`;
160
641
  if (!existingEnv) return `${assignment}
161
642
  `;
162
643
  const lineEnding = existingEnv.includes("\r\n") ? "\r\n" : "\n";
@@ -165,12 +646,12 @@ function mergeHermesEnv(existingEnv, apiKey) {
165
646
  if (hasTrailingNewline) lines.pop();
166
647
  let found = false;
167
648
  const merged = lines.flatMap((line) => {
168
- if (isHermesEnvAssignment(line)) {
649
+ if (isHermesEnvAssignment(line, envKey)) {
169
650
  if (found) return [];
170
651
  found = true;
171
652
  return [assignment];
172
653
  }
173
- if (isAmbiguousHermesEnvLine(line)) {
654
+ if (isAmbiguousHermesEnvLine(line, envKey)) {
174
655
  throw new Error("Hermes environment contains an ambiguous managed key");
175
656
  }
176
657
  return [line];
@@ -180,11 +661,11 @@ function mergeHermesEnv(existingEnv, apiKey) {
180
661
  }
181
662
  return `${merged.join(lineEnding)}${hasTrailingNewline ? lineEnding : ""}`;
182
663
  }
183
- function isHermesEnvAssignment(line) {
184
- return /^\s*(?:export[ \t]+)?MCP_HAVEN_API_KEY[ \t]*=/.test(line);
664
+ function isHermesEnvAssignment(line, envKey) {
665
+ return new RegExp(`^\\s*(?:export[ \\t]+)?${envKey}[ \\t]*=`).test(line);
185
666
  }
186
- function isAmbiguousHermesEnvLine(line) {
187
- return /^\s*(?:export[ \t]+)?MCP_HAVEN_API_KEY\b/.test(line);
667
+ function isAmbiguousHermesEnvLine(line, envKey) {
668
+ return new RegExp(`^\\s*(?:export[ \\t]+)?${envKey}\\b`).test(line);
188
669
  }
189
670
  function appendHermesMcpServers(source, servers) {
190
671
  const documentEnd = /(?:^|\n)[ \t]*\.\.\.[ \t]*(?:#[^\n]*)?\r?\n?$/.exec(source);
@@ -243,11 +724,14 @@ function renderHermesMcpServerEntries(servers, indent) {
243
724
  function isRecord(value) {
244
725
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
245
726
  }
246
- function mergeCodexToml(existingToml, localMcpCommand) {
247
- let next = removeTomlTableTree(removeTomlTableTree(existingToml, "mcp_servers.haven"), "mcp_servers.haven_signer");
727
+ function mergeCodexToml(existingToml, localMcpCommand, names = serverNamesFor()) {
728
+ let next = removeTomlTableTree(
729
+ removeTomlTableTree(existingToml, `mcp_servers.${names.codexHosted}`),
730
+ `mcp_servers.${names.codexSigner}`
731
+ );
248
732
  next = next.trimEnd();
249
733
  const block = [
250
- "[mcp_servers.haven]",
734
+ `[mcp_servers.${names.codexHosted}]`,
251
735
  `command = ${tomlString(localMcpCommand)}`,
252
736
  "args = []",
253
737
  "startup_timeout_sec = 120"
@@ -259,15 +743,18 @@ function mergeCodexToml(existingToml, localMcpCommand) {
259
743
  `;
260
744
  return merged;
261
745
  }
262
- function mergeCodexTomlHosted(existingToml, hostedMcpUrl, apiKey, signerSpec) {
263
- let next = removeTomlTableTree(removeTomlTableTree(existingToml, "mcp_servers.haven"), "mcp_servers.haven_signer");
746
+ function mergeCodexTomlHosted(existingToml, hostedMcpUrl, apiKey, signerSpec, names = serverNamesFor()) {
747
+ let next = removeTomlTableTree(
748
+ removeTomlTableTree(existingToml, `mcp_servers.${names.codexHosted}`),
749
+ `mcp_servers.${names.codexSigner}`
750
+ );
264
751
  next = next.trimEnd();
265
752
  const block = [
266
- "[mcp_servers.haven]",
753
+ `[mcp_servers.${names.codexHosted}]`,
267
754
  `url = ${tomlString(hostedMcpUrl)}`,
268
755
  `http_headers = { "Authorization" = ${tomlString(`Bearer ${apiKey}`)} }`,
269
756
  "",
270
- "[mcp_servers.haven_signer]",
757
+ `[mcp_servers.${names.codexSigner}]`,
271
758
  `command = ${tomlString(signerSpec.command)}`,
272
759
  `args = [${signerSpec.args.map((arg) => tomlString(arg)).join(", ")}]`,
273
760
  "startup_timeout_sec = 120"
@@ -286,7 +773,9 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
286
773
  existing,
287
774
  serverRoot,
288
775
  buildHostedServer(input.hostedMcpUrl, input.apiKey, input.runtime),
289
- buildSignerServer(resolveSignerLaunchSpec(input), input.runtime)
776
+ buildSignerServer(resolveSignerLaunchSpec(input), input.runtime),
777
+ serverNamesFor(input.serverName),
778
+ target
290
779
  );
291
780
  await writeOwnerOnlyText(target, merged);
292
781
  return {
@@ -300,6 +789,7 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
300
789
  messages: [`Updated Haven MCP entries in ${configTargetLabel(input.runtime)}.`]
301
790
  };
302
791
  } catch (err) {
792
+ const unreadable = err instanceof UnreadableRuntimeConfigError;
303
793
  return {
304
794
  hostedConfigured: false,
305
795
  signerConfigured: false,
@@ -308,8 +798,11 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
308
798
  target: configTargetLabel(input.runtime),
309
799
  changed: false,
310
800
  restartRequired: true,
311
- messages: [`Could not update ${configTargetLabel(input.runtime)}: ${err instanceof Error ? err.message : String(err)}`],
312
- errorCode: "runtime_config_write_failed"
801
+ messages: unreadable ? [
802
+ `Could not update ${configTargetLabel(input.runtime)}: ${err.message}.`,
803
+ `Nothing was written to ${err.configPath}. Fix the JSON there (or move the file aside), then run \`${REPAIR_COMMAND_PREFIX} ${input.runtime}\` to write the Haven entries from the credentials already stored on this machine. Do not re-run the setup command: its token is already used.`
804
+ ] : [`Could not update ${configTargetLabel(input.runtime)}: ${err instanceof Error ? err.message : String(err)}`],
805
+ errorCode: unreadable ? "runtime_config_unreadable" : "runtime_config_write_failed"
313
806
  };
314
807
  }
315
808
  }
@@ -318,8 +811,9 @@ async function writeHermesConfig(input, deps) {
318
811
  const envTarget = hermesEnvPath(input.homeDir);
319
812
  try {
320
813
  const [existing, existingEnv] = await Promise.all([readOptional(target), readOptional(envTarget)]);
814
+ const names = serverNamesFor(input.serverName);
321
815
  const hostedServer = {
322
- ...buildHostedServer(input.hostedMcpUrl, `\${${HERMES_API_KEY_ENV}}`, input.runtime),
816
+ ...buildHostedServer(input.hostedMcpUrl, `\${${names.hermesEnvKey}}`, input.runtime),
323
817
  enabled: true
324
818
  };
325
819
  const signerServer = {
@@ -329,9 +823,11 @@ async function writeHermesConfig(input, deps) {
329
823
  const merged = mergeHermesYaml(
330
824
  existing,
331
825
  hostedServer,
332
- signerServer
826
+ signerServer,
827
+ names,
828
+ target
333
829
  );
334
- const mergedEnv = mergeHermesEnv(existingEnv, input.apiKey);
830
+ const mergedEnv = mergeHermesEnv(existingEnv, input.apiKey, names.hermesEnvKey);
335
831
  const writeText = deps.writeOwnerOnlyText ?? writeOwnerOnlyText;
336
832
  await writeText(envTarget, mergedEnv);
337
833
  try {
@@ -352,11 +848,13 @@ async function writeHermesConfig(input, deps) {
352
848
  messages: [
353
849
  `Updated Haven MCP entries in ${target}; stored the hosted MCP identity in ${envTarget}.`,
354
850
  "Restart Hermes (start a new session; gateway users: /restart), then verify with `hermes mcp list`, `hermes mcp test haven`, and `hermes mcp test haven-signer`.",
851
+ "If several long-lived Hermes processes are running (a gateway plus TUI workers), restart EVERY one: each loads its MCP wiring at startup, so a process started before this setup keeps using its old snapshot.",
355
852
  "If no mcp_* tools appear after restart, ensure the MCP SDK is installed in Hermes: pip install mcp"
356
853
  ]
357
854
  };
358
855
  } catch (err) {
359
856
  const recoveryIncomplete = err instanceof HermesConfigRecoveryError;
857
+ const unreadable = err instanceof UnreadableRuntimeConfigError;
360
858
  return {
361
859
  hostedConfigured: false,
362
860
  signerConfigured: false,
@@ -365,8 +863,11 @@ async function writeHermesConfig(input, deps) {
365
863
  target: "Hermes Agent config",
366
864
  changed: false,
367
865
  restartRequired: true,
368
- messages: [recoveryIncomplete ? "Could not update Hermes Agent config. Recovery did not complete; inspect the Hermes configuration before retrying." : "Could not update Hermes Agent config. Existing configuration was left unchanged."],
369
- errorCode: "runtime_config_write_failed"
866
+ messages: unreadable ? [
867
+ `Could not update Hermes Agent config: ${err.message}.`,
868
+ `Nothing was written to ${err.configPath}. Fix the YAML there (or move the file aside), then run \`${REPAIR_COMMAND_PREFIX} hermes\` to write the Haven entries from the credentials already stored on this machine. Do not re-run the setup command: its token is already used.`
869
+ ] : [recoveryIncomplete ? "Could not update Hermes Agent config. Recovery did not complete; inspect the Hermes configuration before retrying." : "Could not update Hermes Agent config. Existing configuration was left unchanged."],
870
+ errorCode: unreadable ? "runtime_config_unreadable" : "runtime_config_write_failed"
370
871
  };
371
872
  }
372
873
  }
@@ -398,7 +899,7 @@ async function writeCodexConfig(input) {
398
899
  if (!input.localMcpCommand) {
399
900
  throw new Error("local MCP wrapper command is required");
400
901
  }
401
- const merged2 = mergeCodexToml(existing ?? "", input.localMcpCommand);
902
+ const merged2 = mergeCodexToml(existing ?? "", input.localMcpCommand, serverNamesFor(input.serverName));
402
903
  await writeOwnerOnlyText(target, merged2);
403
904
  return {
404
905
  hostedConfigured: false,
@@ -413,7 +914,13 @@ async function writeCodexConfig(input) {
413
914
  ]
414
915
  };
415
916
  }
416
- const merged = mergeCodexTomlHosted(existing ?? "", input.hostedMcpUrl, input.apiKey, resolveSignerLaunchSpec(input));
917
+ const merged = mergeCodexTomlHosted(
918
+ existing ?? "",
919
+ input.hostedMcpUrl,
920
+ input.apiKey,
921
+ resolveSignerLaunchSpec(input),
922
+ serverNamesFor(input.serverName)
923
+ );
417
924
  await writeOwnerOnlyText(target, merged);
418
925
  return {
419
926
  hostedConfigured: true,
@@ -455,10 +962,15 @@ async function writeOwnerOnlyText(path$1, value) {
455
962
  await promises.writeFile(path$1, value, { mode: 384 });
456
963
  await promises.chmod(path$1, 384).catch(() => void 0);
457
964
  }
458
- function parseJsonObject(value) {
459
- const parsed = JSON.parse(value);
965
+ function parseJsonObject(value, configPath) {
966
+ let parsed;
967
+ try {
968
+ parsed = JSON.parse(value);
969
+ } catch {
970
+ throw new UnreadableRuntimeConfigError(configPath ?? "the runtime config", "it is not valid JSON");
971
+ }
460
972
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
461
- throw new Error("runtime config must be a JSON object");
973
+ throw new UnreadableRuntimeConfigError(configPath ?? "the runtime config", "the top level is not a JSON object");
462
974
  }
463
975
  return parsed;
464
976
  }
@@ -725,10 +1237,11 @@ function configTargetLabel(runtime) {
725
1237
  return "runtime MCP config";
726
1238
  }
727
1239
  }
728
- var HERMES_API_KEY_ENV, HermesConfigRecoveryError, InvalidCodexTomlError;
1240
+ var HERMES_API_KEY_ENV, HermesConfigRecoveryError, InvalidCodexTomlError, REPAIR_COMMAND_PREFIX, UnreadableRuntimeConfigError;
729
1241
  var init_config_writers = __esm({
730
1242
  "src/config-writers.ts"() {
731
1243
  init_runtime_manifest();
1244
+ init_server_names();
732
1245
  HERMES_API_KEY_ENV = "MCP_HAVEN_API_KEY";
733
1246
  HermesConfigRecoveryError = class extends Error {
734
1247
  constructor() {
@@ -742,27 +1255,136 @@ var init_config_writers = __esm({
742
1255
  this.name = "InvalidCodexTomlError";
743
1256
  }
744
1257
  };
1258
+ REPAIR_COMMAND_PREFIX = "npx @haven_ai/connect@alpha --doctor --repair --runtime";
1259
+ UnreadableRuntimeConfigError = class extends Error {
1260
+ configPath;
1261
+ constructor(configPath, detail) {
1262
+ super(`${configPath} is not a config Haven can merge into (${detail})`);
1263
+ this.name = "UnreadableRuntimeConfigError";
1264
+ this.configPath = configPath;
1265
+ }
1266
+ };
745
1267
  }
746
1268
  });
747
- async function probeHostedMcpTools(apiKey, hostedMcpUrl, fetchImpl = fetch) {
748
- let response;
1269
+ async function acknowledgeLocalMcpConsent(identityPath, signerPath, log) {
749
1270
  try {
750
- response = await fetchWithTimeout(fetchImpl, hostedMcpUrl, {
751
- method: "POST",
752
- headers: {
753
- Authorization: `Bearer ${apiKey}`,
754
- "Content-Type": "application/json",
755
- Accept: "application/json, text/event-stream"
756
- },
757
- body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" })
1271
+ const input = await buildLocalMcpConsentInput(identityPath, signerPath);
1272
+ const decision = await mcp.ensureConsent(input, {
1273
+ credentialsPath: identityPath,
1274
+ writeAck: true,
1275
+ out: log ? { write: (chunk) => writeLogChunk(log, chunk) } : void 0
758
1276
  });
759
- } catch {
760
- return { status: "network_error" };
1277
+ return {
1278
+ acknowledged: decision.ok,
1279
+ hash: decision.hash,
1280
+ reason: decision.reason
1281
+ };
1282
+ } catch (err) {
1283
+ return {
1284
+ acknowledged: false,
1285
+ error: err instanceof Error ? err.message : String(err)
1286
+ };
761
1287
  }
762
- if (response.status === 401 || response.status === 403) return { status: "unauthorized" };
763
- if (!response.ok) return { status: "bad_response" };
1288
+ }
1289
+ async function getLocalMcpConsentStatus(identityPath, signerPath) {
764
1290
  try {
765
- const payload = parseJsonRpcPayload(await response.text());
1291
+ const input = await buildLocalMcpConsentInput(identityPath, signerPath);
1292
+ const hash = mcp.computeConsentHash(input);
1293
+ const stored = await readLocalMcpAckFile(localMcpAckPath(identityPath));
1294
+ if (stored === hash) {
1295
+ return { acknowledged: true, hash, reason: "ack_file_match" };
1296
+ }
1297
+ return {
1298
+ acknowledged: false,
1299
+ hash,
1300
+ reason: stored ? "ack_file_mismatch" : "ack_file_missing"
1301
+ };
1302
+ } catch (err) {
1303
+ return {
1304
+ acknowledged: false,
1305
+ error: err instanceof Error ? err.message : String(err)
1306
+ };
1307
+ }
1308
+ }
1309
+ function localMcpAckPath(identityPath) {
1310
+ return path.resolve(`${identityPath}.ack.json`);
1311
+ }
1312
+ async function buildLocalMcpConsentInput(identityPath, signerPath) {
1313
+ const credentials = await mcp.loadCredentials({ identityPath, signerPath });
1314
+ const unavailableDuringSetup = {
1315
+ getAllowances: async () => {
1316
+ throw new Error("Haven approval is not complete yet.");
1317
+ }
1318
+ };
1319
+ return mcp.consentInputFromClient(
1320
+ unavailableDuringSetup,
1321
+ {
1322
+ apiKey: credentials.apiKey,
1323
+ apiUrl: credentials.apiUrl,
1324
+ agentId: credentials.agentId,
1325
+ safeAddress: credentials.safeAddress,
1326
+ delegateAddress: credentials.delegateAddress,
1327
+ chainId: credentials.chainId,
1328
+ allowanceSummary: credentials.allowanceSummary
1329
+ },
1330
+ mcp.registeredToolNames()
1331
+ );
1332
+ }
1333
+ async function readLocalMcpAckFile(path) {
1334
+ try {
1335
+ const parsed = JSON.parse(await promises.readFile(path, "utf8"));
1336
+ return typeof parsed.ack === "string" ? parsed.ack : null;
1337
+ } catch {
1338
+ return null;
1339
+ }
1340
+ }
1341
+ function writeLogChunk(log, chunk) {
1342
+ const message = String(chunk).trimEnd();
1343
+ if (message) log(message);
1344
+ }
1345
+ var init_local_mcp_consent = __esm({
1346
+ "src/local-mcp-consent.ts"() {
1347
+ }
1348
+ });
1349
+ async function probeHostedAgentIdentity(apiKey, apiUrl, fetchImpl = fetch) {
1350
+ let response;
1351
+ try {
1352
+ response = await fetchWithTimeout(fetchImpl, `${apiUrl.replace(/\/+$/, "")}/machine-payments/agent`, {
1353
+ method: "GET",
1354
+ headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" }
1355
+ });
1356
+ } catch {
1357
+ return { status: "network_error" };
1358
+ }
1359
+ if (response.status === 401 || response.status === 403) return { status: "unauthorized" };
1360
+ if (!response.ok) return { status: "bad_response" };
1361
+ try {
1362
+ const payload = JSON.parse(await response.text());
1363
+ if (typeof payload?.delegate_address !== "string") return { status: "bad_response" };
1364
+ return { status: "ok", agentId: payload.id, delegateAddress: payload.delegate_address };
1365
+ } catch {
1366
+ return { status: "bad_response" };
1367
+ }
1368
+ }
1369
+ async function probeHostedMcpTools(apiKey, hostedMcpUrl, fetchImpl = fetch) {
1370
+ let response;
1371
+ try {
1372
+ response = await fetchWithTimeout(fetchImpl, hostedMcpUrl, {
1373
+ method: "POST",
1374
+ headers: {
1375
+ Authorization: `Bearer ${apiKey}`,
1376
+ "Content-Type": "application/json",
1377
+ Accept: "application/json, text/event-stream"
1378
+ },
1379
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" })
1380
+ });
1381
+ } catch {
1382
+ return { status: "network_error" };
1383
+ }
1384
+ if (response.status === 401 || response.status === 403) return { status: "unauthorized" };
1385
+ if (!response.ok) return { status: "bad_response" };
1386
+ try {
1387
+ const payload = parseJsonRpcPayload(await response.text());
766
1388
  if (!payload || payload.error) return { status: "bad_response" };
767
1389
  const tools = payload.result?.tools;
768
1390
  return { status: "ok", toolCount: Array.isArray(tools) ? tools.length : void 0 };
@@ -781,7 +1403,7 @@ async function probeLocalSignerCredential(signerPath) {
781
1403
  }
782
1404
  }
783
1405
  async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4) {
784
- return new Promise((resolve8) => {
1406
+ return new Promise((resolve9) => {
785
1407
  const child = child_process.spawn(command, args, { stdio: ["pipe", "pipe", "ignore"] });
786
1408
  let stdout = "";
787
1409
  let settled = false;
@@ -793,7 +1415,7 @@ async function probeLocalMcpTools(command, args, requiredTools, timeoutMs = 1e4)
793
1415
  settled = true;
794
1416
  clearTimeout(timeout);
795
1417
  child.kill();
796
- resolve8(result);
1418
+ resolve9(result);
797
1419
  };
798
1420
  const timeout = setTimeout(() => finish({ status: "timeout" }), timeoutMs);
799
1421
  child.on("error", () => finish({ status: "process_error" }));
@@ -907,7 +1529,8 @@ async function prepareSignerRuntime(input, deps = {}) {
907
1529
  wrapperPath,
908
1530
  runtimeDirectory,
909
1531
  npmCacheDirectory,
910
- cliPath
1532
+ cliPath,
1533
+ serverName: input.serverName
911
1534
  });
912
1535
  messages.push(`Prepared stable local Haven signer wrapper: ${wrapperPath}`);
913
1536
  return {
@@ -1008,6 +1631,7 @@ async function readRuntimeSidecar(credentialDirectory) {
1008
1631
  }
1009
1632
  async function writeRuntimeSidecar(input) {
1010
1633
  const value = {
1634
+ ...input.serverName ? { server_name: input.serverName } : {},
1011
1635
  signer_package: exports.MCP_RUNTIME_MANIFEST.signerPackage,
1012
1636
  signer_version: exports.MCP_RUNTIME_MANIFEST.signerVersion,
1013
1637
  sdk_package: exports.MCP_RUNTIME_MANIFEST.sdkPackage,
@@ -1037,61 +1661,394 @@ var init_signer_runtime = __esm({
1037
1661
  SIGNER_INSTALL_HEARTBEAT_MS = 15e3;
1038
1662
  }
1039
1663
  });
1040
-
1041
- // src/runtime-registry.ts
1042
- function runtimeProfile(runtime, env = process.env) {
1043
- return RUNTIME_PROFILES[normalizeRuntime(runtime, env)];
1664
+ async function prepareLocalMcpRuntime(input, deps = {}) {
1665
+ assertSupportedNodeVersion(input.nodeVersion);
1666
+ const homeDir = input.homeDir ?? os.homedir();
1667
+ const runtimeDirectory = path.resolve(homeDir, ".haven", "mcp-runtime", exports.MCP_RUNTIME_MANIFEST.mcpVersion);
1668
+ const npmCacheDirectory = path.resolve(homeDir, ".haven", "npm-cache");
1669
+ const cliPath = path.join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "dist", "cli.js");
1670
+ const messages = [];
1671
+ await promises.mkdir(runtimeDirectory, { recursive: true, mode: 448 });
1672
+ await promises.chmod(runtimeDirectory, 448).catch(() => void 0);
1673
+ await promises.mkdir(npmCacheDirectory, { recursive: true, mode: 448 });
1674
+ await promises.chmod(npmCacheDirectory, 448).catch(() => void 0);
1675
+ if (await installedRuntimeMatches2(runtimeDirectory, cliPath)) {
1676
+ messages.push(`Using existing local Haven MCP runtime ${mcpPackageSpec()}.`);
1677
+ } else {
1678
+ await installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps);
1679
+ messages.push(`Installed local Haven MCP runtime ${mcpPackageSpec()}.`);
1680
+ }
1681
+ await assertFileExists2(cliPath, "local Haven MCP CLI");
1682
+ const wrapperPath = path.join(input.credentialDirectory, "bin", "haven-mcp");
1683
+ await writeWrapper2({
1684
+ wrapperPath,
1685
+ cliPath,
1686
+ identityPath: input.identityPath,
1687
+ signerPath: input.signerPath
1688
+ });
1689
+ await writeRuntimeSidecar2({
1690
+ path: path.join(input.credentialDirectory, "mcp-runtime.json"),
1691
+ wrapperPath,
1692
+ runtimeDirectory,
1693
+ npmCacheDirectory,
1694
+ cliPath,
1695
+ serverName: input.serverName
1696
+ });
1697
+ messages.push(`Prepared stable local Haven MCP wrapper: ${wrapperPath}`);
1698
+ return {
1699
+ command: wrapperPath,
1700
+ args: [],
1701
+ wrapperPath,
1702
+ runtimeDirectory,
1703
+ npmCacheDirectory,
1704
+ cliPath,
1705
+ messages
1706
+ };
1044
1707
  }
1045
- function normalizeRuntime(runtime, env = process.env) {
1046
- const explicit = normalizeRuntimeName(runtime);
1047
- if (explicit) return explicit;
1048
- return detectRuntime(env) ?? "other";
1708
+ function assertSupportedNodeVersion(nodeVersion = process.versions.node, minimumNodeVersion = exports.MCP_RUNTIME_MANIFEST.minimumNodeVersion, subject = "Haven setup") {
1709
+ if (!sdk.isSupportedNodeVersion(nodeVersion, minimumNodeVersion)) {
1710
+ throw new UnsupportedNodeVersionError(nodeVersion, minimumNodeVersion, subject);
1711
+ }
1049
1712
  }
1050
- function restartRequiredForRuntime(runtime, env = process.env) {
1051
- const mode = runtimeProfile(runtime, env).restartMode;
1052
- return mode === "restart-session" || mode === "restart-app";
1713
+ async function installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps) {
1714
+ const { runCommand, onProgress } = deps;
1715
+ const baseArgs = [
1716
+ "install",
1717
+ "--prefix",
1718
+ runtimeDirectory,
1719
+ "--no-audit",
1720
+ "--no-fund",
1721
+ "--omit=dev",
1722
+ "--prefer-offline",
1723
+ mcpPackageSpec(),
1724
+ sdkPackageSpec()
1725
+ ];
1726
+ const run = async (args) => {
1727
+ const startedAt = Date.now();
1728
+ const heartbeat = setInterval(() => {
1729
+ const seconds = Math.round((Date.now() - startedAt) / 1e3);
1730
+ onProgress?.(`Still installing the local Haven MCP runtime\u2026 (${seconds}s \u2014 a cold cache can take several minutes)`);
1731
+ }, SIGNER_INSTALL_HEARTBEAT_MS);
1732
+ heartbeat.unref?.();
1733
+ try {
1734
+ if (runCommand) await runCommand("npm", args);
1735
+ else await execFileAsync2("npm", args, { timeout: SIGNER_INSTALL_TIMEOUT_MS, maxBuffer: 1024 * 1024 });
1736
+ } finally {
1737
+ clearInterval(heartbeat);
1738
+ }
1739
+ };
1740
+ try {
1741
+ await run(baseArgs);
1742
+ } catch {
1743
+ try {
1744
+ await run([...baseArgs, "--cache", npmCacheDirectory]);
1745
+ } catch (err) {
1746
+ throw new Error(`Could not install local Haven MCP runtime ${mcpPackageSpec()}: ${err instanceof Error ? err.message : String(err)}`);
1747
+ }
1748
+ }
1053
1749
  }
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.`;
1750
+ async function installedRuntimeMatches2(runtimeDirectory, cliPath) {
1751
+ try {
1752
+ await assertFileExists2(cliPath, "local Haven MCP CLI");
1753
+ const [mcpPackage, sdkPackage] = await Promise.all([
1754
+ readPackageJson2(path.join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "package.json")),
1755
+ readPackageJson2(path.join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
1756
+ ]);
1757
+ return mcpPackage.version === exports.MCP_RUNTIME_MANIFEST.mcpVersion && sdkPackage.version === exports.MCP_RUNTIME_MANIFEST.sdkVersion;
1758
+ } catch {
1759
+ return false;
1760
+ }
1057
1761
  }
1058
- function normalizeRuntimeName(runtime) {
1059
- const key = runtime?.trim().toLowerCase();
1060
- if (!key) return null;
1061
- return RUNTIME_ALIASES[key.replace(/\s+/g, "-")] ?? null;
1762
+ async function readPackageJson2(path) {
1763
+ return JSON.parse(await promises.readFile(path, "utf8"));
1062
1764
  }
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;
1765
+ async function writeWrapper2(input) {
1766
+ await promises.mkdir(path.dirname(input.wrapperPath), { recursive: true, mode: 448 });
1767
+ await promises.chmod(path.dirname(input.wrapperPath), 448).catch(() => void 0);
1768
+ const source = [
1769
+ "#!/usr/bin/env node",
1770
+ "import { spawn } from 'node:child_process'",
1771
+ "",
1772
+ `const cliPath = ${JSON.stringify(input.cliPath)}`,
1773
+ `const identityPath = ${JSON.stringify(input.identityPath)}`,
1774
+ `const signerPath = ${JSON.stringify(input.signerPath)}`,
1775
+ "",
1776
+ "const child = spawn(process.execPath, [cliPath, '--identity', identityPath, '--signer', signerPath, ...process.argv.slice(2)], {",
1777
+ " stdio: 'inherit',",
1778
+ "})",
1779
+ "",
1780
+ "child.on('exit', (code, signal) => {",
1781
+ " if (signal) process.kill(process.pid, signal)",
1782
+ " else process.exit(code ?? 1)",
1783
+ "})",
1784
+ ""
1785
+ ].join("\n");
1786
+ await promises.writeFile(input.wrapperPath, source, { mode: 448 });
1787
+ await promises.chmod(input.wrapperPath, 448).catch(() => void 0);
1069
1788
  }
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
- },
1789
+ async function writeRuntimeSidecar2(input) {
1790
+ const value = {
1791
+ ...input.serverName ? { server_name: input.serverName } : {},
1792
+ mcp_package: exports.MCP_RUNTIME_MANIFEST.mcpPackage,
1793
+ mcp_version: exports.MCP_RUNTIME_MANIFEST.mcpVersion,
1794
+ sdk_package: exports.MCP_RUNTIME_MANIFEST.sdkPackage,
1795
+ sdk_version: exports.MCP_RUNTIME_MANIFEST.sdkVersion,
1796
+ minimum_node_version: exports.MCP_RUNTIME_MANIFEST.minimumNodeVersion,
1797
+ wrapper_path: input.wrapperPath,
1798
+ runtime_directory: input.runtimeDirectory,
1799
+ npm_cache_directory: input.npmCacheDirectory,
1800
+ cli_path: input.cliPath
1801
+ };
1802
+ await promises.writeFile(input.path, `${JSON.stringify(value, null, 2)}
1803
+ `, { mode: 384 });
1804
+ await promises.chmod(input.path, 384).catch(() => void 0);
1805
+ }
1806
+ async function assertFileExists2(path, label) {
1807
+ try {
1808
+ await promises.access(path);
1809
+ } catch {
1810
+ throw new Error(`Missing ${label}: ${path}`);
1811
+ }
1812
+ }
1813
+ var execFileAsync2, UnsupportedNodeVersionError;
1814
+ var init_local_mcp_runtime = __esm({
1815
+ "src/local-mcp-runtime.ts"() {
1816
+ init_signer_runtime();
1817
+ init_runtime_manifest();
1818
+ execFileAsync2 = util.promisify(child_process.execFile);
1819
+ UnsupportedNodeVersionError = class extends Error {
1820
+ code = "local_mcp_unsupported_node_version";
1821
+ nodeVersion;
1822
+ minimumNodeVersion;
1823
+ constructor(nodeVersion, minimumNodeVersion, subject = "Haven setup") {
1824
+ super(sdk.unsupportedNodeVersionMessage({ subject, nodeVersion, minimumNodeVersion }));
1825
+ this.name = "UnsupportedNodeVersionError";
1826
+ this.nodeVersion = nodeVersion;
1827
+ this.minimumNodeVersion = minimumNodeVersion;
1828
+ }
1829
+ };
1830
+ }
1831
+ });
1832
+ async function installSkillForRuntime(runtime, deps = {}) {
1833
+ switch (runtime) {
1834
+ case "claude-code":
1835
+ return installSkillFile(
1836
+ path.resolve(deps.homeDir ?? os.homedir(), ".claude", "skills", sdk.SKILL_FOLDER_NAME),
1837
+ "~/.claude/skills/haven-pay"
1838
+ );
1839
+ case "hermes":
1840
+ return installSkillFile(
1841
+ path.join(hermesHome(deps), "skills", sdk.SKILL_FOLDER_NAME),
1842
+ "the Hermes skills folder"
1843
+ );
1844
+ case "codex-cli":
1845
+ case "codex-desktop":
1846
+ return installCodexAgentsSection(deps);
1847
+ default:
1848
+ return void 0;
1849
+ }
1850
+ }
1851
+ async function installSkillFile(skillDir, label) {
1852
+ try {
1853
+ await promises.mkdir(skillDir, { recursive: true });
1854
+ const target = path.join(skillDir, "SKILL.md");
1855
+ await promises.writeFile(target, sdk.HAVEN_SKILL_MD, "utf8");
1856
+ return {
1857
+ installed: true,
1858
+ target,
1859
+ messages: [`Installed the generic Haven payment skill (${label}). It contains no secrets.`]
1860
+ };
1861
+ } catch (err) {
1862
+ return {
1863
+ installed: false,
1864
+ messages: [
1865
+ `Could not install the Haven payment skill: ${err instanceof Error ? err.message : String(err)}. Download it from the Haven dashboard instead.`
1866
+ ]
1867
+ };
1868
+ }
1869
+ }
1870
+ async function installCodexAgentsSection(deps) {
1871
+ try {
1872
+ const codexDir = path.resolve(deps.homeDir ?? os.homedir(), ".codex");
1873
+ const target = path.join(codexDir, "AGENTS.md");
1874
+ await promises.mkdir(codexDir, { recursive: true });
1875
+ const existing = await promises.readFile(target, "utf8").catch(() => null);
1876
+ const next = upsertManagedSection(existing, codexManagedSection());
1877
+ if (next !== existing) {
1878
+ await promises.writeFile(target, next, "utf8");
1879
+ }
1880
+ return {
1881
+ installed: true,
1882
+ target,
1883
+ messages: [
1884
+ "Installed the generic Haven payment guidance as a managed section in ~/.codex/AGENTS.md (Codex reads it as global instructions). It contains no secrets; your own content in that file is untouched."
1885
+ ]
1886
+ };
1887
+ } catch (err) {
1888
+ return {
1889
+ installed: false,
1890
+ messages: [
1891
+ `Could not install the Haven payment guidance into ~/.codex/AGENTS.md: ${err instanceof Error ? err.message : String(err)}. Download the skill from the Haven dashboard instead.`
1892
+ ]
1893
+ };
1894
+ }
1895
+ }
1896
+ function codexManagedSection() {
1897
+ return `${CODEX_AGENTS_BEGIN_MARKER}
1898
+
1899
+ ${sdk.HAVEN_SKILL_BODY_MD.trimEnd()}
1900
+
1901
+ ${CODEX_AGENTS_END_MARKER}
1902
+ `;
1903
+ }
1904
+ function upsertManagedSection(existing, section) {
1905
+ if (existing === null || existing.trim() === "") return section;
1906
+ const begins = markerLineIndexes(existing, CODEX_AGENTS_BEGIN_MARKER);
1907
+ const ends = markerLineIndexes(existing, CODEX_AGENTS_END_MARKER);
1908
+ if (begins.length === 1 && ends.length === 1 && ends[0] > begins[0]) {
1909
+ const afterEnd = ends[0] + CODEX_AGENTS_END_MARKER.length;
1910
+ const tail = existing.startsWith("\r\n", afterEnd) ? existing.slice(afterEnd + 2) : existing.startsWith("\n", afterEnd) ? existing.slice(afterEnd + 1) : existing.slice(afterEnd);
1911
+ return existing.slice(0, begins[0]) + section + tail;
1912
+ }
1913
+ if (begins.length > 0 || ends.length > 0) {
1914
+ throw new Error(
1915
+ "found a damaged Haven marker section (orphaned or duplicated markers); remove the leftover marker lines and re-run setup"
1916
+ );
1917
+ }
1918
+ return `${existing.replace(/\n*$/, "\n\n")}${section}`;
1919
+ }
1920
+ function markerLineIndexes(text, marker) {
1921
+ const indexes = [];
1922
+ for (let from = 0; ; ) {
1923
+ const at = text.indexOf(marker, from);
1924
+ if (at === -1) return indexes;
1925
+ if (at === 0 || text[at - 1] === "\n") indexes.push(at);
1926
+ from = at + marker.length;
1927
+ }
1928
+ }
1929
+ function hermesHome(deps) {
1930
+ const env = deps.env ?? process.env;
1931
+ return env.HERMES_HOME ?? path.join(deps.homeDir ?? os.homedir(), ".hermes");
1932
+ }
1933
+ var CODEX_AGENTS_BEGIN_MARKER, CODEX_AGENTS_END_MARKER;
1934
+ var init_skill_install = __esm({
1935
+ "src/skill-install.ts"() {
1936
+ CODEX_AGENTS_BEGIN_MARKER = "<!-- BEGIN haven-pay (managed by @haven_ai/connect; edits inside this section are overwritten on re-setup) -->";
1937
+ CODEX_AGENTS_END_MARKER = "<!-- END haven-pay -->";
1938
+ }
1939
+ });
1940
+
1941
+ // src/connect-error.ts
1942
+ function isConnectError(err) {
1943
+ return err instanceof exports.ConnectError;
1944
+ }
1945
+ exports.ConnectError = void 0;
1946
+ var init_connect_error = __esm({
1947
+ "src/connect-error.ts"() {
1948
+ exports.ConnectError = class extends Error {
1949
+ code;
1950
+ nextAction;
1951
+ constructor(code, message, nextAction2) {
1952
+ super(message);
1953
+ this.name = "ConnectError";
1954
+ this.code = code;
1955
+ this.nextAction = nextAction2;
1956
+ }
1957
+ };
1958
+ }
1959
+ });
1960
+
1961
+ // src/runtime-registry.ts
1962
+ function runtimeProfile(runtime, env = process.env) {
1963
+ return RUNTIME_PROFILES[normalizeRuntime(runtime, env)];
1964
+ }
1965
+ function normalizeRuntime(runtime, env = process.env) {
1966
+ const explicit = normalizeRuntimeName(runtime);
1967
+ if (explicit) return explicit;
1968
+ return detectRuntime(env) ?? "other";
1969
+ }
1970
+ async function resolveRuntimeSelection(explicit, force, options = {}) {
1971
+ const env = options.env ?? process.env;
1972
+ if (force !== void 0) {
1973
+ const forced = normalizeRuntimeName(force);
1974
+ if (!forced) {
1975
+ throw new exports.ConnectError(
1976
+ "runtime_force_unrecognized",
1977
+ `Unknown --runtime-force value "${force}". Valid values: ${exports.RUNTIME_FLAG_VALUES}.`,
1978
+ "rerun_connect_with_a_valid_runtime_name"
1979
+ );
1980
+ }
1981
+ return { runtime: forced, source: "force" };
1982
+ }
1983
+ const detected = detectRuntime(env);
1984
+ const supplied = explicit?.trim() || options.selfReported?.trim() || void 0;
1985
+ const hint = normalizeRuntimeName(supplied);
1986
+ if (supplied && !hint) {
1987
+ if (!detected) {
1988
+ throw new exports.ConnectError(
1989
+ "runtime_unrecognized",
1990
+ `"${supplied}" is not an agent runtime Haven knows. Valid values: ${exports.RUNTIME_FLAG_VALUES} (the aliases cowork, codex and openclaw are accepted too). Re-run with one of those, or --runtime other to store credentials and finish the MCP setup by hand. Nothing was written and the Haven setup token is still unused.`,
1991
+ "rerun_connect_with_a_valid_runtime_name"
1992
+ );
1993
+ }
1994
+ return { runtime: detected, source: "detected", discardedHint: supplied };
1995
+ }
1996
+ if (detected && hint && detected !== hint) {
1997
+ return { runtime: detected, source: "detected", overrodeHint: hint };
1998
+ }
1999
+ if (hint) return { runtime: hint, source: "explicit" };
2000
+ if (detected) return { runtime: detected, source: "detected" };
2001
+ if (options.promptForRuntime) {
2002
+ return { runtime: await options.promptForRuntime(), source: "prompted" };
2003
+ }
2004
+ return { runtime: null, source: "none" };
2005
+ }
2006
+ function restartRequiredForRuntime(runtime, env = process.env) {
2007
+ const mode = runtimeProfile(runtime, env).restartMode;
2008
+ return mode === "restart-session" || mode === "restart-app";
2009
+ }
2010
+ function runtimeVerificationInstruction(runtime) {
2011
+ const label = RUNTIME_PROFILES[runtime].label;
2012
+ 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.`;
2013
+ }
2014
+ function normalizeRuntimeName(runtime) {
2015
+ const key = runtime?.trim().toLowerCase();
2016
+ if (!key) return null;
2017
+ return RUNTIME_ALIASES[key.replace(/\s+/g, "-")] ?? null;
2018
+ }
2019
+ function detectRuntime(env) {
2020
+ if (env.CLAUDECODE || env.CLAUDE_CODE || env.CLAUDECODE_CWD) return "claude-code";
2021
+ if (env.CODEX_SANDBOX || env.CODEX_HOME || env.CODEX_CWD) return "codex-cli";
2022
+ if (env.VSCODE_CWD || env.VSCODE_IPC_HOOK_CLI || env.TERM_PROGRAM === "vscode") return "vscode";
2023
+ if (env.HERMES_HOME || env.HERMES_AGENT) return "hermes";
2024
+ return null;
2025
+ }
2026
+ var RUNTIME_PROFILES, RUNTIME_ALIASES; exports.RUNTIME_FLAG_VALUES = void 0;
2027
+ var init_runtime_registry = __esm({
2028
+ "src/runtime-registry.ts"() {
2029
+ init_connect_error();
2030
+ RUNTIME_PROFILES = {
2031
+ "claude-code": {
2032
+ id: "claude-code",
2033
+ label: "Claude Code",
2034
+ restartMode: "restart-session",
2035
+ canWriteRuntimeConfig: true,
2036
+ activationInstruction: "Start a new Claude Code session so it loads the Haven MCP entries."
2037
+ },
2038
+ "codex-cli": {
2039
+ id: "codex-cli",
2040
+ label: "Codex CLI",
2041
+ restartMode: "restart-session",
2042
+ canWriteRuntimeConfig: true,
2043
+ activationInstruction: "Start a fresh Codex CLI session (for example, run `codex resume --last`)."
2044
+ },
2045
+ "codex-desktop": {
2046
+ id: "codex-desktop",
2047
+ label: "Codex Desktop",
2048
+ restartMode: "restart-session",
2049
+ canWriteRuntimeConfig: true,
2050
+ activationInstruction: "Quit and reopen Codex Desktop so it loads the Haven MCP entries."
2051
+ },
1095
2052
  cursor: {
1096
2053
  id: "cursor",
1097
2054
  label: "Cursor",
@@ -1140,6 +2097,11 @@ var init_runtime_registry = __esm({
1140
2097
  "claude-code": "claude-code",
1141
2098
  claudecode: "claude-code",
1142
2099
  "claude_code": "claude-code",
2100
+ // #1682: Cowork runs Claude Code's config, so it resolves to the same
2101
+ // profile. The dashboard's command carries no --runtime for it (it is a
2102
+ // detected, command-path runtime), but an explicit `--runtime cowork` typed
2103
+ // by hand must not fall through to the no-runtime refusal.
2104
+ cowork: "claude-code",
1143
2105
  codex: "codex-cli",
1144
2106
  "codex-cli": "codex-cli",
1145
2107
  codexcli: "codex-cli",
@@ -1169,9 +2131,18 @@ var init_runtime_registry = __esm({
1169
2131
  "hermes-agent": "hermes",
1170
2132
  hermes_agent: "hermes",
1171
2133
  hermesagent: "hermes",
2134
+ // #1682: OpenClaw is a SNIPPET target — the user pastes an mcpServers entry
2135
+ // into ~/.openclaw/openclaw.json and restarts the gateway. That is exactly
2136
+ // the 'other' profile's behaviour (credentials written to disk, no config
2137
+ // auto-written, manual finish), so it resolves there rather than earning a
2138
+ // profile whose only distinguishing feature would be its label.
2139
+ openclaw: "other",
2140
+ "open-claw": "other",
2141
+ "open_claw": "other",
1172
2142
  other: "other",
1173
2143
  manual: "other"
1174
2144
  };
2145
+ exports.RUNTIME_FLAG_VALUES = "claude-code, codex-cli, codex-desktop, cursor, vscode, vscode-insiders, claude-desktop, hermes, other";
1175
2146
  }
1176
2147
  });
1177
2148
  async function acknowledgeLocalSignerConsent(signerPath, log) {
@@ -1247,1254 +2218,1494 @@ var init_signer_consent = __esm({
1247
2218
  "src/signer-consent.ts"() {
1248
2219
  }
1249
2220
  });
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 {};
2221
+ async function installRuntime(input, deps = {}) {
2222
+ const runtime = normalizeRuntime(input.runtime, deps.env);
2223
+ const profile = runtimeProfile(runtime, deps.env);
2224
+ const progress = deps.onProgress ?? (() => void 0);
2225
+ const localRuntime = input.localMcp === true && supportsLocalMcp(runtime);
2226
+ const consentMessages = [];
2227
+ const localMcpConsent = localRuntime ? await resolveLocalMcpConsent(input, consentMessages) : void 0;
2228
+ const signerConsent = localRuntime ? void 0 : await resolveSignerConsent(input, consentMessages);
2229
+ if (runtime === "other") {
2230
+ const signerCredentialReady2 = await probeLocalSignerCredential(input.signerPath);
2231
+ const signerReady = signerCredentialReady2 && signerConsent?.acknowledged;
2232
+ return {
2233
+ runtime,
2234
+ runtimeMcpMode: "manual",
2235
+ hostedMcpConfigured: false,
2236
+ localSignerConfigured: false,
2237
+ localMcpConfigured: false,
2238
+ probeResult: signerReady ? "manual_runtime_setup_required_local_signer_ready" : "manual_runtime_setup_required_local_signer_unavailable",
2239
+ restartRequired: true,
2240
+ nextUserAction: "return_to_haven_for_wallet_approval_then_configure_runtime",
2241
+ errorCode: "manual_runtime_setup_required",
2242
+ configTarget: "manual runtime setup",
2243
+ signerAcknowledged: signerConsent?.acknowledged,
2244
+ localMcpAcknowledged: false,
2245
+ messages: [
2246
+ ...consentMessages,
2247
+ "Custom runtime: Haven did not auto-configure it. Your credentials are on disk (chmod 600) \u2014 read them at runtime; never paste a key into the agent prompt, memory, or logs.",
2248
+ ` identity (hosted MCP Bearer): ${input.identityPath}`,
2249
+ ` signer (local signing key): ${input.signerPath}`,
2250
+ "After wallet approval, wire the runtime to Haven by reference:",
2251
+ ` Hosted MCP + local signer: point your MCP client at ${input.hostedMcpUrl} with the api_key from identity.json, then run npx -y ${signerPackageSpec()} --credentials ${input.signerPath}`,
2252
+ ` Fully local MCP (no hosted dependency): npx -y ${mcpPackageSpec()} --identity ${input.identityPath} --signer ${input.signerPath}`
2253
+ ]
2254
+ };
1265
2255
  }
1266
- const candidates = [];
1267
- for (const entry of entries) {
1268
- const directory = path.join(root, entry);
2256
+ let localRuntimeInstall;
2257
+ let localRuntimeError;
2258
+ if (localRuntime) {
1269
2259
  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
- });
2260
+ localRuntimeInstall = await prepareRuntimeForLocalMcp(input, deps);
2261
+ } catch (err) {
2262
+ localRuntimeError = err;
1427
2263
  }
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
2264
  }
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) {
2265
+ if (localRuntimeError) {
2266
+ const errorCode2 = localRuntimePrepareErrorCode(localRuntimeError);
1459
2267
  return {
1460
- ok: false,
1461
- messages: [`No agent credentials found to repair \u2014 run the full setup: ${RERUN} --setup <token>.`]
2268
+ runtime,
2269
+ runtimeMcpMode: "local_stdio",
2270
+ hostedMcpConfigured: false,
2271
+ localSignerConfigured: false,
2272
+ localMcpConfigured: false,
2273
+ probeResult: errorCode2 === "local_mcp_unsupported_node_version" ? "local_stdio_mcp_unsupported_node_version" : "local_stdio_mcp_runtime_install_failed",
2274
+ restartRequired: true,
2275
+ nextUserAction: nextAction(runtime, profile.restartMode, errorCode2),
2276
+ errorCode: errorCode2,
2277
+ configTarget: profile.label,
2278
+ signerAcknowledged: signerConsent?.acknowledged,
2279
+ localMcpAcknowledged: localMcpConsent?.acknowledged,
2280
+ activationCommand: void 0,
2281
+ messages: [
2282
+ ...consentMessages,
2283
+ `Could not prepare local Haven MCP runtime: ${localRuntimeError instanceof Error ? localRuntimeError.message : String(localRuntimeError)}`
2284
+ ]
1462
2285
  };
1463
2286
  }
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) {
2287
+ let signerCommand;
2288
+ if (!localRuntime) {
2289
+ progress("Getting the signer ready\u2026");
1475
2290
  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 {
2291
+ const signerRuntime = await prepareSignerForRuntime(input, deps);
2292
+ signerCommand = { command: signerRuntime.command, args: signerRuntime.args };
2293
+ consentMessages.push(...signerRuntime.messages);
2294
+ } catch (err) {
2295
+ return {
2296
+ runtime,
2297
+ runtimeMcpMode: "hosted_plus_signer",
2298
+ hostedMcpConfigured: false,
2299
+ localSignerConfigured: false,
2300
+ localMcpConfigured: false,
2301
+ probeResult: "signer_runtime_install_failed",
2302
+ restartRequired: false,
2303
+ 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",
2304
+ errorCode: "signer_runtime_install_failed",
2305
+ configTarget: profile.label,
2306
+ signerAcknowledged: signerConsent?.acknowledged,
2307
+ localMcpAcknowledged: localMcpConsent?.acknowledged,
2308
+ activationCommand: void 0,
2309
+ signerRuntimePrepared: false,
2310
+ messages: [
2311
+ ...consentMessages,
2312
+ `Could not pre-install the local Haven signer: ${err instanceof Error ? err.message : String(err)}`,
2313
+ "No runtime configuration was written (fail-closed): a config pointing at an uninstalled signer looks wired but cannot start.",
2314
+ "Re-run `npx @haven_ai/connect@alpha` to retry the setup."
2315
+ ]
2316
+ };
1487
2317
  }
1488
2318
  }
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
- })
2319
+ const signerRuntimePrepared = localRuntime ? void 0 : signerCommand !== void 0;
2320
+ progress("Setting up your Haven tools\u2026");
2321
+ const configResult = localRuntime ? runtime === "claude-code" ? await configureClaudeCode(deps, localRuntimeInstall?.command ?? "", input.serverName) : await writeRuntimeConfig({
2322
+ runtime,
2323
+ hostedMcpUrl: input.hostedMcpUrl,
2324
+ apiKey: input.apiKey,
2325
+ identityPath: input.identityPath,
2326
+ signerPath: input.signerPath,
2327
+ serverName: input.serverName,
2328
+ credentialDirectory: input.credentialDirectory,
2329
+ localMcpCommand: localRuntimeInstall?.command,
2330
+ signerCommand,
2331
+ homeDir: deps.homeDir,
2332
+ mode: "local"
2333
+ }) : await writeHostedRuntimeConfig(deps, { ...input, runtime }, signerCommand);
2334
+ if (deps.onRuntimeConfigured) {
2335
+ const signerCredentialOnDisk = await probeLocalSignerCredential(input.signerPath);
2336
+ const earlyLocalMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialOnDisk && Boolean(localMcpConsent?.acknowledged);
2337
+ const earlySignerOk = configResult.runtimeMcpMode === "local_stdio" ? earlyLocalMcpOk : configResult.signerConfigured && signerCredentialOnDisk && Boolean(signerConsent?.acknowledged);
2338
+ try {
2339
+ await deps.onRuntimeConfigured({
2340
+ runtime,
2341
+ runtimeMcpMode: configResult.runtimeMcpMode,
2342
+ hostedMcpConfigured: configResult.hostedConfigured,
2343
+ localSignerConfigured: earlySignerOk,
2344
+ localMcpConfigured: earlyLocalMcpOk,
2345
+ signerAcknowledged: signerConsent?.acknowledged,
2346
+ localMcpAcknowledged: localMcpConsent?.acknowledged,
2347
+ restartRequired: configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env),
2348
+ nextUserAction: nextAction(runtime, profile.restartMode, configResult.errorCode),
2349
+ errorCode: configResult.errorCode
1579
2350
  });
2351
+ } catch {
1580
2352
  }
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
2353
  }
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}`);
2354
+ progress("Almost there \u2014 just confirming everything connects\u2026");
2355
+ const localProbePromise = configResult.runtimeMcpMode === "local_stdio" && localRuntimeInstall ? runLocalMcpProbe(localRuntimeInstall, deps) : Promise.resolve(void 0);
2356
+ const signerProbePromise = configResult.runtimeMcpMode !== "local_stdio" && signerCommand ? (deps.probeSignerTools ?? probeLocalMcpTools)(
2357
+ signerCommand.command,
2358
+ signerCommand.args,
2359
+ exports.MCP_RUNTIME_MANIFEST.requiredSignerTools
2360
+ ) : Promise.resolve(void 0);
2361
+ const [hostedProbe, signerCredentialReady, localMcpProbe, signerProbe] = await Promise.all([
2362
+ configResult.hostedConfigured ? probeHostedMcpTools(input.apiKey, input.hostedMcpUrl, deps.fetch) : Promise.resolve({ status: "bad_response" }),
2363
+ probeLocalSignerCredential(input.signerPath),
2364
+ localProbePromise,
2365
+ signerProbePromise
2366
+ ]);
2367
+ const hostedOk = configResult.hostedConfigured && hostedProbe.status === "ok";
2368
+ const localMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialReady && Boolean(localMcpConsent?.acknowledged) && localMcpProbe?.status === "ok";
2369
+ const signerOk = configResult.runtimeMcpMode === "local_stdio" ? localMcpOk : configResult.signerConfigured && signerCredentialReady && Boolean(signerConsent?.acknowledged) && // #1587: no handshake, no green. A signer command that was registered
2370
+ // but not probed (manual topology) keeps the old semantics.
2371
+ (signerProbe === void 0 || signerProbe.status === "ok");
2372
+ const restartRequired = configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env);
2373
+ const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : hostedMcpErrorCode(configResult.hostedConfigured, hostedProbe.status) ?? signerConsentErrorCode(signerCredentialReady, signerConsent) ?? signerProbeErrorCode(signerProbe));
2374
+ 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."] : [];
2375
+ const signerProbeMessages = signerProbe ? signerProbe.status === "ok" ? ["Verified local Haven signer with a stdio handshake."] : [
2376
+ `Local Haven signer handshake failed: ${signerProbe.status}.`,
2377
+ "Re-run `npx @haven_ai/connect@alpha` to repair the signer setup."
2378
+ ] : [];
2379
+ 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."] : [];
2380
+ const skillInstall = !configResult.errorCode ? await installSkillForRuntime(runtime, { homeDir: deps.homeDir, env: deps.env }) : void 0;
2381
+ return {
2382
+ runtime,
2383
+ runtimeMcpMode: configResult.runtimeMcpMode,
2384
+ hostedMcpConfigured: hostedOk,
2385
+ localSignerConfigured: signerOk,
2386
+ localMcpConfigured: localMcpOk,
2387
+ probeResult: buildProbeResult(configResult.runtimeMcpMode, configResult.hostedConfigured, hostedProbe.status, signerOk, localMcpOk, localMcpProbe?.status),
2388
+ restartRequired,
2389
+ nextUserAction: nextAction(runtime, profile.restartMode, errorCode),
2390
+ errorCode,
2391
+ configTarget: configResult.target,
2392
+ signerAcknowledged: signerConsent?.acknowledged,
2393
+ localMcpAcknowledged: localMcpConsent?.acknowledged,
2394
+ activationCommand: configResult.activationCommand,
2395
+ skillInstalled: skillInstall?.installed,
2396
+ signerRuntimePrepared,
2397
+ messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...hostedProbeMessages, ...signerProbeMessages, ...localProbeMessages, ...skillInstall?.messages ?? []]
2398
+ };
1742
2399
  }
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
- }
2400
+ function runtimeInstallCapabilities(runtime, env = process.env) {
2401
+ const profile = runtimeProfile(runtime, env);
2402
+ return {
2403
+ canWriteRuntimeConfig: profile.canWriteRuntimeConfig,
2404
+ restartRequired: restartRequiredForRuntime(runtime, env)
2405
+ };
1751
2406
  }
1752
-
1753
- // src/runtime-install.ts
1754
- init_config_writers();
1755
- async function acknowledgeLocalMcpConsent(identityPath, signerPath, log) {
2407
+ async function configureClaudeCode(deps, localMcpCommand, serverName) {
2408
+ const runCommand = deps.runCommand ?? defaultRunCommand;
2409
+ const serverJson = JSON.stringify({
2410
+ type: "stdio",
2411
+ command: localMcpCommand,
2412
+ args: [],
2413
+ env: {}
2414
+ });
1756
2415
  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
2416
+ if (!localMcpCommand) throw new Error("local MCP wrapper command is required");
2417
+ const names = serverNamesFor(serverName);
2418
+ await runCommand("claude", ["mcp", "remove", names.hosted]).catch(() => void 0);
2419
+ await runCommand("claude", ["mcp", "remove", names.signer]).catch(() => void 0);
2420
+ await runCommand("claude", ["mcp", "add-json", names.hosted, serverJson, "--scope", "user"]).catch(async () => {
2421
+ await runCommand("claude", ["mcp", "add", names.hosted, "--scope", "user", "--", localMcpCommand]);
1762
2422
  });
2423
+ const verified = await runCommand("claude", ["mcp", "get", names.hosted]).then(() => true).catch(() => false);
1763
2424
  return {
1764
- acknowledged: decision.ok,
1765
- hash: decision.hash,
1766
- reason: decision.reason
2425
+ hostedConfigured: false,
2426
+ signerConfigured: true,
2427
+ localMcpConfigured: true,
2428
+ runtimeMcpMode: "local_stdio",
2429
+ target: "Claude Code MCP config",
2430
+ changed: true,
2431
+ restartRequired: true,
2432
+ messages: [
2433
+ "Updated local Haven MCP entry with Claude Code.",
2434
+ ...verified ? ["Verified Claude Code MCP entry."] : []
2435
+ ]
1767
2436
  };
1768
2437
  } catch (err) {
1769
2438
  return {
1770
- acknowledged: false,
1771
- error: err instanceof Error ? err.message : String(err)
2439
+ hostedConfigured: false,
2440
+ signerConfigured: false,
2441
+ localMcpConfigured: false,
2442
+ runtimeMcpMode: "local_stdio",
2443
+ target: "Claude Code MCP config",
2444
+ changed: false,
2445
+ restartRequired: true,
2446
+ messages: [
2447
+ `Could not update Claude Code MCP config: ${err instanceof Error ? err.message : String(err)}`,
2448
+ "Install Claude Code or rerun the Haven setup command inside a Claude Code terminal."
2449
+ ],
2450
+ errorCode: "claude_code_config_failed"
1772
2451
  };
1773
2452
  }
1774
2453
  }
1775
- async function getLocalMcpConsentStatus(identityPath, signerPath) {
2454
+ async function writeHostedRuntimeConfig(deps, input, signerCommand) {
2455
+ if (input.runtime === "claude-code") {
2456
+ return configureClaudeCodeHosted(deps, input, signerCommand);
2457
+ }
2458
+ return writeRuntimeConfig({
2459
+ runtime: input.runtime,
2460
+ hostedMcpUrl: input.hostedMcpUrl,
2461
+ apiKey: input.apiKey,
2462
+ identityPath: input.identityPath,
2463
+ signerPath: input.signerPath,
2464
+ serverName: input.serverName,
2465
+ credentialDirectory: input.credentialDirectory,
2466
+ signerCommand,
2467
+ homeDir: deps.homeDir,
2468
+ mode: "hosted"
2469
+ });
2470
+ }
2471
+ async function configureClaudeCodeHosted(deps, input, signerCommand) {
2472
+ const runCommand = deps.runCommand ?? defaultRunCommand;
2473
+ const hostedJson = JSON.stringify({
2474
+ type: "http",
2475
+ url: input.hostedMcpUrl,
2476
+ headers: { Authorization: `Bearer ${input.apiKey}` }
2477
+ });
2478
+ const signerJson = JSON.stringify({
2479
+ type: "stdio",
2480
+ command: signerCommand?.command ?? "npx",
2481
+ args: signerCommand?.args ?? ["-y", signerPackageSpec(), "--credentials", input.signerPath],
2482
+ env: {}
2483
+ });
1776
2484
  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
- }
2485
+ const names = serverNamesFor(input.serverName);
2486
+ await runCommand("claude", ["mcp", "remove", names.hosted]).catch(() => void 0);
2487
+ await runCommand("claude", ["mcp", "remove", names.signer]).catch(() => void 0);
2488
+ await runCommand("claude", ["mcp", "add-json", names.hosted, hostedJson, "--scope", "user"]);
2489
+ await runCommand("claude", ["mcp", "add-json", names.signer, signerJson, "--scope", "user"]);
2490
+ const verified = await runCommand("claude", ["mcp", "get", names.hosted]).then(() => true).catch(() => false);
1783
2491
  return {
1784
- acknowledged: false,
1785
- hash,
1786
- reason: stored ? "ack_file_mismatch" : "ack_file_missing"
2492
+ hostedConfigured: true,
2493
+ signerConfigured: true,
2494
+ localMcpConfigured: false,
2495
+ runtimeMcpMode: "hosted_plus_signer",
2496
+ target: "Claude Code MCP config",
2497
+ changed: true,
2498
+ restartRequired: true,
2499
+ messages: [
2500
+ "Updated hosted Haven MCP and local signer entries with Claude Code.",
2501
+ ...verified ? ["Verified Claude Code MCP entry."] : []
2502
+ ]
1787
2503
  };
1788
2504
  } catch (err) {
1789
2505
  return {
1790
- acknowledged: false,
1791
- error: err instanceof Error ? err.message : String(err)
2506
+ hostedConfigured: false,
2507
+ signerConfigured: false,
2508
+ localMcpConfigured: false,
2509
+ runtimeMcpMode: "hosted_plus_signer",
2510
+ target: "Claude Code MCP config",
2511
+ changed: false,
2512
+ restartRequired: true,
2513
+ messages: [
2514
+ `Could not update Claude Code MCP config: ${err instanceof Error ? err.message : String(err)}`,
2515
+ "Install Claude Code or rerun the Haven setup command inside a Claude Code terminal."
2516
+ ],
2517
+ errorCode: "claude_code_config_failed"
1792
2518
  };
1793
2519
  }
1794
2520
  }
1795
- function localMcpAckPath(identityPath) {
1796
- return path.resolve(`${identityPath}.ack.json`);
2521
+ async function defaultRunCommand(command, args) {
2522
+ await execFileAsync3(command, args, { timeout: 1e4 });
1797
2523
  }
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.");
2524
+ function buildProbeResult(mode, hostedConfigured, hostedStatus, signerReady, localMcpReady, localMcpProbeStatus) {
2525
+ if (mode === "local_stdio") {
2526
+ if (localMcpReady) return "local_stdio_mcp_ready";
2527
+ return localMcpProbeStatus ? `local_stdio_mcp_${localMcpProbeStatus}` : "local_stdio_mcp_unavailable";
2528
+ }
2529
+ const hostedPart = hostedConfigured ? `hosted_${hostedStatus}` : "hosted_not_configured";
2530
+ const signerPart = signerReady ? "local_signer_ready" : "local_signer_unavailable";
2531
+ return `${hostedPart}_${signerPart}`.slice(0, 120);
2532
+ }
2533
+ async function resolveLocalMcpConsent(input, messages) {
2534
+ if (input.ackLocalTools || input.ackSigner) {
2535
+ const status = await acknowledgeLocalMcpConsent(input.identityPath, input.signerPath, (message) => messages.push(message));
2536
+ if (status.acknowledged) {
2537
+ messages.push("Prepared the local Haven tools acknowledgement.");
2538
+ } else {
2539
+ messages.push("Local Haven tools acknowledgement still needs attention.");
1803
2540
  }
1804
- };
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
- );
2541
+ return status;
2542
+ }
2543
+ return getLocalMcpConsentStatus(input.identityPath, input.signerPath);
1818
2544
  }
1819
- async function readLocalMcpAckFile(path) {
1820
- try {
1821
- const parsed = JSON.parse(await promises.readFile(path, "utf8"));
1822
- return typeof parsed.ack === "string" ? parsed.ack : null;
1823
- } catch {
1824
- return null;
2545
+ async function resolveSignerConsent(input, messages) {
2546
+ if (input.ackSigner || input.ackLocalTools) {
2547
+ const status = await acknowledgeLocalSignerConsent(input.signerPath, (message) => messages.push(message));
2548
+ if (status.acknowledged) {
2549
+ messages.push("Prepared the local Haven signer acknowledgement.");
2550
+ } else {
2551
+ messages.push("Local Haven signer acknowledgement still needs attention.");
2552
+ }
2553
+ return status;
1825
2554
  }
2555
+ return getLocalSignerConsentStatus(input.signerPath);
1826
2556
  }
1827
- function writeLogChunk(log, chunk) {
1828
- const message = String(chunk).trimEnd();
1829
- if (message) log(message);
2557
+ function signerConsentErrorCode(signerCredentialReady, signerConsent) {
2558
+ if (!signerCredentialReady) return "local_signer_credential_unavailable";
2559
+ if (!signerConsent?.acknowledged) return "local_signer_ack_required";
2560
+ return void 0;
1830
2561
  }
1831
-
1832
- // src/runtime-install.ts
1833
- init_probes();
1834
-
1835
- // src/local-mcp-runtime.ts
1836
- init_signer_runtime();
1837
- init_runtime_manifest();
1838
- var execFileAsync2 = util.promisify(child_process.execFile);
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);
1852
- const homeDir = input.homeDir ?? os.homedir();
1853
- const runtimeDirectory = path.resolve(homeDir, ".haven", "mcp-runtime", exports.MCP_RUNTIME_MANIFEST.mcpVersion);
1854
- const npmCacheDirectory = path.resolve(homeDir, ".haven", "npm-cache");
1855
- const cliPath = path.join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "dist", "cli.js");
1856
- const messages = [];
1857
- await promises.mkdir(runtimeDirectory, { recursive: true, mode: 448 });
1858
- await promises.chmod(runtimeDirectory, 448).catch(() => void 0);
1859
- await promises.mkdir(npmCacheDirectory, { recursive: true, mode: 448 });
1860
- await promises.chmod(npmCacheDirectory, 448).catch(() => void 0);
1861
- if (await installedRuntimeMatches2(runtimeDirectory, cliPath)) {
1862
- messages.push(`Using existing local Haven MCP runtime ${mcpPackageSpec()}.`);
1863
- } else {
1864
- await installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps);
1865
- messages.push(`Installed local Haven MCP runtime ${mcpPackageSpec()}.`);
1866
- }
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,
2562
+ function signerProbeErrorCode(probe) {
2563
+ if (!probe || probe.status === "ok") return void 0;
2564
+ return `local_signer_probe_${probe.status}`;
2565
+ }
2566
+ function hostedMcpErrorCode(hostedConfigured, hostedProbeStatus) {
2567
+ if (!hostedConfigured || hostedProbeStatus === "ok") return void 0;
2568
+ return `hosted_mcp_probe_${hostedProbeStatus}`;
2569
+ }
2570
+ function localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbeStatus) {
2571
+ if (!signerCredentialReady) return "local_signer_credential_unavailable";
2572
+ if (!localMcpConsent?.acknowledged) return "local_mcp_ack_required";
2573
+ if (localMcpProbeStatus && localMcpProbeStatus !== "ok") return `local_mcp_probe_${localMcpProbeStatus}`;
2574
+ return void 0;
2575
+ }
2576
+ function nextAction(runtime, restartMode, errorCode) {
2577
+ if (errorCode) return "return_to_haven_for_wallet_approval_then_finish_runtime_setup";
2578
+ if (restartMode === "hot-reload") return "return_to_haven_for_wallet_approval";
2579
+ if (runtime === "codex-cli" || runtime === "codex-desktop") return "return_to_haven_for_wallet_approval_then_restart_codex";
2580
+ if (runtime === "claude-code") return "return_to_haven_for_wallet_approval_then_restart_claude_code";
2581
+ if (restartMode === "restart-app") return "return_to_haven_for_wallet_approval_then_restart_app";
2582
+ if (restartMode === "restart-session") return "return_to_haven_for_wallet_approval_then_restart_agent_session";
2583
+ return "return_to_haven_for_wallet_approval_then_configure_runtime";
2584
+ }
2585
+ function supportsLocalMcp(runtime) {
2586
+ return runtime === "codex-cli" || runtime === "codex-desktop" || runtime === "claude-code";
2587
+ }
2588
+ async function prepareRuntimeForLocalMcp(input, deps) {
2589
+ const prepare = deps.prepareLocalMcpRuntime ?? ((runtimeInput) => prepareLocalMcpRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress }));
2590
+ return prepare({
2591
+ credentialDirectory: input.credentialDirectory,
1872
2592
  identityPath: input.identityPath,
1873
- signerPath: input.signerPath
1874
- });
1875
- await writeRuntimeSidecar2({
1876
- path: path.join(input.credentialDirectory, "mcp-runtime.json"),
1877
- wrapperPath,
1878
- runtimeDirectory,
1879
- npmCacheDirectory,
1880
- cliPath
2593
+ signerPath: input.signerPath,
2594
+ homeDir: deps.homeDir,
2595
+ serverName: input.serverName
1881
2596
  });
1882
- messages.push(`Prepared stable local Haven MCP wrapper: ${wrapperPath}`);
1883
- return {
1884
- command: wrapperPath,
1885
- args: [],
1886
- wrapperPath,
1887
- runtimeDirectory,
1888
- npmCacheDirectory,
1889
- cliPath,
1890
- messages
1891
- };
1892
2597
  }
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
- }
2598
+ async function prepareSignerForRuntime(input, deps) {
2599
+ const prepare = deps.prepareSignerRuntime ?? ((runtimeInput) => (
2600
+ // onProgress threaded through on purpose (#1586 review): without it the
2601
+ // install heartbeat was dead code in production and the console still
2602
+ // went silent for the whole cold install — the exact symptom the issue
2603
+ // set out to remove, at a longer timeout.
2604
+ prepareSignerRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress })
2605
+ ));
2606
+ return prepare({
2607
+ credentialDirectory: input.credentialDirectory,
2608
+ signerPath: input.signerPath,
2609
+ homeDir: deps.homeDir,
2610
+ serverName: input.serverName
2611
+ });
1897
2612
  }
1898
- async function installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps) {
1899
- const { runCommand, onProgress } = deps;
1900
- const baseArgs = [
1901
- "install",
1902
- "--prefix",
1903
- runtimeDirectory,
1904
- "--no-audit",
1905
- "--no-fund",
1906
- "--omit=dev",
1907
- "--prefer-offline",
1908
- mcpPackageSpec(),
1909
- sdkPackageSpec()
1910
- ];
1911
- const run = async (args) => {
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
- }
1924
- };
2613
+ async function runLocalMcpProbe(runtimeInstall, deps) {
2614
+ const probe = deps.probeLocalMcpTools ?? probeLocalMcpTools;
1925
2615
  try {
1926
- await run(baseArgs);
2616
+ return await probe(runtimeInstall.command, runtimeInstall.args, exports.MCP_RUNTIME_MANIFEST.requiredTools);
1927
2617
  } catch {
1928
- try {
1929
- await run([...baseArgs, "--cache", npmCacheDirectory]);
1930
- } catch (err) {
1931
- throw new Error(`Could not install local Haven MCP runtime ${mcpPackageSpec()}: ${err instanceof Error ? err.message : String(err)}`);
1932
- }
2618
+ return { status: "process_error" };
1933
2619
  }
1934
2620
  }
1935
- async function installedRuntimeMatches2(runtimeDirectory, cliPath) {
1936
- try {
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")),
1940
- readPackageJson2(path.join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
1941
- ]);
1942
- return mcpPackage.version === exports.MCP_RUNTIME_MANIFEST.mcpVersion && sdkPackage.version === exports.MCP_RUNTIME_MANIFEST.sdkVersion;
1943
- } catch {
1944
- return false;
2621
+ function localRuntimePrepareErrorCode(err) {
2622
+ if (err && typeof err === "object" && "code" in err && err.code === "local_mcp_unsupported_node_version") {
2623
+ return "local_mcp_unsupported_node_version";
1945
2624
  }
2625
+ return "local_mcp_runtime_install_failed";
1946
2626
  }
1947
- async function readPackageJson2(path) {
1948
- return JSON.parse(await promises.readFile(path, "utf8"));
1949
- }
1950
- async function writeWrapper2(input) {
1951
- await promises.mkdir(path.dirname(input.wrapperPath), { recursive: true, mode: 448 });
1952
- await promises.chmod(path.dirname(input.wrapperPath), 448).catch(() => void 0);
1953
- const source = [
1954
- "#!/usr/bin/env node",
1955
- "import { spawn } from 'node:child_process'",
2627
+ var execFileAsync3;
2628
+ var init_runtime_install = __esm({
2629
+ "src/runtime-install.ts"() {
2630
+ init_config_writers();
2631
+ init_server_names();
2632
+ init_local_mcp_consent();
2633
+ init_probes();
2634
+ init_local_mcp_runtime();
2635
+ init_signer_runtime();
2636
+ init_runtime_manifest();
2637
+ init_skill_install();
2638
+ init_runtime_registry();
2639
+ init_signer_consent();
2640
+ execFileAsync3 = util.promisify(child_process.execFile);
2641
+ }
2642
+ });
2643
+
2644
+ // src/tombstone.ts
2645
+ var tombstone_exports = {};
2646
+ __export(tombstone_exports, {
2647
+ TOMBSTONE_FILENAME: () => TOMBSTONE_FILENAME,
2648
+ TOMBSTONE_MARKER: () => TOMBSTONE_MARKER,
2649
+ readAgentTombstone: () => readAgentTombstone,
2650
+ writeAgentTombstone: () => writeAgentTombstone
2651
+ });
2652
+ function tombstoneScript(info) {
2653
+ const lines = [
2654
+ `${TOMBSTONE_MARKER}: this Haven agent was retired.`,
1956
2655
  "",
1957
- `const cliPath = ${JSON.stringify(input.cliPath)}`,
1958
- `const identityPath = ${JSON.stringify(input.identityPath)}`,
1959
- `const signerPath = ${JSON.stringify(input.signerPath)}`,
2656
+ ` agent: ${info.agent_id}`,
2657
+ ` retired at: ${info.retired_at}`,
2658
+ ` reason: ${info.reason}`,
2659
+ ...info.replaced_by ? [` replaced by: ${info.replaced_by}`] : [],
1960
2660
  "",
1961
- "const child = spawn(process.execPath, [cliPath, '--identity', identityPath, '--signer', signerPath, ...process.argv.slice(2)], {",
1962
- " stdio: 'inherit',",
1963
- "})",
2661
+ "This process is running with a wiring snapshot that predates the",
2662
+ "retirement \u2014 it loaded its MCP config at startup and has kept it since.",
2663
+ "Restart THIS host to pick up the current wiring. If several long-lived",
2664
+ "hosts are running (a gateway, a TUI worker, an editor), restart EVERY",
2665
+ "one of them: each holds the snapshot from its own start time, so after",
2666
+ "a chain of recreations each can be parked on a DIFFERENT old agent.",
1964
2667
  "",
1965
- "child.on('exit', (code, signal) => {",
1966
- " if (signal) process.kill(process.pid, signal)",
1967
- " else process.exit(code ?? 1)",
1968
- "})",
2668
+ "Then verify with: npx @haven_ai/connect@alpha --doctor --runtime <runtime>"
2669
+ ];
2670
+ return [
2671
+ "#!/usr/bin/env node",
2672
+ `// ${TOMBSTONE_MARKER} \u2014 written by @haven_ai/connect (#1681). Safe to delete`,
2673
+ "// once every long-lived MCP host on this machine has been restarted.",
2674
+ `process.stderr.write(${JSON.stringify(lines.join("\n") + "\n")})`,
2675
+ "process.exit(1)",
1969
2676
  ""
1970
2677
  ].join("\n");
1971
- await promises.writeFile(input.wrapperPath, source, { mode: 448 });
1972
- await promises.chmod(input.wrapperPath, 448).catch(() => void 0);
1973
2678
  }
1974
- async function writeRuntimeSidecar2(input) {
1975
- const value = {
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,
1981
- wrapper_path: input.wrapperPath,
1982
- runtime_directory: input.runtimeDirectory,
1983
- npm_cache_directory: input.npmCacheDirectory,
1984
- cli_path: input.cliPath
2679
+ async function writeAgentTombstone(input) {
2680
+ const dirStat = await promises.stat(input.directory).catch(() => null);
2681
+ if (!dirStat?.isDirectory()) {
2682
+ throw new Error(`Not a directory: ${input.directory} \u2014 nothing to tombstone.`);
2683
+ }
2684
+ const info = {
2685
+ // reason / replaced_by are persisted to disk and re-emitted to the host's
2686
+ // MCP stderr log on EVERY stale probe, potentially for months — redact
2687
+ // like every other output path, at the write layer so any future caller
2688
+ // inherits it. (#1681 review, finding 1)
2689
+ agent_id: input.agentId,
2690
+ retired_at: input.retiredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
2691
+ reason: redactSecrets(input.reason),
2692
+ ...input.replacedBy ? { replaced_by: redactSecrets(input.replacedBy) } : {}
1985
2693
  };
1986
- await promises.writeFile(input.path, `${JSON.stringify(value, null, 2)}
1987
- `, { mode: 384 });
1988
- await promises.chmod(input.path, 384).catch(() => void 0);
1989
- }
1990
- async function assertFileExists2(path, label) {
2694
+ const binDir = path.join(input.directory, "bin");
2695
+ await promises.mkdir(binDir, { recursive: true });
2696
+ const wrapperPath = path.join(binDir, "haven-signer.mjs");
2697
+ await promises.writeFile(wrapperPath, tombstoneScript(info), "utf8");
2698
+ await promises.chmod(wrapperPath, 493);
2699
+ await promises.writeFile(path.join(input.directory, TOMBSTONE_FILENAME), JSON.stringify(info, null, 2) + "\n", "utf8");
2700
+ return info;
2701
+ }
2702
+ async function readAgentTombstone(directory) {
1991
2703
  try {
1992
- await promises.access(path);
2704
+ const parsed = JSON.parse(await promises.readFile(path.join(directory, TOMBSTONE_FILENAME), "utf8"));
2705
+ if (typeof parsed?.agent_id !== "string") return null;
2706
+ return parsed;
1993
2707
  } catch {
1994
- throw new Error(`Missing ${label}: ${path}`);
2708
+ return null;
1995
2709
  }
1996
2710
  }
2711
+ var TOMBSTONE_FILENAME, TOMBSTONE_MARKER;
2712
+ var init_tombstone = __esm({
2713
+ "src/tombstone.ts"() {
2714
+ init_redact();
2715
+ TOMBSTONE_FILENAME = "TOMBSTONE.json";
2716
+ TOMBSTONE_MARKER = "HAVEN-TOMBSTONE";
2717
+ }
2718
+ });
1997
2719
 
1998
- // src/runtime-install.ts
1999
- init_signer_runtime();
2000
- init_runtime_manifest();
2001
- var CODEX_AGENTS_BEGIN_MARKER = "<!-- BEGIN haven-pay (managed by @haven_ai/connect; edits inside this section are overwritten on re-setup) -->";
2002
- var CODEX_AGENTS_END_MARKER = "<!-- END haven-pay -->";
2003
- async function installSkillForRuntime(runtime, deps = {}) {
2004
- switch (runtime) {
2005
- case "claude-code":
2006
- return installSkillFile(
2007
- path.resolve(deps.homeDir ?? os.homedir(), ".claude", "skills", sdk.SKILL_FOLDER_NAME),
2008
- "~/.claude/skills/haven-pay"
2720
+ // src/rekey.ts
2721
+ var rekey_exports = {};
2722
+ __export(rekey_exports, {
2723
+ finishRekey: () => finishRekey,
2724
+ startRekey: () => startRekey
2725
+ });
2726
+ async function startRekey(options, deps = {}) {
2727
+ const now = deps.now ?? (() => Date.now());
2728
+ const stored = await readStoredCredentials(
2729
+ options.serverName,
2730
+ options.agentId,
2731
+ options.credentialsDir
2732
+ );
2733
+ const api = (deps.createApi ?? ((url) => createConnectApiClient(url)))(stored.apiUrl);
2734
+ const identity = await probeIdentity(api, stored.apiKey, "current");
2735
+ assertRekeyable(identity, stored);
2736
+ const key = (deps.generateKey ?? generateDelegateKey)();
2737
+ const startedAt = new Date(now()).toISOString();
2738
+ const expiresAt = new Date(now() + REKEY_PENDING_TTL_MS).toISOString();
2739
+ await writeRekeyPending(stored.directory, {
2740
+ agent_id: stored.agentId,
2741
+ new_delegate_address: key.address,
2742
+ new_delegate_key: key.privateKey,
2743
+ started_at: startedAt,
2744
+ expires_at: expiresAt
2745
+ });
2746
+ const finishCommand = [
2747
+ "npx @haven_ai/connect@alpha --rekey-finish",
2748
+ options.serverName ? `--name ${options.serverName}` : void 0,
2749
+ "--api-key <the key the dashboard showed you>",
2750
+ options.runtime ? `--runtime ${options.runtime}` : "--runtime <your runtime>"
2751
+ ].filter(Boolean).join(" ");
2752
+ return {
2753
+ started: true,
2754
+ agentId: stored.agentId,
2755
+ directory: stored.directory,
2756
+ newDelegateAddress: key.address,
2757
+ expiresAt,
2758
+ messages: [
2759
+ `New signing key generated on this machine for agent ${identity.name || stored.agentId}.`,
2760
+ "",
2761
+ ` New signing address: ${key.address}`,
2762
+ "",
2763
+ "The private half stays here \u2014 Haven never receives it, and there is no way to move it",
2764
+ "between machines. That is what keeps the account non-custodial.",
2765
+ "",
2766
+ 'Next, on the Haven agent page: choose "Replace signing key" and paste that address.',
2767
+ "When it finishes it shows a new API key ONCE. Come back here and run:",
2768
+ "",
2769
+ ` ${finishCommand}`,
2770
+ "",
2771
+ `Nothing has changed yet \u2014 the agent keeps working on its old key until you finish.`,
2772
+ `This pending re-key expires ${expiresAt}.`
2773
+ ]
2774
+ };
2775
+ }
2776
+ async function finishRekey(options, deps = {}) {
2777
+ const now = deps.now ?? (() => Date.now());
2778
+ if (!options.newApiKey) {
2779
+ throw new Error("--rekey-finish needs --api-key <key> \u2014 the one the Haven agent page showed once.");
2780
+ }
2781
+ const stored = await readStoredCredentials(
2782
+ options.serverName,
2783
+ options.agentId,
2784
+ options.credentialsDir
2785
+ );
2786
+ const pending = await readRekeyPending(stored.directory, now());
2787
+ if (pending.agent_id !== stored.agentId) {
2788
+ throw new Error(
2789
+ `The pending re-key at ${stored.directory} belongs to agent ${pending.agent_id}, but that directory now holds ${stored.agentId}. Refusing to write a key into the wrong agent.`
2790
+ );
2791
+ }
2792
+ const api = (deps.createApi ?? ((url) => createConnectApiClient(url)))(stored.apiUrl);
2793
+ const identity = await probeIdentity(api, options.newApiKey, "new");
2794
+ if (identity.id !== stored.agentId) {
2795
+ throw new Error(
2796
+ `That API key belongs to agent ${identity.id}, not ${stored.agentId}. Nothing was changed.`
2797
+ );
2798
+ }
2799
+ const onChain = (identity.delegate_address ?? "").toLowerCase();
2800
+ const expected = pending.new_delegate_address.toLowerCase();
2801
+ if (onChain !== expected) {
2802
+ throw new Error(
2803
+ `Haven says this agent's signing address is ${identity.delegate_address ?? "unset"}, but this machine generated ${pending.new_delegate_address}. Nothing was changed. Either the re-key on the agent page used a different address, or it has not finished yet.`
2804
+ );
2805
+ }
2806
+ await rewriteCredentialFiles({
2807
+ baseDir: options.credentialsDir,
2808
+ agentId: stored.agentId,
2809
+ serverName: options.serverName,
2810
+ apiKey: options.newApiKey,
2811
+ delegateKey: pending.new_delegate_key,
2812
+ delegateAddress: pending.new_delegate_address,
2813
+ safeAddress: stored.safeAddress ?? identity.safe_address ?? void 0,
2814
+ chainId: stored.chainId ?? identity.chain_id ?? void 0,
2815
+ network: stored.network,
2816
+ agentBudget: stored.agentBudget,
2817
+ apiUrl: stored.apiUrl,
2818
+ hostedMcpUrl: stored.hostedMcpUrl,
2819
+ x402BindingSigner: stored.x402BindingSigner,
2820
+ warn: deps.log
2821
+ });
2822
+ await clearRekeyPending(stored.directory);
2823
+ const names = serverNamesFor(options.serverName);
2824
+ const messages = [
2825
+ `Agent ${identity.name || stored.agentId} is now on its new signing key.`,
2826
+ ` Signing address: ${pending.new_delegate_address}`,
2827
+ ` Credentials: ${stored.directory} (rewritten in place)`,
2828
+ ` MCP servers: ${names.hosted} / ${names.signer} (unchanged names)`
2829
+ ];
2830
+ let configRewritten = false;
2831
+ if (options.runtime) {
2832
+ const prepared = await (deps.prepareSigner ?? prepareSignerRuntime)(
2833
+ {
2834
+ credentialDirectory: stored.directory,
2835
+ signerPath: `${stored.directory}/signer.json`,
2836
+ homeDir: options.homeDir
2837
+ },
2838
+ { runCommand: deps.runCommand }
2839
+ );
2840
+ const result = await (deps.writeConfig ?? writeHostedRuntimeConfig)(
2841
+ { runCommand: deps.runCommand, homeDir: options.homeDir },
2842
+ {
2843
+ runtime: options.runtime,
2844
+ hostedMcpUrl: stored.hostedMcpUrl,
2845
+ apiKey: options.newApiKey,
2846
+ identityPath: `${stored.directory}/identity.json`,
2847
+ signerPath: `${stored.directory}/signer.json`,
2848
+ credentialDirectory: stored.directory,
2849
+ serverName: options.serverName
2850
+ },
2851
+ { command: prepared.command, args: prepared.args }
2852
+ );
2853
+ configRewritten = result.hostedConfigured;
2854
+ messages.push(` Config: ${result.target}`);
2855
+ messages.push(...result.messages.map((line) => ` ${line}`));
2856
+ if (!configRewritten) {
2857
+ messages.push(
2858
+ "",
2859
+ "WARNING: the MCP config was NOT updated, so it still carries the OLD API key and every",
2860
+ `wired host will fail with 401. Fix the cause above and re-run with --runtime ${options.runtime},`,
2861
+ "or update the config by hand. The credential files on disk are already on the new key."
2009
2862
  );
2010
- case "hermes":
2011
- return installSkillFile(
2012
- path.join(hermesHome(deps), "skills", sdk.SKILL_FOLDER_NAME),
2013
- "the Hermes skills folder"
2863
+ }
2864
+ } else {
2865
+ messages.push(
2866
+ "",
2867
+ "NOTE: no --runtime was given, so the MCP config still carries the OLD API key and every",
2868
+ "wired host will fail with 401. Re-run with --runtime <name> to rewrite it."
2869
+ );
2870
+ }
2871
+ return {
2872
+ finished: true,
2873
+ agentId: stored.agentId,
2874
+ directory: stored.directory,
2875
+ newDelegateAddress: pending.new_delegate_address,
2876
+ serverNames: { hosted: names.hosted, signer: names.signer },
2877
+ configRewritten,
2878
+ messages
2879
+ };
2880
+ }
2881
+ async function probeIdentity(api, apiKey, which) {
2882
+ try {
2883
+ return await api.getAgentIdentity(apiKey);
2884
+ } catch (err) {
2885
+ const status = err?.status;
2886
+ if (status === 401 || status === 403) {
2887
+ throw new Error(
2888
+ which === "current" ? "This machine's Haven API key is no longer accepted. If a re-key already finished elsewhere, run --rekey-finish with the key that re-key produced instead of starting a new one." : "Haven rejected that API key. Check you pasted the whole key from the agent page \u2014 nothing was changed."
2014
2889
  );
2015
- case "codex-cli":
2016
- case "codex-desktop":
2017
- return installCodexAgentsSection(deps);
2018
- default:
2019
- return void 0;
2890
+ }
2891
+ throw new Error(
2892
+ `Could not reach Haven to check this agent (${redactSecrets(
2893
+ err instanceof Error ? err.message : String(err)
2894
+ )}). Nothing was changed.`
2895
+ );
2020
2896
  }
2021
2897
  }
2022
- async function installSkillFile(skillDir, label) {
2898
+ function assertRekeyable(identity, stored) {
2899
+ if (identity.id !== stored.agentId) {
2900
+ throw new Error(
2901
+ `The credentials at ${stored.directory} say agent ${stored.agentId}, but Haven says that key belongs to ${identity.id}. Refusing to re-key an agent this directory does not own.`
2902
+ );
2903
+ }
2904
+ if (identity.execution_rail === "legacy") {
2905
+ throw new Error(
2906
+ `Agent ${identity.name || identity.id} is on the legacy rail, which cannot be re-keyed \u2014 its authority is per-token Safe allowances, not a signed delegation there is anything to re-issue. Re-onboard the agent on the delegation rail instead.`
2907
+ );
2908
+ }
2909
+ if (identity.status === "revoked") {
2910
+ throw new Error(
2911
+ `Agent ${identity.name || identity.id} is revoked. Re-keying would hand a revoked agent fresh credentials \u2014 create a new agent instead.`
2912
+ );
2913
+ }
2914
+ }
2915
+ var init_rekey = __esm({
2916
+ "src/rekey.ts"() {
2917
+ init_api();
2918
+ init_runtime_install();
2919
+ init_signer_runtime();
2920
+ init_key();
2921
+ init_redact();
2922
+ init_server_names();
2923
+ init_storage();
2924
+ }
2925
+ });
2926
+
2927
+ // src/rekey-restart.ts
2928
+ var rekey_restart_exports = {};
2929
+ __export(rekey_restart_exports, {
2930
+ restartGuidance: () => restartGuidance
2931
+ });
2932
+ function restartGuidance(runtime) {
2933
+ const profile = runtime ? BY_RUNTIME[runtime] : void 0;
2934
+ const lines = ["The new key is only live in a process that started after now.", ""];
2935
+ if (profile) {
2936
+ lines.push(profile.how);
2937
+ for (const command of profile.commands) lines.push(` ${command}`);
2938
+ if (profile.commands.length > 0) lines.push("");
2939
+ } else {
2940
+ lines.push(
2941
+ runtime ? `No standard restart command is known for "${runtime}" \u2014 restart it the way you start it.` : "Pass --runtime <name> to get the restart command for a specific host."
2942
+ );
2943
+ lines.push("");
2944
+ }
2945
+ lines.push(SWEEP);
2946
+ return { commands: profile?.commands ?? [], lines };
2947
+ }
2948
+ var SWEEP, BY_RUNTIME;
2949
+ var init_rekey_restart = __esm({
2950
+ "src/rekey-restart.ts"() {
2951
+ SWEEP = "Then restart EVERY other long-lived MCP host on this machine \u2014 gateways, TUI workers, editors. Each holds the wiring snapshot from its own start time, so after a re-key each one is still presenting the OLD API key and will fail with 401 until it restarts.";
2952
+ BY_RUNTIME = {
2953
+ "claude-code": {
2954
+ commands: ["claude --continue"],
2955
+ how: "Exit this Claude Code session and start a new one:"
2956
+ },
2957
+ "codex-cli": {
2958
+ commands: ["codex resume --last"],
2959
+ how: "Start a fresh Codex CLI session:"
2960
+ },
2961
+ "codex-desktop": {
2962
+ commands: [],
2963
+ how: "Quit Codex Desktop completely (not just the window) and reopen it."
2964
+ },
2965
+ "claude-desktop": {
2966
+ commands: [],
2967
+ how: "Quit Claude Desktop completely (not just the window) and reopen it."
2968
+ },
2969
+ hermes: {
2970
+ commands: ["systemctl --user restart hermes-gateway", "/restart"],
2971
+ how: "Restart the Hermes gateway, or run /restart inside a Hermes session. The first form is the one that matters if you run it as a user service:"
2972
+ },
2973
+ cursor: {
2974
+ commands: [],
2975
+ how: 'Cursor hot-reloads MCP config, so it will pick the new key up on its own. If a tool call still fails with 401, reload the window (Cmd/Ctrl+Shift+P \u2192 "Reload Window").'
2976
+ },
2977
+ vscode: {
2978
+ commands: [],
2979
+ how: 'VS Code hot-reloads MCP config. If a tool call still fails with 401, reload the window (Cmd/Ctrl+Shift+P \u2192 "Reload Window").'
2980
+ },
2981
+ "vscode-insiders": {
2982
+ commands: [],
2983
+ how: 'VS Code Insiders hot-reloads MCP config. If a tool call still fails with 401, reload the window (Cmd/Ctrl+Shift+P \u2192 "Reload Window").'
2984
+ }
2985
+ };
2986
+ }
2987
+ });
2988
+
2989
+ // src/doctor.ts
2990
+ var doctor_exports = {};
2991
+ __export(doctor_exports, {
2992
+ runDoctor: () => runDoctor,
2993
+ runRepair: () => runRepair
2994
+ });
2995
+ async function discoverCredentialDirectory(homeDir, explicit) {
2996
+ const root = explicit ? path.dirname(explicit) : path.join(homeDir, ".haven", "agents");
2997
+ let entries = [];
2023
2998
  try {
2024
- await promises.mkdir(skillDir, { recursive: true });
2025
- const target = path.join(skillDir, "SKILL.md");
2026
- await promises.writeFile(target, sdk.HAVEN_SKILL_MD, "utf8");
2027
- return {
2028
- installed: true,
2029
- target,
2030
- messages: [`Installed the generic Haven payment skill (${label}). It contains no secrets.`]
2031
- };
2032
- } catch (err) {
2999
+ entries = await promises.readdir(root);
3000
+ } catch {
3001
+ return explicit ? { directory: explicit, others: [], parkedOnly: /* @__PURE__ */ new Set() } : { others: [], parkedOnly: /* @__PURE__ */ new Set() };
3002
+ }
3003
+ const candidates = [];
3004
+ const tombstonedOnly = [];
3005
+ const parkedOnly = [];
3006
+ for (const entry of entries) {
3007
+ const directory = path.join(root, entry);
3008
+ try {
3009
+ const s = await promises.stat(path.join(directory, "identity.json"));
3010
+ candidates.push({ directory, mtimeMs: s.mtimeMs });
3011
+ } catch {
3012
+ try {
3013
+ await promises.stat(path.join(directory, TOMBSTONE_FILENAME));
3014
+ tombstonedOnly.push(directory);
3015
+ } catch {
3016
+ try {
3017
+ await promises.stat(path.join(directory, REKEY_PENDING_FILENAME));
3018
+ parkedOnly.push(directory);
3019
+ } catch {
3020
+ }
3021
+ }
3022
+ }
3023
+ }
3024
+ const parkedOnlySet = new Set(parkedOnly);
3025
+ candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
3026
+ if (explicit) {
2033
3027
  return {
2034
- installed: false,
2035
- messages: [
2036
- `Could not install the Haven payment skill: ${err instanceof Error ? err.message : String(err)}. Download it from the Haven dashboard instead.`
2037
- ]
3028
+ directory: explicit,
3029
+ others: [...candidates.map((c) => c.directory), ...tombstonedOnly, ...parkedOnly].filter((d) => d !== explicit),
3030
+ parkedOnly: parkedOnlySet
2038
3031
  };
2039
3032
  }
3033
+ if (candidates.length === 0 && tombstonedOnly.length === 0 && parkedOnly.length === 0) {
3034
+ return { others: [], parkedOnly: parkedOnlySet };
3035
+ }
3036
+ return {
3037
+ directory: candidates[0]?.directory,
3038
+ others: [...candidates.slice(1).map((c) => c.directory), ...tombstonedOnly, ...parkedOnly],
3039
+ parkedOnly: parkedOnlySet
3040
+ };
2040
3041
  }
2041
- async function installCodexAgentsSection(deps) {
2042
- try {
2043
- const codexDir = path.resolve(deps.homeDir ?? os.homedir(), ".codex");
2044
- const target = path.join(codexDir, "AGENTS.md");
2045
- await promises.mkdir(codexDir, { recursive: true });
2046
- const existing = await promises.readFile(target, "utf8").catch(() => null);
2047
- const next = upsertManagedSection(existing, codexManagedSection());
2048
- if (next !== existing) {
2049
- await promises.writeFile(target, next, "utf8");
3042
+ function agentIsWired(configText, names, slug, identity, sidecar, isPrimary, bareOwnerExists) {
3043
+ if (configText === null) return isPrimary;
3044
+ if (slug) {
3045
+ for (const name of [names.hosted, names.codexHosted, names.signer, names.codexSigner]) {
3046
+ if (new RegExp(`(^|[."'\\s\\[])${name}(["'\\]:\\s]|$)`, "m").test(configText)) return true;
2050
3047
  }
3048
+ return false;
3049
+ }
3050
+ if (sidecar?.wrapper_path && configText.includes(sidecar.wrapper_path)) return true;
3051
+ if (bareOwnerExists) return false;
3052
+ return isPrimary && Boolean(identity?.hosted_mcp_url && configText.includes(identity.hosted_mcp_url));
3053
+ }
3054
+ function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
3055
+ const label = "Pending re-key";
3056
+ const nameFlag = slug ? ` --name ${slug}` : "";
3057
+ if (status.state === "unreadable") {
2051
3058
  return {
2052
- installed: true,
2053
- target,
2054
- messages: [
2055
- "Installed the generic Haven payment guidance as a managed section in ~/.codex/AGENTS.md (Codex reads it as global instructions). It contains no secrets; your own content in that file is untouched."
2056
- ]
3059
+ id: "rekey_pending",
3060
+ label,
3061
+ ok: false,
3062
+ detail: `A re-key was started here but ${status.path} does not parse, so neither the address it generated nor when it started can be read. The file still holds what was a private key.`,
3063
+ repair: `Delete ${status.path}, then start again: ${RERUN} --rekey${nameFlag}`
2057
3064
  };
2058
- } catch (err) {
3065
+ }
3066
+ const started = status.startedAt ?? "an unknown time";
3067
+ const address = status.newDelegateAddress ?? "unknown";
3068
+ const completedOnHaven = hostedDelegateAddress !== void 0 && status.newDelegateAddress !== void 0 && hostedDelegateAddress.toLowerCase() === status.newDelegateAddress.toLowerCase();
3069
+ if (completedOnHaven) {
2059
3070
  return {
2060
- installed: false,
2061
- messages: [
2062
- `Could not install the Haven payment guidance into ~/.codex/AGENTS.md: ${err instanceof Error ? err.message : String(err)}. Download the skill from the Haven dashboard instead.`
2063
- ]
3071
+ id: "rekey_pending",
3072
+ label,
3073
+ ok: false,
3074
+ detail: `A re-key started ${started} has COMPLETED on Haven \u2014 the agent's signing address is already ${address}, the one this machine generated \u2014 but the local half was never finished, so the credential files here still hold the old key. Parked at ${status.path}.` + (status.state === "expired" ? " The local file is also past its 24h TTL, which --rekey-finish refuses, so the finish command below will not accept it any more." : ""),
3075
+ repair: status.state === "expired" ? `The parked key expired. Start again \u2014 ${RERUN} --rekey${nameFlag} \u2014 and re-run "Replace signing key" on the Haven agent page with the new address it prints.` : `Run: ${RERUN} --rekey-finish${nameFlag} --api-key <the key the agent page showed you> --runtime ${runtime}`
2064
3076
  };
2065
3077
  }
2066
- }
2067
- function codexManagedSection() {
2068
- return `${CODEX_AGENTS_BEGIN_MARKER}
2069
-
2070
- ${sdk.HAVEN_SKILL_BODY_MD.trimEnd()}
2071
-
2072
- ${CODEX_AGENTS_END_MARKER}
2073
- `;
2074
- }
2075
- function upsertManagedSection(existing, section) {
2076
- if (existing === null || existing.trim() === "") return section;
2077
- const begins = markerLineIndexes(existing, CODEX_AGENTS_BEGIN_MARKER);
2078
- const ends = markerLineIndexes(existing, CODEX_AGENTS_END_MARKER);
2079
- if (begins.length === 1 && ends.length === 1 && ends[0] > begins[0]) {
2080
- const afterEnd = ends[0] + CODEX_AGENTS_END_MARKER.length;
2081
- const tail = existing.startsWith("\r\n", afterEnd) ? existing.slice(afterEnd + 2) : existing.startsWith("\n", afterEnd) ? existing.slice(afterEnd + 1) : existing.slice(afterEnd);
2082
- return existing.slice(0, begins[0]) + section + tail;
2083
- }
2084
- if (begins.length > 0 || ends.length > 0) {
2085
- throw new Error(
2086
- "found a damaged Haven marker section (orphaned or duplicated markers); remove the leftover marker lines and re-run setup"
2087
- );
3078
+ const wedgeNote = "Haven is NOT yet on this address, so the re-key did not complete. This machine cannot tell whether the on-chain revoke on the agent page already ran: if it did not, closing this costs nothing; if it did, the agent's old delegations are revoked, no new ones were issued, and only an owner re-grant restores its spend authority (#1868). Check the agent page before assuming the harmless case.";
3079
+ if (status.state === "expired") {
3080
+ return {
3081
+ id: "rekey_pending",
3082
+ label,
3083
+ ok: false,
3084
+ detail: `A re-key started ${started} EXPIRED ${status.expiresAt ?? ""} without being finished. Its address was ${address}; the private half it generated is still on disk at ${status.path}. ` + wedgeNote,
3085
+ repair: `Either delete ${status.path} to drop the parked key, or start over: ${RERUN} --rekey${nameFlag}. Connect never deletes it for you \u2014 an expired TTL is a refusal to USE the key, not a licence to destroy key material you may still be mid-flow on.`
3086
+ };
2088
3087
  }
2089
- return `${existing.replace(/\n*$/, "\n\n")}${section}`;
3088
+ return {
3089
+ id: "rekey_pending",
3090
+ label,
3091
+ ok: true,
3092
+ detail: `A re-key started ${started} is still open (expires ${status.expiresAt ?? "unknown"}). Paste this address into "Replace signing key" on the Haven agent page: ${address}. Parked at ${status.path}. ` + wedgeNote
3093
+ };
2090
3094
  }
2091
- function markerLineIndexes(text, marker) {
2092
- const indexes = [];
2093
- for (let from = 0; ; ) {
2094
- const at = text.indexOf(marker, from);
2095
- if (at === -1) return indexes;
2096
- if (at === 0 || text[at - 1] === "\n") indexes.push(at);
2097
- from = at + marker.length;
3095
+ async function readIdentity(directory) {
3096
+ try {
3097
+ return JSON.parse(await promises.readFile(path.join(directory, "identity.json"), "utf8"));
3098
+ } catch {
3099
+ return void 0;
2098
3100
  }
2099
3101
  }
2100
- function hermesHome(deps) {
2101
- const env = deps.env ?? process.env;
2102
- return env.HERMES_HOME ?? path.join(deps.homeDir ?? os.homedir(), ".hermes");
2103
- }
2104
-
2105
- // src/runtime-install.ts
2106
- init_runtime_registry();
2107
- init_signer_consent();
2108
- var execFileAsync3 = util.promisify(child_process.execFile);
2109
- async function installRuntime(input, deps = {}) {
2110
- const runtime = normalizeRuntime(input.runtime, deps.env);
2111
- const profile = runtimeProfile(runtime, deps.env);
2112
- const progress = deps.onProgress ?? (() => void 0);
2113
- const localRuntime = input.localMcp === true && supportsLocalMcp(runtime);
2114
- const consentMessages = [];
2115
- const localMcpConsent = localRuntime ? await resolveLocalMcpConsent(input, consentMessages) : void 0;
2116
- const signerConsent = localRuntime ? void 0 : await resolveSignerConsent(input, consentMessages);
2117
- if (runtime === "other") {
2118
- const signerCredentialReady2 = await probeLocalSignerCredential(input.signerPath);
2119
- const signerReady = signerCredentialReady2 && signerConsent?.acknowledged;
2120
- return {
2121
- runtime,
2122
- runtimeMcpMode: "manual",
2123
- hostedMcpConfigured: false,
2124
- localSignerConfigured: false,
2125
- localMcpConfigured: false,
2126
- probeResult: signerReady ? "manual_runtime_setup_required_local_signer_ready" : "manual_runtime_setup_required_local_signer_unavailable",
2127
- restartRequired: true,
2128
- nextUserAction: "return_to_haven_for_wallet_approval_then_configure_runtime",
2129
- errorCode: "manual_runtime_setup_required",
2130
- configTarget: "manual runtime setup",
2131
- signerAcknowledged: signerConsent?.acknowledged,
2132
- localMcpAcknowledged: false,
2133
- messages: [
2134
- ...consentMessages,
2135
- "Custom runtime: Haven did not auto-configure it. Your credentials are on disk (chmod 600) \u2014 read them at runtime; never paste a key into the agent prompt, memory, or logs.",
2136
- ` identity (hosted MCP Bearer): ${input.identityPath}`,
2137
- ` signer (local signing key): ${input.signerPath}`,
2138
- "After wallet approval, wire the runtime to Haven by reference:",
2139
- ` Hosted MCP + local signer: point your MCP client at ${input.hostedMcpUrl} with the api_key from identity.json, then run npx -y ${signerPackageSpec()} --credentials ${input.signerPath}`,
2140
- ` Fully local MCP (no hosted dependency): npx -y ${mcpPackageSpec()} --identity ${input.identityPath} --signer ${input.signerPath}`
2141
- ]
2142
- };
3102
+ async function checksForAgent(entry, input, deps) {
3103
+ const { directory, identity, sidecar } = entry;
3104
+ const checks = [];
3105
+ let signerCapabilities;
3106
+ let signerFile;
3107
+ try {
3108
+ const parsed = JSON.parse(await promises.readFile(path.join(directory, "signer.json"), "utf8"));
3109
+ signerFile = typeof parsed === "object" && parsed !== null ? parsed : void 0;
3110
+ } catch {
3111
+ signerFile = void 0;
2143
3112
  }
2144
- let localRuntimeInstall;
2145
- let localRuntimeError;
2146
- if (localRuntime) {
2147
- try {
2148
- localRuntimeInstall = await prepareRuntimeForLocalMcp(input, deps);
2149
- } catch (err) {
2150
- localRuntimeError = err;
3113
+ const credentialsOk = Boolean(identity?.api_key) && signerFile !== void 0;
3114
+ checks.push({
3115
+ id: "credentials",
3116
+ label: "Agent credentials",
3117
+ ok: credentialsOk,
3118
+ detail: credentialsOk ? `identity.json and signer.json parse (agent ${identity?.agent_id ?? "unknown"})` : "identity.json or signer.json is missing or unparseable.",
3119
+ ...credentialsOk ? {} : { repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` }
3120
+ });
3121
+ if (!sidecar) {
3122
+ checks.push({
3123
+ id: "signer_runtime",
3124
+ label: "Signer runtime (preinstalled wrapper)",
3125
+ ok: false,
3126
+ detail: "No signer-runtime.json sidecar \u2014 the pinned signer runtime was never prepared (or a pre-#1586 npx config).",
3127
+ repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
3128
+ });
3129
+ } else {
3130
+ const matches = await installedRuntimeMatches(sidecar.runtime_directory, sidecar.cli_path);
3131
+ const versionOk = sidecar.signer_version === exports.MCP_RUNTIME_MANIFEST.signerVersion;
3132
+ const ok = matches && versionOk;
3133
+ checks.push({
3134
+ id: "signer_runtime",
3135
+ label: "Signer runtime (preinstalled wrapper)",
3136
+ ok,
3137
+ 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.`,
3138
+ ...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
3139
+ });
3140
+ }
3141
+ const hostedUrl = identity?.hosted_mcp_url ?? (identity?.api_url ? `${identity.api_url}/mcp` : void 0);
3142
+ if (identity?.api_key && hostedUrl) {
3143
+ const probe = await (deps.probeHosted ?? probeHostedMcpTools)(identity.api_key, hostedUrl, deps.fetch);
3144
+ checks.push({
3145
+ id: "hosted_mcp",
3146
+ label: "Hosted Haven MCP",
3147
+ ok: probe.status === "ok",
3148
+ detail: probe.status === "ok" ? `Reachable and authorized (${hostedUrl}).` : `Probe failed: ${probe.status} (${hostedUrl}).`,
3149
+ ...probe.status === "ok" ? {} : {
3150
+ 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."
3151
+ }
3152
+ });
3153
+ } else {
3154
+ checks.push({
3155
+ id: "hosted_mcp",
3156
+ label: "Hosted Haven MCP",
3157
+ ok: false,
3158
+ detail: "No stored API key / hosted MCP URL to probe with.",
3159
+ repair: `Re-run the full setup: ${RERUN} --setup <token>.`
3160
+ });
3161
+ }
3162
+ const localDelegate = typeof signerFile?.delegate_address === "string" ? signerFile.delegate_address : void 0;
3163
+ let hostedDelegateAddress;
3164
+ if (identity?.api_key && identity.api_url) {
3165
+ const probe = await (deps.probeHostedIdentity ?? probeHostedAgentIdentity)(
3166
+ identity.api_key,
3167
+ identity.api_url,
3168
+ deps.fetch
3169
+ );
3170
+ if (probe.status === "ok") hostedDelegateAddress = probe.delegateAddress;
3171
+ if (probe.status !== "ok") {
3172
+ checks.push({
3173
+ id: "identity_match",
3174
+ label: "Hosted identity matches the local signing key",
3175
+ ok: false,
3176
+ detail: probe.status === "unauthorized" ? "The stored API key was rejected, so the agent it authenticates as cannot be compared with the local signing key." : `Could not read the hosted identity (${probe.status}) \u2014 the comparison did not happen, so it cannot be reported as a match.`,
3177
+ repair: probe.status === "unauthorized" ? `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` : `Restore network access to the Haven API, then re-run: ${RERUN} --doctor --runtime ${input.runtime}`
3178
+ });
3179
+ } else if (!localDelegate) {
3180
+ checks.push({
3181
+ id: "identity_match",
3182
+ label: "Hosted identity matches the local signing key",
3183
+ ok: false,
3184
+ detail: "signer.json holds no delegate_address to compare against the hosted identity.",
3185
+ repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.`
3186
+ });
3187
+ } else {
3188
+ const same = probe.delegateAddress?.toLowerCase() === localDelegate.toLowerCase();
3189
+ checks.push({
3190
+ id: "identity_match",
3191
+ label: "Hosted identity matches the local signing key",
3192
+ ok: same,
3193
+ detail: same ? `The stored API key authenticates as the agent whose signing key is in this directory (${shortAddress(localDelegate)}).` : `MISMATCH: the stored API key authenticates as agent ${probe.agentId ?? "unknown"} with delegate ${shortAddress(probe.delegateAddress ?? "unknown")}, but signer.json here holds ${shortAddress(localDelegate)}. This runtime would quote as one agent and sign as another.`,
3194
+ ...same ? {} : {
3195
+ repair: `Re-run setup for this agent so its API key and signing key come from one run: ${RERUN} --setup <token>. Do not hand-edit either file.`
3196
+ }
3197
+ });
2151
3198
  }
2152
3199
  }
2153
- if (localRuntimeError) {
2154
- const errorCode2 = localRuntimePrepareErrorCode(localRuntimeError);
2155
- return {
2156
- runtime,
2157
- runtimeMcpMode: "local_stdio",
2158
- hostedMcpConfigured: false,
2159
- localSignerConfigured: false,
2160
- localMcpConfigured: false,
2161
- probeResult: errorCode2 === "local_mcp_unsupported_node_version" ? "local_stdio_mcp_unsupported_node_version" : "local_stdio_mcp_runtime_install_failed",
2162
- restartRequired: true,
2163
- nextUserAction: nextAction(runtime, profile.restartMode, errorCode2),
2164
- errorCode: errorCode2,
2165
- configTarget: profile.label,
2166
- signerAcknowledged: signerConsent?.acknowledged,
2167
- localMcpAcknowledged: localMcpConsent?.acknowledged,
2168
- activationCommand: void 0,
2169
- messages: [
2170
- ...consentMessages,
2171
- `Could not prepare local Haven MCP runtime: ${localRuntimeError instanceof Error ? localRuntimeError.message : String(localRuntimeError)}`
2172
- ]
2173
- };
3200
+ const pending = await inspectRekeyPending(directory, deps.now?.() ?? Date.now());
3201
+ if (pending) {
3202
+ checks.push(rekeyPendingCheck(pending, hostedDelegateAddress, input.runtime, sidecar?.server_name));
2174
3203
  }
2175
- let signerCommand;
2176
- if (!localRuntime) {
2177
- progress("Getting the signer ready\u2026");
2178
- try {
2179
- const signerRuntime = await prepareSignerForRuntime(input, deps);
2180
- signerCommand = { command: signerRuntime.command, args: signerRuntime.args };
2181
- consentMessages.push(...signerRuntime.messages);
2182
- } catch (err) {
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
- };
3204
+ if (sidecar) {
3205
+ const consent = await getLocalSignerConsentStatus(path.join(directory, "signer.json"));
3206
+ if (!consent.acknowledged) {
3207
+ checks.push({
3208
+ id: "signer_process",
3209
+ label: "Signer stdio handshake",
3210
+ ok: false,
3211
+ detail: "The local-tools consent is not acknowledged, so the signer refuses to start (by design).",
3212
+ repair: `Run: ${RERUN} --ack-local-tools --setup <token> (or re-run your original setup command with --ack-local-tools).`
3213
+ });
3214
+ } else {
3215
+ const probe = await (deps.probeSignerTools ?? probeLocalMcpTools)(
3216
+ sidecar.wrapper_path,
3217
+ [],
3218
+ exports.MCP_RUNTIME_MANIFEST.requiredSignerTools
3219
+ );
3220
+ const experimental = probe.capabilities?.experimental ?? probe.capabilities;
3221
+ const compat = experimental?.["haven/signer-compatibility"];
3222
+ signerCapabilities = compat ? { "haven/signer-compatibility": compat } : void 0;
3223
+ const compatDetail = compat ? ` Compat: x402 expected-context v${JSON.stringify(compat.x402_expected_context_versions ?? "?")}.` : "";
3224
+ checks.push({
3225
+ id: "signer_process",
3226
+ label: "Signer stdio handshake",
3227
+ ok: probe.status === "ok",
3228
+ detail: probe.status === "ok" ? `Signer started, listed ${probe.toolNames?.length ?? 0} tools${probe.serverInfo?.version ? ` (v${probe.serverInfo.version})` : ""}.${compatDetail}` : `Handshake failed: ${probe.status}.`,
3229
+ ...probe.status === "ok" ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
3230
+ });
2205
3231
  }
3232
+ } else {
3233
+ checks.push({
3234
+ id: "signer_process",
3235
+ label: "Signer stdio handshake",
3236
+ ok: false,
3237
+ detail: "Skipped \u2014 no prepared signer runtime to probe.",
3238
+ repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
3239
+ });
2206
3240
  }
2207
- const signerRuntimePrepared = localRuntime ? void 0 : signerCommand !== void 0;
2208
- progress("Setting up your Haven tools\u2026");
2209
- const configResult = runtime === "claude-code" ? localRuntime ? await configureClaudeCode(deps, localRuntimeInstall?.command ?? "") : await configureClaudeCodeHosted(deps, input, signerCommand) : await writeRuntimeConfig({
2210
- runtime,
2211
- hostedMcpUrl: input.hostedMcpUrl,
2212
- apiKey: input.apiKey,
2213
- identityPath: input.identityPath,
2214
- signerPath: input.signerPath,
2215
- credentialDirectory: input.credentialDirectory,
2216
- localMcpCommand: localRuntimeInstall?.command,
2217
- signerCommand,
2218
- homeDir: deps.homeDir,
2219
- mode: localRuntime ? "local" : "hosted"
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);
3241
+ return { checks, ...signerCapabilities ? { signerCapabilities } : {} };
3242
+ }
3243
+ async function runDoctor(input, deps = {}) {
3244
+ const homeDir = deps.homeDir ?? os.homedir();
3245
+ const checks = [];
3246
+ let signerCapabilities;
3247
+ const { directory, others, parkedOnly } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
3248
+ const configPath = runtimeConfigPathFor(input.runtime, homeDir);
3249
+ let configText = null;
3250
+ if (configPath !== null) {
2225
3251
  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
- });
3252
+ configText = await promises.readFile(configPath, "utf8");
2238
3253
  } catch {
3254
+ configText = null;
2239
3255
  }
2240
3256
  }
2241
- progress("Almost there \u2014 just confirming everything connects\u2026");
2242
- const localProbePromise = configResult.runtimeMcpMode === "local_stdio" && localRuntimeInstall ? runLocalMcpProbe(localRuntimeInstall, deps) : Promise.resolve(void 0);
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([
2249
- configResult.hostedConfigured ? probeHostedMcpTools(input.apiKey, input.hostedMcpUrl, deps.fetch) : Promise.resolve({ status: "bad_response" }),
2250
- probeLocalSignerCredential(input.signerPath),
2251
- localProbePromise,
2252
- signerProbePromise
2253
- ]);
2254
- const hostedOk = configResult.hostedConfigured && hostedProbe.status === "ok";
2255
- const localMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialReady && Boolean(localMcpConsent?.acknowledged) && localMcpProbe?.status === "ok";
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");
2259
- const restartRequired = configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env);
2260
- const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : hostedMcpErrorCode(configResult.hostedConfigured, hostedProbe.status) ?? signerConsentErrorCode(signerCredentialReady, signerConsent) ?? signerProbeErrorCode(signerProbe));
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
- ] : [];
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."] : [];
2267
- const skillInstall = !configResult.errorCode ? await installSkillForRuntime(runtime, { homeDir: deps.homeDir, env: deps.env }) : void 0;
2268
- return {
2269
- runtime,
2270
- runtimeMcpMode: configResult.runtimeMcpMode,
2271
- hostedMcpConfigured: hostedOk,
2272
- localSignerConfigured: signerOk,
2273
- localMcpConfigured: localMcpOk,
2274
- probeResult: buildProbeResult(configResult.runtimeMcpMode, configResult.hostedConfigured, hostedProbe.status, signerOk, localMcpOk, localMcpProbe?.status),
2275
- restartRequired,
2276
- nextUserAction: nextAction(runtime, profile.restartMode, errorCode),
2277
- errorCode,
2278
- configTarget: configResult.target,
2279
- signerAcknowledged: signerConsent?.acknowledged,
2280
- localMcpAcknowledged: localMcpConsent?.acknowledged,
2281
- activationCommand: configResult.activationCommand,
2282
- skillInstalled: skillInstall?.installed,
2283
- signerRuntimePrepared,
2284
- messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...hostedProbeMessages, ...signerProbeMessages, ...localProbeMessages, ...skillInstall?.messages ?? []]
2285
- };
2286
- }
2287
- function runtimeInstallCapabilities(runtime, env = process.env) {
2288
- const profile = runtimeProfile(runtime, env);
3257
+ const allDirectories = directory ? [directory, ...others] : others;
3258
+ let bareOwnerExists = false;
3259
+ for (const dir of allDirectories) {
3260
+ const sidecar = await readRuntimeSidecar(dir);
3261
+ if (!sidecar?.server_name && sidecar?.wrapper_path && configText?.includes(sidecar.wrapper_path)) {
3262
+ bareOwnerExists = true;
3263
+ break;
3264
+ }
3265
+ }
3266
+ const inventory = [];
3267
+ const capabilitiesByDirectory = /* @__PURE__ */ new Map();
3268
+ const primaryChecksById = /* @__PURE__ */ new Map();
3269
+ for (const dir of allDirectories) {
3270
+ const identity = await readIdentity(dir);
3271
+ const sidecar = await readRuntimeSidecar(dir);
3272
+ const tombstone = await readAgentTombstone(dir);
3273
+ const slug = sidecar?.server_name;
3274
+ const names = serverNamesFor(slug);
3275
+ const rekeyPending = await inspectRekeyPending(dir, deps.now?.() ?? Date.now());
3276
+ if (!identity?.api_key) {
3277
+ const agentId = tombstone?.agent_id ?? rekeyPending?.agentId;
3278
+ inventory.push({
3279
+ ...slug ? { slug } : {},
3280
+ ...agentId ? { agentId } : {},
3281
+ directory: dir,
3282
+ // A tombstone is a deliberate record and outranks the discovery tell:
3283
+ // a retired directory that also holds a parked key stays `retired`.
3284
+ classification: tombstone ? "retired" : parkedOnly.has(dir) ? "parked" : "orphaned",
3285
+ checks: rekeyPending ? [rekeyPendingCheck(rekeyPending, void 0, input.runtime, slug)] : [],
3286
+ ...rekeyPending ? { rekeyPending } : {}
3287
+ });
3288
+ continue;
3289
+ }
3290
+ const wired = agentIsWired(configText, names, slug, identity, sidecar, dir === directory, bareOwnerExists);
3291
+ const entry = {
3292
+ ...slug ? { slug } : {},
3293
+ ...identity.agent_id ? { agentId: identity.agent_id } : {},
3294
+ directory: dir,
3295
+ classification: wired ? "wired" : "superseded",
3296
+ checks: [],
3297
+ ...rekeyPending ? { rekeyPending } : {}
3298
+ };
3299
+ if (wired) {
3300
+ const result = await checksForAgent({ directory: dir, identity, sidecar }, input, deps);
3301
+ entry.checks = result.checks;
3302
+ capabilitiesByDirectory.set(dir, result.signerCapabilities);
3303
+ } else if (rekeyPending) {
3304
+ entry.checks = [rekeyPendingCheck(rekeyPending, void 0, input.runtime, slug)];
3305
+ }
3306
+ inventory.push(entry);
3307
+ }
3308
+ const wiredDirectories = inventory.filter((entry) => entry.classification === "wired").map((entry) => entry.directory);
3309
+ const primaryDirectory = input.credentialsDir ? directory : wiredDirectories.includes(directory ?? "") ? directory : wiredDirectories[0] ?? directory;
3310
+ if (primaryDirectory) {
3311
+ const primaryEntry = inventory.find((entry) => entry.directory === primaryDirectory);
3312
+ signerCapabilities = capabilitiesByDirectory.get(primaryDirectory);
3313
+ for (const check of primaryEntry?.checks ?? []) primaryChecksById.set(check.id, check);
3314
+ }
3315
+ if (!primaryDirectory) {
3316
+ checks.push({
3317
+ id: "credentials",
3318
+ label: "Agent credentials",
3319
+ ok: false,
3320
+ detail: "No agent credential directory with an identity.json under ~/.haven/agents.",
3321
+ repair: `Run the full setup once: ${RERUN} --setup <token from the Haven dashboard>.`
3322
+ });
3323
+ } else {
3324
+ const primaryIdentity = await readIdentity(primaryDirectory);
3325
+ const primarySidecar = await readRuntimeSidecar(primaryDirectory);
3326
+ if (!primaryChecksById.has("credentials")) {
3327
+ const result = await checksForAgent(
3328
+ { directory: primaryDirectory, identity: primaryIdentity, sidecar: primarySidecar },
3329
+ input,
3330
+ deps
3331
+ );
3332
+ signerCapabilities = result.signerCapabilities;
3333
+ for (const check of result.checks) primaryChecksById.set(check.id, check);
3334
+ }
3335
+ for (const id of ["credentials", "signer_runtime"]) {
3336
+ const check = primaryChecksById.get(id);
3337
+ if (check) checks.push(check);
3338
+ }
3339
+ }
3340
+ if (configPath === null) {
3341
+ checks.push({
3342
+ id: "runtime_config",
3343
+ label: "Runtime MCP config",
3344
+ ok: true,
3345
+ detail: `Runtime '${input.runtime}' has no file-based config the connector owns (CLI-managed) \u2014 skipping the file check.`
3346
+ });
3347
+ } else if (configText === null) {
3348
+ checks.push({
3349
+ id: "runtime_config",
3350
+ label: "Runtime MCP config",
3351
+ ok: false,
3352
+ detail: `No runtime config at ${configPath}.`,
3353
+ repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
3354
+ });
3355
+ } else {
3356
+ const primaryIdentity = await readIdentity(primaryDirectory ?? "");
3357
+ const primarySidecar = primaryDirectory ? await readRuntimeSidecar(primaryDirectory) : null;
3358
+ const hasHaven = primaryIdentity?.hosted_mcp_url ? configText.includes(primaryIdentity.hosted_mcp_url) : configText.includes("haven");
3359
+ const signerViaNpx = configText.includes("@haven_ai/signer");
3360
+ const wrapperReferenced = primarySidecar ? configText.includes(primarySidecar.wrapper_path) : false;
3361
+ const ok = hasHaven && !signerViaNpx && (primarySidecar ? wrapperReferenced : true);
3362
+ checks.push({
3363
+ id: "runtime_config",
3364
+ label: "Runtime MCP config",
3365
+ ok,
3366
+ 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${primarySidecar && !wrapperReferenced ? " (or references a different signer wrapper)" : ""}.`,
3367
+ ...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
3368
+ });
3369
+ }
3370
+ for (const id of ["hosted_mcp", "identity_match", "rekey_pending"]) {
3371
+ const check = primaryChecksById.get(id);
3372
+ if (check) checks.push(check);
3373
+ }
3374
+ const otherEntries = inventory.filter((entry) => entry.directory !== primaryDirectory);
3375
+ if (otherEntries.length > 0) {
3376
+ const live = [];
3377
+ const revoked = [];
3378
+ const unverifiable = [];
3379
+ const retired = [];
3380
+ for (const entry of otherEntries) {
3381
+ const identity = await readIdentity(entry.directory);
3382
+ const tombstone = await readAgentTombstone(entry.directory);
3383
+ const otherAgent = entry.agentId ?? path.basename(entry.directory);
3384
+ const otherUrl = identity?.hosted_mcp_url ?? (identity?.api_url ? `${identity.api_url}/mcp` : void 0);
3385
+ if (!identity?.api_key || !otherUrl) {
3386
+ if (tombstone) retired.push(`${otherAgent} (retired ${tombstone.retired_at})`);
3387
+ else unverifiable.push(`${otherAgent} (no stored key/URL to probe)`);
3388
+ continue;
3389
+ }
3390
+ const suffix = tombstone ? " [tombstoned \u2014 key material still present]" : "";
3391
+ const probe = await (deps.probeHosted ?? probeHostedMcpTools)(identity.api_key, otherUrl, deps.fetch);
3392
+ if (probe.status === "ok") live.push({ label: `${otherAgent}${suffix}`, entry });
3393
+ else if (probe.status === "unauthorized") revoked.push(`${otherAgent}${suffix}`);
3394
+ else unverifiable.push(`${otherAgent} (${probe.status})${suffix}`);
3395
+ }
3396
+ const parts = [];
3397
+ if (live.length > 0) parts.push(`STILL SPEND-CAPABLE: ${live.map((item) => item.label).join(", ")}`);
3398
+ if (revoked.length > 0) parts.push(`already revoked: ${revoked.join(", ")}`);
3399
+ if (retired.length > 0) parts.push(`tombstoned (keys removed): ${retired.join(", ")}`);
3400
+ if (unverifiable.length > 0) parts.push(`could not verify: ${unverifiable.join(", ")}`);
3401
+ const supersededLive = live.filter((item) => item.entry.classification !== "wired").map((item) => item.label);
3402
+ checks.push({
3403
+ id: "superseded_agents",
3404
+ label: "Superseded agent credentials",
3405
+ ok: supersededLive.length === 0,
3406
+ detail: supersededLive.length > 0 ? `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}. A host started before your latest setup keeps authenticating (and spending) as the old agent.` : `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}.`,
3407
+ ...supersededLive.length > 0 ? {
3408
+ repair: `Revoke ${supersededLive.join(", ")} on the Haven agent page, then remove the old director(y/ies) under ~/.haven/agents. Connect never revokes or deletes for you.`
3409
+ } : {}
3410
+ });
3411
+ }
3412
+ const parkedElsewhere = inventory.filter((entry) => entry.directory !== primaryDirectory && entry.rekeyPending).map((entry) => ({ entry, pending: entry.rekeyPending }));
3413
+ if (parkedElsewhere.length > 0) {
3414
+ const abandoned = parkedElsewhere.filter((item) => item.pending.state !== "pending");
3415
+ const describe = (item) => `${item.entry.slug ?? item.entry.agentId ?? path.basename(item.entry.directory)} (${item.pending.state}, ${item.pending.path})`;
3416
+ checks.push({
3417
+ id: "rekey_pending_elsewhere",
3418
+ label: "Parked re-keys in other credential directories",
3419
+ ok: abandoned.length === 0,
3420
+ detail: abandoned.length > 0 ? `ABANDONED re-key key material outside the agent this report describes: ${abandoned.map(describe).join(", ")}. Each holds a private key that was generated for a re-key nobody finished.` : `${parkedElsewhere.length} other director(y/ies) hold an open pending re-key: ${parkedElsewhere.map(describe).join(", ")}.`,
3421
+ ...abandoned.length > 0 ? {
3422
+ repair: "Check the Haven agent page for each before deleting: if its on-chain revoke already ran, the agent has no spend authority until you re-grant it (#1868), and that is not visible from this machine. Connect never deletes key material for you."
3423
+ } : {}
3424
+ });
3425
+ }
3426
+ const signerProcess = primaryChecksById.get("signer_process");
3427
+ if (signerProcess) checks.push(signerProcess);
3428
+ const restart = restartRequiredForRuntime(input.runtime, deps.env);
3429
+ checks.push({
3430
+ id: "restart",
3431
+ label: "Runtime restart",
3432
+ ok: true,
3433
+ 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."
3434
+ });
3435
+ const wiredOk = inventory.filter((entry) => entry.classification === "wired").every((entry) => entry.checks.every((check) => check.ok));
2289
3436
  return {
2290
- canWriteRuntimeConfig: profile.canWriteRuntimeConfig,
2291
- restartRequired: restartRequiredForRuntime(runtime, env)
3437
+ version: 1,
3438
+ ok: checks.every((check) => check.ok) && wiredOk,
3439
+ runtime: input.runtime,
3440
+ credentialDirectory: primaryDirectory,
3441
+ checks,
3442
+ agents: inventory,
3443
+ ...signerCapabilities ? { signerCapabilities } : {}
2292
3444
  };
2293
3445
  }
2294
- async function configureClaudeCode(deps, localMcpCommand) {
2295
- const runCommand = deps.runCommand ?? defaultRunCommand;
2296
- const serverJson = JSON.stringify({
2297
- type: "stdio",
2298
- command: localMcpCommand,
2299
- args: [],
2300
- env: {}
2301
- });
2302
- try {
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);
2306
- await runCommand("claude", ["mcp", "add-json", "haven", serverJson, "--scope", "user"]).catch(async () => {
2307
- await runCommand("claude", ["mcp", "add", "haven", "--scope", "user", "--", localMcpCommand]);
2308
- });
2309
- const verified = await runCommand("claude", ["mcp", "get", "haven"]).then(() => true).catch(() => false);
2310
- return {
2311
- hostedConfigured: false,
2312
- signerConfigured: true,
2313
- localMcpConfigured: true,
2314
- runtimeMcpMode: "local_stdio",
2315
- target: "Claude Code MCP config",
2316
- changed: true,
2317
- restartRequired: true,
2318
- messages: [
2319
- "Updated local Haven MCP entry with Claude Code.",
2320
- ...verified ? ["Verified Claude Code MCP entry."] : []
2321
- ]
2322
- };
2323
- } catch (err) {
3446
+ async function runRepair(input, deps = {}) {
3447
+ const homeDir = deps.homeDir ?? os.homedir();
3448
+ const messages = [];
3449
+ const { directory, others } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
3450
+ if (others.length > 0) {
3451
+ messages.push(`Note: ${others.length} other agent credential dir(s) exist \u2014 run --doctor for their status.`);
3452
+ }
3453
+ if (!directory) {
2324
3454
  return {
2325
- hostedConfigured: false,
2326
- signerConfigured: false,
2327
- localMcpConfigured: false,
2328
- runtimeMcpMode: "local_stdio",
2329
- target: "Claude Code MCP config",
2330
- changed: false,
2331
- restartRequired: true,
2332
- messages: [
2333
- `Could not update Claude Code MCP config: ${err instanceof Error ? err.message : String(err)}`,
2334
- "Install Claude Code or rerun the Haven setup command inside a Claude Code terminal."
2335
- ],
2336
- errorCode: "claude_code_config_failed"
3455
+ ok: false,
3456
+ messages: [`No agent credentials found to repair \u2014 run the full setup: ${RERUN} --setup <token>.`]
2337
3457
  };
2338
3458
  }
2339
- }
2340
- async function configureClaudeCodeHosted(deps, input, signerCommand) {
2341
- const runCommand = deps.runCommand ?? defaultRunCommand;
2342
- const hostedJson = JSON.stringify({
2343
- type: "http",
2344
- url: input.hostedMcpUrl,
2345
- headers: { Authorization: `Bearer ${input.apiKey}` }
2346
- });
2347
- const signerJson = JSON.stringify({
2348
- type: "stdio",
2349
- command: signerCommand?.command ?? "npx",
2350
- args: signerCommand?.args ?? ["-y", signerPackageSpec(), "--credentials", input.signerPath],
2351
- env: {}
2352
- });
3459
+ let identity;
2353
3460
  try {
2354
- await runCommand("claude", ["mcp", "remove", "haven"]).catch(() => void 0);
2355
- await runCommand("claude", ["mcp", "remove", "haven-signer"]).catch(() => void 0);
2356
- await runCommand("claude", ["mcp", "add-json", "haven", hostedJson, "--scope", "user"]);
2357
- await runCommand("claude", ["mcp", "add-json", "haven-signer", signerJson, "--scope", "user"]);
2358
- const verified = await runCommand("claude", ["mcp", "get", "haven"]).then(() => true).catch(() => false);
2359
- return {
2360
- hostedConfigured: true,
2361
- signerConfigured: true,
2362
- localMcpConfigured: false,
2363
- runtimeMcpMode: "hosted_plus_signer",
2364
- target: "Claude Code MCP config",
2365
- changed: true,
2366
- restartRequired: true,
2367
- messages: [
2368
- "Updated hosted Haven MCP and local signer entries with Claude Code.",
2369
- ...verified ? ["Verified Claude Code MCP entry."] : []
2370
- ]
2371
- };
2372
- } catch (err) {
2373
- return {
2374
- hostedConfigured: false,
2375
- signerConfigured: false,
2376
- localMcpConfigured: false,
2377
- runtimeMcpMode: "hosted_plus_signer",
2378
- target: "Claude Code MCP config",
2379
- changed: false,
2380
- restartRequired: true,
2381
- messages: [
2382
- `Could not update Claude Code MCP config: ${err instanceof Error ? err.message : String(err)}`,
2383
- "Install Claude Code or rerun the Haven setup command inside a Claude Code terminal."
2384
- ],
2385
- errorCode: "claude_code_config_failed"
2386
- };
3461
+ identity = JSON.parse(await promises.readFile(path.join(directory, "identity.json"), "utf8"));
3462
+ } catch {
3463
+ return { ok: false, messages: ["identity.json is unreadable \u2014 re-run the full setup with a fresh token."] };
2387
3464
  }
2388
- }
2389
- async function defaultRunCommand(command, args) {
2390
- await execFileAsync3(command, args, { timeout: 1e4 });
2391
- }
2392
- function buildProbeResult(mode, hostedConfigured, hostedStatus, signerReady, localMcpReady, localMcpProbeStatus) {
2393
- if (mode === "local_stdio") {
2394
- if (localMcpReady) return "local_stdio_mcp_ready";
2395
- return localMcpProbeStatus ? `local_stdio_mcp_${localMcpProbeStatus}` : "local_stdio_mcp_unavailable";
3465
+ if (!identity.api_key || !(identity.hosted_mcp_url || identity.api_url)) {
3466
+ return { ok: false, messages: ["identity.json lacks the stored API key / hosted URL \u2014 re-run the full setup."] };
2396
3467
  }
2397
- const hostedPart = hostedConfigured ? `hosted_${hostedStatus}` : "hosted_not_configured";
2398
- const signerPart = signerReady ? "local_signer_ready" : "local_signer_unavailable";
2399
- return `${hostedPart}_${signerPart}`.slice(0, 120);
2400
- }
2401
- async function resolveLocalMcpConsent(input, messages) {
2402
- if (input.ackLocalTools || input.ackSigner) {
2403
- const status = await acknowledgeLocalMcpConsent(input.identityPath, input.signerPath, (message) => messages.push(message));
2404
- if (status.acknowledged) {
2405
- messages.push("Prepared the local Haven tools acknowledgement.");
2406
- } else {
2407
- messages.push("Local Haven tools acknowledgement still needs attention.");
3468
+ const configPath = runtimeConfigPathFor(input.runtime, homeDir);
3469
+ if (configPath) {
3470
+ try {
3471
+ const existing = await promises.readFile(configPath, "utf8");
3472
+ if (existing.includes("bin/haven-mcp") || existing.includes(".haven/mcp-runtime")) {
3473
+ return {
3474
+ ok: false,
3475
+ messages: [
3476
+ `The config at ${configPath} is the LOCAL-stdio topology (--local). Repair currently rewrites only the hosted+signer shape and will not touch it.`,
3477
+ "Re-run your original setup command (with --local) to repair a local-stdio install."
3478
+ ]
3479
+ };
3480
+ }
3481
+ } catch {
2408
3482
  }
2409
- return status;
2410
3483
  }
2411
- return getLocalMcpConsentStatus(input.identityPath, input.signerPath);
3484
+ const existingSidecar = await readRuntimeSidecar(directory);
3485
+ const serverName = existingSidecar?.server_name;
3486
+ const signerPath = path.join(directory, "signer.json");
3487
+ const prepared = await prepareSignerRuntime(
3488
+ { credentialDirectory: directory, signerPath, homeDir, serverName },
3489
+ { runCommand: deps.runCommand }
3490
+ );
3491
+ messages.push(...prepared.messages);
3492
+ const names = serverNamesFor(serverName);
3493
+ messages.push(`Rewriting MCP entries ${names.hosted} / ${names.signer}${serverName ? ` (agent "${serverName}")` : " (unnamed pair)"} \u2014 no other pair is touched.`);
3494
+ const configResult = await writeRuntimeConfig({
3495
+ runtime: input.runtime,
3496
+ hostedMcpUrl: identity.hosted_mcp_url ?? `${identity.api_url}/mcp`,
3497
+ apiKey: identity.api_key,
3498
+ identityPath: path.join(directory, "identity.json"),
3499
+ signerPath,
3500
+ credentialDirectory: directory,
3501
+ signerCommand: { command: prepared.command, args: prepared.args },
3502
+ homeDir,
3503
+ mode: "hosted",
3504
+ serverName
3505
+ });
3506
+ messages.push(...configResult.messages);
3507
+ messages.push("Repair complete \u2014 restart the runtime, then verify with --doctor.");
3508
+ return { ok: true, messages };
2412
3509
  }
2413
- async function resolveSignerConsent(input, messages) {
2414
- if (input.ackSigner || input.ackLocalTools) {
2415
- const status = await acknowledgeLocalSignerConsent(input.signerPath, (message) => messages.push(message));
2416
- if (status.acknowledged) {
2417
- messages.push("Prepared the local Haven signer acknowledgement.");
2418
- } else {
2419
- messages.push("Local Haven signer acknowledgement still needs attention.");
3510
+ var RERUN;
3511
+ var init_doctor = __esm({
3512
+ "src/doctor.ts"() {
3513
+ init_runtime_manifest();
3514
+ init_probes();
3515
+ init_signer_runtime();
3516
+ init_config_writers();
3517
+ init_runtime_registry();
3518
+ init_signer_consent();
3519
+ init_tombstone();
3520
+ init_server_names();
3521
+ init_storage();
3522
+ init_redact();
3523
+ RERUN = "npx @haven_ai/connect@alpha";
3524
+ }
3525
+ });
3526
+
3527
+ // src/runtime.ts
3528
+ init_api();
3529
+ init_key();
3530
+ init_redact();
3531
+ init_server_names();
3532
+ init_storage();
3533
+ init_runtime_install();
3534
+ init_runtime_registry();
3535
+ init_connect_error();
3536
+
3537
+ // src/installed-clients.ts
3538
+ init_connect_error();
3539
+ init_config_writers();
3540
+ init_runtime_registry();
3541
+ var SCAN_ORDER = [
3542
+ "claude-code",
3543
+ "codex-cli",
3544
+ "cursor",
3545
+ "vscode",
3546
+ "vscode-insiders",
3547
+ "claude-desktop",
3548
+ "hermes"
3549
+ ];
3550
+ function installedClientTargets(homeDir = os.homedir(), cwd = process.cwd(), env = process.env) {
3551
+ const targets = [
3552
+ {
3553
+ runtime: "claude-code",
3554
+ label: "Claude Code",
3555
+ // Claude Code is configured through its own CLI (`claude mcp add-json`),
3556
+ // not by writing a file this module owns — so its evidence is the
3557
+ // client directory, never a config path.
3558
+ configPath: null,
3559
+ markers: [path.join(homeDir, ".claude"), path.join(homeDir, ".claude.json")]
3560
+ },
3561
+ {
3562
+ runtime: "codex-cli",
3563
+ // Both Codex surfaces write the same ~/.codex/config.toml, so they are
3564
+ // ONE candidate. Splitting them would ask the user to answer a question
3565
+ // whose answers are the same write.
3566
+ label: "Codex (CLI or Desktop)",
3567
+ configPath: runtimeConfigPathFor("codex-cli", homeDir),
3568
+ markers: [path.join(homeDir, ".codex")]
3569
+ },
3570
+ {
3571
+ runtime: "cursor",
3572
+ label: "Cursor",
3573
+ configPath: runtimeConfigPathFor("cursor", homeDir),
3574
+ markers: [path.join(homeDir, ".cursor")]
3575
+ },
3576
+ {
3577
+ runtime: "vscode",
3578
+ label: "VS Code",
3579
+ configPath: runtimeConfigPathFor("vscode", homeDir),
3580
+ markers: [path.resolve(cwd, ".vscode")]
3581
+ },
3582
+ {
3583
+ runtime: "vscode-insiders",
3584
+ label: "VS Code Insiders",
3585
+ configPath: runtimeConfigPathFor("vscode-insiders", homeDir),
3586
+ markers: []
3587
+ },
3588
+ {
3589
+ runtime: "claude-desktop",
3590
+ label: "Claude Desktop",
3591
+ configPath: runtimeConfigPathFor("claude-desktop", homeDir),
3592
+ markers: []
3593
+ },
3594
+ {
3595
+ runtime: "hermes",
3596
+ label: "Hermes Agent",
3597
+ configPath: runtimeConfigPathFor("hermes", homeDir),
3598
+ markers: [env.HERMES_HOME ?? path.join(homeDir, ".hermes")]
3599
+ }
3600
+ ];
3601
+ return targets.filter((target) => runtimeProfile(target.runtime, {}).canWriteRuntimeConfig);
3602
+ }
3603
+ async function scanInstalledClients(options = {}) {
3604
+ const exists = options.exists ?? pathExists;
3605
+ const targets = installedClientTargets(options.homeDir, options.cwd, options.env ?? process.env);
3606
+ const found = [];
3607
+ for (const target of targets) {
3608
+ if (target.configPath && await exists(target.configPath)) {
3609
+ found.push({
3610
+ runtime: target.runtime,
3611
+ label: target.label,
3612
+ detail: `MCP config found at ${target.configPath}`,
3613
+ configPath: target.configPath,
3614
+ evidence: "config-file"
3615
+ });
3616
+ continue;
3617
+ }
3618
+ for (const marker of target.markers) {
3619
+ if (!await exists(marker)) continue;
3620
+ found.push({
3621
+ runtime: target.runtime,
3622
+ label: target.label,
3623
+ detail: `installed (${marker})`,
3624
+ configPath: target.configPath,
3625
+ evidence: "client-directory"
3626
+ });
3627
+ break;
2420
3628
  }
2421
- return status;
2422
3629
  }
2423
- return getLocalSignerConsentStatus(input.signerPath);
2424
- }
2425
- function signerConsentErrorCode(signerCredentialReady, signerConsent) {
2426
- if (!signerCredentialReady) return "local_signer_credential_unavailable";
2427
- if (!signerConsent?.acknowledged) return "local_signer_ack_required";
2428
- return void 0;
2429
- }
2430
- function signerProbeErrorCode(probe) {
2431
- if (!probe || probe.status === "ok") return void 0;
2432
- return `local_signer_probe_${probe.status}`;
2433
- }
2434
- function hostedMcpErrorCode(hostedConfigured, hostedProbeStatus) {
2435
- if (!hostedConfigured || hostedProbeStatus === "ok") return void 0;
2436
- return `hosted_mcp_probe_${hostedProbeStatus}`;
2437
- }
2438
- function localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbeStatus) {
2439
- if (!signerCredentialReady) return "local_signer_credential_unavailable";
2440
- if (!localMcpConsent?.acknowledged) return "local_mcp_ack_required";
2441
- if (localMcpProbeStatus && localMcpProbeStatus !== "ok") return `local_mcp_probe_${localMcpProbeStatus}`;
2442
- return void 0;
2443
- }
2444
- function nextAction(runtime, restartMode, errorCode) {
2445
- if (errorCode) return "return_to_haven_for_wallet_approval_then_finish_runtime_setup";
2446
- if (restartMode === "hot-reload") return "return_to_haven_for_wallet_approval";
2447
- if (runtime === "codex-cli" || runtime === "codex-desktop") return "return_to_haven_for_wallet_approval_then_restart_codex";
2448
- if (runtime === "claude-code") return "return_to_haven_for_wallet_approval_then_restart_claude_code";
2449
- if (restartMode === "restart-app") return "return_to_haven_for_wallet_approval_then_restart_app";
2450
- if (restartMode === "restart-session") return "return_to_haven_for_wallet_approval_then_restart_agent_session";
2451
- return "return_to_haven_for_wallet_approval_then_configure_runtime";
2452
- }
2453
- function supportsLocalMcp(runtime) {
2454
- return runtime === "codex-cli" || runtime === "codex-desktop" || runtime === "claude-code";
2455
- }
2456
- async function prepareRuntimeForLocalMcp(input, deps) {
2457
- const prepare = deps.prepareLocalMcpRuntime ?? ((runtimeInput) => prepareLocalMcpRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress }));
2458
- return prepare({
2459
- credentialDirectory: input.credentialDirectory,
2460
- identityPath: input.identityPath,
2461
- signerPath: input.signerPath,
2462
- homeDir: deps.homeDir
3630
+ return found.sort((a, b) => {
3631
+ if (a.evidence !== b.evidence) return a.evidence === "config-file" ? -1 : 1;
3632
+ return SCAN_ORDER.indexOf(a.runtime) - SCAN_ORDER.indexOf(b.runtime);
2463
3633
  });
2464
3634
  }
2465
- async function prepareSignerForRuntime(input, deps) {
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
- ));
2473
- return prepare({
2474
- credentialDirectory: input.credentialDirectory,
2475
- signerPath: input.signerPath,
2476
- homeDir: deps.homeDir
3635
+ var MAX_PROMPT_ATTEMPTS = 3;
3636
+ async function promptForInstalledClient(candidates, io = defaultPromptIo()) {
3637
+ if (candidates.length === 0) throw noInstalledClientsError();
3638
+ io.write("Haven could not detect which agent runtime this is.\n");
3639
+ io.write("These agent clients are installed on this machine:\n");
3640
+ candidates.forEach((candidate, index) => {
3641
+ io.write(` ${index + 1}) ${candidate.label} \u2014 ${candidate.detail}
3642
+ `);
2477
3643
  });
3644
+ io.write("Haven writes an API key and a signing key into the client you pick, so pick the one your agent actually runs in.\n");
3645
+ for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS; attempt += 1) {
3646
+ const answer = await io.question(`Which one? [1-${candidates.length}] (default 1 \u2014 ${candidates[0].label}): `);
3647
+ if (answer === null) throw promptAbortedError("the prompt was cancelled");
3648
+ const trimmed = answer.trim();
3649
+ if (trimmed === "") return candidates[0].runtime;
3650
+ const picked = Number.parseInt(trimmed, 10);
3651
+ if (Number.isInteger(picked) && picked >= 1 && picked <= candidates.length) {
3652
+ return candidates[picked - 1].runtime;
3653
+ }
3654
+ io.write(`"${trimmed}" is not one of 1-${candidates.length}.
3655
+ `);
3656
+ }
3657
+ throw promptAbortedError(`no valid choice after ${MAX_PROMPT_ATTEMPTS} attempts`);
3658
+ }
3659
+ async function resolveRuntimeByInstalledClientPrompt(options = {}) {
3660
+ const candidates = await scanInstalledClients(options);
3661
+ if (candidates.length === 0) throw noInstalledClientsError();
3662
+ return promptForInstalledClient(candidates, options.io ?? defaultPromptIo());
3663
+ }
3664
+ function noInstalledClientsError() {
3665
+ return new exports.ConnectError(
3666
+ "runtime_no_installed_clients",
3667
+ "Could not determine the agent runtime: nothing was detected in this environment, and no agent client Haven can configure is installed on this machine. Re-run with --runtime <name> naming the client you want configured, or --runtime other to store credentials and finish the MCP setup by hand.",
3668
+ "rerun_connect_with_explicit_runtime"
3669
+ );
2478
3670
  }
2479
- async function runLocalMcpProbe(runtimeInstall, deps) {
2480
- const probe = deps.probeLocalMcpTools ?? probeLocalMcpTools;
3671
+ function promptAbortedError(reason) {
3672
+ return new exports.ConnectError(
3673
+ "runtime_prompt_aborted",
3674
+ `Runtime not chosen (${reason}). Nothing was written: no agent was created, no credentials were stored, and the Haven setup token is still unused. Run the setup command again, or pass --runtime <name> to skip the prompt.`,
3675
+ "rerun_connect_and_choose_a_runtime"
3676
+ );
3677
+ }
3678
+ async function pathExists(path) {
2481
3679
  try {
2482
- return await probe(runtimeInstall.command, runtimeInstall.args, exports.MCP_RUNTIME_MANIFEST.requiredTools);
3680
+ await promises.access(path);
3681
+ return true;
2483
3682
  } catch {
2484
- return { status: "process_error" };
3683
+ return false;
2485
3684
  }
2486
3685
  }
2487
- function localRuntimePrepareErrorCode(err) {
2488
- if (err && typeof err === "object" && "code" in err && err.code === "local_mcp_unsupported_node_version") {
2489
- return "local_mcp_unsupported_node_version";
2490
- }
2491
- return "local_mcp_runtime_install_failed";
3686
+ function defaultPromptIo() {
3687
+ return {
3688
+ write: (text) => process.stdout.write(text),
3689
+ question: (query) => new Promise((resolvePromise) => {
3690
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
3691
+ let settled = false;
3692
+ const settle = (value) => {
3693
+ if (settled) return;
3694
+ settled = true;
3695
+ rl.close();
3696
+ resolvePromise(value);
3697
+ };
3698
+ rl.once("SIGINT", () => settle(null));
3699
+ rl.once("close", () => settle(null));
3700
+ rl.question(query, (answer) => settle(answer));
3701
+ })
3702
+ };
2492
3703
  }
2493
3704
 
2494
3705
  // src/runtime.ts
2495
- init_runtime_registry();
3706
+ init_local_mcp_runtime();
2496
3707
  init_runtime_manifest();
2497
- var CONNECTOR_VERSION = "0.1.28-alpha.0";
3708
+ var CONNECTOR_VERSION = "0.1.30-alpha.0";
2498
3709
  var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
2499
3710
  async function runConnect(options, deps = {}) {
2500
3711
  assertSupportedNodeVersion(deps.nodeVersion, exports.MCP_RUNTIME_MANIFEST.minimumNodeVersion);
@@ -2510,24 +3721,49 @@ async function runConnect(options, deps = {}) {
2510
3721
  const runRuntimeInstall = deps.installRuntime ?? installRuntime;
2511
3722
  const generateKey = deps.generateKey ?? generateDelegateKey;
2512
3723
  const generateLocalApiKey = deps.generateApiKey ?? generateAgentApiKey;
2513
- const installCapabilities = runtimeInstallCapabilities(options.runtime);
3724
+ const selection = await resolveRuntimeSelection(options.runtime, options.runtimeForce, {
3725
+ env: deps.env ?? process.env,
3726
+ selfReported: options.runtimeSelfReport,
3727
+ promptForRuntime: runtimeSelectionPrompt(options, deps)
3728
+ });
3729
+ if (!selection.runtime) {
3730
+ throw new exports.ConnectError(
3731
+ "runtime_undetermined",
3732
+ `Could not determine the agent runtime: nothing was detected in this environment and no --runtime was given. If you are an AI agent running this command: re-run it once, unchanged except for adding --runtime <name>, naming the harness you are running in \u2014 one of: ${exports.RUNTIME_FLAG_VALUES} (the aliases cowork, codex and openclaw are accepted too). Do not guess: if your harness is not one of those, use --runtime other, which stores the credentials and prints the manual MCP steps. Nothing was written and the Haven setup token is still unused.`,
3733
+ "rerun_connect_with_explicit_runtime"
3734
+ );
3735
+ }
3736
+ const runtime = selection.runtime;
3737
+ const installCapabilities = runtimeInstallCapabilities(runtime);
2514
3738
  if (options.localMcp) {
2515
- const resolvedRuntime = normalizeRuntime(options.runtime);
2516
- if (!supportsLocalMcp(resolvedRuntime)) {
3739
+ if (!supportsLocalMcp(runtime)) {
2517
3740
  throw new Error(
2518
- `--local (fully-local Haven MCP) is only available for Claude Code and Codex. The detected runtime is ${runtimeProfile(resolvedRuntime).label}. Re-run without --local to use the default hosted MCP + local signer setup.`
3741
+ `--local (fully-local Haven MCP) is only available for Claude Code and Codex. The detected runtime is ${runtimeProfile(runtime).label}. Re-run without --local to use the default hosted MCP + local signer setup.`
2519
3742
  );
2520
3743
  }
2521
3744
  }
3745
+ if (selection.overrodeHint) {
3746
+ log(`runtime: ${runtime} (detected; ignoring the ${selection.overrodeHint} hint \u2014 pass --runtime-force ${selection.overrodeHint} to override)`);
3747
+ }
3748
+ if (selection.discardedHint) {
3749
+ log(`runtime: ${runtime} (detected; "${selection.discardedHint}" is not a runtime Haven knows \u2014 valid values: ${exports.RUNTIME_FLAG_VALUES})`);
3750
+ }
3751
+ if (selection.source === "prompted") {
3752
+ log(`runtime: ${runtime} (chosen at the prompt \u2014 nothing was detected in this environment)`);
3753
+ }
2522
3754
  log("Warming up your connection to Haven\u2026");
2523
3755
  const setup = await api.resolveSetup({
2524
3756
  setupToken: options.setupToken,
2525
3757
  connectorVersion,
2526
- runtime: options.runtime
3758
+ runtime
2527
3759
  });
2528
3760
  assertSetupChallengeIsUsable(setup.challenge.expires_at);
2529
3761
  printSetupSummary(setup, log);
2530
3762
  await preflightStorage({ baseDir: options.credentialsDir, warn: log });
3763
+ if (options.serverName) {
3764
+ assertValidServerSlug(options.serverName);
3765
+ await assertServerSlugAvailable(options.serverName, options.credentialsDir);
3766
+ }
2531
3767
  log("Checked local credential storage \u2014 all clear.");
2532
3768
  const localKey = generateKey();
2533
3769
  const localApiKey = generateLocalApiKey();
@@ -2539,12 +3775,17 @@ async function runConnect(options, deps = {}) {
2539
3775
  registration = await api.registerSetup({
2540
3776
  setupToken: options.setupToken,
2541
3777
  connectorVersion,
2542
- runtime: options.runtime,
3778
+ runtime,
2543
3779
  challengeId: setup.challenge.id,
2544
3780
  delegateAddress: localKey.address,
2545
3781
  proofSignature,
2546
3782
  apiKeyHash: hashAgentApiKey(localApiKey),
2547
3783
  apiKeyPrefix: agentApiKeyPrefix(localApiKey),
3784
+ // #1878: report the pair we are ACTUALLY wiring, bare pair included, so
3785
+ // the dashboard can name it. Derived here rather than sent as the raw
3786
+ // slug — `serverNamesFor` is the one place the naming rule lives, and
3787
+ // the hosted name is what a user pastes into an MCP config.
3788
+ mcpServerName: serverNamesFor(options.serverName).hosted,
2548
3789
  connectorContext: {
2549
3790
  environment_label: options.environmentLabel ?? "Local workspace",
2550
3791
  config_target: installCapabilities.canWriteRuntimeConfig ? "agent runtime MCP config" : "local credential files"
@@ -2564,6 +3805,7 @@ async function runConnect(options, deps = {}) {
2564
3805
  const credentialPaths = await writeCredentials({
2565
3806
  baseDir: options.credentialsDir,
2566
3807
  agentId: registration.agent_id,
3808
+ serverName: options.serverName,
2567
3809
  apiKey: localApiKey,
2568
3810
  delegateKey: localKey.privateKey,
2569
3811
  delegateAddress: localKey.address,
@@ -2591,7 +3833,7 @@ async function runConnect(options, deps = {}) {
2591
3833
  );
2592
3834
  }
2593
3835
  const runtimeInstall = await runRuntimeInstall({
2594
- runtime: options.runtime,
3836
+ runtime,
2595
3837
  hostedMcpUrl: registration.hosted_mcp_url,
2596
3838
  apiKey: localApiKey,
2597
3839
  signerPath: credentialPaths.signerPath,
@@ -2600,7 +3842,8 @@ async function runConnect(options, deps = {}) {
2600
3842
  environmentLabel: options.environmentLabel ?? "Local workspace",
2601
3843
  ackSigner: options.ackSigner,
2602
3844
  ackLocalTools: options.ackLocalTools,
2603
- localMcp: options.localMcp
3845
+ localMcp: options.localMcp,
3846
+ serverName: options.serverName
2604
3847
  }, {
2605
3848
  onProgress: log,
2606
3849
  // #1543: report "runtime configured" the moment the config write settles,
@@ -2636,6 +3879,19 @@ async function runConnect(options, deps = {}) {
2636
3879
  } else {
2637
3880
  log("Haven setup on this machine is complete.");
2638
3881
  }
3882
+ try {
3883
+ const supersededIds = await listOtherAgentIds(options.credentialsDir, credentialPaths.directory);
3884
+ if (supersededIds.length > 0) {
3885
+ log("");
3886
+ log(
3887
+ `Heads-up: this setup created a NEW agent. Your previous agent(s) \u2014 ${supersededIds.join(", ")} \u2014 still exist with their own keys, and any host that was already running keeps acting as them.`
3888
+ );
3889
+ log(
3890
+ `If you meant to replace them: revoke them on the Haven agent page, then restart EVERY long-lived host (gateways, TUI workers, editors) \u2014 each holds the MCP wiring snapshot from its own start time, so after repeated setups each can be stuck on a DIFFERENT old agent. Then remove their directories under ~/.haven/agents (or ${RERUN_HINT} --tombstone <dir> to leave a diagnostic in their place). Run ${RERUN_HINT} --doctor to check whether their keys are still live.`
3891
+ );
3892
+ }
3893
+ } catch {
3894
+ }
2639
3895
  try {
2640
3896
  await api.updateInstallStatus(registration.setup_id, localApiKey, {
2641
3897
  runtime: runtimeInstall.runtime,
@@ -2710,11 +3966,16 @@ function completionOutcome(input) {
2710
3966
  };
2711
3967
  return outcome;
2712
3968
  }
3969
+ function runtimeSelectionPrompt(options, deps) {
3970
+ if (options.interactive !== true) return void 0;
3971
+ if (!(deps.isTty ?? Boolean(process.stdin.isTTY))) return void 0;
3972
+ return deps.promptRuntime ?? (() => resolveRuntimeByInstalledClientPrompt());
3973
+ }
2713
3974
  function failedConnectOutcome(runtimeHint, error) {
2714
3975
  const message = error instanceof Error ? error.message : "";
2715
- const code = /Node\.js >=/i.test(message) ? "unsupported_node_version" : /setup challenge.*expired|expired or invalid/i.test(message) ? "setup_challenge_expired_or_invalid" : /only available for Claude Code and Codex/i.test(message) ? "local_mcp_unsupported_runtime" : "connect_failed";
3976
+ const code = error instanceof exports.ConnectError ? error.code : /Node\.js >=/i.test(message) ? "unsupported_node_version" : /setup challenge.*expired|expired or invalid/i.test(message) ? "setup_challenge_expired_or_invalid" : /only available for Claude Code and Codex/i.test(message) ? "local_mcp_unsupported_runtime" : "connect_failed";
2716
3977
  const runtime = normalizeRuntime(runtimeHint);
2717
- const nextAction2 = code === "setup_challenge_expired_or_invalid" ? "return_to_haven_for_fresh_setup" : code === "unsupported_node_version" ? "install_supported_node_and_rerun_connect" : code === "local_mcp_unsupported_runtime" ? "rerun_without_local_mcp" : "review_the_safe_error_output_and_start_a_fresh_haven_setup_if_needed";
3978
+ const nextAction2 = error instanceof exports.ConnectError ? error.nextAction : code === "setup_challenge_expired_or_invalid" ? "return_to_haven_for_fresh_setup" : code === "unsupported_node_version" ? "install_supported_node_and_rerun_connect" : code === "local_mcp_unsupported_runtime" ? "rerun_without_local_mcp" : "review_the_safe_error_output_and_start_a_fresh_haven_setup_if_needed";
2718
3979
  return {
2719
3980
  schema_version: CONNECT_OUTCOME_SCHEMA_VERSION,
2720
3981
  outcome: "failed",
@@ -2800,7 +4061,7 @@ function describeApprovedBudget(budget) {
2800
4061
  async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
2801
4062
  const intervalMs = options.intervalMs ?? 5e3;
2802
4063
  const timeoutMs = options.timeoutMs ?? 18e4;
2803
- const sleep = options.sleep ?? ((ms) => new Promise((resolve8) => setTimeout(resolve8, ms)));
4064
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve9) => setTimeout(resolve9, ms)));
2804
4065
  const maxPolls = Math.max(1, Math.floor(timeoutMs / intervalMs));
2805
4066
  const remindEvery = Math.max(1, Math.floor(3e4 / intervalMs));
2806
4067
  let waitingAnnounced = false;
@@ -2886,11 +4147,39 @@ function activationInstructionWithWhy(profile) {
2886
4147
  }
2887
4148
  return profile.activationInstruction;
2888
4149
  }
4150
+ var RERUN_HINT = "npx @haven_ai/connect@alpha";
4151
+ async function listOtherAgentIds(baseDir, currentDirectory) {
4152
+ const root = defaultCredentialRoot(baseDir);
4153
+ let entries = [];
4154
+ try {
4155
+ entries = await promises.readdir(root);
4156
+ } catch {
4157
+ return [];
4158
+ }
4159
+ const ids = [];
4160
+ for (const entry of entries) {
4161
+ if (path.join(root, entry) === currentDirectory) continue;
4162
+ const identityPath = path.join(root, entry, "identity.json");
4163
+ try {
4164
+ await promises.stat(identityPath);
4165
+ } catch {
4166
+ continue;
4167
+ }
4168
+ try {
4169
+ const identity = JSON.parse(await promises.readFile(identityPath, "utf8"));
4170
+ ids.push(identity.agent_id ?? entry);
4171
+ } catch {
4172
+ ids.push(entry);
4173
+ }
4174
+ }
4175
+ return ids;
4176
+ }
2889
4177
  function printNextSteps(result, log, approval) {
2890
4178
  for (const line of completionHandoffLines(result, approval)) log(line);
2891
4179
  }
2892
4180
 
2893
4181
  // src/args.ts
4182
+ init_server_names();
2894
4183
  function parseArgs(argv, env = process.env) {
2895
4184
  const options = {
2896
4185
  apiBaseUrl: env.HAVEN_API_URL ?? "http://localhost:3001",
@@ -2900,6 +4189,11 @@ function parseArgs(argv, env = process.env) {
2900
4189
  let json = false;
2901
4190
  let doctor = false;
2902
4191
  let repair = false;
4192
+ let rekeyPhase;
4193
+ let newApiKey;
4194
+ let tombstoneDir;
4195
+ let tombstoneReason;
4196
+ let tombstoneReplacedBy;
2903
4197
  for (let i = 0; i < argv.length; i += 1) {
2904
4198
  const arg = argv[i];
2905
4199
  if (arg === "--help" || arg === "-h") {
@@ -2910,14 +4204,31 @@ function parseArgs(argv, env = process.env) {
2910
4204
  doctor = true;
2911
4205
  } else if (arg === "--repair") {
2912
4206
  repair = true;
4207
+ } else if (arg === "--rekey") {
4208
+ rekeyPhase = "start";
4209
+ } else if (arg === "--rekey-finish") {
4210
+ rekeyPhase = "finish";
4211
+ } else if (arg === "--api-key") {
4212
+ newApiKey = requireValue(argv, ++i, arg);
4213
+ } else if (arg === "--tombstone") {
4214
+ tombstoneDir = requireValue(argv, ++i, arg);
4215
+ } else if (arg === "--reason") {
4216
+ tombstoneReason = requireValue(argv, ++i, arg);
4217
+ } else if (arg === "--replaced-by") {
4218
+ tombstoneReplacedBy = requireValue(argv, ++i, arg);
2913
4219
  } else if (arg === "--setup" || arg === "--setup-token") {
2914
4220
  options.setupToken = requireValue(argv, ++i, arg);
2915
4221
  } else if (arg === "--api" || arg === "--api-url") {
2916
4222
  options.apiBaseUrl = requireValue(argv, ++i, arg);
2917
4223
  } else if (arg === "--runtime") {
2918
4224
  options.runtime = requireValue(argv, ++i, arg);
4225
+ } else if (arg === "--runtime-force") {
4226
+ options.runtimeForce = requireValue(argv, ++i, arg);
2919
4227
  } else if (arg === "--credentials-dir") {
2920
4228
  options.credentialsDir = requireValue(argv, ++i, arg);
4229
+ } else if (arg === "--name") {
4230
+ options.serverName = requireValue(argv, ++i, arg);
4231
+ assertValidServerSlug(options.serverName);
2921
4232
  } else if (arg === "--environment-label") {
2922
4233
  options.environmentLabel = requireValue(argv, ++i, arg);
2923
4234
  } else if (arg === "--ack-local-tools") {
@@ -2935,14 +4246,39 @@ function parseArgs(argv, env = process.env) {
2935
4246
  throw new Error(`Unknown option: ${arg}`);
2936
4247
  }
2937
4248
  }
4249
+ const tombstone = tombstoneDir ? { directory: tombstoneDir, reason: tombstoneReason, replacedBy: tombstoneReplacedBy } : void 0;
4250
+ const rekey = rekeyPhase ? { phase: rekeyPhase, newApiKey } : void 0;
2938
4251
  if (help) {
2939
- return { options, help, json, doctor, repair };
4252
+ return { options, help, json, doctor, repair, tombstone, rekey };
4253
+ }
4254
+ if (rekey) {
4255
+ if (options.setupToken) {
4256
+ throw new Error("--rekey replaces an existing agent's key; it does not take --setup. Drop one of them.");
4257
+ }
4258
+ if (rekey.phase === "start" && newApiKey !== void 0) {
4259
+ throw new Error(
4260
+ "--api-key belongs to --rekey-finish. --rekey generates the new key here and prints the address to paste into Haven; the API key does not exist yet."
4261
+ );
4262
+ }
4263
+ if (rekey.phase === "finish" && !newApiKey) {
4264
+ throw new Error("--rekey-finish needs --api-key <key> \u2014 the one the Haven agent page showed once.");
4265
+ }
4266
+ return { options, help, json, doctor, repair, tombstone, rekey };
4267
+ }
4268
+ if (newApiKey !== void 0) {
4269
+ throw new Error("--api-key requires --rekey-finish.");
4270
+ }
4271
+ if (!tombstoneDir && (tombstoneReason !== void 0 || tombstoneReplacedBy !== void 0)) {
4272
+ throw new Error("--reason and --replaced-by require --tombstone <dir>.");
4273
+ }
4274
+ if (tombstone) {
4275
+ return { options, help, json, doctor, repair, tombstone, rekey };
2940
4276
  }
2941
4277
  if (doctor || repair) {
2942
4278
  if (!options.runtime) {
2943
4279
  throw new Error("--doctor/--repair need --runtime <runtime> (which config to examine).");
2944
4280
  }
2945
- return { options, help, json, doctor, repair };
4281
+ return { options, help, json, doctor, repair, tombstone, rekey };
2946
4282
  }
2947
4283
  if (!options.setupToken) {
2948
4284
  throw new Error("Missing --setup <hv_setup_...> setup token.");
@@ -2951,7 +4287,7 @@ function parseArgs(argv, env = process.env) {
2951
4287
  throw new Error("Missing --api <Haven API URL>.");
2952
4288
  }
2953
4289
  options.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, "");
2954
- return { options, help, json, doctor, repair };
4290
+ return { options, help, json, doctor, repair, tombstone, rekey };
2955
4291
  }
2956
4292
  function helpText() {
2957
4293
  return [
@@ -2961,14 +4297,23 @@ function helpText() {
2961
4297
  "sends Haven only the public signing address plus a proof signature.",
2962
4298
  "",
2963
4299
  "Usage:",
2964
- " npx -y @haven_ai/connect --setup hv_setup_... --api https://api.haven.example --ack-local-tools --runtime claude-code",
4300
+ " npx -y @haven_ai/connect --setup hv_setup_... --api https://api.haven.example --ack-local-tools",
2965
4301
  "",
2966
4302
  "Options:",
2967
4303
  " --setup <token> Short-lived setup token from Haven.",
2968
4304
  " --api <url> Haven backend API URL. Defaults to HAVEN_API_URL or http://localhost:3001.",
2969
4305
  " --runtime <name> Agent runtime hint, such as claude-code, codex-cli, codex-desktop, cursor, vscode, claude-desktop, or hermes.",
4306
+ " Usually unnecessary: the connector detects the runtime it runs inside, and a detection",
4307
+ " that contradicts this hint wins (with a printed notice). When nothing is detected, an",
4308
+ " interactive terminal is offered the agent clients installed on this machine; this flag is",
4309
+ " how an agent, or a non-interactive run, answers instead. An unknown name is refused, never guessed.",
4310
+ " --runtime-force <name> Escape hatch: use exactly this runtime, ignoring environment detection.",
2970
4311
  " --credentials-dir <path> Credential directory fallback. Defaults to ~/.haven/agents.",
2971
4312
  " --environment-label <text> Non-sensitive label shown in Haven setup review.",
4313
+ " --name <slug> Wiring slug for a NAMED agent: writes haven-<slug> / haven-signer-<slug>",
4314
+ " MCP entries and stores credentials at ~/.haven/agents/<slug>/, so several",
4315
+ " agents can run side by side in one runtime. 1-32 lowercase letters, digits,",
4316
+ " single hyphens; immutable once wired. Omit for the bare haven / haven-signer pair.",
2972
4317
  " --ack-local-tools Write the one-time local Haven tools acknowledgement during setup.",
2973
4318
  " --ack-signer Backward-compatible alias for --ack-local-tools.",
2974
4319
  " --local Advanced: install the fully-local Haven MCP (no hosted dependency).",
@@ -2979,6 +4324,19 @@ function helpText() {
2979
4324
  " --repair Repair, then re-diagnose (implies --doctor): reinstall the pinned signer",
2980
4325
  " runtime, rewrite the wrapper and runtime config from stored credentials.",
2981
4326
  " Hosted topology only (refuses to touch a --local config). No keys, no token.",
4327
+ " --rekey Replace this agent's signing key (no token). Generates a fresh keypair HERE and",
4328
+ " prints its public address to paste into the Haven agent page. Nothing changes",
4329
+ " until you finish; the agent keeps working on its old key throughout.",
4330
+ " Add --name <slug> for a named agent. Refuses a legacy-rail or revoked agent.",
4331
+ " --rekey-finish Second half of --rekey: writes the new key and the API key the agent page",
4332
+ " showed once, in place at the same path, and rewrites only this agent's MCP",
4333
+ " config pair. Server names do not change, so wired hosts need only a restart.",
4334
+ " --api-key <key> The new API key, for --rekey-finish.",
4335
+ " --tombstone <dir> Retire an agent credential directory in place (no token): replaces its signer",
4336
+ " wrapper with a diagnostic that names the retirement in MCP stderr logs, and",
4337
+ " writes TOMBSTONE.json. Touches NO key material and revokes nothing.",
4338
+ " --reason <text> Reason recorded in the tombstone (with --tombstone).",
4339
+ " --replaced-by <agent-id> Successor agent recorded in the tombstone (with --tombstone).",
2982
4340
  " --help Show this help.",
2983
4341
  "",
2984
4342
  "The connector never prints the private key and never sends it to Haven. JSON output never includes credential contents or full credential paths."
@@ -2991,6 +4349,12 @@ function requireValue(argv, index, option) {
2991
4349
  }
2992
4350
  return value;
2993
4351
  }
4352
+
4353
+ // src/index.ts
4354
+ init_api();
4355
+ init_key();
4356
+ init_redact();
4357
+ init_redact();
2994
4358
  async function runCli(argv, io = {
2995
4359
  stdout: (message) => process.stdout.write(message),
2996
4360
  stderr: (message) => process.stderr.write(message)
@@ -3014,6 +4378,103 @@ async function runCli(argv, io = {
3014
4378
  `);
3015
4379
  return 0;
3016
4380
  }
4381
+ if (parsed.tombstone) {
4382
+ const { writeAgentTombstone: writeAgentTombstone2 } = await Promise.resolve().then(() => (init_tombstone(), tombstone_exports));
4383
+ const { readFile: readFile12 } = await import('fs/promises');
4384
+ const { join: join10 } = await import('path');
4385
+ try {
4386
+ let agentId = "unknown";
4387
+ try {
4388
+ const identity = JSON.parse(
4389
+ await readFile12(join10(parsed.tombstone.directory, "identity.json"), "utf8")
4390
+ );
4391
+ agentId = identity.agent_id ?? "unknown";
4392
+ } catch {
4393
+ }
4394
+ const info = await writeAgentTombstone2({
4395
+ directory: parsed.tombstone.directory,
4396
+ agentId,
4397
+ reason: parsed.tombstone.reason ?? "retired by operator via --tombstone",
4398
+ replacedBy: parsed.tombstone.replacedBy
4399
+ });
4400
+ if (parsed.json) {
4401
+ io.stdout(`${redactSecrets(JSON.stringify({ tombstoned: true, ...info }))}
4402
+ `);
4403
+ } else {
4404
+ io.stdout(redactSecrets(`Tombstoned agent ${info.agent_id} at ${parsed.tombstone.directory}.
4405
+ `));
4406
+ io.stdout(
4407
+ "Key files were NOT touched and nothing was revoked \u2014 revoke the agent on the Haven agent page if you have not already.\n"
4408
+ );
4409
+ io.stdout(
4410
+ "Restart EVERY long-lived MCP host (gateway, TUI workers, editors): each holds the wiring snapshot from its own start time, and the tombstone only speaks when a stale host next probes the old path.\n"
4411
+ );
4412
+ }
4413
+ return 0;
4414
+ } catch (err) {
4415
+ io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
4416
+ `);
4417
+ return 1;
4418
+ }
4419
+ }
4420
+ if (parsed.rekey) {
4421
+ const { startRekey: startRekey2, finishRekey: finishRekey2 } = await Promise.resolve().then(() => (init_rekey(), rekey_exports));
4422
+ const { restartGuidance: restartGuidance2 } = await Promise.resolve().then(() => (init_rekey_restart(), rekey_restart_exports));
4423
+ const common = {
4424
+ serverName: parsed.options.serverName,
4425
+ credentialsDir: parsed.options.credentialsDir,
4426
+ runtime: parsed.options.runtime
4427
+ };
4428
+ try {
4429
+ if (parsed.rekey.phase === "start") {
4430
+ const result2 = await startRekey2(common);
4431
+ if (parsed.json) {
4432
+ io.stdout(
4433
+ `${redactSecrets(
4434
+ JSON.stringify({
4435
+ rekey: "started",
4436
+ agent_id: result2.agentId,
4437
+ new_delegate_address: result2.newDelegateAddress,
4438
+ expires_at: result2.expiresAt
4439
+ })
4440
+ )}
4441
+ `
4442
+ );
4443
+ } else {
4444
+ for (const line of result2.messages) io.stdout(redactSecrets(`${line}
4445
+ `));
4446
+ }
4447
+ return 0;
4448
+ }
4449
+ const result = await finishRekey2({ ...common, newApiKey: parsed.rekey.newApiKey });
4450
+ const restart = restartGuidance2(parsed.options.runtime);
4451
+ if (parsed.json) {
4452
+ io.stdout(
4453
+ `${redactSecrets(
4454
+ JSON.stringify({
4455
+ rekey: "finished",
4456
+ agent_id: result.agentId,
4457
+ new_delegate_address: result.newDelegateAddress,
4458
+ mcp_servers: result.serverNames,
4459
+ restart_commands: restart.commands
4460
+ })
4461
+ )}
4462
+ `
4463
+ );
4464
+ } else {
4465
+ for (const line of result.messages) io.stdout(redactSecrets(`${line}
4466
+ `));
4467
+ io.stdout("\n");
4468
+ for (const line of restart.lines) io.stdout(redactSecrets(`${line}
4469
+ `));
4470
+ }
4471
+ return 0;
4472
+ } catch (err) {
4473
+ io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
4474
+ `);
4475
+ return 1;
4476
+ }
4477
+ }
3017
4478
  if (parsed.doctor || parsed.repair) {
3018
4479
  const { runDoctor: runDoctor2, runRepair: runRepair2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
3019
4480
  const runtime = parsed.options.runtime ?? "";
@@ -3036,6 +4497,23 @@ async function runCli(argv, io = {
3036
4497
  if (check.repair) io.stdout(redactSecrets(` \u21B3 repair: ${check.repair}
3037
4498
  `));
3038
4499
  }
4500
+ const otherAgents = report.agents.filter((agent) => agent.directory !== report.credentialDirectory);
4501
+ if (otherAgents.length > 0) {
4502
+ io.stdout("\nOther agents on this machine:\n");
4503
+ for (const agent of otherAgents) {
4504
+ const name = agent.slug ? `${agent.slug} (${agent.agentId ?? "unknown"})` : agent.agentId ?? "unknown";
4505
+ const failed = agent.checks.filter((check) => !check.ok);
4506
+ const verdict = agent.classification === "wired" ? failed.length === 0 ? "wired, all checks passed" : `wired, ${failed.length} check(s) FAILED` : agent.classification === "parked" ? "parked re-key only \u2014 no identity.json in this directory, but key material is still there" : agent.classification;
4507
+ io.stdout(redactSecrets(` ${failed.length > 0 ? "\u2717" : "\u2022"} ${name}: ${verdict}
4508
+ `));
4509
+ for (const check of failed) {
4510
+ io.stdout(redactSecrets(` \u2717 ${check.label}: ${check.detail}
4511
+ `));
4512
+ if (check.repair) io.stdout(redactSecrets(` \u21B3 repair: ${check.repair}
4513
+ `));
4514
+ }
4515
+ }
4516
+ }
3039
4517
  io.stdout(report.ok ? "All checks passed.\n" : "One or more checks FAILED \u2014 see repairs above.\n");
3040
4518
  }
3041
4519
  return report.ok ? 0 : 1;
@@ -3047,7 +4525,15 @@ async function runCli(argv, io = {
3047
4525
  }
3048
4526
  try {
3049
4527
  const result = await runConnect(
3050
- { ...parsed.options, waitForApproval: !parsed.json },
4528
+ {
4529
+ ...parsed.options,
4530
+ waitForApproval: !parsed.json,
4531
+ // #1719: only a human-facing run may be asked which installed client to
4532
+ // configure. --json is the automation contract — it must fail with a
4533
+ // machine-readable code, never block on stdin. runConnect additionally
4534
+ // requires a real TTY before it prompts.
4535
+ interactive: !parsed.json
4536
+ },
3051
4537
  {
3052
4538
  log: (message) => (parsed.json ? io.stderr : io.stdout)(`${message}
3053
4539
  `),
@@ -3083,8 +4569,11 @@ function isCliEntrypoint(argvPath = process.argv[1], moduleUrl = (typeof documen
3083
4569
  if (isCliEntrypoint()) void main();
3084
4570
 
3085
4571
  // src/index.ts
4572
+ init_runtime_install();
3086
4573
  init_signer_runtime();
3087
4574
  init_runtime_registry();
4575
+ init_connect_error();
4576
+ init_storage();
3088
4577
  init_runtime_manifest();
3089
4578
 
3090
4579
  exports.CONNECTOR_VERSION = CONNECTOR_VERSION;
@@ -3097,15 +4586,20 @@ exports.failedConnectOutcome = failedConnectOutcome;
3097
4586
  exports.generateDelegateKey = generateDelegateKey;
3098
4587
  exports.helpText = helpText;
3099
4588
  exports.installRuntime = installRuntime;
4589
+ exports.isConnectError = isConnectError;
3100
4590
  exports.mcpPackageSpec = mcpPackageSpec;
3101
4591
  exports.normalizeRuntime = normalizeRuntime;
3102
4592
  exports.parseArgs = parseArgs;
3103
4593
  exports.prepareSignerRuntime = prepareSignerRuntime;
4594
+ exports.promptForInstalledClient = promptForInstalledClient;
3104
4595
  exports.redactSecrets = redactSecrets;
4596
+ exports.resolveRuntimeByInstalledClientPrompt = resolveRuntimeByInstalledClientPrompt;
4597
+ exports.resolveRuntimeSelection = resolveRuntimeSelection;
3105
4598
  exports.runCli = runCli;
3106
4599
  exports.runConnect = runConnect;
3107
4600
  exports.runtimeInstallCapabilities = runtimeInstallCapabilities;
3108
4601
  exports.runtimeProfile = runtimeProfile;
4602
+ exports.scanInstalledClients = scanInstalledClients;
3109
4603
  exports.sdkPackageSpec = sdkPackageSpec;
3110
4604
  exports.shortAddress = shortAddress;
3111
4605
  exports.signerPackageSpec = signerPackageSpec;