@haven_ai/connect 0.1.34-alpha.0 → 0.1.36-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -55,6 +55,7 @@ function createConnectApiClient(baseUrl, fetchImpl = fetch) {
55
55
  runtime: input.runtime,
56
56
  connector_version: input.connectorVersion,
57
57
  mcp_server_name: input.mcpServerName,
58
+ run_mode: input.runMode,
58
59
  connector_context: input.connectorContext,
59
60
  install_capabilities: input.installCapabilities && {
60
61
  can_write_runtime_config: input.installCapabilities.canWriteRuntimeConfig,
@@ -90,7 +91,21 @@ function createConnectApiClient(baseUrl, fetchImpl = fetch) {
90
91
  restart_required: input.restartRequired,
91
92
  next_user_action: input.nextUserAction,
92
93
  error_code: input.errorCode ?? null,
93
- environment_label: input.environmentLabel
94
+ environment_label: input.environmentLabel,
95
+ // Three states on the wire, and ABSENT is a fourth (#2561 review).
96
+ // `?? null` alone collapsed the fourth into the third: a report that
97
+ // simply had nothing to say — the early config-written ping, which
98
+ // fires before the scan has started — asserted "the scan could not
99
+ // run" instead of leaving the key alone. Inert today, because the
100
+ // complete report overwrites it seconds later and nothing reads the
101
+ // row in between; a landmine the moment any caller relies on
102
+ // "absent = unchanged", which is what the backend's jsonb merge
103
+ // means and what this field's own contract says.
104
+ //
105
+ // So: a caller that passes the field says something (`null` included,
106
+ // deliberately, since "could not run" is a claim worth making); a
107
+ // caller that omits it says nothing.
108
+ ...input.supersededAgentIds !== void 0 ? { superseded_agent_ids: input.supersededAgentIds } : {}
94
109
  })
95
110
  });
96
111
  }
@@ -535,9 +550,9 @@ var init_runtime_manifest = __esm({
535
550
  mcpPackage: "@haven_ai/mcp",
536
551
  mcpVersion: mcp.MCP_VERSION,
537
552
  sdkPackage: "@haven_ai/sdk",
538
- sdkVersion: "0.1.34-alpha.0",
553
+ sdkVersion: "0.1.36-alpha.0",
539
554
  signerPackage: "@haven_ai/signer",
540
- signerVersion: "0.1.34-alpha.0",
555
+ signerVersion: "0.1.36-alpha.0",
541
556
  // Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
542
557
  // while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
543
558
  // so the guard that was supposed to enforce the floor waved Node v23 through
@@ -903,7 +918,7 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
903
918
  restartRequired: true,
904
919
  messages: unreadable ? [
905
920
  `Could not update ${configTargetLabel(input.runtime)}: ${err.message}.`,
906
- `Nothing was written to ${err.configPath}. Fix the JSON there (or move the file aside), then run \`${REPAIR_COMMAND_PREFIX} ${input.runtime}\` to write the Haven entries from the credentials already stored on this machine. Do not re-run the setup command: its token is already used.`
921
+ `Nothing was written to ${err.configPath}. Fix the JSON there (or move the file aside), then run \`${REPAIR_COMMAND_PREFIX} ${input.runtime}\` to write the Haven entries from the credentials already stored on this machine. Do not re-run the connector command: its setup token is already used.`
907
922
  ] : [`Could not update ${configTargetLabel(input.runtime)}: ${err instanceof Error ? err.message : String(err)}`],
908
923
  errorCode: unreadable ? "runtime_config_unreadable" : "runtime_config_write_failed"
909
924
  };
@@ -968,7 +983,7 @@ async function writeHermesConfig(input, deps) {
968
983
  restartRequired: true,
969
984
  messages: unreadable ? [
970
985
  `Could not update Hermes Agent config: ${err.message}.`,
971
- `Nothing was written to ${err.configPath}. Fix the YAML there (or move the file aside), then run \`${REPAIR_COMMAND_PREFIX} hermes\` to write the Haven entries from the credentials already stored on this machine. Do not re-run the setup command: its token is already used.`
986
+ `Nothing was written to ${err.configPath}. Fix the YAML there (or move the file aside), then run \`${REPAIR_COMMAND_PREFIX} hermes\` to write the Haven entries from the credentials already stored on this machine. Do not re-run the connector command: its setup token is already used.`
972
987
  ] : [recoveryIncomplete ? "Could not update Hermes Agent config. Recovery did not complete; inspect the Hermes configuration before retrying." : "Could not update Hermes Agent config. Existing configuration was left unchanged."],
973
988
  errorCode: unreadable ? "runtime_config_unreadable" : "runtime_config_write_failed"
974
989
  };
@@ -1358,7 +1373,7 @@ var init_config_writers = __esm({
1358
1373
  this.name = "InvalidCodexTomlError";
1359
1374
  }
1360
1375
  };
1361
- REPAIR_COMMAND_PREFIX = "npx @haven_ai/connect@alpha --doctor --repair --runtime";
1376
+ REPAIR_COMMAND_PREFIX = sdk.connectorRerunCommand("--doctor --repair --runtime");
1362
1377
  UnreadableRuntimeConfigError = class extends Error {
1363
1378
  configPath;
1364
1379
  constructor(configPath, detail) {
@@ -1608,32 +1623,129 @@ var init_probes = __esm({
1608
1623
  "src/probes.ts"() {
1609
1624
  }
1610
1625
  });
1626
+ function resolveRuntimeSpecOverride(env) {
1627
+ const override = {};
1628
+ for (const pkg of Object.keys(RUNTIME_SPEC_ENV)) {
1629
+ const variable = RUNTIME_SPEC_ENV[pkg];
1630
+ const raw = env[variable];
1631
+ if (raw === void 0) continue;
1632
+ override[pkg] = validateSpec(variable, raw);
1633
+ }
1634
+ return Object.keys(override).length > 0 ? override : void 0;
1635
+ }
1636
+ function validateSpec(variable, raw) {
1637
+ if (raw.length === 0 || raw.trim().length === 0) {
1638
+ throw new RuntimeSpecOverrideError(variable, "it is empty");
1639
+ }
1640
+ if (/\s/.test(raw)) {
1641
+ throw new RuntimeSpecOverrideError(variable, "it contains whitespace");
1642
+ }
1643
+ if (/[\u0000-\u001f\u007f]/.test(raw)) {
1644
+ throw new RuntimeSpecOverrideError(variable, "it contains a control character");
1645
+ }
1646
+ const meta = SHELL_METACHARACTERS.exec(raw);
1647
+ if (meta) {
1648
+ throw new RuntimeSpecOverrideError(variable, `it contains the shell metacharacter ${JSON.stringify(meta[0])}`);
1649
+ }
1650
+ return raw;
1651
+ }
1652
+ function runtimeSpecOverrideDirectoryKey(resolvedSpecs) {
1653
+ const digest = crypto.createHash("sha256").update(resolvedSpecs.join("\n")).digest("hex");
1654
+ return `override-${digest.slice(0, 12)}`;
1655
+ }
1656
+ function overrideApplies(override, packages) {
1657
+ return override !== void 0 && packages.some((pkg) => override[pkg] !== void 0);
1658
+ }
1659
+ function runtimeSpecOverrideNotice(runtimeLabel, override, pinned, runtimeDirectory) {
1660
+ const lines = [`RUNTIME SPEC OVERRIDE ACTIVE for the local Haven ${runtimeLabel} \u2014 this is NOT the pinned manifest.`];
1661
+ for (const pkg of Object.keys(RUNTIME_SPEC_ENV)) {
1662
+ const spec = override[pkg];
1663
+ if (spec === void 0) continue;
1664
+ const pin = pinned[pkg];
1665
+ lines.push(` ${RUNTIME_SPEC_ENV[pkg]}=${spec}${pin ? ` (instead of ${pin})` : ""}`);
1666
+ }
1667
+ lines.push(` Installing into ${runtimeDirectory} \u2014 the pinned runtime directory is untouched.`);
1668
+ lines.push(" Unset the variable(s) and re-run to return to the pinned manifest.");
1669
+ return lines;
1670
+ }
1671
+ function describeRuntimeSpecOverride(override) {
1672
+ return Object.keys(RUNTIME_SPEC_ENV).filter((pkg) => override[pkg] !== void 0).map((pkg) => `${RUNTIME_SPEC_ENV[pkg]}=${override[pkg]}`).join(" ");
1673
+ }
1674
+ var RUNTIME_SPEC_ENV, SHELL_METACHARACTERS, RuntimeSpecOverrideError;
1675
+ var init_runtime_spec_override = __esm({
1676
+ "src/runtime-spec-override.ts"() {
1677
+ RUNTIME_SPEC_ENV = {
1678
+ signer: "HAVEN_SIGNER_SPEC",
1679
+ sdk: "HAVEN_SDK_SPEC",
1680
+ mcp: "HAVEN_MCP_SPEC"
1681
+ };
1682
+ SHELL_METACHARACTERS = /[;&|<>$`'"()!#*?[\]{}\\]/;
1683
+ RuntimeSpecOverrideError = class extends Error {
1684
+ code = "runtime_spec_override_invalid";
1685
+ variable;
1686
+ constructor(variable, reason) {
1687
+ 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>).`);
1688
+ this.name = "RuntimeSpecOverrideError";
1689
+ this.variable = variable;
1690
+ }
1691
+ };
1692
+ }
1693
+ });
1611
1694
  async function prepareSignerRuntime(input, deps = {}) {
1612
1695
  const homeDir = input.homeDir ?? os.homedir();
1613
- const runtimeDirectory = path.resolve(homeDir, ".haven", "signer-runtime", MCP_RUNTIME_MANIFEST.signerVersion);
1696
+ const override = resolveSignerRuntimeOverride(deps.env ?? process.env);
1697
+ const signerSpec = override?.signer ?? signerPackageSpec();
1698
+ const sdkSpec = override?.sdk ?? sdkPackageSpec();
1699
+ const resolvedSpecs = [signerSpec, sdkSpec];
1700
+ const overrideRecord = override ? { specs: override, resolved_specs: resolvedSpecs, directory_key: runtimeSpecOverrideDirectoryKey(resolvedSpecs) } : void 0;
1701
+ const runtimeDirectory = path.resolve(
1702
+ homeDir,
1703
+ ".haven",
1704
+ "signer-runtime",
1705
+ overrideRecord ? overrideRecord.directory_key : MCP_RUNTIME_MANIFEST.signerVersion
1706
+ );
1614
1707
  const npmCacheDirectory = path.resolve(homeDir, ".haven", "npm-cache");
1615
1708
  const cliPath = path.join(runtimeDirectory, "node_modules", "@haven_ai", "signer", "dist", "cli.js");
1616
1709
  const messages = [];
1710
+ if (override) {
1711
+ messages.push(...runtimeSpecOverrideNotice(
1712
+ "signer runtime",
1713
+ override,
1714
+ { signer: signerPackageSpec(), sdk: sdkPackageSpec() },
1715
+ runtimeDirectory
1716
+ ));
1717
+ }
1617
1718
  await promises.mkdir(runtimeDirectory, { recursive: true, mode: 448 });
1618
1719
  await promises.chmod(runtimeDirectory, 448).catch(() => void 0);
1619
1720
  await promises.mkdir(npmCacheDirectory, { recursive: true, mode: 448 });
1620
1721
  await promises.chmod(npmCacheDirectory, 448).catch(() => void 0);
1621
- if (await installedRuntimeMatches(runtimeDirectory, cliPath)) {
1722
+ if (override) {
1723
+ await installRuntimePackages(runtimeDirectory, npmCacheDirectory, resolvedSpecs, deps);
1724
+ messages.push(`Installed local Haven signer runtime from override (${resolvedSpecs.join(" ")}).`);
1725
+ } else if (await installedRuntimeMatches(runtimeDirectory, cliPath)) {
1622
1726
  messages.push(`Using existing local Haven signer runtime ${signerPackageSpec()}.`);
1623
1727
  } else {
1624
- await installRuntimePackages(runtimeDirectory, npmCacheDirectory, deps);
1728
+ await installRuntimePackages(runtimeDirectory, npmCacheDirectory, resolvedSpecs, deps);
1625
1729
  messages.push(`Installed local Haven signer runtime ${signerPackageSpec()}.`);
1626
1730
  }
1627
1731
  await assertFileExists(cliPath, "local Haven signer CLI");
1732
+ const installedVersions = override ? await readInstalledVersions(runtimeDirectory) : void 0;
1628
1733
  const wrapperPath = path.join(input.credentialDirectory, "bin", "haven-signer.mjs");
1629
- await writeWrapper({ wrapperPath, cliPath, signerPath: input.signerPath });
1734
+ await writeWrapper({
1735
+ wrapperPath,
1736
+ cliPath,
1737
+ signerPath: input.signerPath,
1738
+ overrideComment: override ? describeRuntimeSpecOverride(override) : void 0
1739
+ });
1630
1740
  await writeRuntimeSidecar({
1631
1741
  path: path.join(input.credentialDirectory, "signer-runtime.json"),
1632
1742
  wrapperPath,
1633
1743
  runtimeDirectory,
1634
1744
  npmCacheDirectory,
1635
1745
  cliPath,
1636
- serverName: input.serverName
1746
+ serverName: input.serverName,
1747
+ override: overrideRecord,
1748
+ installedVersions
1637
1749
  });
1638
1750
  messages.push(`Prepared stable local Haven signer wrapper: ${wrapperPath}`);
1639
1751
  return {
@@ -1643,10 +1755,17 @@ async function prepareSignerRuntime(input, deps = {}) {
1643
1755
  runtimeDirectory,
1644
1756
  npmCacheDirectory,
1645
1757
  cliPath,
1646
- messages
1758
+ messages,
1759
+ ...overrideRecord ? { runtimeSpecOverride: overrideRecord } : {}
1647
1760
  };
1648
1761
  }
1649
- async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, deps) {
1762
+ function resolveSignerRuntimeOverride(env) {
1763
+ const override = resolveRuntimeSpecOverride(env);
1764
+ if (!overrideApplies(override, ["signer", "sdk"])) return void 0;
1765
+ const { signer, sdk } = override;
1766
+ return { ...signer !== void 0 ? { signer } : {}, ...sdk !== void 0 ? { sdk } : {} };
1767
+ }
1768
+ async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, packageSpecs, deps) {
1650
1769
  const { runCommand, onProgress } = deps;
1651
1770
  const baseArgs = [
1652
1771
  "install",
@@ -1656,8 +1775,7 @@ async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, deps)
1656
1775
  "--no-fund",
1657
1776
  "--omit=dev",
1658
1777
  "--prefer-offline",
1659
- signerPackageSpec(),
1660
- sdkPackageSpec()
1778
+ ...packageSpecs
1661
1779
  ];
1662
1780
  const run = async (args) => {
1663
1781
  const startedAt = Date.now();
@@ -1680,23 +1798,33 @@ async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, deps)
1680
1798
  await run([...baseArgs, "--cache", npmCacheDirectory]);
1681
1799
  } catch (err) {
1682
1800
  throw new Error(
1683
- `Could not install local Haven signer runtime ${signerPackageSpec()}: ${err instanceof Error ? err.message : String(err)}`
1801
+ `Could not install local Haven signer runtime ${packageSpecs.join(" ")}: ${err instanceof Error ? err.message : String(err)}`
1684
1802
  );
1685
1803
  }
1686
1804
  }
