@haven_ai/connect 0.1.29-alpha.0 → 0.1.31-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.js CHANGED
@@ -1,14 +1,14 @@
1
- import { ensureConsent, computeConsentHash, loadCredentials, consentInputFromClient, registeredToolNames, MCP_VERSION } from '@haven_ai/mcp';
2
- import { ensureSignerConsent, computeSignerConsentHash, loadSignerCredentials, createEdgeSigner, toolSchemas } from '@haven_ai/signer';
3
- import { isSupportedNodeVersion, unsupportedNodeVersionMessage, SKILL_FOLDER_NAME, resolveTokenFromAddress, HAVEN_SKILL_MD, HAVEN_SKILL_BODY_MD, HAVEN_MINIMUM_NODE_VERSION } from '@haven_ai/sdk';
4
- import { mkdir, rm, stat, readdir, readFile, chmod, access, writeFile, unlink } from 'fs/promises';
1
+ import crypto from 'crypto';
2
+ import { Wallet } from 'ethers';
3
+ import { mkdir, rm, stat, readdir, readFile, chmod, access, writeFile, unlink, rename } from 'fs/promises';
5
4
  import { homedir, platform } from 'os';
6
5
  import { join, resolve, dirname, basename } from 'path';
6
+ import { ensureConsent, computeConsentHash, loadCredentials, consentInputFromClient, registeredToolNames, MCP_VERSION } from '@haven_ai/mcp';
7
+ import { ensureSignerConsent, computeSignerConsentHash, loadSignerCredentials, createEdgeSigner, toolSchemas } from '@haven_ai/signer';
8
+ import { isSupportedNodeVersion, SKILL_FOLDER_NAME, resolveTokenFromAddress, HAVEN_SKILL_MD, HAVEN_SKILL_BODY_MD, HAVEN_MINIMUM_NODE_VERSION, unsupportedNodeVersionMessage } from '@haven_ai/sdk';
7
9
  import { parseDocument, isMap, stringify } from 'yaml';
8
- import { execFile, spawn } from 'child_process';
10
+ import { spawn, execFile } from 'child_process';
9
11
  import { promisify } from 'util';
10
- import crypto from 'crypto';
11
- import { Wallet } from 'ethers';
12
12
  import { createInterface } from 'readline';
13
13
  import { realpathSync } from 'fs';
14
14
  import { fileURLToPath, pathToFileURL } from 'url';
@@ -23,10 +23,132 @@ var __export = (target, all) => {
23
23
  __defProp(target, name, { get: all[name], enumerable: true });
24
24
  };
25
25
 
26
+ // src/api.ts
27
+ function createConnectApiClient(baseUrl, fetchImpl = fetch) {
28
+ const root = baseUrl.replace(/\/+$/, "");
29
+ return {
30
+ resolveSetup: (input) => request(fetchImpl, `${root}/agent-connection-setups/resolve`, {
31
+ method: "POST",
32
+ body: JSON.stringify({
33
+ setup_token: input.setupToken,
34
+ connector_version: input.connectorVersion,
35
+ runtime: input.runtime
36
+ })
37
+ }),
38
+ registerSetup: (input) => request(fetchImpl, `${root}/agent-connection-setups/register`, {
39
+ method: "POST",
40
+ body: JSON.stringify({
41
+ setup_token: input.setupToken,
42
+ challenge_id: input.challengeId,
43
+ delegate_address: input.delegateAddress,
44
+ proof_signature: input.proofSignature,
45
+ api_key_hash: input.apiKeyHash,
46
+ api_key_prefix: input.apiKeyPrefix,
47
+ runtime: input.runtime,
48
+ connector_version: input.connectorVersion,
49
+ mcp_server_name: input.mcpServerName,
50
+ connector_context: input.connectorContext,
51
+ install_capabilities: input.installCapabilities && {
52
+ can_write_runtime_config: input.installCapabilities.canWriteRuntimeConfig,
53
+ restart_required: input.installCapabilities.restartRequired
54
+ }
55
+ })
56
+ }),
57
+ getAgentIdentity: (apiKey) => request(fetchImpl, `${root}/machine-payments/agent`, {
58
+ method: "GET",
59
+ headers: { Authorization: `Bearer ${apiKey}` }
60
+ }),
61
+ getConnectorStatus: (setupId, apiKey) => request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/connector-status`, {
62
+ method: "GET",
63
+ headers: { Authorization: `Bearer ${apiKey}` }
64
+ }),
65
+ updateInstallStatus: async (setupId, apiKey, input) => {
66
+ await request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/install-status`, {
67
+ method: "POST",
68
+ headers: { Authorization: `Bearer ${apiKey}` },
69
+ body: JSON.stringify({
70
+ runtime: input.runtime,
71
+ connector_version: input.connectorVersion,
72
+ runtime_mcp_mode: input.runtimeMcpMode,
73
+ hosted_mcp_configured: input.hostedMcpConfigured,
74
+ local_signer_configured: input.localSignerConfigured,
75
+ local_mcp_configured: input.localMcpConfigured,
76
+ credential_files_written: input.credentialFilesWritten,
77
+ signer_acknowledged: input.signerAcknowledged,
78
+ local_mcp_acknowledged: input.localMcpAcknowledged,
79
+ activation_command_available: input.activationCommandAvailable,
80
+ skill_installed: input.skillInstalled,
81
+ probe_result: input.probeResult,
82
+ restart_required: input.restartRequired,
83
+ next_user_action: input.nextUserAction,
84
+ error_code: input.errorCode ?? null,
85
+ environment_label: input.environmentLabel
86
+ })
87
+ });
88
+ }
89
+ };
90
+ }
91
+ async function request(fetchImpl, url, init) {
92
+ const response = await fetchImpl(url, {
93
+ ...init,
94
+ headers: {
95
+ "Content-Type": "application/json",
96
+ ...init.headers ?? {}
97
+ }
98
+ });
99
+ const text = await response.text();
100
+ const body = text ? JSON.parse(text) : null;
101
+ if (!response.ok) {
102
+ const message = body?.error ?? body?.message ?? `${response.status} ${response.statusText}`;
103
+ throw new ConnectRequestError(`Haven setup request failed: ${message}`, response.status);
104
+ }
105
+ return body;
106
+ }
107
+ var ConnectRequestError;
108
+ var init_api = __esm({
109
+ "src/api.ts"() {
110
+ ConnectRequestError = class extends Error {
111
+ constructor(message, status) {
112
+ super(message);
113
+ this.status = status;
114
+ this.name = "ConnectRequestError";
115
+ }
116
+ status;
117
+ };
118
+ }
119
+ });
120
+ function generateDelegateKey() {
121
+ return delegateKeyFromPrivateKey(Wallet.createRandom().privateKey);
122
+ }
123
+ function delegateKeyFromPrivateKey(privateKey) {
124
+ const wallet = new Wallet(privateKey);
125
+ return {
126
+ privateKey: wallet.privateKey,
127
+ address: wallet.address,
128
+ signChallenge: (message) => wallet.signMessage(message)
129
+ };
130
+ }
131
+ function generateAgentApiKey() {
132
+ return `sk_agent_${crypto.randomBytes(24).toString("hex")}`;
133
+ }
134
+ function hashAgentApiKey(apiKey) {
135
+ return crypto.createHash("sha256").update(apiKey).digest("hex");
136
+ }
137
+ function agentApiKeyPrefix(apiKey) {
138
+ return apiKey.slice(0, 12);
139
+ }
140
+ var init_key = __esm({
141
+ "src/key.ts"() {
142
+ }
143
+ });
144
+
26
145
  // src/redact.ts
27
146
  function redactSecrets(value) {
28
147
  return value.replace(API_KEY_RE, "sk_agent_[redacted]").replace(PRIVATE_KEY_RE, "0x[redacted-private-key]");
29
148
  }
