@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.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import crypto from 'crypto';
|
|
1
|
+
import crypto, { createHash } from 'crypto';
|
|
2
2
|
import { Wallet } from 'ethers';
|
|
3
3
|
import { mkdir, rm, stat, readdir, readFile, writeFile, chmod, access, unlink, rename } from 'fs/promises';
|
|
4
4
|
import { homedir, platform } from 'os';
|
|
5
5
|
import { join, resolve, dirname, basename } from 'path';
|
|
6
6
|
import { ensureConsent, computeConsentHash, loadCredentials, consentInputFromClient, registeredToolNames, MCP_VERSION } from '@haven_ai/mcp';
|
|
7
7
|
import { ensureSignerConsent, computeSignerConsentHash, loadSignerCredentials, createEdgeSigner, toolSchemas } from '@haven_ai/signer';
|
|
8
|
-
import { isSupportedNodeVersion, SKILL_FOLDER_NAME, resolveTokenFromAddress, HAVEN_SKILL_MD, HAVEN_SKILL_BODY_MD, HAVEN_MINIMUM_NODE_VERSION, unsupportedNodeVersionMessage } from '@haven_ai/sdk';
|
|
8
|
+
import { connectorRerunCommand, isSupportedNodeVersion, SKILL_FOLDER_NAME, resolveTokenFromAddress, HAVEN_SKILL_MD, HAVEN_SKILL_BODY_MD, HAVEN_MINIMUM_NODE_VERSION, unsupportedNodeVersionMessage } from '@haven_ai/sdk';
|
|
9
9
|
import { parseDocument, isMap, stringify } from 'yaml';
|
|
10
10
|
import { spawn, execFile } from 'child_process';
|
|
11
11
|
import { promisify } from 'util';
|
|
@@ -47,6 +47,7 @@ function createConnectApiClient(baseUrl, fetchImpl = fetch) {
|
|
|
47
47
|
runtime: input.runtime,
|
|
48
48
|
connector_version: input.connectorVersion,
|
|
49
49
|
mcp_server_name: input.mcpServerName,
|
|
50
|
+
run_mode: input.runMode,
|
|
50
51
|
connector_context: input.connectorContext,
|
|
51
52
|
install_capabilities: input.installCapabilities && {
|
|
52
53
|
can_write_runtime_config: input.installCapabilities.canWriteRuntimeConfig,
|
|
@@ -82,7 +83,21 @@ function createConnectApiClient(baseUrl, fetchImpl = fetch) {
|
|
|
82
83
|
restart_required: input.restartRequired,
|
|
83
84
|
next_user_action: input.nextUserAction,
|
|
84
85
|
error_code: input.errorCode ?? null,
|
|
85
|
-
environment_label: input.environmentLabel
|
|
86
|
+
environment_label: input.environmentLabel,
|
|
87
|
+
// Three states on the wire, and ABSENT is a fourth (#2561 review).
|
|
88
|
+
// `?? null` alone collapsed the fourth into the third: a report that
|
|
89
|
+
// simply had nothing to say — the early config-written ping, which
|
|
90
|
+
// fires before the scan has started — asserted "the scan could not
|
|
91
|
+
// run" instead of leaving the key alone. Inert today, because the
|
|
92
|
+
// complete report overwrites it seconds later and nothing reads the
|
|
93
|
+
// row in between; a landmine the moment any caller relies on
|
|
94
|
+
// "absent = unchanged", which is what the backend's jsonb merge
|
|
95
|
+
// means and what this field's own contract says.
|
|
96
|
+
//
|
|
97
|
+
// So: a caller that passes the field says something (`null` included,
|
|
98
|
+
// deliberately, since "could not run" is a claim worth making); a
|
|
99
|
+
// caller that omits it says nothing.
|
|
100
|
+
...input.supersededAgentIds !== void 0 ? { superseded_agent_ids: input.supersededAgentIds } : {}
|
|
86
101
|
})
|
|
87
102
|
});
|
|
88
103
|
}
|
|
@@ -527,9 +542,9 @@ var init_runtime_manifest = __esm({
|
|
|
527
542
|
mcpPackage: "@haven_ai/mcp",
|
|
528
543
|
mcpVersion: MCP_VERSION,
|
|
529
544
|
sdkPackage: "@haven_ai/sdk",
|
|
530
|
-
sdkVersion: "0.1.
|
|
545
|
+
sdkVersion: "0.1.35-alpha.0",
|
|
531
546
|
signerPackage: "@haven_ai/signer",
|
|
532
|
-
signerVersion: "0.1.
|
|
547
|
+
signerVersion: "0.1.35-alpha.0",
|
|
533
548
|
// Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
|
|
534
549
|
// while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
|
|
535
550
|
// so the guard that was supposed to enforce the floor waved Node v23 through
|
|
@@ -895,7 +910,7 @@ async function writeJsonRuntimeConfig(input, target, serverRoot) {
|
|
|
895
910
|
restartRequired: true,
|
|
896
911
|
messages: unreadable ? [
|
|
897
912
|
`Could not update ${configTargetLabel(input.runtime)}: ${err.message}.`,
|
|
898
|
-
`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
|
|
913
|
+
`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.`
|
|
899
914
|
] : [`Could not update ${configTargetLabel(input.runtime)}: ${err instanceof Error ? err.message : String(err)}`],
|
|
900
915
|
errorCode: unreadable ? "runtime_config_unreadable" : "runtime_config_write_failed"
|
|
901
916
|
};
|
|
@@ -960,7 +975,7 @@ async function writeHermesConfig(input, deps) {
|
|
|
960
975
|
restartRequired: true,
|
|
961
976
|
messages: unreadable ? [
|
|
962
977
|
`Could not update Hermes Agent config: ${err.message}.`,
|
|
963
|
-
`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
|
|
978
|
+
`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.`
|
|
964
979
|
] : [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."],
|
|
965
980
|
errorCode: unreadable ? "runtime_config_unreadable" : "runtime_config_write_failed"
|
|
966
981
|
};
|
|
@@ -1350,7 +1365,7 @@ var init_config_writers = __esm({
|
|
|
1350
1365
|
this.name = "InvalidCodexTomlError";
|
|
1351
1366
|
}
|
|
1352
1367
|
};
|
|
1353
|
-
REPAIR_COMMAND_PREFIX = "
|
|
1368
|
+
REPAIR_COMMAND_PREFIX = connectorRerunCommand("--doctor --repair --runtime");
|
|
1354
1369
|
UnreadableRuntimeConfigError = class extends Error {
|
|
1355
1370
|
configPath;
|
|
1356
1371
|
constructor(configPath, detail) {
|
|
@@ -1600,32 +1615,129 @@ var init_probes = __esm({
|
|
|
1600
1615
|
"src/probes.ts"() {
|
|
1601
1616
|
}
|
|
1602
1617
|
});
|
|
1618
|
+
function resolveRuntimeSpecOverride(env) {
|
|
1619
|
+
const override = {};
|
|
1620
|
+
for (const pkg of Object.keys(RUNTIME_SPEC_ENV)) {
|
|
1621
|
+
const variable = RUNTIME_SPEC_ENV[pkg];
|
|
1622
|
+
const raw = env[variable];
|
|
1623
|
+
if (raw === void 0) continue;
|
|
1624
|
+
override[pkg] = validateSpec(variable, raw);
|
|
1625
|
+
}
|
|
1626
|
+
return Object.keys(override).length > 0 ? override : void 0;
|
|
1627
|
+
}
|
|
1628
|
+
function validateSpec(variable, raw) {
|
|
1629
|
+
if (raw.length === 0 || raw.trim().length === 0) {
|
|
1630
|
+
throw new RuntimeSpecOverrideError(variable, "it is empty");
|
|
1631
|
+
}
|
|
1632
|
+
if (/\s/.test(raw)) {
|
|
1633
|
+
throw new RuntimeSpecOverrideError(variable, "it contains whitespace");
|
|
1634
|
+
}
|
|
1635
|
+
if (/[\u0000-\u001f\u007f]/.test(raw)) {
|
|
1636
|
+
throw new RuntimeSpecOverrideError(variable, "it contains a control character");
|
|
1637
|
+
}
|
|
1638
|
+
const meta = SHELL_METACHARACTERS.exec(raw);
|
|
1639
|
+
if (meta) {
|
|
1640
|
+
throw new RuntimeSpecOverrideError(variable, `it contains the shell metacharacter ${JSON.stringify(meta[0])}`);
|
|
1641
|
+
}
|
|
1642
|
+
return raw;
|
|
1643
|
+
}
|
|
1644
|
+
function runtimeSpecOverrideDirectoryKey(resolvedSpecs) {
|
|
1645
|
+
const digest = createHash("sha256").update(resolvedSpecs.join("\n")).digest("hex");
|
|
1646
|
+
return `override-${digest.slice(0, 12)}`;
|
|
1647
|
+
}
|
|
1648
|
+
function overrideApplies(override, packages) {
|
|
1649
|
+
return override !== void 0 && packages.some((pkg) => override[pkg] !== void 0);
|
|
1650
|
+
}
|
|
1651
|
+
function runtimeSpecOverrideNotice(runtimeLabel, override, pinned, runtimeDirectory) {
|
|
1652
|
+
const lines = [`RUNTIME SPEC OVERRIDE ACTIVE for the local Haven ${runtimeLabel} \u2014 this is NOT the pinned manifest.`];
|
|
1653
|
+
for (const pkg of Object.keys(RUNTIME_SPEC_ENV)) {
|
|
1654
|
+
const spec = override[pkg];
|
|
1655
|
+
if (spec === void 0) continue;
|
|
1656
|
+
const pin = pinned[pkg];
|
|
1657
|
+
lines.push(` ${RUNTIME_SPEC_ENV[pkg]}=${spec}${pin ? ` (instead of ${pin})` : ""}`);
|
|
1658
|
+
}
|
|
1659
|
+
lines.push(` Installing into ${runtimeDirectory} \u2014 the pinned runtime directory is untouched.`);
|
|
1660
|
+
lines.push(" Unset the variable(s) and re-run to return to the pinned manifest.");
|
|
1661
|
+
return lines;
|
|
1662
|
+
}
|
|
1663
|
+
function describeRuntimeSpecOverride(override) {
|
|
1664
|
+
return Object.keys(RUNTIME_SPEC_ENV).filter((pkg) => override[pkg] !== void 0).map((pkg) => `${RUNTIME_SPEC_ENV[pkg]}=${override[pkg]}`).join(" ");
|
|
1665
|
+
}
|
|
1666
|
+
var RUNTIME_SPEC_ENV, SHELL_METACHARACTERS, RuntimeSpecOverrideError;
|
|
1667
|
+
var init_runtime_spec_override = __esm({
|
|
1668
|
+
"src/runtime-spec-override.ts"() {
|
|
1669
|
+
RUNTIME_SPEC_ENV = {
|
|
1670
|
+
signer: "HAVEN_SIGNER_SPEC",
|
|
1671
|
+
sdk: "HAVEN_SDK_SPEC",
|
|
1672
|
+
mcp: "HAVEN_MCP_SPEC"
|
|
1673
|
+
};
|
|
1674
|
+
SHELL_METACHARACTERS = /[;&|<>$`'"()!#*?[\]{}\\]/;
|
|
1675
|
+
RuntimeSpecOverrideError = class extends Error {
|
|
1676
|
+
code = "runtime_spec_override_invalid";
|
|
1677
|
+
variable;
|
|
1678
|
+
constructor(variable, reason) {
|
|
1679
|
+
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>).`);
|
|
1680
|
+
this.name = "RuntimeSpecOverrideError";
|
|
1681
|
+
this.variable = variable;
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
});
|
|
1603
1686
|
async function prepareSignerRuntime(input, deps = {}) {
|
|
1604
1687
|
const homeDir = input.homeDir ?? homedir();
|
|
1605
|
-
const
|
|
1688
|
+
const override = resolveSignerRuntimeOverride(deps.env ?? process.env);
|
|
1689
|
+
const signerSpec = override?.signer ?? signerPackageSpec();
|
|
1690
|
+
const sdkSpec = override?.sdk ?? sdkPackageSpec();
|
|
1691
|
+
const resolvedSpecs = [signerSpec, sdkSpec];
|
|
1692
|
+
const overrideRecord = override ? { specs: override, resolved_specs: resolvedSpecs, directory_key: runtimeSpecOverrideDirectoryKey(resolvedSpecs) } : void 0;
|
|
1693
|
+
const runtimeDirectory = resolve(
|
|
1694
|
+
homeDir,
|
|
1695
|
+
".haven",
|
|
1696
|
+
"signer-runtime",
|
|
1697
|
+
overrideRecord ? overrideRecord.directory_key : MCP_RUNTIME_MANIFEST.signerVersion
|
|
1698
|
+
);
|
|
1606
1699
|
const npmCacheDirectory = resolve(homeDir, ".haven", "npm-cache");
|
|
1607
1700
|
const cliPath = join(runtimeDirectory, "node_modules", "@haven_ai", "signer", "dist", "cli.js");
|
|
1608
1701
|
const messages = [];
|
|
1702
|
+
if (override) {
|
|
1703
|
+
messages.push(...runtimeSpecOverrideNotice(
|
|
1704
|
+
"signer runtime",
|
|
1705
|
+
override,
|
|
1706
|
+
{ signer: signerPackageSpec(), sdk: sdkPackageSpec() },
|
|
1707
|
+
runtimeDirectory
|
|
1708
|
+
));
|
|
1709
|
+
}
|
|
1609
1710
|
await mkdir(runtimeDirectory, { recursive: true, mode: 448 });
|
|
1610
1711
|
await chmod(runtimeDirectory, 448).catch(() => void 0);
|
|
1611
1712
|
await mkdir(npmCacheDirectory, { recursive: true, mode: 448 });
|
|
1612
1713
|
await chmod(npmCacheDirectory, 448).catch(() => void 0);
|
|
1613
|
-
if (
|
|
1714
|
+
if (override) {
|
|
1715
|
+
await installRuntimePackages(runtimeDirectory, npmCacheDirectory, resolvedSpecs, deps);
|
|
1716
|
+
messages.push(`Installed local Haven signer runtime from override (${resolvedSpecs.join(" ")}).`);
|
|
1717
|
+
} else if (await installedRuntimeMatches(runtimeDirectory, cliPath)) {
|
|
1614
1718
|
messages.push(`Using existing local Haven signer runtime ${signerPackageSpec()}.`);
|
|
1615
1719
|
} else {
|
|
1616
|
-
await installRuntimePackages(runtimeDirectory, npmCacheDirectory, deps);
|
|
1720
|
+
await installRuntimePackages(runtimeDirectory, npmCacheDirectory, resolvedSpecs, deps);
|
|
1617
1721
|
messages.push(`Installed local Haven signer runtime ${signerPackageSpec()}.`);
|
|
1618
1722
|
}
|
|
1619
1723
|
await assertFileExists(cliPath, "local Haven signer CLI");
|
|
1724
|
+
const installedVersions = override ? await readInstalledVersions(runtimeDirectory) : void 0;
|
|
1620
1725
|
const wrapperPath = join(input.credentialDirectory, "bin", "haven-signer.mjs");
|
|
1621
|
-
await writeWrapper({
|
|
1726
|
+
await writeWrapper({
|
|
1727
|
+
wrapperPath,
|
|
1728
|
+
cliPath,
|
|
1729
|
+
signerPath: input.signerPath,
|
|
1730
|
+
overrideComment: override ? describeRuntimeSpecOverride(override) : void 0
|
|
1731
|
+
});
|
|
1622
1732
|
await writeRuntimeSidecar({
|
|
1623
1733
|
path: join(input.credentialDirectory, "signer-runtime.json"),
|
|
1624
1734
|
wrapperPath,
|
|
1625
1735
|
runtimeDirectory,
|
|
1626
1736
|
npmCacheDirectory,
|
|
1627
1737
|
cliPath,
|
|
1628
|
-
serverName: input.serverName
|
|
1738
|
+
serverName: input.serverName,
|
|
1739
|
+
override: overrideRecord,
|
|
1740
|
+
installedVersions
|
|
1629
1741
|
});
|
|
1630
1742
|
messages.push(`Prepared stable local Haven signer wrapper: ${wrapperPath}`);
|
|
1631
1743
|
return {
|
|
@@ -1635,10 +1747,17 @@ async function prepareSignerRuntime(input, deps = {}) {
|
|
|
1635
1747
|
runtimeDirectory,
|
|
1636
1748
|
npmCacheDirectory,
|
|
1637
1749
|
cliPath,
|
|
1638
|
-
messages
|
|
1750
|
+
messages,
|
|
1751
|
+
...overrideRecord ? { runtimeSpecOverride: overrideRecord } : {}
|
|
1639
1752
|
};
|
|
1640
1753
|
}
|
|
1641
|
-
|
|
1754
|
+
function resolveSignerRuntimeOverride(env) {
|
|
1755
|
+
const override = resolveRuntimeSpecOverride(env);
|
|
1756
|
+
if (!overrideApplies(override, ["signer", "sdk"])) return void 0;
|
|
1757
|
+
const { signer, sdk } = override;
|
|
1758
|
+
return { ...signer !== void 0 ? { signer } : {}, ...sdk !== void 0 ? { sdk } : {} };
|
|
1759
|
+
}
|
|
1760
|
+
async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, packageSpecs, deps) {
|
|
1642
1761
|
const { runCommand, onProgress } = deps;
|
|
1643
1762
|
const baseArgs = [
|
|
1644
1763
|
"install",
|
|
@@ -1648,8 +1767,7 @@ async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, deps)
|
|
|
1648
1767
|
"--no-fund",
|
|
1649
1768
|
"--omit=dev",
|
|
1650
1769
|
"--prefer-offline",
|
|
1651
|
-
|
|
1652
|
-
sdkPackageSpec()
|
|
1770
|
+
...packageSpecs
|
|
1653
1771
|
];
|
|
1654
1772
|
const run = async (args) => {
|
|
1655
1773
|
const startedAt = Date.now();
|
|
@@ -1672,23 +1790,33 @@ async function installRuntimePackages(runtimeDirectory, npmCacheDirectory, deps)
|
|
|
1672
1790
|
await run([...baseArgs, "--cache", npmCacheDirectory]);
|
|
1673
1791
|
} catch (err) {
|
|
1674
1792
|
throw new Error(
|
|
1675
|
-
`Could not install local Haven signer runtime ${
|
|
1793
|
+
`Could not install local Haven signer runtime ${packageSpecs.join(" ")}: ${err instanceof Error ? err.message : String(err)}`
|
|
1676
1794
|
);
|
|
1677
1795
|
}
|
|
1678
1796
|
}
|
|
1679
1797
|
}
|
|
1680
1798
|
async function installedRuntimeMatches(runtimeDirectory, cliPath) {
|
|
1799
|
+
return installedRuntimeMatchesVersions(runtimeDirectory, cliPath, {
|
|
1800
|
+
signerVersion: MCP_RUNTIME_MANIFEST.signerVersion,
|
|
1801
|
+
sdkVersion: MCP_RUNTIME_MANIFEST.sdkVersion
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
async function installedRuntimeMatchesVersions(runtimeDirectory, cliPath, expected) {
|
|
1681
1805
|
try {
|
|
1682
1806
|
await assertFileExists(cliPath, "local Haven signer CLI");
|
|
1683
|
-
const
|
|
1684
|
-
|
|
1685
|
-
readPackageJson(join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
|
|
1686
|
-
]);
|
|
1687
|
-
return signerPackage.version === MCP_RUNTIME_MANIFEST.signerVersion && sdkPackage.version === MCP_RUNTIME_MANIFEST.sdkVersion;
|
|
1807
|
+
const installed = await readInstalledVersions(runtimeDirectory);
|
|
1808
|
+
return installed.signerVersion === expected.signerVersion && installed.sdkVersion === expected.sdkVersion;
|
|
1688
1809
|
} catch {
|
|
1689
1810
|
return false;
|
|
1690
1811
|
}
|
|
1691
1812
|
}
|
|
1813
|
+
async function readInstalledVersions(runtimeDirectory) {
|
|
1814
|
+
const [signerPackage, sdkPackage] = await Promise.all([
|
|
1815
|
+
readPackageJson(join(runtimeDirectory, "node_modules", "@haven_ai", "signer", "package.json")),
|
|
1816
|
+
readPackageJson(join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
|
|
1817
|
+
]);
|
|
1818
|
+
return { signerVersion: signerPackage.version ?? "", sdkVersion: sdkPackage.version ?? "" };
|
|
1819
|
+
}
|
|
1692
1820
|
async function readPackageJson(path) {
|
|
1693
1821
|
return JSON.parse(await readFile(path, "utf8"));
|
|
1694
1822
|
}
|
|
@@ -1697,6 +1825,7 @@ async function writeWrapper(input) {
|
|
|
1697
1825
|
await chmod(dirname(input.wrapperPath), 448).catch(() => void 0);
|
|
1698
1826
|
const source = [
|
|
1699
1827
|
"#!/usr/bin/env node",
|
|
1828
|
+
...input.overrideComment ? [`// HAVEN RUNTIME SPEC OVERRIDE (#2424): ${input.overrideComment} \u2014 this wrapper launches a NON-pinned signer build.`] : [],
|
|
1700
1829
|
"import { spawn } from 'node:child_process'",
|
|
1701
1830
|
"",
|
|
1702
1831
|
`const cliPath = ${JSON.stringify(input.cliPath)}`,
|
|
@@ -1728,13 +1857,14 @@ async function writeRuntimeSidecar(input) {
|
|
|
1728
1857
|
const value = {
|
|
1729
1858
|
...input.serverName ? { server_name: input.serverName } : {},
|
|
1730
1859
|
signer_package: MCP_RUNTIME_MANIFEST.signerPackage,
|
|
1731
|
-
signer_version: MCP_RUNTIME_MANIFEST.signerVersion,
|
|
1860
|
+
signer_version: input.installedVersions?.signerVersion ?? MCP_RUNTIME_MANIFEST.signerVersion,
|
|
1732
1861
|
sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
|
|
1733
|
-
sdk_version: MCP_RUNTIME_MANIFEST.sdkVersion,
|
|
1862
|
+
sdk_version: input.installedVersions?.sdkVersion ?? MCP_RUNTIME_MANIFEST.sdkVersion,
|
|
1734
1863
|
wrapper_path: input.wrapperPath,
|
|
1735
1864
|
runtime_directory: input.runtimeDirectory,
|
|
1736
1865
|
npm_cache_directory: input.npmCacheDirectory,
|
|
1737
|
-
cli_path: input.cliPath
|
|
1866
|
+
cli_path: input.cliPath,
|
|
1867
|
+
...input.override ? { runtime_spec_override: input.override } : {}
|
|
1738
1868
|
};
|
|
1739
1869
|
await writeFile(input.path, `${JSON.stringify(value, null, 2)}
|
|
1740
1870
|
`, { mode: 384 });
|
|
@@ -1751,6 +1881,7 @@ var execFileAsync, SIGNER_INSTALL_TIMEOUT_MS, SIGNER_INSTALL_HEARTBEAT_MS;
|
|
|
1751
1881
|
var init_signer_runtime = __esm({
|
|
1752
1882
|
"src/signer-runtime.ts"() {
|
|
1753
1883
|
init_runtime_manifest();
|
|
1884
|
+
init_runtime_spec_override();
|
|
1754
1885
|
execFileAsync = promisify(execFile);
|
|
1755
1886
|
SIGNER_INSTALL_TIMEOUT_MS = 6e5;
|
|
1756
1887
|
SIGNER_INSTALL_HEARTBEAT_MS = 15e3;
|
|
@@ -1759,27 +1890,50 @@ var init_signer_runtime = __esm({
|
|
|
1759
1890
|
async function prepareLocalMcpRuntime(input, deps = {}) {
|
|
1760
1891
|
assertSupportedNodeVersion(input.nodeVersion);
|
|
1761
1892
|
const homeDir = input.homeDir ?? homedir();
|
|
1762
|
-
const
|
|
1893
|
+
const override = resolveLocalMcpRuntimeOverride(deps.env ?? process.env);
|
|
1894
|
+
const mcpSpec = override?.mcp ?? mcpPackageSpec();
|
|
1895
|
+
const sdkSpec = override?.sdk ?? sdkPackageSpec();
|
|
1896
|
+
const resolvedSpecs = [mcpSpec, sdkSpec];
|
|
1897
|
+
const overrideRecord = override ? { specs: override, resolved_specs: resolvedSpecs, directory_key: runtimeSpecOverrideDirectoryKey(resolvedSpecs) } : void 0;
|
|
1898
|
+
const runtimeDirectory = resolve(
|
|
1899
|
+
homeDir,
|
|
1900
|
+
".haven",
|
|
1901
|
+
"mcp-runtime",
|
|
1902
|
+
overrideRecord ? overrideRecord.directory_key : MCP_RUNTIME_MANIFEST.mcpVersion
|
|
1903
|
+
);
|
|
1763
1904
|
const npmCacheDirectory = resolve(homeDir, ".haven", "npm-cache");
|
|
1764
1905
|
const cliPath = join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "dist", "cli.js");
|
|
1765
1906
|
const messages = [];
|
|
1907
|
+
if (override) {
|
|
1908
|
+
messages.push(...runtimeSpecOverrideNotice(
|
|
1909
|
+
"MCP runtime",
|
|
1910
|
+
override,
|
|
1911
|
+
{ mcp: mcpPackageSpec(), sdk: sdkPackageSpec() },
|
|
1912
|
+
runtimeDirectory
|
|
1913
|
+
));
|
|
1914
|
+
}
|
|
1766
1915
|
await mkdir(runtimeDirectory, { recursive: true, mode: 448 });
|
|
1767
1916
|
await chmod(runtimeDirectory, 448).catch(() => void 0);
|
|
1768
1917
|
await mkdir(npmCacheDirectory, { recursive: true, mode: 448 });
|
|
1769
1918
|
await chmod(npmCacheDirectory, 448).catch(() => void 0);
|
|
1770
|
-
if (
|
|
1919
|
+
if (override) {
|
|
1920
|
+
await installRuntimePackages2(runtimeDirectory, npmCacheDirectory, resolvedSpecs, deps);
|
|
1921
|
+
messages.push(`Installed local Haven MCP runtime from override (${resolvedSpecs.join(" ")}).`);
|
|
1922
|
+
} else if (await installedRuntimeMatches2(runtimeDirectory, cliPath)) {
|
|
1771
1923
|
messages.push(`Using existing local Haven MCP runtime ${mcpPackageSpec()}.`);
|
|
1772
1924
|
} else {
|
|
1773
|
-
await installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps);
|
|
1925
|
+
await installRuntimePackages2(runtimeDirectory, npmCacheDirectory, resolvedSpecs, deps);
|
|
1774
1926
|
messages.push(`Installed local Haven MCP runtime ${mcpPackageSpec()}.`);
|
|
1775
1927
|
}
|
|
1776
1928
|
await assertFileExists2(cliPath, "local Haven MCP CLI");
|
|
1929
|
+
const installedVersions = override ? await readInstalledVersions2(runtimeDirectory) : void 0;
|
|
1777
1930
|
const wrapperPath = join(input.credentialDirectory, "bin", "haven-mcp");
|
|
1778
1931
|
await writeWrapper2({
|
|
1779
1932
|
wrapperPath,
|
|
1780
1933
|
cliPath,
|
|
1781
1934
|
identityPath: input.identityPath,
|
|
1782
|
-
signerPath: input.signerPath
|
|
1935
|
+
signerPath: input.signerPath,
|
|
1936
|
+
overrideComment: override ? describeRuntimeSpecOverride(override) : void 0
|
|
1783
1937
|
});
|
|
1784
1938
|
await writeRuntimeSidecar2({
|
|
1785
1939
|
path: join(input.credentialDirectory, "mcp-runtime.json"),
|
|
@@ -1787,7 +1941,9 @@ async function prepareLocalMcpRuntime(input, deps = {}) {
|
|
|
1787
1941
|
runtimeDirectory,
|
|
1788
1942
|
npmCacheDirectory,
|
|
1789
1943
|
cliPath,
|
|
1790
|
-
serverName: input.serverName
|
|
1944
|
+
serverName: input.serverName,
|
|
1945
|
+
override: overrideRecord,
|
|
1946
|
+
installedVersions
|
|
1791
1947
|
});
|
|
1792
1948
|
messages.push(`Prepared stable local Haven MCP wrapper: ${wrapperPath}`);
|
|
1793
1949
|
return {
|
|
@@ -1797,7 +1953,8 @@ async function prepareLocalMcpRuntime(input, deps = {}) {
|
|
|
1797
1953
|
runtimeDirectory,
|
|
1798
1954
|
npmCacheDirectory,
|
|
1799
1955
|
cliPath,
|
|
1800
|
-
messages
|
|
1956
|
+
messages,
|
|
1957
|
+
...overrideRecord ? { runtimeSpecOverride: overrideRecord } : {}
|
|
1801
1958
|
};
|
|
1802
1959
|
}
|
|
1803
1960
|
function assertSupportedNodeVersion(nodeVersion = process.versions.node, minimumNodeVersion = MCP_RUNTIME_MANIFEST.minimumNodeVersion, subject = "Haven setup") {
|
|
@@ -1805,7 +1962,13 @@ function assertSupportedNodeVersion(nodeVersion = process.versions.node, minimum
|
|
|
1805
1962
|
throw new UnsupportedNodeVersionError(nodeVersion, minimumNodeVersion, subject);
|
|
1806
1963
|
}
|
|
1807
1964
|
}
|
|
1808
|
-
|
|
1965
|
+
function resolveLocalMcpRuntimeOverride(env) {
|
|
1966
|
+
const override = resolveRuntimeSpecOverride(env);
|
|
1967
|
+
if (!overrideApplies(override, ["mcp", "sdk"])) return void 0;
|
|
1968
|
+
const { mcp, sdk } = override;
|
|
1969
|
+
return { ...mcp !== void 0 ? { mcp } : {}, ...sdk !== void 0 ? { sdk } : {} };
|
|
1970
|
+
}
|
|
1971
|
+
async function installRuntimePackages2(runtimeDirectory, npmCacheDirectory, packageSpecs, deps) {
|
|
1809
1972
|
const { runCommand, onProgress } = deps;
|
|
1810
1973
|
const baseArgs = [
|
|
1811
1974
|
"install",
|
|
@@ -1815,8 +1978,7 @@ async function installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps
|
|
|
1815
1978
|
"--no-fund",
|
|
1816
1979
|
"--omit=dev",
|
|
1817
1980
|
"--prefer-offline",
|
|
1818
|
-
|
|
1819
|
-
sdkPackageSpec()
|
|
1981
|
+
...packageSpecs
|
|
1820
1982
|
];
|
|
1821
1983
|
const run = async (args) => {
|
|
1822
1984
|
const startedAt = Date.now();
|
|
@@ -1838,22 +2000,26 @@ async function installRuntimePackages2(runtimeDirectory, npmCacheDirectory, deps
|
|
|
1838
2000
|
try {
|
|
1839
2001
|
await run([...baseArgs, "--cache", npmCacheDirectory]);
|
|
1840
2002
|
} catch (err) {
|
|
1841
|
-
throw new Error(`Could not install local Haven MCP runtime ${
|
|
2003
|
+
throw new Error(`Could not install local Haven MCP runtime ${packageSpecs.join(" ")}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1842
2004
|
}
|
|
1843
2005
|
}
|
|
1844
2006
|
}
|
|
1845
2007
|
async function installedRuntimeMatches2(runtimeDirectory, cliPath) {
|
|
1846
2008
|
try {
|
|
1847
2009
|
await assertFileExists2(cliPath, "local Haven MCP CLI");
|
|
1848
|
-
const
|
|
1849
|
-
|
|
1850
|
-
readPackageJson2(join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
|
|
1851
|
-
]);
|
|
1852
|
-
return mcpPackage.version === MCP_RUNTIME_MANIFEST.mcpVersion && sdkPackage.version === MCP_RUNTIME_MANIFEST.sdkVersion;
|
|
2010
|
+
const installed = await readInstalledVersions2(runtimeDirectory);
|
|
2011
|
+
return installed.mcpVersion === MCP_RUNTIME_MANIFEST.mcpVersion && installed.sdkVersion === MCP_RUNTIME_MANIFEST.sdkVersion;
|
|
1853
2012
|
} catch {
|
|
1854
2013
|
return false;
|
|
1855
2014
|
}
|
|
1856
2015
|
}
|
|
2016
|
+
async function readInstalledVersions2(runtimeDirectory) {
|
|
2017
|
+
const [mcpPackage, sdkPackage] = await Promise.all([
|
|
2018
|
+
readPackageJson2(join(runtimeDirectory, "node_modules", "@haven_ai", "mcp", "package.json")),
|
|
2019
|
+
readPackageJson2(join(runtimeDirectory, "node_modules", "@haven_ai", "sdk", "package.json"))
|
|
2020
|
+
]);
|
|
2021
|
+
return { mcpVersion: mcpPackage.version ?? "", sdkVersion: sdkPackage.version ?? "" };
|
|
2022
|
+
}
|
|
1857
2023
|
async function readPackageJson2(path) {
|
|
1858
2024
|
return JSON.parse(await readFile(path, "utf8"));
|
|
1859
2025
|
}
|
|
@@ -1862,6 +2028,7 @@ async function writeWrapper2(input) {
|
|
|
1862
2028
|
await chmod(dirname(input.wrapperPath), 448).catch(() => void 0);
|
|
1863
2029
|
const source = [
|
|
1864
2030
|
"#!/usr/bin/env node",
|
|
2031
|
+
...input.overrideComment ? [`// HAVEN RUNTIME SPEC OVERRIDE (#2424): ${input.overrideComment} \u2014 this wrapper launches a NON-pinned MCP build.`] : [],
|
|
1865
2032
|
"import { spawn } from 'node:child_process'",
|
|
1866
2033
|
"",
|
|
1867
2034
|
`const cliPath = ${JSON.stringify(input.cliPath)}`,
|
|
@@ -1885,14 +2052,15 @@ async function writeRuntimeSidecar2(input) {
|
|
|
1885
2052
|
const value = {
|
|
1886
2053
|
...input.serverName ? { server_name: input.serverName } : {},
|
|
1887
2054
|
mcp_package: MCP_RUNTIME_MANIFEST.mcpPackage,
|
|
1888
|
-
mcp_version: MCP_RUNTIME_MANIFEST.mcpVersion,
|
|
2055
|
+
mcp_version: input.installedVersions?.mcpVersion ?? MCP_RUNTIME_MANIFEST.mcpVersion,
|
|
1889
2056
|
sdk_package: MCP_RUNTIME_MANIFEST.sdkPackage,
|
|
1890
|
-
sdk_version: MCP_RUNTIME_MANIFEST.sdkVersion,
|
|
2057
|
+
sdk_version: input.installedVersions?.sdkVersion ?? MCP_RUNTIME_MANIFEST.sdkVersion,
|
|
1891
2058
|
minimum_node_version: MCP_RUNTIME_MANIFEST.minimumNodeVersion,
|
|
1892
2059
|
wrapper_path: input.wrapperPath,
|
|
1893
2060
|
runtime_directory: input.runtimeDirectory,
|
|
1894
2061
|
npm_cache_directory: input.npmCacheDirectory,
|
|
1895
|
-
cli_path: input.cliPath
|
|
2062
|
+
cli_path: input.cliPath,
|
|
2063
|
+
...input.override ? { runtime_spec_override: input.override } : {}
|
|
1896
2064
|
};
|
|
1897
2065
|
await writeFile(input.path, `${JSON.stringify(value, null, 2)}
|
|
1898
2066
|
`, { mode: 384 });
|
|
@@ -1910,6 +2078,7 @@ var init_local_mcp_runtime = __esm({
|
|
|
1910
2078
|
"src/local-mcp-runtime.ts"() {
|
|
1911
2079
|
init_signer_runtime();
|
|
1912
2080
|
init_runtime_manifest();
|
|
2081
|
+
init_runtime_spec_override();
|
|
1913
2082
|
execFileAsync2 = promisify(execFile);
|
|
1914
2083
|
UnsupportedNodeVersionError = class extends Error {
|
|
1915
2084
|
code = "local_mcp_unsupported_node_version";
|
|
@@ -2410,7 +2579,7 @@ async function installRuntime(input, deps = {}) {
|
|
|
2410
2579
|
localMcpConfigured: false,
|
|
2411
2580
|
probeResult: "signer_runtime_install_failed",
|
|
2412
2581
|
restartRequired: false,
|
|
2413
|
-
nextUserAction:
|
|
2582
|
+
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: ${connectorRerunCommand()}`,
|
|
2414
2583
|
errorCode: "signer_runtime_install_failed",
|
|
2415
2584
|
configTarget: profile.label,
|
|
2416
2585
|
signerAcknowledged: signerConsent?.acknowledged,
|
|
@@ -2421,7 +2590,7 @@ async function installRuntime(input, deps = {}) {
|
|
|
2421
2590
|
...consentMessages,
|
|
2422
2591
|
`Could not pre-install the local Haven signer: ${err instanceof Error ? err.message : String(err)}`,
|
|
2423
2592
|
"No runtime configuration was written (fail-closed): a config pointing at an uninstalled signer looks wired but cannot start.",
|
|
2424
|
-
|
|
2593
|
+
`Re-run \`${connectorRerunCommand()}\` to retry the setup.`
|
|
2425
2594
|
]
|
|
2426
2595
|
};
|
|
2427
2596
|
}
|
|
@@ -2484,7 +2653,7 @@ async function installRuntime(input, deps = {}) {
|
|
|
2484
2653
|
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."] : [];
|
|
2485
2654
|
const signerProbeMessages = signerProbe ? signerProbe.status === "ok" ? ["Verified local Haven signer with a stdio handshake."] : [
|
|
2486
2655
|
`Local Haven signer handshake failed: ${signerProbe.status}.`,
|
|
2487
|
-
|
|
2656
|
+
`Re-run \`${connectorRerunCommand()}\` to repair the signer setup.`
|
|
2488
2657
|
] : [];
|
|
2489
2658
|
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."] : [];
|
|
2490
2659
|
const skillInstall = !configResult.errorCode ? await installSkillForRuntime(runtime, { homeDir: deps.homeDir, env: deps.env }) : void 0;
|
|
@@ -2555,7 +2724,7 @@ async function configureClaudeCode(deps, localMcpCommand, serverName) {
|
|
|
2555
2724
|
restartRequired: true,
|
|
2556
2725
|
messages: [
|
|
2557
2726
|
`Could not update Claude Code MCP config: ${err instanceof Error ? err.message : String(err)}`,
|
|
2558
|
-
"Install Claude Code or rerun the Haven
|
|
2727
|
+
"Install Claude Code or rerun the Haven connector command inside a Claude Code terminal."
|
|
2559
2728
|
],
|
|
2560
2729
|
errorCode: "claude_code_config_failed"
|
|
2561
2730
|
};
|
|
@@ -2622,7 +2791,7 @@ async function configureClaudeCodeHosted(deps, input, signerCommand) {
|
|
|
2622
2791
|
restartRequired: true,
|
|
2623
2792
|
messages: [
|
|
2624
2793
|
`Could not update Claude Code MCP config: ${err instanceof Error ? err.message : String(err)}`,
|
|
2625
|
-
"Install Claude Code or rerun the Haven
|
|
2794
|
+
"Install Claude Code or rerun the Haven connector command inside a Claude Code terminal."
|
|
2626
2795
|
],
|
|
2627
2796
|
errorCode: "claude_code_config_failed"
|
|
2628
2797
|
};
|
|
@@ -2696,7 +2865,7 @@ function supportsLocalMcp(runtime) {
|
|
|
2696
2865
|
return runtime === "codex-cli" || runtime === "codex-desktop" || runtime === "claude-code";
|
|
2697
2866
|
}
|
|
2698
2867
|
async function prepareRuntimeForLocalMcp(input, deps) {
|
|
2699
|
-
const prepare = deps.prepareLocalMcpRuntime ?? ((runtimeInput) => prepareLocalMcpRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress }));
|
|
2868
|
+
const prepare = deps.prepareLocalMcpRuntime ?? ((runtimeInput) => prepareLocalMcpRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress, env: deps.env }));
|
|
2700
2869
|
return prepare({
|
|
2701
2870
|
credentialDirectory: input.credentialDirectory,
|
|
2702
2871
|
identityPath: input.identityPath,
|
|
@@ -2711,7 +2880,7 @@ async function prepareSignerForRuntime(input, deps) {
|
|
|
2711
2880
|
// install heartbeat was dead code in production and the console still
|
|
2712
2881
|
// went silent for the whole cold install — the exact symptom the issue
|
|
2713
2882
|
// set out to remove, at a longer timeout.
|
|
2714
|
-
prepareSignerRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress })
|
|
2883
|
+
prepareSignerRuntime(runtimeInput, { runCommand: deps.runCommand, onProgress: deps.onProgress, env: deps.env })
|
|
2715
2884
|
));
|
|
2716
2885
|
return prepare({
|
|
2717
2886
|
credentialDirectory: input.credentialDirectory,
|
|
@@ -2751,14 +2920,6 @@ var init_runtime_install = __esm({
|
|
|
2751
2920
|
}
|
|
2752
2921
|
});
|
|
2753
2922
|
|
|
2754
|
-
// src/rekey-messages.ts
|
|
2755
|
-
var REKEY_FINISH_NEEDS_API_KEY;
|
|
2756
|
-
var init_rekey_messages = __esm({
|
|
2757
|
-
"src/rekey-messages.ts"() {
|
|
2758
|
-
REKEY_FINISH_NEEDS_API_KEY = "--rekey-finish needs --api-key <key> \u2014 the one the Haven agent page showed once.";
|
|
2759
|
-
}
|
|
2760
|
-
});
|
|
2761
|
-
|
|
2762
2923
|
// src/tombstone.ts
|
|
2763
2924
|
var tombstone_exports = {};
|
|
2764
2925
|
__export(tombstone_exports, {
|
|
@@ -2810,7 +2971,7 @@ function tombstoneScript(info) {
|
|
|
2810
2971
|
"one of them: each holds the snapshot from its own start time, so after",
|
|
2811
2972
|
"a chain of recreations each can be parked on a DIFFERENT old agent.",
|
|
2812
2973
|
"",
|
|
2813
|
-
|
|
2974
|
+
`Then verify with: ${connectorRerunCommand("--doctor --runtime <runtime>")}`
|
|
2814
2975
|
];
|
|
2815
2976
|
return [
|
|
2816
2977
|
"#!/usr/bin/env node",
|
|
@@ -2876,6 +3037,9 @@ var init_tombstone = __esm({
|
|
|
2876
3037
|
// src/unwire.ts
|
|
2877
3038
|
var unwire_exports = {};
|
|
2878
3039
|
__export(unwire_exports, {
|
|
3040
|
+
readIdentityFile: () => readIdentityFile,
|
|
3041
|
+
teardownLocalKeyMaterial: () => teardownLocalKeyMaterial,
|
|
3042
|
+
tombstoneDirectoryIfAbsent: () => tombstoneDirectoryIfAbsent,
|
|
2879
3043
|
unwireAgent: () => unwireAgent
|
|
2880
3044
|
});
|
|
2881
3045
|
function identityAt(directory) {
|
|
@@ -2915,18 +3079,13 @@ async function unwireAgent(input) {
|
|
|
2915
3079
|
const slug = input.slug ?? sidecar?.server_name;
|
|
2916
3080
|
const names = serverNamesFor(slug);
|
|
2917
3081
|
const runtimes = [];
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
replacedBy: input.replacedBy,
|
|
2926
|
-
tombstonesDir: input.tombstonesDir
|
|
2927
|
-
});
|
|
2928
|
-
tombstoned = true;
|
|
2929
|
-
}
|
|
3082
|
+
const tombstoned = await tombstoneDirectoryIfAbsent({
|
|
3083
|
+
directory: input.directory,
|
|
3084
|
+
agentId,
|
|
3085
|
+
reason: input.reason ?? "unwired via --unwire",
|
|
3086
|
+
replacedBy: input.replacedBy,
|
|
3087
|
+
tombstonesDir: input.tombstonesDir
|
|
3088
|
+
});
|
|
2930
3089
|
for (const model of RUNTIMES) {
|
|
2931
3090
|
const path = runtimeConfigPathFor(model.runtime, homeDir);
|
|
2932
3091
|
if (path === null) continue;
|
|
@@ -3011,16 +3170,27 @@ async function unwireAgent(input) {
|
|
|
3011
3170
|
}
|
|
3012
3171
|
}
|
|
3013
3172
|
}
|
|
3173
|
+
await teardownLocalKeyMaterial(input.directory, identity);
|
|
3174
|
+
return { directory: input.directory, agentId, slug, tombstoned, runtimes };
|
|
3175
|
+
}
|
|
3176
|
+
async function tombstoneDirectoryIfAbsent(input) {
|
|
3177
|
+
if (await readOptionalText(join(input.directory, TOMBSTONE_FILENAME)) !== null) return false;
|
|
3178
|
+
await writeAgentTombstone(input);
|
|
3179
|
+
return true;
|
|
3180
|
+
}
|
|
3181
|
+
async function teardownLocalKeyMaterial(directory, identity) {
|
|
3014
3182
|
await Promise.all([
|
|
3015
|
-
rm(join(
|
|
3016
|
-
rm(join(
|
|
3183
|
+
rm(join(directory, "signer.json"), { force: true }),
|
|
3184
|
+
rm(join(directory, REKEY_PENDING_FILENAME), { force: true })
|
|
3017
3185
|
]);
|
|
3018
3186
|
if (identity && identity.api_key !== void 0) {
|
|
3019
3187
|
const { api_key: _dropped, ...rest } = identity;
|
|
3020
|
-
await writeFile(join(
|
|
3188
|
+
await writeFile(join(directory, "identity.json"), `${JSON.stringify(rest, null, 2)}
|
|
3021
3189
|
`, { mode: 384 });
|
|
3022
3190
|
}
|
|
3023
|
-
|
|
3191
|
+
}
|
|
3192
|
+
async function readIdentityFile(directory) {
|
|
3193
|
+
return identityAt(directory);
|
|
3024
3194
|
}
|
|
3025
3195
|
var RUNTIMES;
|
|
3026
3196
|
var init_unwire = __esm({
|
|
@@ -3041,6 +3211,14 @@ var init_unwire = __esm({
|
|
|
3041
3211
|
}
|
|
3042
3212
|
});
|
|
3043
3213
|
|
|
3214
|
+
// src/rekey-messages.ts
|
|
3215
|
+
var REKEY_FINISH_NEEDS_API_KEY;
|
|
3216
|
+
var init_rekey_messages = __esm({
|
|
3217
|
+
"src/rekey-messages.ts"() {
|
|
3218
|
+
REKEY_FINISH_NEEDS_API_KEY = "--rekey-finish needs --api-key <key> \u2014 the one the Haven agent page showed once.";
|
|
3219
|
+
}
|
|
3220
|
+
});
|
|
3221
|
+
|
|
3044
3222
|
// src/rekey.ts
|
|
3045
3223
|
var rekey_exports = {};
|
|
3046
3224
|
__export(rekey_exports, {
|
|
@@ -3068,7 +3246,7 @@ async function startRekey(options, deps = {}) {
|
|
|
3068
3246
|
expires_at: expiresAt
|
|
3069
3247
|
});
|
|
3070
3248
|
const finishCommand = [
|
|
3071
|
-
"
|
|
3249
|
+
connectorRerunCommand("--rekey-finish"),
|
|
3072
3250
|
options.serverName ? `--name ${options.serverName}` : void 0,
|
|
3073
3251
|
"--api-key <the key the dashboard showed you>",
|
|
3074
3252
|
options.runtime ? `--runtime ${options.runtime}` : "--runtime <your runtime>"
|
|
@@ -3159,7 +3337,7 @@ async function finishRekey(options, deps = {}) {
|
|
|
3159
3337
|
signerPath: `${stored.directory}/signer.json`,
|
|
3160
3338
|
homeDir: options.homeDir
|
|
3161
3339
|
},
|
|
3162
|
-
{ runCommand: deps.runCommand }
|
|
3340
|
+
{ runCommand: deps.runCommand, env: deps.env }
|
|
3163
3341
|
);
|
|
3164
3342
|
const result = await (deps.writeConfig ?? writeHostedRuntimeConfig)(
|
|
3165
3343
|
{ runCommand: deps.runCommand, homeDir: options.homeDir },
|
|
@@ -3175,6 +3353,11 @@ async function finishRekey(options, deps = {}) {
|
|
|
3175
3353
|
{ command: prepared.command, args: prepared.args }
|
|
3176
3354
|
);
|
|
3177
3355
|
configRewritten = result.hostedConfigured;
|
|
3356
|
+
if (prepared.runtimeSpecOverride) {
|
|
3357
|
+
messages.push(
|
|
3358
|
+
` Signer runtime: RUNTIME SPEC OVERRIDE ACTIVE \u2014 ${describeRuntimeSpecOverride(prepared.runtimeSpecOverride.specs)} (NOT the pinned manifest; installed into ${prepared.runtimeDirectory})`
|
|
3359
|
+
);
|
|
3360
|
+
}
|
|
3178
3361
|
messages.push(` Config: ${result.target}`);
|
|
3179
3362
|
messages.push(...result.messages.map((line) => ` ${line}`));
|
|
3180
3363
|
if (!configRewritten) {
|
|
@@ -3241,6 +3424,7 @@ var init_rekey = __esm({
|
|
|
3241
3424
|
init_api();
|
|
3242
3425
|
init_runtime_install();
|
|
3243
3426
|
init_signer_runtime();
|
|
3427
|
+
init_runtime_spec_override();
|
|
3244
3428
|
init_key();
|
|
3245
3429
|
init_redact();
|
|
3246
3430
|
init_rekey_messages();
|
|
@@ -3417,6 +3601,46 @@ function rekeyPendingCheck(status, hostedDelegateAddress, runtime, slug) {
|
|
|
3417
3601
|
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
|
|
3418
3602
|
};
|
|
3419
3603
|
}
|
|
3604
|
+
async function runtimeSpecOverrideCheck(directory, sidecar, env) {
|
|
3605
|
+
const facts = [];
|
|
3606
|
+
const installed = sidecar?.runtime_spec_override;
|
|
3607
|
+
if (installed) {
|
|
3608
|
+
facts.push(
|
|
3609
|
+
`signer runtime installed under ${describeRuntimeSpecOverride(installed.specs)} (resolved: ${installed.resolved_specs.join(" ")}; directory key ${installed.directory_key})`
|
|
3610
|
+
);
|
|
3611
|
+
}
|
|
3612
|
+
const mcpInstalled = await readMcpSidecarOverride(directory);
|
|
3613
|
+
if (mcpInstalled) {
|
|
3614
|
+
facts.push(
|
|
3615
|
+
`local MCP runtime installed under ${describeRuntimeSpecOverride(mcpInstalled.specs)} (resolved: ${mcpInstalled.resolved_specs.join(" ")}; directory key ${mcpInstalled.directory_key})`
|
|
3616
|
+
);
|
|
3617
|
+
}
|
|
3618
|
+
let shell;
|
|
3619
|
+
try {
|
|
3620
|
+
const active = resolveRuntimeSpecOverride(env);
|
|
3621
|
+
if (active) shell = `set in this shell: ${describeRuntimeSpecOverride(active)} \u2014 a --repair from here installs these, not the pin`;
|
|
3622
|
+
} catch (err) {
|
|
3623
|
+
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)}`;
|
|
3624
|
+
}
|
|
3625
|
+
if (shell) facts.push(shell);
|
|
3626
|
+
if (facts.length === 0) return void 0;
|
|
3627
|
+
const variables = Object.values(RUNTIME_SPEC_ENV).join(" / ");
|
|
3628
|
+
return {
|
|
3629
|
+
id: "runtime_spec_override",
|
|
3630
|
+
label: "Runtime spec override",
|
|
3631
|
+
ok: false,
|
|
3632
|
+
detail: `runtime spec overridden \u2014 not the pinned manifest (${MCP_RUNTIME_MANIFEST.signerPackage}@${MCP_RUNTIME_MANIFEST.signerVersion}, ${MCP_RUNTIME_MANIFEST.sdkPackage}@${MCP_RUNTIME_MANIFEST.sdkVersion}). ${facts.join(". ")}.`,
|
|
3633
|
+
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.`
|
|
3634
|
+
};
|
|
3635
|
+
}
|
|
3636
|
+
async function readMcpSidecarOverride(directory) {
|
|
3637
|
+
try {
|
|
3638
|
+
const parsed = JSON.parse(await readFile(join(directory, "mcp-runtime.json"), "utf8"));
|
|
3639
|
+
return parsed.runtime_spec_override;
|
|
3640
|
+
} catch {
|
|
3641
|
+
return void 0;
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3420
3644
|
async function readIdentity(directory) {
|
|
3421
3645
|
try {
|
|
3422
3646
|
return JSON.parse(await readFile(join(directory, "identity.json"), "utf8"));
|
|
@@ -3451,6 +3675,18 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3451
3675
|
detail: "No signer-runtime.json sidecar \u2014 the pinned signer runtime was never prepared (or a pre-#1586 npx config).",
|
|
3452
3676
|
repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}`
|
|
3453
3677
|
});
|
|
3678
|
+
} else if (sidecar.runtime_spec_override) {
|
|
3679
|
+
const matches = await installedRuntimeMatchesVersions(sidecar.runtime_directory, sidecar.cli_path, {
|
|
3680
|
+
signerVersion: sidecar.signer_version,
|
|
3681
|
+
sdkVersion: sidecar.sdk_version
|
|
3682
|
+
});
|
|
3683
|
+
checks.push({
|
|
3684
|
+
id: "signer_runtime",
|
|
3685
|
+
label: "Signer runtime (preinstalled wrapper)",
|
|
3686
|
+
ok: matches,
|
|
3687
|
+
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.`,
|
|
3688
|
+
...matches ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime} with the same HAVEN_*_SPEC variables set.` }
|
|
3689
|
+
});
|
|
3454
3690
|
} else {
|
|
3455
3691
|
const matches = await installedRuntimeMatches(sidecar.runtime_directory, sidecar.cli_path);
|
|
3456
3692
|
const versionOk = sidecar.signer_version === MCP_RUNTIME_MANIFEST.signerVersion;
|
|
@@ -3463,6 +3699,8 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3463
3699
|
...ok ? {} : { repair: `Run: ${RERUN} --doctor --repair --runtime ${input.runtime}` }
|
|
3464
3700
|
});
|
|
3465
3701
|
}
|
|
3702
|
+
const overrideCheck = await runtimeSpecOverrideCheck(directory, sidecar, deps.env ?? process.env);
|
|
3703
|
+
if (overrideCheck) checks.push(overrideCheck);
|
|
3466
3704
|
const hostedUrl = identity?.hosted_mcp_url ?? (identity?.api_url ? `${identity.api_url}/mcp` : void 0);
|
|
3467
3705
|
if (identity?.api_key && hostedUrl) {
|
|
3468
3706
|
const probe = await (deps.probeHosted ?? probeHostedMcpTools)(identity.api_key, hostedUrl, deps.fetch);
|
|
@@ -3534,7 +3772,7 @@ async function checksForAgent(entry, input, deps) {
|
|
|
3534
3772
|
label: "Signer stdio handshake",
|
|
3535
3773
|
ok: false,
|
|
3536
3774
|
detail: "The local-tools consent is not acknowledged, so the signer refuses to start (by design).",
|
|
3537
|
-
repair: `Run: ${RERUN} --ack-local-tools --setup <token> (or re-run your original
|
|
3775
|
+
repair: `Run: ${RERUN} --ack-local-tools --setup <token> (or re-run your original connector command with --ack-local-tools).`
|
|
3538
3776
|
});
|
|
3539
3777
|
} else {
|
|
3540
3778
|
const probe = await (deps.probeSignerTools ?? probeLocalMcpTools)(
|
|
@@ -3657,7 +3895,7 @@ async function runDoctor(input, deps = {}) {
|
|
|
3657
3895
|
signerCapabilities = result.signerCapabilities;
|
|
3658
3896
|
for (const check of result.checks) primaryChecksById.set(check.id, check);
|
|
3659
3897
|
}
|
|
3660
|
-
for (const id of ["credentials", "signer_runtime"]) {
|
|
3898
|
+
for (const id of ["credentials", "signer_runtime", "runtime_spec_override"]) {
|
|
3661
3899
|
const check = primaryChecksById.get(id);
|
|
3662
3900
|
if (check) checks.push(check);
|
|
3663
3901
|
}
|
|
@@ -3808,7 +4046,7 @@ async function runRepair(input, deps = {}) {
|
|
|
3808
4046
|
ok: false,
|
|
3809
4047
|
messages: [
|
|
3810
4048
|
`The config at ${configPath} is the LOCAL-stdio topology (--local). Repair currently rewrites only the hosted+signer shape and will not touch it.`,
|
|
3811
|
-
"Re-run your original
|
|
4049
|
+
"Re-run your original connector command (with --local) to repair a local-stdio install."
|
|
3812
4050
|
]
|
|
3813
4051
|
};
|
|
3814
4052
|
}
|
|
@@ -3820,7 +4058,7 @@ async function runRepair(input, deps = {}) {
|
|
|
3820
4058
|
const signerPath = join(directory, "signer.json");
|
|
3821
4059
|
const prepared = await prepareSignerRuntime(
|
|
3822
4060
|
{ credentialDirectory: directory, signerPath, homeDir, serverName },
|
|
3823
|
-
{ runCommand: deps.runCommand }
|
|
4061
|
+
{ runCommand: deps.runCommand, env: deps.env }
|
|
3824
4062
|
);
|
|
3825
4063
|
messages.push(...prepared.messages);
|
|
3826
4064
|
const names = serverNamesFor(serverName);
|
|
@@ -3847,6 +4085,7 @@ var init_doctor = __esm({
|
|
|
3847
4085
|
init_runtime_manifest();
|
|
3848
4086
|
init_probes();
|
|
3849
4087
|
init_signer_runtime();
|
|
4088
|
+
init_runtime_spec_override();
|
|
3850
4089
|
init_config_writers();
|
|
3851
4090
|
init_runtime_registry();
|
|
3852
4091
|
init_signer_consent();
|
|
@@ -3854,7 +4093,7 @@ var init_doctor = __esm({
|
|
|
3854
4093
|
init_server_names();
|
|
3855
4094
|
init_storage();
|
|
3856
4095
|
init_redact();
|
|
3857
|
-
RERUN =
|
|
4096
|
+
RERUN = connectorRerunCommand();
|
|
3858
4097
|
}
|
|
3859
4098
|
});
|
|
3860
4099
|
|
|
@@ -4013,7 +4252,7 @@ function noInstalledClientsError() {
|
|
|
4013
4252
|
function promptAbortedError(reason) {
|
|
4014
4253
|
return new ConnectError(
|
|
4015
4254
|
"runtime_prompt_aborted",
|
|
4016
|
-
`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
|
|
4255
|
+
`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.`,
|
|
4017
4256
|
"rerun_connect_and_choose_a_runtime"
|
|
4018
4257
|
);
|
|
4019
4258
|
}
|
|
@@ -4044,10 +4283,119 @@ function defaultPromptIo() {
|
|
|
4044
4283
|
};
|
|
4045
4284
|
}
|
|
4046
4285
|
|
|
4286
|
+
// src/wiring-collision.ts
|
|
4287
|
+
init_server_names();
|
|
4288
|
+
init_signer_runtime();
|
|
4289
|
+
init_storage();
|
|
4290
|
+
init_tombstone();
|
|
4291
|
+
async function detectWiringCollision(input) {
|
|
4292
|
+
const root = defaultCredentialRoot(input.credentialsDir);
|
|
4293
|
+
let entries = [];
|
|
4294
|
+
try {
|
|
4295
|
+
entries = await readdir(root);
|
|
4296
|
+
} catch {
|
|
4297
|
+
return null;
|
|
4298
|
+
}
|
|
4299
|
+
const superseded = [];
|
|
4300
|
+
const taken = /* @__PURE__ */ new Set();
|
|
4301
|
+
for (const entry of entries) {
|
|
4302
|
+
const directory = join(root, entry);
|
|
4303
|
+
let identityRaw;
|
|
4304
|
+
try {
|
|
4305
|
+
identityRaw = await readFile(join(directory, "identity.json"), "utf8");
|
|
4306
|
+
} catch {
|
|
4307
|
+
continue;
|
|
4308
|
+
}
|
|
4309
|
+
taken.add(entry);
|
|
4310
|
+
let identity;
|
|
4311
|
+
try {
|
|
4312
|
+
identity = JSON.parse(identityRaw);
|
|
4313
|
+
} catch {
|
|
4314
|
+
identity = void 0;
|
|
4315
|
+
}
|
|
4316
|
+
if (!identity?.api_key) continue;
|
|
4317
|
+
if (await pathExists2(join(directory, TOMBSTONE_FILENAME))) continue;
|
|
4318
|
+
const sidecar = await readRuntimeSidecar(directory);
|
|
4319
|
+
if (sidecar?.server_name) continue;
|
|
4320
|
+
superseded.push({ directory, agentId: identity.agent_id ?? entry });
|
|
4321
|
+
}
|
|
4322
|
+
if (superseded.length === 0) return null;
|
|
4323
|
+
return {
|
|
4324
|
+
superseded,
|
|
4325
|
+
suggestedServerName: proposeServerSlug(input.agentName, taken)
|
|
4326
|
+
};
|
|
4327
|
+
}
|
|
4328
|
+
function proposeServerSlug(agentName, taken) {
|
|
4329
|
+
let base = agentName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, 32).replace(/-+$/g, "");
|
|
4330
|
+
if (!slugIsValid(base)) base = "agent";
|
|
4331
|
+
if (!taken.has(base)) return base;
|
|
4332
|
+
for (let n = 2; n < 1e3; n += 1) {
|
|
4333
|
+
const suffix = `-${n}`;
|
|
4334
|
+
const candidate = `${base.slice(0, 32 - suffix.length).replace(/-+$/g, "")}${suffix}`;
|
|
4335
|
+
if (slugIsValid(candidate) && !taken.has(candidate)) return candidate;
|
|
4336
|
+
}
|
|
4337
|
+
throw new Error(`Could not propose an unused server name for ${JSON.stringify(agentName)}.`);
|
|
4338
|
+
}
|
|
4339
|
+
function slugIsValid(slug) {
|
|
4340
|
+
try {
|
|
4341
|
+
assertValidServerSlug(slug);
|
|
4342
|
+
return true;
|
|
4343
|
+
} catch {
|
|
4344
|
+
return false;
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4347
|
+
var MAX_PROMPT_ATTEMPTS2 = 3;
|
|
4348
|
+
async function promptWiringCollisionResolution(collision, agentName, io) {
|
|
4349
|
+
const ids = collision.superseded.map((entry) => entry.agentId).join(", ");
|
|
4350
|
+
io.write(`This machine is already wired to a Haven agent with a live key: ${ids}.
|
|
4351
|
+
`);
|
|
4352
|
+
io.write(`Setting up "${agentName}" on the bare haven / haven-signer pair would replace that wiring.
|
|
4353
|
+
`);
|
|
4354
|
+
io.write(" r) replace \u2014 re-point haven / haven-signer at the new agent and retire the previous directory locally\n");
|
|
4355
|
+
io.write(" (tombstoned, local key files removed; you still revoke it on the Haven agent page)\n");
|
|
4356
|
+
io.write(` a) alongside \u2014 install as a named agent (suggested: ${collision.suggestedServerName}) with its own
|
|
4357
|
+
`);
|
|
4358
|
+
io.write(` haven-<name> / haven-signer-<name> pair, leaving the current wiring untouched
|
|
4359
|
+
`);
|
|
4360
|
+
io.write(" q) quit \u2014 nothing is written and the setup token stays unused\n");
|
|
4361
|
+
for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS2; attempt += 1) {
|
|
4362
|
+
const answer = await io.question("Replace, install alongside, or quit? [r/a/q]: ");
|
|
4363
|
+
if (answer === null) return { action: "abort" };
|
|
4364
|
+
const trimmed = answer.trim().toLowerCase();
|
|
4365
|
+
if (trimmed === "r" || trimmed === "replace") return { action: "replace" };
|
|
4366
|
+
if (trimmed === "q" || trimmed === "quit") return { action: "abort" };
|
|
4367
|
+
if (trimmed === "a" || trimmed === "alongside") {
|
|
4368
|
+
const typed = await io.question(`Server name [${collision.suggestedServerName}]: `);
|
|
4369
|
+
if (typed === null) return { action: "abort" };
|
|
4370
|
+
const serverName = typed.trim() === "" ? collision.suggestedServerName : typed.trim();
|
|
4371
|
+
try {
|
|
4372
|
+
assertValidServerSlug(serverName);
|
|
4373
|
+
} catch (err) {
|
|
4374
|
+
io.write(`${err instanceof Error ? err.message : String(err)}
|
|
4375
|
+
`);
|
|
4376
|
+
continue;
|
|
4377
|
+
}
|
|
4378
|
+
return { action: "alongside", serverName };
|
|
4379
|
+
}
|
|
4380
|
+
io.write(`"${trimmed}" is not one of r, a, q.
|
|
4381
|
+
`);
|
|
4382
|
+
}
|
|
4383
|
+
return { action: "abort" };
|
|
4384
|
+
}
|
|
4385
|
+
async function pathExists2(path) {
|
|
4386
|
+
try {
|
|
4387
|
+
await stat(path);
|
|
4388
|
+
return true;
|
|
4389
|
+
} catch {
|
|
4390
|
+
return false;
|
|
4391
|
+
}
|
|
4392
|
+
}
|
|
4393
|
+
|
|
4047
4394
|
// src/runtime.ts
|
|
4395
|
+
init_unwire();
|
|
4048
4396
|
init_local_mcp_runtime();
|
|
4049
4397
|
init_runtime_manifest();
|
|
4050
|
-
var CONNECTOR_VERSION = "0.1.
|
|
4398
|
+
var CONNECTOR_VERSION = "0.1.35-alpha.0";
|
|
4051
4399
|
var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
|
|
4052
4400
|
var failureOutcomesByError = /* @__PURE__ */ new WeakMap();
|
|
4053
4401
|
function failureOutcomeFor(runtimeHint, error) {
|
|
@@ -4152,6 +4500,28 @@ async function executeConnect(options, deps, trace) {
|
|
|
4152
4500
|
await assertServerSlugAvailable(options.serverName, options.credentialsDir);
|
|
4153
4501
|
}
|
|
4154
4502
|
log("Checked local credential storage \u2014 all clear.");
|
|
4503
|
+
let serverName = options.serverName;
|
|
4504
|
+
let replacing;
|
|
4505
|
+
if (!serverName) {
|
|
4506
|
+
const collision = await detectWiringCollision({
|
|
4507
|
+
credentialsDir: options.credentialsDir,
|
|
4508
|
+
agentName: setup.agent.name
|
|
4509
|
+
});
|
|
4510
|
+
if (collision) {
|
|
4511
|
+
const resolution = await resolveWiringCollision(collision, setup.agent.name, options, deps);
|
|
4512
|
+
if (resolution.action === "replace") {
|
|
4513
|
+
replacing = collision;
|
|
4514
|
+
log(
|
|
4515
|
+
`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.`
|
|
4516
|
+
);
|
|
4517
|
+
} else {
|
|
4518
|
+
serverName = resolution.serverName;
|
|
4519
|
+
assertValidServerSlug(serverName);
|
|
4520
|
+
await assertServerSlugAvailable(serverName, options.credentialsDir);
|
|
4521
|
+
log(`Installing alongside the existing wiring as a named agent: ${serverNamesFor(serverName).hosted} / ${serverNamesFor(serverName).signer}.`);
|
|
4522
|
+
}
|
|
4523
|
+
}
|
|
4524
|
+
}
|
|
4155
4525
|
const localKey = generateKey();
|
|
4156
4526
|
const localApiKey = generateLocalApiKey();
|
|
4157
4527
|
log("Minting a fresh signing key and API key \u2014 both stay on this machine.");
|
|
@@ -4172,7 +4542,10 @@ async function executeConnect(options, deps, trace) {
|
|
|
4172
4542
|
// the dashboard can name it. Derived here rather than sent as the raw
|
|
4173
4543
|
// slug — `serverNamesFor` is the one place the naming rule lives, and
|
|
4174
4544
|
// the hosted name is what a user pastes into an MCP config.
|
|
4175
|
-
mcpServerName: serverNamesFor(
|
|
4545
|
+
mcpServerName: serverNamesFor(serverName).hosted,
|
|
4546
|
+
// #2528: 'prose' is the default because it is what a caller who says
|
|
4547
|
+
// nothing is doing — the library entry point is not a --json run.
|
|
4548
|
+
runMode: options.runMode ?? "prose",
|
|
4176
4549
|
connectorContext: {
|
|
4177
4550
|
environment_label: options.environmentLabel ?? "Local workspace",
|
|
4178
4551
|
config_target: installCapabilities.canWriteRuntimeConfig ? "agent runtime MCP config" : "local credential files"
|
|
@@ -4184,7 +4557,7 @@ async function executeConnect(options, deps, trace) {
|
|
|
4184
4557
|
if (dead) throw dead;
|
|
4185
4558
|
if (isExpiredSetupChallenge(err)) {
|
|
4186
4559
|
throw new Error(
|
|
4187
|
-
"The Haven setup challenge expired while connecting. Return to Haven, start a fresh connection, and run its new
|
|
4560
|
+
"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."
|
|
4188
4561
|
);
|
|
4189
4562
|
}
|
|
4190
4563
|
throw err;
|
|
@@ -4194,7 +4567,7 @@ async function executeConnect(options, deps, trace) {
|
|
|
4194
4567
|
const credentialPaths = await writeCredentials({
|
|
4195
4568
|
baseDir: options.credentialsDir,
|
|
4196
4569
|
agentId: registration.agent_id,
|
|
4197
|
-
serverName
|
|
4570
|
+
serverName,
|
|
4198
4571
|
apiKey: localApiKey,
|
|
4199
4572
|
delegateKey: localKey.privateKey,
|
|
4200
4573
|
delegateAddress: localKey.address,
|
|
@@ -4233,7 +4606,7 @@ async function executeConnect(options, deps, trace) {
|
|
|
4233
4606
|
ackSigner: options.ackSigner,
|
|
4234
4607
|
ackLocalTools: options.ackLocalTools,
|
|
4235
4608
|
localMcp: options.localMcp,
|
|
4236
|
-
serverName
|
|
4609
|
+
serverName
|
|
4237
4610
|
}, {
|
|
4238
4611
|
onProgress: log,
|
|
4239
4612
|
// #1543: report "runtime configured" the moment the config write settles,
|
|
@@ -4260,7 +4633,7 @@ async function executeConnect(options, deps, trace) {
|
|
|
4260
4633
|
environmentLabel: options.environmentLabel ?? "Local workspace"
|
|
4261
4634
|
});
|
|
4262
4635
|
if (!early.errorCode || early.errorCode === "manual_runtime_setup_required") {
|
|
4263
|
-
log(
|
|
4636
|
+
log(approveBudgetCta(setup, registration.approval_url));
|
|
4264
4637
|
}
|
|
4265
4638
|
} catch {
|
|
4266
4639
|
}
|
|
@@ -4272,10 +4645,45 @@ async function executeConnect(options, deps, trace) {
|
|
|
4272
4645
|
} else {
|
|
4273
4646
|
log("Haven setup on this machine is complete.");
|
|
4274
4647
|
}
|
|
4648
|
+
let supersededAgentsRetiredLocally;
|
|
4649
|
+
const retiredAgentIds = [];
|
|
4650
|
+
if (replacing) {
|
|
4651
|
+
if (runtimeInstall.errorCode) {
|
|
4652
|
+
supersededAgentsRetiredLocally = false;
|
|
4653
|
+
log(
|
|
4654
|
+
`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>.`
|
|
4655
|
+
);
|
|
4656
|
+
} else {
|
|
4657
|
+
supersededAgentsRetiredLocally = true;
|
|
4658
|
+
for (const entry of replacing.superseded) {
|
|
4659
|
+
try {
|
|
4660
|
+
await tombstoneDirectoryIfAbsent({
|
|
4661
|
+
directory: entry.directory,
|
|
4662
|
+
agentId: entry.agentId,
|
|
4663
|
+
reason: "replaced by a new setup (--replace)",
|
|
4664
|
+
replacedBy: registration.agent_id
|
|
4665
|
+
});
|
|
4666
|
+
await teardownLocalKeyMaterial(entry.directory, await readIdentityFile(entry.directory));
|
|
4667
|
+
retiredAgentIds.push(entry.agentId);
|
|
4668
|
+
log(`Retired previous agent ${entry.agentId} locally: tombstoned, local key files removed.`);
|
|
4669
|
+
} catch (err) {
|
|
4670
|
+
supersededAgentsRetiredLocally = false;
|
|
4671
|
+
log(`Could not retire previous agent ${entry.agentId} locally: ${err instanceof Error ? err.message : String(err)}`);
|
|
4672
|
+
}
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
}
|
|
4676
|
+
let supersededScan = null;
|
|
4275
4677
|
let supersededAgentIds = [];
|
|
4276
4678
|
try {
|
|
4277
|
-
|
|
4278
|
-
|
|
4679
|
+
supersededScan = await listOtherAgentIds(options.credentialsDir, credentialPaths.directory);
|
|
4680
|
+
supersededAgentIds = supersededScan ?? [];
|
|
4681
|
+
if (supersededAgentsRetiredLocally === true) {
|
|
4682
|
+
log("");
|
|
4683
|
+
log(
|
|
4684
|
+
`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.`
|
|
4685
|
+
);
|
|
4686
|
+
} else if (supersededAgentIds.length > 0) {
|
|
4279
4687
|
log("");
|
|
4280
4688
|
log(
|
|
4281
4689
|
`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.`
|
|
@@ -4303,23 +4711,31 @@ async function executeConnect(options, deps, trace) {
|
|
|
4303
4711
|
restartRequired: runtimeInstall.restartRequired,
|
|
4304
4712
|
nextUserAction: runtimeInstall.nextUserAction,
|
|
4305
4713
|
errorCode: runtimeInstall.errorCode,
|
|
4306
|
-
environmentLabel: options.environmentLabel ?? "Local workspace"
|
|
4714
|
+
environmentLabel: options.environmentLabel ?? "Local workspace",
|
|
4715
|
+
// The raw scan result, `null` and all — NOT the flattened
|
|
4716
|
+
// `supersededAgentIds` the outcome carries. Flattening here would hand
|
|
4717
|
+
// the dashboard the same ambiguity this field exists to remove (#2561).
|
|
4718
|
+
supersededAgentIds: supersededScan
|
|
4307
4719
|
});
|
|
4308
4720
|
} catch (err) {
|
|
4309
4721
|
log(`Could not report install status to Haven: ${err instanceof Error ? err.message : String(err)}`);
|
|
4310
4722
|
}
|
|
4311
4723
|
let approval;
|
|
4312
|
-
|
|
4724
|
+
const narrateApprovalWait = options.waitForApproval === true || options.waitForApproval === void 0 && (deps.isStdoutTty ?? Boolean(process.stdout.isTTY));
|
|
4725
|
+
if (narrateApprovalWait && !runtimeInstall.errorCode) {
|
|
4313
4726
|
approval = await waitForBudgetApproval(api, registration.setup_id, localApiKey, log, options.approvalWait);
|
|
4314
4727
|
}
|
|
4315
|
-
printNextSteps(runtimeInstall, log, approval);
|
|
4728
|
+
printNextSteps(runtimeInstall, log, approval, registration.approval_url);
|
|
4316
4729
|
const outcome = completionOutcome({
|
|
4317
4730
|
runtimeInstall,
|
|
4318
4731
|
delegateAddress: registration.delegate_address,
|
|
4319
4732
|
hostedMcpUrl: registration.hosted_mcp_url,
|
|
4320
4733
|
supersededAgentIds,
|
|
4734
|
+
supersededAgentsRetiredLocally,
|
|
4735
|
+
...replacing ? { retiredAgentIds } : {},
|
|
4321
4736
|
setupChallengeExpiresAt: setup.challenge.expires_at,
|
|
4322
|
-
approvalRequired: registration.agent_status === "pending_approval"
|
|
4737
|
+
approvalRequired: registration.agent_status === "pending_approval",
|
|
4738
|
+
approvalUrl: registration.approval_url
|
|
4323
4739
|
});
|
|
4324
4740
|
if (await recordConnectOutcome(deps, credentialPaths.directory, outcome)) {
|
|
4325
4741
|
log(`Saved this run's outcome to ${CONNECT_OUTCOME_FILENAME} in the agent's credential directory.`);
|
|
@@ -4352,7 +4768,16 @@ function completionOutcome(input) {
|
|
|
4352
4768
|
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
|
|
4353
4769
|
},
|
|
4354
4770
|
next_action: nextAction2,
|
|
4355
|
-
approval: {
|
|
4771
|
+
approval: {
|
|
4772
|
+
required: input.approvalRequired,
|
|
4773
|
+
expires_at: null,
|
|
4774
|
+
// Only when there is something to approve AND the backend supplied a
|
|
4775
|
+
// link. Never synthesised here: the connector does not know the
|
|
4776
|
+
// dashboard's origin, and a guessed URL is worse than none — it is the
|
|
4777
|
+
// "do not invent one" rule the agent runbook states, applied to the
|
|
4778
|
+
// tool rather than the agent.
|
|
4779
|
+
...input.approvalRequired && input.approvalUrl ? { url: input.approvalUrl } : {}
|
|
4780
|
+
},
|
|
4356
4781
|
verification: {
|
|
4357
4782
|
tools: ["haven_get_agent", "haven_get_allowances"],
|
|
4358
4783
|
instruction: runtimeVerificationInstruction(runtimeInstall.runtime)
|
|
@@ -4370,6 +4795,8 @@ function completionOutcome(input) {
|
|
|
4370
4795
|
// agents" is a fact a caller needs, and an omitted key would be
|
|
4371
4796
|
// indistinguishable from an older connector that never reported it.
|
|
4372
4797
|
superseded_agent_ids: input.supersededAgentIds ?? [],
|
|
4798
|
+
...input.supersededAgentsRetiredLocally !== void 0 ? { superseded_agents_retired_locally: input.supersededAgentsRetiredLocally } : {},
|
|
4799
|
+
...input.retiredAgentIds ? { retired_agent_ids: input.retiredAgentIds } : {},
|
|
4373
4800
|
...input.setupChallengeExpiresAt ? { setup_challenge_expires_at: input.setupChallengeExpiresAt } : {},
|
|
4374
4801
|
...runtimeInstall.errorCode ? { error: { code: runtimeInstall.errorCode, next_action: nextAction2 } } : {}
|
|
4375
4802
|
};
|
|
@@ -4391,10 +4818,38 @@ function installedClientProse(hint) {
|
|
|
4391
4818
|
return `Haven can see these agent clients installed here, likeliest first: ${found.join(", ")}.${suggestion} `;
|
|
4392
4819
|
}
|
|
4393
4820
|
function runtimeSelectionPrompt(options, deps) {
|
|
4394
|
-
if (options
|
|
4395
|
-
if (!(deps.isTty ?? Boolean(process.stdin.isTTY))) return void 0;
|
|
4821
|
+
if (!interactivePromptAllowed(options, deps)) return void 0;
|
|
4396
4822
|
return deps.promptRuntime ?? (() => resolveRuntimeByInstalledClientPrompt());
|
|
4397
4823
|
}
|
|
4824
|
+
function interactivePromptAllowed(options, deps) {
|
|
4825
|
+
if (options.interactive !== true) return false;
|
|
4826
|
+
return deps.isTty ?? Boolean(process.stdin.isTTY);
|
|
4827
|
+
}
|
|
4828
|
+
function supersededIds(collision) {
|
|
4829
|
+
return collision.superseded.map((entry) => entry.agentId).join(", ");
|
|
4830
|
+
}
|
|
4831
|
+
async function resolveWiringCollision(collision, agentName, options, deps) {
|
|
4832
|
+
if (options.replaceExistingWiring) return { action: "replace" };
|
|
4833
|
+
if (interactivePromptAllowed(options, deps)) {
|
|
4834
|
+
const prompt = deps.promptWiringCollision ?? ((c, name) => promptWiringCollisionResolution(c, name, defaultPromptIo()));
|
|
4835
|
+
const resolution = await prompt(collision, agentName);
|
|
4836
|
+
if (resolution.action === "abort") {
|
|
4837
|
+
throw new ConnectError(
|
|
4838
|
+
"wiring_collision_declined",
|
|
4839
|
+
`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.`,
|
|
4840
|
+
"rerun_connect_with_replace_or_name",
|
|
4841
|
+
{ supersededAgentIds: collision.superseded.map((e) => e.agentId), suggestedServerName: collision.suggestedServerName }
|
|
4842
|
+
);
|
|
4843
|
+
}
|
|
4844
|
+
return resolution;
|
|
4845
|
+
}
|
|
4846
|
+
throw new ConnectError(
|
|
4847
|
+
"wiring_collision",
|
|
4848
|
+
`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.`,
|
|
4849
|
+
"relay_wiring_collision_to_user",
|
|
4850
|
+
{ supersededAgentIds: collision.superseded.map((e) => e.agentId), suggestedServerName: collision.suggestedServerName }
|
|
4851
|
+
);
|
|
4852
|
+
}
|
|
4398
4853
|
function failedConnectOutcome(runtimeHint, error) {
|
|
4399
4854
|
const message = error instanceof Error ? error.message : "";
|
|
4400
4855
|
const code = error instanceof ConnectError ? error.code : /Node\.js >=/i.test(message) ? "unsupported_node_version" : /setup challenge.*expired|expired or invalid/i.test(message) ? "setup_challenge_expired_or_invalid" : /only available for Claude Code and Codex/i.test(message) ? "local_mcp_unsupported_runtime" : "connect_failed";
|
|
@@ -4425,7 +4880,9 @@ function failedConnectOutcome(runtimeHint, error) {
|
|
|
4425
4880
|
...error instanceof ConnectError && message ? { message: redactForAutomation(message) } : {},
|
|
4426
4881
|
...error instanceof ConnectError && error.details.allowedRuntimes ? { allowed_runtimes: error.details.allowedRuntimes } : {},
|
|
4427
4882
|
...error instanceof ConnectError && error.details.installedClients?.length ? { installed_clients: error.details.installedClients } : {},
|
|
4428
|
-
...error instanceof ConnectError && error.details.suggestedRuntime ? { suggested_runtime: error.details.suggestedRuntime } : {}
|
|
4883
|
+
...error instanceof ConnectError && error.details.suggestedRuntime ? { suggested_runtime: error.details.suggestedRuntime } : {},
|
|
4884
|
+
...error instanceof ConnectError && error.details.supersededAgentIds ? { superseded_agent_ids: error.details.supersededAgentIds } : {},
|
|
4885
|
+
...error instanceof ConnectError && error.details.suggestedServerName ? { suggested_name: error.details.suggestedServerName } : {}
|
|
4429
4886
|
}
|
|
4430
4887
|
};
|
|
4431
4888
|
}
|
|
@@ -4441,6 +4898,22 @@ function printSetupSummary(setup, log) {
|
|
|
4441
4898
|
}
|
|
4442
4899
|
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.`);
|
|
4443
4900
|
}
|
|
4901
|
+
function approveBudgetCta(setup, approvalUrl) {
|
|
4902
|
+
const where = approvalUrl ? `at ${approvalUrl}` : "in the Haven dashboard";
|
|
4903
|
+
const budgetPhrase = setup.agent_budget.length > 0 ? setup.agent_budget.map((budget) => `up to ${describeSetupBudget(budget)}`).join(", ") : void 0;
|
|
4904
|
+
if (budgetPhrase) {
|
|
4905
|
+
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.`;
|
|
4906
|
+
}
|
|
4907
|
+
return `\u2192 Action needed: approve this agent's budget ${where} \u2014 the approval button is live now. Setup continues here in the meantime.`;
|
|
4908
|
+
}
|
|
4909
|
+
function describeSetupBudget(budget) {
|
|
4910
|
+
return describeApprovedBudget({
|
|
4911
|
+
token_symbol: budget.token_symbol,
|
|
4912
|
+
token_address: budget.token_address,
|
|
4913
|
+
amount: budget.allowance_amount,
|
|
4914
|
+
reset_period_min: budget.reset_period_min
|
|
4915
|
+
});
|
|
4916
|
+
}
|
|
4444
4917
|
function assertSetupChallengeIsUsable(expiresAt) {
|
|
4445
4918
|
const expiresAtMs = Date.parse(expiresAt);
|
|
4446
4919
|
if (!Number.isNaN(expiresAtMs) && expiresAtMs > Date.now()) return;
|
|
@@ -4452,7 +4925,7 @@ function deadSetupTokenError(err) {
|
|
|
4452
4925
|
if (!(err instanceof ConnectRequestError) || err.status !== 410 && err.status !== 401) return null;
|
|
4453
4926
|
return new ConnectError(
|
|
4454
4927
|
"setup_challenge_expired_or_invalid",
|
|
4455
|
-
"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
|
|
4928
|
+
"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.",
|
|
4456
4929
|
"return_to_haven_for_fresh_setup"
|
|
4457
4930
|
);
|
|
4458
4931
|
}
|
|
@@ -4553,18 +5026,19 @@ async function waitForBudgetApproval(api, setupId, apiKey, log, options = {}) {
|
|
|
4553
5026
|
);
|
|
4554
5027
|
return "pending";
|
|
4555
5028
|
}
|
|
4556
|
-
function completionHandoffLines(result, approval) {
|
|
5029
|
+
function completionHandoffLines(result, approval, approvalUrl) {
|
|
5030
|
+
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.";
|
|
4557
5031
|
if (result.errorCode === "manual_runtime_setup_required") {
|
|
4558
5032
|
return [
|
|
4559
5033
|
"Next steps:",
|
|
4560
|
-
|
|
5034
|
+
`1. ${approveStep}`,
|
|
4561
5035
|
"2. Finish the manual MCP setup using the secret-free file references printed above, then start a fresh session in your runtime.",
|
|
4562
5036
|
`3. ${runtimeVerificationInstruction(result.runtime)}`
|
|
4563
5037
|
];
|
|
4564
5038
|
}
|
|
4565
5039
|
if (result.errorCode) {
|
|
4566
5040
|
return [
|
|
4567
|
-
"Recovery: runtime setup is not complete. Resolve the reported problem, then return to Haven for a fresh connection and run its new
|
|
5041
|
+
"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."
|
|
4568
5042
|
];
|
|
4569
5043
|
}
|
|
4570
5044
|
if (approval === "ended") {
|
|
@@ -4583,7 +5057,7 @@ function completionHandoffLines(result, approval) {
|
|
|
4583
5057
|
}
|
|
4584
5058
|
return [
|
|
4585
5059
|
"Next steps:",
|
|
4586
|
-
|
|
5060
|
+
`1. ${approveStep}`,
|
|
4587
5061
|
`2. ${activation}`,
|
|
4588
5062
|
`3. ${runtimeVerificationInstruction(result.runtime)}`
|
|
4589
5063
|
];
|
|
@@ -4597,14 +5071,14 @@ function activationInstructionWithWhy(profile) {
|
|
|
4597
5071
|
}
|
|
4598
5072
|
return profile.activationInstruction;
|
|
4599
5073
|
}
|
|
4600
|
-
var RERUN_HINT =
|
|
5074
|
+
var RERUN_HINT = connectorRerunCommand();
|
|
4601
5075
|
async function listOtherAgentIds(baseDir, currentDirectory) {
|
|
4602
5076
|
const root = defaultCredentialRoot(baseDir);
|
|
4603
5077
|
let entries = [];
|
|
4604
5078
|
try {
|
|
4605
5079
|
entries = await readdir(root);
|
|
4606
5080
|
} catch {
|
|
4607
|
-
return
|
|
5081
|
+
return null;
|
|
4608
5082
|
}
|
|
4609
5083
|
const ids = [];
|
|
4610
5084
|
for (const entry of entries) {
|
|
@@ -4624,8 +5098,8 @@ async function listOtherAgentIds(baseDir, currentDirectory) {
|
|
|
4624
5098
|
}
|
|
4625
5099
|
return ids;
|
|
4626
5100
|
}
|
|
4627
|
-
function printNextSteps(result, log, approval) {
|
|
4628
|
-
for (const line of completionHandoffLines(result, approval)) log(line);
|
|
5101
|
+
function printNextSteps(result, log, approval, approvalUrl) {
|
|
5102
|
+
for (const line of completionHandoffLines(result, approval, approvalUrl)) log(line);
|
|
4629
5103
|
}
|
|
4630
5104
|
|
|
4631
5105
|
// src/args.ts
|
|
@@ -4647,6 +5121,7 @@ function parseArgs(argv, env = process.env) {
|
|
|
4647
5121
|
let tombstoneReplacedBy;
|
|
4648
5122
|
let unwire;
|
|
4649
5123
|
let unwireDir;
|
|
5124
|
+
let replace = false;
|
|
4650
5125
|
for (let i = 0; i < argv.length; i += 1) {
|
|
4651
5126
|
const arg = argv[i];
|
|
4652
5127
|
if (arg === "--help" || arg === "-h") {
|
|
@@ -4686,6 +5161,8 @@ function parseArgs(argv, env = process.env) {
|
|
|
4686
5161
|
options.runtimeForce = requireValue(argv, ++i, arg);
|
|
4687
5162
|
} else if (arg === "--credentials-dir") {
|
|
4688
5163
|
options.credentialsDir = requireValue(argv, ++i, arg);
|
|
5164
|
+
} else if (arg === "--replace") {
|
|
5165
|
+
replace = true;
|
|
4689
5166
|
} else if (arg === "--name") {
|
|
4690
5167
|
options.serverName = requireValue(argv, ++i, arg);
|
|
4691
5168
|
assertValidServerSlug(options.serverName);
|
|
@@ -4711,6 +5188,17 @@ function parseArgs(argv, env = process.env) {
|
|
|
4711
5188
|
if (help) {
|
|
4712
5189
|
return { options, help, json, doctor, repair, tombstone, rekey };
|
|
4713
5190
|
}
|
|
5191
|
+
if (replace) {
|
|
5192
|
+
if (rekeyPhase || tombstoneDir || unwire || doctor || repair) {
|
|
5193
|
+
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.");
|
|
5194
|
+
}
|
|
5195
|
+
if (options.serverName) {
|
|
5196
|
+
throw new Error(
|
|
5197
|
+
"--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."
|
|
5198
|
+
);
|
|
5199
|
+
}
|
|
5200
|
+
options.replaceExistingWiring = true;
|
|
5201
|
+
}
|
|
4714
5202
|
if (rekey) {
|
|
4715
5203
|
if (options.setupToken) {
|
|
4716
5204
|
throw new Error("--rekey replaces an existing agent's key; it does not take --setup. Drop one of them.");
|
|
@@ -4789,6 +5277,12 @@ function helpText() {
|
|
|
4789
5277
|
" --runtime-force <name> Escape hatch: use exactly this runtime, ignoring environment detection.",
|
|
4790
5278
|
" --credentials-dir <path> Credential directory fallback. Defaults to ~/.haven/agents.",
|
|
4791
5279
|
" --environment-label <text> Non-sensitive label shown in Haven setup review.",
|
|
5280
|
+
" --replace When this machine is already wired to a different Haven agent on the bare",
|
|
5281
|
+
" haven / haven-signer pair, re-point that pair at the new agent and retire the",
|
|
5282
|
+
" previous agent directory locally (tombstoned, local key files removed).",
|
|
5283
|
+
" Nothing is revoked \u2014 revoke the old agent on the Haven agent page. Without",
|
|
5284
|
+
" this flag a non-interactive run REFUSES such a collision (wiring_collision)",
|
|
5285
|
+
" and an interactive terminal is asked; --name installs alongside instead.",
|
|
4792
5286
|
" --name <slug> Wiring slug for a NAMED agent: writes haven-<slug> / haven-signer-<slug>",
|
|
4793
5287
|
" MCP entries and stores credentials at ~/.haven/agents/<slug>/, so several",
|
|
4794
5288
|
" agents can run side by side in one runtime. 1-32 lowercase letters, digits,",
|
|
@@ -4895,13 +5389,13 @@ async function runCli(argv, io = {
|
|
|
4895
5389
|
}
|
|
4896
5390
|
if (parsed.tombstone) {
|
|
4897
5391
|
const { writeAgentTombstone: writeAgentTombstone2 } = await Promise.resolve().then(() => (init_tombstone(), tombstone_exports));
|
|
4898
|
-
const { readFile:
|
|
4899
|
-
const { join:
|
|
5392
|
+
const { readFile: readFile14 } = await import('fs/promises');
|
|
5393
|
+
const { join: join12 } = await import('path');
|
|
4900
5394
|
try {
|
|
4901
5395
|
let agentId = "unknown";
|
|
4902
5396
|
try {
|
|
4903
5397
|
const identity = JSON.parse(
|
|
4904
|
-
await
|
|
5398
|
+
await readFile14(join12(parsed.tombstone.directory, "identity.json"), "utf8")
|
|
4905
5399
|
);
|
|
4906
5400
|
agentId = identity.agent_id ?? "unknown";
|
|
4907
5401
|
} catch {
|
|
@@ -4940,10 +5434,10 @@ async function runCli(argv, io = {
|
|
|
4940
5434
|
if (parsed.unwire) {
|
|
4941
5435
|
const { unwireAgent: unwireAgent2 } = await Promise.resolve().then(() => (init_unwire(), unwire_exports));
|
|
4942
5436
|
const { homedir: homedir10 } = await import('os');
|
|
4943
|
-
const { join:
|
|
5437
|
+
const { join: join12 } = await import('path');
|
|
4944
5438
|
const homeDir = homedir10();
|
|
4945
|
-
const root = parsed.options.credentialsDir ??
|
|
4946
|
-
const directory = parsed.unwireDir ?? (parsed.options.serverName ?
|
|
5439
|
+
const root = parsed.options.credentialsDir ?? join12(homeDir, ".haven", "agents");
|
|
5440
|
+
const directory = parsed.unwireDir ?? (parsed.options.serverName ? join12(root, parsed.options.serverName) : root);
|
|
4947
5441
|
try {
|
|
4948
5442
|
const result = await unwireAgent2({
|
|
4949
5443
|
directory,
|
|
@@ -5110,12 +5604,23 @@ async function runCli(argv, io = {
|
|
|
5110
5604
|
const result = await runConnect(
|
|
5111
5605
|
{
|
|
5112
5606
|
...parsed.options,
|
|
5113
|
-
|
|
5607
|
+
// #1377 D / #2484: leave prose runs UNSPECIFIED (undefined) so
|
|
5608
|
+
// runConnect's stdout-TTY narration gate decides whether there is a
|
|
5609
|
+
// watching human to narrate to — an agent invoking prose as a tool
|
|
5610
|
+
// call has none and must not sit opaque in the wait. --json stays an
|
|
5611
|
+
// explicit false (skip, emit promptly).
|
|
5612
|
+
waitForApproval: parsed.json ? false : void 0,
|
|
5114
5613
|
// #1719: only a human-facing run may be asked which installed client to
|
|
5115
5614
|
// configure. --json is the automation contract — it must fail with a
|
|
5116
5615
|
// machine-readable code, never block on stdin. runConnect additionally
|
|
5117
5616
|
// requires a real TTY before it prompts.
|
|
5118
|
-
interactive: !parsed.json
|
|
5617
|
+
interactive: !parsed.json,
|
|
5618
|
+
// #2528: reported to the backend at register, so the funnel can tell a
|
|
5619
|
+
// machine-readable run from a narrated one. Read from the SAME
|
|
5620
|
+
// `parsed.json` the three flags above use, rather than inferred later
|
|
5621
|
+
// from `waitForApproval` — that flag is already false for a prose run
|
|
5622
|
+
// with no TTY (#2484), so inferring would mislabel real prose runs.
|
|
5623
|
+
runMode: parsed.json ? "json" : "prose"
|
|
5119
5624
|
},
|
|
5120
5625
|
{
|
|
5121
5626
|
log: (message) => (parsed.json ? io.stderr : io.stdout)(`${message}
|