@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/README.md +255 -37
- package/dist/cli.cjs +620 -115
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +623 -118
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +620 -115
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +257 -7
- package/dist/index.d.ts +257 -7
- package/dist/index.js +622 -117
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
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.
|
|
552
|
+
sdkVersion: "0.1.35-alpha.0",
|
|
538
553
|
signerPackage: "@haven_ai/signer",
|
|
539
|
-
signerVersion: "0.1.
|
|
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
|
|
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
|
|
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 = "
|
|
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
|
|
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 (
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
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 ${
|
|
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
|
|
1691
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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 ${
|
|
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
|
|
1856
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
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(
|
|
3023
|
-
promises.rm(path.join(
|
|
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(
|
|
3195
|
+
await promises.writeFile(path.join(directory, "identity.json"), `${JSON.stringify(rest, null, 2)}
|
|
3028
3196
|
`, { mode: 384 });
|
|
3029
3197
|
}
|
|
3030
|
-
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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.
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
4285
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
4906
|
-
const { join:
|
|
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
|
|
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:
|
|
5444
|
+
const { join: join12 } = await import('path');
|
|
4951
5445
|
const homeDir = homedir10();
|
|
4952
|
-
const root = parsed.options.credentialsDir ??
|
|
4953
|
-
const directory = parsed.unwireDir ?? (parsed.options.serverName ?
|
|
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
|
-
|
|
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}
|