149
+ function redactForAutomation(value) {
150
+ return redactSecrets(value).replace(/(?:~|\/)[^\s`"']*\/(?:identity|signer|agent)\.json\b/g, "[credential-file-redacted]").replace(/(?:~|\/)[^\s`"']*\/\.env\b/g, "[credential-env-redacted]");
151
+ }
30
152
  function shortAddress(address) {
31
153
  if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return address;
32
154
  return `${address.slice(0, 6)}...${address.slice(-4)}`;
@@ -85,119 +207,415 @@ var init_server_names = __esm({
85
207
  SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
86
208
  }
87
209
  });
88
- function mcpPackageSpec() {
89
- return `${MCP_RUNTIME_MANIFEST.mcpPackage}@${MCP_RUNTIME_MANIFEST.mcpVersion}`;
90
- }
91
- function sdkPackageSpec() {
92
- return `${MCP_RUNTIME_MANIFEST.sdkPackage}@${MCP_RUNTIME_MANIFEST.sdkVersion}`;
93
- }
94
- function signerPackageSpec() {
95
- return `${MCP_RUNTIME_MANIFEST.signerPackage}@${MCP_RUNTIME_MANIFEST.signerVersion}`;
210
+ async function preflightCredentialStorage(input = {}) {
211
+ const directory = defaultCredentialRoot(input.baseDir);
212
+ await mkdir(directory, { recursive: true, mode: 448 });
213
+ await restrictPermissions(directory, 448, input.warn);
214
+ const probePath = join(directory, `.haven-connect-preflight-${crypto.randomBytes(8).toString("hex")}`);
215
+ try {
216
+ await writeOwnerOnlyJson(probePath, { ok: true }, input.warn);
217
+ } finally {
218
+ await rm(probePath, { force: true }).catch(() => void 0);
219
+ }
220
+ return directory;
96
221
  }
97
- var MCP_RUNTIME_MANIFEST;
98
- var init_runtime_manifest = __esm({
99
- "src/runtime-manifest.ts"() {
100
- MCP_RUNTIME_MANIFEST = {
101
- mcpPackage: "@haven_ai/mcp",
102
- mcpVersion: MCP_VERSION,
103
- sdkPackage: "@haven_ai/sdk",
104
- sdkVersion: "0.1.29-alpha.0",
105
- signerPackage: "@haven_ai/signer",
106
- signerVersion: "0.1.29-alpha.0",
107
- // Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
108
- // while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
109
- // so the guard that was supposed to enforce the floor waved Node v23 through
110
- // — including on the `--local` path where it does run. A hand-maintained
111
- // second copy of a number is a drift waiting to happen; a guard test pins
112
- // this against `package.json`'s `engines.node`.
113
- minimumNodeVersion: HAVEN_MINIMUM_NODE_VERSION,
114
- supportedClients: ["codex-cli", "codex-desktop", "claude-code"],
115
- requiredTools: registeredToolNames(),
116
- /**
117
- * The signer MCP's tool surface, DERIVED from the pinned @haven_ai/signer
118
- * package (#1587) — same anti-drift rule as `requiredTools` above: a
119
- * literal list here would rot the first time the signer gains a tool.
120
- * The handshake probe requires all of them.
121
- */
122
- requiredSignerTools: Object.keys(toolSchemas)
123
- };
222
+ async function writeCredentialFiles(input) {
223
+ const directory = defaultAgentDirectory(input.serverName ?? input.agentId, input.baseDir);
224
+ await mkdir(directory, { recursive: true, mode: 448 });
225
+ await restrictPermissions(directory, 448, input.warn);
226
+ const identityPath = join(directory, "identity.json");
227
+ const signerPath = join(directory, "signer.json");
228
+ const agentPath = join(directory, "agent.json");
229
+ await assertDoesNotExist(identityPath);
230
+ await assertDoesNotExist(signerPath);
231
+ await assertDoesNotExist(agentPath);
232
+ await writeOwnerOnlyJson(signerPath, signerPayload(input), input.warn);
233
+ try {
234
+ await writeOwnerOnlyJson(identityPath, identityPayload(input), input.warn);
235
+ } catch (err) {
236
+ await rm(signerPath, { force: true }).catch(() => void 0);
237
+ throw err;
124
238
  }
125
- });
126
- async function writeRuntimeConfig(input, deps = {}) {
127
- switch (input.runtime) {
128
- case "codex-cli":
129
- case "codex-desktop":
130
- return writeCodexConfig(input);
131
- case "cursor":
132
- return writeJsonRuntimeConfig(input, cursorConfigPath(input.homeDir), "mcpServers");
133
- case "vscode":
134
- return writeJsonRuntimeConfig(input, vscodeConfigPath(input.homeDir), "servers");
135
- case "vscode-insiders":
136
- return writeJsonRuntimeConfig(input, vscodeInsidersConfigPath(input.homeDir), "servers");
137
- case "claude-desktop":
138
- return writeJsonRuntimeConfig(input, claudeDesktopConfigPath(input.homeDir), "mcpServers");
139
- case "hermes":
140
- return writeHermesConfig(input, deps);
141
- default:
142
- return {
143
- hostedConfigured: false,
144
- signerConfigured: false,
145
- localMcpConfigured: false,
146
- runtimeMcpMode: "manual",
147
- target: "manual runtime setup",
148
- changed: false,
149
- restartRequired: true,
150
- messages: ["Runtime config needs to be added manually for this agent environment."],
151
- errorCode: "manual_runtime_setup_required"
152
- };
239
+ try {
240
+ await writeOwnerOnlyJson(agentPath, agentPayload(input), input.warn);
241
+ } catch (err) {
242
+ await rm(signerPath, { force: true }).catch(() => void 0);
243
+ await rm(identityPath, { force: true }).catch(() => void 0);
244
+ await rm(agentPath, { force: true }).catch(() => void 0);
245
+ throw err;
153
246
  }
247
+ return { directory, identityPath, signerPath, agentPath };
154
248
  }
155
- function buildHostedServer(hostedMcpUrl, apiKey, runtime) {
156
- if (runtime === "vscode" || runtime === "vscode-insiders") {
157
- return {
158
- type: "http",
159
- url: hostedMcpUrl,
160
- headers: { Authorization: `Bearer ${apiKey}` }
161
- };
162
- }
249
+ function signerPayload(input) {
163
250
  return {
164
- url: hostedMcpUrl,
165
- headers: { Authorization: `Bearer ${apiKey}` }
166
- };
167
- }
168
- function resolveSignerLaunchSpec(input) {
169
- return input.signerCommand ?? {
170
- command: "npx",
171
- args: ["-y", signerPackageSpec(), "--credentials", input.signerPath]
251
+ delegate_key: input.delegateKey,
252
+ delegate_address: input.delegateAddress,
253
+ agent_id: input.agentId,
254
+ safe_address: input.safeAddress,
255
+ chain_id: input.chainId,
256
+ network: input.network,
257
+ x402_binding_signer: input.x402BindingSigner,
258
+ note: "Local signer credential. Haven backend never receives this private key."
172
259
  };
173
260
  }
174
- function buildSignerServer(spec, runtime) {
175
- const server = {
176
- command: spec.command,
177
- args: spec.args
261
+ function identityPayload(input) {
262
+ return {
263
+ api_key: input.apiKey,
264
+ agent_id: input.agentId,
265
+ safe_address: input.safeAddress,
266
+ chain_id: input.chainId,
267
+ network: input.network,
268
+ api_url: input.apiUrl,
269
+ hosted_mcp_url: input.hostedMcpUrl,
270
+ agent_budget: input.agentBudget,
271
+ note: "Haven API key identifies the agent only. It cannot spend without the local signer key and on-chain Haven wallet rules."
178
272
  };
179
- if (runtime === "vscode" || runtime === "vscode-insiders") return { type: "stdio", ...server };
180
- return server;
181
273
  }
182
- function mergeJsonMcpConfig(existingJson, serverRoot, hostedServer, signerServer, names = serverNamesFor(), configPath) {
183
- const config = existingJson?.trim() ? parseJsonObject(existingJson, configPath) : {};
184
- const existingRoot = config[serverRoot];
185
- const servers = existingRoot && typeof existingRoot === "object" && !Array.isArray(existingRoot) ? existingRoot : {};
186
- config[serverRoot] = {
187
- ...servers,
188
- [names.hosted]: hostedServer,
189
- [names.signer]: signerServer
274
+ function agentPayload(input) {
275
+ return {
276
+ agent_id: input.agentId,
277
+ delegate_address: input.delegateAddress,
278
+ safe_address: input.safeAddress,
279
+ chain_id: input.chainId,
280
+ network: input.network,
281
+ agent_budget: input.agentBudget,
282
+ 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."
190
283
  };
191
- return `${JSON.stringify(config, null, 2)}
192
- `;
193
284
  }
194
- function mergeHermesYaml(existingYaml, hostedServer, signerServer, names = serverNamesFor(), configPath) {
195
- if (!existingYaml?.trim()) {
196
- return renderHermesYaml({ [names.hosted]: hostedServer, [names.signer]: signerServer });
285
+ async function readStoredCredentials(serverName, agentIdOrSlug, baseDir) {
286
+ const key = serverName ?? agentIdOrSlug;
287
+ const directory = key ? defaultAgentDirectory(key, baseDir) : await discoverSoleAgentDirectory(baseDir);
288
+ const identity = await readJsonFile(join(directory, "identity.json"));
289
+ if (!identity) {
290
+ throw new Error(
291
+ `No Haven credentials at ${directory}. Nothing to re-key \u2014 connect this agent first, or pass the --name you wired it under.`
292
+ );
197
293
  }
198
- const doc = parseDocument(existingYaml, { keepSourceTokens: true });
199
- if (doc.errors.length > 0 || !isMap(doc.contents)) {
200
- throw new UnreadableRuntimeConfigError(configPath ?? "the Hermes config", "it is not a YAML object");
294
+ const agent = await readJsonFile(join(directory, "agent.json")) ?? {};
295
+ const signer = await readJsonFile(join(directory, "signer.json")) ?? {};
296
+ const agentId = asString(identity.agent_id);
297
+ const apiKey = asString(identity.api_key);
298
+ const apiUrl = asString(identity.api_url);
299
+ const hostedMcpUrl = asString(identity.hosted_mcp_url);
300
+ if (!agentId || !apiKey || !apiUrl || !hostedMcpUrl) {
301
+ throw new Error(
302
+ `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.`
303
+ );
304
+ }
305
+ return {
306
+ directory,
307
+ agentId,
308
+ apiKey,
309
+ apiUrl,
310
+ hostedMcpUrl,
311
+ delegateAddress: asString(agent.delegate_address) ?? asString(signer.delegate_address),
312
+ safeAddress: asString(identity.safe_address) ?? asString(agent.safe_address),
313
+ chainId: typeof identity.chain_id === "number" ? identity.chain_id : void 0,
314
+ network: asString(identity.network),
315
+ x402BindingSigner: asString(signer.x402_binding_signer),
316
+ agentBudget: Array.isArray(identity.agent_budget) ? identity.agent_budget : void 0
317
+ };
318
+ }
319
+ async function discoverSoleAgentDirectory(baseDir) {
320
+ const root = defaultCredentialRoot(baseDir);
321
+ let entries = [];
322
+ try {
323
+ entries = await readdir(root);
324
+ } catch {
325
+ throw new Error(`No Haven credentials found under ${root}. Connect an agent on this machine first.`);
326
+ }
327
+ const candidates = [];
328
+ for (const entry of entries) {
329
+ const directory = join(root, entry);
330
+ if (!await readJsonFile(join(directory, "identity.json"))) continue;
331
+ if (await readJsonFile(join(directory, "TOMBSTONE.json"))) continue;
332
+ candidates.push(directory);
333
+ }
334
+ if (candidates.length === 1) return candidates[0];
335
+ if (candidates.length === 0) {
336
+ throw new Error(`No Haven credentials found under ${root}. Connect an agent on this machine first.`);
337
+ }
338
+ throw new Error(
339
+ `Several agents are wired on this machine, so --rekey cannot tell which one you mean:
340
+ ` + candidates.map((d) => ` ${d}`).join("\n") + "\nRe-run with --name <slug> to pick one."
341
+ );
342
+ }
343
+ async function rewriteCredentialFiles(input) {
344
+ const directory = defaultAgentDirectory(input.serverName ?? input.agentId, input.baseDir);
345
+ const identityPath = join(directory, "identity.json");
346
+ const signerPath = join(directory, "signer.json");
347
+ const agentPath = join(directory, "agent.json");
348
+ const targets = [
349
+ { path: signerPath, payload: signerPayload(input) },
350
+ { path: identityPath, payload: identityPayload(input) },
351
+ { path: agentPath, payload: agentPayload(input) }
352
+ ];
353
+ const originals = /* @__PURE__ */ new Map();
354
+ for (const { path } of targets) {
355
+ originals.set(path, await readRawFile(path));
356
+ }
357
+ const temps = [];
358
+ try {
359
+ for (const { path, payload } of targets) {
360
+ const temp = `${path}.rekey-${crypto.randomBytes(6).toString("hex")}.tmp`;
361
+ await writeOwnerOnlyJson(temp, payload, input.warn);
362
+ temps.push({ from: temp, to: path });
363
+ }
364
+ for (const { from, to } of temps) {
365
+ await rename(from, to);
366
+ }
367
+ } catch (err) {
368
+ for (const { from } of temps) await rm(from, { force: true }).catch(() => void 0);
369
+ for (const [path, contents] of originals) {
370
+ if (contents === null) {
371
+ await rm(path, { force: true }).catch(() => void 0);
372
+ } else {
373
+ await writeFile(path, contents, { mode: 384 }).catch(() => void 0);
374
+ }
375
+ }
376
+ throw err;
377
+ }
378
+ return { directory, identityPath, signerPath, agentPath };
379
+ }
380
+ async function writeRekeyPending(directory, pending, warn) {
381
+ const path = join(directory, REKEY_PENDING_FILENAME);
382
+ await rm(path, { force: true }).catch(() => void 0);
383
+ await writeOwnerOnlyJson(path, { ...pending }, warn);
384
+ return path;
385
+ }
386
+ async function readRekeyPending(directory, now = Date.now()) {
387
+ const path = join(directory, REKEY_PENDING_FILENAME);
388
+ const raw = await readJsonFile(path);
389
+ if (!raw) {
390
+ throw new Error(
391
+ `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.`
392
+ );
393
+ }
394
+ const pending = raw;
395
+ if (!pending.new_delegate_key || !pending.new_delegate_address || !pending.agent_id) {
396
+ throw new Error(`The pending re-key at ${path} is unreadable. Delete it and start again with --rekey.`);
397
+ }
398
+ if (pending.expires_at && Date.parse(pending.expires_at) < now) {
399
+ throw new Error(
400
+ `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.`
401
+ );
402
+ }
403
+ return pending;
404
+ }
405
+ async function inspectRekeyPending(directory, now = Date.now()) {
406
+ const path = join(directory, REKEY_PENDING_FILENAME);
407
+ const raw = await readJsonFile(path);
408
+ if (!raw) {
409
+ const present = await readRawFile(path) !== null;
410
+ return present ? { state: "unreadable", path } : null;
411
+ }
412
+ const agentId = asString(raw.agent_id);
413
+ const newDelegateAddress = asString(raw.new_delegate_address);
414
+ const startedAt = asString(raw.started_at);
415
+ const expiresAt = asString(raw.expires_at);
416
+ if (!agentId || !newDelegateAddress) {
417
+ return { state: "unreadable", path, ...startedAt ? { startedAt } : {} };
418
+ }
419
+ const expired = expiresAt !== void 0 && Date.parse(expiresAt) < now;
420
+ return {
421
+ state: expired ? "expired" : "pending",
422
+ path,
423
+ agentId,
424
+ newDelegateAddress,
425
+ ...startedAt ? { startedAt } : {},
426
+ ...expiresAt ? { expiresAt } : {}
427
+ };
428
+ }
429
+ async function clearRekeyPending(directory) {
430
+ await rm(join(directory, REKEY_PENDING_FILENAME), { force: true }).catch(() => void 0);
431
+ }
432
+ async function readJsonFile(path) {
433
+ const raw = await readRawFile(path);
434
+ if (raw === null) return null;
435
+ try {
436
+ const parsed = JSON.parse(raw);
437
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
438
+ } catch {
439
+ return null;
440
+ }
441
+ }
442
+ async function readRawFile(path) {
443
+ try {
444
+ return await readFile(path, "utf8");
445
+ } catch {
446
+ return null;
447
+ }
448
+ }
449
+ function asString(value) {
450
+ return typeof value === "string" && value.length > 0 ? value : void 0;
451
+ }
452
+ async function assertServerSlugAvailable(serverName, baseDir) {
453
+ const directory = defaultAgentDirectory(serverName, baseDir);
454
+ try {
455
+ await stat(join(directory, "identity.json"));
456
+ } catch {
457
+ return;
458
+ }
459
+ throw new Error(
460
+ `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.`
461
+ );
462
+ }
463
+ function defaultAgentDirectory(agentId, baseDir = join(homedir(), ".haven", "agents")) {
464
+ return resolve(defaultCredentialRoot(baseDir), safePathPart(agentId));
465
+ }
466
+ function defaultCredentialRoot(baseDir = join(homedir(), ".haven", "agents")) {
467
+ return resolve(baseDir);
468
+ }
469
+ async function writeOwnerOnlyJson(path, value, warn) {
470
+ const json = JSON.stringify(dropUndefined(value), null, 2);
471
+ await writeFile(path, `${json}
472
+ `, { mode: 384, flag: "wx" });
473
+ await restrictPermissions(path, 384, warn);
474
+ }
475
+ function safePathPart(value) {
476
+ return value.replace(/[^A-Za-z0-9_.-]/g, "_");
477
+ }
478
+ function dropUndefined(value) {
479
+ return Object.fromEntries(Object.entries(value).filter(([, field]) => field !== void 0));
480
+ }
481
+ async function assertDoesNotExist(path) {
482
+ try {
483
+ await access(path);
484
+ } catch (err) {
485
+ if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") return;
486
+ throw err;
487
+ }
488
+ throw new Error(`Refusing to overwrite existing Haven credential file: ${path}`);
489
+ }
490
+ async function restrictPermissions(path, mode, warn) {
491
+ try {
492
+ await chmod(path, mode);
493
+ } catch (err) {
494
+ warn?.(
495
+ `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)}`
496
+ );
497
+ }
498
+ }
499
+ var REKEY_PENDING_FILENAME, REKEY_PENDING_TTL_MS;
500
+ var init_storage = __esm({
501
+ "src/storage.ts"() {
502
+ REKEY_PENDING_FILENAME = "rekey-pending.json";
503
+ REKEY_PENDING_TTL_MS = 24 * 60 * 60 * 1e3;
504
+ }
505
+ });
506
+ function mcpPackageSpec() {
507
+ return `${MCP_RUNTIME_MANIFEST.mcpPackage}@${MCP_RUNTIME_MANIFEST.mcpVersion}`;
508
+ }
509
+ function sdkPackageSpec() {
510
+ return `${MCP_RUNTIME_MANIFEST.sdkPackage}@${MCP_RUNTIME_MANIFEST.sdkVersion}`;
511
+ }
512
+ function signerPackageSpec() {
513
+ return `${MCP_RUNTIME_MANIFEST.signerPackage}@${MCP_RUNTIME_MANIFEST.signerVersion}`;
514
+ }
515
+ var MCP_RUNTIME_MANIFEST;
516
+ var init_runtime_manifest = __esm({
517
+ "src/runtime-manifest.ts"() {
518
+ MCP_RUNTIME_MANIFEST = {
519
+ mcpPackage: "@haven_ai/mcp",
520
+ mcpVersion: MCP_VERSION,
521
+ sdkPackage: "@haven_ai/sdk",
522
+ sdkVersion: "0.1.31-alpha.0",
523
+ signerPackage: "@haven_ai/signer",
524
+ signerVersion: "0.1.31-alpha.0",
525
+ // Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
526
+ // while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
527
+ // so the guard that was supposed to enforce the floor waved Node v23 through
528
+ // — including on the `--local` path where it does run. A hand-maintained
529
+ // second copy of a number is a drift waiting to happen; a guard test pins
530
+ // this against `package.json`'s `engines.node`.
531
+ minimumNodeVersion: HAVEN_MINIMUM_NODE_VERSION,
532
+ supportedClients: ["codex-cli", "codex-desktop", "claude-code"],
533
+ requiredTools: registeredToolNames(),
534
+ /**
535
+ * The signer MCP's tool surface, DERIVED from the pinned @haven_ai/signer
536
+ * package (#1587) — same anti-drift rule as `requiredTools` above: a
537
+ * literal list here would rot the first time the signer gains a tool.
538
+ * The handshake probe requires all of them.
539
+ */
540
+ requiredSignerTools: Object.keys(toolSchemas)
541
+ };
542
+ }
543
+ });
544
+ async function writeRuntimeConfig(input, deps = {}) {
545
+ switch (input.runtime) {
546
+ case "codex-cli":
547
+ case "codex-desktop":
548
+ return writeCodexConfig(input);
549
+ case "cursor":
550
+ return writeJsonRuntimeConfig(input, cursorConfigPath(input.homeDir), "mcpServers");
551
+ case "vscode":
552
+ return writeJsonRuntimeConfig(input, vscodeConfigPath(input.homeDir), "servers");
553
+ case "vscode-insiders":
554
+ return writeJsonRuntimeConfig(input, vscodeInsidersConfigPath(input.homeDir), "servers");
555
+ case "claude-desktop":
556
+ return writeJsonRuntimeConfig(input, claudeDesktopConfigPath(input.homeDir), "mcpServers");
557
+ case "hermes":
558
+ return writeHermesConfig(input, deps);
559
+ default:
560
+ return {
561
+ hostedConfigured: false,
562
+ signerConfigured: false,
563
+ localMcpConfigured: false,
564
+ runtimeMcpMode: "manual",
565
+ target: "manual runtime setup",
566
+ changed: false,
567
+ restartRequired: true,
568
+ messages: ["Runtime config needs to be added manually for this agent environment."],
569
+ errorCode: "manual_runtime_setup_required"
570
+ };
571
+ }
572
+ }
573
+ function buildHostedServer(hostedMcpUrl, apiKey, runtime) {
574
+ if (runtime === "vscode" || runtime === "vscode-insiders") {
575
+ return {
576
+ type: "http",
577
+ url: hostedMcpUrl,
578
+ headers: { Authorization: `Bearer ${apiKey}` }
579
+ };
580
+ }
581
+ return {
582
+ url: hostedMcpUrl,
583
+ headers: { Authorization: `Bearer ${apiKey}` }
584
+ };
585
+ }
586
+ function resolveSignerLaunchSpec(input) {
587
+ return input.signerCommand ?? {
588
+ command: "npx",
589
+ args: ["-y", signerPackageSpec(), "--credentials", input.signerPath]
590
+ };
591
+ }
592
+ function buildSignerServer(spec, runtime) {
593
+ const server = {
594
+ command: spec.command,
595
+ args: spec.args
596
+ };
597
+ if (runtime === "vscode" || runtime === "vscode-insiders") return { type: "stdio", ...server };
598
+ return server;
599
+ }
600
+ function mergeJsonMcpConfig(existingJson, serverRoot, hostedServer, signerServer, names = serverNamesFor(), configPath) {
601
+ const config = existingJson?.trim() ? parseJsonObject(existingJson, configPath) : {};
602
+ const existingRoot = config[serverRoot];
603
+ const servers = existingRoot && typeof existingRoot === "object" && !Array.isArray(existingRoot) ? existingRoot : {};
604
+ config[serverRoot] = {
605
+ ...servers,
606
+ [names.hosted]: hostedServer,
607
+ [names.signer]: signerServer
608
+ };
609
+ return `${JSON.stringify(config, null, 2)}
610
+ `;
611
+ }
612
+ function mergeHermesYaml(existingYaml, hostedServer, signerServer, names = serverNamesFor(), configPath) {
613
+ if (!existingYaml?.trim()) {
614
+ return renderHermesYaml({ [names.hosted]: hostedServer, [names.signer]: signerServer });
615
+ }
616
+ const doc = parseDocument(existingYaml, { keepSourceTokens: true });
617
+ if (doc.errors.length > 0 || !isMap(doc.contents)) {
618
+ throw new UnreadableRuntimeConfigError(configPath ?? "the Hermes config", "it is not a YAML object");
201
619
  }
202
620
  const mcpPair = doc.contents.items.find((item) => item.key?.toString() === "mcp_servers");
203
621
  const existingServers = mcpPair && isMap(mcpPair.value) ? mcpPair.value.toJSON() : {};
@@ -844,27 +1262,107 @@ var init_config_writers = __esm({
844
1262
  };
845
1263
  }
846
1264
  });
847
- async function probeHostedAgentIdentity(apiKey, apiUrl, fetchImpl = fetch) {
848
- let response;
1265
+ async function acknowledgeLocalMcpConsent(identityPath, signerPath, log) {
849
1266
  try {
850
- response = await fetchWithTimeout(fetchImpl, `${apiUrl.replace(/\/+$/, "")}/machine-payments/agent`, {
851
- method: "GET",
852
- headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" }
1267
+ const input = await buildLocalMcpConsentInput(identityPath, signerPath);
1268
+ const decision = await ensureConsent(input, {
1269
+ credentialsPath: identityPath,
1270
+ writeAck: true,
1271
+ out: log ? { write: (chunk) => writeLogChunk(log, chunk) } : void 0
853
1272
  });
854
- } catch {
855
- return { status: "network_error" };
856
- }
857
- if (response.status === 401 || response.status === 403) return { status: "unauthorized" };
858
- if (!response.ok) return { status: "bad_response" };
859
- try {
860
- const payload = JSON.parse(await response.text());
861
- if (typeof payload?.delegate_address !== "string") return { status: "bad_response" };
862
- return { status: "ok", agentId: payload.id, delegateAddress: payload.delegate_address };
863
- } catch {
864
- return { status: "bad_response" };
1273
+ return {
1274
+ acknowledged: decision.ok,
1275
+ hash: decision.hash,
1276
+ reason: decision.reason
1277
+ };
1278
+ } catch (err) {
1279
+ return {
1280
+ acknowledged: false,
1281
+ error: err instanceof Error ? err.message : String(err)
1282
+ };
865
1283
  }
866
1284
  }
867
- async function probeHostedMcpTools(apiKey, hostedMcpUrl, fetchImpl = fetch) {
1285
+ async function getLocalMcpConsentStatus(identityPath, signerPath) {
1286
+ try {
1287
+ const input = await buildLocalMcpConsentInput(identityPath, signerPath);
1288
+ const hash = computeConsentHash(input);
1289
+ const stored = await readLocalMcpAckFile(localMcpAckPath(identityPath));
1290
+ if (stored === hash) {
1291
+ return { acknowledged: true, hash, reason: "ack_file_match" };
1292
+ }
1293
+ return {
1294
+ acknowledged: false,
1295
+ hash,
1296
+ reason: stored ? "ack_file_mismatch" : "ack_file_missing"
1297
+ };
1298
+ } catch (err) {
1299
+ return {
1300
+ acknowledged: false,
1301
+ error: err instanceof Error ? err.message : String(err)
1302
+ };
1303
+ }
1304
+ }
1305
+ function localMcpAckPath(identityPath) {
1306
+ return resolve(`${identityPath}.ack.json`);
1307
+ }
1308
+ async function buildLocalMcpConsentInput(identityPath, signerPath) {
1309
+ const credentials = await loadCredentials({ identityPath, signerPath });
1310
+ const unavailableDuringSetup = {
1311
+ getAllowances: async () => {
1312
+ throw new Error("Haven approval is not complete yet.");
1313
+ }
1314
+ };
1315
+ return consentInputFromClient(
1316
+ unavailableDuringSetup,
1317
+ {
1318
+ apiKey: credentials.apiKey,
1319
+ apiUrl: credentials.apiUrl,
1320
+ agentId: credentials.agentId,
1321
+ safeAddress: credentials.safeAddress,
1322
+ delegateAddress: credentials.delegateAddress,
1323
+ chainId: credentials.chainId,
1324
+ allowanceSummary: credentials.allowanceSummary
1325
+ },
1326
+ registeredToolNames()
1327
+ );
1328
+ }
1329
+ async function readLocalMcpAckFile(path) {
1330
+ try {
1331
+ const parsed = JSON.parse(await readFile(path, "utf8"));
1332
+ return typeof parsed.ack === "string" ? parsed.ack : null;
1333
+ } catch {
1334
+ return null;
1335
+ }
1336
+ }
1337
+ function writeLogChunk(log, chunk) {
1338
+ const message = String(chunk).trimEnd();
1339
+ if (message) log(message);
1340
+ }
1341
+ var init_local_mcp_consent = __esm({
1342
+ "src/local-mcp-consent.ts"() {
1343
+ }
1344
+ });
1345
+ async function probeHostedAgentIdentity(apiKey, apiUrl, fetchImpl = fetch) {
1346
+ let response;
1347
+ try {
1348
+ response = await fetchWithTimeout(fetchImpl, `${apiUrl.replace(/\/+$/, "")}/machine-payments/agent`, {
1349
+ method: "GET",
1350
+ headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" }
1351
+ });
1352
+ } catch {
1353
+ return { status: "network_error" };
1354
+ }
1355
+ if (response.status === 401 || response.status === 403) return { status: "unauthorized" };
1356
+ if (!response.ok) return { status: "bad_response" };
1357
+ try {
1358
+ const payload = JSON.parse(await response.text());
1359
+ if (typeof payload?.delegate_address !== "string") return { status: "bad_response" };
1360
+ return { status: "ok", agentId: payload.id, delegateAddress: payload.delegate_address };
1361
+ } catch {
1362
+ return { status: "bad_response" };
1363
+ }
1364
+ }
1365
+ async function probeHostedMcpTools(apiKey, hostedMcpUrl, fetchImpl = fetch) {
868
1366
  let response;
869
1367
  try {
870
1368
  response = await fetchWithTimeout(fetchImpl, hostedMcpUrl, {
@@ -1159,81 +1657,361 @@ var init_signer_runtime = __esm({
1159
1657
  SIGNER_INSTALL_HEARTBEAT_MS = 15e3;
1160
1658
  }
1161
1659
  });
1162
-
1163
- // src/connect-error.ts
1164
- function isConnectError(err) {
1165
- return err instanceof ConnectError;
1166
- }
1167
- var ConnectError;
1168
- var init_connect_error = __esm({
1169
- "src/connect-error.ts"() {
1170
- ConnectError = class extends Error {
1171
- code;
1172
- nextAction;
1173
- constructor(code, message, nextAction2) {
1174
- super(message);
1175
- this.name = "ConnectError";
1176
- this.code = code;
1177
- this.nextAction = nextAction2;
1178
- }
1179
- };
1660
+ async function prepareLocalMcpRuntime(input, deps = {}) {
1661
+ assertSupportedNodeVersion(input.nodeVersion);
1662
+ const homeDir = input.homeDir ?? homedir();
1663
+ const runtimeDirectory = resolve(homeDir, ".haven", "mcp-runtime", MCP_RUNTIME_MANIFEST.mcpVersion);
1664
+ const npmCacheDirectory = resolve(homeDir, ".haven", "npm-cache");
1665
+ const cliPath = join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "dist", "cli.js");
1666
+ const messages = [];
1667
+ await mkdir(runtimeDirectory, { recursive: true, mode: 448 });
1668
+ await chmod(runtimeDirectory, 448).catch(() => void 0);
1669
+ await mkdir(npmCacheDirectory, { recursive: true, mode: 448 });
1670
+ await chmod(npmCacheDirectory, 448).catch(() => void 0);
1671
+ if (await installedRuntimeMatches2(runtimeDirectory, cliPath)) {
1672
+ messages.push(`Using existing local Haven MCP runtime ${mcpPackageSpec()}.`);
1673
+ } else {
1674
+ await installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps);
1675
+ messages.push(`Installed local Haven MCP runtime ${mcpPackageSpec()}.`);
1180
1676
  }
1181
- });
1182
-
1183
- // src/runtime-registry.ts
1184
- function runtimeProfile(runtime, env = process.env) {
1185
- return RUNTIME_PROFILES[normalizeRuntime(runtime, env)];
1677
+ await assertFileExists2(cliPath, "local Haven MCP CLI");
1678
+ const wrapperPath = join(input.credentialDirectory, "bin", "haven-mcp");
1679
+ await writeWrapper2({
1680
+ wrapperPath,
1681
+ cliPath,
1682
+ identityPath: input.identityPath,
1683
+ signerPath: input.signerPath
1684
+ });
1685
+ await writeRuntimeSidecar2({
1686
+ path: join(input.credentialDirectory, "mcp-runtime.json"),
1687
+ wrapperPath,
1688
+ runtimeDirectory,
1689
+ npmCacheDirectory,
1690
+ cliPath,
1691
+ serverName: input.serverName
1692
+ });
1693
+ messages.push(`Prepared stable local Haven MCP wrapper: ${wrapperPath}`);
1694
+ return {
1695
+ command: wrapperPath,
1696
+ args: [],
1697
+ wrapperPath,
1698
+ runtimeDirectory,
1699
+ npmCacheDirectory,
1700
+ cliPath,
1701
+ messages
1702
+ };
1186
1703
  }
1187
- function normalizeRuntime(runtime, env = process.env) {
1188
- const explicit = normalizeRuntimeName(runtime);
1189
- if (explicit) return explicit;
1190
- return detectRuntime(env) ?? "other";
1704
+ function assertSupportedNodeVersion(nodeVersion = process.versions.node, minimumNodeVersion = MCP_RUNTIME_MANIFEST.minimumNodeVersion, subject = "Haven setup") {
1705
+ if (!isSupportedNodeVersion(nodeVersion, minimumNodeVersion)) {
1706
+ throw new UnsupportedNodeVersionError(nodeVersion, minimumNodeVersion, subject);
1707
+ }
1191
1708
  }
1192
- async function resolveRuntimeSelection(explicit, force, options = {}) {
1193
- const env = options.env ?? process.env;
1194
- if (force !== void 0) {
1195
- const forced = normalizeRuntimeName(force);
1196
- if (!forced) {
1197
- throw new ConnectError(
1198
- "runtime_force_unrecognized",
1199
- `Unknown --runtime-force value "${force}". Valid values: ${RUNTIME_FLAG_VALUES}.`,
1200
- "rerun_connect_with_a_valid_runtime_name"
1201
- );
1709
+ async function installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps) {
1710
+ const { runCommand, onProgress } = deps;
1711
+ const baseArgs = [
1712
+ "install",
1713
+ "--prefix",
1714
+ runtimeDirectory,
1715
+ "--no-audit",
1716
+ "--no-fund",
1717
+ "--omit=dev",
1718
+ "--prefer-offline",
1719
+ mcpPackageSpec(),
1720
+ sdkPackageSpec()
1721
+ ];
1722
+ const run = async (args) => {
1723
+ const startedAt = Date.now();
1724
+ const heartbeat = setInterval(() => {
1725
+ const seconds = Math.round((Date.now() - startedAt) / 1e3);
1726
+ onProgress?.(`Still installing the local Haven MCP runtime\u2026 (${seconds}s \u2014 a cold cache can take several minutes)`);
1727
+ }, SIGNER_INSTALL_HEARTBEAT_MS);
1728
+ heartbeat.unref?.();
1729
+ try {
1730
+ if (runCommand) await runCommand("npm", args);
1731
+ else await execFileAsync2("npm", args, { timeout: SIGNER_INSTALL_TIMEOUT_MS, maxBuffer: 1024 * 1024 });
1732
+ } finally {
1733
+ clearInterval(heartbeat);
1202
1734
  }
1203
- return { runtime: forced, source: "force" };
1204
- }
1205
- const detected = detectRuntime(env);
1206
- const supplied = explicit?.trim() || options.selfReported?.trim() || void 0;
1207
- const hint = normalizeRuntimeName(supplied);
1208
- if (supplied && !hint) {
1209
- if (!detected) {
1210
- throw new ConnectError(
1211
- "runtime_unrecognized",
1212
- `"${supplied}" is not an agent runtime Haven knows. Valid values: ${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.`,
1213
- "rerun_connect_with_a_valid_runtime_name"
1214
- );
1735
+ };
1736
+ try {
1737
+ await run(baseArgs);
1738
+ } catch {
1739
+ try {
1740
+ await run([...baseArgs, "--cache", npmCacheDirectory]);
1741
+ } catch (err) {
1742
+ throw new Error(`Could not install local Haven MCP runtime ${mcpPackageSpec()}: ${err instanceof Error ? err.message : String(err)}`);
1215
1743
  }
1216
- return { runtime: detected, source: "detected", discardedHint: supplied };
1217
- }
1218
- if (detected && hint && detected !== hint) {
1219
- return { runtime: detected, source: "detected", overrodeHint: hint };
1220
1744
  }
1221
- if (hint) return { runtime: hint, source: "explicit" };
1222
- if (detected) return { runtime: detected, source: "detected" };
1223
- if (options.promptForRuntime) {
1224
- return { runtime: await options.promptForRuntime(), source: "prompted" };
1745
+ }
1746
+ async function installedRuntimeMatches2(runtimeDirectory, cliPath) {
1747
+ try {
1748
+ await assertFileExists2(cliPath, "local Haven MCP CLI");
1749
+ const [mcpPackage, sdkPackage] = await Promise.all([
1750
+ readPackageJson2(join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "package.json")),
1751
+ readPackageJson2(join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
1752
+ ]);
1753
+ return mcpPackage.version === MCP_RUNTIME_MANIFEST.mcpVersion && sdkPackage.version === MCP_RUNTIME_MANIFEST.sdkVersion;
1754
+ } catch {
1755
+ return false;
1225
1756
  }
1226
- return { runtime: null, source: "none" };
1227
1757
  }
1228
- function restartRequiredForRuntime(runtime, env = process.env) {
1229
- const mode = runtimeProfile(runtime, env).restartMode;
1230
- return mode === "restart-session" || mode === "restart-app";
1758
+ async function readPackageJson2(path) {
1759
+ return JSON.parse(await readFile(path, "utf8"));
1231
1760
  }
1232
- function runtimeVerificationInstruction(runtime) {
1233
- const label = RUNTIME_PROFILES[runtime].label;
1234
- 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.`;
1761
+ async function writeWrapper2(input) {
1762
+ await mkdir(dirname(input.wrapperPath), { recursive: true, mode: 448 });
1763
+ await chmod(dirname(input.wrapperPath), 448).catch(() => void 0);
1764
+ const source = [
1765
+ "#!/usr/bin/env node",
1766
+ "import { spawn } from 'node:child_process'",
1767
+ "",
1768
+ `const cliPath = ${JSON.stringify(input.cliPath)}`,
1769
+ `const identityPath = ${JSON.stringify(input.identityPath)}`,
1770
+ `const signerPath = ${JSON.stringify(input.signerPath)}`,
1771
+ "",
1772
+ "const child = spawn(process.execPath, [cliPath, '--identity', identityPath, '--signer', signerPath, ...process.argv.slice(2)], {",
1773
+ " stdio: 'inherit',",
1774
+ "})",
1775
+ "",
1776
+ "child.on('exit', (code, signal) => {",
1777
+ " if (signal) process.kill(process.pid, signal)",
1778
+ " else process.exit(code ?? 1)",
1779
+ "})",
1780
+ ""
1781
+ ].join("\n");
1782
+ await writeFile(input.wrapperPath, source, { mode: 448 });
1783
+ await chmod(input.wrapperPath, 448).catch(() => void 0);
1235
1784
  }
1236
- function normalizeRuntimeName(runtime) {
1785
+ async function writeRuntimeSidecar2(input) {
1786
+ const value = {
1787
+ ...input.serverName ? { server_name: input.serverName } : {},
1788
+ mcp_package: MCP_RUNTIME_MANIFEST.mcpPackage,
1789
+ mcp_version: MCP_RUNTIME_MANIFEST.mcpVersion,
1790
+ sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
1791
+ sdk_version: MCP_RUNTIME_MANIFEST.sdkVersion,
1792
+ minimum_node_version: MCP_RUNTIME_MANIFEST.minimumNodeVersion,
1793
+ wrapper_path: input.wrapperPath,
1794
+ runtime_directory: input.runtimeDirectory,
1795
+ npm_cache_directory: input.npmCacheDirectory,
1796
+ cli_path: input.cliPath
1797
+ };
1798
+ await writeFile(input.path, `${JSON.stringify(value, null, 2)}
1799
+ `, { mode: 384 });
1800
+ await chmod(input.path, 384).catch(() => void 0);
1801
+ }
1802
+ async function assertFileExists2(path, label) {
1803
+ try {
1804
+ await access(path);
1805
+ } catch {
1806
+ throw new Error(`Missing ${label}: ${path}`);
1807
+ }
1808
+ }
1809
+ var execFileAsync2, UnsupportedNodeVersionError;
1810
+ var init_local_mcp_runtime = __esm({
1811
+ "src/local-mcp-runtime.ts"() {
1812
+ init_signer_runtime();
1813
+ init_runtime_manifest();
1814
+ execFileAsync2 = promisify(execFile);
1815
+ UnsupportedNodeVersionError = class extends Error {
1816
+ code = "local_mcp_unsupported_node_version";
1817
+ nodeVersion;
1818
+ minimumNodeVersion;
1819
+ constructor(nodeVersion, minimumNodeVersion, subject = "Haven setup") {
1820
+ super(unsupportedNodeVersionMessage({ subject, nodeVersion, minimumNodeVersion }));
1821
+ this.name = "UnsupportedNodeVersionError";
1822
+ this.nodeVersion = nodeVersion;
1823
+ this.minimumNodeVersion = minimumNodeVersion;
1824
+ }
1825
+ };
1826
+ }
1827
+ });
1828
+ async function installSkillForRuntime(runtime, deps = {}) {
1829
+ switch (runtime) {
1830
+ case "claude-code":
1831
+ return installSkillFile(
1832
+ resolve(deps.homeDir ?? homedir(), ".claude", "skills", SKILL_FOLDER_NAME),
1833
+ "~/.claude/skills/haven-pay"
1834
+ );
1835
+ case "hermes":
1836
+ return installSkillFile(
1837
+ join(hermesHome(deps), "skills", SKILL_FOLDER_NAME),
1838
+ "the Hermes skills folder"
1839
+ );
1840
+ case "codex-cli":
1841
+ case "codex-desktop":
1842
+ return installCodexAgentsSection(deps);
1843
+ default:
1844
+ return void 0;
1845
+ }
1846
+ }
1847
+ async function installSkillFile(skillDir, label) {
1848
+ try {
1849
+ await mkdir(skillDir, { recursive: true });
1850
+ const target = join(skillDir, "SKILL.md");
1851
+ await writeFile(target, HAVEN_SKILL_MD, "utf8");
1852
+ return {
1853
+ installed: true,
1854
+ target,
1855
+ messages: [`Installed the generic Haven payment skill (${label}). It contains no secrets.`]
1856
+ };
1857
+ } catch (err) {
1858
+ return {
1859
+ installed: false,
1860
+ messages: [
1861
+ `Could not install the Haven payment skill: ${err instanceof Error ? err.message : String(err)}. Download it from the Haven dashboard instead.`
1862
+ ]
1863
+ };
1864
+ }
1865
+ }
1866
+ async function installCodexAgentsSection(deps) {
1867
+ try {
1868
+ const codexDir = resolve(deps.homeDir ?? homedir(), ".codex");
1869
+ const target = join(codexDir, "AGENTS.md");
1870
+ await mkdir(codexDir, { recursive: true });
1871
+ const existing = await readFile(target, "utf8").catch(() => null);
1872
+ const next = upsertManagedSection(existing, codexManagedSection());
1873
+ if (next !== existing) {
1874
+ await writeFile(target, next, "utf8");
1875
+ }
1876
+ return {
1877
+ installed: true,
1878
+ target,
1879
+ messages: [
1880
+ "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."
1881
+ ]
1882
+ };
1883
+ } catch (err) {
1884
+ return {
1885
+ installed: false,
1886
+ messages: [
1887
+ `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.`
1888
+ ]
1889
+ };
1890
+ }
1891
+ }
1892
+ function codexManagedSection() {
1893
+ return `${CODEX_AGENTS_BEGIN_MARKER}
1894
+
1895
+ ${HAVEN_SKILL_BODY_MD.trimEnd()}
1896
+
1897
+ ${CODEX_AGENTS_END_MARKER}
1898
+ `;
1899
+ }
1900
+ function upsertManagedSection(existing, section) {
1901
+ if (existing === null || existing.trim() === "") return section;
1902
+ const begins = markerLineIndexes(existing, CODEX_AGENTS_BEGIN_MARKER);
1903
+ const ends = markerLineIndexes(existing, CODEX_AGENTS_END_MARKER);
1904
+ if (begins.length === 1 && ends.length === 1 && ends[0] > begins[0]) {
1905
+ const afterEnd = ends[0] + CODEX_AGENTS_END_MARKER.length;
1906
+ const tail = existing.startsWith("\r\n", afterEnd) ? existing.slice(afterEnd + 2) : existing.startsWith("\n", afterEnd) ? existing.slice(afterEnd + 1) : existing.slice(afterEnd);
1907
+ return existing.slice(0, begins[0]) + section + tail;
1908
+ }
1909
+ if (begins.length > 0 || ends.length > 0) {
1910
+ throw new Error(
1911
+ "found a damaged Haven marker section (orphaned or duplicated markers); remove the leftover marker lines and re-run setup"
1912
+ );
1913
+ }
1914
+ return `${existing.replace(/\n*$/, "\n\n")}${section}`;
1915
+ }
1916
+ function markerLineIndexes(text, marker) {
1917
+ const indexes = [];
1918
+ for (let from = 0; ; ) {
1919
+ const at = text.indexOf(marker, from);
1920
+ if (at === -1) return indexes;
1921
+ if (at === 0 || text[at - 1] === "\n") indexes.push(at);
1922
+ from = at + marker.length;
1923
+ }
1924
+ }
1925
+ function hermesHome(deps) {
1926
+ const env = deps.env ?? process.env;
1927
+ return env.HERMES_HOME ?? join(deps.homeDir ?? homedir(), ".hermes");
1928
+ }
1929
+ var CODEX_AGENTS_BEGIN_MARKER, CODEX_AGENTS_END_MARKER;
1930
+ var init_skill_install = __esm({
1931
+ "src/skill-install.ts"() {
1932
+ CODEX_AGENTS_BEGIN_MARKER = "<!-- BEGIN haven-pay (managed by @haven_ai/connect; edits inside this section are overwritten on re-setup) -->";
1933
+ CODEX_AGENTS_END_MARKER = "<!-- END haven-pay -->";
1934
+ }
1935
+ });
1936
+
1937
+ // src/connect-error.ts
1938
+ function isConnectError(err) {
1939
+ return err instanceof ConnectError;
1940
+ }
1941
+ var ConnectError;
1942
+ var init_connect_error = __esm({
1943
+ "src/connect-error.ts"() {
1944
+ ConnectError = class extends Error {
1945
+ code;
1946
+ nextAction;
1947
+ details;
1948
+ constructor(code, message, nextAction2, details = {}) {
1949
+ super(message);
1950
+ this.name = "ConnectError";
1951
+ this.code = code;
1952
+ this.nextAction = nextAction2;
1953
+ this.details = details;
1954
+ }
1955
+ };
1956
+ }
1957
+ });
1958
+
1959
+ // src/runtime-registry.ts
1960
+ function runtimeProfile(runtime, env = process.env) {
1961
+ return RUNTIME_PROFILES[normalizeRuntime(runtime, env)];
1962
+ }
1963
+ function normalizeRuntime(runtime, env = process.env) {
1964
+ const explicit = normalizeRuntimeName(runtime);
1965
+ if (explicit) return explicit;
1966
+ return detectRuntime(env) ?? "other";
1967
+ }
1968
+ async function resolveRuntimeSelection(explicit, force, options = {}) {
1969
+ const env = options.env ?? process.env;
1970
+ if (force !== void 0) {
1971
+ const forced = normalizeRuntimeName(force);
1972
+ if (!forced) {
1973
+ throw new ConnectError(
1974
+ "runtime_force_unrecognized",
1975
+ `Unknown --runtime-force value "${force}". Valid values: ${RUNTIME_FLAG_VALUES}.`,
1976
+ "rerun_connect_with_a_valid_runtime_name",
1977
+ { allowedRuntimes: RUNTIME_FLAG_VALUE_LIST }
1978
+ );
1979
+ }
1980
+ return { runtime: forced, source: "force" };
1981
+ }
1982
+ const detected = detectRuntime(env);
1983
+ const supplied = explicit?.trim() || options.selfReported?.trim() || void 0;
1984
+ const hint = normalizeRuntimeName(supplied);
1985
+ if (supplied && !hint) {
1986
+ if (!detected) {
1987
+ throw new ConnectError(
1988
+ "runtime_unrecognized",
1989
+ `"${supplied}" is not an agent runtime Haven knows. Valid values: ${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.`,
1990
+ "rerun_connect_with_a_valid_runtime_name",
1991
+ { allowedRuntimes: RUNTIME_FLAG_VALUE_LIST }
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) {
1237
2015
  const key = runtime?.trim().toLowerCase();
1238
2016
  if (!key) return null;
1239
2017
  return RUNTIME_ALIASES[key.replace(/\s+/g, "-")] ?? null;
@@ -1245,7 +2023,7 @@ function detectRuntime(env) {
1245
2023
  if (env.HERMES_HOME || env.HERMES_AGENT) return "hermes";
1246
2024
  return null;
1247
2025
  }
1248
- var RUNTIME_PROFILES, RUNTIME_ALIASES, RUNTIME_FLAG_VALUES;
2026
+ var RUNTIME_PROFILES, RUNTIME_ALIASES, RUNTIME_FLAG_VALUE_LIST, RUNTIME_FLAG_VALUES;
1249
2027
  var init_runtime_registry = __esm({
1250
2028
  "src/runtime-registry.ts"() {
1251
2029
  init_connect_error();
@@ -1364,7 +2142,18 @@ var init_runtime_registry = __esm({
1364
2142
  other: "other",
1365
2143
  manual: "other"
1366
2144
  };
1367
- RUNTIME_FLAG_VALUES = "claude-code, codex-cli, codex-desktop, cursor, vscode, vscode-insiders, claude-desktop, hermes, other";
2145
+ RUNTIME_FLAG_VALUE_LIST = [
2146
+ "claude-code",
2147
+ "codex-cli",
2148
+ "codex-desktop",
2149
+ "cursor",
2150
+ "vscode",
2151
+ "vscode-insiders",
2152
+ "claude-desktop",
2153
+ "hermes",
2154
+ "other"
2155
+ ];
2156
+ RUNTIME_FLAG_VALUES = RUNTIME_FLAG_VALUE_LIST.join(", ");
1368
2157
  }
1369
2158
  });
1370
2159
  async function acknowledgeLocalSignerConsent(signerPath, log) {
@@ -1440,1519 +2229,1319 @@ var init_signer_consent = __esm({
1440
2229
  "src/signer-consent.ts"() {
1441
2230
  }
1442
2231
  });
1443
-
1444
- // src/tombstone.ts
1445
- var tombstone_exports = {};
1446
- __export(tombstone_exports, {
1447
- TOMBSTONE_FILENAME: () => TOMBSTONE_FILENAME,
1448
- TOMBSTONE_MARKER: () => TOMBSTONE_MARKER,
1449
- readAgentTombstone: () => readAgentTombstone,
1450
- writeAgentTombstone: () => writeAgentTombstone
1451
- });
1452
- function tombstoneScript(info) {
1453
- const lines = [
1454
- `${TOMBSTONE_MARKER}: this Haven agent was retired.`,
1455
- "",
1456
- ` agent: ${info.agent_id}`,
1457
- ` retired at: ${info.retired_at}`,
1458
- ` reason: ${info.reason}`,
1459
- ...info.replaced_by ? [` replaced by: ${info.replaced_by}`] : [],
1460
- "",
1461
- "This process is running with a wiring snapshot that predates the",
1462
- "retirement \u2014 it loaded its MCP config at startup and has kept it since.",
1463
- "Restart THIS host to pick up the current wiring. If several long-lived",
1464
- "hosts are running (a gateway, a TUI worker, an editor), restart EVERY",
1465
- "one of them: each holds the snapshot from its own start time, so after",
1466
- "a chain of recreations each can be parked on a DIFFERENT old agent.",
1467
- "",
1468
- "Then verify with: npx @haven_ai/connect@alpha --doctor --runtime <runtime>"
1469
- ];
1470
- return [
1471
- "#!/usr/bin/env node",
1472
- `// ${TOMBSTONE_MARKER} \u2014 written by @haven_ai/connect (#1681). Safe to delete`,
1473
- "// once every long-lived MCP host on this machine has been restarted.",
1474
- `process.stderr.write(${JSON.stringify(lines.join("\n") + "\n")})`,
1475
- "process.exit(1)",
1476
- ""
1477
- ].join("\n");
1478
- }
1479
- async function writeAgentTombstone(input) {
1480
- const dirStat = await stat(input.directory).catch(() => null);
1481
- if (!dirStat?.isDirectory()) {
1482
- throw new Error(`Not a directory: ${input.directory} \u2014 nothing to tombstone.`);
1483
- }
1484
- const info = {
1485
- // reason / replaced_by are persisted to disk and re-emitted to the host's
1486
- // MCP stderr log on EVERY stale probe, potentially for months — redact
1487
- // like every other output path, at the write layer so any future caller
1488
- // inherits it. (#1681 review, finding 1)
1489
- agent_id: input.agentId,
1490
- retired_at: input.retiredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1491
- reason: redactSecrets(input.reason),
1492
- ...input.replacedBy ? { replaced_by: redactSecrets(input.replacedBy) } : {}
1493
- };
1494
- const binDir = join(input.directory, "bin");
1495
- await mkdir(binDir, { recursive: true });
1496
- const wrapperPath = join(binDir, "haven-signer.mjs");
1497
- await writeFile(wrapperPath, tombstoneScript(info), "utf8");
1498
- await chmod(wrapperPath, 493);
1499
- await writeFile(join(input.directory, TOMBSTONE_FILENAME), JSON.stringify(info, null, 2) + "\n", "utf8");
1500
- return info;
1501
- }
1502
- async function readAgentTombstone(directory) {
1503
- try {
1504
- const parsed = JSON.parse(await readFile(join(directory, TOMBSTONE_FILENAME), "utf8"));
1505
- if (typeof parsed?.agent_id !== "string") return null;
1506
- return parsed;
1507
- } catch {
1508
- return null;
1509
- }
1510
- }
1511
- var TOMBSTONE_FILENAME, TOMBSTONE_MARKER;
1512
- var init_tombstone = __esm({
1513
- "src/tombstone.ts"() {
1514
- init_redact();
1515
- TOMBSTONE_FILENAME = "TOMBSTONE.json";
1516
- TOMBSTONE_MARKER = "HAVEN-TOMBSTONE";
1517
- }
1518
- });
1519
-
1520
- // src/doctor.ts
1521
- var doctor_exports = {};
1522
- __export(doctor_exports, {
1523
- runDoctor: () => runDoctor,
1524
- runRepair: () => runRepair
1525
- });
1526
- async function discoverCredentialDirectory(homeDir, explicit) {
1527
- const root = explicit ? dirname(explicit) : join(homeDir, ".haven", "agents");
1528
- let entries = [];
1529
- try {
1530
- entries = await readdir(root);
1531
- } catch {
1532
- return explicit ? { directory: explicit, others: [] } : { others: [] };
2232
+ async function installRuntime(input, deps = {}) {
2233
+ const runtime = normalizeRuntime(input.runtime, deps.env);
2234
+ const profile = runtimeProfile(runtime, deps.env);
2235
+ const progress = deps.onProgress ?? (() => void 0);
2236
+ const localRuntime = input.localMcp === true && supportsLocalMcp(runtime);
2237
+ const consentMessages = [];
2238
+ const localMcpConsent = localRuntime ? await resolveLocalMcpConsent(input, consentMessages) : void 0;
2239
+ const signerConsent = localRuntime ? void 0 : await resolveSignerConsent(input, consentMessages);
2240
+ if (runtime === "other") {
2241
+ const signerCredentialReady2 = await probeLocalSignerCredential(input.signerPath);
2242
+ const signerReady = signerCredentialReady2 && signerConsent?.acknowledged;
2243
+ return {
2244
+ runtime,
2245
+ runtimeMcpMode: "manual",
2246
+ hostedMcpConfigured: false,
2247
+ localSignerConfigured: false,
2248
+ localMcpConfigured: false,
2249
+ probeResult: signerReady ? "manual_runtime_setup_required_local_signer_ready" : "manual_runtime_setup_required_local_signer_unavailable",
2250
+ restartRequired: true,
2251
+ nextUserAction: "return_to_haven_for_wallet_approval_then_configure_runtime",
2252
+ errorCode: "manual_runtime_setup_required",
2253
+ configTarget: "manual runtime setup",
2254
+ signerAcknowledged: signerConsent?.acknowledged,
2255
+ localMcpAcknowledged: false,
2256
+ messages: [
2257
+ ...consentMessages,
2258
+ "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.",
2259
+ ` identity (hosted MCP Bearer): ${input.identityPath}`,
2260
+ ` signer (local signing key): ${input.signerPath}`,
2261
+ "After wallet approval, wire the runtime to Haven by reference:",
2262
+ ` 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}`,
2263
+ ` Fully local MCP (no hosted dependency): npx -y ${mcpPackageSpec()} --identity ${input.identityPath} --signer ${input.signerPath}`
2264
+ ]
2265
+ };
1533
2266
  }
1534
- const candidates = [];
1535
- const tombstonedOnly = [];
1536
- for (const entry of entries) {
1537
- const directory = join(root, entry);
2267
+ let localRuntimeInstall;
2268
+ let localRuntimeError;
2269
+ if (localRuntime) {
1538
2270
  try {
1539
- const s = await stat(join(directory, "identity.json"));
1540
- candidates.push({ directory, mtimeMs: s.mtimeMs });
1541
- } catch {
1542
- try {
1543
- await stat(join(directory, TOMBSTONE_FILENAME));
1544
- tombstonedOnly.push(directory);
1545
- } catch {
1546
- }
2271
+ localRuntimeInstall = await prepareRuntimeForLocalMcp(input, deps);
2272
+ } catch (err) {
2273
+ localRuntimeError = err;
1547
2274
  }
1548
2275
  }
1549
- candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
1550
- if (explicit) {
2276
+ if (localRuntimeError) {
2277
+ const errorCode2 = localRuntimePrepareErrorCode(localRuntimeError);
1551
2278
  return {
1552
- directory: explicit,
1553
- others: [...candidates.map((c) => c.directory), ...tombstonedOnly].filter((d) => d !== explicit)
2279
+ runtime,
2280
+ runtimeMcpMode: "local_stdio",
2281
+ hostedMcpConfigured: false,
2282
+ localSignerConfigured: false,
2283
+ localMcpConfigured: false,
2284
+ probeResult: errorCode2 === "local_mcp_unsupported_node_version" ? "local_stdio_mcp_unsupported_node_version" : "local_stdio_mcp_runtime_install_failed",
2285
+ restartRequired: true,
2286
+ nextUserAction: nextAction(runtime, profile.restartMode, errorCode2),
2287
+ errorCode: errorCode2,
2288
+ configTarget: profile.label,
2289
+ signerAcknowledged: signerConsent?.acknowledged,
2290
+ localMcpAcknowledged: localMcpConsent?.acknowledged,
2291
+ activationCommand: void 0,
2292
+ messages: [
2293
+ ...consentMessages,
2294
+ `Could not prepare local Haven MCP runtime: ${localRuntimeError instanceof Error ? localRuntimeError.message : String(localRuntimeError)}`
2295
+ ]
1554
2296
  };
1555
2297
  }
1556
- if (candidates.length === 0 && tombstonedOnly.length === 0) return { others: [] };
2298
+ let signerCommand;
2299
+ if (!localRuntime) {
2300
+ progress("Getting the signer ready\u2026");
2301
+ try {
2302
+ const signerRuntime = await prepareSignerForRuntime(input, deps);
2303
+ signerCommand = { command: signerRuntime.command, args: signerRuntime.args };
2304
+ consentMessages.push(...signerRuntime.messages);
2305
+ } catch (err) {
2306
+ return {
2307
+ runtime,
2308
+ runtimeMcpMode: "hosted_plus_signer",
2309
+ hostedMcpConfigured: false,
2310
+ localSignerConfigured: false,
2311
+ localMcpConfigured: false,
2312
+ probeResult: "signer_runtime_install_failed",
2313
+ restartRequired: false,
2314
+ 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",
2315
+ errorCode: "signer_runtime_install_failed",
2316
+ configTarget: profile.label,
2317
+ signerAcknowledged: signerConsent?.acknowledged,
2318
+ localMcpAcknowledged: localMcpConsent?.acknowledged,
2319
+ activationCommand: void 0,
2320
+ signerRuntimePrepared: false,
2321
+ messages: [
2322
+ ...consentMessages,
2323
+ `Could not pre-install the local Haven signer: ${err instanceof Error ? err.message : String(err)}`,
2324
+ "No runtime configuration was written (fail-closed): a config pointing at an uninstalled signer looks wired but cannot start.",
2325
+ "Re-run `npx @haven_ai/connect@alpha` to retry the setup."
2326
+ ]
2327
+ };
2328
+ }
2329
+ }
2330
+ const signerRuntimePrepared = localRuntime ? void 0 : signerCommand !== void 0;
2331
+ progress("Setting up your Haven tools\u2026");
2332
+ const configResult = localRuntime ? runtime === "claude-code" ? await configureClaudeCode(deps, localRuntimeInstall?.command ?? "", input.serverName) : await writeRuntimeConfig({
2333
+ runtime,
2334
+ hostedMcpUrl: input.hostedMcpUrl,
2335
+ apiKey: input.apiKey,
2336
+ identityPath: input.identityPath,
2337
+ signerPath: input.signerPath,
2338
+ serverName: input.serverName,
2339
+ credentialDirectory: input.credentialDirectory,
2340
+ localMcpCommand: localRuntimeInstall?.command,
2341
+ signerCommand,
2342
+ homeDir: deps.homeDir,
2343
+ mode: "local"
2344
+ }) : await writeHostedRuntimeConfig(deps, { ...input, runtime }, signerCommand);
2345
+ if (deps.onRuntimeConfigured) {
2346
+ const signerCredentialOnDisk = await probeLocalSignerCredential(input.signerPath);
2347
+ const earlyLocalMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialOnDisk && Boolean(localMcpConsent?.acknowledged);
2348
+ const earlySignerOk = configResult.runtimeMcpMode === "local_stdio" ? earlyLocalMcpOk : configResult.signerConfigured && signerCredentialOnDisk && Boolean(signerConsent?.acknowledged);
2349
+ try {
2350
+ await deps.onRuntimeConfigured({
2351
+ runtime,
2352
+ runtimeMcpMode: configResult.runtimeMcpMode,
2353
+ hostedMcpConfigured: configResult.hostedConfigured,
2354
+ localSignerConfigured: earlySignerOk,
2355
+ localMcpConfigured: earlyLocalMcpOk,
2356
+ signerAcknowledged: signerConsent?.acknowledged,
2357
+ localMcpAcknowledged: localMcpConsent?.acknowledged,
2358
+ restartRequired: configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env),
2359
+ nextUserAction: nextAction(runtime, profile.restartMode, configResult.errorCode),
2360
+ errorCode: configResult.errorCode
2361
+ });
2362
+ } catch {
2363
+ }
2364
+ }
2365
+ progress("Almost there \u2014 just confirming everything connects\u2026");
2366
+ const localProbePromise = configResult.runtimeMcpMode === "local_stdio" && localRuntimeInstall ? runLocalMcpProbe(localRuntimeInstall, deps) : Promise.resolve(void 0);
2367
+ const signerProbePromise = configResult.runtimeMcpMode !== "local_stdio" && signerCommand ? (deps.probeSignerTools ?? probeLocalMcpTools)(
2368
+ signerCommand.command,
2369
+ signerCommand.args,
2370
+ MCP_RUNTIME_MANIFEST.requiredSignerTools
2371
+ ) : Promise.resolve(void 0);
2372
+ const [hostedProbe, signerCredentialReady, localMcpProbe, signerProbe] = await Promise.all([
2373
+ configResult.hostedConfigured ? probeHostedMcpTools(input.apiKey, input.hostedMcpUrl, deps.fetch) : Promise.resolve({ status: "bad_response" }),
2374
+ probeLocalSignerCredential(input.signerPath),
2375
+ localProbePromise,
2376
+ signerProbePromise
2377
+ ]);
2378
+ const hostedOk = configResult.hostedConfigured && hostedProbe.status === "ok";
2379
+ const localMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialReady && Boolean(localMcpConsent?.acknowledged) && localMcpProbe?.status === "ok";
2380
+ const signerOk = configResult.runtimeMcpMode === "local_stdio" ? localMcpOk : configResult.signerConfigured && signerCredentialReady && Boolean(signerConsent?.acknowledged) && // #1587: no handshake, no green. A signer command that was registered
2381
+ // but not probed (manual topology) keeps the old semantics.
2382
+ (signerProbe === void 0 || signerProbe.status === "ok");
2383
+ const restartRequired = configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env);
2384
+ const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : hostedMcpErrorCode(configResult.hostedConfigured, hostedProbe.status) ?? signerConsentErrorCode(signerCredentialReady, signerConsent) ?? signerProbeErrorCode(signerProbe));
2385
+ 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."] : [];
2386
+ const signerProbeMessages = signerProbe ? signerProbe.status === "ok" ? ["Verified local Haven signer with a stdio handshake."] : [
2387
+ `Local Haven signer handshake failed: ${signerProbe.status}.`,
2388
+ "Re-run `npx @haven_ai/connect@alpha` to repair the signer setup."
2389
+ ] : [];
2390
+ 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."] : [];
2391
+ const skillInstall = !configResult.errorCode ? await installSkillForRuntime(runtime, { homeDir: deps.homeDir, env: deps.env }) : void 0;
1557
2392
  return {
1558
- directory: candidates[0]?.directory,
1559
- others: [...candidates.slice(1).map((c) => c.directory), ...tombstonedOnly]
2393
+ runtime,
2394
+ runtimeMcpMode: configResult.runtimeMcpMode,
2395
+ hostedMcpConfigured: hostedOk,
2396
+ localSignerConfigured: signerOk,
2397
+ localMcpConfigured: localMcpOk,
2398
+ probeResult: buildProbeResult(configResult.runtimeMcpMode, configResult.hostedConfigured, hostedProbe.status, signerOk, localMcpOk, localMcpProbe?.status),
2399
+ restartRequired,
2400
+ nextUserAction: nextAction(runtime, profile.restartMode, errorCode),
2401
+ errorCode,
2402
+ configTarget: configResult.target,
2403
+ signerAcknowledged: signerConsent?.acknowledged,
2404
+ localMcpAcknowledged: localMcpConsent?.acknowledged,
2405
+ activationCommand: configResult.activationCommand,
2406
+ skillInstalled: skillInstall?.installed,
2407
+ signerRuntimePrepared,
2408
+ messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...hostedProbeMessages, ...signerProbeMessages, ...localProbeMessages, ...skillInstall?.messages ?? []]
1560
2409
  };
1561
2410
  }
1562
- function agentIsWired(configText, names, slug, identity, sidecar, isPrimary, bareOwnerExists) {
1563
- if (configText === null) return isPrimary;
1564
- if (slug) {
1565
- for (const name of [names.hosted, names.codexHosted, names.signer, names.codexSigner]) {
1566
- if (new RegExp(`(^|[."'\\s\\[])${name}(["'\\]:\\s]|$)`, "m").test(configText)) return true;
1567
- }
1568
- return false;
1569
- }
1570
- if (sidecar?.wrapper_path && configText.includes(sidecar.wrapper_path)) return true;
1571
- if (bareOwnerExists) return false;
1572
- return isPrimary && Boolean(identity?.hosted_mcp_url && configText.includes(identity.hosted_mcp_url));
2411
+ function runtimeInstallCapabilities(runtime, env = process.env) {
2412
+ const profile = runtimeProfile(runtime, env);
2413
+ return {
2414
+ canWriteRuntimeConfig: profile.canWriteRuntimeConfig,
2415
+ restartRequired: restartRequiredForRuntime(runtime, env)
2416
+ };
1573
2417
  }
1574
- async function readIdentity(directory) {
2418
+ async function configureClaudeCode(deps, localMcpCommand, serverName) {
2419
+ const runCommand = deps.runCommand ?? defaultRunCommand;
2420
+ const serverJson = JSON.stringify({
2421
+ type: "stdio",
2422
+ command: localMcpCommand,
2423
+ args: [],
2424
+ env: {}
2425
+ });
1575
2426
  try {
1576
- return JSON.parse(await readFile(join(directory, "identity.json"), "utf8"));
1577
- } catch {
1578
- return void 0;
1579
- }
1580
- }
1581
- async function checksForAgent(entry, input, deps) {
1582
- const { directory, identity, sidecar } = entry;
1583
- const checks = [];
1584
- let signerCapabilities;
1585
- let signerFile;
1586
- try {
1587
- const parsed = JSON.parse(await readFile(join(directory, "signer.json"), "utf8"));
1588
- signerFile = typeof parsed === "object" && parsed !== null ? parsed : void 0;
1589
- } catch {
1590
- signerFile = void 0;
2427
+ if (!localMcpCommand) throw new Error("local MCP wrapper command is required");
2428
+ const names = serverNamesFor(serverName);
2429
+ await runCommand("claude", ["mcp", "remove", names.hosted]).catch(() => void 0);
2430
+ await runCommand("claude", ["mcp", "remove", names.signer]).catch(() => void 0);
2431
+ await runCommand("claude", ["mcp", "add-json", names.hosted, serverJson, "--scope", "user"]).catch(async () => {
2432
+ await runCommand("claude", ["mcp", "add", names.hosted, "--scope", "user", "--", localMcpCommand]);
2433
+ });
2434
+ const verified = await runCommand("claude", ["mcp", "get", names.hosted]).then(() => true).catch(() => false);
2435
+ return {
2436
+ hostedConfigured: false,
2437
+ signerConfigured: true,
2438
+ localMcpConfigured: true,
2439
+ runtimeMcpMode: "local_stdio",
2440
+ target: "Claude Code MCP config",
2441
+ changed: true,
2442
+ restartRequired: true,
2443
+ messages: [
2444
+ "Updated local Haven MCP entry with Claude Code.",
2445
+ ...verified ? ["Verified Claude Code MCP entry."] : []
2446
+ ]
2447
+ };
2448
+ } catch (err) {
2449
+ return {
2450
+ hostedConfigured: false,
2451
+ signerConfigured: false,
2452
+ localMcpConfigured: false,
2453
+ runtimeMcpMode: "local_stdio",
2454
+ target: "Claude Code MCP config",
2455
+ changed: false,
2456
+ restartRequired: true,
2457
+ messages: [
2458
+ `Could not update Claude Code MCP config: ${err instanceof Error ? err.message : String(err)}`,
2459
+ "Install Claude Code or rerun the Haven setup command inside a Claude Code terminal."
2460
+ ],
2461
+ errorCode: "claude_code_config_failed"
2462
+ };
1591
2463
  }
1592
- const credentialsOk = Boolean(identity?.api_key) && signerFile !== void 0;
1593
- checks.push({
1594
- id: "credentials",
1595
- label: "Agent credentials",
1596
- ok: credentialsOk,
1597
- detail: credentialsOk ? `identity.json and signer.json parse (agent ${identity?.agent_id ?? "unknown"})` : "identity.json or signer.json is missing or unparseable.",
1598
- ...credentialsOk ? {} : { repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` }
2464
+ }
2465
+ async function writeHostedRuntimeConfig(deps, input, signerCommand) {
2466
+ if (input.runtime === "claude-code") {
2467
+ return configureClaudeCodeHosted(deps, input, signerCommand);
2468
+ }
2469
+ return writeRuntimeConfig({
2470
+ runtime: input.runtime,
2471
+ hostedMcpUrl: input.hostedMcpUrl,
2472
+ apiKey: input.apiKey,
2473
+ identityPath: input.identityPath,
2474
+ signerPath: input.signerPath,
2475
+ serverName: input.serverName,
2476
+ credentialDirectory: input.credentialDirectory,
2477
+ signerCommand,
2478
+ homeDir: deps.homeDir,
2479
+ mode: "hosted"
1599
2480
  });
1600
- if (!sidecar) {
1601
- checks.push({
1602
- id: "signer_runtime",
1603
- label: "Signer runtime (preinstalled wrapper)",
1604
- ok: false,
1605
- detail: "No signer-runtime.json sidecar \u2014 the pinned signer runtime was never prepared (or a pre-#1586 npx config).",
1606
- repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
1607
- });
1608
- } else {
1609
- const matches = await installedRuntimeMatches(sidecar.runtime_directory, sidecar.cli_path);
1610
- const versionOk = sidecar.signer_version === MCP_RUNTIME_MANIFEST.signerVersion;
1611
- const ok = matches && versionOk;
1612
- checks.push({
1613
- id: "signer_runtime",
1614
- label: "Signer runtime (preinstalled wrapper)",
1615
- ok,
1616
- detail: ok ? `Installed ${sidecar.signer_package}@${sidecar.signer_version} at ${sidecar.runtime_directory}` : matches ? `Installed version ${sidecar.signer_version} does not match the connector's pinned ${MCP_RUNTIME_MANIFEST.signerVersion}.` : `Runtime directory is stale or empty (${sidecar.runtime_directory}) \u2014 the CLI or package versions are missing.`,
1617
- ...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
1618
- });
2481
+ }
2482
+ async function configureClaudeCodeHosted(deps, input, signerCommand) {
2483
+ const runCommand = deps.runCommand ?? defaultRunCommand;
2484
+ const hostedJson = JSON.stringify({
2485
+ type: "http",
2486
+ url: input.hostedMcpUrl,
2487
+ headers: { Authorization: `Bearer ${input.apiKey}` }
2488
+ });
2489
+ const signerJson = JSON.stringify({
2490
+ type: "stdio",
2491
+ command: signerCommand?.command ?? "npx",
2492
+ args: signerCommand?.args ?? ["-y", signerPackageSpec(), "--credentials", input.signerPath],
2493
+ env: {}
2494
+ });
2495
+ try {
2496
+ const names = serverNamesFor(input.serverName);
2497
+ await runCommand("claude", ["mcp", "remove", names.hosted]).catch(() => void 0);
2498
+ await runCommand("claude", ["mcp", "remove", names.signer]).catch(() => void 0);
2499
+ await runCommand("claude", ["mcp", "add-json", names.hosted, hostedJson, "--scope", "user"]);
2500
+ await runCommand("claude", ["mcp", "add-json", names.signer, signerJson, "--scope", "user"]);
2501
+ const verified = await runCommand("claude", ["mcp", "get", names.hosted]).then(() => true).catch(() => false);
2502
+ return {
2503
+ hostedConfigured: true,
2504
+ signerConfigured: true,
2505
+ localMcpConfigured: false,
2506
+ runtimeMcpMode: "hosted_plus_signer",
2507
+ target: "Claude Code MCP config",
2508
+ changed: true,
2509
+ restartRequired: true,
2510
+ messages: [
2511
+ "Updated hosted Haven MCP and local signer entries with Claude Code.",
2512
+ ...verified ? ["Verified Claude Code MCP entry."] : []
2513
+ ]
2514
+ };
2515
+ } catch (err) {
2516
+ return {
2517
+ hostedConfigured: false,
2518
+ signerConfigured: false,
2519
+ localMcpConfigured: false,
2520
+ runtimeMcpMode: "hosted_plus_signer",
2521
+ target: "Claude Code MCP config",
2522
+ changed: false,
2523
+ restartRequired: true,
2524
+ messages: [
2525
+ `Could not update Claude Code MCP config: ${err instanceof Error ? err.message : String(err)}`,
2526
+ "Install Claude Code or rerun the Haven setup command inside a Claude Code terminal."
2527
+ ],
2528
+ errorCode: "claude_code_config_failed"
2529
+ };
1619
2530
  }
1620
- const hostedUrl = identity?.hosted_mcp_url ?? (identity?.api_url ? `${identity.api_url}/mcp` : void 0);
1621
- if (identity?.api_key && hostedUrl) {
1622
- const probe = await (deps.probeHosted ?? probeHostedMcpTools)(identity.api_key, hostedUrl, deps.fetch);
1623
- checks.push({
1624
- id: "hosted_mcp",
1625
- label: "Hosted Haven MCP",
1626
- ok: probe.status === "ok",
1627
- detail: probe.status === "ok" ? `Reachable and authorized (${hostedUrl}).` : `Probe failed: ${probe.status} (${hostedUrl}).`,
1628
- ...probe.status === "ok" ? {} : {
1629
- 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."
1630
- }
1631
- });
1632
- } else {
1633
- checks.push({
1634
- id: "hosted_mcp",
1635
- label: "Hosted Haven MCP",
1636
- ok: false,
1637
- detail: "No stored API key / hosted MCP URL to probe with.",
1638
- repair: `Re-run the full setup: ${RERUN} --setup <token>.`
1639
- });
2531
+ }
2532
+ async function defaultRunCommand(command, args) {
2533
+ await execFileAsync3(command, args, { timeout: 1e4 });
2534
+ }
2535
+ function buildProbeResult(mode, hostedConfigured, hostedStatus, signerReady, localMcpReady, localMcpProbeStatus) {
2536
+ if (mode === "local_stdio") {
2537
+ if (localMcpReady) return "local_stdio_mcp_ready";
2538
+ return localMcpProbeStatus ? `local_stdio_mcp_${localMcpProbeStatus}` : "local_stdio_mcp_unavailable";
1640
2539
  }
1641
- const localDelegate = typeof signerFile?.delegate_address === "string" ? signerFile.delegate_address : void 0;
1642
- if (identity?.api_key && identity.api_url) {
1643
- const probe = await (deps.probeHostedIdentity ?? probeHostedAgentIdentity)(
1644
- identity.api_key,
1645
- identity.api_url,
1646
- deps.fetch
1647
- );
1648
- if (probe.status !== "ok") {
1649
- checks.push({
1650
- id: "identity_match",
1651
- label: "Hosted identity matches the local signing key",
1652
- ok: false,
1653
- 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.`,
1654
- 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}`
1655
- });
1656
- } else if (!localDelegate) {
1657
- checks.push({
1658
- id: "identity_match",
1659
- label: "Hosted identity matches the local signing key",
1660
- ok: false,
1661
- detail: "signer.json holds no delegate_address to compare against the hosted identity.",
1662
- repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.`
1663
- });
2540
+ const hostedPart = hostedConfigured ? `hosted_${hostedStatus}` : "hosted_not_configured";
2541
+ const signerPart = signerReady ? "local_signer_ready" : "local_signer_unavailable";
2542
+ return `${hostedPart}_${signerPart}`.slice(0, 120);
2543
+ }
2544
+ async function resolveLocalMcpConsent(input, messages) {
2545
+ if (input.ackLocalTools || input.ackSigner) {
2546
+ const status = await acknowledgeLocalMcpConsent(input.identityPath, input.signerPath, (message) => messages.push(message));
2547
+ if (status.acknowledged) {
2548
+ messages.push("Prepared the local Haven tools acknowledgement.");
1664
2549
  } else {
1665
- const same = probe.delegateAddress?.toLowerCase() === localDelegate.toLowerCase();
1666
- checks.push({
1667
- id: "identity_match",
1668
- label: "Hosted identity matches the local signing key",
1669
- ok: same,
1670
- 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.`,
1671
- ...same ? {} : {
1672
- 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.`
1673
- }
1674
- });
2550
+ messages.push("Local Haven tools acknowledgement still needs attention.");
1675
2551
  }
2552
+ return status;
1676
2553
  }
1677
- if (sidecar) {
1678
- const consent = await getLocalSignerConsentStatus(join(directory, "signer.json"));
1679
- if (!consent.acknowledged) {
1680
- checks.push({
1681
- id: "signer_process",
1682
- label: "Signer stdio handshake",
1683
- ok: false,
1684
- detail: "The local-tools consent is not acknowledged, so the signer refuses to start (by design).",
1685
- repair: `Run: ${RERUN} --ack-local-tools --setup <token> (or re-run your original setup command with --ack-local-tools).`
1686
- });
2554
+ return getLocalMcpConsentStatus(input.identityPath, input.signerPath);
2555
+ }
2556
+ async function resolveSignerConsent(input, messages) {
2557
+ if (input.ackSigner || input.ackLocalTools) {
2558
+ const status = await acknowledgeLocalSignerConsent(input.signerPath, (message) => messages.push(message));
2559
+ if (status.acknowledged) {
2560
+ messages.push("Prepared the local Haven signer acknowledgement.");
1687
2561
  } else {
1688
- const probe = await (deps.probeSignerTools ?? probeLocalMcpTools)(
1689
- sidecar.wrapper_path,
1690
- [],
1691
- MCP_RUNTIME_MANIFEST.requiredSignerTools
1692
- );
1693
- const experimental = probe.capabilities?.experimental ?? probe.capabilities;
1694
- const compat = experimental?.["haven/signer-compatibility"];
1695
- signerCapabilities = compat ? { "haven/signer-compatibility": compat } : void 0;
1696
- const compatDetail = compat ? ` Compat: x402 expected-context v${JSON.stringify(compat.x402_expected_context_versions ?? "?")}.` : "";
1697
- checks.push({
1698
- id: "signer_process",
1699
- label: "Signer stdio handshake",
1700
- ok: probe.status === "ok",
1701
- detail: probe.status === "ok" ? `Signer started, listed ${probe.toolNames?.length ?? 0} tools${probe.serverInfo?.version ? ` (v${probe.serverInfo.version})` : ""}.${compatDetail}` : `Handshake failed: ${probe.status}.`,
1702
- ...probe.status === "ok" ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
1703
- });
2562
+ messages.push("Local Haven signer acknowledgement still needs attention.");
1704
2563
  }
1705
- } else {
1706
- checks.push({
1707
- id: "signer_process",
1708
- label: "Signer stdio handshake",
1709
- ok: false,
1710
- detail: "Skipped \u2014 no prepared signer runtime to probe.",
1711
- repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
1712
- });
2564
+ return status;
1713
2565
  }
1714
- return { checks, ...signerCapabilities ? { signerCapabilities } : {} };
2566
+ return getLocalSignerConsentStatus(input.signerPath);
1715
2567
  }
1716
- async function runDoctor(input, deps = {}) {
1717
- const homeDir = deps.homeDir ?? homedir();
1718
- const checks = [];
1719
- let signerCapabilities;
1720
- const { directory, others } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
1721
- const configPath = runtimeConfigPathFor(input.runtime, homeDir);
1722
- let configText = null;
1723
- if (configPath !== null) {
1724
- try {
1725
- configText = await readFile(configPath, "utf8");
1726
- } catch {
1727
- configText = null;
1728
- }
2568
+ function signerConsentErrorCode(signerCredentialReady, signerConsent) {
2569
+ if (!signerCredentialReady) return "local_signer_credential_unavailable";
2570
+ if (!signerConsent?.acknowledged) return "local_signer_ack_required";
2571
+ return void 0;
2572
+ }
2573
+ function signerProbeErrorCode(probe) {
2574
+ if (!probe || probe.status === "ok") return void 0;
2575
+ return `local_signer_probe_${probe.status}`;
2576
+ }
2577
+ function hostedMcpErrorCode(hostedConfigured, hostedProbeStatus) {
2578
+ if (!hostedConfigured || hostedProbeStatus === "ok") return void 0;
2579
+ return `hosted_mcp_probe_${hostedProbeStatus}`;
2580
+ }
2581
+ function localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbeStatus) {
2582
+ if (!signerCredentialReady) return "local_signer_credential_unavailable";
2583
+ if (!localMcpConsent?.acknowledged) return "local_mcp_ack_required";
2584
+ if (localMcpProbeStatus && localMcpProbeStatus !== "ok") return `local_mcp_probe_${localMcpProbeStatus}`;
2585
+ return void 0;
2586
+ }
2587
+ function nextAction(runtime, restartMode, errorCode) {
2588
+ if (errorCode) return "return_to_haven_for_wallet_approval_then_finish_runtime_setup";
2589
+ if (restartMode === "hot-reload") return "return_to_haven_for_wallet_approval";
2590
+ if (runtime === "codex-cli" || runtime === "codex-desktop") return "return_to_haven_for_wallet_approval_then_restart_codex";
2591
+ if (runtime === "claude-code") return "return_to_haven_for_wallet_approval_then_restart_claude_code";
2592
+ if (restartMode === "restart-app") return "return_to_haven_for_wallet_approval_then_restart_app";
2593
+ if (restartMode === "restart-session") return "return_to_haven_for_wallet_approval_then_restart_agent_session";
2594
+ return "return_to_haven_for_wallet_approval_then_configure_runtime";
2595
+ }
2596
+ function supportsLocalMcp(runtime) {
2597
+ return runtime === "codex-cli" || runtime === "codex-desktop" || runtime === "claude-code";
2598
+ }
2599
+ async function prepareRuntimeForLocalMcp(input, deps) {
2600
+ const prepare = deps.prepareLocalMcpRuntime ?? ((runtimeInput) => prepareLocalMcpRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress }));
2601
+ return prepare({
2602
+ credentialDirectory: input.credentialDirectory,
2603
+ identityPath: input.identityPath,
2604
+ signerPath: input.signerPath,
2605
+ homeDir: deps.homeDir,
2606
+ serverName: input.serverName
2607
+ });
2608
+ }
2609
+ async function prepareSignerForRuntime(input, deps) {
2610
+ const prepare = deps.prepareSignerRuntime ?? ((runtimeInput) => (
2611
+ // onProgress threaded through on purpose (#1586 review): without it the
2612
+ // install heartbeat was dead code in production and the console still
2613
+ // went silent for the whole cold install — the exact symptom the issue
2614
+ // set out to remove, at a longer timeout.
2615
+ prepareSignerRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress })
2616
+ ));
2617
+ return prepare({
2618
+ credentialDirectory: input.credentialDirectory,
2619
+ signerPath: input.signerPath,
2620
+ homeDir: deps.homeDir,
2621
+ serverName: input.serverName
2622
+ });
2623
+ }
2624
+ async function runLocalMcpProbe(runtimeInstall, deps) {
2625
+ const probe = deps.probeLocalMcpTools ?? probeLocalMcpTools;
2626
+ try {
2627
+ return await probe(runtimeInstall.command, runtimeInstall.args, MCP_RUNTIME_MANIFEST.requiredTools);
2628
+ } catch {
2629
+ return { status: "process_error" };
1729
2630
  }
1730
- const allDirectories = directory ? [directory, ...others] : others;
1731
- let bareOwnerExists = false;
1732
- for (const dir of allDirectories) {
1733
- const sidecar = await readRuntimeSidecar(dir);
1734
- if (!sidecar?.server_name && sidecar?.wrapper_path && configText?.includes(sidecar.wrapper_path)) {
1735
- bareOwnerExists = true;
1736
- break;
1737
- }
2631
+ }
2632
+ function localRuntimePrepareErrorCode(err) {
2633
+ if (err && typeof err === "object" && "code" in err && err.code === "local_mcp_unsupported_node_version") {
2634
+ return "local_mcp_unsupported_node_version";
1738
2635
  }
1739
- const inventory = [];
1740
- const capabilitiesByDirectory = /* @__PURE__ */ new Map();
1741
- const primaryChecksById = /* @__PURE__ */ new Map();
1742
- for (const dir of allDirectories) {
1743
- const identity = await readIdentity(dir);
1744
- const sidecar = await readRuntimeSidecar(dir);
1745
- const tombstone = await readAgentTombstone(dir);
1746
- const slug = sidecar?.server_name;
1747
- const names = serverNamesFor(slug);
1748
- if (!identity?.api_key) {
1749
- inventory.push({
1750
- ...slug ? { slug } : {},
1751
- ...tombstone?.agent_id ? { agentId: tombstone.agent_id } : {},
1752
- directory: dir,
1753
- classification: tombstone ? "retired" : "orphaned",
1754
- checks: []
1755
- });
1756
- continue;
1757
- }
1758
- const wired = agentIsWired(configText, names, slug, identity, sidecar, dir === directory, bareOwnerExists);
1759
- const entry = {
1760
- ...slug ? { slug } : {},
1761
- ...identity.agent_id ? { agentId: identity.agent_id } : {},
1762
- directory: dir,
1763
- classification: wired ? "wired" : "superseded",
1764
- checks: []
1765
- };
1766
- if (wired) {
1767
- const result = await checksForAgent({ directory: dir, identity, sidecar }, input, deps);
1768
- entry.checks = result.checks;
1769
- capabilitiesByDirectory.set(dir, result.signerCapabilities);
1770
- }
1771
- inventory.push(entry);
2636
+ return "local_mcp_runtime_install_failed";
2637
+ }
2638
+ var execFileAsync3;
2639
+ var init_runtime_install = __esm({
2640
+ "src/runtime-install.ts"() {
2641
+ init_config_writers();
2642
+ init_server_names();
2643
+ init_local_mcp_consent();
2644
+ init_probes();
2645
+ init_local_mcp_runtime();
2646
+ init_signer_runtime();
2647
+ init_runtime_manifest();
2648
+ init_skill_install();
2649
+ init_runtime_registry();
2650
+ init_signer_consent();
2651
+ execFileAsync3 = promisify(execFile);
1772
2652
  }
1773
- const wiredDirectories = inventory.filter((entry) => entry.classification === "wired").map((entry) => entry.directory);
1774
- const primaryDirectory = input.credentialsDir ? directory : wiredDirectories.includes(directory ?? "") ? directory : wiredDirectories[0] ?? directory;
1775
- if (primaryDirectory) {
1776
- const primaryEntry = inventory.find((entry) => entry.directory === primaryDirectory);
1777
- signerCapabilities = capabilitiesByDirectory.get(primaryDirectory);
1778
- for (const check of primaryEntry?.checks ?? []) primaryChecksById.set(check.id, check);
2653
+ });
2654
+
2655
+ // src/tombstone.ts
2656
+ var tombstone_exports = {};
2657
+ __export(tombstone_exports, {
2658
+ TOMBSTONE_FILENAME: () => TOMBSTONE_FILENAME,
2659
+ TOMBSTONE_MARKER: () => TOMBSTONE_MARKER,
2660
+ readAgentTombstone: () => readAgentTombstone,
2661
+ writeAgentTombstone: () => writeAgentTombstone
2662
+ });
2663
+ function tombstoneScript(info) {
2664
+ const lines = [
2665
+ `${TOMBSTONE_MARKER}: this Haven agent was retired.`,
2666
+ "",
2667
+ ` agent: ${info.agent_id}`,
2668
+ ` retired at: ${info.retired_at}`,
2669
+ ` reason: ${info.reason}`,
2670
+ ...info.replaced_by ? [` replaced by: ${info.replaced_by}`] : [],
2671
+ "",
2672
+ "This process is running with a wiring snapshot that predates the",
2673
+ "retirement \u2014 it loaded its MCP config at startup and has kept it since.",
2674
+ "Restart THIS host to pick up the current wiring. If several long-lived",
2675
+ "hosts are running (a gateway, a TUI worker, an editor), restart EVERY",
2676
+ "one of them: each holds the snapshot from its own start time, so after",
2677
+ "a chain of recreations each can be parked on a DIFFERENT old agent.",
2678
+ "",
2679
+ "Then verify with: npx @haven_ai/connect@alpha --doctor --runtime <runtime>"
2680
+ ];
2681
+ return [
2682
+ "#!/usr/bin/env node",
2683
+ `// ${TOMBSTONE_MARKER} \u2014 written by @haven_ai/connect (#1681). Safe to delete`,
2684
+ "// once every long-lived MCP host on this machine has been restarted.",
2685
+ `process.stderr.write(${JSON.stringify(lines.join("\n") + "\n")})`,
2686
+ "process.exit(1)",
2687
+ ""
2688
+ ].join("\n");
2689
+ }
2690
+ async function writeAgentTombstone(input) {
2691
+ const dirStat = await stat(input.directory).catch(() => null);
2692
+ if (!dirStat?.isDirectory()) {
2693
+ throw new Error(`Not a directory: ${input.directory} \u2014 nothing to tombstone.`);
1779
2694
  }
1780
- if (!primaryDirectory) {
1781
- checks.push({
1782
- id: "credentials",
1783
- label: "Agent credentials",
1784
- ok: false,
1785
- detail: "No agent credential directory with an identity.json under ~/.haven/agents.",
1786
- repair: `Run the full setup once: ${RERUN} --setup <token from the Haven dashboard>.`
1787
- });
1788
- } else {
1789
- const primaryIdentity = await readIdentity(primaryDirectory);
1790
- const primarySidecar = await readRuntimeSidecar(primaryDirectory);
1791
- if (primaryChecksById.size === 0) {
1792
- const result = await checksForAgent(
1793
- { directory: primaryDirectory, identity: primaryIdentity, sidecar: primarySidecar },
1794
- input,
1795
- deps
1796
- );
1797
- signerCapabilities = result.signerCapabilities;
1798
- for (const check of result.checks) primaryChecksById.set(check.id, check);
1799
- }
1800
- for (const id of ["credentials", "signer_runtime"]) {
1801
- const check = primaryChecksById.get(id);
1802
- if (check) checks.push(check);
1803
- }
2695
+ const info = {
2696
+ // reason / replaced_by are persisted to disk and re-emitted to the host's
2697
+ // MCP stderr log on EVERY stale probe, potentially for months — redact
2698
+ // like every other output path, at the write layer so any future caller
2699
+ // inherits it. (#1681 review, finding 1)
2700
+ agent_id: input.agentId,
2701
+ retired_at: input.retiredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
2702
+ reason: redactSecrets(input.reason),
2703
+ ...input.replacedBy ? { replaced_by: redactSecrets(input.replacedBy) } : {}
2704
+ };
2705
+ const binDir = join(input.directory, "bin");
2706
+ await mkdir(binDir, { recursive: true });
2707
+ const wrapperPath = join(binDir, "haven-signer.mjs");
2708
+ await writeFile(wrapperPath, tombstoneScript(info), "utf8");
2709
+ await chmod(wrapperPath, 493);
2710
+ await writeFile(join(input.directory, TOMBSTONE_FILENAME), JSON.stringify(info, null, 2) + "\n", "utf8");
2711
+ return info;
2712
+ }
2713
+ async function readAgentTombstone(directory) {
2714
+ try {
2715
+ const parsed = JSON.parse(await readFile(join(directory, TOMBSTONE_FILENAME), "utf8"));
2716
+ if (typeof parsed?.agent_id !== "string") return null;
2717
+ return parsed;
2718
+ } catch {
2719
+ return null;
1804
2720
  }
1805
- if (configPath === null) {
1806
- checks.push({
1807
- id: "runtime_config",
1808
- label: "Runtime MCP config",
1809
- ok: true,
1810
- detail: `Runtime '${input.runtime}' has no file-based config the connector owns (CLI-managed) \u2014 skipping the file check.`
1811
- });
1812
- } else if (configText === null) {
1813
- checks.push({
1814
- id: "runtime_config",
1815
- label: "Runtime MCP config",
1816
- ok: false,
1817
- detail: `No runtime config at ${configPath}.`,
1818
- repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
1819
- });
1820
- } else {
1821
- const primaryIdentity = await readIdentity(primaryDirectory ?? "");
1822
- const primarySidecar = primaryDirectory ? await readRuntimeSidecar(primaryDirectory) : null;
1823
- const hasHaven = primaryIdentity?.hosted_mcp_url ? configText.includes(primaryIdentity.hosted_mcp_url) : configText.includes("haven");
1824
- const signerViaNpx = configText.includes("@haven_ai/signer");
1825
- const wrapperReferenced = primarySidecar ? configText.includes(primarySidecar.wrapper_path) : false;
1826
- const ok = hasHaven && !signerViaNpx && (primarySidecar ? wrapperReferenced : true);
1827
- checks.push({
1828
- id: "runtime_config",
1829
- label: "Runtime MCP config",
1830
- ok,
1831
- 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)" : ""}.`,
1832
- ...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
1833
- });
2721
+ }
2722
+ var TOMBSTONE_FILENAME, TOMBSTONE_MARKER;
2723
+ var init_tombstone = __esm({
2724
+ "src/tombstone.ts"() {
2725
+ init_redact();
2726
+ TOMBSTONE_FILENAME = "TOMBSTONE.json";
2727
+ TOMBSTONE_MARKER = "HAVEN-TOMBSTONE";
1834
2728
  }
1835
- for (const id of ["hosted_mcp", "identity_match"]) {
1836
- const check = primaryChecksById.get(id);
1837
- if (check) checks.push(check);
2729
+ });
2730
+
2731
+ // src/rekey.ts
2732
+ var rekey_exports = {};
2733
+ __export(rekey_exports, {
2734
+ finishRekey: () => finishRekey,
2735
+ startRekey: () => startRekey
2736
+ });
2737
+ async function startRekey(options, deps = {}) {
2738
+ const now = deps.now ?? (() => Date.now());
2739
+ const stored = await readStoredCredentials(
2740
+ options.serverName,
2741
+ options.agentId,
2742
+ options.credentialsDir
2743
+ );
2744
+ const api = (deps.createApi ?? ((url) => createConnectApiClient(url)))(stored.apiUrl);
2745
+ const identity = await probeIdentity(api, stored.apiKey, "current");
2746
+ assertRekeyable(identity, stored);
2747
+ const key = (deps.generateKey ?? generateDelegateKey)();
2748
+ const startedAt = new Date(now()).toISOString();
2749
+ const expiresAt = new Date(now() + REKEY_PENDING_TTL_MS).toISOString();
2750
+ await writeRekeyPending(stored.directory, {
2751
+ agent_id: stored.agentId,
2752
+ new_delegate_address: key.address,
2753
+ new_delegate_key: key.privateKey,
2754
+ started_at: startedAt,
2755
+ expires_at: expiresAt
2756
+ });
2757
+ const finishCommand = [
2758
+ "npx @haven_ai/connect@alpha --rekey-finish",
2759
+ options.serverName ? `--name ${options.serverName}` : void 0,
2760
+ "--api-key <the key the dashboard showed you>",
2761
+ options.runtime ? `--runtime ${options.runtime}` : "--runtime <your runtime>"
2762
+ ].filter(Boolean).join(" ");
2763
+ return {
2764
+ started: true,
2765
+ agentId: stored.agentId,
2766
+ directory: stored.directory,
2767
+ newDelegateAddress: key.address,
2768
+ expiresAt,
2769
+ messages: [
2770
+ `New signing key generated on this machine for agent ${identity.name || stored.agentId}.`,
2771
+ "",
2772
+ ` New signing address: ${key.address}`,
2773
+ "",
2774
+ "The private half stays here \u2014 Haven never receives it, and there is no way to move it",
2775
+ "between machines. That is what keeps the account non-custodial.",
2776
+ "",
2777
+ 'Next, on the Haven agent page: choose "Replace signing key" and paste that address.',
2778
+ "When it finishes it shows a new API key ONCE. Come back here and run:",
2779
+ "",
2780
+ ` ${finishCommand}`,
2781
+ "",
2782
+ `Nothing has changed yet \u2014 the agent keeps working on its old key until you finish.`,
2783
+ `This pending re-key expires ${expiresAt}.`
2784
+ ]
2785
+ };
2786
+ }
2787
+ async function finishRekey(options, deps = {}) {
2788
+ const now = deps.now ?? (() => Date.now());
2789
+ if (!options.newApiKey) {
2790
+ throw new Error("--rekey-finish needs --api-key <key> \u2014 the one the Haven agent page showed once.");
1838
2791
  }
1839
- const otherEntries = inventory.filter((entry) => entry.directory !== primaryDirectory);
1840
- if (otherEntries.length > 0) {
1841
- const live = [];
1842
- const revoked = [];
1843
- const unverifiable = [];
1844
- const retired = [];
1845
- for (const entry of otherEntries) {
1846
- const identity = await readIdentity(entry.directory);
1847
- const tombstone = await readAgentTombstone(entry.directory);
1848
- const otherAgent = identity?.agent_id ?? tombstone?.agent_id ?? basename(entry.directory);
1849
- const otherUrl = identity?.hosted_mcp_url ?? (identity?.api_url ? `${identity.api_url}/mcp` : void 0);
1850
- if (!identity?.api_key || !otherUrl) {
1851
- if (tombstone) retired.push(`${otherAgent} (retired ${tombstone.retired_at})`);
1852
- else unverifiable.push(`${otherAgent} (no stored key/URL to probe)`);
1853
- continue;
1854
- }
1855
- const suffix = tombstone ? " [tombstoned \u2014 key material still present]" : "";
1856
- const probe = await (deps.probeHosted ?? probeHostedMcpTools)(identity.api_key, otherUrl, deps.fetch);
1857
- if (probe.status === "ok") live.push({ label: `${otherAgent}${suffix}`, entry });
1858
- else if (probe.status === "unauthorized") revoked.push(`${otherAgent}${suffix}`);
1859
- else unverifiable.push(`${otherAgent} (${probe.status})${suffix}`);
1860
- }
1861
- const parts = [];
1862
- if (live.length > 0) parts.push(`STILL SPEND-CAPABLE: ${live.map((item) => item.label).join(", ")}`);
1863
- if (revoked.length > 0) parts.push(`already revoked: ${revoked.join(", ")}`);
1864
- if (retired.length > 0) parts.push(`tombstoned (keys removed): ${retired.join(", ")}`);
1865
- if (unverifiable.length > 0) parts.push(`could not verify: ${unverifiable.join(", ")}`);
1866
- const supersededLive = live.filter((item) => item.entry.classification !== "wired").map((item) => item.label);
1867
- checks.push({
1868
- id: "superseded_agents",
1869
- label: "Superseded agent credentials",
1870
- ok: supersededLive.length === 0,
1871
- 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("; ")}.`,
1872
- ...supersededLive.length > 0 ? {
1873
- 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.`
1874
- } : {}
1875
- });
2792
+ const stored = await readStoredCredentials(
2793
+ options.serverName,
2794
+ options.agentId,
2795
+ options.credentialsDir
2796
+ );
2797
+ const pending = await readRekeyPending(stored.directory, now());
2798
+ if (pending.agent_id !== stored.agentId) {
2799
+ throw new Error(
2800
+ `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.`
2801
+ );
1876
2802
  }
1877
- const signerProcess = primaryChecksById.get("signer_process");
1878
- if (signerProcess) checks.push(signerProcess);
1879
- const restart = restartRequiredForRuntime(input.runtime, deps.env);
1880
- checks.push({
1881
- id: "restart",
1882
- label: "Runtime restart",
1883
- ok: true,
1884
- 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."
2803
+ const api = (deps.createApi ?? ((url) => createConnectApiClient(url)))(stored.apiUrl);
2804
+ const identity = await probeIdentity(api, options.newApiKey, "new");
2805
+ if (identity.id !== stored.agentId) {
2806
+ throw new Error(
2807
+ `That API key belongs to agent ${identity.id}, not ${stored.agentId}. Nothing was changed.`
2808
+ );
2809
+ }
2810
+ const onChain = (identity.delegate_address ?? "").toLowerCase();
2811
+ const expected = pending.new_delegate_address.toLowerCase();
2812
+ if (onChain !== expected) {
2813
+ throw new Error(
2814
+ `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.`
2815
+ );
2816
+ }
2817
+ await rewriteCredentialFiles({
2818
+ baseDir: options.credentialsDir,
2819
+ agentId: stored.agentId,
2820
+ serverName: options.serverName,
2821
+ apiKey: options.newApiKey,
2822
+ delegateKey: pending.new_delegate_key,
2823
+ delegateAddress: pending.new_delegate_address,
2824
+ safeAddress: stored.safeAddress ?? identity.safe_address ?? void 0,
2825
+ chainId: stored.chainId ?? identity.chain_id ?? void 0,
2826
+ network: stored.network,
2827
+ agentBudget: stored.agentBudget,
2828
+ apiUrl: stored.apiUrl,
2829
+ hostedMcpUrl: stored.hostedMcpUrl,
2830
+ x402BindingSigner: stored.x402BindingSigner,
2831
+ warn: deps.log
1885
2832
  });
1886
- const wiredOk = inventory.filter((entry) => entry.classification === "wired").every((entry) => entry.checks.every((check) => check.ok));
2833
+ await clearRekeyPending(stored.directory);
2834
+ const names = serverNamesFor(options.serverName);
2835
+ const messages = [
2836
+ `Agent ${identity.name || stored.agentId} is now on its new signing key.`,
2837
+ ` Signing address: ${pending.new_delegate_address}`,
2838
+ ` Credentials: ${stored.directory} (rewritten in place)`,
2839
+ ` MCP servers: ${names.hosted} / ${names.signer} (unchanged names)`
2840
+ ];
2841
+ let configRewritten = false;
2842
+ if (options.runtime) {
2843
+ const prepared = await (deps.prepareSigner ?? prepareSignerRuntime)(
2844
+ {
2845
+ credentialDirectory: stored.directory,
2846
+ signerPath: `${stored.directory}/signer.json`,
2847
+ homeDir: options.homeDir
2848
+ },
2849
+ { runCommand: deps.runCommand }
2850
+ );
2851
+ const result = await (deps.writeConfig ?? writeHostedRuntimeConfig)(
2852
+ { runCommand: deps.runCommand, homeDir: options.homeDir },
2853
+ {
2854
+ runtime: options.runtime,
2855
+ hostedMcpUrl: stored.hostedMcpUrl,
2856
+ apiKey: options.newApiKey,
2857
+ identityPath: `${stored.directory}/identity.json`,
2858
+ signerPath: `${stored.directory}/signer.json`,
2859
+ credentialDirectory: stored.directory,
2860
+ serverName: options.serverName
2861
+ },
2862
+ { command: prepared.command, args: prepared.args }
2863
+ );
2864
+ configRewritten = result.hostedConfigured;
2865
+ messages.push(` Config: ${result.target}`);
2866
+ messages.push(...result.messages.map((line) => ` ${line}`));
2867
+ if (!configRewritten) {
2868
+ messages.push(
2869
+ "",
2870
+ "WARNING: the MCP config was NOT updated, so it still carries the OLD API key and every",
2871
+ `wired host will fail with 401. Fix the cause above and re-run with --runtime ${options.runtime},`,
2872
+ "or update the config by hand. The credential files on disk are already on the new key."
2873
+ );
2874
+ }
2875
+ } else {
2876
+ messages.push(
2877
+ "",
2878
+ "NOTE: no --runtime was given, so the MCP config still carries the OLD API key and every",
2879
+ "wired host will fail with 401. Re-run with --runtime <name> to rewrite it."
2880
+ );
2881
+ }
1887
2882
  return {
1888
- version: 1,
1889
- ok: checks.every((check) => check.ok) && wiredOk,
1890
- runtime: input.runtime,
1891
- credentialDirectory: primaryDirectory,
1892
- checks,
1893
- agents: inventory,
1894
- ...signerCapabilities ? { signerCapabilities } : {}
2883
+ finished: true,
2884
+ agentId: stored.agentId,
2885
+ directory: stored.directory,
2886
+ newDelegateAddress: pending.new_delegate_address,
2887
+ serverNames: { hosted: names.hosted, signer: names.signer },
2888
+ configRewritten,
2889
+ messages
1895
2890
  };
1896
2891
  }
1897
- async function runRepair(input, deps = {}) {
1898
- const homeDir = deps.homeDir ?? homedir();
1899
- const messages = [];
1900
- const { directory, others } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
1901
- if (others.length > 0) {
1902
- messages.push(`Note: ${others.length} other agent credential dir(s) exist \u2014 run --doctor for their status.`);
2892
+ async function probeIdentity(api, apiKey, which) {
2893
+ try {
2894
+ return await api.getAgentIdentity(apiKey);
2895
+ } catch (err) {
2896
+ const status = err?.status;
2897
+ if (status === 401 || status === 403) {
2898
+ throw new Error(
2899
+ 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."
2900
+ );
2901
+ }
2902
+ throw new Error(
2903
+ `Could not reach Haven to check this agent (${redactSecrets(
2904
+ err instanceof Error ? err.message : String(err)
2905
+ )}). Nothing was changed.`
2906
+ );
1903
2907
  }
1904
- if (!directory) {
1905
- return {
1906
- ok: false,
1907
- messages: [`No agent credentials found to repair \u2014 run the full setup: ${RERUN} --setup <token>.`]
2908
+ }
2909
+ function assertRekeyable(identity, stored) {
2910
+ if (identity.id !== stored.agentId) {
2911
+ throw new Error(
2912
+ `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.`
2913
+ );
2914
+ }
2915
+ if (identity.execution_rail === "legacy") {
2916
+ throw new Error(
2917
+ `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.`
2918
+ );
2919
+ }
2920
+ if (identity.status === "revoked") {
2921
+ throw new Error(
2922
+ `Agent ${identity.name || identity.id} is revoked. Re-keying would hand a revoked agent fresh credentials \u2014 create a new agent instead.`
2923
+ );
2924
+ }
2925
+ }
2926
+ var init_rekey = __esm({
2927
+ "src/rekey.ts"() {
2928
+ init_api();
2929
+ init_runtime_install();
2930
+ init_signer_runtime();
2931
+ init_key();
2932
+ init_redact();
2933
+ init_server_names();
2934
+ init_storage();
2935
+ }
2936
+ });
2937
+
2938
+ // src/rekey-restart.ts
2939
+ var rekey_restart_exports = {};
2940
+ __export(rekey_restart_exports, {
2941
+ restartGuidance: () => restartGuidance
2942
+ });
2943
+ function restartGuidance(runtime) {
2944
+ const profile = runtime ? BY_RUNTIME[runtime] : void 0;
2945
+ const lines = ["The new key is only live in a process that started after now.", ""];
2946
+ if (profile) {
2947
+ lines.push(profile.how);
2948
+ for (const command of profile.commands) lines.push(` ${command}`);
2949
+ if (profile.commands.length > 0) lines.push("");
2950
+ } else {
2951
+ lines.push(
2952
+ 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."
2953
+ );
2954
+ lines.push("");
2955
+ }
2956
+ lines.push(SWEEP);
2957
+ return { commands: profile?.commands ?? [], lines };
2958
+ }
2959
+ var SWEEP, BY_RUNTIME;
2960
+ var init_rekey_restart = __esm({
2961
+ "src/rekey-restart.ts"() {
2962
+ 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.";
2963
+ BY_RUNTIME = {
2964
+ "claude-code": {
2965
+ commands: ["claude --continue"],
2966
+ how: "Exit this Claude Code session and start a new one:"
2967
+ },
2968
+ "codex-cli": {
2969
+ commands: ["codex resume --last"],
2970
+ how: "Start a fresh Codex CLI session:"
2971
+ },
2972
+ "codex-desktop": {
2973
+ commands: [],
2974
+ how: "Quit Codex Desktop completely (not just the window) and reopen it."
2975
+ },
2976
+ "claude-desktop": {
2977
+ commands: [],
2978
+ how: "Quit Claude Desktop completely (not just the window) and reopen it."
2979
+ },
2980
+ hermes: {
2981
+ commands: ["systemctl --user restart hermes-gateway", "/restart"],
2982
+ 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:"
2983
+ },
2984
+ cursor: {
2985
+ commands: [],
2986
+ 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").'
2987
+ },
2988
+ vscode: {
2989
+ commands: [],
2990
+ 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").'
2991
+ },
2992
+ "vscode-insiders": {
2993
+ commands: [],
2994
+ 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").'
2995
+ }
1908
2996
  };
1909
2997
  }
1910
- let identity;
2998
+ });
2999
+
3000
+ // src/doctor.ts
3001
+ var doctor_exports = {};
3002
+ __export(doctor_exports, {
3003
+ runDoctor: () => runDoctor,
3004
+ runRepair: () => runRepair
3005
+ });
3006
+ async function discoverCredentialDirectory(homeDir, explicit) {
3007
+ const root = explicit ? dirname(explicit) : join(homeDir, ".haven", "agents");
3008
+ let entries = [];
1911
3009
  try {
1912
- identity = JSON.parse(await readFile(join(directory, "identity.json"), "utf8"));
3010
+ entries = await readdir(root);
1913
3011
  } catch {
1914
- return { ok: false, messages: ["identity.json is unreadable \u2014 re-run the full setup with a fresh token."] };
1915
- }
1916
- if (!identity.api_key || !(identity.hosted_mcp_url || identity.api_url)) {
1917
- return { ok: false, messages: ["identity.json lacks the stored API key / hosted URL \u2014 re-run the full setup."] };
3012
+ return explicit ? { directory: explicit, others: [], parkedOnly: /* @__PURE__ */ new Set() } : { others: [], parkedOnly: /* @__PURE__ */ new Set() };
1918
3013
  }
1919
- const configPath = runtimeConfigPathFor(input.runtime, homeDir);
1920
- if (configPath) {
3014
+ const candidates = [];
3015
+ const tombstonedOnly = [];
3016
+ const parkedOnly = [];
3017
+ for (const entry of entries) {
3018
+ const directory = join(root, entry);
1921
3019
  try {
1922
- const existing = await readFile(configPath, "utf8");
1923
- if (existing.includes("bin/haven-mcp") || existing.includes(".haven/mcp-runtime")) {
1924
- return {
1925
- ok: false,
1926
- messages: [
1927
- `The config at ${configPath} is the LOCAL-stdio topology (--local). Repair currently rewrites only the hosted+signer shape and will not touch it.`,
1928
- "Re-run your original setup command (with --local) to repair a local-stdio install."
1929
- ]
1930
- };
1931
- }
3020
+ const s = await stat(join(directory, "identity.json"));
3021
+ candidates.push({ directory, mtimeMs: s.mtimeMs });
1932
3022
  } catch {
3023
+ try {
3024
+ await stat(join(directory, TOMBSTONE_FILENAME));
3025
+ tombstonedOnly.push(directory);
3026
+ } catch {
3027
+ try {
3028
+ await stat(join(directory, REKEY_PENDING_FILENAME));
3029
+ parkedOnly.push(directory);
3030
+ } catch {
3031
+ }
3032
+ }
1933
3033
  }
1934
3034
  }
1935
- const signerPath = join(directory, "signer.json");
1936
- const prepared = await prepareSignerRuntime(
1937
- { credentialDirectory: directory, signerPath, homeDir },
1938
- { runCommand: deps.runCommand }
1939
- );
1940
- messages.push(...prepared.messages);
1941
- const configResult = await writeRuntimeConfig({
1942
- runtime: input.runtime,
1943
- hostedMcpUrl: identity.hosted_mcp_url ?? `${identity.api_url}/mcp`,
1944
- apiKey: identity.api_key,
1945
- identityPath: join(directory, "identity.json"),
1946
- signerPath,
1947
- credentialDirectory: directory,
1948
- signerCommand: { command: prepared.command, args: prepared.args },
1949
- homeDir,
1950
- mode: "hosted"
1951
- });
1952
- messages.push(...configResult.messages);
1953
- messages.push("Repair complete \u2014 restart the runtime, then verify with --doctor.");
1954
- return { ok: true, messages };
3035
+ const parkedOnlySet = new Set(parkedOnly);
3036
+ candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
3037
+ if (explicit) {
3038
+ return {
3039
+ directory: explicit,
3040
+ others: [...candidates.map((c) => c.directory), ...tombstonedOnly, ...parkedOnly].filter((d) => d !== explicit),
3041
+ parkedOnly: parkedOnlySet
3042
+ };
3043
+ }
3044
+ if (candidates.length === 0 && tombstonedOnly.length === 0 && parkedOnly.length === 0) {
3045
+ return { others: [], parkedOnly: parkedOnlySet };
3046
+ }
3047
+ return {
3048
+ directory: candidates[0]?.directory,
3049
+ others: [...candidates.slice(1).map((c) => c.directory), ...tombstonedOnly, ...parkedOnly],
3050
+ parkedOnly: parkedOnlySet
3051
+ };
1955
3052
  }
1956
- var RERUN;
1957
- var init_doctor = __esm({
1958
- "src/doctor.ts"() {
1959
- init_runtime_manifest();
1960
- init_probes();
1961
- init_signer_runtime();
1962
- init_config_writers();
1963
- init_runtime_registry();
1964
- init_signer_consent();
1965
- init_tombstone();
1966
- init_server_names();
1967
- init_redact();
1968
- RERUN = "npx @haven_ai/connect@alpha";
3053
+ function agentIsWired(configText, names, slug, identity, sidecar, isPrimary, bareOwnerExists) {
3054
+ if (configText === null) return isPrimary;
3055
+ if (slug) {
3056
+ for (const name of [names.hosted, names.codexHosted, names.signer, names.codexSigner]) {
3057
+ if (new RegExp(`(^|[."'\\s\\[])${name}(["'\\]:\\s]|$)`, "m").test(configText)) return true;
3058
+ }
3059
+ return false;
3060
+ }
3061
+ if (sidecar?.wrapper_path && configText.includes(sidecar.wrapper_path)) return true;
3062
+ if (bareOwnerExists) return false;
3063
+ return isPrimary && Boolean(identity?.hosted_mcp_url && configText.includes(identity.hosted_mcp_url));
3064
+ }
3065
+ function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
3066
+ const label = "Pending re-key";
3067
+ const nameFlag = slug ? ` --name ${slug}` : "";
3068
+ if (status.state === "unreadable") {
3069
+ return {
3070
+ id: "rekey_pending",
3071
+ label,
3072
+ ok: false,
3073
+ 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.`,
3074
+ repair: `Delete ${status.path}, then start again: ${RERUN} --rekey${nameFlag}`
3075
+ };
3076
+ }
3077
+ const started = status.startedAt ?? "an unknown time";
3078
+ const address = status.newDelegateAddress ?? "unknown";
3079
+ const completedOnHaven = hostedDelegateAddress !== void 0 && status.newDelegateAddress !== void 0 && hostedDelegateAddress.toLowerCase() === status.newDelegateAddress.toLowerCase();
3080
+ if (completedOnHaven) {
3081
+ return {
3082
+ id: "rekey_pending",
3083
+ label,
3084
+ ok: false,
3085
+ 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." : ""),
3086
+ 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}`
3087
+ };
3088
+ }
3089
+ 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.";
3090
+ if (status.state === "expired") {
3091
+ return {
3092
+ id: "rekey_pending",
3093
+ label,
3094
+ ok: false,
3095
+ 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,
3096
+ 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.`
3097
+ };
1969
3098
  }
1970
- });
1971
-
1972
- // src/api.ts
1973
- function createConnectApiClient(baseUrl, fetchImpl = fetch) {
1974
- const root = baseUrl.replace(/\/+$/, "");
1975
3099
  return {
1976
- resolveSetup: (input) => request(fetchImpl, `${root}/agent-connection-setups/resolve`, {
1977
- method: "POST",
1978
- body: JSON.stringify({
1979
- setup_token: input.setupToken,
1980
- connector_version: input.connectorVersion,
1981
- runtime: input.runtime
1982
- })
1983
- }),
1984
- registerSetup: (input) => request(fetchImpl, `${root}/agent-connection-setups/register`, {
1985
- method: "POST",
1986
- body: JSON.stringify({
1987
- setup_token: input.setupToken,
1988
- challenge_id: input.challengeId,
1989
- delegate_address: input.delegateAddress,
1990
- proof_signature: input.proofSignature,
1991
- api_key_hash: input.apiKeyHash,
1992
- api_key_prefix: input.apiKeyPrefix,
1993
- runtime: input.runtime,
1994
- connector_version: input.connectorVersion,
1995
- connector_context: input.connectorContext,
1996
- install_capabilities: input.installCapabilities && {
1997
- can_write_runtime_config: input.installCapabilities.canWriteRuntimeConfig,
1998
- restart_required: input.installCapabilities.restartRequired
1999
- }
2000
- })
2001
- }),
2002
- getConnectorStatus: (setupId, apiKey) => request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/connector-status`, {
2003
- method: "GET",
2004
- headers: { Authorization: `Bearer ${apiKey}` }
2005
- }),
2006
- updateInstallStatus: async (setupId, apiKey, input) => {
2007
- await request(fetchImpl, `${root}/agent-connection-setups/${encodeURIComponent(setupId)}/install-status`, {
2008
- method: "POST",
2009
- headers: { Authorization: `Bearer ${apiKey}` },
2010
- body: JSON.stringify({
2011
- runtime: input.runtime,
2012
- connector_version: input.connectorVersion,
2013
- runtime_mcp_mode: input.runtimeMcpMode,
2014
- hosted_mcp_configured: input.hostedMcpConfigured,
2015
- local_signer_configured: input.localSignerConfigured,
2016
- local_mcp_configured: input.localMcpConfigured,
2017
- credential_files_written: input.credentialFilesWritten,
2018
- signer_acknowledged: input.signerAcknowledged,
2019
- local_mcp_acknowledged: input.localMcpAcknowledged,
2020
- activation_command_available: input.activationCommandAvailable,
2021
- skill_installed: input.skillInstalled,
2022
- probe_result: input.probeResult,
2023
- restart_required: input.restartRequired,
2024
- next_user_action: input.nextUserAction,
2025
- error_code: input.errorCode ?? null,
2026
- environment_label: input.environmentLabel
2027
- })
2028
- });
2029
- }
2030
- };
2031
- }
2032
- var ConnectRequestError = class extends Error {
2033
- constructor(message, status) {
2034
- super(message);
2035
- this.status = status;
2036
- this.name = "ConnectRequestError";
2037
- }
2038
- status;
2039
- };
2040
- async function request(fetchImpl, url, init) {
2041
- const response = await fetchImpl(url, {
2042
- ...init,
2043
- headers: {
2044
- "Content-Type": "application/json",
2045
- ...init.headers ?? {}
2046
- }
2047
- });
2048
- const text = await response.text();
2049
- const body = text ? JSON.parse(text) : null;
2050
- if (!response.ok) {
2051
- const message = body?.error ?? body?.message ?? `${response.status} ${response.statusText}`;
2052
- throw new ConnectRequestError(`Haven setup request failed: ${message}`, response.status);
2053
- }
2054
- return body;
2055
- }
2056
- function generateDelegateKey() {
2057
- return delegateKeyFromPrivateKey(Wallet.createRandom().privateKey);
2058
- }
2059
- function delegateKeyFromPrivateKey(privateKey) {
2060
- const wallet = new Wallet(privateKey);
2061
- return {
2062
- privateKey: wallet.privateKey,
2063
- address: wallet.address,
2064
- signChallenge: (message) => wallet.signMessage(message)
3100
+ id: "rekey_pending",
3101
+ label,
3102
+ ok: true,
3103
+ 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
2065
3104
  };
2066
3105
  }
2067
- function generateAgentApiKey() {
2068
- return `sk_agent_${crypto.randomBytes(24).toString("hex")}`;
2069
- }
2070
- function hashAgentApiKey(apiKey) {
2071
- return crypto.createHash("sha256").update(apiKey).digest("hex");
2072
- }
2073
- function agentApiKeyPrefix(apiKey) {
2074
- return apiKey.slice(0, 12);
2075
- }
2076
-
2077
- // src/runtime.ts
2078
- init_redact();
2079
- init_server_names();
2080
- async function preflightCredentialStorage(input = {}) {
2081
- const directory = defaultCredentialRoot(input.baseDir);
2082
- await mkdir(directory, { recursive: true, mode: 448 });
2083
- await restrictPermissions(directory, 448, input.warn);
2084
- const probePath = join(directory, `.haven-connect-preflight-${crypto.randomBytes(8).toString("hex")}`);
2085
- try {
2086
- await writeOwnerOnlyJson(probePath, { ok: true }, input.warn);
2087
- } finally {
2088
- await rm(probePath, { force: true }).catch(() => void 0);
2089
- }
2090
- return directory;
2091
- }
2092
- async function writeCredentialFiles(input) {
2093
- const directory = defaultAgentDirectory(input.serverName ?? input.agentId, input.baseDir);
2094
- await mkdir(directory, { recursive: true, mode: 448 });
2095
- await restrictPermissions(directory, 448, input.warn);
2096
- const identityPath = join(directory, "identity.json");
2097
- const signerPath = join(directory, "signer.json");
2098
- const agentPath = join(directory, "agent.json");
2099
- await assertDoesNotExist(identityPath);
2100
- await assertDoesNotExist(signerPath);
2101
- await assertDoesNotExist(agentPath);
2102
- await writeOwnerOnlyJson(
2103
- signerPath,
2104
- {
2105
- delegate_key: input.delegateKey,
2106
- delegate_address: input.delegateAddress,
2107
- agent_id: input.agentId,
2108
- safe_address: input.safeAddress,
2109
- chain_id: input.chainId,
2110
- network: input.network,
2111
- x402_binding_signer: input.x402BindingSigner,
2112
- note: "Local signer credential. Haven backend never receives this private key."
2113
- },
2114
- input.warn
2115
- );
2116
- try {
2117
- await writeOwnerOnlyJson(
2118
- identityPath,
2119
- {
2120
- api_key: input.apiKey,
2121
- agent_id: input.agentId,
2122
- safe_address: input.safeAddress,
2123
- chain_id: input.chainId,
2124
- network: input.network,
2125
- api_url: input.apiUrl,
2126
- hosted_mcp_url: input.hostedMcpUrl,
2127
- agent_budget: input.agentBudget,
2128
- note: "Haven API key identifies the agent only. It cannot spend without the local signer key and on-chain Haven wallet rules."
2129
- },
2130
- input.warn
2131
- );
2132
- } catch (err) {
2133
- await rm(signerPath, { force: true }).catch(() => void 0);
2134
- throw err;
2135
- }
2136
- try {
2137
- await writeOwnerOnlyJson(
2138
- agentPath,
2139
- {
2140
- agent_id: input.agentId,
2141
- delegate_address: input.delegateAddress,
2142
- safe_address: input.safeAddress,
2143
- chain_id: input.chainId,
2144
- network: input.network,
2145
- agent_budget: input.agentBudget,
2146
- 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."
2147
- },
2148
- input.warn
2149
- );
2150
- } catch (err) {
2151
- await rm(signerPath, { force: true }).catch(() => void 0);
2152
- await rm(identityPath, { force: true }).catch(() => void 0);
2153
- await rm(agentPath, { force: true }).catch(() => void 0);
2154
- throw err;
2155
- }
2156
- return { directory, identityPath, signerPath, agentPath };
2157
- }
2158
- async function assertServerSlugAvailable(serverName, baseDir) {
2159
- const directory = defaultAgentDirectory(serverName, baseDir);
3106
+ async function readIdentity(directory) {
2160
3107
  try {
2161
- await stat(join(directory, "identity.json"));
3108
+ return JSON.parse(await readFile(join(directory, "identity.json"), "utf8"));
2162
3109
  } catch {
2163
- return;
3110
+ return void 0;
2164
3111
  }
2165
- throw new Error(
2166
- `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.`
2167
- );
2168
- }
2169
- function defaultAgentDirectory(agentId, baseDir = join(homedir(), ".haven", "agents")) {
2170
- return resolve(defaultCredentialRoot(baseDir), safePathPart(agentId));
2171
- }
2172
- function defaultCredentialRoot(baseDir = join(homedir(), ".haven", "agents")) {
2173
- return resolve(baseDir);
2174
- }
2175
- async function writeOwnerOnlyJson(path, value, warn) {
2176
- const json = JSON.stringify(dropUndefined(value), null, 2);
2177
- await writeFile(path, `${json}
2178
- `, { mode: 384, flag: "wx" });
2179
- await restrictPermissions(path, 384, warn);
2180
- }
2181
- function safePathPart(value) {
2182
- return value.replace(/[^A-Za-z0-9_.-]/g, "_");
2183
- }
2184
- function dropUndefined(value) {
2185
- return Object.fromEntries(Object.entries(value).filter(([, field]) => field !== void 0));
2186
3112
  }
2187
- async function assertDoesNotExist(path) {
3113
+ async function checksForAgent(entry, input, deps) {
3114
+ const { directory, identity, sidecar } = entry;
3115
+ const checks = [];
3116
+ let signerCapabilities;
3117
+ let signerFile;
2188
3118
  try {
2189
- await access(path);
2190
- } catch (err) {
2191
- if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") return;
2192
- throw err;
3119
+ const parsed = JSON.parse(await readFile(join(directory, "signer.json"), "utf8"));
3120
+ signerFile = typeof parsed === "object" && parsed !== null ? parsed : void 0;
3121
+ } catch {
3122
+ signerFile = void 0;
2193
3123
  }
2194
- throw new Error(`Refusing to overwrite existing Haven credential file: ${path}`);
2195
- }
2196
- async function restrictPermissions(path, mode, warn) {
2197
- try {
2198
- await chmod(path, mode);
2199
- } catch (err) {
2200
- warn?.(
2201
- `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)}`
2202
- );
3124
+ const credentialsOk = Boolean(identity?.api_key) && signerFile !== void 0;
3125
+ checks.push({
3126
+ id: "credentials",
3127
+ label: "Agent credentials",
3128
+ ok: credentialsOk,
3129
+ detail: credentialsOk ? `identity.json and signer.json parse (agent ${identity?.agent_id ?? "unknown"})` : "identity.json or signer.json is missing or unparseable.",
3130
+ ...credentialsOk ? {} : { repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.` }
3131
+ });
3132
+ if (!sidecar) {
3133
+ checks.push({
3134
+ id: "signer_runtime",
3135
+ label: "Signer runtime (preinstalled wrapper)",
3136
+ ok: false,
3137
+ detail: "No signer-runtime.json sidecar \u2014 the pinned signer runtime was never prepared (or a pre-#1586 npx config).",
3138
+ repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
3139
+ });
3140
+ } else {
3141
+ const matches = await installedRuntimeMatches(sidecar.runtime_directory, sidecar.cli_path);
3142
+ const versionOk = sidecar.signer_version === MCP_RUNTIME_MANIFEST.signerVersion;
3143
+ const ok = matches && versionOk;
3144
+ checks.push({
3145
+ id: "signer_runtime",
3146
+ label: "Signer runtime (preinstalled wrapper)",
3147
+ ok,
3148
+ detail: ok ? `Installed ${sidecar.signer_package}@${sidecar.signer_version} at ${sidecar.runtime_directory}` : matches ? `Installed version ${sidecar.signer_version} does not match the connector's pinned ${MCP_RUNTIME_MANIFEST.signerVersion}.` : `Runtime directory is stale or empty (${sidecar.runtime_directory}) \u2014 the CLI or package versions are missing.`,
3149
+ ...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
3150
+ });
2203
3151
  }
2204
- }
2205
-
2206
- // src/runtime-install.ts
2207
- init_config_writers();
2208
- init_server_names();
2209
- async function acknowledgeLocalMcpConsent(identityPath, signerPath, log) {
2210
- try {
2211
- const input = await buildLocalMcpConsentInput(identityPath, signerPath);
2212
- const decision = await ensureConsent(input, {
2213
- credentialsPath: identityPath,
2214
- writeAck: true,
2215
- out: log ? { write: (chunk) => writeLogChunk(log, chunk) } : void 0
3152
+ const hostedUrl = identity?.hosted_mcp_url ?? (identity?.api_url ? `${identity.api_url}/mcp` : void 0);
3153
+ if (identity?.api_key && hostedUrl) {
3154
+ const probe = await (deps.probeHosted ?? probeHostedMcpTools)(identity.api_key, hostedUrl, deps.fetch);
3155
+ checks.push({
3156
+ id: "hosted_mcp",
3157
+ label: "Hosted Haven MCP",
3158
+ ok: probe.status === "ok",
3159
+ detail: probe.status === "ok" ? `Reachable and authorized (${hostedUrl}).` : `Probe failed: ${probe.status} (${hostedUrl}).`,
3160
+ ...probe.status === "ok" ? {} : {
3161
+ 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."
3162
+ }
3163
+ });
3164
+ } else {
3165
+ checks.push({
3166
+ id: "hosted_mcp",
3167
+ label: "Hosted Haven MCP",
3168
+ ok: false,
3169
+ detail: "No stored API key / hosted MCP URL to probe with.",
3170
+ repair: `Re-run the full setup: ${RERUN} --setup <token>.`
2216
3171
  });
2217
- return {
2218
- acknowledged: decision.ok,
2219
- hash: decision.hash,
2220
- reason: decision.reason
2221
- };
2222
- } catch (err) {
2223
- return {
2224
- acknowledged: false,
2225
- error: err instanceof Error ? err.message : String(err)
2226
- };
2227
3172
  }
2228
- }
2229
- async function getLocalMcpConsentStatus(identityPath, signerPath) {
2230
- try {
2231
- const input = await buildLocalMcpConsentInput(identityPath, signerPath);
2232
- const hash = computeConsentHash(input);
2233
- const stored = await readLocalMcpAckFile(localMcpAckPath(identityPath));
2234
- if (stored === hash) {
2235
- return { acknowledged: true, hash, reason: "ack_file_match" };
2236
- }
2237
- return {
2238
- acknowledged: false,
2239
- hash,
2240
- reason: stored ? "ack_file_mismatch" : "ack_file_missing"
2241
- };
2242
- } catch (err) {
2243
- return {
2244
- acknowledged: false,
2245
- error: err instanceof Error ? err.message : String(err)
2246
- };
2247
- }
2248
- }
2249
- function localMcpAckPath(identityPath) {
2250
- return resolve(`${identityPath}.ack.json`);
2251
- }
2252
- async function buildLocalMcpConsentInput(identityPath, signerPath) {
2253
- const credentials = await loadCredentials({ identityPath, signerPath });
2254
- const unavailableDuringSetup = {
2255
- getAllowances: async () => {
2256
- throw new Error("Haven approval is not complete yet.");
3173
+ const localDelegate = typeof signerFile?.delegate_address === "string" ? signerFile.delegate_address : void 0;
3174
+ let hostedDelegateAddress;
3175
+ if (identity?.api_key && identity.api_url) {
3176
+ const probe = await (deps.probeHostedIdentity ?? probeHostedAgentIdentity)(
3177
+ identity.api_key,
3178
+ identity.api_url,
3179
+ deps.fetch
3180
+ );
3181
+ if (probe.status === "ok") hostedDelegateAddress = probe.delegateAddress;
3182
+ if (probe.status !== "ok") {
3183
+ checks.push({
3184
+ id: "identity_match",
3185
+ label: "Hosted identity matches the local signing key",
3186
+ ok: false,
3187
+ 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.`,
3188
+ 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}`
3189
+ });
3190
+ } else if (!localDelegate) {
3191
+ checks.push({
3192
+ id: "identity_match",
3193
+ label: "Hosted identity matches the local signing key",
3194
+ ok: false,
3195
+ detail: "signer.json holds no delegate_address to compare against the hosted identity.",
3196
+ repair: `Re-run the full setup with a fresh token: ${RERUN} --setup <token>.`
3197
+ });
3198
+ } else {
3199
+ const same = probe.delegateAddress?.toLowerCase() === localDelegate.toLowerCase();
3200
+ checks.push({
3201
+ id: "identity_match",
3202
+ label: "Hosted identity matches the local signing key",
3203
+ ok: same,
3204
+ 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.`,
3205
+ ...same ? {} : {
3206
+ 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.`
3207
+ }
3208
+ });
2257
3209
  }
2258
- };
2259
- return consentInputFromClient(
2260
- unavailableDuringSetup,
2261
- {
2262
- apiKey: credentials.apiKey,
2263
- apiUrl: credentials.apiUrl,
2264
- agentId: credentials.agentId,
2265
- safeAddress: credentials.safeAddress,
2266
- delegateAddress: credentials.delegateAddress,
2267
- chainId: credentials.chainId,
2268
- allowanceSummary: credentials.allowanceSummary
2269
- },
2270
- registeredToolNames()
2271
- );
2272
- }
2273
- async function readLocalMcpAckFile(path) {
2274
- try {
2275
- const parsed = JSON.parse(await readFile(path, "utf8"));
2276
- return typeof parsed.ack === "string" ? parsed.ack : null;
2277
- } catch {
2278
- return null;
2279
3210
  }
2280
- }
2281
- function writeLogChunk(log, chunk) {
2282
- const message = String(chunk).trimEnd();
2283
- if (message) log(message);
2284
- }
2285
-
2286
- // src/runtime-install.ts
2287
- init_probes();
2288
-
2289
- // src/local-mcp-runtime.ts
2290
- init_signer_runtime();
2291
- init_runtime_manifest();
2292
- var execFileAsync2 = promisify(execFile);
2293
- var UnsupportedNodeVersionError = class extends Error {
2294
- code = "local_mcp_unsupported_node_version";
2295
- nodeVersion;
2296
- minimumNodeVersion;
2297
- constructor(nodeVersion, minimumNodeVersion, subject = "Haven setup") {
2298
- super(unsupportedNodeVersionMessage({ subject, nodeVersion, minimumNodeVersion }));
2299
- this.name = "UnsupportedNodeVersionError";
2300
- this.nodeVersion = nodeVersion;
2301
- this.minimumNodeVersion = minimumNodeVersion;
3211
+ const pending = await inspectRekeyPending(directory, deps.now?.() ?? Date.now());
3212
+ if (pending) {
3213
+ checks.push(rekeyPendingCheck(pending, hostedDelegateAddress, input.runtime, sidecar?.server_name));
2302
3214
  }
2303
- };
2304
- async function prepareLocalMcpRuntime(input, deps = {}) {
2305
- assertSupportedNodeVersion(input.nodeVersion);
2306
- const homeDir = input.homeDir ?? homedir();
2307
- const runtimeDirectory = resolve(homeDir, ".haven", "mcp-runtime", MCP_RUNTIME_MANIFEST.mcpVersion);
2308
- const npmCacheDirectory = resolve(homeDir, ".haven", "npm-cache");
2309
- const cliPath = join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "dist", "cli.js");
2310
- const messages = [];
2311
- await mkdir(runtimeDirectory, { recursive: true, mode: 448 });
2312
- await chmod(runtimeDirectory, 448).catch(() => void 0);
2313
- await mkdir(npmCacheDirectory, { recursive: true, mode: 448 });
2314
- await chmod(npmCacheDirectory, 448).catch(() => void 0);
2315
- if (await installedRuntimeMatches2(runtimeDirectory, cliPath)) {
2316
- messages.push(`Using existing local Haven MCP runtime ${mcpPackageSpec()}.`);
3215
+ if (sidecar) {
3216
+ const consent = await getLocalSignerConsentStatus(join(directory, "signer.json"));
3217
+ if (!consent.acknowledged) {
3218
+ checks.push({
3219
+ id: "signer_process",
3220
+ label: "Signer stdio handshake",
3221
+ ok: false,
3222
+ detail: "The local-tools consent is not acknowledged, so the signer refuses to start (by design).",
3223
+ repair: `Run: ${RERUN} --ack-local-tools --setup <token> (or re-run your original setup command with --ack-local-tools).`
3224
+ });
3225
+ } else {
3226
+ const probe = await (deps.probeSignerTools ?? probeLocalMcpTools)(
3227
+ sidecar.wrapper_path,
3228
+ [],
3229
+ MCP_RUNTIME_MANIFEST.requiredSignerTools
3230
+ );
3231
+ const experimental = probe.capabilities?.experimental ?? probe.capabilities;
3232
+ const compat = experimental?.["haven/signer-compatibility"];
3233
+ signerCapabilities = compat ? { "haven/signer-compatibility": compat } : void 0;
3234
+ const compatDetail = compat ? ` Compat: x402 expected-context v${JSON.stringify(compat.x402_expected_context_versions ?? "?")}.` : "";
3235
+ checks.push({
3236
+ id: "signer_process",
3237
+ label: "Signer stdio handshake",
3238
+ ok: probe.status === "ok",
3239
+ detail: probe.status === "ok" ? `Signer started, listed ${probe.toolNames?.length ?? 0} tools${probe.serverInfo?.version ? ` (v${probe.serverInfo.version})` : ""}.${compatDetail}` : `Handshake failed: ${probe.status}.`,
3240
+ ...probe.status === "ok" ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
3241
+ });
3242
+ }
2317
3243
  } else {
2318
- await installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps);
2319
- messages.push(`Installed local Haven MCP runtime ${mcpPackageSpec()}.`);
2320
- }
2321
- await assertFileExists2(cliPath, "local Haven MCP CLI");
2322
- const wrapperPath = join(input.credentialDirectory, "bin", "haven-mcp");
2323
- await writeWrapper2({
2324
- wrapperPath,
2325
- cliPath,
2326
- identityPath: input.identityPath,
2327
- signerPath: input.signerPath
2328
- });
2329
- await writeRuntimeSidecar2({
2330
- path: join(input.credentialDirectory, "mcp-runtime.json"),
2331
- wrapperPath,
2332
- runtimeDirectory,
2333
- npmCacheDirectory,
2334
- cliPath,
2335
- serverName: input.serverName
2336
- });
2337
- messages.push(`Prepared stable local Haven MCP wrapper: ${wrapperPath}`);
2338
- return {
2339
- command: wrapperPath,
2340
- args: [],
2341
- wrapperPath,
2342
- runtimeDirectory,
2343
- npmCacheDirectory,
2344
- cliPath,
2345
- messages
2346
- };
2347
- }
2348
- function assertSupportedNodeVersion(nodeVersion = process.versions.node, minimumNodeVersion = MCP_RUNTIME_MANIFEST.minimumNodeVersion, subject = "Haven setup") {
2349
- if (!isSupportedNodeVersion(nodeVersion, minimumNodeVersion)) {
2350
- throw new UnsupportedNodeVersionError(nodeVersion, minimumNodeVersion, subject);
3244
+ checks.push({
3245
+ id: "signer_process",
3246
+ label: "Signer stdio handshake",
3247
+ ok: false,
3248
+ detail: "Skipped \u2014 no prepared signer runtime to probe.",
3249
+ repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
3250
+ });
2351
3251
  }
3252
+ return { checks, ...signerCapabilities ? { signerCapabilities } : {} };
2352
3253
  }
2353
- async function installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps) {
2354
- const { runCommand, onProgress } = deps;
2355
- const baseArgs = [
2356
- "install",
2357
- "--prefix",
2358
- runtimeDirectory,
2359
- "--no-audit",
2360
- "--no-fund",
2361
- "--omit=dev",
2362
- "--prefer-offline",
2363
- mcpPackageSpec(),
2364
- sdkPackageSpec()
2365
- ];
2366
- const run = async (args) => {
2367
- const startedAt = Date.now();
2368
- const heartbeat = setInterval(() => {
2369
- const seconds = Math.round((Date.now() - startedAt) / 1e3);
2370
- onProgress?.(`Still installing the local Haven MCP runtime\u2026 (${seconds}s \u2014 a cold cache can take several minutes)`);
2371
- }, SIGNER_INSTALL_HEARTBEAT_MS);
2372
- heartbeat.unref?.();
2373
- try {
2374
- if (runCommand) await runCommand("npm", args);
2375
- else await execFileAsync2("npm", args, { timeout: SIGNER_INSTALL_TIMEOUT_MS, maxBuffer: 1024 * 1024 });
2376
- } finally {
2377
- clearInterval(heartbeat);
2378
- }
2379
- };
2380
- try {
2381
- await run(baseArgs);
2382
- } catch {
3254
+ async function runDoctor(input, deps = {}) {
3255
+ const homeDir = deps.homeDir ?? homedir();
3256
+ const checks = [];
3257
+ let signerCapabilities;
3258
+ const { directory, others, parkedOnly } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
3259
+ const configPath = runtimeConfigPathFor(input.runtime, homeDir);
3260
+ let configText = null;
3261
+ if (configPath !== null) {
2383
3262
  try {
2384
- await run([...baseArgs, "--cache", npmCacheDirectory]);
2385
- } catch (err) {
2386
- throw new Error(`Could not install local Haven MCP runtime ${mcpPackageSpec()}: ${err instanceof Error ? err.message : String(err)}`);
3263
+ configText = await readFile(configPath, "utf8");
3264
+ } catch {
3265
+ configText = null;
2387
3266
  }
2388
3267
  }
2389
- }
2390
- async function installedRuntimeMatches2(runtimeDirectory, cliPath) {
2391
- try {
2392
- await assertFileExists2(cliPath, "local Haven MCP CLI");
2393
- const [mcpPackage, sdkPackage] = await Promise.all([
2394
- readPackageJson2(join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "package.json")),
2395
- readPackageJson2(join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
2396
- ]);
2397
- return mcpPackage.version === MCP_RUNTIME_MANIFEST.mcpVersion && sdkPackage.version === MCP_RUNTIME_MANIFEST.sdkVersion;
2398
- } catch {
2399
- return false;
3268
+ const allDirectories = directory ? [directory, ...others] : others;
3269
+ let bareOwnerExists = false;
3270
+ for (const dir of allDirectories) {
3271
+ const sidecar = await readRuntimeSidecar(dir);
3272
+ if (!sidecar?.server_name && sidecar?.wrapper_path && configText?.includes(sidecar.wrapper_path)) {
3273
+ bareOwnerExists = true;
3274
+ break;
3275
+ }
2400
3276
  }
2401
- }
2402
- async function readPackageJson2(path) {
2403
- return JSON.parse(await readFile(path, "utf8"));
2404
- }
2405
- async function writeWrapper2(input) {
2406
- await mkdir(dirname(input.wrapperPath), { recursive: true, mode: 448 });
2407
- await chmod(dirname(input.wrapperPath), 448).catch(() => void 0);
2408
- const source = [
2409
- "#!/usr/bin/env node",
2410
- "import { spawn } from 'node:child_process'",
2411
- "",
2412
- `const cliPath = ${JSON.stringify(input.cliPath)}`,
2413
- `const identityPath = ${JSON.stringify(input.identityPath)}`,
2414
- `const signerPath = ${JSON.stringify(input.signerPath)}`,
2415
- "",
2416
- "const child = spawn(process.execPath, [cliPath, '--identity', identityPath, '--signer', signerPath, ...process.argv.slice(2)], {",
2417
- " stdio: 'inherit',",
2418
- "})",
2419
- "",
2420
- "child.on('exit', (code, signal) => {",
2421
- " if (signal) process.kill(process.pid, signal)",
2422
- " else process.exit(code ?? 1)",
2423
- "})",
2424
- ""
2425
- ].join("\n");
2426
- await writeFile(input.wrapperPath, source, { mode: 448 });
2427
- await chmod(input.wrapperPath, 448).catch(() => void 0);
2428
- }
2429
- async function writeRuntimeSidecar2(input) {
2430
- const value = {
2431
- ...input.serverName ? { server_name: input.serverName } : {},
2432
- mcp_package: MCP_RUNTIME_MANIFEST.mcpPackage,
2433
- mcp_version: MCP_RUNTIME_MANIFEST.mcpVersion,
2434
- sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
2435
- sdk_version: MCP_RUNTIME_MANIFEST.sdkVersion,
2436
- minimum_node_version: MCP_RUNTIME_MANIFEST.minimumNodeVersion,
2437
- wrapper_path: input.wrapperPath,
2438
- runtime_directory: input.runtimeDirectory,
2439
- npm_cache_directory: input.npmCacheDirectory,
2440
- cli_path: input.cliPath
2441
- };
2442
- await writeFile(input.path, `${JSON.stringify(value, null, 2)}
2443
- `, { mode: 384 });
2444
- await chmod(input.path, 384).catch(() => void 0);
2445
- }
2446
- async function assertFileExists2(path, label) {
2447
- try {
2448
- await access(path);
2449
- } catch {
2450
- throw new Error(`Missing ${label}: ${path}`);
2451
- }
2452
- }
2453
-
2454
- // src/runtime-install.ts
2455
- init_signer_runtime();
2456
- init_runtime_manifest();
2457
- var CODEX_AGENTS_BEGIN_MARKER = "<!-- BEGIN haven-pay (managed by @haven_ai/connect; edits inside this section are overwritten on re-setup) -->";
2458
- var CODEX_AGENTS_END_MARKER = "<!-- END haven-pay -->";
2459
- async function installSkillForRuntime(runtime, deps = {}) {
2460
- switch (runtime) {
2461
- case "claude-code":
2462
- return installSkillFile(
2463
- resolve(deps.homeDir ?? homedir(), ".claude", "skills", SKILL_FOLDER_NAME),
2464
- "~/.claude/skills/haven-pay"
2465
- );
2466
- case "hermes":
2467
- return installSkillFile(
2468
- join(hermesHome(deps), "skills", SKILL_FOLDER_NAME),
2469
- "the Hermes skills folder"
2470
- );
2471
- case "codex-cli":
2472
- case "codex-desktop":
2473
- return installCodexAgentsSection(deps);
2474
- default:
2475
- return void 0;
2476
- }
2477
- }
2478
- async function installSkillFile(skillDir, label) {
2479
- try {
2480
- await mkdir(skillDir, { recursive: true });
2481
- const target = join(skillDir, "SKILL.md");
2482
- await writeFile(target, HAVEN_SKILL_MD, "utf8");
2483
- return {
2484
- installed: true,
2485
- target,
2486
- messages: [`Installed the generic Haven payment skill (${label}). It contains no secrets.`]
2487
- };
2488
- } catch (err) {
2489
- return {
2490
- installed: false,
2491
- messages: [
2492
- `Could not install the Haven payment skill: ${err instanceof Error ? err.message : String(err)}. Download it from the Haven dashboard instead.`
2493
- ]
2494
- };
2495
- }
2496
- }
2497
- async function installCodexAgentsSection(deps) {
2498
- try {
2499
- const codexDir = resolve(deps.homeDir ?? homedir(), ".codex");
2500
- const target = join(codexDir, "AGENTS.md");
2501
- await mkdir(codexDir, { recursive: true });
2502
- const existing = await readFile(target, "utf8").catch(() => null);
2503
- const next = upsertManagedSection(existing, codexManagedSection());
2504
- if (next !== existing) {
2505
- await writeFile(target, next, "utf8");
3277
+ const inventory = [];
3278
+ const capabilitiesByDirectory = /* @__PURE__ */ new Map();
3279
+ const primaryChecksById = /* @__PURE__ */ new Map();
3280
+ for (const dir of allDirectories) {
3281
+ const identity = await readIdentity(dir);
3282
+ const sidecar = await readRuntimeSidecar(dir);
3283
+ const tombstone = await readAgentTombstone(dir);
3284
+ const slug = sidecar?.server_name;
3285
+ const names = serverNamesFor(slug);
3286
+ const rekeyPending = await inspectRekeyPending(dir, deps.now?.() ?? Date.now());
3287
+ if (!identity?.api_key) {
3288
+ const agentId = tombstone?.agent_id ?? rekeyPending?.agentId;
3289
+ inventory.push({
3290
+ ...slug ? { slug } : {},
3291
+ ...agentId ? { agentId } : {},
3292
+ directory: dir,
3293
+ // A tombstone is a deliberate record and outranks the discovery tell:
3294
+ // a retired directory that also holds a parked key stays `retired`.
3295
+ classification: tombstone ? "retired" : parkedOnly.has(dir) ? "parked" : "orphaned",
3296
+ checks: rekeyPending ? [rekeyPendingCheck(rekeyPending, void 0, input.runtime, slug)] : [],
3297
+ ...rekeyPending ? { rekeyPending } : {}
3298
+ });
3299
+ continue;
2506
3300
  }
2507
- return {
2508
- installed: true,
2509
- target,
2510
- messages: [
2511
- "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."
2512
- ]
2513
- };
2514
- } catch (err) {
2515
- return {
2516
- installed: false,
2517
- messages: [
2518
- `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.`
2519
- ]
2520
- };
2521
- }
2522
- }
2523
- function codexManagedSection() {
2524
- return `${CODEX_AGENTS_BEGIN_MARKER}
2525
-
2526
- ${HAVEN_SKILL_BODY_MD.trimEnd()}
2527
-
2528
- ${CODEX_AGENTS_END_MARKER}
2529
- `;
2530
- }
2531
- function upsertManagedSection(existing, section) {
2532
- if (existing === null || existing.trim() === "") return section;
2533
- const begins = markerLineIndexes(existing, CODEX_AGENTS_BEGIN_MARKER);
2534
- const ends = markerLineIndexes(existing, CODEX_AGENTS_END_MARKER);
2535
- if (begins.length === 1 && ends.length === 1 && ends[0] > begins[0]) {
2536
- const afterEnd = ends[0] + CODEX_AGENTS_END_MARKER.length;
2537
- const tail = existing.startsWith("\r\n", afterEnd) ? existing.slice(afterEnd + 2) : existing.startsWith("\n", afterEnd) ? existing.slice(afterEnd + 1) : existing.slice(afterEnd);
2538
- return existing.slice(0, begins[0]) + section + tail;
2539
- }
2540
- if (begins.length > 0 || ends.length > 0) {
2541
- throw new Error(
2542
- "found a damaged Haven marker section (orphaned or duplicated markers); remove the leftover marker lines and re-run setup"
2543
- );
2544
- }
2545
- return `${existing.replace(/\n*$/, "\n\n")}${section}`;
2546
- }
2547
- function markerLineIndexes(text, marker) {
2548
- const indexes = [];
2549
- for (let from = 0; ; ) {
2550
- const at = text.indexOf(marker, from);
2551
- if (at === -1) return indexes;
2552
- if (at === 0 || text[at - 1] === "\n") indexes.push(at);
2553
- from = at + marker.length;
2554
- }
2555
- }
2556
- function hermesHome(deps) {
2557
- const env = deps.env ?? process.env;
2558
- return env.HERMES_HOME ?? join(deps.homeDir ?? homedir(), ".hermes");
2559
- }
2560
-
2561
- // src/runtime-install.ts
2562
- init_runtime_registry();
2563
- init_signer_consent();
2564
- var execFileAsync3 = promisify(execFile);
2565
- async function installRuntime(input, deps = {}) {
2566
- const runtime = normalizeRuntime(input.runtime, deps.env);
2567
- const profile = runtimeProfile(runtime, deps.env);
2568
- const progress = deps.onProgress ?? (() => void 0);
2569
- const localRuntime = input.localMcp === true && supportsLocalMcp(runtime);
2570
- const consentMessages = [];
2571
- const localMcpConsent = localRuntime ? await resolveLocalMcpConsent(input, consentMessages) : void 0;
2572
- const signerConsent = localRuntime ? void 0 : await resolveSignerConsent(input, consentMessages);
2573
- if (runtime === "other") {
2574
- const signerCredentialReady2 = await probeLocalSignerCredential(input.signerPath);
2575
- const signerReady = signerCredentialReady2 && signerConsent?.acknowledged;
2576
- return {
2577
- runtime,
2578
- runtimeMcpMode: "manual",
2579
- hostedMcpConfigured: false,
2580
- localSignerConfigured: false,
2581
- localMcpConfigured: false,
2582
- probeResult: signerReady ? "manual_runtime_setup_required_local_signer_ready" : "manual_runtime_setup_required_local_signer_unavailable",
2583
- restartRequired: true,
2584
- nextUserAction: "return_to_haven_for_wallet_approval_then_configure_runtime",
2585
- errorCode: "manual_runtime_setup_required",
2586
- configTarget: "manual runtime setup",
2587
- signerAcknowledged: signerConsent?.acknowledged,
2588
- localMcpAcknowledged: false,
2589
- messages: [
2590
- ...consentMessages,
2591
- "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.",
2592
- ` identity (hosted MCP Bearer): ${input.identityPath}`,
2593
- ` signer (local signing key): ${input.signerPath}`,
2594
- "After wallet approval, wire the runtime to Haven by reference:",
2595
- ` 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}`,
2596
- ` Fully local MCP (no hosted dependency): npx -y ${mcpPackageSpec()} --identity ${input.identityPath} --signer ${input.signerPath}`
2597
- ]
3301
+ const wired = agentIsWired(configText, names, slug, identity, sidecar, dir === directory, bareOwnerExists);
3302
+ const entry = {
3303
+ ...slug ? { slug } : {},
3304
+ ...identity.agent_id ? { agentId: identity.agent_id } : {},
3305
+ directory: dir,
3306
+ classification: wired ? "wired" : "superseded",
3307
+ checks: [],
3308
+ ...rekeyPending ? { rekeyPending } : {}
2598
3309
  };
2599
- }
2600
- let localRuntimeInstall;
2601
- let localRuntimeError;
2602
- if (localRuntime) {
2603
- try {
2604
- localRuntimeInstall = await prepareRuntimeForLocalMcp(input, deps);
2605
- } catch (err) {
2606
- localRuntimeError = err;
3310
+ if (wired) {
3311
+ const result = await checksForAgent({ directory: dir, identity, sidecar }, input, deps);
3312
+ entry.checks = result.checks;
3313
+ capabilitiesByDirectory.set(dir, result.signerCapabilities);
3314
+ } else if (rekeyPending) {
3315
+ entry.checks = [rekeyPendingCheck(rekeyPending, void 0, input.runtime, slug)];
2607
3316
  }
3317
+ inventory.push(entry);
2608
3318
  }
2609
- if (localRuntimeError) {
2610
- const errorCode2 = localRuntimePrepareErrorCode(localRuntimeError);
2611
- return {
2612
- runtime,
2613
- runtimeMcpMode: "local_stdio",
2614
- hostedMcpConfigured: false,
2615
- localSignerConfigured: false,
2616
- localMcpConfigured: false,
2617
- probeResult: errorCode2 === "local_mcp_unsupported_node_version" ? "local_stdio_mcp_unsupported_node_version" : "local_stdio_mcp_runtime_install_failed",
2618
- restartRequired: true,
2619
- nextUserAction: nextAction(runtime, profile.restartMode, errorCode2),
2620
- errorCode: errorCode2,
2621
- configTarget: profile.label,
2622
- signerAcknowledged: signerConsent?.acknowledged,
2623
- localMcpAcknowledged: localMcpConsent?.acknowledged,
2624
- activationCommand: void 0,
2625
- messages: [
2626
- ...consentMessages,
2627
- `Could not prepare local Haven MCP runtime: ${localRuntimeError instanceof Error ? localRuntimeError.message : String(localRuntimeError)}`
2628
- ]
2629
- };
3319
+ const wiredDirectories = inventory.filter((entry) => entry.classification === "wired").map((entry) => entry.directory);
3320
+ const primaryDirectory = input.credentialsDir ? directory : wiredDirectories.includes(directory ?? "") ? directory : wiredDirectories[0] ?? directory;
3321
+ if (primaryDirectory) {
3322
+ const primaryEntry = inventory.find((entry) => entry.directory === primaryDirectory);
3323
+ signerCapabilities = capabilitiesByDirectory.get(primaryDirectory);
3324
+ for (const check of primaryEntry?.checks ?? []) primaryChecksById.set(check.id, check);
2630
3325
  }
2631
- let signerCommand;
2632
- if (!localRuntime) {
2633
- progress("Getting the signer ready\u2026");
2634
- try {
2635
- const signerRuntime = await prepareSignerForRuntime(input, deps);
2636
- signerCommand = { command: signerRuntime.command, args: signerRuntime.args };
2637
- consentMessages.push(...signerRuntime.messages);
2638
- } catch (err) {
2639
- return {
2640
- runtime,
2641
- runtimeMcpMode: "hosted_plus_signer",
2642
- hostedMcpConfigured: false,
2643
- localSignerConfigured: false,
2644
- localMcpConfigured: false,
2645
- probeResult: "signer_runtime_install_failed",
2646
- restartRequired: false,
2647
- 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",
2648
- errorCode: "signer_runtime_install_failed",
2649
- configTarget: profile.label,
2650
- signerAcknowledged: signerConsent?.acknowledged,
2651
- localMcpAcknowledged: localMcpConsent?.acknowledged,
2652
- activationCommand: void 0,
2653
- signerRuntimePrepared: false,
2654
- messages: [
2655
- ...consentMessages,
2656
- `Could not pre-install the local Haven signer: ${err instanceof Error ? err.message : String(err)}`,
2657
- "No runtime configuration was written (fail-closed): a config pointing at an uninstalled signer looks wired but cannot start.",
2658
- "Re-run `npx @haven_ai/connect@alpha` to retry the setup."
2659
- ]
2660
- };
3326
+ if (!primaryDirectory) {
3327
+ checks.push({
3328
+ id: "credentials",
3329
+ label: "Agent credentials",
3330
+ ok: false,
3331
+ detail: "No agent credential directory with an identity.json under ~/.haven/agents.",
3332
+ repair: `Run the full setup once: ${RERUN} --setup <token from the Haven dashboard>.`
3333
+ });
3334
+ } else {
3335
+ const primaryIdentity = await readIdentity(primaryDirectory);
3336
+ const primarySidecar = await readRuntimeSidecar(primaryDirectory);
3337
+ if (!primaryChecksById.has("credentials")) {
3338
+ const result = await checksForAgent(
3339
+ { directory: primaryDirectory, identity: primaryIdentity, sidecar: primarySidecar },
3340
+ input,
3341
+ deps
3342
+ );
3343
+ signerCapabilities = result.signerCapabilities;
3344
+ for (const check of result.checks) primaryChecksById.set(check.id, check);
2661
3345
  }
2662
- }
2663
- const signerRuntimePrepared = localRuntime ? void 0 : signerCommand !== void 0;
2664
- progress("Setting up your Haven tools\u2026");
2665
- const configResult = runtime === "claude-code" ? localRuntime ? await configureClaudeCode(deps, localRuntimeInstall?.command ?? "", input.serverName) : await configureClaudeCodeHosted(deps, input, signerCommand) : await writeRuntimeConfig({
2666
- runtime,
2667
- hostedMcpUrl: input.hostedMcpUrl,
2668
- apiKey: input.apiKey,
2669
- identityPath: input.identityPath,
2670
- signerPath: input.signerPath,
2671
- serverName: input.serverName,
2672
- credentialDirectory: input.credentialDirectory,
2673
- localMcpCommand: localRuntimeInstall?.command,
2674
- signerCommand,
2675
- homeDir: deps.homeDir,
2676
- mode: localRuntime ? "local" : "hosted"
2677
- });
2678
- if (deps.onRuntimeConfigured) {
2679
- const signerCredentialOnDisk = await probeLocalSignerCredential(input.signerPath);
2680
- const earlyLocalMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialOnDisk && Boolean(localMcpConsent?.acknowledged);
2681
- const earlySignerOk = configResult.runtimeMcpMode === "local_stdio" ? earlyLocalMcpOk : configResult.signerConfigured && signerCredentialOnDisk && Boolean(signerConsent?.acknowledged);
2682
- try {
2683
- await deps.onRuntimeConfigured({
2684
- runtime,
2685
- runtimeMcpMode: configResult.runtimeMcpMode,
2686
- hostedMcpConfigured: configResult.hostedConfigured,
2687
- localSignerConfigured: earlySignerOk,
2688
- localMcpConfigured: earlyLocalMcpOk,
2689
- signerAcknowledged: signerConsent?.acknowledged,
2690
- localMcpAcknowledged: localMcpConsent?.acknowledged,
2691
- restartRequired: configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env),
2692
- nextUserAction: nextAction(runtime, profile.restartMode, configResult.errorCode),
2693
- errorCode: configResult.errorCode
2694
- });
2695
- } catch {
3346
+ for (const id of ["credentials", "signer_runtime"]) {
3347
+ const check = primaryChecksById.get(id);
3348
+ if (check) checks.push(check);
2696
3349
  }
2697
3350
  }
2698
- progress("Almost there \u2014 just confirming everything connects\u2026");
2699
- const localProbePromise = configResult.runtimeMcpMode === "local_stdio" && localRuntimeInstall ? runLocalMcpProbe(localRuntimeInstall, deps) : Promise.resolve(void 0);
2700
- const signerProbePromise = configResult.runtimeMcpMode !== "local_stdio" && signerCommand ? (deps.probeSignerTools ?? probeLocalMcpTools)(
2701
- signerCommand.command,
2702
- signerCommand.args,
2703
- MCP_RUNTIME_MANIFEST.requiredSignerTools
2704
- ) : Promise.resolve(void 0);
2705
- const [hostedProbe, signerCredentialReady, localMcpProbe, signerProbe] = await Promise.all([
2706
- configResult.hostedConfigured ? probeHostedMcpTools(input.apiKey, input.hostedMcpUrl, deps.fetch) : Promise.resolve({ status: "bad_response" }),
2707
- probeLocalSignerCredential(input.signerPath),
2708
- localProbePromise,
2709
- signerProbePromise
2710
- ]);
2711
- const hostedOk = configResult.hostedConfigured && hostedProbe.status === "ok";
2712
- const localMcpOk = configResult.runtimeMcpMode === "local_stdio" && configResult.localMcpConfigured && signerCredentialReady && Boolean(localMcpConsent?.acknowledged) && localMcpProbe?.status === "ok";
2713
- const signerOk = configResult.runtimeMcpMode === "local_stdio" ? localMcpOk : configResult.signerConfigured && signerCredentialReady && Boolean(signerConsent?.acknowledged) && // #1587: no handshake, no green. A signer command that was registered
2714
- // but not probed (manual topology) keeps the old semantics.
2715
- (signerProbe === void 0 || signerProbe.status === "ok");
2716
- const restartRequired = configResult.restartRequired || restartRequiredForRuntime(runtime, deps.env);
2717
- const errorCode = configResult.errorCode ?? (configResult.runtimeMcpMode === "local_stdio" ? localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbe?.status) : hostedMcpErrorCode(configResult.hostedConfigured, hostedProbe.status) ?? signerConsentErrorCode(signerCredentialReady, signerConsent) ?? signerProbeErrorCode(signerProbe));
2718
- 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."] : [];
2719
- const signerProbeMessages = signerProbe ? signerProbe.status === "ok" ? ["Verified local Haven signer with a stdio handshake."] : [
2720
- `Local Haven signer handshake failed: ${signerProbe.status}.`,
2721
- "Re-run `npx @haven_ai/connect@alpha` to repair the signer setup."
2722
- ] : [];
2723
- 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."] : [];
2724
- const skillInstall = !configResult.errorCode ? await installSkillForRuntime(runtime, { homeDir: deps.homeDir, env: deps.env }) : void 0;
2725
- return {
2726
- runtime,
2727
- runtimeMcpMode: configResult.runtimeMcpMode,
2728
- hostedMcpConfigured: hostedOk,
2729
- localSignerConfigured: signerOk,
2730
- localMcpConfigured: localMcpOk,
2731
- probeResult: buildProbeResult(configResult.runtimeMcpMode, configResult.hostedConfigured, hostedProbe.status, signerOk, localMcpOk, localMcpProbe?.status),
2732
- restartRequired,
2733
- nextUserAction: nextAction(runtime, profile.restartMode, errorCode),
2734
- errorCode,
2735
- configTarget: configResult.target,
2736
- signerAcknowledged: signerConsent?.acknowledged,
2737
- localMcpAcknowledged: localMcpConsent?.acknowledged,
2738
- activationCommand: configResult.activationCommand,
2739
- skillInstalled: skillInstall?.installed,
2740
- signerRuntimePrepared,
2741
- messages: [...consentMessages, ...localRuntimeInstall?.messages ?? [], ...configResult.messages, ...hostedProbeMessages, ...signerProbeMessages, ...localProbeMessages, ...skillInstall?.messages ?? []]
2742
- };
2743
- }
2744
- function runtimeInstallCapabilities(runtime, env = process.env) {
2745
- const profile = runtimeProfile(runtime, env);
2746
- return {
2747
- canWriteRuntimeConfig: profile.canWriteRuntimeConfig,
2748
- restartRequired: restartRequiredForRuntime(runtime, env)
2749
- };
2750
- }
2751
- async function configureClaudeCode(deps, localMcpCommand, serverName) {
2752
- const runCommand = deps.runCommand ?? defaultRunCommand;
2753
- const serverJson = JSON.stringify({
2754
- type: "stdio",
2755
- command: localMcpCommand,
2756
- args: [],
2757
- env: {}
2758
- });
2759
- try {
2760
- if (!localMcpCommand) throw new Error("local MCP wrapper command is required");
2761
- const names = serverNamesFor(serverName);
2762
- await runCommand("claude", ["mcp", "remove", names.hosted]).catch(() => void 0);
2763
- await runCommand("claude", ["mcp", "remove", names.signer]).catch(() => void 0);
2764
- await runCommand("claude", ["mcp", "add-json", names.hosted, serverJson, "--scope", "user"]).catch(async () => {
2765
- await runCommand("claude", ["mcp", "add", names.hosted, "--scope", "user", "--", localMcpCommand]);
3351
+ if (configPath === null) {
3352
+ checks.push({
3353
+ id: "runtime_config",
3354
+ label: "Runtime MCP config",
3355
+ ok: true,
3356
+ detail: `Runtime '${input.runtime}' has no file-based config the connector owns (CLI-managed) \u2014 skipping the file check.`
3357
+ });
3358
+ } else if (configText === null) {
3359
+ checks.push({
3360
+ id: "runtime_config",
3361
+ label: "Runtime MCP config",
3362
+ ok: false,
3363
+ detail: `No runtime config at ${configPath}.`,
3364
+ repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
3365
+ });
3366
+ } else {
3367
+ const primaryIdentity = await readIdentity(primaryDirectory ?? "");
3368
+ const primarySidecar = primaryDirectory ? await readRuntimeSidecar(primaryDirectory) : null;
3369
+ const hasHaven = primaryIdentity?.hosted_mcp_url ? configText.includes(primaryIdentity.hosted_mcp_url) : configText.includes("haven");
3370
+ const signerViaNpx = configText.includes("@haven_ai/signer");
3371
+ const wrapperReferenced = primarySidecar ? configText.includes(primarySidecar.wrapper_path) : false;
3372
+ const ok = hasHaven && !signerViaNpx && (primarySidecar ? wrapperReferenced : true);
3373
+ checks.push({
3374
+ id: "runtime_config",
3375
+ label: "Runtime MCP config",
3376
+ ok,
3377
+ 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)" : ""}.`,
3378
+ ...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
2766
3379
  });
2767
- const verified = await runCommand("claude", ["mcp", "get", names.hosted]).then(() => true).catch(() => false);
2768
- return {
2769
- hostedConfigured: false,
2770
- signerConfigured: true,
2771
- localMcpConfigured: true,
2772
- runtimeMcpMode: "local_stdio",
2773
- target: "Claude Code MCP config",
2774
- changed: true,
2775
- restartRequired: true,
2776
- messages: [
2777
- "Updated local Haven MCP entry with Claude Code.",
2778
- ...verified ? ["Verified Claude Code MCP entry."] : []
2779
- ]
2780
- };
2781
- } catch (err) {
2782
- return {
2783
- hostedConfigured: false,
2784
- signerConfigured: false,
2785
- localMcpConfigured: false,
2786
- runtimeMcpMode: "local_stdio",
2787
- target: "Claude Code MCP config",
2788
- changed: false,
2789
- restartRequired: true,
2790
- messages: [
2791
- `Could not update Claude Code MCP config: ${err instanceof Error ? err.message : String(err)}`,
2792
- "Install Claude Code or rerun the Haven setup command inside a Claude Code terminal."
2793
- ],
2794
- errorCode: "claude_code_config_failed"
2795
- };
2796
- }
2797
- }
2798
- async function configureClaudeCodeHosted(deps, input, signerCommand) {
2799
- const runCommand = deps.runCommand ?? defaultRunCommand;
2800
- const hostedJson = JSON.stringify({
2801
- type: "http",
2802
- url: input.hostedMcpUrl,
2803
- headers: { Authorization: `Bearer ${input.apiKey}` }
2804
- });
2805
- const signerJson = JSON.stringify({
2806
- type: "stdio",
2807
- command: signerCommand?.command ?? "npx",
2808
- args: signerCommand?.args ?? ["-y", signerPackageSpec(), "--credentials", input.signerPath],
2809
- env: {}
2810
- });
2811
- try {
2812
- const names = serverNamesFor(input.serverName);
2813
- await runCommand("claude", ["mcp", "remove", names.hosted]).catch(() => void 0);
2814
- await runCommand("claude", ["mcp", "remove", names.signer]).catch(() => void 0);
2815
- await runCommand("claude", ["mcp", "add-json", names.hosted, hostedJson, "--scope", "user"]);
2816
- await runCommand("claude", ["mcp", "add-json", names.signer, signerJson, "--scope", "user"]);
2817
- const verified = await runCommand("claude", ["mcp", "get", names.hosted]).then(() => true).catch(() => false);
2818
- return {
2819
- hostedConfigured: true,
2820
- signerConfigured: true,
2821
- localMcpConfigured: false,
2822
- runtimeMcpMode: "hosted_plus_signer",
2823
- target: "Claude Code MCP config",
2824
- changed: true,
2825
- restartRequired: true,
2826
- messages: [
2827
- "Updated hosted Haven MCP and local signer entries with Claude Code.",
2828
- ...verified ? ["Verified Claude Code MCP entry."] : []
2829
- ]
2830
- };
2831
- } catch (err) {
2832
- return {
2833
- hostedConfigured: false,
2834
- signerConfigured: false,
2835
- localMcpConfigured: false,
2836
- runtimeMcpMode: "hosted_plus_signer",
2837
- target: "Claude Code MCP config",
2838
- changed: false,
2839
- restartRequired: true,
2840
- messages: [
2841
- `Could not update Claude Code MCP config: ${err instanceof Error ? err.message : String(err)}`,
2842
- "Install Claude Code or rerun the Haven setup command inside a Claude Code terminal."
2843
- ],
2844
- errorCode: "claude_code_config_failed"
2845
- };
2846
- }
2847
- }
2848
- async function defaultRunCommand(command, args) {
2849
- await execFileAsync3(command, args, { timeout: 1e4 });
2850
- }
2851
- function buildProbeResult(mode, hostedConfigured, hostedStatus, signerReady, localMcpReady, localMcpProbeStatus) {
2852
- if (mode === "local_stdio") {
2853
- if (localMcpReady) return "local_stdio_mcp_ready";
2854
- return localMcpProbeStatus ? `local_stdio_mcp_${localMcpProbeStatus}` : "local_stdio_mcp_unavailable";
2855
3380
  }
2856
- const hostedPart = hostedConfigured ? `hosted_${hostedStatus}` : "hosted_not_configured";
2857
- const signerPart = signerReady ? "local_signer_ready" : "local_signer_unavailable";
2858
- return `${hostedPart}_${signerPart}`.slice(0, 120);
2859
- }
2860
- async function resolveLocalMcpConsent(input, messages) {
2861
- if (input.ackLocalTools || input.ackSigner) {
2862
- const status = await acknowledgeLocalMcpConsent(input.identityPath, input.signerPath, (message) => messages.push(message));
2863
- if (status.acknowledged) {
2864
- messages.push("Prepared the local Haven tools acknowledgement.");
2865
- } else {
2866
- messages.push("Local Haven tools acknowledgement still needs attention.");
2867
- }
2868
- return status;
3381
+ for (const id of ["hosted_mcp", "identity_match", "rekey_pending"]) {
3382
+ const check = primaryChecksById.get(id);
3383
+ if (check) checks.push(check);
2869
3384
  }
2870
- return getLocalMcpConsentStatus(input.identityPath, input.signerPath);
2871
- }
2872
- async function resolveSignerConsent(input, messages) {
2873
- if (input.ackSigner || input.ackLocalTools) {
2874
- const status = await acknowledgeLocalSignerConsent(input.signerPath, (message) => messages.push(message));
2875
- if (status.acknowledged) {
2876
- messages.push("Prepared the local Haven signer acknowledgement.");
2877
- } else {
2878
- messages.push("Local Haven signer acknowledgement still needs attention.");
3385
+ const otherEntries = inventory.filter((entry) => entry.directory !== primaryDirectory);
3386
+ if (otherEntries.length > 0) {
3387
+ const live = [];
3388
+ const revoked = [];
3389
+ const unverifiable = [];
3390
+ const retired = [];
3391
+ for (const entry of otherEntries) {
3392
+ const identity = await readIdentity(entry.directory);
3393
+ const tombstone = await readAgentTombstone(entry.directory);
3394
+ const otherAgent = entry.agentId ?? basename(entry.directory);
3395
+ const otherUrl = identity?.hosted_mcp_url ?? (identity?.api_url ? `${identity.api_url}/mcp` : void 0);
3396
+ if (!identity?.api_key || !otherUrl) {
3397
+ if (tombstone) retired.push(`${otherAgent} (retired ${tombstone.retired_at})`);
3398
+ else unverifiable.push(`${otherAgent} (no stored key/URL to probe)`);
3399
+ continue;
3400
+ }
3401
+ const suffix = tombstone ? " [tombstoned \u2014 key material still present]" : "";
3402
+ const probe = await (deps.probeHosted ?? probeHostedMcpTools)(identity.api_key, otherUrl, deps.fetch);
3403
+ if (probe.status === "ok") live.push({ label: `${otherAgent}${suffix}`, entry });
3404
+ else if (probe.status === "unauthorized") revoked.push(`${otherAgent}${suffix}`);
3405
+ else unverifiable.push(`${otherAgent} (${probe.status})${suffix}`);
2879
3406
  }
2880
- return status;
3407
+ const parts = [];
3408
+ if (live.length > 0) parts.push(`STILL SPEND-CAPABLE: ${live.map((item) => item.label).join(", ")}`);
3409
+ if (revoked.length > 0) parts.push(`already revoked: ${revoked.join(", ")}`);
3410
+ if (retired.length > 0) parts.push(`tombstoned (keys removed): ${retired.join(", ")}`);
3411
+ if (unverifiable.length > 0) parts.push(`could not verify: ${unverifiable.join(", ")}`);
3412
+ const supersededLive = live.filter((item) => item.entry.classification !== "wired").map((item) => item.label);
3413
+ checks.push({
3414
+ id: "superseded_agents",
3415
+ label: "Superseded agent credentials",
3416
+ ok: supersededLive.length === 0,
3417
+ 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("; ")}.`,
3418
+ ...supersededLive.length > 0 ? {
3419
+ 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.`
3420
+ } : {}
3421
+ });
2881
3422
  }
2882
- return getLocalSignerConsentStatus(input.signerPath);
2883
- }
2884
- function signerConsentErrorCode(signerCredentialReady, signerConsent) {
2885
- if (!signerCredentialReady) return "local_signer_credential_unavailable";
2886
- if (!signerConsent?.acknowledged) return "local_signer_ack_required";
2887
- return void 0;
2888
- }
2889
- function signerProbeErrorCode(probe) {
2890
- if (!probe || probe.status === "ok") return void 0;
2891
- return `local_signer_probe_${probe.status}`;
2892
- }
2893
- function hostedMcpErrorCode(hostedConfigured, hostedProbeStatus) {
2894
- if (!hostedConfigured || hostedProbeStatus === "ok") return void 0;
2895
- return `hosted_mcp_probe_${hostedProbeStatus}`;
2896
- }
2897
- function localMcpErrorCode(signerCredentialReady, localMcpConsent, localMcpProbeStatus) {
2898
- if (!signerCredentialReady) return "local_signer_credential_unavailable";
2899
- if (!localMcpConsent?.acknowledged) return "local_mcp_ack_required";
2900
- if (localMcpProbeStatus && localMcpProbeStatus !== "ok") return `local_mcp_probe_${localMcpProbeStatus}`;
2901
- return void 0;
2902
- }
2903
- function nextAction(runtime, restartMode, errorCode) {
2904
- if (errorCode) return "return_to_haven_for_wallet_approval_then_finish_runtime_setup";
2905
- if (restartMode === "hot-reload") return "return_to_haven_for_wallet_approval";
2906
- if (runtime === "codex-cli" || runtime === "codex-desktop") return "return_to_haven_for_wallet_approval_then_restart_codex";
2907
- if (runtime === "claude-code") return "return_to_haven_for_wallet_approval_then_restart_claude_code";
2908
- if (restartMode === "restart-app") return "return_to_haven_for_wallet_approval_then_restart_app";
2909
- if (restartMode === "restart-session") return "return_to_haven_for_wallet_approval_then_restart_agent_session";
2910
- return "return_to_haven_for_wallet_approval_then_configure_runtime";
2911
- }
2912
- function supportsLocalMcp(runtime) {
2913
- return runtime === "codex-cli" || runtime === "codex-desktop" || runtime === "claude-code";
2914
- }
2915
- async function prepareRuntimeForLocalMcp(input, deps) {
2916
- const prepare = deps.prepareLocalMcpRuntime ?? ((runtimeInput) => prepareLocalMcpRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress }));
2917
- return prepare({
2918
- credentialDirectory: input.credentialDirectory,
2919
- identityPath: input.identityPath,
2920
- signerPath: input.signerPath,
2921
- homeDir: deps.homeDir,
2922
- serverName: input.serverName
2923
- });
2924
- }
2925
- async function prepareSignerForRuntime(input, deps) {
2926
- const prepare = deps.prepareSignerRuntime ?? ((runtimeInput) => (
2927
- // onProgress threaded through on purpose (#1586 review): without it the
2928
- // install heartbeat was dead code in production and the console still
2929
- // went silent for the whole cold install — the exact symptom the issue
2930
- // set out to remove, at a longer timeout.
2931
- prepareSignerRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress })
2932
- ));
2933
- return prepare({
2934
- credentialDirectory: input.credentialDirectory,
2935
- signerPath: input.signerPath,
2936
- homeDir: deps.homeDir,
2937
- serverName: input.serverName
3423
+ const parkedElsewhere = inventory.filter((entry) => entry.directory !== primaryDirectory && entry.rekeyPending).map((entry) => ({ entry, pending: entry.rekeyPending }));
3424
+ if (parkedElsewhere.length > 0) {
3425
+ const abandoned = parkedElsewhere.filter((item) => item.pending.state !== "pending");
3426
+ const describe = (item) => `${item.entry.slug ?? item.entry.agentId ?? basename(item.entry.directory)} (${item.pending.state}, ${item.pending.path})`;
3427
+ checks.push({
3428
+ id: "rekey_pending_elsewhere",
3429
+ label: "Parked re-keys in other credential directories",
3430
+ ok: abandoned.length === 0,
3431
+ 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(", ")}.`,
3432
+ ...abandoned.length > 0 ? {
3433
+ 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."
3434
+ } : {}
3435
+ });
3436
+ }
3437
+ const signerProcess = primaryChecksById.get("signer_process");
3438
+ if (signerProcess) checks.push(signerProcess);
3439
+ const restart = restartRequiredForRuntime(input.runtime, deps.env);
3440
+ checks.push({
3441
+ id: "restart",
3442
+ label: "Runtime restart",
3443
+ ok: true,
3444
+ 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."
2938
3445
  });
3446
+ const wiredOk = inventory.filter((entry) => entry.classification === "wired").every((entry) => entry.checks.every((check) => check.ok));
3447
+ return {
3448
+ version: 1,
3449
+ ok: checks.every((check) => check.ok) && wiredOk,
3450
+ runtime: input.runtime,
3451
+ credentialDirectory: primaryDirectory,
3452
+ checks,
3453
+ agents: inventory,
3454
+ ...signerCapabilities ? { signerCapabilities } : {}
3455
+ };
2939
3456
  }
2940
- async function runLocalMcpProbe(runtimeInstall, deps) {
2941
- const probe = deps.probeLocalMcpTools ?? probeLocalMcpTools;
3457
+ async function runRepair(input, deps = {}) {
3458
+ const homeDir = deps.homeDir ?? homedir();
3459
+ const messages = [];
3460
+ const { directory, others } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
3461
+ if (others.length > 0) {
3462
+ messages.push(`Note: ${others.length} other agent credential dir(s) exist \u2014 run --doctor for their status.`);
3463
+ }
3464
+ if (!directory) {
3465
+ return {
3466
+ ok: false,
3467
+ messages: [`No agent credentials found to repair \u2014 run the full setup: ${RERUN} --setup <token>.`]
3468
+ };
3469
+ }
3470
+ let identity;
2942
3471
  try {
2943
- return await probe(runtimeInstall.command, runtimeInstall.args, MCP_RUNTIME_MANIFEST.requiredTools);
3472
+ identity = JSON.parse(await readFile(join(directory, "identity.json"), "utf8"));
2944
3473
  } catch {
2945
- return { status: "process_error" };
3474
+ return { ok: false, messages: ["identity.json is unreadable \u2014 re-run the full setup with a fresh token."] };
2946
3475
  }
2947
- }
2948
- function localRuntimePrepareErrorCode(err) {
2949
- if (err && typeof err === "object" && "code" in err && err.code === "local_mcp_unsupported_node_version") {
2950
- return "local_mcp_unsupported_node_version";
3476
+ if (!identity.api_key || !(identity.hosted_mcp_url || identity.api_url)) {
3477
+ return { ok: false, messages: ["identity.json lacks the stored API key / hosted URL \u2014 re-run the full setup."] };
2951
3478
  }
2952
- return "local_mcp_runtime_install_failed";
3479
+ const configPath = runtimeConfigPathFor(input.runtime, homeDir);
3480
+ if (configPath) {
3481
+ try {
3482
+ const existing = await readFile(configPath, "utf8");
3483
+ if (existing.includes("bin/haven-mcp") || existing.includes(".haven/mcp-runtime")) {
3484
+ return {
3485
+ ok: false,
3486
+ messages: [
3487
+ `The config at ${configPath} is the LOCAL-stdio topology (--local). Repair currently rewrites only the hosted+signer shape and will not touch it.`,
3488
+ "Re-run your original setup command (with --local) to repair a local-stdio install."
3489
+ ]
3490
+ };
3491
+ }
3492
+ } catch {
3493
+ }
3494
+ }
3495
+ const existingSidecar = await readRuntimeSidecar(directory);
3496
+ const serverName = existingSidecar?.server_name;
3497
+ const signerPath = join(directory, "signer.json");
3498
+ const prepared = await prepareSignerRuntime(
3499
+ { credentialDirectory: directory, signerPath, homeDir, serverName },
3500
+ { runCommand: deps.runCommand }
3501
+ );
3502
+ messages.push(...prepared.messages);
3503
+ const names = serverNamesFor(serverName);
3504
+ messages.push(`Rewriting MCP entries ${names.hosted} / ${names.signer}${serverName ? ` (agent "${serverName}")` : " (unnamed pair)"} \u2014 no other pair is touched.`);
3505
+ const configResult = await writeRuntimeConfig({
3506
+ runtime: input.runtime,
3507
+ hostedMcpUrl: identity.hosted_mcp_url ?? `${identity.api_url}/mcp`,
3508
+ apiKey: identity.api_key,
3509
+ identityPath: join(directory, "identity.json"),
3510
+ signerPath,
3511
+ credentialDirectory: directory,
3512
+ signerCommand: { command: prepared.command, args: prepared.args },
3513
+ homeDir,
3514
+ mode: "hosted",
3515
+ serverName
3516
+ });
3517
+ messages.push(...configResult.messages);
3518
+ messages.push("Repair complete \u2014 restart the runtime, then verify with --doctor.");
3519
+ return { ok: true, messages };
2953
3520
  }
3521
+ var RERUN;
3522
+ var init_doctor = __esm({
3523
+ "src/doctor.ts"() {
3524
+ init_runtime_manifest();
3525
+ init_probes();
3526
+ init_signer_runtime();
3527
+ init_config_writers();
3528
+ init_runtime_registry();
3529
+ init_signer_consent();
3530
+ init_tombstone();
3531
+ init_server_names();
3532
+ init_storage();
3533
+ init_redact();
3534
+ RERUN = "npx @haven_ai/connect@alpha";
3535
+ }
3536
+ });
2954
3537
 
2955
3538
  // src/runtime.ts
3539
+ init_api();
3540
+ init_key();
3541
+ init_redact();
3542
+ init_server_names();
3543
+ init_storage();
3544
+ init_runtime_install();
2956
3545
  init_runtime_registry();
2957
3546
  init_connect_error();
2958
3547
 
@@ -3125,8 +3714,9 @@ function defaultPromptIo() {
3125
3714
  }
3126
3715
 
3127
3716
  // src/runtime.ts
3717
+ init_local_mcp_runtime();
3128
3718
  init_runtime_manifest();
3129
- var CONNECTOR_VERSION = "0.1.29-alpha.0";
3719
+ var CONNECTOR_VERSION = "0.1.31-alpha.0";
3130
3720
  var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
3131
3721
  async function runConnect(options, deps = {}) {
3132
3722
  assertSupportedNodeVersion(deps.nodeVersion, MCP_RUNTIME_MANIFEST.minimumNodeVersion);
@@ -3151,7 +3741,14 @@ async function runConnect(options, deps = {}) {
3151
3741
  throw new ConnectError(
3152
3742
  "runtime_undetermined",
3153
3743
  `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: ${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.`,
3154
- "rerun_connect_with_explicit_runtime"
3744
+ "rerun_connect_with_explicit_runtime",
3745
+ // #2091: the values must ride structurally too. The backend's setup
3746
+ // prompt permits a retry only with "one of the values that refusal
3747
+ // lists" — and --json discards prose, so a prose-only list deadlocked
3748
+ // every automation run in an undetected runtime (Codex in the field:
3749
+ // npx needs network, Codex runs network commands unsandboxed, and the
3750
+ // unsandboxed path carries none of the CODEX_* detection vars).
3751
+ { allowedRuntimes: RUNTIME_FLAG_VALUE_LIST }
3155
3752
  );
3156
3753
  }
3157
3754
  const runtime = selection.runtime;
@@ -3173,11 +3770,16 @@ async function runConnect(options, deps = {}) {
3173
3770
  log(`runtime: ${runtime} (chosen at the prompt \u2014 nothing was detected in this environment)`);
3174
3771
  }
3175
3772
  log("Warming up your connection to Haven\u2026");
3176
- const setup = await api.resolveSetup({
3177
- setupToken: options.setupToken,
3178
- connectorVersion,
3179
- runtime
3180
- });
3773
+ let setup;
3774
+ try {
3775
+ setup = await api.resolveSetup({
3776
+ setupToken: options.setupToken,
3777
+ connectorVersion,
3778
+ runtime
3779
+ });
3780
+ } catch (err) {
3781
+ throw deadSetupTokenError(err) ?? err;
3782
+ }
3181
3783
  assertSetupChallengeIsUsable(setup.challenge.expires_at);
3182
3784
  printSetupSummary(setup, log);
3183
3785
  await preflightStorage({ baseDir: options.credentialsDir, warn: log });
@@ -3202,6 +3804,11 @@ async function runConnect(options, deps = {}) {
3202
3804
  proofSignature,
3203
3805
  apiKeyHash: hashAgentApiKey(localApiKey),
3204
3806
  apiKeyPrefix: agentApiKeyPrefix(localApiKey),
3807
+ // #1878: report the pair we are ACTUALLY wiring, bare pair included, so
3808
+ // the dashboard can name it. Derived here rather than sent as the raw
3809
+ // slug — `serverNamesFor` is the one place the naming rule lives, and
3810
+ // the hosted name is what a user pastes into an MCP config.
3811
+ mcpServerName: serverNamesFor(options.serverName).hosted,
3205
3812
  connectorContext: {
3206
3813
  environment_label: options.environmentLabel ?? "Local workspace",
3207
3814
  config_target: installCapabilities.canWriteRuntimeConfig ? "agent runtime MCP config" : "local credential files"
@@ -3209,6 +3816,8 @@ async function runConnect(options, deps = {}) {
3209
3816
  installCapabilities
3210
3817
  });
3211
3818
  } catch (err) {
3819
+ const dead = deadSetupTokenError(err);
3820
+ if (dead) throw dead;
3212
3821
  if (isExpiredSetupChallenge(err)) {
3213
3822
  throw new Error(
3214
3823
  "The Haven setup challenge expired while connecting. Return to Haven, start a fresh connection, and run its new Connect command. Do not reuse or paste credentials."
@@ -3406,7 +4015,17 @@ function failedConnectOutcome(runtimeHint, error) {
3406
4015
  tools: ["haven_get_agent", "haven_get_allowances"],
3407
4016
  instruction: "After a successful setup and activation, verify only with haven_get_agent and haven_get_allowances."
3408
4017
  },
3409
- error: { code, next_action: nextAction2 }
4018
+ error: {
4019
+ code,
4020
+ next_action: nextAction2,
4021
+ // Only a ConnectError's message enters the JSON record: the vocabulary's
4022
+ // prose is connector-authored and safe to serialize, while a plain
4023
+ // Error can carry arbitrary server or filesystem detail (that stance is
4024
+ // pinned by test). Redaction stays on as belt-and-braces; plain-Error
4025
+ // runs still get their redacted message on stderr via the CLI mirror.
4026
+ ...error instanceof ConnectError && message ? { message: redactForAutomation(message) } : {},
4027
+ ...error instanceof ConnectError && error.details.allowedRuntimes ? { allowed_runtimes: error.details.allowedRuntimes } : {}
4028
+ }
3410
4029
  };
3411
4030
  }
3412
4031
  function printSetupSummary(setup, log) {
@@ -3428,16 +4047,20 @@ function assertSetupChallengeIsUsable(expiresAt) {
3428
4047
  "This Haven setup challenge is expired or invalid. Return to Haven, start a fresh connection, and rerun Connect. No local credentials were written."
3429
4048
  );
3430
4049
  }
4050
+ function deadSetupTokenError(err) {
4051
+ if (!(err instanceof ConnectRequestError) || err.status !== 410 && err.status !== 401) return null;
4052
+ return new ConnectError(
4053
+ "setup_challenge_expired_or_invalid",
4054
+ "This Haven setup token is expired or invalid \u2014 tokens are single-use and expire 30 minutes after the dashboard issues them, and a mistyped token reads the same way. Return to Haven, start a fresh connection, and run its new Connect command. No local credentials were written.",
4055
+ "return_to_haven_for_fresh_setup"
4056
+ );
4057
+ }
3431
4058
  function isExpiredSetupChallenge(err) {
3432
4059
  return err instanceof Error && /(?:setup )?challenge.*expir|expir.*(?:setup )?challenge/i.test(err.message);
3433
4060
  }
3434
4061
  function secureLogger(log, redactPaths = false) {
3435
4062
  return (message) => {
3436
- let safe = redactSecrets(message);
3437
- if (redactPaths) {
3438
- safe = safe.replace(/(?:~|\/)[^\s`"']*\/(?:identity|signer|agent)\.json\b/g, "[credential-file-redacted]").replace(/(?:~|\/)[^\s`"']*\/\.env\b/g, "[credential-env-redacted]");
3439
- }
3440
- log(safe);
4063
+ log(redactPaths ? redactForAutomation(message) : redactSecrets(message));
3441
4064
  };
3442
4065
  }
3443
4066
  function printRuntimeInstall(result, log) {
@@ -3605,6 +4228,8 @@ function parseArgs(argv, env = process.env) {
3605
4228
  let json = false;
3606
4229
  let doctor = false;
3607
4230
  let repair = false;
4231
+ let rekeyPhase;
4232
+ let newApiKey;
3608
4233
  let tombstoneDir;
3609
4234
  let tombstoneReason;
3610
4235
  let tombstoneReplacedBy;
@@ -3618,6 +4243,12 @@ function parseArgs(argv, env = process.env) {
3618
4243
  doctor = true;
3619
4244
  } else if (arg === "--repair") {
3620
4245
  repair = true;
4246
+ } else if (arg === "--rekey") {
4247
+ rekeyPhase = "start";
4248
+ } else if (arg === "--rekey-finish") {
4249
+ rekeyPhase = "finish";
4250
+ } else if (arg === "--api-key") {
4251
+ newApiKey = requireValue(argv, ++i, arg);
3621
4252
  } else if (arg === "--tombstone") {
3622
4253
  tombstoneDir = requireValue(argv, ++i, arg);
3623
4254
  } else if (arg === "--reason") {
@@ -3655,20 +4286,38 @@ function parseArgs(argv, env = process.env) {
3655
4286
  }
3656
4287
  }
3657
4288
  const tombstone = tombstoneDir ? { directory: tombstoneDir, reason: tombstoneReason, replacedBy: tombstoneReplacedBy } : void 0;
4289
+ const rekey = rekeyPhase ? { phase: rekeyPhase, newApiKey } : void 0;
3658
4290
  if (help) {
3659
- return { options, help, json, doctor, repair, tombstone };
4291
+ return { options, help, json, doctor, repair, tombstone, rekey };
4292
+ }
4293
+ if (rekey) {
4294
+ if (options.setupToken) {
4295
+ throw new Error("--rekey replaces an existing agent's key; it does not take --setup. Drop one of them.");
4296
+ }
4297
+ if (rekey.phase === "start" && newApiKey !== void 0) {
4298
+ throw new Error(
4299
+ "--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."
4300
+ );
4301
+ }
4302
+ if (rekey.phase === "finish" && !newApiKey) {
4303
+ throw new Error("--rekey-finish needs --api-key <key> \u2014 the one the Haven agent page showed once.");
4304
+ }
4305
+ return { options, help, json, doctor, repair, tombstone, rekey };
4306
+ }
4307
+ if (newApiKey !== void 0) {
4308
+ throw new Error("--api-key requires --rekey-finish.");
3660
4309
  }
3661
4310
  if (!tombstoneDir && (tombstoneReason !== void 0 || tombstoneReplacedBy !== void 0)) {
3662
4311
  throw new Error("--reason and --replaced-by require --tombstone <dir>.");
3663
4312
  }
3664
4313
  if (tombstone) {
3665
- return { options, help, json, doctor, repair, tombstone };
4314
+ return { options, help, json, doctor, repair, tombstone, rekey };
3666
4315
  }
3667
4316
  if (doctor || repair) {
3668
4317
  if (!options.runtime) {
3669
4318
  throw new Error("--doctor/--repair need --runtime <runtime> (which config to examine).");
3670
4319
  }
3671
- return { options, help, json, doctor, repair, tombstone };
4320
+ return { options, help, json, doctor, repair, tombstone, rekey };
3672
4321
  }
3673
4322
  if (!options.setupToken) {
3674
4323
  throw new Error("Missing --setup <hv_setup_...> setup token.");
@@ -3677,7 +4326,7 @@ function parseArgs(argv, env = process.env) {
3677
4326
  throw new Error("Missing --api <Haven API URL>.");
3678
4327
  }
3679
4328
  options.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, "");
3680
- return { options, help, json, doctor, repair, tombstone };
4329
+ return { options, help, json, doctor, repair, tombstone, rekey };
3681
4330
  }
3682
4331
  function helpText() {
3683
4332
  return [
@@ -3714,6 +4363,14 @@ function helpText() {
3714
4363
  " --repair Repair, then re-diagnose (implies --doctor): reinstall the pinned signer",
3715
4364
  " runtime, rewrite the wrapper and runtime config from stored credentials.",
3716
4365
  " Hosted topology only (refuses to touch a --local config). No keys, no token.",
4366
+ " --rekey Replace this agent's signing key (no token). Generates a fresh keypair HERE and",
4367
+ " prints its public address to paste into the Haven agent page. Nothing changes",
4368
+ " until you finish; the agent keeps working on its old key throughout.",
4369
+ " Add --name <slug> for a named agent. Refuses a legacy-rail or revoked agent.",
4370
+ " --rekey-finish Second half of --rekey: writes the new key and the API key the agent page",
4371
+ " showed once, in place at the same path, and rewrites only this agent's MCP",
4372
+ " config pair. Server names do not change, so wired hosts need only a restart.",
4373
+ " --api-key <key> The new API key, for --rekey-finish.",
3717
4374
  " --tombstone <dir> Retire an agent credential directory in place (no token): replaces its signer",
3718
4375
  " wrapper with a diagnostic that names the retirement in MCP stderr logs, and",
3719
4376
  " writes TOMBSTONE.json. Touches NO key material and revokes nothing.",
@@ -3733,6 +4390,8 @@ function requireValue(argv, index, option) {
3733
4390
  }
3734
4391
 
3735
4392
  // src/index.ts
4393
+ init_api();
4394
+ init_key();
3736
4395
  init_redact();
3737
4396
  init_redact();
3738
4397
  async function runCli(argv, io = {
@@ -3745,6 +4404,8 @@ async function runCli(argv, io = {
3745
4404
  parsed = parseArgs(argv);
3746
4405
  } catch (err) {
3747
4406
  if (wantsJson) {
4407
+ io.stderr(`${redactForAutomation(err instanceof Error ? err.message : String(err))}
4408
+ `);
3748
4409
  io.stdout(`${JSON.stringify(failedConnectOutcome(void 0, err))}
3749
4410
  `);
3750
4411
  } else {
@@ -3760,13 +4421,13 @@ async function runCli(argv, io = {
3760
4421
  }
3761
4422
  if (parsed.tombstone) {
3762
4423
  const { writeAgentTombstone: writeAgentTombstone2 } = await Promise.resolve().then(() => (init_tombstone(), tombstone_exports));
3763
- const { readFile: readFile11 } = await import('fs/promises');
4424
+ const { readFile: readFile12 } = await import('fs/promises');
3764
4425
  const { join: join10 } = await import('path');
3765
4426
  try {
3766
4427
  let agentId = "unknown";
3767
4428
  try {
3768
4429
  const identity = JSON.parse(
3769
- await readFile11(join10(parsed.tombstone.directory, "identity.json"), "utf8")
4430
+ await readFile12(join10(parsed.tombstone.directory, "identity.json"), "utf8")
3770
4431
  );
3771
4432
  agentId = identity.agent_id ?? "unknown";
3772
4433
  } catch {
@@ -3793,6 +4454,64 @@ async function runCli(argv, io = {
3793
4454
  return 0;
3794
4455
  } catch (err) {
3795
4456
  io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
4457
+ `);
4458
+ return 1;
4459
+ }
4460
+ }
4461
+ if (parsed.rekey) {
4462
+ const { startRekey: startRekey2, finishRekey: finishRekey2 } = await Promise.resolve().then(() => (init_rekey(), rekey_exports));
4463
+ const { restartGuidance: restartGuidance2 } = await Promise.resolve().then(() => (init_rekey_restart(), rekey_restart_exports));
4464
+ const common = {
4465
+ serverName: parsed.options.serverName,
4466
+ credentialsDir: parsed.options.credentialsDir,
4467
+ runtime: parsed.options.runtime
4468
+ };
4469
+ try {
4470
+ if (parsed.rekey.phase === "start") {
4471
+ const result2 = await startRekey2(common);
4472
+ if (parsed.json) {
4473
+ io.stdout(
4474
+ `${redactSecrets(
4475
+ JSON.stringify({
4476
+ rekey: "started",
4477
+ agent_id: result2.agentId,
4478
+ new_delegate_address: result2.newDelegateAddress,
4479
+ expires_at: result2.expiresAt
4480
+ })
4481
+ )}
4482
+ `
4483
+ );
4484
+ } else {
4485
+ for (const line of result2.messages) io.stdout(redactSecrets(`${line}
4486
+ `));
4487
+ }
4488
+ return 0;
4489
+ }
4490
+ const result = await finishRekey2({ ...common, newApiKey: parsed.rekey.newApiKey });
4491
+ const restart = restartGuidance2(parsed.options.runtime);
4492
+ if (parsed.json) {
4493
+ io.stdout(
4494
+ `${redactSecrets(
4495
+ JSON.stringify({
4496
+ rekey: "finished",
4497
+ agent_id: result.agentId,
4498
+ new_delegate_address: result.newDelegateAddress,
4499
+ mcp_servers: result.serverNames,
4500
+ restart_commands: restart.commands
4501
+ })
4502
+ )}
4503
+ `
4504
+ );
4505
+ } else {
4506
+ for (const line of result.messages) io.stdout(redactSecrets(`${line}
4507
+ `));
4508
+ io.stdout("\n");
4509
+ for (const line of restart.lines) io.stdout(redactSecrets(`${line}
4510
+ `));
4511
+ }
4512
+ return 0;
4513
+ } catch (err) {
4514
+ io.stderr(`${redactSecrets(err instanceof Error ? err.message : String(err))}
3796
4515
  `);
3797
4516
  return 1;
3798
4517
  }
@@ -3825,7 +4544,7 @@ async function runCli(argv, io = {
3825
4544
  for (const agent of otherAgents) {
3826
4545
  const name = agent.slug ? `${agent.slug} (${agent.agentId ?? "unknown"})` : agent.agentId ?? "unknown";
3827
4546
  const failed = agent.checks.filter((check) => !check.ok);
3828
- const verdict = agent.classification === "wired" ? failed.length === 0 ? "wired, all checks passed" : `wired, ${failed.length} check(s) FAILED` : agent.classification;
4547
+ 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;
3829
4548
  io.stdout(redactSecrets(` ${failed.length > 0 ? "\u2717" : "\u2022"} ${name}: ${verdict}
3830
4549
  `));
3831
4550
  for (const check of failed) {
@@ -3867,6 +4586,8 @@ async function runCli(argv, io = {
3867
4586
  return 0;
3868
4587
  } catch (err) {
3869
4588
  if (parsed.json) {
4589
+ io.stderr(`${redactForAutomation(err instanceof Error ? err.message : String(err))}
4590
+ `);
3870
4591
  io.stdout(`${JSON.stringify(failedConnectOutcome(parsed.options.runtime, err))}
3871
4592
  `);
3872
4593
  } else {
@@ -3891,9 +4612,11 @@ function isCliEntrypoint(argvPath = process.argv[1], moduleUrl = import.meta.url
3891
4612
  if (isCliEntrypoint()) void main();
3892
4613
 
3893
4614
  // src/index.ts
4615
+ init_runtime_install();
3894
4616
  init_signer_runtime();
3895
4617
  init_runtime_registry();
3896
4618
  init_connect_error();
4619
+ init_storage();
3897
4620
  init_runtime_manifest();
3898
4621
 
3899
4622
  export { CONNECTOR_VERSION, CONNECT_OUTCOME_SCHEMA_VERSION, ConnectError, MCP_RUNTIME_MANIFEST, RUNTIME_FLAG_VALUES, completionOutcome, createConnectApiClient, defaultAgentDirectory, delegateKeyFromPrivateKey, failedConnectOutcome, generateDelegateKey, helpText, installRuntime, isConnectError, mcpPackageSpec, normalizeRuntime, parseArgs, prepareSignerRuntime, promptForInstalledClient, redactSecrets, resolveRuntimeByInstalledClientPrompt, resolveRuntimeSelection, runCli, runConnect, runtimeInstallCapabilities, runtimeProfile, scanInstalledClients, sdkPackageSpec, shortAddress, signerPackageSpec, writeCredentialFiles };