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