1687
1805
  }
1688
1806
  async function installedRuntimeMatches(runtimeDirectory, cliPath) {
1807
+ return installedRuntimeMatchesVersions(runtimeDirectory, cliPath, {
1808
+ signerVersion: MCP_RUNTIME_MANIFEST.signerVersion,
1809
+ sdkVersion: MCP_RUNTIME_MANIFEST.sdkVersion
1810
+ });
1811
+ }
1812
+ async function installedRuntimeMatchesVersions(runtimeDirectory, cliPath, expected) {
1689
1813
  try {
1690
1814
  await assertFileExists(cliPath, "local Haven signer CLI");
1691
- const [signerPackage, sdkPackage] = await Promise.all([
1692
- readPackageJson(path.join(runtimeDirectory, "node_modules", "@haven_ai", "signer", "package.json")),
1693
- readPackageJson(path.join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
1694
- ]);
1695
- return signerPackage.version === MCP_RUNTIME_MANIFEST.signerVersion && sdkPackage.version === MCP_RUNTIME_MANIFEST.sdkVersion;
1815
+ const installed = await readInstalledVersions(runtimeDirectory);
1816
+ return installed.signerVersion === expected.signerVersion && installed.sdkVersion === expected.sdkVersion;
1696
1817
  } catch {
1697
1818
  return false;
1698
1819
  }
1699
1820
  }
1821
+ async function readInstalledVersions(runtimeDirectory) {
1822
+ const [signerPackage, sdkPackage] = await Promise.all([
1823
+ readPackageJson(path.join(runtimeDirectory, "node_modules", "@haven_ai", "signer", "package.json")),
1824
+ readPackageJson(path.join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
1825
+ ]);
1826
+ return { signerVersion: signerPackage.version ?? "", sdkVersion: sdkPackage.version ?? "" };
1827
+ }
1700
1828
  async function readPackageJson(path) {
1701
1829
  return JSON.parse(await promises.readFile(path, "utf8"));
1702
1830
  }
@@ -1705,6 +1833,7 @@ async function writeWrapper(input) {
1705
1833
  await promises.chmod(path.dirname(input.wrapperPath), 448).catch(() => void 0);
1706
1834
  const source = [
1707
1835
  "#!/usr/bin/env node",
1836
+ ...input.overrideComment ? [`// HAVEN RUNTIME SPEC OVERRIDE (#2424): ${input.overrideComment} \u2014 this wrapper launches a NON-pinned signer build.`] : [],
1708
1837
  "import { spawn } from 'node:child_process'",
1709
1838
  "",
1710
1839
  `const cliPath = ${JSON.stringify(input.cliPath)}`,
@@ -1736,13 +1865,14 @@ async function writeRuntimeSidecar(input) {
1736
1865
  const value = {
1737
1866
  ...input.serverName ? { server_name: input.serverName } : {},
1738
1867
  signer_package: MCP_RUNTIME_MANIFEST.signerPackage,
1739
- signer_version: MCP_RUNTIME_MANIFEST.signerVersion,
1868
+ signer_version: input.installedVersions?.signerVersion ?? MCP_RUNTIME_MANIFEST.signerVersion,
1740
1869
  sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
1741
- sdk_version: MCP_RUNTIME_MANIFEST.sdkVersion,
1870
+ sdk_version: input.installedVersions?.sdkVersion ?? MCP_RUNTIME_MANIFEST.sdkVersion,
1742
1871
  wrapper_path: input.wrapperPath,
1743
1872
  runtime_directory: input.runtimeDirectory,
1744
1873
  npm_cache_directory: input.npmCacheDirectory,
1745
- cli_path: input.cliPath
1874
+ cli_path: input.cliPath,
1875
+ ...input.override ? { runtime_spec_override: input.override } : {}
1746
1876
  };
1747
1877
  await promises.writeFile(input.path, `${JSON.stringify(value, null, 2)}
1748
1878
  `, { mode: 384 });
@@ -1759,6 +1889,7 @@ var execFileAsync, SIGNER_INSTALL_TIMEOUT_MS, SIGNER_INSTALL_HEARTBEAT_MS;
1759
1889
  var init_signer_runtime = __esm({
1760
1890
  "src/signer-runtime.ts"() {
1761
1891
  init_runtime_manifest();
1892
+ init_runtime_spec_override();
1762
1893
  execFileAsync = util.promisify(child_process.execFile);
1763
1894
  SIGNER_INSTALL_TIMEOUT_MS = 6e5;
1764
1895
  SIGNER_INSTALL_HEARTBEAT_MS = 15e3;
@@ -1767,27 +1898,50 @@ var init_signer_runtime = __esm({
1767
1898
  async function prepareLocalMcpRuntime(input, deps = {}) {
1768
1899
  assertSupportedNodeVersion(input.nodeVersion);
1769
1900
  const homeDir = input.homeDir ?? os.homedir();
1770
- const runtimeDirectory = path.resolve(homeDir, ".haven", "mcp-runtime", MCP_RUNTIME_MANIFEST.mcpVersion);
1901
+ const override = resolveLocalMcpRuntimeOverride(deps.env ?? process.env);
1902
+ const mcpSpec = override?.mcp ?? mcpPackageSpec();
1903
+ const sdkSpec = override?.sdk ?? sdkPackageSpec();
1904
+ const resolvedSpecs = [mcpSpec, sdkSpec];
1905
+ const overrideRecord = override ? { specs: override, resolved_specs: resolvedSpecs, directory_key: runtimeSpecOverrideDirectoryKey(resolvedSpecs) } : void 0;
1906
+ const runtimeDirectory = path.resolve(
1907
+ homeDir,
1908
+ ".haven",
1909
+ "mcp-runtime",
1910
+ overrideRecord ? overrideRecord.directory_key : MCP_RUNTIME_MANIFEST.mcpVersion
1911
+ );
1771
1912
  const npmCacheDirectory = path.resolve(homeDir, ".haven", "npm-cache");
1772
1913
  const cliPath = path.join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "dist", "cli.js");
1773
1914
  const messages = [];
1915
+ if (override) {
1916
+ messages.push(...runtimeSpecOverrideNotice(
1917
+ "MCP runtime",
1918
+ override,
1919
+ { mcp: mcpPackageSpec(), sdk: sdkPackageSpec() },
1920
+ runtimeDirectory
1921
+ ));
1922
+ }
1774
1923
  await promises.mkdir(runtimeDirectory, { recursive: true, mode: 448 });
1775
1924
  await promises.chmod(runtimeDirectory, 448).catch(() => void 0);
1776
1925
  await promises.mkdir(npmCacheDirectory, { recursive: true, mode: 448 });
1777
1926
  await promises.chmod(npmCacheDirectory, 448).catch(() => void 0);
1778
- if (await installedRuntimeMatches2(runtimeDirectory, cliPath)) {
1927
+ if (override) {
1928
+ await installRuntimePackages2(runtimeDirectory, npmCacheDirectory, resolvedSpecs, deps);
1929
+ messages.push(`Installed local Haven MCP runtime from override (${resolvedSpecs.join(" ")}).`);
1930
+ } else if (await installedRuntimeMatches2(runtimeDirectory, cliPath)) {
1779
1931
  messages.push(`Using existing local Haven MCP runtime ${mcpPackageSpec()}.`);
1780
1932
  } else {
1781
- await installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps);
1933
+ await installRuntimePackages2(runtimeDirectory, npmCacheDirectory, resolvedSpecs, deps);
1782
1934
  messages.push(`Installed local Haven MCP runtime ${mcpPackageSpec()}.`);
1783
1935
  }
1784
1936
  await assertFileExists2(cliPath, "local Haven MCP CLI");
1937
+ const installedVersions = override ? await readInstalledVersions2(runtimeDirectory) : void 0;
1785
1938
  const wrapperPath = path.join(input.credentialDirectory, "bin", "haven-mcp");
1786
1939
  await writeWrapper2({
1787
1940
  wrapperPath,
1788
1941
  cliPath,
1789
1942
  identityPath: input.identityPath,
1790
- signerPath: input.signerPath
1943
+ signerPath: input.signerPath,
1944
+ overrideComment: override ? describeRuntimeSpecOverride(override) : void 0
1791
1945
  });
1792
1946
  await writeRuntimeSidecar2({
1793
1947
  path: path.join(input.credentialDirectory, "mcp-runtime.json"),
@@ -1795,7 +1949,9 @@ async function prepareLocalMcpRuntime(input, deps = {}) {
1795
1949
  runtimeDirectory,
1796
1950
  npmCacheDirectory,
1797
1951
  cliPath,
1798
- serverName: input.serverName
1952
+ serverName: input.serverName,
1953
+ override: overrideRecord,
1954
+ installedVersions
1799
1955
  });
1800
1956
  messages.push(`Prepared stable local Haven MCP wrapper: ${wrapperPath}`);
1801
1957
  return {
@@ -1805,7 +1961,8 @@ async function prepareLocalMcpRuntime(input, deps = {}) {
1805
1961
  runtimeDirectory,
1806
1962
  npmCacheDirectory,
1807
1963
  cliPath,
1808
- messages
1964
+ messages,
1965
+ ...overrideRecord ? { runtimeSpecOverride: overrideRecord } : {}
1809
1966
  };
1810
1967
  }
1811
1968
  function assertSupportedNodeVersion(nodeVersion = process.versions.node, minimumNodeVersion = MCP_RUNTIME_MANIFEST.minimumNodeVersion, subject = "Haven setup") {
@@ -1813,7 +1970,13 @@ function assertSupportedNodeVersion(nodeVersion = process.versions.node, minimum
1813
1970
  throw new UnsupportedNodeVersionError(nodeVersion, minimumNodeVersion, subject);
1814
1971
  }
1815
1972
  }
1816
- async function installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps) {
1973
+ function resolveLocalMcpRuntimeOverride(env) {
1974
+ const override = resolveRuntimeSpecOverride(env);
1975
+ if (!overrideApplies(override, ["mcp", "sdk"])) return void 0;
1976
+ const { mcp, sdk } = override;
1977
+ return { ...mcp !== void 0 ? { mcp } : {}, ...sdk !== void 0 ? { sdk } : {} };
1978
+ }
1979
+ async function installRuntimePackages2(runtimeDirectory, npmCacheDirectory, packageSpecs, deps) {
1817
1980
  const { runCommand, onProgress } = deps;
1818
1981
  const baseArgs = [
1819
1982
  "install",
@@ -1823,8 +1986,7 @@ async function installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps
1823
1986
  "--no-fund",
1824
1987
  "--omit=dev",
1825
1988
  "--prefer-offline",
1826
- mcpPackageSpec(),
1827
- sdkPackageSpec()
1989
+ ...packageSpecs
1828
1990
  ];
1829
1991
  const run = async (args) => {
1830
1992
  const startedAt = Date.now();
@@ -1846,22 +2008,26 @@ async function installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps
1846
2008
  try {
1847
2009
  await run([...baseArgs, "--cache", npmCacheDirectory]);
1848
2010
  } catch (err) {
1849
- throw new Error(`Could not install local Haven MCP runtime ${mcpPackageSpec()}: ${err instanceof Error ? err.message : String(err)}`);
2011
+ throw new Error(`Could not install local Haven MCP runtime ${packageSpecs.join(" ")}: ${err instanceof Error ? err.message : String(err)}`);
1850
2012
  }
1851
2013
  }
1852
2014
  }
1853
2015
  async function installedRuntimeMatches2(runtimeDirectory, cliPath) {
1854
2016
  try {
1855
2017
  await assertFileExists2(cliPath, "local Haven MCP CLI");
1856
- const [mcpPackage, sdkPackage] = await Promise.all([
1857
- readPackageJson2(path.join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "package.json")),
1858
- readPackageJson2(path.join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
1859
- ]);
1860
- return mcpPackage.version === MCP_RUNTIME_MANIFEST.mcpVersion && sdkPackage.version === MCP_RUNTIME_MANIFEST.sdkVersion;
2018
+ const installed = await readInstalledVersions2(runtimeDirectory);
2019
+ return installed.mcpVersion === MCP_RUNTIME_MANIFEST.mcpVersion && installed.sdkVersion === MCP_RUNTIME_MANIFEST.sdkVersion;
1861
2020
  } catch {
1862
2021
  return false;
1863
2022
  }
1864
2023
  }
2024
+ async function readInstalledVersions2(runtimeDirectory) {
2025
+ const [mcpPackage, sdkPackage] = await Promise.all([
2026
+ readPackageJson2(path.join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "package.json")),
2027
+ readPackageJson2(path.join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
2028
+ ]);
2029
+ return { mcpVersion: mcpPackage.version ?? "", sdkVersion: sdkPackage.version ?? "" };
2030
+ }
1865
2031
  async function readPackageJson2(path) {
1866
2032
  return JSON.parse(await promises.readFile(path, "utf8"));
1867
2033
  }
@@ -1870,6 +2036,7 @@ async function writeWrapper2(input) {
1870
2036
  await promises.chmod(path.dirname(input.wrapperPath), 448).catch(() => void 0);
1871
2037
  const source = [
1872
2038
  "#!/usr/bin/env node",
2039
+ ...input.overrideComment ? [`// HAVEN RUNTIME SPEC OVERRIDE (#2424): ${input.overrideComment} \u2014 this wrapper launches a NON-pinned MCP build.`] : [],
1873
2040
  "import { spawn } from 'node:child_process'",
1874
2041
  "",
1875
2042
  `const cliPath = ${JSON.stringify(input.cliPath)}`,
@@ -1893,14 +2060,15 @@ async function writeRuntimeSidecar2(input) {
1893
2060
  const value = {
1894
2061
  ...input.serverName ? { server_name: input.serverName } : {},
1895
2062
  mcp_package: MCP_RUNTIME_MANIFEST.mcpPackage,
1896
- mcp_version: MCP_RUNTIME_MANIFEST.mcpVersion,
2063
+ mcp_version: input.installedVersions?.mcpVersion ?? MCP_RUNTIME_MANIFEST.mcpVersion,
1897
2064
  sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
1898
- sdk_version: MCP_RUNTIME_MANIFEST.sdkVersion,
2065
+ sdk_version: input.installedVersions?.sdkVersion ?? MCP_RUNTIME_MANIFEST.sdkVersion,
1899
2066
  minimum_node_version: MCP_RUNTIME_MANIFEST.minimumNodeVersion,
1900
2067
  wrapper_path: input.wrapperPath,
1901
2068
  runtime_directory: input.runtimeDirectory,
1902
2069
  npm_cache_directory: input.npmCacheDirectory,
1903
- cli_path: input.cliPath
2070
+ cli_path: input.cliPath,
2071
+ ...input.override ? { runtime_spec_override: input.override } : {}
1904
2072
  };
1905
2073
  await promises.writeFile(input.path, `${JSON.stringify(value, null, 2)}
1906
2074
  `, { mode: 384 });
@@ -1918,6 +2086,7 @@ var init_local_mcp_runtime = __esm({
1918
2086
  "src/local-mcp-runtime.ts"() {
1919
2087
  init_signer_runtime();
1920
2088
  init_runtime_manifest();
2089
+ init_runtime_spec_override();
1921
2090
  execFileAsync2 = util.promisify(child_process.execFile);
1922
2091
  UnsupportedNodeVersionError = class extends Error {
1923
2092
  code = "local_mcp_unsupported_node_version";
@@ -2418,7 +2587,7 @@ async function installRuntime(input, deps = {}) {
2418
2587
  localMcpConfigured: false,
2419
2588
  probeResult: "signer_runtime_install_failed",
2420
2589
  restartRequired: false,
2421
- 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",
2590
+ 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: ${sdk.connectorRerunCommand()}`,
2422
2591
  errorCode: "signer_runtime_install_failed",
2423
2592
  configTarget: profile.label,
2424
2593
  signerAcknowledged: signerConsent?.acknowledged,
@@ -2429,7 +2598,7 @@ async function installRuntime(input, deps = {}) {
2429
2598
  ...consentMessages,
2430
2599
  `Could not pre-install the local Haven signer: ${err instanceof Error ? err.message : String(err)}`,
2431
2600
  "No runtime configuration was written (fail-closed): a config pointing at an uninstalled signer looks wired but cannot start.",
2432
- "Re-run `npx @haven_ai/connect@alpha` to retry the setup."
2601
+ `Re-run \`${sdk.connectorRerunCommand()}\` to retry the setup.`
2433
2602
  ]
2434
2603
  };
2435
2604
  }
@@ -2492,7 +2661,7 @@ async function installRuntime(input, deps = {}) {
2492
2661
  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."] : [];
2493
2662
  const signerProbeMessages = signerProbe ? signerProbe.status === "ok" ? ["Verified local Haven signer with a stdio handshake."] : [
2494
2663
  `Local Haven signer handshake failed: ${signerProbe.status}.`,
2495
- "Re-run `npx @haven_ai/connect@alpha` to repair the signer setup."
2664
+ `Re-run \`${sdk.connectorRerunCommand()}\` to repair the signer setup.`
2496
2665
  ] : [];
2497
2666
  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."] : [];
2498
2667
  const skillInstall = !configResult.errorCode ? await installSkillForRuntime(runtime, { homeDir: deps.homeDir, env: deps.env }) : void 0;
@@ -2563,7 +2732,7 @@ async function configureClaudeCode(deps, localMcpCommand, serverName) {
2563
2732
  restartRequired: true,
2564
2733
  messages: [
2565
2734
  `Could not update Claude Code MCP config: ${err instanceof Error ? err.message : String(err)}`,
2566
- "Install Claude Code or rerun the Haven setup command inside a Claude Code terminal."
2735
+ "Install Claude Code or rerun the Haven connector command inside a Claude Code terminal."
2567
2736
  ],
2568
2737
  errorCode: "claude_code_config_failed"
2569
2738
  };
@@ -2630,7 +2799,7 @@ async function configureClaudeCodeHosted(deps, input, signerCommand) {
2630
2799
  restartRequired: true,
2631
2800
  messages: [
2632
2801
  `Could not update Claude Code MCP config: ${err instanceof Error ? err.message : String(err)}`,
2633
- "Install Claude Code or rerun the Haven setup command inside a Claude Code terminal."
2802
+ "Install Claude Code or rerun the Haven connector command inside a Claude Code terminal."
2634
2803
  ],
2635
2804
  errorCode: "claude_code_config_failed"
2636
2805
  };
@@ -2704,7 +2873,7 @@ function supportsLocalMcp(runtime) {
2704
2873
  return runtime === "codex-cli" || runtime === "codex-desktop" || runtime === "claude-code";
2705
2874
  }
2706
2875
  async function prepareRuntimeForLocalMcp(input, deps) {
2707
- const prepare = deps.prepareLocalMcpRuntime ?? ((runtimeInput) => prepareLocalMcpRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress }));
2876
+ const prepare = deps.prepareLocalMcpRuntime ?? ((runtimeInput) => prepareLocalMcpRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress, env: deps.env }));
2708
2877
  return prepare({
2709
2878
  credentialDirectory: input.credentialDirectory,
2710
2879
  identityPath: input.identityPath,
@@ -2719,7 +2888,7 @@ async function prepareSignerForRuntime(input, deps) {
2719
2888
  // install heartbeat was dead code in production and the console still
2720
2889
  // went silent for the whole cold install — the exact symptom the issue
2721
2890
  // set out to remove, at a longer timeout.
2722
- prepareSignerRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress })
2891
+ prepareSignerRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress, env: deps.env })
2723
2892
  ));
2724
2893
  return prepare({
2725
2894
  credentialDirectory: input.credentialDirectory,
@@ -2759,14 +2928,6 @@ var init_runtime_install = __esm({
2759
2928
  }
2760
2929
  });
2761
2930
 
2762
- // src/rekey-messages.ts
2763
- var REKEY_FINISH_NEEDS_API_KEY;
2764
- var init_rekey_messages = __esm({
2765
- "src/rekey-messages.ts"() {
2766
- REKEY_FINISH_NEEDS_API_KEY = "--rekey-finish needs --api-key <key> \u2014 the one the Haven agent page showed once.";
2767
- }
2768
- });
2769
-
2770
2931
  // src/tombstone.ts
2771
2932
  var tombstone_exports = {};
2772
2933
  __export(tombstone_exports, {
@@ -2818,7 +2979,7 @@ function tombstoneScript(info) {
2818
2979
  "one of them: each holds the snapshot from its own start time, so after",
2819
2980
  "a chain of recreations each can be parked on a DIFFERENT old agent.",
2820
2981
  "",
2821
- "Then verify with: npx @haven_ai/connect@alpha --doctor --runtime <runtime>"
2982
+ `Then verify with: ${sdk.connectorRerunCommand("--doctor --runtime <runtime>")}`
2822
2983
  ];
2823
2984
  return [
2824
2985
  "#!/usr/bin/env node",
@@ -2884,6 +3045,9 @@ var init_tombstone = __esm({
2884
3045
  // src/unwire.ts
2885
3046
  var unwire_exports = {};
2886
3047
  __export(unwire_exports, {
3048
+ readIdentityFile: () => readIdentityFile,
3049
+ teardownLocalKeyMaterial: () => teardownLocalKeyMaterial,
3050
+ tombstoneDirectoryIfAbsent: () => tombstoneDirectoryIfAbsent,
2887
3051
  unwireAgent: () => unwireAgent
2888
3052
  });
2889
3053
  function identityAt(directory) {
@@ -2923,18 +3087,13 @@ async function unwireAgent(input) {
2923
3087
  const slug = input.slug ?? sidecar?.server_name;
2924
3088
  const names = serverNamesFor(slug);
2925
3089
  const runtimes = [];
2926
- let tombstoned = false;
2927
- const tombstonePath = path.join(input.directory, TOMBSTONE_FILENAME);
2928
- if (await readOptionalText(tombstonePath) === null) {
2929
- await writeAgentTombstone({
2930
- directory: input.directory,
2931
- agentId,
2932
- reason: input.reason ?? "unwired via --unwire",
2933
- replacedBy: input.replacedBy,
2934
- tombstonesDir: input.tombstonesDir
2935
- });
2936
- tombstoned = true;
2937
- }
3090
+ const tombstoned = await tombstoneDirectoryIfAbsent({
3091
+ directory: input.directory,
3092
+ agentId,
3093
+ reason: input.reason ?? "unwired via --unwire",
3094
+ replacedBy: input.replacedBy,
3095
+ tombstonesDir: input.tombstonesDir
3096
+ });
2938
3097
  for (const model of RUNTIMES) {
2939
3098
  const path = runtimeConfigPathFor(model.runtime, homeDir);
2940
3099
  if (path === null) continue;
@@ -3019,16 +3178,27 @@ async function unwireAgent(input) {
3019
3178
  }
3020
3179
  }
3021
3180
  }
3181
+ await teardownLocalKeyMaterial(input.directory, identity);
3182
+ return { directory: input.directory, agentId, slug, tombstoned, runtimes };
3183
+ }
3184
+ async function tombstoneDirectoryIfAbsent(input) {
3185
+ if (await readOptionalText(path.join(input.directory, TOMBSTONE_FILENAME)) !== null) return false;
3186
+ await writeAgentTombstone(input);
3187
+ return true;
3188
+ }
3189
+ async function teardownLocalKeyMaterial(directory, identity) {
3022
3190
  await Promise.all([
3023
- promises.rm(path.join(input.directory, "signer.json"), { force: true }),
3024
- promises.rm(path.join(input.directory, REKEY_PENDING_FILENAME), { force: true })
3191
+ promises.rm(path.join(directory, "signer.json"), { force: true }),
3192
+ promises.rm(path.join(directory, REKEY_PENDING_FILENAME), { force: true })
3025
3193
  ]);
3026
3194
  if (identity && identity.api_key !== void 0) {
3027
3195
  const { api_key: _dropped, ...rest } = identity;
3028
- await promises.writeFile(path.join(input.directory, "identity.json"), `${JSON.stringify(rest, null, 2)}
3196
+ await promises.writeFile(path.join(directory, "identity.json"), `${JSON.stringify(rest, null, 2)}
3029
3197
  `, { mode: 384 });
3030
3198
  }
3031
- return { directory: input.directory, agentId, slug, tombstoned, runtimes };
3199
+ }
3200
+ async function readIdentityFile(directory) {
3201
+ return identityAt(directory);
3032
3202
  }
3033
3203
  var RUNTIMES;
3034
3204
  var init_unwire = __esm({
@@ -3049,6 +3219,14 @@ var init_unwire = __esm({
3049
3219
  }
3050
3220
  });
3051
3221
 
3222
+ // src/rekey-messages.ts
3223
+ var REKEY_FINISH_NEEDS_API_KEY;
3224
+ var init_rekey_messages = __esm({
3225
+ "src/rekey-messages.ts"() {
3226
+ REKEY_FINISH_NEEDS_API_KEY = "--rekey-finish needs --api-key <key> \u2014 the one the Haven agent page showed once.";
3227
+ }
3228
+ });
3229
+
3052
3230
  // src/rekey.ts
3053
3231
  var rekey_exports = {};
3054
3232
  __export(rekey_exports, {
@@ -3076,7 +3254,7 @@ async function startRekey(options, deps = {}) {
3076
3254
  expires_at: expiresAt
3077
3255
  });
3078
3256
  const finishCommand = [
3079
- "npx @haven_ai/connect@alpha --rekey-finish",
3257
+ sdk.connectorRerunCommand("--rekey-finish"),
3080
3258
  options.serverName ? `--name ${options.serverName}` : void 0,
3081
3259
  "--api-key <the key the dashboard showed you>",
3082
3260
  options.runtime ? `--runtime ${options.runtime}` : "--runtime <your runtime>"
@@ -3167,7 +3345,7 @@ async function finishRekey(options, deps = {}) {
3167
3345
  signerPath: `${stored.directory}/signer.json`,
3168
3346
  homeDir: options.homeDir
3169
3347
  },
3170
- { runCommand: deps.runCommand }
3348
+ { runCommand: deps.runCommand, env: deps.env }
3171
3349
  );
3172
3350
  const result = await (deps.writeConfig ?? writeHostedRuntimeConfig)(
3173
3351
  { runCommand: deps.runCommand, homeDir: options.homeDir },
@@ -3183,6 +3361,11 @@ async function finishRekey(options, deps = {}) {
3183
3361
  { command: prepared.command, args: prepared.args }
3184
3362
  );
3185
3363
  configRewritten = result.hostedConfigured;
3364
+ if (prepared.runtimeSpecOverride) {
3365
+ messages.push(
3366
+ ` Signer runtime: RUNTIME SPEC OVERRIDE ACTIVE \u2014 ${describeRuntimeSpecOverride(prepared.runtimeSpecOverride.specs)} (NOT the pinned manifest; installed into ${prepared.runtimeDirectory})`
3367
+ );
3368
+ }
3186
3369
  messages.push(` Config: ${result.target}`);
3187
3370
  messages.push(...result.messages.map((line) => ` ${line}`));
3188
3371
  if (!configRewritten) {
@@ -3249,6 +3432,7 @@ var init_rekey = __esm({
3249
3432
  init_api();
3250
3433
  init_runtime_install();
3251
3434
  init_signer_runtime();
3435
+ init_runtime_spec_override();
3252
3436
  init_key();
3253
3437
  init_redact();
3254
3438
  init_rekey_messages();
@@ -3425,6 +3609,46 @@ function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
3425
3609
  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
3426
3610
  };
3427
3611
  }
3612
+ async function runtimeSpecOverrideCheck(directory, sidecar, env) {
3613
+ const facts = [];
3614
+ const installed = sidecar?.runtime_spec_override;
3615
+ if (installed) {
3616
+ facts.push(
3617
+ `signer runtime installed under ${describeRuntimeSpecOverride(installed.specs)} (resolved: ${installed.resolved_specs.join(" ")}; directory key ${installed.directory_key})`
3618
+ );
3619
+ }
3620
+ const mcpInstalled = await readMcpSidecarOverride(directory);
3621
+ if (mcpInstalled) {
3622
+ facts.push(
3623
+ `local MCP runtime installed under ${describeRuntimeSpecOverride(mcpInstalled.specs)} (resolved: ${mcpInstalled.resolved_specs.join(" ")}; directory key ${mcpInstalled.directory_key})`
3624
+ );
3625
+ }
3626
+ let shell;
3627
+ try {
3628
+ const active = resolveRuntimeSpecOverride(env);
3629
+ if (active) shell = `set in this shell: ${describeRuntimeSpecOverride(active)} \u2014 a --repair from here installs these, not the pin`;
3630
+ } catch (err) {
3631
+ shell = err instanceof RuntimeSpecOverrideError ? `set in this shell but REFUSED: ${err.message}` : `set in this shell but unreadable: ${err instanceof Error ? err.message : String(err)}`;
3632
+ }
3633
+ if (shell) facts.push(shell);
3634
+ if (facts.length === 0) return void 0;
3635
+ const variables = Object.values(RUNTIME_SPEC_ENV).join(" / ");
3636
+ return {
3637
+ id: "runtime_spec_override",
3638
+ label: "Runtime spec override",
3639
+ ok: false,
3640
+ detail: `runtime spec overridden \u2014 not the pinned manifest (${MCP_RUNTIME_MANIFEST.signerPackage}@${MCP_RUNTIME_MANIFEST.signerVersion}, ${MCP_RUNTIME_MANIFEST.sdkPackage}@${MCP_RUNTIME_MANIFEST.sdkVersion}). ${facts.join(". ")}.`,
3641
+ repair: `Developer override (#2424). To return to the pinned manifest: unset ${variables}, then run ${RERUN} --doctor --repair --runtime <runtime>. If the override is intentional, this finding is the record of it.`
3642
+ };
3643
+ }
3644
+ async function readMcpSidecarOverride(directory) {
3645
+ try {
3646
+ const parsed = JSON.parse(await promises.readFile(path.join(directory, "mcp-runtime.json"), "utf8"));
3647
+ return parsed.runtime_spec_override;
3648
+ } catch {
3649
+ return void 0;
3650
+ }
3651
+ }
3428
3652
  async function readIdentity(directory) {
3429
3653
  try {
3430
3654
  return JSON.parse(await promises.readFile(path.join(directory, "identity.json"), "utf8"));
@@ -3459,6 +3683,18 @@ async function checksForAgent(entry, input, deps) {
3459
3683
  detail: "No signer-runtime.json sidecar \u2014 the pinned signer runtime was never prepared (or a pre-#1586 npx config).",
3460
3684
  repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
3461
3685
  });
3686
+ } else if (sidecar.runtime_spec_override) {
3687
+ const matches = await installedRuntimeMatchesVersions(sidecar.runtime_directory, sidecar.cli_path, {
3688
+ signerVersion: sidecar.signer_version,
3689
+ sdkVersion: sidecar.sdk_version
3690
+ });
3691
+ checks.push({
3692
+ id: "signer_runtime",
3693
+ label: "Signer runtime (preinstalled wrapper)",
3694
+ ok: matches,
3695
+ detail: matches ? `Installed ${sidecar.signer_package}@${sidecar.signer_version} at ${sidecar.runtime_directory} (override install \u2014 see runtime_spec_override)` : `Override runtime directory is stale or empty (${sidecar.runtime_directory}) \u2014 the CLI or package versions are missing.`,
3696
+ ...matches ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime} with the same HAVEN_*_SPEC variables set.` }
3697
+ });
3462
3698
  } else {
3463
3699
  const matches = await installedRuntimeMatches(sidecar.runtime_directory, sidecar.cli_path);
3464
3700
  const versionOk = sidecar.signer_version === MCP_RUNTIME_MANIFEST.signerVersion;
@@ -3471,6 +3707,8 @@ async function checksForAgent(entry, input, deps) {
3471
3707
  ...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
3472
3708
  });
3473
3709
  }
3710
+ const overrideCheck = await runtimeSpecOverrideCheck(directory, sidecar, deps.env ?? process.env);
3711
+ if (overrideCheck) checks.push(overrideCheck);
3474
3712
  const hostedUrl = identity?.hosted_mcp_url ?? (identity?.api_url ? `${identity.api_url}/mcp` : void 0);
3475
3713
  if (identity?.api_key && hostedUrl) {
3476
3714
  const probe = await (deps.probeHosted ?? probeHostedMcpTools)(identity.api_key, hostedUrl, deps.fetch);
@@ -3542,7 +3780,7 @@ async function checksForAgent(entry, input, deps) {
3542
3780
  label: "Signer stdio handshake",
3543
3781
  ok: false,
3544
3782
  detail: "The local-tools consent is not acknowledged, so the signer refuses to start (by design).",
3545
- repair: `Run: ${RERUN} --ack-local-tools --setup <token> (or re-run your original setup command with --ack-local-tools).`
3783
+ repair: `Run: ${RERUN} --ack-local-tools --setup <token> (or re-run your original connector command with --ack-local-tools).`
3546
3784
  });
3547
3785
  } else {
3548
3786
  const probe = await (deps.probeSignerTools ?? probeLocalMcpTools)(
@@ -3665,7 +3903,7 @@ async function runDoctor(input, deps = {}) {
3665
3903
  signerCapabilities = result.signerCapabilities;
3666
3904
  for (const check of result.checks) primaryChecksById.set(check.id, check);
3667
3905
  }
3668
- for (const id of ["credentials", "signer_runtime"]) {
3906
+ for (const id of ["credentials", "signer_runtime", "runtime_spec_override"]) {
3669
3907
  const check = primaryChecksById.get(id);
3670
3908
  if (check) checks.push(check);
3671
3909
  }
@@ -3816,7 +4054,7 @@ async function runRepair(input, deps = {}) {
3816
4054
  ok: false,
3817
4055
  messages: [
3818
4056
  `The config at ${configPath} is the LOCAL-stdio topology (--local). Repair currently rewrites only the hosted+signer shape and will not touch it.`,
3819
- "Re-run your original setup command (with --local) to repair a local-stdio install."
4057
+ "Re-run your original connector command (with --local) to repair a local-stdio install."
3820
4058
  ]
3821
4059
  };
3822
4060
  }
@@ -3828,7 +4066,7 @@ async function runRepair(input, deps = {}) {
3828
4066
  const signerPath = path.join(directory, "signer.json");
3829
4067
  const prepared = await prepareSignerRuntime(
3830
4068
  { credentialDirectory: directory, signerPath, homeDir, serverName },
3831
- { runCommand: deps.runCommand }
4069
+ { runCommand: deps.runCommand, env: deps.env }
3832
4070
  );
3833
4071
  messages.push(...prepared.messages);
3834
4072
  const names = serverNamesFor(serverName);
@@ -3855,6 +4093,7 @@ var init_doctor = __esm({
3855
4093
  init_runtime_manifest();
3856
4094
  init_probes();
3857
4095
  init_signer_runtime();
4096
+ init_runtime_spec_override();
3858
4097
  init_config_writers();
3859
4098
  init_runtime_registry();
3860
4099
  init_signer_consent();
@@ -3862,7 +4101,7 @@ var init_doctor = __esm({
3862
4101
  init_server_names();
3863
4102
  init_storage();
3864
4103
  init_redact();
3865
- RERUN = "npx @haven_ai/connect@alpha";
4104
+ RERUN = sdk.connectorRerunCommand();
3866
4105
  }
3867
4106
  });
3868
4107
 
@@ -4021,7 +4260,7 @@ function noInstalledClientsError() {
4021
4260
  function promptAbortedError(reason) {
4022
4261
  return new ConnectError(
4023
4262
  "runtime_prompt_aborted",
4024
- `Runtime not chosen (${reason}). Nothing was written: no agent was created, no credentials were stored, and the Haven setup token is still unused. Run the setup command again, or pass --runtime <name> to skip the prompt.`,
4263
+ `Runtime not chosen (${reason}). Nothing was written: no agent was created, no credentials were stored, and the Haven setup token is still unused. Run the connector command again, or pass --runtime <name> to skip the prompt.`,
4025
4264
  "rerun_connect_and_choose_a_runtime"
4026
4265
  );
4027
4266
  }
@@ -4052,10 +4291,119 @@ function defaultPromptIo() {
4052
4291
  };
4053
4292
  }
4054
4293
 
4294
+ // src/wiring-collision.ts
4295
+ init_server_names();
4296
+ init_signer_runtime();
4297
+ init_storage();
4298
+ init_tombstone();
4299
+ async function detectWiringCollision(input) {
4300
+ const root = defaultCredentialRoot(input.credentialsDir);
4301
+ let entries = [];
4302
+ try {
4303
+ entries = await promises.readdir(root);
4304
+ } catch {
4305
+ return null;
4306
+ }
4307
+ const superseded = [];
4308
+ const taken = /* @__PURE__ */ new Set();
4309
+ for (const entry of entries) {
4310
+ const directory = path.join(root, entry);
4311
+ let identityRaw;
4312
+ try {
4313
+ identityRaw = await promises.readFile(path.join(directory, "identity.json"), "utf8");
4314
+ } catch {
4315
+ continue;
4316
+ }
4317
+ taken.add(entry);
4318
+ let identity;
4319
+ try {
4320
+ identity = JSON.parse(identityRaw);
4321
+ } catch {
4322
+ identity = void 0;
4323
+ }
4324
+ if (!identity?.api_key) continue;
4325
+ if (await pathExists2(path.join(directory, TOMBSTONE_FILENAME))) continue;
4326
+ const sidecar = await readRuntimeSidecar(directory);
4327
+ if (sidecar?.server_name) continue;
4328
+ superseded.push({ directory, agentId: identity.agent_id ?? entry });
4329
+ }
4330
+ if (superseded.length === 0) return null;
4331
+ return {
4332
+ superseded,
4333
+ suggestedServerName: proposeServerSlug(input.agentName, taken)
4334
+ };
4335
+ }
4336
+ function proposeServerSlug(agentName, taken) {
4337
+ let base = agentName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, 32).replace(/-+$/g, "");
4338
+ if (!slugIsValid(base)) base = "agent";
4339
+ if (!taken.has(base)) return base;
4340
+ for (let n = 2; n < 1e3; n += 1) {
4341
+ const suffix = `-${n}`;
4342
+ const candidate = `${base.slice(0, 32 - suffix.length).replace(/-+$/g, "")}${suffix}`;
4343
+ if (slugIsValid(candidate) && !taken.has(candidate)) return candidate;
4344
+ }
4345
+ throw new Error(`Could not propose an unused server name for ${JSON.stringify(agentName)}.`);
4346
+ }
4347
+ function slugIsValid(slug) {
4348
+ try {
4349
+ assertValidServerSlug(slug);
4350
+ return true;
4351
+ } catch {
4352
+ return false;
4353
+ }
4354
+ }
4355
+ var MAX_PROMPT_ATTEMPTS2 = 3;
4356
+ async function promptWiringCollisionResolution(collision, agentName, io) {
4357
+ const ids = collision.superseded.map((entry) => entry.agentId).join(", ");
4358
+ io.write(`This machine is already wired to a Haven agent with a live key: ${ids}.
4359
+ `);
4360
+ io.write(`Setting up "${agentName}" on the bare haven / haven-signer pair would replace that wiring.
4361
+ `);
4362
+ io.write(" r) replace \u2014 re-point haven / haven-signer at the new agent and retire the previous directory locally\n");
4363
+ io.write(" (tombstoned, local key files removed; you still revoke it on the Haven agent page)\n");
4364
+ io.write(` a) alongside \u2014 install as a named agent (suggested: ${collision.suggestedServerName}) with its own
4365
+ `);
4366
+ io.write(` haven-<name> / haven-signer-<name> pair, leaving the current wiring untouched
4367
+ `);
4368
+ io.write(" q) quit \u2014 nothing is written and the setup token stays unused\n");
4369
+ for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS2; attempt += 1) {
4370
+ const answer = await io.question("Replace, install alongside, or quit? [r/a/q]: ");
4371
+ if (answer === null) return { action: "abort" };
4372
+ const trimmed = answer.trim().toLowerCase();
4373
+ if (trimmed === "r" || trimmed === "replace") return { action: "replace" };
4374
+ if (trimmed === "q" || trimmed === "quit") return { action: "abort" };
4375
+ if (trimmed === "a" || trimmed === "alongside") {
4376
+ const typed = await io.question(`Server name [${collision.suggestedServerName}]: `);
4377
+ if (typed === null) return { action: "abort" };
4378
+ const serverName = typed.trim() === "" ? collision.suggestedServerName : typed.trim();
4379
+ try {
4380
+ assertValidServerSlug(serverName);
4381
+ } catch (err) {
4382
+ io.write(`${err instanceof Error ? err.message : String(err)}
4383
+ `);
4384
+ continue;
4385
+ }
4386
+ return { action: "alongside", serverName };
4387
+ }
4388
+ io.write(`"${trimmed}" is not one of r, a, q.
4389
+ `);
4390
+ }
4391
+ return { action: "abort" };
4392
+ }
4393
+ async function pathExists2(path) {
4394
+ try {
4395
+ await promises.stat(path);
4396
+ return true;
4397
+ } catch {
4398
+ return false;
4399
+ }
4400
+ }
4401
+
4055
4402
  // src/runtime.ts
4403
+ init_unwire();
4056
4404
  init_local_mcp_runtime();
4057
4405
  init_runtime_manifest();
4058
- var CONNECTOR_VERSION = "0.1.34-alpha.0";
4406
+ var CONNECTOR_VERSION = "0.1.36-alpha.0";
4059
4407
  var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
4060
4408
  var failureOutcomesByError = /* @__PURE__ */ new WeakMap();
4061
4409
  function failureOutcomeFor(runtimeHint, error) {
@@ -4160,6 +4508,28 @@ async function executeConnect(options, deps, trace) {
4160
4508
  await assertServerSlugAvailable(options.serverName, options.credentialsDir);
4161
4509
  }
4162
4510
  log("Checked local credential storage \u2014 all clear.");
4511
+ let serverName = options.serverName;
4512
+ let replacing;
4513
+ if (!serverName) {
4514
+ const collision = await detectWiringCollision({
4515
+ credentialsDir: options.credentialsDir,
4516
+ agentName: setup.agent.name
4517
+ });
4518
+ if (collision) {
4519
+ const resolution = await resolveWiringCollision(collision, setup.agent.name, options, deps);
4520
+ if (resolution.action === "replace") {
4521
+ replacing = collision;
4522
+ log(
4523
+ `Replacing the existing haven / haven-signer wiring (previous agent(s): ${supersededIds(collision)}). The previous directory is retired locally once the new wiring is written; nothing is revoked.`
4524
+ );
4525
+ } else {
4526
+ serverName = resolution.serverName;
4527
+ assertValidServerSlug(serverName);
4528
+ await assertServerSlugAvailable(serverName, options.credentialsDir);
4529
+ log(`Installing alongside the existing wiring as a named agent: ${serverNamesFor(serverName).hosted} / ${serverNamesFor(serverName).signer}.`);
4530
+ }
4531
+ }
4532
+ }
4163
4533
  const localKey = generateKey();
4164
4534
  const localApiKey = generateLocalApiKey();
4165
4535
  log("Minting a fresh signing key and API key \u2014 both stay on this machine.");
@@ -4180,7 +4550,10 @@ async function executeConnect(options, deps, trace) {
4180
4550
  // the dashboard can name it. Derived here rather than sent as the raw
4181
4551
  // slug — `serverNamesFor` is the one place the naming rule lives, and
4182
4552
  // the hosted name is what a user pastes into an MCP config.
4183
- mcpServerName: serverNamesFor(options.serverName).hosted,
4553
+ mcpServerName: serverNamesFor(serverName).hosted,
4554
+ // #2528: 'prose' is the default because it is what a caller who says
4555
+ // nothing is doing — the library entry point is not a --json run.
4556
+ runMode: options.runMode ?? "prose",
4184
4557
  connectorContext: {
4185
4558
  environment_label: options.environmentLabel ?? "Local workspace",
4186
4559
  config_target: installCapabilities.canWriteRuntimeConfig ? "agent runtime MCP config" : "local credential files"
@@ -4192,7 +4565,7 @@ async function executeConnect(options, deps, trace) {
4192
4565
  if (dead) throw dead;
4193
4566
  if (isExpiredSetupChallenge(err)) {
4194
4567
  throw new Error(
4195
- "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."
4568
+ "The Haven setup challenge expired while connecting. Return to Haven, start a fresh connection, and run its new connector command. Do not reuse or paste credentials."
4196
4569
  );
4197
4570
  }
4198
4571
  throw err;
@@ -4202,7 +4575,7 @@ async function executeConnect(options, deps, trace) {
4202
4575
  const credentialPaths = await writeCredentials({
4203
4576
  baseDir: options.credentialsDir,
4204
4577
  agentId: registration.agent_id,
4205
- serverName: options.serverName,
4578
+ serverName,
4206
4579
  apiKey: localApiKey,
4207
4580
  delegateKey: localKey.privateKey,
4208
4581
  delegateAddress: localKey.address,
@@ -4241,7 +4614,7 @@ async function executeConnect(options, deps, trace) {
4241
4614
  ackSigner: options.ackSigner,
4242
4615
  ackLocalTools: options.ackLocalTools,
4243
4616
  localMcp: options.localMcp,
4244
- serverName: options.serverName
4617
+ serverName
4245
4618
  }, {
4246
4619
  onProgress: log,
4247
4620
  // #1543: report "runtime configured" the moment the config write settles,
@@ -4268,7 +4641,7 @@ async function executeConnect(options, deps, trace) {
4268
4641
  environmentLabel: options.environmentLabel ?? "Local workspace"
4269
4642
  });
4270
4643
  if (!early.errorCode || early.errorCode === "manual_runtime_setup_required") {
4271
- log("\u2192 Action needed: approve this agent's budget in the Haven dashboard \u2014 the approval button is live now. Setup continues here in the meantime.");
4644
+ log(approveBudgetCta(setup, registration.approval_url));
4272
4645
  }
4273
4646
  } catch {
4274
4647
  }
@@ -4280,10 +4653,45 @@ async function executeConnect(options, deps, trace) {
4280
4653
  } else {
4281
4654
  log("Haven setup on this machine is complete.");
4282
4655
  }
4656
+ let supersededAgentsRetiredLocally;
4657
+ const retiredAgentIds = [];
4658
+ if (replacing) {
4659
+ if (runtimeInstall.errorCode) {
4660
+ supersededAgentsRetiredLocally = false;
4661
+ log(
4662
+ `Previous agent(s) ${supersededIds(replacing)} were NOT retired: the runtime install did not complete, so their wiring may still be the only working one. Resolve the install, then retire them with ${RERUN_HINT} --unwire <dir>.`
4663
+ );
4664
+ } else {
4665
+ supersededAgentsRetiredLocally = true;
4666
+ for (const entry of replacing.superseded) {
4667
+ try {
4668
+ await tombstoneDirectoryIfAbsent({
4669
+ directory: entry.directory,
4670
+ agentId: entry.agentId,
4671
+ reason: "replaced by a new setup (--replace)",
4672
+ replacedBy: registration.agent_id
4673
+ });
4674
+ await teardownLocalKeyMaterial(entry.directory, await readIdentityFile(entry.directory));
4675
+ retiredAgentIds.push(entry.agentId);
4676
+ log(`Retired previous agent ${entry.agentId} locally: tombstoned, local key files removed.`);
4677
+ } catch (err) {
4678
+ supersededAgentsRetiredLocally = false;
4679
+ log(`Could not retire previous agent ${entry.agentId} locally: ${err instanceof Error ? err.message : String(err)}`);
4680
+ }
4681
+ }
4682
+ }
4683
+ }
4684
+ let supersededScan = null;
4283
4685
  let supersededAgentIds = [];
4284
4686
  try {
4285
- supersededAgentIds = await listOtherAgentIds(options.credentialsDir, credentialPaths.directory);
4286
- if (supersededAgentIds.length > 0) {
4687
+ supersededScan = await listOtherAgentIds(options.credentialsDir, credentialPaths.directory);
4688
+ supersededAgentIds = supersededScan ?? [];
4689
+ if (supersededAgentsRetiredLocally === true) {
4690
+ log("");
4691
+ log(
4692
+ `Replaced: previous agent(s) ${supersededIds(replacing)} are retired on this machine but NOT revoked \u2014 revoke them on the Haven agent page, then restart EVERY long-lived host (gateways, TUI workers, editors): each holds the MCP wiring snapshot from its own start time, and the tombstone speaks only when a stale host next probes the old path.`
4693
+ );
4694
+ } else if (supersededAgentIds.length > 0) {
4287
4695
  log("");
4288
4696
  log(
4289
4697
  `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.`
@@ -4311,23 +4719,31 @@ async function executeConnect(options, deps, trace) {
4311
4719
  restartRequired: runtimeInstall.restartRequired,
4312
4720
  nextUserAction: runtimeInstall.nextUserAction,
4313
4721
  errorCode: runtimeInstall.errorCode,
4314
- environmentLabel: options.environmentLabel ?? "Local workspace"
4722
+ environmentLabel: options.environmentLabel ?? "Local workspace",
4723
+ // The raw scan result, `null` and all — NOT the flattened
4724
+ // `supersededAgentIds` the outcome carries. Flattening here would hand
4725
+ // the dashboard the same ambiguity this field exists to remove (#2561).
4726
+ supersededAgentIds: supersededScan
4315
4727
  });
4316
4728
  } catch (err) {
4317
4729
  log(`Could not report install status to Haven: ${err instanceof Error ? err.message : String(err)}`);
4318
4730
  }
4319
4731
  let approval;
4320
- if (options.waitForApproval !== false && !runtimeInstall.errorCode) {
4732
+ const narrateApprovalWait = options.waitForApproval === true || options.waitForApproval === void 0 && (deps.isStdoutTty ?? Boolean(process.stdout.isTTY));
4733
+ if (narrateApprovalWait && !runtimeInstall.errorCode) {
4321
4734
  approval = await waitForBudgetApproval(api, registration.setup_id, localApiKey, log, options.approvalWait);
4322
4735
  }
4323
- printNextSteps(runtimeInstall, log, approval);
4736
+ printNextSteps(runtimeInstall, log, approval, registration.approval_url);
4324
4737
  const outcome = completionOutcome({
4325
4738
  runtimeInstall,
4326
4739
  delegateAddress: registration.delegate_address,
4327
4740
  hostedMcpUrl: registration.hosted_mcp_url,
4328
4741
  supersededAgentIds,
4742
+ supersededAgentsRetiredLocally,
4743
+ ...replacing ? { retiredAgentIds } : {},
4329
4744
  setupChallengeExpiresAt: setup.challenge.expires_at,
4330
- approvalRequired: registration.agent_status === "pending_approval"
4745
+ approvalRequired: registration.agent_status === "pending_approval",
4746
+ approvalUrl: registration.approval_url
4331
4747
  });
4332
4748
  if (await recordConnectOutcome(deps, credentialPaths.directory, outcome)) {
4333
4749
  log(`Saved this run's outcome to ${CONNECT_OUTCOME_FILENAME} in the agent's credential directory.`);
@@ -4360,7 +4776,16 @@ function completionOutcome(input) {
4360
4776
  instruction: manualSetup ? "Finish the manual MCP setup using the secret-free references shown in normal Connect output, then start a fresh session." : runtimeProfile(runtimeInstall.runtime).activationInstruction
4361
4777
  },
4362
4778
  next_action: nextAction2,
4363
- approval: { required: input.approvalRequired, expires_at: null },
4779
+ approval: {
4780
+ required: input.approvalRequired,
4781
+ expires_at: null,
4782
+ // Only when there is something to approve AND the backend supplied a
4783
+ // link. Never synthesised here: the connector does not know the
4784
+ // dashboard's origin, and a guessed URL is worse than none — it is the
4785
+ // "do not invent one" rule the agent runbook states, applied to the
4786
+ // tool rather than the agent.
4787
+ ...input.approvalRequired && input.approvalUrl ? { url: input.approvalUrl } : {}
4788
+ },
4364
4789
  verification: {
4365
4790
  tools: ["haven_get_agent", "haven_get_allowances"],
4366
4791
  instruction: runtimeVerificationInstruction(runtimeInstall.runtime)
@@ -4378,6 +4803,8 @@ function completionOutcome(input) {
4378
4803
  // agents" is a fact a caller needs, and an omitted key would be
4379
4804
  // indistinguishable from an older connector that never reported it.
4380
4805
  superseded_agent_ids: input.supersededAgentIds ?? [],
4806
+ ...input.supersededAgentsRetiredLocally !== void 0 ? { superseded_agents_retired_locally: input.supersededAgentsRetiredLocally } : {},
4807
+ ...input.retiredAgentIds ? { retired_agent_ids: input.retiredAgentIds } : {},
4381
4808
  ...input.setupChallengeExpiresAt ? { setup_challenge_expires_at: input.setupChallengeExpiresAt } : {},
4382
4809
  ...runtimeInstall.errorCode ? { error: { code: runtimeInstall.errorCode, next_action: nextAction2 } } : {}
4383
4810
  };
@@ -4399,10 +4826,38 @@ function installedClientProse(hint) {
4399
4826
  return `Haven can see these agent clients installed here, likeliest first: ${found.join(", ")}.${suggestion} `;
4400
4827
  }
4401
4828
  function runtimeSelectionPrompt(options, deps) {
4402
- if (options.interactive !== true) return void 0;
4403
- if (!(deps.isTty ?? Boolean(process.stdin.isTTY))) return void 0;
4829
+ if (!interactivePromptAllowed(options, deps)) return void 0;
4404
4830
  return deps.promptRuntime ?? (() => resolveRuntimeByInstalledClientPrompt());
4405
4831
  }
4832
+ function interactivePromptAllowed(options, deps) {
4833
+ if (options.interactive !== true) return false;
4834
+ return deps.isTty ?? Boolean(process.stdin.isTTY);
4835
+ }
4836
+ function supersededIds(collision) {
4837
+ return collision.superseded.map((entry) => entry.agentId).join(", ");
4838
+ }
4839
+ async function resolveWiringCollision(collision, agentName, options, deps) {
4840
+ if (options.replaceExistingWiring) return { action: "replace" };
4841
+ if (interactivePromptAllowed(options, deps)) {
4842
+ const prompt = deps.promptWiringCollision ?? ((c, name) => promptWiringCollisionResolution(c, name, defaultPromptIo()));
4843
+ const resolution = await prompt(collision, agentName);
4844
+ if (resolution.action === "abort") {
4845
+ throw new ConnectError(
4846
+ "wiring_collision_declined",
4847
+ `Setup stopped at your request: this machine is already wired to ${supersededIds(collision)} and you chose neither to replace that wiring nor to install alongside it. Nothing was written: no agent was created, no credentials were stored, and the Haven setup token is still unused. Run the connector command again with --replace to re-point haven / haven-signer at the new agent, or with --name <slug> (e.g. --name ${collision.suggestedServerName}) to install alongside.`,
4848
+ "rerun_connect_with_replace_or_name",
4849
+ { supersededAgentIds: collision.superseded.map((e) => e.agentId), suggestedServerName: collision.suggestedServerName }
4850
+ );
4851
+ }
4852
+ return resolution;
4853
+ }
4854
+ throw new ConnectError(
4855
+ "wiring_collision",
4856
+ `This machine is already wired to a Haven agent with a live key (${supersededIds(collision)}), and setting up "${agentName}" on the bare haven / haven-signer pair would replace that wiring. Nothing was written and the Haven setup token is still unused. If you are an AI agent running this command: do NOT add a flag yourself \u2014 relay this to your user and stop. Your user decides: REPLACE the existing wiring (re-run the same command with --replace added, which re-points the pair at the new agent and retires the previous directory locally \u2014 they still revoke the old agent on the Haven agent page), or install ALONGSIDE it (re-run with --name <slug> added, e.g. --name ${collision.suggestedServerName}, which gives the new agent its own haven-<slug> / haven-signer-<slug> pair). Re-run only with the flag your user chooses.`,
4857
+ "relay_wiring_collision_to_user",
4858
+ { supersededAgentIds: collision.superseded.map((e) => e.agentId), suggestedServerName: collision.suggestedServerName }
4859
+ );
4860
+ }
4406
4861
  function failedConnectOutcome(runtimeHint, error) {
4407
4862
  const message = error instanceof Error ? error.message : "";
4408
4863
  const code = error instanceof ConnectError ? error.code : /Node\.js >=/i.test(message) ? "unsupported_node_version" : /setup challenge.*expired|expired or invalid/i.test(message) ? "setup_challenge_expired_or_invalid" : /only available for Claude Code and Codex/i.test(message) ? "local_mcp_unsupported_runtime" : "connect_failed";
@@ -4433,7 +4888,9 @@ function failedConnectOutcome(runtimeHint, error) {
4433
4888
  ...error instanceof ConnectError && message ? { message: redactForAutomation(message) } : {},
4434
4889
  ...error instanceof ConnectError && error.details.allowedRuntimes ? { allowed_runtimes: error.details.allowedRuntimes } : {},
4435
4890
  ...error instanceof ConnectError && error.details.installedClients?.length ? { installed_clients: error.details.installedClients } : {},
4436
- ...error instanceof ConnectError && error.details.suggestedRuntime ? { suggested_runtime: error.details.suggestedRuntime } : {}
4891
+ ...error instanceof ConnectError && error.details.suggestedRuntime ? { suggested_runtime: error.details.suggestedRuntime } : {},
4892
+ ...error instanceof ConnectError && error.details.supersededAgentIds ? { superseded_agent_ids: error.details.supersededAgentIds } : {},
4893
+ ...error instanceof ConnectError && error.details.suggestedServerName ? { suggested_name: error.details.suggestedServerName } : {}
4437
4894
  }
4438
4895
  };
4439
4896
  }
@@ -4449,6 +4906,22 @@ function printSetupSummary(setup, log) {
4449
4906
  }
4450
4907
  log(`Setup challenge expires at ${setup.challenge.expires_at}. If it expires, return to Haven for a fresh setup and rerun Connect \u2014 do not reuse or paste credentials.`);
4451
4908
  }
4909
+ function approveBudgetCta(setup, approvalUrl) {
4910
+ const where = approvalUrl ? `at ${approvalUrl}` : "in the Haven dashboard";
4911
+ const budgetPhrase = setup.agent_budget.length > 0 ? setup.agent_budget.map((budget) => `up to ${describeSetupBudget(budget)}`).join(", ") : void 0;
4912
+ if (budgetPhrase) {
4913
+ return `\u2192 Action needed: approve this agent's budget \u2014 ${budgetPhrase} from ${setup.haven_wallet.name} on ${setup.haven_wallet.network} \u2014 ${where}. The approval button is live now; setup continues here in the meantime.`;
4914
+ }
4915
+ return `\u2192 Action needed: approve this agent's budget ${where} \u2014 the approval button is live now. Setup continues here in the meantime.`;
4916
+ }
4917
+ function describeSetupBudget(budget) {
4918
+ return describeApprovedBudget({
4919
+ token_symbol: budget.token_symbol,
4920
+ token_address: budget.token_address,
4921
+ amount: budget.allowance_amount,
4922
+ reset_period_min: budget.reset_period_min
4923
+ });
4924
+ }
4452
4925
  function assertSetupChallengeIsUsable(expiresAt) {
4453
4926
  const expiresAtMs = Date.parse(expiresAt);
4454
4927
  if (!Number.isNaN(expiresAtMs) && expiresAtMs > Date.now()) return;
@@ -4460,7 +4933,7 @@ function deadSetupTokenError(err) {
4460
4933
  if (!(err instanceof ConnectRequestError) || err.status !== 410 && err.status !== 401) return null;
4461
4934
  return new ConnectError(
4462
4935
  "setup_challenge_expired_or_invalid",
4463
- "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.",
4936
+ "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 connector command. No local credentials were written.",
4464
4937
  "return_to_haven_for_fresh_setup"
4465
4938
  );
4466
4939
  }
@@ -4561,18 +5034,19 @@ async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
4561
5034
  );
4562
5035
  return "pending";
4563
5036
  }
4564
- function completionHandoffLines(result, approval) {
5037
+ function completionHandoffLines(result, approval, approvalUrl) {
5038
+ const approveStep = approvalUrl ? `Approve the budget at ${approvalUrl}. Approval \u2014 not restarting \u2014 unlocks Haven tools.` : "Return to Haven and approve the budget. Approval \u2014 not restarting \u2014 unlocks Haven tools.";
4565
5039
  if (result.errorCode === "manual_runtime_setup_required") {
4566
5040
  return [
4567
5041
  "Next steps:",
4568
- "1. Return to Haven and approve the budget. Approval \u2014 not restarting \u2014 unlocks Haven tools.",
5042
+ `1. ${approveStep}`,
4569
5043
  "2. Finish the manual MCP setup using the secret-free file references printed above, then start a fresh session in your runtime.",
4570
5044
  `3. ${runtimeVerificationInstruction(result.runtime)}`
4571
5045
  ];
4572
5046
  }
4573
5047
  if (result.errorCode) {
4574
5048
  return [
4575
- "Recovery: runtime setup is not complete. Resolve the reported problem, then return to Haven for a fresh connection and run its new Connect command. Do not manually edit runtime config or paste credentials into prompts, logs, or config."
5049
+ "Recovery: runtime setup is not complete. Resolve the reported problem, then return to Haven for a fresh connection and run its new connector command. Do not manually edit runtime config or paste credentials into prompts, logs, or config."
4576
5050
  ];
4577
5051
  }
4578
5052
  if (approval === "ended") {
@@ -4591,7 +5065,7 @@ function completionHandoffLines(result, approval) {
4591
5065
  }
4592
5066
  return [
4593
5067
  "Next steps:",
4594
- "1. Return to Haven and approve the budget. Approval \u2014 not restarting \u2014 unlocks Haven tools.",
5068
+ `1. ${approveStep}`,
4595
5069
  `2. ${activation}`,
4596
5070
  `3. ${runtimeVerificationInstruction(result.runtime)}`
4597
5071
  ];
@@ -4605,14 +5079,14 @@ function activationInstructionWithWhy(profile) {
4605
5079
  }
4606
5080
  return profile.activationInstruction;
4607
5081
  }
4608
- var RERUN_HINT = "npx @haven_ai/connect@alpha";
5082
+ var RERUN_HINT = sdk.connectorRerunCommand();
4609
5083
  async function listOtherAgentIds(baseDir, currentDirectory) {
4610
5084
  const root = defaultCredentialRoot(baseDir);
4611
5085
  let entries = [];
4612
5086
  try {
4613
5087
  entries = await promises.readdir(root);
4614
5088
  } catch {
4615
- return [];
5089
+ return null;
4616
5090
  }
4617
5091
  const ids = [];
4618
5092
  for (const entry of entries) {
@@ -4632,8 +5106,8 @@ async function listOtherAgentIds(baseDir, currentDirectory) {
4632
5106
  }
4633
5107
  return ids;
4634
5108
  }
4635
- function printNextSteps(result, log, approval) {
4636
- for (const line of completionHandoffLines(result, approval)) log(line);
5109
+ function printNextSteps(result, log, approval, approvalUrl) {
5110
+ for (const line of completionHandoffLines(result, approval, approvalUrl)) log(line);
4637
5111
  }
4638
5112
 
4639
5113
  // src/args.ts
@@ -4655,6 +5129,7 @@ function parseArgs(argv, env = process.env) {
4655
5129
  let tombstoneReplacedBy;
4656
5130
  let unwire;
4657
5131
  let unwireDir;
5132
+ let replace = false;
4658
5133
  for (let i = 0; i < argv.length; i += 1) {
4659
5134
  const arg = argv[i];
4660
5135
  if (arg === "--help" || arg === "-h") {
@@ -4694,6 +5169,8 @@ function parseArgs(argv, env = process.env) {
4694
5169
  options.runtimeForce = requireValue(argv, ++i, arg);
4695
5170
  } else if (arg === "--credentials-dir") {
4696
5171
  options.credentialsDir = requireValue(argv, ++i, arg);
5172
+ } else if (arg === "--replace") {
5173
+ replace = true;
4697
5174
  } else if (arg === "--name") {
4698
5175
  options.serverName = requireValue(argv, ++i, arg);
4699
5176
  assertValidServerSlug(options.serverName);
@@ -4719,6 +5196,17 @@ function parseArgs(argv, env = process.env) {
4719
5196
  if (help) {
4720
5197
  return { options, help, json, doctor, repair, tombstone, rekey };
4721
5198
  }
5199
+ if (replace) {
5200
+ if (rekeyPhase || tombstoneDir || unwire || doctor || repair) {
5201
+ throw new Error("--replace belongs to a --setup run: it says what to do when the bare haven / haven-signer pair is already wired to another agent.");
5202
+ }
5203
+ if (options.serverName) {
5204
+ throw new Error(
5205
+ "--replace and --name contradict each other: --replace overwrites the bare haven / haven-signer wiring, --name installs alongside it under its own pair. Pass one of them."
5206
+ );
5207
+ }
5208
+ options.replaceExistingWiring = true;
5209
+ }
4722
5210
  if (rekey) {
4723
5211
  if (options.setupToken) {
4724
5212
  throw new Error("--rekey replaces an existing agent's key; it does not take --setup. Drop one of them.");
@@ -4797,6 +5285,12 @@ function helpText() {
4797
5285
  " --runtime-force <name> Escape hatch: use exactly this runtime, ignoring environment detection.",
4798
5286
  " --credentials-dir <path> Credential directory fallback. Defaults to ~/.haven/agents.",
4799
5287
  " --environment-label <text> Non-sensitive label shown in Haven setup review.",
5288
+ " --replace When this machine is already wired to a different Haven agent on the bare",
5289
+ " haven / haven-signer pair, re-point that pair at the new agent and retire the",
5290
+ " previous agent directory locally (tombstoned, local key files removed).",
5291
+ " Nothing is revoked \u2014 revoke the old agent on the Haven agent page. Without",
5292
+ " this flag a non-interactive run REFUSES such a collision (wiring_collision)",
5293
+ " and an interactive terminal is asked; --name installs alongside instead.",
4800
5294
  " --name <slug> Wiring slug for a NAMED agent: writes haven-<slug> / haven-signer-<slug>",
4801
5295
  " MCP entries and stores credentials at ~/.haven/agents/<slug>/, so several",
4802
5296
  " agents can run side by side in one runtime. 1-32 lowercase letters, digits,",
@@ -4900,13 +5394,13 @@ async function runCli(argv, io = {
4900
5394
  }
4901
5395
  if (parsed.tombstone) {
4902
5396
  const { writeAgentTombstone: writeAgentTombstone2 } = await Promise.resolve().then(() => (init_tombstone(), tombstone_exports));
4903
- const { readFile: readFile13 } = await import('fs/promises');
4904
- const { join: join11 } = await import('path');
5397
+ const { readFile: readFile14 } = await import('fs/promises');
5398
+ const { join: join12 } = await import('path');
4905
5399
  try {
4906
5400
  let agentId = "unknown";
4907
5401
  try {
4908
5402
  const identity = JSON.parse(
4909
- await readFile13(join11(parsed.tombstone.directory, "identity.json"), "utf8")
5403
+ await readFile14(join12(parsed.tombstone.directory, "identity.json"), "utf8")
4910
5404
  );
4911
5405
  agentId = identity.agent_id ?? "unknown";
4912
5406
  } catch {
@@ -4945,10 +5439,10 @@ async function runCli(argv, io = {
4945
5439
  if (parsed.unwire) {
4946
5440
  const { unwireAgent: unwireAgent2 } = await Promise.resolve().then(() => (init_unwire(), unwire_exports));
4947
5441
  const { homedir: homedir10 } = await import('os');
4948
- const { join: join11 } = await import('path');
5442
+ const { join: join12 } = await import('path');
4949
5443
  const homeDir = homedir10();
4950
- const root = parsed.options.credentialsDir ?? join11(homeDir, ".haven", "agents");
4951
- const directory = parsed.unwireDir ?? (parsed.options.serverName ? join11(root, parsed.options.serverName) : root);
5444
+ const root = parsed.options.credentialsDir ?? join12(homeDir, ".haven", "agents");
5445
+ const directory = parsed.unwireDir ?? (parsed.options.serverName ? join12(root, parsed.options.serverName) : root);
4952
5446
  try {
4953
5447
  const result = await unwireAgent2({
4954
5448
  directory,
@@ -5115,12 +5609,23 @@ async function runCli(argv, io = {
5115
5609
  const result = await runConnect(
5116
5610
  {
5117
5611
  ...parsed.options,
5118
- waitForApproval: !parsed.json,
5612
+ // #1377 D / #2484: leave prose runs UNSPECIFIED (undefined) so
5613
+ // runConnect's stdout-TTY narration gate decides whether there is a
5614
+ // watching human to narrate to — an agent invoking prose as a tool
5615
+ // call has none and must not sit opaque in the wait. --json stays an
5616
+ // explicit false (skip, emit promptly).
5617
+ waitForApproval: parsed.json ? false : void 0,
5119
5618
  // #1719: only a human-facing run may be asked which installed client to
5120
5619
  // configure. --json is the automation contract — it must fail with a
5121
5620
  // machine-readable code, never block on stdin. runConnect additionally
5122
5621
  // requires a real TTY before it prompts.
5123
- interactive: !parsed.json
5622
+ interactive: !parsed.json,
5623
+ // #2528: reported to the backend at register, so the funnel can tell a
5624
+ // machine-readable run from a narrated one. Read from the SAME
5625
+ // `parsed.json` the three flags above use, rather than inferred later
5626
+ // from `waitForApproval` — that flag is already false for a prose run
5627
+ // with no TTY (#2484), so inferring would mislabel real prose runs.
5628
+ runMode: parsed.json ? "json" : "prose"
5124
5629
  },
5125
5630
  {
5126
5631
  log: (message) => (parsed.json ? io.stderr : io.stdout)(`${message}