@haven_ai/connect 0.9.0-alpha.0 → 0.10.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -2,15 +2,15 @@
2
2
 
3
3
  var crypto = require('crypto');
4
4
  var ethers = require('ethers');
5
- var promises = require('fs/promises');
6
- var os = require('os');
7
- var path = require('path');
8
5
  var mcp = require('@haven_ai/mcp');
9
6
  var signer = require('@haven_ai/signer');
10
7
  var sdk = require('@haven_ai/sdk');
11
- var yaml = require('yaml');
12
8
  var child_process = require('child_process');
9
+ var promises = require('fs/promises');
10
+ var os = require('os');
11
+ var path = require('path');
13
12
  var util = require('util');
13
+ var yaml = require('yaml');
14
14
  var readline = require('readline');
15
15
  var fs = require('fs');
16
16
  var url = require('url');
@@ -71,6 +71,25 @@ function createConnectApiClient(baseUrl, fetchImpl = fetch, clientIdentity) {
71
71
  method: "GET",
72
72
  headers: { Authorization: `Bearer ${apiKey}` }
73
73
  }),
74
+ reportRetiredAgents: async (input) => {
75
+ try {
76
+ const body = await send(
77
+ `${root}/agent-connection-setups/${encodeURIComponent(input.setupId)}/retired-agents`,
78
+ {
79
+ method: "POST",
80
+ headers: { Authorization: `Bearer ${input.apiKey}` },
81
+ body: JSON.stringify({ retired_agent_ids: input.retiredAgentIds })
82
+ }
83
+ );
84
+ const recorded = body?.recorded_agent_ids;
85
+ return {
86
+ recordedAgentIds: Array.isArray(recorded) ? recorded.filter((id) => typeof id === "string") : []
87
+ };
88
+ } catch (err) {
89
+ if (err instanceof ConnectRequestError && err.status === 404) return null;
90
+ throw err;
91
+ }
92
+ },
74
93
  updateInstallStatus: async (setupId, apiKey, input) => {
75
94
  await send(`${root}/agent-connection-setups/${encodeURIComponent(setupId)}/install-status`, {
76
95
  method: "POST",
@@ -165,81 +184,574 @@ var init_key = __esm({
165
184
  }
166
185
  });
167
186
 
168
- // src/redact.ts
169
- function redactSecrets(value) {
170
- return value.replace(API_KEY_RE, "sk_agent_[redacted]").replace(PRIVATE_KEY_RE, "0x[redacted-private-key]");
187
+ // src/redact.ts
188
+ function redactSecrets(value) {
189
+ return value.replace(API_KEY_RE, "sk_agent_[redacted]").replace(PRIVATE_KEY_RE, "0x[redacted-private-key]");
190
+ }
191
+ function redactForAutomation(value) {
192
+ return redactSecrets(value).replace(/(?:~|\/)[^\s`"']*\/(?:identity|signer|agent)\.json\b/g, "[credential-file-redacted]").replace(/(?:~|\/)[^\s`"']*\/\.env\b/g, "[credential-env-redacted]");
193
+ }
194
+ function withoutUserinfo(url) {
195
+ try {
196
+ const parsed = new URL(url);
197
+ parsed.username = "";
198
+ parsed.password = "";
199
+ return parsed.toString().replace(/\/+$/, "");
200
+ } catch {
201
+ return url;
202
+ }
203
+ }
204
+ function shortAddress(address) {
205
+ if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return address;
206
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
207
+ }
208
+ var API_KEY_RE, PRIVATE_KEY_RE;
209
+ var init_redact = __esm({
210
+ "src/redact.ts"() {
211
+ API_KEY_RE = /sk_agent_[A-Za-z0-9]+/g;
212
+ PRIVATE_KEY_RE = /0x[0-9a-fA-F]{64}/g;
213
+ }
214
+ });
215
+
216
+ // src/server-names.ts
217
+ function assertValidServerSlug(slug) {
218
+ if (slug.length === 0 || slug.length > 32 || !SLUG_RE.test(slug)) {
219
+ throw new Error(
220
+ `Invalid server name ${JSON.stringify(slug)}: use 1-32 lowercase letters, digits, and single hyphens (e.g. "research").`
221
+ );
222
+ }
223
+ if (slug === "haven" || slug === "haven-signer") {
224
+ throw new Error(
225
+ `Invalid server name ${JSON.stringify(slug)}: "haven" and "haven-signer" are the unnamed pair's own names \u2014 omit --name for the bare pair.`
226
+ );
227
+ }
228
+ if (slug === "signer" || slug.startsWith("signer-")) {
229
+ throw new Error(
230
+ `Invalid server name ${JSON.stringify(slug)}: "signer" and "signer-*" are reserved \u2014 they would collide with another pair's haven-signer-* entry.`
231
+ );
232
+ }
233
+ }
234
+ function serverNamesFor(slug) {
235
+ if (slug === void 0) {
236
+ return {
237
+ hosted: "haven",
238
+ signer: "haven-signer",
239
+ // Historical Codex table names — every wired Codex host has these.
240
+ codexHosted: "haven",
241
+ codexSigner: "haven_signer",
242
+ hermesEnvKey: "MCP_HAVEN_API_KEY"
243
+ };
244
+ }
245
+ assertValidServerSlug(slug);
246
+ return {
247
+ hosted: `haven-${slug}`,
248
+ signer: `haven-signer-${slug}`,
249
+ // Hyphens are valid TOML bare keys, so named Codex tables match the
250
+ // JSON/YAML names instead of inheriting the legacy underscore.
251
+ codexHosted: `haven-${slug}`,
252
+ codexSigner: `haven-signer-${slug}`,
253
+ hermesEnvKey: `MCP_HAVEN_${slug.toUpperCase().replace(/-/g, "_")}_API_KEY`
254
+ };
255
+ }
256
+ var SLUG_RE;
257
+ var init_server_names = __esm({
258
+ "src/server-names.ts"() {
259
+ SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
260
+ }
261
+ });
262
+ function mcpPackageSpec() {
263
+ return `${exports.MCP_RUNTIME_MANIFEST.mcpPackage}@${exports.MCP_RUNTIME_MANIFEST.mcpVersion}`;
264
+ }
265
+ function sdkPackageSpec() {
266
+ return `${exports.MCP_RUNTIME_MANIFEST.sdkPackage}@${exports.MCP_RUNTIME_MANIFEST.sdkVersion}`;
267
+ }
268
+ function signerPackageSpec() {
269
+ return `${exports.MCP_RUNTIME_MANIFEST.signerPackage}@${exports.MCP_RUNTIME_MANIFEST.signerVersion}`;
270
+ }
271
+ exports.MCP_RUNTIME_MANIFEST = void 0;
272
+ var init_runtime_manifest = __esm({
273
+ "src/runtime-manifest.ts"() {
274
+ exports.MCP_RUNTIME_MANIFEST = {
275
+ mcpPackage: "@haven_ai/mcp",
276
+ mcpVersion: mcp.MCP_VERSION,
277
+ sdkPackage: "@haven_ai/sdk",
278
+ sdkVersion: "0.10.0-alpha.0",
279
+ signerPackage: "@haven_ai/signer",
280
+ signerVersion: "0.10.0-alpha.0",
281
+ // Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
282
+ // while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
283
+ // so the guard that was supposed to enforce the floor waved Node v23 through
284
+ // — including on the `--local` path where it does run. A hand-maintained
285
+ // second copy of a number is a drift waiting to happen; a guard test pins
286
+ // this against `package.json`'s `engines.node`.
287
+ minimumNodeVersion: sdk.HAVEN_MINIMUM_NODE_VERSION,
288
+ supportedClients: ["codex-cli", "codex-desktop", "claude-code"],
289
+ requiredTools: mcp.registeredToolNames(),
290
+ /**
291
+ * The signer MCP's tool surface, DERIVED from the pinned @haven_ai/signer
292
+ * package (#1587) — same anti-drift rule as `requiredTools` above: a
293
+ * literal list here would rot the first time the signer gains a tool.
294
+ * The handshake probe requires all of them.
295
+ */
296
+ requiredSignerTools: Object.keys(signer.toolSchemas)
297
+ };
298
+ }
299
+ });
300
+ function resolveRuntimeSpecOverride(env) {
301
+ const override = {};
302
+ for (const pkg of Object.keys(RUNTIME_SPEC_ENV)) {
303
+ const variable = RUNTIME_SPEC_ENV[pkg];
304
+ const raw = env[variable];
305
+ if (raw === void 0) continue;
306
+ override[pkg] = validateSpec(variable, raw);
307
+ }
308
+ return Object.keys(override).length > 0 ? override : void 0;
309
+ }
310
+ function validateSpec(variable, raw) {
311
+ if (raw.length === 0 || raw.trim().length === 0) {
312
+ throw new RuntimeSpecOverrideError(variable, "it is empty");
313
+ }
314
+ if (/\s/.test(raw)) {
315
+ throw new RuntimeSpecOverrideError(variable, "it contains whitespace");
316
+ }
317
+ if (/[\u0000-\u001f\u007f]/.test(raw)) {
318
+ throw new RuntimeSpecOverrideError(variable, "it contains a control character");
319
+ }
320
+ const meta = SHELL_METACHARACTERS.exec(raw);
321
+ if (meta) {
322
+ throw new RuntimeSpecOverrideError(variable, `it contains the shell metacharacter ${JSON.stringify(meta[0])}`);
323
+ }
324
+ return raw;
325
+ }
326
+ function runtimeSpecOverrideDirectoryKey(resolvedSpecs) {
327
+ const digest = crypto.createHash("sha256").update(resolvedSpecs.join("\n")).digest("hex");
328
+ return `override-${digest.slice(0, 12)}`;
329
+ }
330
+ function overrideApplies(override, packages) {
331
+ return override !== void 0 && packages.some((pkg) => override[pkg] !== void 0);
332
+ }
333
+ function runtimeSpecOverrideNotice(runtimeLabel, override, pinned, runtimeDirectory) {
334
+ const lines = [`RUNTIME SPEC OVERRIDE ACTIVE for the local Haven ${runtimeLabel} \u2014 this is NOT the pinned manifest.`];
335
+ for (const pkg of Object.keys(RUNTIME_SPEC_ENV)) {
336
+ const spec = override[pkg];
337
+ if (spec === void 0) continue;
338
+ const pin = pinned[pkg];
339
+ lines.push(` ${RUNTIME_SPEC_ENV[pkg]}=${spec}${pin ? ` (instead of ${pin})` : ""}`);
340
+ }
341
+ lines.push(` Installing into ${runtimeDirectory} \u2014 the pinned runtime directory is untouched.`);
342
+ lines.push(" Unset the variable(s) and re-run to return to the pinned manifest.");
343
+ return lines;
344
+ }
345
+ function describeRuntimeSpecOverride(override) {
346
+ return Object.keys(RUNTIME_SPEC_ENV).filter((pkg) => override[pkg] !== void 0).map((pkg) => `${RUNTIME_SPEC_ENV[pkg]}=${override[pkg]}`).join(" ");
347
+ }
348
+ var RUNTIME_SPEC_ENV, SHELL_METACHARACTERS, RuntimeSpecOverrideError;
349
+ var init_runtime_spec_override = __esm({
350
+ "src/runtime-spec-override.ts"() {
351
+ RUNTIME_SPEC_ENV = {
352
+ signer: "HAVEN_SIGNER_SPEC",
353
+ sdk: "HAVEN_SDK_SPEC",
354
+ mcp: "HAVEN_MCP_SPEC"
355
+ };
356
+ SHELL_METACHARACTERS = /[;&|<>$`'"()!#*?[\]{}\\]/;
357
+ RuntimeSpecOverrideError = class extends Error {
358
+ code = "runtime_spec_override_invalid";
359
+ variable;
360
+ constructor(variable, reason) {
361
+ super(`${variable} is set but not usable as an npm package spec: ${reason}. Unset it, or set it to a spec npm install accepts (file:/abs/path, a .tgz, or @haven_ai/<pkg>@<version>).`);
362
+ this.name = "RuntimeSpecOverrideError";
363
+ this.variable = variable;
364
+ }
365
+ };
366
+ }
367
+ });
368
+ async function prepareSignerRuntime(input, deps = {}) {
369
+ const homeDir = input.homeDir ?? os.homedir();
370
+ const override = resolveSignerRuntimeOverride(deps.env ?? process.env);
371
+ const signerSpec = override?.signer ?? signerPackageSpec();
372
+ const sdkSpec = override?.sdk ?? sdkPackageSpec();
373
+ const resolvedSpecs = [signerSpec, sdkSpec];
374
+ const overrideRecord = override ? { specs: override, resolved_specs: resolvedSpecs, directory_key: runtimeSpecOverrideDirectoryKey(resolvedSpecs) } : void 0;
375
+ const runtimeDirectory = path.resolve(
376
+ homeDir,
377
+ ".haven",
378
+ "signer-runtime",
379
+ overrideRecord ? overrideRecord.directory_key : exports.MCP_RUNTIME_MANIFEST.signerVersion
380
+ );
381
+ const npmCacheDirectory = path.resolve(homeDir, ".haven", "npm-cache");
382
+ const cliPath = path.join(runtimeDirectory, "node_modules", "@haven_ai", "signer", "dist", "cli.js");
383
+ const messages = [];
384
+ if (override) {
385
+ messages.push(...runtimeSpecOverrideNotice(
386
+ "signer runtime",
387
+ override,
388
+ { signer: signerPackageSpec(), sdk: sdkPackageSpec() },
389
+ runtimeDirectory
390
+ ));
391
+ }
392
+ await promises.mkdir(runtimeDirectory, { recursive: true, mode: 448 });
393
+ await promises.chmod(runtimeDirectory, 448).catch(() => void 0);
394
+ await promises.mkdir(npmCacheDirectory, { recursive: true, mode: 448 });
395
+ await promises.chmod(npmCacheDirectory, 448).catch(() => void 0);
396
+ if (override) {
397
+ await installRuntimePackages(runtimeDirectory, npmCacheDirectory, resolvedSpecs, deps);
398
+ messages.push(`Installed local Haven signer runtime from override (${resolvedSpecs.join(" ")}).`);
399
+ } else if (await installedRuntimeMatches(runtimeDirectory, cliPath)) {
400
+ messages.push(`Using existing local Haven signer runtime ${signerPackageSpec()}.`);
401
+ } else {
402
+ await installRuntimePackages(runtimeDirectory, npmCacheDirectory, resolvedSpecs, deps);
403
+ messages.push(`Installed local Haven signer runtime ${signerPackageSpec()}.`);
404
+ }
405
+ await assertFileExists(cliPath, "local Haven signer CLI");
406
+ const installedVersions = override ? await readInstalledVersions(runtimeDirectory) : void 0;
407
+ const wrapperPath = path.join(input.credentialDirectory, "bin", "haven-signer.mjs");
408
+ await writeWrapper({
409
+ wrapperPath,
410
+ cliPath,
411
+ signerPath: input.signerPath,
412
+ overrideComment: override ? describeRuntimeSpecOverride(override) : void 0
413
+ });
414
+ await writeRuntimeSidecar({
415
+ path: path.join(input.credentialDirectory, "signer-runtime.json"),
416
+ wrapperPath,
417
+ runtimeDirectory,
418
+ npmCacheDirectory,
419
+ cliPath,
420
+ serverName: input.serverName,
421
+ override: overrideRecord,
422
+ installedVersions
423
+ });
424
+ messages.push(`Prepared stable local Haven signer wrapper: ${wrapperPath}`);
425
+ return {
426
+ command: wrapperPath,
427
+ args: [],
428
+ wrapperPath,
429
+ runtimeDirectory,
430
+ npmCacheDirectory,
431
+ cliPath,
432
+ messages,
433
+ ...overrideRecord ? { runtimeSpecOverride: overrideRecord } : {}
434
+ };
435
+ }
436
+ function resolveSignerRuntimeOverride(env) {
437
+ const override = resolveRuntimeSpecOverride(env);
438
+ if (!overrideApplies(override, ["signer", "sdk"])) return void 0;
439
+ const { signer, sdk } = override;
440
+ return { ...signer !== void 0 ? { signer } : {}, ...sdk !== void 0 ? { sdk } : {} };
441
+ }
442
+ async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, packageSpecs, deps) {
443
+ const { runCommand, onProgress } = deps;
444
+ const baseArgs = [
445
+ "install",
446
+ "--prefix",
447
+ runtimeDirectory,
448
+ "--no-audit",
449
+ "--no-fund",
450
+ "--omit=dev",
451
+ "--prefer-offline",
452
+ ...packageSpecs
453
+ ];
454
+ const run = async (args) => {
455
+ const startedAt = Date.now();
456
+ const heartbeat = setInterval(() => {
457
+ const seconds = Math.round((Date.now() - startedAt) / 1e3);
458
+ onProgress?.(`Still installing the local Haven signer runtime\u2026 (${seconds}s \u2014 a cold cache can take several minutes)`);
459
+ }, SIGNER_INSTALL_HEARTBEAT_MS);
460
+ heartbeat.unref?.();
461
+ try {
462
+ if (runCommand) await runCommand("npm", args);
463
+ else await execFileAsync("npm", args, { timeout: SIGNER_INSTALL_TIMEOUT_MS, maxBuffer: 1024 * 1024 });
464
+ } finally {
465
+ clearInterval(heartbeat);
466
+ }
467
+ };
468
+ try {
469
+ await run(baseArgs);
470
+ } catch {
471
+ try {
472
+ await run([...baseArgs, "--cache", npmCacheDirectory]);
473
+ } catch (err) {
474
+ throw new Error(
475
+ `Could not install local Haven signer runtime ${packageSpecs.join(" ")}: ${err instanceof Error ? err.message : String(err)}`
476
+ );
477
+ }
478
+ }
479
+ }
480
+ async function installedRuntimeMatches(runtimeDirectory, cliPath) {
481
+ return installedRuntimeMatchesVersions(runtimeDirectory, cliPath, {
482
+ signerVersion: exports.MCP_RUNTIME_MANIFEST.signerVersion,
483
+ sdkVersion: exports.MCP_RUNTIME_MANIFEST.sdkVersion
484
+ });
485
+ }
486
+ async function installedRuntimeMatchesVersions(runtimeDirectory, cliPath, expected) {
487
+ try {
488
+ await assertFileExists(cliPath, "local Haven signer CLI");
489
+ const installed = await readInstalledVersions(runtimeDirectory);
490
+ return installed.signerVersion === expected.signerVersion && installed.sdkVersion === expected.sdkVersion;
491
+ } catch {
492
+ return false;
493
+ }
494
+ }
495
+ async function readInstalledVersions(runtimeDirectory) {
496
+ const [signerPackage, sdkPackage] = await Promise.all([
497
+ readPackageJson(path.join(runtimeDirectory, "node_modules", "@haven_ai", "signer", "package.json")),
498
+ readPackageJson(path.join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
499
+ ]);
500
+ return { signerVersion: signerPackage.version ?? "", sdkVersion: sdkPackage.version ?? "" };
501
+ }
502
+ async function readPackageJson(path) {
503
+ return JSON.parse(await promises.readFile(path, "utf8"));
504
+ }
505
+ async function writeWrapper(input) {
506
+ await promises.mkdir(path.dirname(input.wrapperPath), { recursive: true, mode: 448 });
507
+ await promises.chmod(path.dirname(input.wrapperPath), 448).catch(() => void 0);
508
+ const source = [
509
+ "#!/usr/bin/env node",
510
+ ...input.overrideComment ? [`// HAVEN RUNTIME SPEC OVERRIDE (#2424): ${input.overrideComment} \u2014 this wrapper launches a NON-pinned signer build.`] : [],
511
+ "import { spawn } from 'node:child_process'",
512
+ "",
513
+ `const cliPath = ${JSON.stringify(input.cliPath)}`,
514
+ `const signerPath = ${JSON.stringify(input.signerPath)}`,
515
+ "",
516
+ "const child = spawn(process.execPath, [cliPath, '--credentials', signerPath, ...process.argv.slice(2)], {",
517
+ " stdio: 'inherit',",
518
+ "})",
519
+ "",
520
+ "child.on('exit', (code, signal) => {",
521
+ " if (signal) process.kill(process.pid, signal)",
522
+ " else process.exit(code ?? 1)",
523
+ "})",
524
+ ""
525
+ ].join("\n");
526
+ await promises.writeFile(input.wrapperPath, source, { mode: 448 });
527
+ await promises.chmod(input.wrapperPath, 448).catch(() => void 0);
528
+ }
529
+ async function readRuntimeSidecar(credentialDirectory) {
530
+ try {
531
+ return JSON.parse(
532
+ await promises.readFile(path.join(credentialDirectory, "signer-runtime.json"), "utf8")
533
+ );
534
+ } catch {
535
+ return null;
536
+ }
537
+ }
538
+ async function writeRuntimeSidecar(input) {
539
+ const value = {
540
+ ...input.serverName ? { server_name: input.serverName } : {},
541
+ signer_package: exports.MCP_RUNTIME_MANIFEST.signerPackage,
542
+ signer_version: input.installedVersions?.signerVersion ?? exports.MCP_RUNTIME_MANIFEST.signerVersion,
543
+ sdk_package: exports.MCP_RUNTIME_MANIFEST.sdkPackage,
544
+ sdk_version: input.installedVersions?.sdkVersion ?? exports.MCP_RUNTIME_MANIFEST.sdkVersion,
545
+ wrapper_path: input.wrapperPath,
546
+ runtime_directory: input.runtimeDirectory,
547
+ npm_cache_directory: input.npmCacheDirectory,
548
+ cli_path: input.cliPath,
549
+ ...input.override ? { runtime_spec_override: input.override } : {}
550
+ };
551
+ await promises.writeFile(input.path, `${JSON.stringify(value, null, 2)}
552
+ `, { mode: 384 });
553
+ await promises.chmod(input.path, 384).catch(() => void 0);
554
+ }
555
+ async function assertFileExists(path, label) {
556
+ try {
557
+ await promises.access(path);
558
+ } catch {
559
+ throw new Error(`Missing ${label}: ${path}`);
560
+ }
561
+ }
562
+ var execFileAsync, SIGNER_INSTALL_TIMEOUT_MS, SIGNER_INSTALL_HEARTBEAT_MS;
563
+ var init_signer_runtime = __esm({
564
+ "src/signer-runtime.ts"() {
565
+ init_runtime_manifest();
566
+ init_runtime_spec_override();
567
+ execFileAsync = util.promisify(child_process.execFile);
568
+ SIGNER_INSTALL_TIMEOUT_MS = 6e5;
569
+ SIGNER_INSTALL_HEARTBEAT_MS = 15e3;
570
+ }
571
+ });
572
+
573
+ // src/connect-error.ts
574
+ function isConnectError(err) {
575
+ return err instanceof exports.ConnectError;
576
+ }
577
+ exports.ConnectError = void 0;
578
+ var init_connect_error = __esm({
579
+ "src/connect-error.ts"() {
580
+ exports.ConnectError = class extends Error {
581
+ code;
582
+ nextAction;
583
+ details;
584
+ constructor(code, message, nextAction2, details = {}) {
585
+ super(message);
586
+ this.name = "ConnectError";
587
+ this.code = code;
588
+ this.nextAction = nextAction2;
589
+ this.details = details;
590
+ }
591
+ };
592
+ }
593
+ });
594
+
595
+ // src/tombstone.ts
596
+ var tombstone_exports = {};
597
+ __export(tombstone_exports, {
598
+ ROOT_TOMBSTONES_DIRNAME: () => ROOT_TOMBSTONES_DIRNAME,
599
+ TOMBSTONE_FILENAME: () => TOMBSTONE_FILENAME,
600
+ TOMBSTONE_MARKER: () => TOMBSTONE_MARKER,
601
+ defaultTombstonesDir: () => defaultTombstonesDir,
602
+ readAgentTombstone: () => readAgentTombstone,
603
+ readTombstoneRecords: () => readTombstoneRecords,
604
+ tombstonesDirForAgentDirectory: () => tombstonesDirForAgentDirectory,
605
+ tombstonesDirForCredentialRoot: () => tombstonesDirForCredentialRoot,
606
+ writeAgentTombstone: () => writeAgentTombstone
607
+ });
608
+ function defaultTombstonesDir(baseDir) {
609
+ return path.join(baseDir ?? path.join(os.homedir(), ".haven"), "tombstones");
171
610
  }
172
- function redactForAutomation(value) {
173
- return redactSecrets(value).replace(/(?:~|\/)[^\s`"']*\/(?:identity|signer|agent)\.json\b/g, "[credential-file-redacted]").replace(/(?:~|\/)[^\s`"']*\/\.env\b/g, "[credential-env-redacted]");
611
+ function tombstonesDirForCredentialRoot(credentialRoot, homeDir = os.homedir()) {
612
+ if (!credentialRoot) return defaultTombstonesDir(path.join(homeDir, ".haven"));
613
+ const root = path.resolve(credentialRoot);
614
+ if (root === path.resolve(homeDir, ".haven", "agents")) return defaultTombstonesDir(path.join(homeDir, ".haven"));
615
+ return path.join(root, ROOT_TOMBSTONES_DIRNAME);
174
616
  }
175
- function withoutUserinfo(url) {
617
+ function tombstonesDirForAgentDirectory(directory, homeDir = os.homedir()) {
618
+ return tombstonesDirForCredentialRoot(path.dirname(path.resolve(directory)), homeDir);
619
+ }
620
+ async function readTombstoneRecords(tombstonesDir) {
621
+ const root = tombstonesDir ?? defaultTombstonesDir();
622
+ let entries = [];
176
623
  try {
177
- const parsed = new URL(url);
178
- parsed.username = "";
179
- parsed.password = "";
180
- return parsed.toString().replace(/\/+$/, "");
624
+ entries = await promises.readdir(root);
181
625
  } catch {
182
- return url;
626
+ return [];
627
+ }
628
+ const records = [];
629
+ for (const entry of entries) {
630
+ if (!entry.endsWith(".json")) continue;
631
+ const recordPath = path.join(root, entry);
632
+ try {
633
+ const parsed = JSON.parse(await promises.readFile(recordPath, "utf8"));
634
+ if (typeof parsed?.agent_id === "string") {
635
+ records.push({ ...parsed, recordPath });
636
+ }
637
+ } catch {
638
+ }
183
639
  }
640
+ return records;
184
641
  }
185
- function shortAddress(address) {
186
- if (!/^0x[0-9a-fA-F]{40}$/.test(address)) return address;
187
- return `${address.slice(0, 6)}...${address.slice(-4)}`;
642
+ function tombstoneScript(info) {
643
+ const lines = [
644
+ `${TOMBSTONE_MARKER}: this Haven agent was retired.`,
645
+ "",
646
+ ` agent: ${info.agent_id}`,
647
+ ` retired at: ${info.retired_at}`,
648
+ ` reason: ${info.reason}`,
649
+ ...info.replaced_by ? [` replaced by: ${info.replaced_by}`] : [],
650
+ "",
651
+ "This process is running with a wiring snapshot that predates the",
652
+ "retirement \u2014 it loaded its MCP config at startup and has kept it since.",
653
+ "Restart THIS host to pick up the current wiring. If several long-lived",
654
+ "hosts are running (a gateway, a TUI worker, an editor), restart EVERY",
655
+ "one of them: each holds the snapshot from its own start time, so after",
656
+ "a chain of recreations each can be parked on a DIFFERENT old agent.",
657
+ "",
658
+ `Then verify with: ${sdk.connectorRerunCommand("--doctor --runtime <runtime>")}`
659
+ ];
660
+ return [
661
+ "#!/usr/bin/env node",
662
+ `// ${TOMBSTONE_MARKER} \u2014 written by @haven_ai/connect (#1681). Safe to delete`,
663
+ "// once every long-lived MCP host on this machine has been restarted.",
664
+ `process.stderr.write(${JSON.stringify(lines.join("\n") + "\n")})`,
665
+ "process.exit(1)",
666
+ ""
667
+ ].join("\n");
188
668
  }
189
- var API_KEY_RE, PRIVATE_KEY_RE;
190
- var init_redact = __esm({
191
- "src/redact.ts"() {
192
- API_KEY_RE = /sk_agent_[A-Za-z0-9]+/g;
193
- PRIVATE_KEY_RE = /0x[0-9a-fA-F]{64}/g;
194
- }
195
- });
196
-
197
- // src/server-names.ts
198
- function assertValidServerSlug(slug) {
199
- if (slug.length === 0 || slug.length > 32 || !SLUG_RE.test(slug)) {
200
- throw new Error(
201
- `Invalid server name ${JSON.stringify(slug)}: use 1-32 lowercase letters, digits, and single hyphens (e.g. "research").`
202
- );
203
- }
204
- if (slug === "haven" || slug === "haven-signer") {
205
- throw new Error(
206
- `Invalid server name ${JSON.stringify(slug)}: "haven" and "haven-signer" are the unnamed pair's own names \u2014 omit --name for the bare pair.`
669
+ async function writeAgentTombstone(input) {
670
+ const dirStat = await promises.stat(input.directory).catch(() => null);
671
+ if (!dirStat?.isDirectory()) {
672
+ throw new exports.ConnectError(
673
+ "tombstone_directory_not_found",
674
+ `Not a directory: ${input.directory} \u2014 nothing to tombstone. Agent directories are named by their wiring SLUG when the agent has one, and by the agent id otherwise \u2014 so a path built from an agent id will not exist for a named agent. List ~/.haven/agents (or read the directories from --doctor --json) and pass one of those.`,
675
+ "retry_with_an_existing_agent_directory"
207
676
  );
208
677
  }
209
- if (slug === "signer" || slug.startsWith("signer-")) {
210
- throw new Error(
211
- `Invalid server name ${JSON.stringify(slug)}: "signer" and "signer-*" are reserved \u2014 they would collide with another pair's haven-signer-* entry.`
212
- );
678
+ const info = {
679
+ // reason / replaced_by are persisted to disk and re-emitted to the host's
680
+ // MCP stderr log on EVERY stale probe, potentially for months — redact
681
+ // like every other output path, at the write layer so any future caller
682
+ // inherits it. (#1681 review, finding 1)
683
+ agent_id: input.agentId,
684
+ retired_at: input.retiredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
685
+ reason: redactSecrets(input.reason),
686
+ ...input.replacedBy ? { replaced_by: redactSecrets(input.replacedBy) } : {}
687
+ };
688
+ const binDir = path.join(input.directory, "bin");
689
+ await promises.mkdir(binDir, { recursive: true });
690
+ const wrapperPath = path.join(binDir, "haven-signer.mjs");
691
+ await promises.writeFile(wrapperPath, tombstoneScript(info), "utf8");
692
+ await promises.chmod(wrapperPath, 493);
693
+ const record = JSON.stringify(info, null, 2) + "\n";
694
+ await promises.writeFile(path.join(input.directory, TOMBSTONE_FILENAME), record, "utf8");
695
+ const root = input.tombstonesDir ?? defaultTombstonesDir();
696
+ const recordPath = path.join(root, `${info.agent_id}.json`);
697
+ try {
698
+ await promises.mkdir(root, { recursive: true, mode: 448 });
699
+ await promises.writeFile(recordPath, record, { mode: MIRROR_MODE });
700
+ } catch (err) {
701
+ const code = err?.code;
702
+ return { ...info, recordPath: null, mirrorError: typeof code === "string" && /^E[A-Z]+$/.test(code) ? code : "mirror_write_failed" };
213
703
  }
704
+ return { ...info, recordPath };
214
705
  }
215
- function serverNamesFor(slug) {
216
- if (slug === void 0) {
217
- return {
218
- hosted: "haven",
219
- signer: "haven-signer",
220
- // Historical Codex table names — every wired Codex host has these.
221
- codexHosted: "haven",
222
- codexSigner: "haven_signer",
223
- hermesEnvKey: "MCP_HAVEN_API_KEY"
224
- };
706
+ async function readAgentTombstone(directory) {
707
+ try {
708
+ const parsed = JSON.parse(await promises.readFile(path.join(directory, TOMBSTONE_FILENAME), "utf8"));
709
+ if (typeof parsed?.agent_id !== "string") return null;
710
+ return parsed;
711
+ } catch {
712
+ return null;
225
713
  }
226
- assertValidServerSlug(slug);
227
- return {
228
- hosted: `haven-${slug}`,
229
- signer: `haven-signer-${slug}`,
230
- // Hyphens are valid TOML bare keys, so named Codex tables match the
231
- // JSON/YAML names instead of inheriting the legacy underscore.
232
- codexHosted: `haven-${slug}`,
233
- codexSigner: `haven-signer-${slug}`,
234
- hermesEnvKey: `MCP_HAVEN_${slug.toUpperCase().replace(/-/g, "_")}_API_KEY`
235
- };
236
714
  }
237
- var SLUG_RE;
238
- var init_server_names = __esm({
239
- "src/server-names.ts"() {
240
- SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
715
+ var TOMBSTONE_FILENAME, ROOT_TOMBSTONES_DIRNAME, MIRROR_MODE, TOMBSTONE_MARKER;
716
+ var init_tombstone = __esm({
717
+ "src/tombstone.ts"() {
718
+ init_redact();
719
+ init_connect_error();
720
+ TOMBSTONE_FILENAME = "TOMBSTONE.json";
721
+ ROOT_TOMBSTONES_DIRNAME = ".tombstones";
722
+ MIRROR_MODE = 384;
723
+ TOMBSTONE_MARKER = "HAVEN-TOMBSTONE";
241
724
  }
242
725
  });
726
+
727
+ // src/storage.ts
728
+ var storage_exports = {};
729
+ __export(storage_exports, {
730
+ CONNECT_OUTCOME_FILENAME: () => exports.CONNECT_OUTCOME_FILENAME,
731
+ MCP_SERVER_BINDING_FILENAME: () => MCP_SERVER_BINDING_FILENAME,
732
+ REKEY_PENDING_FILENAME: () => REKEY_PENDING_FILENAME,
733
+ REKEY_PENDING_TTL_MS: () => REKEY_PENDING_TTL_MS,
734
+ assertDirectoryRecordsSlug: () => assertDirectoryRecordsSlug,
735
+ assertServerSlugAvailable: () => assertServerSlugAvailable,
736
+ clearMcpServerBinding: () => clearMcpServerBinding,
737
+ clearRekeyPending: () => clearRekeyPending,
738
+ defaultAgentDirectory: () => defaultAgentDirectory,
739
+ defaultCredentialRoot: () => defaultCredentialRoot,
740
+ inspectRekeyPending: () => inspectRekeyPending,
741
+ listMcpServerBindings: () => listMcpServerBindings,
742
+ preflightCredentialStorage: () => preflightCredentialStorage,
743
+ readConnectOutcomeRuntime: () => readConnectOutcomeRuntime,
744
+ readMcpServerBinding: () => readMcpServerBinding,
745
+ readRekeyPending: () => readRekeyPending,
746
+ readStoredAccountAddress: () => readStoredAccountAddress,
747
+ readStoredCredentials: () => readStoredCredentials,
748
+ resolveAgentDirectoryBySlug: () => resolveAgentDirectoryBySlug,
749
+ rewriteCredentialFiles: () => rewriteCredentialFiles,
750
+ writeConnectOutcomeRecord: () => writeConnectOutcomeRecord,
751
+ writeCredentialFiles: () => writeCredentialFiles,
752
+ writeMcpServerBinding: () => writeMcpServerBinding,
753
+ writeRekeyPending: () => writeRekeyPending
754
+ });
243
755
  async function preflightCredentialStorage(input = {}) {
244
756
  const directory = defaultCredentialRoot(input.baseDir);
245
757
  await promises.mkdir(directory, { recursive: true, mode: 448 });
@@ -316,8 +828,7 @@ function agentPayload(input) {
316
828
  };
317
829
  }
318
830
  async function readStoredCredentials(serverName, agentIdOrSlug, baseDir) {
319
- const key = serverName ?? agentIdOrSlug;
320
- const directory = key ? defaultAgentDirectory(key, baseDir) : await discoverSoleAgentDirectory(baseDir);
831
+ const directory = serverName ? await resolveAgentDirectoryBySlug(serverName, { root: baseDir }) : agentIdOrSlug ? defaultAgentDirectory(agentIdOrSlug, baseDir) : await discoverSoleAgentDirectory(baseDir);
321
832
  const identity = await readJsonFile(path.join(directory, "identity.json"));
322
833
  if (!identity) {
323
834
  throw new Error(
@@ -325,7 +836,7 @@ async function readStoredCredentials(serverName, agentIdOrSlug, baseDir) {
325
836
  );
326
837
  }
327
838
  let discoveredSlug;
328
- if (!key) {
839
+ if (!serverName && !agentIdOrSlug) {
329
840
  const binding = await readJsonFile(path.join(directory, "mcp-server-binding.json"));
330
841
  const hostedName = binding && typeof binding === "object" ? asString(binding.server_name) : void 0;
331
842
  if (hostedName && hostedName.startsWith("haven-")) {
@@ -396,6 +907,65 @@ async function discoverSoleAgentDirectory(baseDir) {
396
907
  ` + candidates.map((d) => ` ${d}`).join("\n") + "\nRe-run with --name <slug> to pick one."
397
908
  );
398
909
  }
910
+ async function assertDirectoryRecordsSlug(directory, slug) {
911
+ assertValidServerSlug(slug);
912
+ const sidecar = await readRuntimeSidecar(directory);
913
+ const bindingRecord = await readJsonFile(path.join(directory, "mcp-server-binding.json"));
914
+ const bindingSlug = bindingRecord ? asString(bindingRecord.server_name) : void 0;
915
+ const claimed = sidecar?.server_name ?? bindingSlug;
916
+ if (claimed === slug) return;
917
+ if (!claimed && path.basename(directory) === slug) return;
918
+ throw new Error(
919
+ `--name ${slug} and --credentials-dir ${directory} disagree: that directory records ` + (claimed ? `the pair 'haven-${claimed}'` : "no named pair") + `. Pass one of them, not both \u2014 or point --credentials-dir at the directory that records 'haven-${slug}'.`
920
+ );
921
+ }
922
+ async function resolveAgentDirectoryBySlug(slug, opts = {}) {
923
+ assertValidServerSlug(slug);
924
+ const root = defaultCredentialRoot(opts.root);
925
+ let entries = [];
926
+ try {
927
+ entries = await promises.readdir(root);
928
+ } catch {
929
+ throw new Error(
930
+ `No agent directories under ${root} \u2014 nothing records the pair 'haven-${slug}'. Connect this agent first, or pass --credentials-dir <path> for the exact directory.`
931
+ );
932
+ }
933
+ const claims = [];
934
+ const recordedSlugs = [];
935
+ let barePairCount = 0;
936
+ let legacyFolder;
937
+ for (const entry of entries.sort()) {
938
+ const directory = path.join(root, entry);
939
+ const retired = await readJsonFile(path.join(directory, TOMBSTONE_FILENAME)) !== null;
940
+ const sidecar = await readRuntimeSidecar(directory);
941
+ const bindingRecord = await readJsonFile(path.join(directory, "mcp-server-binding.json"));
942
+ const bindingSlug = bindingRecord ? asString(bindingRecord.server_name) : void 0;
943
+ const claimedSlug = sidecar?.server_name ?? bindingSlug;
944
+ if (claimedSlug && !recordedSlugs.includes(claimedSlug)) recordedSlugs.push(claimedSlug);
945
+ if (sidecar && !sidecar.server_name) barePairCount++;
946
+ if (retired && !opts.allowRetired) continue;
947
+ if (claimedSlug === slug) {
948
+ const identity = await readJsonFile(path.join(directory, "identity.json"));
949
+ claims.push({ directory, agentId: identity ? asString(identity.agent_id) ?? null : null, retired });
950
+ }
951
+ if (!claimedSlug && !retired && entry === slug) legacyFolder = directory;
952
+ }
953
+ if (claims.length > 1) {
954
+ throw new Error(
955
+ `${claims.length} agent directories on this machine record the pair 'haven-${slug}' \u2014 the MCP server name changed hands (mcp_server_name_rebound):
956
+ ` + claims.map((c) => ` \u2022 ${c.agentId ? `agent ${c.agentId}` : "agent (no agent_id recorded)"} \u2014 ${c.directory}${c.retired ? " (retired)" : ""}`).join("\n") + "\nRetire the earlier one with --unwire, or name the directory exactly with --credentials-dir <path>. Nothing was changed."
957
+ );
958
+ }
959
+ if (claims.length === 1) return claims[0].directory;
960
+ if (legacyFolder) return legacyFolder;
961
+ const known = [
962
+ ...recordedSlugs.map((s) => `'haven-${s}'`),
963
+ ...barePairCount > 0 ? [`${barePairCount} unnamed 'haven' / 'haven-signer' pair${barePairCount === 1 ? "" : "s"} (no --name)`] : []
964
+ ];
965
+ throw new Error(
966
+ `No agent directory on this machine records the pair 'haven-${slug}'.` + (known.length > 0 ? ` Directories under ${root} record: ${known.join(", ")}.` : ` Nothing under ${root} records a named pair.`) + " Check the agent id against the Haven agent page, or name the directory exactly with --credentials-dir <path>."
967
+ );
968
+ }
399
969
  async function rewriteCredentialFiles(input) {
400
970
  const directory = defaultAgentDirectory(input.serverName ?? input.agentId, input.baseDir);
401
971
  const identityPath = path.join(directory, "identity.json");
@@ -605,6 +1175,7 @@ async function listMcpServerBindings(baseDir, excludeDirectory) {
605
1175
  const out = [];
606
1176
  for (const entry of entries) {
607
1177
  const directory = path.join(root, entry);
1178
+ if (excludeDirectory && directory === excludeDirectory) continue;
608
1179
  const binding = await readMcpServerBinding(directory);
609
1180
  if (binding) out.push({ directory, binding });
610
1181
  }
@@ -614,48 +1185,12 @@ var REKEY_PENDING_FILENAME, REKEY_PENDING_TTL_MS; exports.CONNECT_OUTCOME_FILENA
614
1185
  var init_storage = __esm({
615
1186
  "src/storage.ts"() {
616
1187
  init_server_names();
1188
+ init_signer_runtime();
1189
+ init_tombstone();
617
1190
  REKEY_PENDING_FILENAME = "rekey-pending.json";
618
1191
  REKEY_PENDING_TTL_MS = 24 * 60 * 60 * 1e3;
619
1192
  exports.CONNECT_OUTCOME_FILENAME = "last-connect-outcome.json";
620
- MCP_SERVER_BINDING_FILENAME = "mcp-server-binding.json";
621
- }
622
- });
623
- function mcpPackageSpec() {
624
- return `${exports.MCP_RUNTIME_MANIFEST.mcpPackage}@${exports.MCP_RUNTIME_MANIFEST.mcpVersion}`;
625
- }
626
- function sdkPackageSpec() {
627
- return `${exports.MCP_RUNTIME_MANIFEST.sdkPackage}@${exports.MCP_RUNTIME_MANIFEST.sdkVersion}`;
628
- }
629
- function signerPackageSpec() {
630
- return `${exports.MCP_RUNTIME_MANIFEST.signerPackage}@${exports.MCP_RUNTIME_MANIFEST.signerVersion}`;
631
- }
632
- exports.MCP_RUNTIME_MANIFEST = void 0;
633
- var init_runtime_manifest = __esm({
634
- "src/runtime-manifest.ts"() {
635
- exports.MCP_RUNTIME_MANIFEST = {
636
- mcpPackage: "@haven_ai/mcp",
637
- mcpVersion: mcp.MCP_VERSION,
638
- sdkPackage: "@haven_ai/sdk",
639
- sdkVersion: "0.9.0-alpha.0",
640
- signerPackage: "@haven_ai/signer",
641
- signerVersion: "0.9.0-alpha.0",
642
- // Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
643
- // while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
644
- // so the guard that was supposed to enforce the floor waved Node v23 through
645
- // — including on the `--local` path where it does run. A hand-maintained
646
- // second copy of a number is a drift waiting to happen; a guard test pins
647
- // this against `package.json`'s `engines.node`.
648
- minimumNodeVersion: sdk.HAVEN_MINIMUM_NODE_VERSION,
649
- supportedClients: ["codex-cli", "codex-desktop", "claude-code"],
650
- requiredTools: mcp.registeredToolNames(),
651
- /**
652
- * The signer MCP's tool surface, DERIVED from the pinned @haven_ai/signer
653
- * package (#1587) — same anti-drift rule as `requiredTools` above: a
654
- * literal list here would rot the first time the signer gains a tool.
655
- * The handshake probe requires all of them.
656
- */
657
- requiredSignerTools: Object.keys(signer.toolSchemas)
658
- };
1193
+ MCP_SERVER_BINDING_FILENAME = "mcp-server-binding.json";
659
1194
  }
660
1195
  });
661
1196
  async function writeRuntimeConfig(input, deps = {}) {
@@ -1692,295 +2227,23 @@ function parseJsonRpcPayload(raw) {
1692
2227
  const dataLines = trimmed.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim());
1693
2228
  for (let i = dataLines.length - 1; i >= 0; i -= 1) {
1694
2229
  try {
1695
- return JSON.parse(dataLines[i]);
1696
- } catch {
1697
- }
1698
- }
1699
- return null;
1700
- }
1701
- async function fetchWithTimeout(fetchImpl, url, init) {
1702
- const controller = new AbortController();
1703
- const timeout = setTimeout(() => controller.abort(), 3e3);
1704
- try {
1705
- return await fetchImpl(url, { ...init, signal: controller.signal });
1706
- } finally {
1707
- clearTimeout(timeout);
1708
- }
1709
- }
1710
- var init_probes = __esm({
1711
- "src/probes.ts"() {
1712
- }
1713
- });
1714
- function resolveRuntimeSpecOverride(env) {
1715
- const override = {};
1716
- for (const pkg of Object.keys(RUNTIME_SPEC_ENV)) {
1717
- const variable = RUNTIME_SPEC_ENV[pkg];
1718
- const raw = env[variable];
1719
- if (raw === void 0) continue;
1720
- override[pkg] = validateSpec(variable, raw);
1721
- }
1722
- return Object.keys(override).length > 0 ? override : void 0;
1723
- }
1724
- function validateSpec(variable, raw) {
1725
- if (raw.length === 0 || raw.trim().length === 0) {
1726
- throw new RuntimeSpecOverrideError(variable, "it is empty");
1727
- }
1728
- if (/\s/.test(raw)) {
1729
- throw new RuntimeSpecOverrideError(variable, "it contains whitespace");
1730
- }
1731
- if (/[\u0000-\u001f\u007f]/.test(raw)) {
1732
- throw new RuntimeSpecOverrideError(variable, "it contains a control character");
1733
- }
1734
- const meta = SHELL_METACHARACTERS.exec(raw);
1735
- if (meta) {
1736
- throw new RuntimeSpecOverrideError(variable, `it contains the shell metacharacter ${JSON.stringify(meta[0])}`);
1737
- }
1738
- return raw;
1739
- }
1740
- function runtimeSpecOverrideDirectoryKey(resolvedSpecs) {
1741
- const digest = crypto.createHash("sha256").update(resolvedSpecs.join("\n")).digest("hex");
1742
- return `override-${digest.slice(0, 12)}`;
1743
- }
1744
- function overrideApplies(override, packages) {
1745
- return override !== void 0 && packages.some((pkg) => override[pkg] !== void 0);
1746
- }
1747
- function runtimeSpecOverrideNotice(runtimeLabel, override, pinned, runtimeDirectory) {
1748
- const lines = [`RUNTIME SPEC OVERRIDE ACTIVE for the local Haven ${runtimeLabel} \u2014 this is NOT the pinned manifest.`];
1749
- for (const pkg of Object.keys(RUNTIME_SPEC_ENV)) {
1750
- const spec = override[pkg];
1751
- if (spec === void 0) continue;
1752
- const pin = pinned[pkg];
1753
- lines.push(` ${RUNTIME_SPEC_ENV[pkg]}=${spec}${pin ? ` (instead of ${pin})` : ""}`);
1754
- }
1755
- lines.push(` Installing into ${runtimeDirectory} \u2014 the pinned runtime directory is untouched.`);
1756
- lines.push(" Unset the variable(s) and re-run to return to the pinned manifest.");
1757
- return lines;
1758
- }
1759
- function describeRuntimeSpecOverride(override) {
1760
- return Object.keys(RUNTIME_SPEC_ENV).filter((pkg) => override[pkg] !== void 0).map((pkg) => `${RUNTIME_SPEC_ENV[pkg]}=${override[pkg]}`).join(" ");
1761
- }
1762
- var RUNTIME_SPEC_ENV, SHELL_METACHARACTERS, RuntimeSpecOverrideError;
1763
- var init_runtime_spec_override = __esm({
1764
- "src/runtime-spec-override.ts"() {
1765
- RUNTIME_SPEC_ENV = {
1766
- signer: "HAVEN_SIGNER_SPEC",
1767
- sdk: "HAVEN_SDK_SPEC",
1768
- mcp: "HAVEN_MCP_SPEC"
1769
- };
1770
- SHELL_METACHARACTERS = /[;&|<>$`'"()!#*?[\]{}\\]/;
1771
- RuntimeSpecOverrideError = class extends Error {
1772
- code = "runtime_spec_override_invalid";
1773
- variable;
1774
- constructor(variable, reason) {
1775
- super(`${variable} is set but not usable as an npm package spec: ${reason}. Unset it, or set it to a spec npm install accepts (file:/abs/path, a .tgz, or @haven_ai/<pkg>@<version>).`);
1776
- this.name = "RuntimeSpecOverrideError";
1777
- this.variable = variable;
1778
- }
1779
- };
1780
- }
1781
- });
1782
- async function prepareSignerRuntime(input, deps = {}) {
1783
- const homeDir = input.homeDir ?? os.homedir();
1784
- const override = resolveSignerRuntimeOverride(deps.env ?? process.env);
1785
- const signerSpec = override?.signer ?? signerPackageSpec();
1786
- const sdkSpec = override?.sdk ?? sdkPackageSpec();
1787
- const resolvedSpecs = [signerSpec, sdkSpec];
1788
- const overrideRecord = override ? { specs: override, resolved_specs: resolvedSpecs, directory_key: runtimeSpecOverrideDirectoryKey(resolvedSpecs) } : void 0;
1789
- const runtimeDirectory = path.resolve(
1790
- homeDir,
1791
- ".haven",
1792
- "signer-runtime",
1793
- overrideRecord ? overrideRecord.directory_key : exports.MCP_RUNTIME_MANIFEST.signerVersion
1794
- );
1795
- const npmCacheDirectory = path.resolve(homeDir, ".haven", "npm-cache");
1796
- const cliPath = path.join(runtimeDirectory, "node_modules", "@haven_ai", "signer", "dist", "cli.js");
1797
- const messages = [];
1798
- if (override) {
1799
- messages.push(...runtimeSpecOverrideNotice(
1800
- "signer runtime",
1801
- override,
1802
- { signer: signerPackageSpec(), sdk: sdkPackageSpec() },
1803
- runtimeDirectory
1804
- ));
1805
- }
1806
- await promises.mkdir(runtimeDirectory, { recursive: true, mode: 448 });
1807
- await promises.chmod(runtimeDirectory, 448).catch(() => void 0);
1808
- await promises.mkdir(npmCacheDirectory, { recursive: true, mode: 448 });
1809
- await promises.chmod(npmCacheDirectory, 448).catch(() => void 0);
1810
- if (override) {
1811
- await installRuntimePackages(runtimeDirectory, npmCacheDirectory, resolvedSpecs, deps);
1812
- messages.push(`Installed local Haven signer runtime from override (${resolvedSpecs.join(" ")}).`);
1813
- } else if (await installedRuntimeMatches(runtimeDirectory, cliPath)) {
1814
- messages.push(`Using existing local Haven signer runtime ${signerPackageSpec()}.`);
1815
- } else {
1816
- await installRuntimePackages(runtimeDirectory, npmCacheDirectory, resolvedSpecs, deps);
1817
- messages.push(`Installed local Haven signer runtime ${signerPackageSpec()}.`);
1818
- }
1819
- await assertFileExists(cliPath, "local Haven signer CLI");
1820
- const installedVersions = override ? await readInstalledVersions(runtimeDirectory) : void 0;
1821
- const wrapperPath = path.join(input.credentialDirectory, "bin", "haven-signer.mjs");
1822
- await writeWrapper({
1823
- wrapperPath,
1824
- cliPath,
1825
- signerPath: input.signerPath,
1826
- overrideComment: override ? describeRuntimeSpecOverride(override) : void 0
1827
- });
1828
- await writeRuntimeSidecar({
1829
- path: path.join(input.credentialDirectory, "signer-runtime.json"),
1830
- wrapperPath,
1831
- runtimeDirectory,
1832
- npmCacheDirectory,
1833
- cliPath,
1834
- serverName: input.serverName,
1835
- override: overrideRecord,
1836
- installedVersions
1837
- });
1838
- messages.push(`Prepared stable local Haven signer wrapper: ${wrapperPath}`);
1839
- return {
1840
- command: wrapperPath,
1841
- args: [],
1842
- wrapperPath,
1843
- runtimeDirectory,
1844
- npmCacheDirectory,
1845
- cliPath,
1846
- messages,
1847
- ...overrideRecord ? { runtimeSpecOverride: overrideRecord } : {}
1848
- };
1849
- }
1850
- function resolveSignerRuntimeOverride(env) {
1851
- const override = resolveRuntimeSpecOverride(env);
1852
- if (!overrideApplies(override, ["signer", "sdk"])) return void 0;
1853
- const { signer, sdk } = override;
1854
- return { ...signer !== void 0 ? { signer } : {}, ...sdk !== void 0 ? { sdk } : {} };
1855
- }
1856
- async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, packageSpecs, deps) {
1857
- const { runCommand, onProgress } = deps;
1858
- const baseArgs = [
1859
- "install",
1860
- "--prefix",
1861
- runtimeDirectory,
1862
- "--no-audit",
1863
- "--no-fund",
1864
- "--omit=dev",
1865
- "--prefer-offline",
1866
- ...packageSpecs
1867
- ];
1868
- const run = async (args) => {
1869
- const startedAt = Date.now();
1870
- const heartbeat = setInterval(() => {
1871
- const seconds = Math.round((Date.now() - startedAt) / 1e3);
1872
- onProgress?.(`Still installing the local Haven signer runtime\u2026 (${seconds}s \u2014 a cold cache can take several minutes)`);
1873
- }, SIGNER_INSTALL_HEARTBEAT_MS);
1874
- heartbeat.unref?.();
1875
- try {
1876
- if (runCommand) await runCommand("npm", args);
1877
- else await execFileAsync("npm", args, { timeout: SIGNER_INSTALL_TIMEOUT_MS, maxBuffer: 1024 * 1024 });
1878
- } finally {
1879
- clearInterval(heartbeat);
1880
- }
1881
- };
1882
- try {
1883
- await run(baseArgs);
1884
- } catch {
1885
- try {
1886
- await run([...baseArgs, "--cache", npmCacheDirectory]);
1887
- } catch (err) {
1888
- throw new Error(
1889
- `Could not install local Haven signer runtime ${packageSpecs.join(" ")}: ${err instanceof Error ? err.message : String(err)}`
1890
- );
1891
- }
1892
- }
1893
- }
1894
- async function installedRuntimeMatches(runtimeDirectory, cliPath) {
1895
- return installedRuntimeMatchesVersions(runtimeDirectory, cliPath, {
1896
- signerVersion: exports.MCP_RUNTIME_MANIFEST.signerVersion,
1897
- sdkVersion: exports.MCP_RUNTIME_MANIFEST.sdkVersion
1898
- });
1899
- }
1900
- async function installedRuntimeMatchesVersions(runtimeDirectory, cliPath, expected) {
1901
- try {
1902
- await assertFileExists(cliPath, "local Haven signer CLI");
1903
- const installed = await readInstalledVersions(runtimeDirectory);
1904
- return installed.signerVersion === expected.signerVersion && installed.sdkVersion === expected.sdkVersion;
1905
- } catch {
1906
- return false;
1907
- }
1908
- }
1909
- async function readInstalledVersions(runtimeDirectory) {
1910
- const [signerPackage, sdkPackage] = await Promise.all([
1911
- readPackageJson(path.join(runtimeDirectory, "node_modules", "@haven_ai", "signer", "package.json")),
1912
- readPackageJson(path.join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
1913
- ]);
1914
- return { signerVersion: signerPackage.version ?? "", sdkVersion: sdkPackage.version ?? "" };
1915
- }
1916
- async function readPackageJson(path) {
1917
- return JSON.parse(await promises.readFile(path, "utf8"));
1918
- }
1919
- async function writeWrapper(input) {
1920
- await promises.mkdir(path.dirname(input.wrapperPath), { recursive: true, mode: 448 });
1921
- await promises.chmod(path.dirname(input.wrapperPath), 448).catch(() => void 0);
1922
- const source = [
1923
- "#!/usr/bin/env node",
1924
- ...input.overrideComment ? [`// HAVEN RUNTIME SPEC OVERRIDE (#2424): ${input.overrideComment} \u2014 this wrapper launches a NON-pinned signer build.`] : [],
1925
- "import { spawn } from 'node:child_process'",
1926
- "",
1927
- `const cliPath = ${JSON.stringify(input.cliPath)}`,
1928
- `const signerPath = ${JSON.stringify(input.signerPath)}`,
1929
- "",
1930
- "const child = spawn(process.execPath, [cliPath, '--credentials', signerPath, ...process.argv.slice(2)], {",
1931
- " stdio: 'inherit',",
1932
- "})",
1933
- "",
1934
- "child.on('exit', (code, signal) => {",
1935
- " if (signal) process.kill(process.pid, signal)",
1936
- " else process.exit(code ?? 1)",
1937
- "})",
1938
- ""
1939
- ].join("\n");
1940
- await promises.writeFile(input.wrapperPath, source, { mode: 448 });
1941
- await promises.chmod(input.wrapperPath, 448).catch(() => void 0);
1942
- }
1943
- async function readRuntimeSidecar(credentialDirectory) {
1944
- try {
1945
- return JSON.parse(
1946
- await promises.readFile(path.join(credentialDirectory, "signer-runtime.json"), "utf8")
1947
- );
1948
- } catch {
1949
- return null;
2230
+ return JSON.parse(dataLines[i]);
2231
+ } catch {
2232
+ }
1950
2233
  }
2234
+ return null;
1951
2235
  }
1952
- async function writeRuntimeSidecar(input) {
1953
- const value = {
1954
- ...input.serverName ? { server_name: input.serverName } : {},
1955
- signer_package: exports.MCP_RUNTIME_MANIFEST.signerPackage,
1956
- signer_version: input.installedVersions?.signerVersion ?? exports.MCP_RUNTIME_MANIFEST.signerVersion,
1957
- sdk_package: exports.MCP_RUNTIME_MANIFEST.sdkPackage,
1958
- sdk_version: input.installedVersions?.sdkVersion ?? exports.MCP_RUNTIME_MANIFEST.sdkVersion,
1959
- wrapper_path: input.wrapperPath,
1960
- runtime_directory: input.runtimeDirectory,
1961
- npm_cache_directory: input.npmCacheDirectory,
1962
- cli_path: input.cliPath,
1963
- ...input.override ? { runtime_spec_override: input.override } : {}
1964
- };
1965
- await promises.writeFile(input.path, `${JSON.stringify(value, null, 2)}
1966
- `, { mode: 384 });
1967
- await promises.chmod(input.path, 384).catch(() => void 0);
1968
- }
1969
- async function assertFileExists(path, label) {
2236
+ async function fetchWithTimeout(fetchImpl, url, init) {
2237
+ const controller = new AbortController();
2238
+ const timeout = setTimeout(() => controller.abort(), 3e3);
1970
2239
  try {
1971
- await promises.access(path);
1972
- } catch {
1973
- throw new Error(`Missing ${label}: ${path}`);
2240
+ return await fetchImpl(url, { ...init, signal: controller.signal });
2241
+ } finally {
2242
+ clearTimeout(timeout);
1974
2243
  }
1975
2244
  }
1976
- var execFileAsync, SIGNER_INSTALL_TIMEOUT_MS, SIGNER_INSTALL_HEARTBEAT_MS;
1977
- var init_signer_runtime = __esm({
1978
- "src/signer-runtime.ts"() {
1979
- init_runtime_manifest();
1980
- init_runtime_spec_override();
1981
- execFileAsync = util.promisify(child_process.execFile);
1982
- SIGNER_INSTALL_TIMEOUT_MS = 6e5;
1983
- SIGNER_INSTALL_HEARTBEAT_MS = 15e3;
2245
+ var init_probes = __esm({
2246
+ "src/probes.ts"() {
1984
2247
  }
1985
2248
  });
1986
2249
  async function prepareLocalMcpRuntime(input, deps = {}) {
@@ -2298,28 +2561,6 @@ var init_skill_install = __esm({
2298
2561
  }
2299
2562
  });
2300
2563
 
2301
- // src/connect-error.ts
2302
- function isConnectError(err) {
2303
- return err instanceof exports.ConnectError;
2304
- }
2305
- exports.ConnectError = void 0;
2306
- var init_connect_error = __esm({
2307
- "src/connect-error.ts"() {
2308
- exports.ConnectError = class extends Error {
2309
- code;
2310
- nextAction;
2311
- details;
2312
- constructor(code, message, nextAction2, details = {}) {
2313
- super(message);
2314
- this.name = "ConnectError";
2315
- this.code = code;
2316
- this.nextAction = nextAction2;
2317
- this.details = details;
2318
- }
2319
- };
2320
- }
2321
- });
2322
-
2323
2564
  // src/runtime-registry.ts
2324
2565
  function runtimeProfile(runtime, env = process.env) {
2325
2566
  return RUNTIME_PROFILES[normalizeRuntime(runtime, env)];
@@ -3241,138 +3482,6 @@ var init_installed_clients = __esm({
3241
3482
  MAX_PROMPT_ATTEMPTS = 3;
3242
3483
  }
3243
3484
  });
3244
-
3245
- // src/tombstone.ts
3246
- var tombstone_exports = {};
3247
- __export(tombstone_exports, {
3248
- ROOT_TOMBSTONES_DIRNAME: () => ROOT_TOMBSTONES_DIRNAME,
3249
- TOMBSTONE_FILENAME: () => TOMBSTONE_FILENAME,
3250
- TOMBSTONE_MARKER: () => TOMBSTONE_MARKER,
3251
- defaultTombstonesDir: () => defaultTombstonesDir,
3252
- readAgentTombstone: () => readAgentTombstone,
3253
- readTombstoneRecords: () => readTombstoneRecords,
3254
- tombstonesDirForAgentDirectory: () => tombstonesDirForAgentDirectory,
3255
- tombstonesDirForCredentialRoot: () => tombstonesDirForCredentialRoot,
3256
- writeAgentTombstone: () => writeAgentTombstone
3257
- });
3258
- function defaultTombstonesDir(baseDir) {
3259
- return path.join(baseDir ?? path.join(os.homedir(), ".haven"), "tombstones");
3260
- }
3261
- function tombstonesDirForCredentialRoot(credentialRoot, homeDir = os.homedir()) {
3262
- if (!credentialRoot) return defaultTombstonesDir(path.join(homeDir, ".haven"));
3263
- const root = path.resolve(credentialRoot);
3264
- if (root === path.resolve(homeDir, ".haven", "agents")) return defaultTombstonesDir(path.join(homeDir, ".haven"));
3265
- return path.join(root, ROOT_TOMBSTONES_DIRNAME);
3266
- }
3267
- function tombstonesDirForAgentDirectory(directory, homeDir = os.homedir()) {
3268
- return tombstonesDirForCredentialRoot(path.dirname(path.resolve(directory)), homeDir);
3269
- }
3270
- async function readTombstoneRecords(tombstonesDir) {
3271
- const root = tombstonesDir ?? defaultTombstonesDir();
3272
- let entries = [];
3273
- try {
3274
- entries = await promises.readdir(root);
3275
- } catch {
3276
- return [];
3277
- }
3278
- const records = [];
3279
- for (const entry of entries) {
3280
- if (!entry.endsWith(".json")) continue;
3281
- const recordPath = path.join(root, entry);
3282
- try {
3283
- const parsed = JSON.parse(await promises.readFile(recordPath, "utf8"));
3284
- if (typeof parsed?.agent_id === "string") {
3285
- records.push({ ...parsed, recordPath });
3286
- }
3287
- } catch {
3288
- }
3289
- }
3290
- return records;
3291
- }
3292
- function tombstoneScript(info) {
3293
- const lines = [
3294
- `${TOMBSTONE_MARKER}: this Haven agent was retired.`,
3295
- "",
3296
- ` agent: ${info.agent_id}`,
3297
- ` retired at: ${info.retired_at}`,
3298
- ` reason: ${info.reason}`,
3299
- ...info.replaced_by ? [` replaced by: ${info.replaced_by}`] : [],
3300
- "",
3301
- "This process is running with a wiring snapshot that predates the",
3302
- "retirement \u2014 it loaded its MCP config at startup and has kept it since.",
3303
- "Restart THIS host to pick up the current wiring. If several long-lived",
3304
- "hosts are running (a gateway, a TUI worker, an editor), restart EVERY",
3305
- "one of them: each holds the snapshot from its own start time, so after",
3306
- "a chain of recreations each can be parked on a DIFFERENT old agent.",
3307
- "",
3308
- `Then verify with: ${sdk.connectorRerunCommand("--doctor --runtime <runtime>")}`
3309
- ];
3310
- return [
3311
- "#!/usr/bin/env node",
3312
- `// ${TOMBSTONE_MARKER} \u2014 written by @haven_ai/connect (#1681). Safe to delete`,
3313
- "// once every long-lived MCP host on this machine has been restarted.",
3314
- `process.stderr.write(${JSON.stringify(lines.join("\n") + "\n")})`,
3315
- "process.exit(1)",
3316
- ""
3317
- ].join("\n");
3318
- }
3319
- async function writeAgentTombstone(input) {
3320
- const dirStat = await promises.stat(input.directory).catch(() => null);
3321
- if (!dirStat?.isDirectory()) {
3322
- throw new exports.ConnectError(
3323
- "tombstone_directory_not_found",
3324
- `Not a directory: ${input.directory} \u2014 nothing to tombstone. Agent directories are named by their wiring SLUG when the agent has one, and by the agent id otherwise \u2014 so a path built from an agent id will not exist for a named agent. List ~/.haven/agents (or read the directories from --doctor --json) and pass one of those.`,
3325
- "retry_with_an_existing_agent_directory"
3326
- );
3327
- }
3328
- const info = {
3329
- // reason / replaced_by are persisted to disk and re-emitted to the host's
3330
- // MCP stderr log on EVERY stale probe, potentially for months — redact
3331
- // like every other output path, at the write layer so any future caller
3332
- // inherits it. (#1681 review, finding 1)
3333
- agent_id: input.agentId,
3334
- retired_at: input.retiredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
3335
- reason: redactSecrets(input.reason),
3336
- ...input.replacedBy ? { replaced_by: redactSecrets(input.replacedBy) } : {}
3337
- };
3338
- const binDir = path.join(input.directory, "bin");
3339
- await promises.mkdir(binDir, { recursive: true });
3340
- const wrapperPath = path.join(binDir, "haven-signer.mjs");
3341
- await promises.writeFile(wrapperPath, tombstoneScript(info), "utf8");
3342
- await promises.chmod(wrapperPath, 493);
3343
- const record = JSON.stringify(info, null, 2) + "\n";
3344
- await promises.writeFile(path.join(input.directory, TOMBSTONE_FILENAME), record, "utf8");
3345
- const root = input.tombstonesDir ?? defaultTombstonesDir();
3346
- const recordPath = path.join(root, `${info.agent_id}.json`);
3347
- try {
3348
- await promises.mkdir(root, { recursive: true, mode: 448 });
3349
- await promises.writeFile(recordPath, record, { mode: MIRROR_MODE });
3350
- } catch (err) {
3351
- const code = err?.code;
3352
- return { ...info, recordPath: null, mirrorError: typeof code === "string" && /^E[A-Z]+$/.test(code) ? code : "mirror_write_failed" };
3353
- }
3354
- return { ...info, recordPath };
3355
- }
3356
- async function readAgentTombstone(directory) {
3357
- try {
3358
- const parsed = JSON.parse(await promises.readFile(path.join(directory, TOMBSTONE_FILENAME), "utf8"));
3359
- if (typeof parsed?.agent_id !== "string") return null;
3360
- return parsed;
3361
- } catch {
3362
- return null;
3363
- }
3364
- }
3365
- var TOMBSTONE_FILENAME, ROOT_TOMBSTONES_DIRNAME, MIRROR_MODE, TOMBSTONE_MARKER;
3366
- var init_tombstone = __esm({
3367
- "src/tombstone.ts"() {
3368
- init_redact();
3369
- init_connect_error();
3370
- TOMBSTONE_FILENAME = "TOMBSTONE.json";
3371
- ROOT_TOMBSTONES_DIRNAME = ".tombstones";
3372
- MIRROR_MODE = 384;
3373
- TOMBSTONE_MARKER = "HAVEN-TOMBSTONE";
3374
- }
3375
- });
3376
3485
  async function detectWiringCollision(input) {
3377
3486
  const root = defaultCredentialRoot(input.credentialsDir);
3378
3487
  let entries = [];
@@ -4088,10 +4197,10 @@ async function executeConnect(options, deps, trace) {
4088
4197
  } else if (supersededAgentIds.length > 0) {
4089
4198
  log("");
4090
4199
  log(
4091
- `Heads-up: this setup created a NEW agent. Your previous agent(s) \u2014 ${supersededAgentIds.join(", ")} \u2014 still exist with their own keys, and any host that was already running keeps acting as them.`
4200
+ `Heads-up: other agent directories on this machine \u2014 ${supersededAgentIds.join(", ")}. This setup created a NEW agent and revoked none of them; any not revoked in Haven keep their own keys and any budgets.`
4092
4201
  );
4093
4202
  log(
4094
- `If you meant to replace them: use Remove agent\u2026 on the Haven agent page for each (it ends their live budgets), then restart EVERY long-lived host (gateways, TUI workers, editors) \u2014 each holds the MCP wiring snapshot from its own start time, so after repeated setups each can be stuck on a DIFFERENT old agent. Then remove their directories under ~/.haven/agents (or ${RERUN_HINT} --tombstone <dir> to leave a diagnostic in their place). Run ${RERUN_HINT} --doctor to check whether their keys are still live.`
4203
+ `To retire one you no longer use: use Remove agent\u2026 on its Haven agent page (it ends its live budget), restart any long-lived host (gateways, TUI workers, editors) still wired to it \u2014 each holds the MCP wiring snapshot from its own start time \u2014 and remove its directory under ~/.haven/agents (or ${RERUN_HINT} --tombstone <dir> to leave a diagnostic in its place). To rewire the bare haven / haven-signer pair instead of adding an agent, re-run with --replace. Run ${RERUN_HINT} --doctor to check which keys are still live.`
4095
4204
  );
4096
4205
  }
4097
4206
  } catch {
@@ -4122,6 +4231,18 @@ async function executeConnect(options, deps, trace) {
4122
4231
  } catch (err) {
4123
4232
  log(`Could not report install status to Haven: ${err instanceof Error ? err.message : String(err)}`);
4124
4233
  }
4234
+ const reportableRetiredIds = [...new Set(retiredAgentIds.filter((id) => UUID_SHAPE.test(id)))];
4235
+ if (reportableRetiredIds.length > 0 && api.reportRetiredAgents) {
4236
+ try {
4237
+ await api.reportRetiredAgents({
4238
+ setupId: registration.setup_id,
4239
+ apiKey: localApiKey,
4240
+ retiredAgentIds: reportableRetiredIds
4241
+ });
4242
+ } catch (err) {
4243
+ log(`Could not tell Haven which agents were retired: ${err instanceof Error ? err.message : String(err)}`);
4244
+ }
4245
+ }
4125
4246
  let approval;
4126
4247
  const narrateApprovalWait = options.waitForApproval === true || options.waitForApproval === void 0 && (deps.isStdoutTty ?? Boolean(process.stdout.isTTY));
4127
4248
  if (narrateApprovalWait && !runtimeInstall.errorCode) {
@@ -4564,7 +4685,7 @@ async function listOtherAgentIds(baseDir, currentDirectory) {
4564
4685
  function printNextSteps(result, log, approval, approvalUrl) {
4565
4686
  for (const line of completionHandoffLines(result, approval, approvalUrl)) log(line);
4566
4687
  }
4567
- exports.CONNECTOR_VERSION = void 0; var PRODUCTION_CHAIN_ID, CONNECTOR_CLIENT_IDENTITY; exports.CONNECT_OUTCOME_SCHEMA_VERSION = void 0; var failureOutcomesByError, RERUN_HINT;
4688
+ exports.CONNECTOR_VERSION = void 0; var PRODUCTION_CHAIN_ID, UUID_SHAPE, CONNECTOR_CLIENT_IDENTITY; exports.CONNECT_OUTCOME_SCHEMA_VERSION = void 0; var failureOutcomesByError, RERUN_HINT;
4568
4689
  var init_runtime = __esm({
4569
4690
  "src/runtime.ts"() {
4570
4691
  init_api();
@@ -4581,8 +4702,9 @@ var init_runtime = __esm({
4581
4702
  init_tombstone();
4582
4703
  init_local_mcp_runtime();
4583
4704
  init_runtime_manifest();
4584
- exports.CONNECTOR_VERSION = "0.9.0-alpha.0";
4705
+ exports.CONNECTOR_VERSION = "0.10.0-alpha.0";
4585
4706
  PRODUCTION_CHAIN_ID = 8453;
4707
+ UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
4586
4708
  CONNECTOR_CLIENT_IDENTITY = `@haven_ai/connect/${exports.CONNECTOR_VERSION}`;
4587
4709
  exports.CONNECT_OUTCOME_SCHEMA_VERSION = 1;
4588
4710
  failureOutcomesByError = /* @__PURE__ */ new WeakMap();
@@ -5063,6 +5185,40 @@ function rollUpLevel(checks) {
5063
5185
  if (checks.some((check) => check.level === "advisory")) return "advisory";
5064
5186
  return "ok";
5065
5187
  }
5188
+ function runWrapperAck(wrapperPath, timeoutMs = 3e4) {
5189
+ return new Promise((resolve11, reject) => {
5190
+ const child = child_process.spawn(wrapperPath, ["--ack"], { stdio: ["ignore", "pipe", "pipe"] });
5191
+ let stdout = "";
5192
+ let settled = false;
5193
+ const timer = setTimeout(() => {
5194
+ if (settled) return;
5195
+ settled = true;
5196
+ child.kill();
5197
+ reject(new Error(`signer --ack timed out after ${Math.round(timeoutMs / 1e3)}s`));
5198
+ }, timeoutMs);
5199
+ child.stdout?.on("data", (chunk) => {
5200
+ stdout += chunk.toString("utf8");
5201
+ });
5202
+ child.on("error", (err) => {
5203
+ if (settled) return;
5204
+ settled = true;
5205
+ clearTimeout(timer);
5206
+ reject(err);
5207
+ });
5208
+ child.on("close", (code) => {
5209
+ if (settled) return;
5210
+ settled = true;
5211
+ clearTimeout(timer);
5212
+ if (code === 0) resolve11({ stdout });
5213
+ else reject(new Error(`signer --ack exited ${code ?? "abnormally"}`));
5214
+ });
5215
+ });
5216
+ }
5217
+ function repairCommandNamedFor(runtime, slug) {
5218
+ const nameFlag = ` --name ${slug}`;
5219
+ if (normalizeRuntimeName(runtime)) return `Run: ${RERUN} --doctor --repair --runtime ${runtime}${nameFlag}`;
5220
+ return `Run: ${RERUN} --doctor --repair --runtime <runtime>${nameFlag} \u2014 --repair needs the runtime named; one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`;
5221
+ }
5066
5222
  function runtimeFlagFor(runtime) {
5067
5223
  return normalizeRuntimeName(runtime) ? ` --runtime ${runtime}` : "";
5068
5224
  }
@@ -5144,6 +5300,35 @@ async function discoverCredentialDirectory(homeDir, explicit) {
5144
5300
  identityDirectories
5145
5301
  };
5146
5302
  }
5303
+ async function readClaudeCodeWiringEvidence(homeDir) {
5304
+ let parsed;
5305
+ try {
5306
+ parsed = JSON.parse(await promises.readFile(path.join(homeDir, ".claude.json"), "utf8"));
5307
+ } catch {
5308
+ return null;
5309
+ }
5310
+ if (typeof parsed !== "object" || parsed === null) return null;
5311
+ const blocks = [parsed.mcpServers];
5312
+ const projects = parsed.projects;
5313
+ if (typeof projects === "object" && projects !== null) {
5314
+ for (const project of Object.values(projects)) {
5315
+ if (typeof project === "object" && project !== null) blocks.push(project.mcpServers);
5316
+ }
5317
+ }
5318
+ const lines = [];
5319
+ for (const block of blocks) {
5320
+ if (typeof block !== "object" || block === null) continue;
5321
+ for (const [name, server] of Object.entries(block)) {
5322
+ const entry = typeof server === "object" && server !== null ? server : {};
5323
+ const parts = [JSON.stringify(name) + ":"];
5324
+ if (typeof entry.command === "string") parts.push(entry.command);
5325
+ if (Array.isArray(entry.args)) parts.push(...entry.args.filter((arg) => typeof arg === "string"));
5326
+ if (typeof entry.url === "string") parts.push(entry.url);
5327
+ lines.push(parts.join(" "));
5328
+ }
5329
+ }
5330
+ return lines.length > 0 ? lines.join("\n") : null;
5331
+ }
5147
5332
  function agentIsWired(configText, names, slug, identity, sidecar, isPrimary, bareOwnerExists) {
5148
5333
  if (configText === null) return isPrimary;
5149
5334
  if (slug) {
@@ -5389,8 +5574,11 @@ async function verdictsForAgent(entry, input, deps) {
5389
5574
  id: "signer_process",
5390
5575
  label: "Signer stdio handshake",
5391
5576
  level: "failed",
5392
- detail: "The local-tools consent is not acknowledged, so the signer refuses to start (by design).",
5393
- repair: `Run: ${RERUN} --ack-local-tools --setup <token> (or re-run your original connector command with --ack-local-tools).`
5577
+ // #3799: honest about what the mismatch does and does not prove. The
5578
+ // stored ack covers a tool SET; a doctor bundled with a newer signer
5579
+ // reports exactly this against an older, still-working install.
5580
+ detail: "The stored local-tools consent does not match the tool set this connector bundles \u2014 by design a signer with a different tool set refuses to start. This does not prove the currently running signer is broken: a doctor newer than the installed signer reports exactly this mismatch.",
5581
+ repair: `Run: ${sidecar.wrapper_path} --ack < /dev/null \u2014 re-acknowledge consent with the installed signer, no setup token needed. Or re-run repair with --ack-local-tools: ${repairCommandFor(input.runtime, " --ack-local-tools", input.repairDir)}.`
5394
5582
  });
5395
5583
  } else {
5396
5584
  const probe = await (deps.probeSignerTools ?? probeLocalMcpTools)(
@@ -5421,12 +5609,21 @@ async function verdictsForAgent(entry, input, deps) {
5421
5609
  }
5422
5610
  return { checks, ...signerCapabilities ? { signerCapabilities } : {} };
5423
5611
  }
5612
+ async function resolveNamedDirectory(input, homeDir) {
5613
+ if (!input.serverName) return input.credentialsDir;
5614
+ if (input.credentialsDir) {
5615
+ await assertDirectoryRecordsSlug(input.credentialsDir, input.serverName);
5616
+ return input.credentialsDir;
5617
+ }
5618
+ return resolveAgentDirectoryBySlug(input.serverName, { root: path.join(homeDir, ".haven", "agents") });
5619
+ }
5424
5620
  async function runDoctor(input, deps = {}) {
5425
5621
  const homeDir = deps.homeDir ?? os.homedir();
5426
5622
  const checks = [];
5427
5623
  let signerCapabilities;
5428
- const { directory, others, parkedOnly, identityDirectories } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
5429
- const nameRepairDir = identityDirectories.length > 1 || Boolean(input.credentialsDir);
5624
+ const namedDir = await resolveNamedDirectory(input, homeDir);
5625
+ const { directory, others, parkedOnly, identityDirectories } = await discoverCredentialDirectory(homeDir, namedDir);
5626
+ const nameRepairDir = identityDirectories.length > 1 || Boolean(namedDir);
5430
5627
  const resolution = await resolveDoctorRuntime(input, directory);
5431
5628
  const runtime = resolution.runtime;
5432
5629
  const input2 = { ...input, runtime };
@@ -5440,6 +5637,7 @@ async function runDoctor(input, deps = {}) {
5440
5637
  configText = null;
5441
5638
  }
5442
5639
  }
5640
+ const claudeCodeWiring = configText === null && normalizedRuntime === "claude-code" ? await readClaudeCodeWiringEvidence(homeDir) : null;
5443
5641
  const allDirectories = directory ? [directory, ...others] : others;
5444
5642
  let bareOwnerExists = false;
5445
5643
  for (const dir of allDirectories) {
@@ -5473,7 +5671,10 @@ async function runDoctor(input, deps = {}) {
5473
5671
  });
5474
5672
  continue;
5475
5673
  }
5476
- const wired = agentIsWired(configText, names, slug, identity, sidecar, dir === directory, bareOwnerExists);
5674
+ const wired = agentIsWired(configText, names, slug, identity, sidecar, dir === directory, bareOwnerExists) || // Additive only: a name or wrapper path in ~/.claude.json proves this
5675
+ // directory is wired; its absence proves nothing (#3830), so the
5676
+ // fallback verdict above is never downgraded.
5677
+ claudeCodeWiring !== null && agentIsWired(claudeCodeWiring, names, slug, identity, sidecar, false, false);
5477
5678
  const entry = {
5478
5679
  ...slug ? { slug } : {},
5479
5680
  ...identity.agent_id ? { agentId: identity.agent_id } : {},
@@ -5541,15 +5742,20 @@ async function runDoctor(input, deps = {}) {
5541
5742
  id: "runtime_config",
5542
5743
  label: "Runtime MCP config",
5543
5744
  level: "failed",
5544
- detail: `Runtime is unknown \u2014 no runtime flag was given and the connector's record in ${primaryDirectory ?? input.credentialsDir ?? "~/.haven/agents"} carries no resolvable ${exports.CONNECT_OUTCOME_FILENAME} runtime. The runtime config was NOT checked. Re-run the doctor naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`,
5745
+ detail: `Runtime is unknown \u2014 no runtime flag was given and the connector's record in ${primaryDirectory ?? namedDir ?? "~/.haven/agents"} carries no resolvable ${exports.CONNECT_OUTCOME_FILENAME} runtime. The runtime config was NOT checked. Re-run the doctor naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`,
5545
5746
  repair: `Re-run the doctor naming the runtime \u2014 one of: ${RUNTIME_FLAG_VALUE_LIST.join(", ")}.`
5546
5747
  });
5547
5748
  } else if (configPath === null) {
5749
+ const claudePairSuffix = input2.runtime === "claude-code" && primaryDirectory ? await (async () => {
5750
+ const sidecar = await readRuntimeSidecar(primaryDirectory);
5751
+ const names = serverNamesFor(sidecar?.server_name);
5752
+ return ` The MCP pair recorded for this folder is ${names.hosted} / ${names.signer} \u2014 confirm with \`claude mcp get ${names.signer}\`.`;
5753
+ })() : "";
5548
5754
  checks.push({
5549
5755
  id: "runtime_config",
5550
5756
  label: "Runtime MCP config",
5551
5757
  level: "ok",
5552
- detail: `Runtime '${input2.runtime}' is configured through its own CLI or by hand (${input2.runtime === "other" ? "manual runtime" : "CLI-managed"}) and has no file-based config the connector owns \u2014 skipping the file check.`
5758
+ detail: `Runtime '${input2.runtime}' is configured through its own CLI or by hand (${input2.runtime === "other" ? "manual runtime" : "CLI-managed"}) and has no file-based config the connector owns \u2014 skipping the file check.${claudePairSuffix}`
5553
5759
  });
5554
5760
  } else if (configText === null) {
5555
5761
  checks.push({
@@ -5623,9 +5829,13 @@ async function runDoctor(input, deps = {}) {
5623
5829
  const supersededLevel = supersededLive.length === 0 ? "ok" : classificationUnreliable ? "advisory" : "failed";
5624
5830
  checks.push({
5625
5831
  id: "superseded_agents",
5626
- label: "Superseded agent credentials",
5832
+ // #3830: an advisory means the connector could not tell wired from
5833
+ // superseded, so its label must not assert "superseded" either. The
5834
+ // inventory `classification` stays as is (#3121 decision 5); the label
5835
+ // is what a person reads.
5836
+ label: supersededLevel === "advisory" ? "Other agent credentials (wiring not verifiable)" : "Superseded agent credentials",
5627
5837
  level: supersededLevel,
5628
- detail: supersededLevel === "failed" ? `${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.` : supersededLevel === "advisory" ? `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}. Runtime '${input2.runtime}' has no config file the connector can read, so which of these agents are wired cannot be verified from this machine: a live key here may be an agent you use deliberately, or one a host started before your latest setup is still spending as.` : `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}.`,
5838
+ detail: supersededLevel === "failed" ? `${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.` : supersededLevel === "advisory" ? `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}. Runtime '${input2.runtime}' has no config file the connector writes, and none it can read names these agents, so which of them are wired cannot be verified from this machine: a live key here may be an agent you use deliberately, or one a host started before your latest setup is still spending as.` : `${otherEntries.length} other credential dir(s) found \u2014 ${parts.join("; ")}.`,
5629
5839
  ...supersededLevel === "failed" ? {
5630
5840
  repair: `Use Remove agent\u2026 on the Haven agent page for ${supersededLive.join(", ")} (it ends each one\u2019s live budgets), then remove the old director(y/ies) under ~/.haven/agents. Connect never revokes or deletes for you.`
5631
5841
  } : supersededLevel === "advisory" ? {
@@ -5654,7 +5864,7 @@ async function runDoctor(input, deps = {}) {
5654
5864
  label: "MCP server names bound more than once",
5655
5865
  level: "advisory",
5656
5866
  detail: `${rebound.length} MCP server name${rebound.length === 1 ? "" : "s"} changed hands on this machine (locally recorded bindings; the backend's own record is the authority for the same backend): ${parts.join("; ")}. A saved session, script or document naming the server may still mean the earlier agent.`,
5657
- repair: `Retire the earlier director(y/ies) with ${RERUN} --unwire <dir> to release the name, or keep both and address them by --name.`
5867
+ repair: `Retire the earlier director(y/ies) with ${RERUN} --unwire <dir> to release the name, or keep both and address them by --name \u2014 --doctor --repair --name <slug> now selects the pair directly.`
5658
5868
  });
5659
5869
  }
5660
5870
  const parkedElsewhere = inventory.filter((entry) => entry.directory !== primaryDirectory && entry.rekeyPending).map((entry) => ({ entry, pending: entry.rekeyPending }));
@@ -5714,20 +5924,24 @@ async function runDoctor(input, deps = {}) {
5714
5924
  async function runRepair(input, deps = {}) {
5715
5925
  const homeDir = deps.homeDir ?? os.homedir();
5716
5926
  const messages = [];
5717
- const { directory, others, identityDirectories } = await discoverCredentialDirectory(homeDir, input.credentialsDir);
5718
- if (!input.credentialsDir && identityDirectories.length > 1) {
5927
+ const namedDir = await resolveNamedDirectory(input, homeDir);
5928
+ const { directory, others, identityDirectories } = await discoverCredentialDirectory(homeDir, namedDir);
5929
+ if (!namedDir && identityDirectories.length > 1) {
5719
5930
  const lines = await Promise.all(
5720
5931
  identityDirectories.map(async (dir) => {
5721
5932
  const identity2 = await readIdentity(dir);
5722
5933
  const who = identity2?.agent_id ? `agent ${identity2.agent_id}` : "agent (no agent_id recorded)";
5723
5934
  const retired = !identity2?.api_key || await pathExists3(path.join(dir, TOMBSTONE_FILENAME));
5724
- return retired ? ` \u2022 ${who} (${dir}): retired \u2014 not a repair target` : ` \u2022 ${who}: ${repairCommandFor(input.runtime, "", dir).replace(/^Run: /, "")}`;
5935
+ const sidecar = await readRuntimeSidecar(dir);
5936
+ const slug = sidecar?.server_name;
5937
+ const pair = slug ? `${serverNamesFor(slug).hosted} / ${serverNamesFor(slug).signer}` : "the bare haven / haven-signer pair";
5938
+ return retired ? ` \u2022 ${who} \u2014 ${pair} (${dir}): retired \u2014 not a repair target` : slug ? ` \u2022 ${who} \u2014 owns ${pair}: ${repairCommandNamedFor(input.runtime, slug).replace(/^Run: /, "")}` : ` \u2022 ${who} \u2014 owns ${pair}: ${repairCommandFor(input.runtime, "", dir).replace(/^Run: /, "")}`;
5725
5939
  })
5726
5940
  );
5727
5941
  return {
5728
5942
  ok: false,
5729
5943
  messages: [
5730
- `${identityDirectories.length} agent credential directories exist on this machine, and a repair rewrites the runtime config from ONE of them. Name the agent to repair with --credentials-dir \u2014 nothing was changed:`,
5944
+ `${identityDirectories.length} agent credential directories exist on this machine, and a repair rewrites the runtime config from ONE of them. Name the agent to repair with --name <slug> (its recorded pair) or --credentials-dir \u2014 nothing was changed:`,
5731
5945
  ...lines,
5732
5946
  "Not sure which agent is which? Compare the agent ids against the Haven agent page, or run --doctor first."
5733
5947
  ]
@@ -5785,13 +5999,62 @@ async function runRepair(input, deps = {}) {
5785
5999
  const existingSidecar = await readRuntimeSidecar(directory);
5786
6000
  const serverName = existingSidecar?.server_name;
5787
6001
  const signerPath = path.join(directory, "signer.json");
6002
+ const consent = await getLocalSignerConsentStatus(signerPath);
6003
+ const consentMismatch = !consent.acknowledged && (consent.reason === "ack_file_missing" || consent.reason === "ack_file_mismatch");
6004
+ if (consentMismatch && !input.ackLocalTools) {
6005
+ const wrapperPath = existingSidecar?.wrapper_path ?? path.join(directory, "bin", "haven-signer.mjs");
6006
+ return {
6007
+ ok: false,
6008
+ messages: [
6009
+ "Repair stopped before writing anything: the signer it installs would refuse to start until local-tools consent is re-acknowledged \u2014 the stored acknowledgement does not match the tool set of the signer this connector pins.",
6010
+ "The mismatch does not prove the currently running signer is broken; a newer tool set is enough to explain it.",
6011
+ `Re-consent without a setup token, produced by the installed signer: ${wrapperPath} --ack < /dev/null`,
6012
+ `Or re-run repair with --ack-local-tools to re-consent during the repair: ${repairCommandFor(input2.runtime, " --ack-local-tools", directory)}`
6013
+ ]
6014
+ };
6015
+ }
5788
6016
  const prepared = await prepareSignerRuntime(
5789
6017
  { credentialDirectory: directory, signerPath, homeDir, serverName },
5790
6018
  { runCommand: deps.runCommand, env: deps.env }
5791
6019
  );
5792
6020
  messages.push(...prepared.messages);
5793
6021
  const names = serverNamesFor(serverName);
5794
- messages.push(`Rewriting MCP entries ${names.hosted} / ${names.signer}${serverName ? ` (agent "${serverName}")` : " (unnamed pair)"} \u2014 no other pair is touched.`);
6022
+ const rewritesConfig = runtimeConfigPathFor(normalizeRuntimeName(input2.runtime) ?? input2.runtime, homeDir) !== null;
6023
+ if (rewritesConfig) {
6024
+ messages.push(`Rewriting MCP entries ${names.hosted} / ${names.signer}${serverName ? ` (agent "${serverName}")` : " (unnamed pair)"} \u2014 no other pair is touched.`);
6025
+ } else {
6026
+ messages.push(
6027
+ `Re-pointed ${prepared.wrapperPath} at signer ${signerPackageSpec()}. ` + (input2.runtime === "claude-code" ? `Claude Code's MCP entries were not touched \u2014 the entry that launches this wrapper uses the new signer after a restart. Confirm with \`claude mcp get ${names.signer}\`.` : `No MCP config was written \u2014 '${input2.runtime}' is configured through its own CLI or by hand; the entry that launches this wrapper uses the new signer after a restart.`)
6028
+ );
6029
+ }
6030
+ if (consentMismatch && input.ackLocalTools) {
6031
+ messages.push(`Re-acknowledging local-tools consent with the installed signer (${signerPackageSpec()}):`);
6032
+ try {
6033
+ const ack = await (deps.runSignerAck ?? runWrapperAck)(prepared.wrapperPath);
6034
+ for (const line of ack.stdout.split("\n")) {
6035
+ if (line.trim()) messages.push(` ${line}`);
6036
+ }
6037
+ } catch (err) {
6038
+ return {
6039
+ ok: false,
6040
+ messages: [
6041
+ ...messages,
6042
+ `The re-acknowledgement failed: ${err instanceof Error ? err.message : String(err)} \u2014 consent was NOT written; the runtime config was not rewritten.`
6043
+ ]
6044
+ };
6045
+ }
6046
+ const after = await getLocalSignerConsentStatus(signerPath);
6047
+ if (!after.acknowledged) {
6048
+ return {
6049
+ ok: false,
6050
+ messages: [
6051
+ ...messages,
6052
+ `Consent still does not match after the re-acknowledgement (${after.reason ?? after.error}). The runtime config was not rewritten.`
6053
+ ]
6054
+ };
6055
+ }
6056
+ messages.push("Local-tools consent acknowledged for the installed signer.");
6057
+ }
5795
6058
  const configResult = await writeRuntimeConfig({
5796
6059
  // Normalized for the WRITE too (#3145 review round 3): `writeRuntimeConfig`
5797
6060
  // switches on the id, and the raw alias fell to its "manual runtime" arm
@@ -6011,6 +6274,11 @@ function parseArgs(argv, env = process.env) {
6011
6274
  "--repair needs --runtime <runtime>: it rewrites that runtime's config, so the runtime is named explicitly rather than inherited from the setup record. --doctor alone resolves it from the record."
6012
6275
  );
6013
6276
  }
6277
+ if (options.ackLocalTools && !repair) {
6278
+ throw new Error(
6279
+ "--ack-local-tools does nothing with --doctor alone: the doctor is read-only. Re-consent during a repair (--repair --ack-local-tools), or token-free with the recorded wrapper: <agent dir>/bin/haven-signer.mjs --ack"
6280
+ );
6281
+ }
6014
6282
  return { options, help, json, doctor, repair, tombstone, rekey };
6015
6283
  }
6016
6284
  if (!options.setupToken) {
@@ -6062,7 +6330,9 @@ function helpText() {
6062
6330
  " agents can run side by side in one runtime. 1-32 lowercase letters, digits,",
6063
6331
  " single hyphens; immutable once wired. Without it the pair is named from the",
6064
6332
  " agent's display name; --bare wires the bare haven / haven-signer pair.",
6065
- " --ack-local-tools Write the one-time local Haven tools acknowledgement during setup.",
6333
+ " --ack-local-tools Write the local Haven tools acknowledgement: during setup, or (with --repair)",
6334
+ " re-acknowledge consent for the signer the repair installs \u2014 no setup token.",
6335
+ " With a bare --doctor it is refused: --doctor is read-only.",
6066
6336
  " --ack-signer Backward-compatible alias for --ack-local-tools.",
6067
6337
  " --local Advanced: install the fully-local Haven MCP (no hosted dependency).",
6068
6338
  " Only available for Claude Code and Codex. Default is hosted MCP + local signer.",
@@ -6071,10 +6341,13 @@ function helpText() {
6071
6341
  " signer runtime, hosted MCP, and a live signer handshake. Exits non-zero only on a failed check; an advisory (!) exits 0.",
6072
6342
  " --runtime is optional: without it the doctor checks the runtime the setup recorded,",
6073
6343
  " and reports a failure (not a pass) when no record names one.",
6344
+ " --name <slug> selects the agent directory by its recorded MCP pair.",
6074
6345
  " --repair Repair, then re-diagnose (implies --doctor): reinstall the pinned signer",
6075
6346
  " runtime, rewrite the wrapper and runtime config from stored credentials.",
6076
6347
  " Hosted topology only (refuses to touch a --local config). No keys, no token.",
6077
6348
  " Requires --runtime <runtime>: it rewrites that config, so the runtime is never inherited.",
6349
+ " --name <slug> selects the agent to repair by its recorded pair; --ack-local-tools",
6350
+ " re-consents the installed signer (the old token-free path: <agent dir>/bin/haven-signer.mjs --ack).",
6078
6351
  " --rekey Replace this agent's signing key (no token). Generates a fresh keypair HERE and",
6079
6352
  " prints its public address to paste into the Haven agent page. Nothing changes",
6080
6353
  " until you finish; the agent keeps working on its old key throughout.",
@@ -6239,11 +6512,12 @@ async function runCli(argv, io = {
6239
6512
  if (parsed.unwire) {
6240
6513
  const { unwireAgent: unwireAgent2 } = await Promise.resolve().then(() => (init_unwire(), unwire_exports));
6241
6514
  const { tombstonesDirForAgentDirectory: tombstonesDirForAgentDirectory2 } = await Promise.resolve().then(() => (init_tombstone(), tombstone_exports));
6515
+ const { resolveAgentDirectoryBySlug: resolveAgentDirectoryBySlug2 } = await Promise.resolve().then(() => (init_storage(), storage_exports));
6242
6516
  const { homedir: homedir11 } = await import('os');
6243
6517
  const { join: join13 } = await import('path');
6244
6518
  const homeDir = homedir11();
6245
6519
  const root = parsed.options.credentialsDir ?? join13(homeDir, ".haven", "agents");
6246
- const directory = parsed.unwireDir ?? (parsed.options.serverName ? join13(root, parsed.options.serverName) : root);
6520
+ const directory = parsed.unwireDir ?? (parsed.options.serverName ? await resolveAgentDirectoryBySlug2(parsed.options.serverName, { root, allowRetired: true }) : root);
6247
6521
  const ledgerDir = tombstonesDirForAgentDirectory2(directory, homeDir);
6248
6522
  try {
6249
6523
  const result = await unwireAgent2({
@@ -6445,14 +6719,16 @@ async function runCli(argv, io = {
6445
6719
  const { runDoctor: runDoctor2, runRepair: runRepair2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
6446
6720
  const runtime = parsed.options.runtime ?? "";
6447
6721
  const credentialsDir = parsed.options.credentialsDir;
6722
+ const serverName = parsed.options.serverName;
6723
+ const ackLocalTools = parsed.options.ackLocalTools === true;
6448
6724
  try {
6449
6725
  if (parsed.repair) {
6450
- const repair = await runRepair2({ runtime, credentialsDir });
6726
+ const repair = await runRepair2({ runtime, credentialsDir, serverName, ackLocalTools });
6451
6727
  for (const message of repair.messages) io.stderr(`${redactSecrets(message)}
6452
6728
  `);
6453
6729
  if (!repair.ok) return 1;
6454
6730
  }
6455
- const report = await runDoctor2({ runtime, credentialsDir });
6731
+ const report = await runDoctor2({ runtime, credentialsDir, serverName });
6456
6732
  if (parsed.json) {
6457
6733
  io.stdout(`${redactSecrets(JSON.stringify(report))}
6458
6734
  `);
@@ -6464,13 +6740,16 @@ async function runCli(argv, io = {
6464
6740
  `));
6465
6741
  }
6466
6742
  const otherAgents = report.agents.filter((agent) => agent.directory !== report.credentialDirectory);
6743
+ const wiringUnverifiable = report.checks.some(
6744
+ (check) => check.id === "superseded_agents" && check.level === "advisory"
6745
+ );
6467
6746
  if (otherAgents.length > 0) {
6468
6747
  io.stdout("\nOther agents on this machine:\n");
6469
6748
  for (const agent of otherAgents) {
6470
6749
  const name = agent.slug ? `${agent.slug} (${agent.agentId ?? "unknown"})` : agent.agentId ?? "unknown";
6471
6750
  const failed = agent.checks.filter((check) => check.level === "failed");
6472
6751
  const advised = agent.checks.filter((check) => check.level === "advisory");
6473
- const verdict = agent.classification === "wired" ? failed.length > 0 ? `wired, ${failed.length} check(s) FAILED` : advised.length > 0 ? `wired, ${advised.length} advisory finding(s)` : "wired, all checks passed" : agent.classification === "parked" ? "parked re-key only \u2014 no identity.json in this directory, but key material is still there" : agent.classification;
6752
+ const verdict = agent.classification === "wired" ? failed.length > 0 ? `wired, ${failed.length} check(s) FAILED` : advised.length > 0 ? `wired, ${advised.length} advisory finding(s)` : "wired, all checks passed" : agent.classification === "parked" ? "parked re-key only \u2014 no identity.json in this directory, but key material is still there" : agent.classification === "superseded" && wiringUnverifiable ? "not verified as wired \u2014 no config the connector can read names it" : agent.classification;
6474
6753
  io.stdout(redactSecrets(` ${failed.length > 0 ? "\u2717" : advised.length > 0 ? "!" : "\u2022"} ${name}: ${verdict}
6475
6754
  `));
6476
6755
  for (const check of [...failed, ...advised]) {
@@ -6561,6 +6840,7 @@ init_installed_clients();
6561
6840
  init_storage();
6562
6841
  init_runtime_manifest();
6563
6842
 
6843
+ exports.assertDirectoryRecordsSlug = assertDirectoryRecordsSlug;
6564
6844
  exports.completionOutcome = completionOutcome;
6565
6845
  exports.createConnectApiClient = createConnectApiClient;
6566
6846
  exports.defaultAgentDirectory = defaultAgentDirectory;
@@ -6578,6 +6858,7 @@ exports.parseArgs = parseArgs;
6578
6858
  exports.prepareSignerRuntime = prepareSignerRuntime;
6579
6859
  exports.promptForInstalledClient = promptForInstalledClient;
6580
6860
  exports.redactSecrets = redactSecrets;
6861
+ exports.resolveAgentDirectoryBySlug = resolveAgentDirectoryBySlug;
6581
6862
  exports.resolveRuntimeByInstalledClientPrompt = resolveRuntimeByInstalledClientPrompt;
6582
6863
  exports.resolveRuntimeSelection = resolveRuntimeSelection;
6583
6864
  exports.runCli = runCli;